commit
4802671152
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vben/web-antd",
|
||||
"version": "5.5.6",
|
||||
"version": "5.5.7",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
|
|
@ -12,16 +12,14 @@ export namespace CrmCustomerLimitConfigApi {
|
|||
maxCount?: number;
|
||||
dealCountEnabled?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户限制配置类型
|
||||
*/
|
||||
export enum LimitConfType {
|
||||
/** 锁定客户数限制 */
|
||||
CUSTOMER_LOCK_LIMIT = 2,
|
||||
/** 拥有客户数限制 */
|
||||
CUSTOMER_QUANTITY_LIMIT = 1,
|
||||
}
|
||||
/** 客户限制配置类型 */
|
||||
export enum LimitConfType {
|
||||
/** 锁定客户数限制 */
|
||||
CUSTOMER_LOCK_LIMIT = 2,
|
||||
/** 拥有客户数限制 */
|
||||
CUSTOMER_QUANTITY_LIMIT = 1,
|
||||
}
|
||||
|
||||
/** 查询客户限制配置列表 */
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
import type { PageResult } from '@vben/request';
|
||||
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace CrmOperateLogApi {
|
||||
/** 操作日志查询参数 */
|
||||
export interface OperateLogQuery extends PageParam {
|
||||
export interface OperateLogQuery {
|
||||
bizType: number;
|
||||
bizId: number;
|
||||
}
|
||||
|
@ -24,7 +26,7 @@ export namespace CrmOperateLogApi {
|
|||
|
||||
/** 获得操作日志 */
|
||||
export function getOperateLogPage(params: CrmOperateLogApi.OperateLogQuery) {
|
||||
return requestClient.get<PageResult<CrmOperateLogApi.OperateLog>>(
|
||||
return requestClient.get<PageResult<SystemOperateLogApi.OperateLog>>(
|
||||
'/crm/operate-log/page',
|
||||
{ params },
|
||||
);
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace CrmPermissionApi {
|
||||
/** 数据权限信息 */
|
||||
export interface Permission {
|
||||
id?: number; // 数据权限编号
|
||||
userId: number; // 用户编号
|
||||
ids?: number[];
|
||||
userId?: number; // 用户编号
|
||||
bizType: number; // Crm 类型
|
||||
bizId: number; // Crm 类型数据编号
|
||||
level: number; // 权限级别
|
||||
|
@ -15,7 +14,6 @@ export namespace CrmPermissionApi {
|
|||
nickname?: string; // 用户昵称
|
||||
postNames?: string[]; // 岗位名称数组
|
||||
createTime?: Date;
|
||||
ids?: number[];
|
||||
}
|
||||
|
||||
/** 数据权限转移请求 */
|
||||
|
@ -26,33 +24,38 @@ export namespace CrmPermissionApi {
|
|||
toBizTypes?: number[]; // 转移客户时,需要额外有【联系人】【商机】【合同】的 checkbox 选择
|
||||
}
|
||||
|
||||
/**
|
||||
* CRM 业务类型枚举
|
||||
*/
|
||||
export enum BizType {
|
||||
CRM_BUSINESS = 4, // 商机
|
||||
CRM_CLUE = 1, // 线索
|
||||
CRM_CONTACT = 3, // 联系人
|
||||
CRM_CONTRACT = 5, // 合同
|
||||
CRM_CUSTOMER = 2, // 客户
|
||||
CRM_PRODUCT = 6, // 产品
|
||||
CRM_RECEIVABLE = 7, // 回款
|
||||
CRM_RECEIVABLE_PLAN = 8, // 回款计划
|
||||
}
|
||||
|
||||
/**
|
||||
* CRM 数据权限级别枚举
|
||||
*/
|
||||
export enum PermissionLevel {
|
||||
OWNER = 1, // 负责人
|
||||
READ = 2, // 只读
|
||||
WRITE = 3, // 读写
|
||||
export interface PermissionListReq {
|
||||
bizId: number; // 模块数据编号
|
||||
bizType: number; // 模块类型
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CRM 业务类型枚举
|
||||
*/
|
||||
export enum BizTypeEnum {
|
||||
CRM_BUSINESS = 4, // 商机
|
||||
CRM_CLUE = 1, // 线索
|
||||
CRM_CONTACT = 3, // 联系人
|
||||
CRM_CONTRACT = 5, // 合同
|
||||
CRM_CUSTOMER = 2, // 客户
|
||||
CRM_PRODUCT = 6, // 产品
|
||||
CRM_RECEIVABLE = 7, // 回款
|
||||
CRM_RECEIVABLE_PLAN = 8, // 回款计划
|
||||
}
|
||||
|
||||
/**
|
||||
* CRM 数据权限级别枚举
|
||||
*/
|
||||
export enum PermissionLevelEnum {
|
||||
OWNER = 1, // 负责人
|
||||
READ = 2, // 只读
|
||||
WRITE = 3, // 读写
|
||||
}
|
||||
|
||||
/** 获得数据权限列表(查询团队成员列表) */
|
||||
export function getPermissionList(params: PageParam) {
|
||||
return requestClient.get<PageResult<CrmPermissionApi.Permission>>(
|
||||
export function getPermissionList(params: CrmPermissionApi.PermissionListReq) {
|
||||
return requestClient.get<CrmPermissionApi.Permission[]>(
|
||||
'/crm/permission/list',
|
||||
{ params },
|
||||
);
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import type { PageParam } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace CrmProductCategoryApi {
|
||||
|
@ -38,7 +36,7 @@ export function deleteProductCategory(id: number) {
|
|||
}
|
||||
|
||||
/** 产品分类列表 */
|
||||
export function getProductCategoryList(params?: PageParam) {
|
||||
export function getProductCategoryList(params?: any) {
|
||||
return requestClient.get<CrmProductCategoryApi.ProductCategory[]>(
|
||||
'/crm/product-category/list',
|
||||
{ params },
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace MpAccountApi {
|
||||
/** 公众号账号信息 */
|
||||
export interface Account {
|
||||
id?: number;
|
||||
name: string;
|
||||
account: string;
|
||||
appId: string;
|
||||
appSecret: string;
|
||||
token: string;
|
||||
aesKey?: string;
|
||||
qrCodeUrl?: string;
|
||||
remark?: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
|
||||
export interface AccountSimple {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询公众号账号列表 */
|
||||
export function getAccountPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<MpAccountApi.Account>>(
|
||||
'/mp/account/page',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询公众号账号详情 */
|
||||
export function getAccount(id: number) {
|
||||
return requestClient.get<MpAccountApi.Account>(`/mp/account/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 查询公众号账号列表 */
|
||||
export function getSimpleAccountList() {
|
||||
return requestClient.get<MpAccountApi.AccountSimple[]>(
|
||||
'/mp/account/list-all-simple',
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增公众号账号 */
|
||||
export function createAccount(data: MpAccountApi.Account) {
|
||||
return requestClient.post('/mp/account/create', data);
|
||||
}
|
||||
|
||||
/** 修改公众号账号 */
|
||||
export function updateAccount(data: MpAccountApi.Account) {
|
||||
return requestClient.put('/mp/account/update', data);
|
||||
}
|
||||
|
||||
/** 删除公众号账号 */
|
||||
export function deleteAccount(id: number) {
|
||||
return requestClient.delete(`/mp/account/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 生成公众号账号二维码 */
|
||||
export function generateAccountQrCode(id: number) {
|
||||
return requestClient.post(`/mp/account/generate-qr-code?id=${id}`);
|
||||
}
|
||||
|
||||
/** 清空公众号账号 API 配额 */
|
||||
export function clearAccountQuota(id: number) {
|
||||
return requestClient.post(`/mp/account/clear-quota?id=${id}`);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace MpAutoReplyApi {
|
||||
/** 自动回复信息 */
|
||||
export interface AutoReply {
|
||||
id?: number;
|
||||
accountId: number;
|
||||
type: number;
|
||||
keyword: string;
|
||||
content: string;
|
||||
status: number;
|
||||
remark?: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询自动回复列表 */
|
||||
export function getAutoReplyPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<MpAutoReplyApi.AutoReply>>(
|
||||
'/mp/auto-reply/page',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询自动回复详情 */
|
||||
export function getAutoReply(id: number) {
|
||||
return requestClient.get<MpAutoReplyApi.AutoReply>(
|
||||
`/mp/auto-reply/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增自动回复 */
|
||||
export function createAutoReply(data: MpAutoReplyApi.AutoReply) {
|
||||
return requestClient.post('/mp/auto-reply/create', data);
|
||||
}
|
||||
|
||||
/** 修改自动回复 */
|
||||
export function updateAutoReply(data: MpAutoReplyApi.AutoReply) {
|
||||
return requestClient.put('/mp/auto-reply/update', data);
|
||||
}
|
||||
|
||||
/** 删除自动回复 */
|
||||
export function deleteAutoReply(id: number) {
|
||||
return requestClient.delete(`/mp/auto-reply/delete?id=${id}`);
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace MpDraftApi {
|
||||
/** 草稿文章信息 */
|
||||
export interface Article {
|
||||
title: string;
|
||||
author: string;
|
||||
digest: string;
|
||||
content: string;
|
||||
contentSourceUrl: string;
|
||||
thumbMediaId: string;
|
||||
showCoverPic: number;
|
||||
needOpenComment: number;
|
||||
onlyFansCanComment: number;
|
||||
}
|
||||
|
||||
/** 草稿信息 */
|
||||
export interface Draft {
|
||||
id?: number;
|
||||
accountId: number;
|
||||
mediaId: string;
|
||||
articles: Article[];
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询草稿列表 */
|
||||
export function getDraftPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<MpDraftApi.Draft>>('/mp/draft/page', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 创建草稿 */
|
||||
export function createDraft(accountId: number, articles: MpDraftApi.Article[]) {
|
||||
return requestClient.post('/mp/draft/create', articles, {
|
||||
params: { accountId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新草稿 */
|
||||
export function updateDraft(
|
||||
accountId: number,
|
||||
mediaId: string,
|
||||
articles: MpDraftApi.Article[],
|
||||
) {
|
||||
return requestClient.put('/mp/draft/update', articles, {
|
||||
params: { accountId, mediaId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除草稿 */
|
||||
export function deleteDraft(accountId: number, mediaId: string) {
|
||||
return requestClient.delete('/mp/draft/delete', {
|
||||
params: { accountId, mediaId },
|
||||
});
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace MpFreePublishApi {
|
||||
/** 自由发布文章信息 */
|
||||
export interface FreePublish {
|
||||
id?: number;
|
||||
accountId: number;
|
||||
mediaId: string;
|
||||
articleId: string;
|
||||
title: string;
|
||||
author: string;
|
||||
digest: string;
|
||||
content: string;
|
||||
thumbUrl: string;
|
||||
status: number;
|
||||
publishTime?: Date;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询自由发布文章列表 */
|
||||
export function getFreePublishPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<MpFreePublishApi.FreePublish>>(
|
||||
'/mp/free-publish/page',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 删除自由发布文章 */
|
||||
export function deleteFreePublish(accountId: number, articleId: string) {
|
||||
return requestClient.delete('/mp/free-publish/delete', {
|
||||
params: { accountId, articleId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 发布自由发布文章 */
|
||||
export function submitFreePublish(accountId: number, mediaId: string) {
|
||||
return requestClient.post('/mp/free-publish/submit', null, {
|
||||
params: { accountId, mediaId },
|
||||
});
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/** 素材类型枚举 */
|
||||
export enum MaterialType {
|
||||
IMAGE = 1, // 图片
|
||||
THUMB = 4, // 缩略图
|
||||
VIDEO = 3, // 视频
|
||||
VOICE = 2, // 语音
|
||||
}
|
||||
|
||||
export namespace MpMaterialApi {
|
||||
/** 素材信息 */
|
||||
export interface Material {
|
||||
id?: number;
|
||||
accountId: number;
|
||||
type: MaterialType;
|
||||
mediaId: string;
|
||||
url: string;
|
||||
name: string;
|
||||
size: number;
|
||||
remark?: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询素材列表 */
|
||||
export function getMaterialPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<MpMaterialApi.Material>>(
|
||||
'/mp/material/page',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 删除永久素材 */
|
||||
export function deletePermanentMaterial(id: number) {
|
||||
return requestClient.delete('/mp/material/delete-permanent', {
|
||||
params: { id },
|
||||
});
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import { requestClient } from '#/api/request';
|
||||
|
||||
/** 菜单类型枚举 */
|
||||
export enum MenuType {
|
||||
CLICK = 'click', // 点击推事件
|
||||
LOCATION_SELECT = 'location_select', // 发送位置
|
||||
MEDIA_ID = 'media_id', // 下发消息
|
||||
MINIPROGRAM = 'miniprogram', // 小程序
|
||||
PIC_PHOTO_OR_ALBUM = 'pic_photo_or_album', // 拍照或者相册发图
|
||||
PIC_SYSPHOTO = 'pic_sysphoto', // 系统拍照发图
|
||||
PIC_WEIXIN = 'pic_weixin', // 微信相册发图
|
||||
SCANCODE_PUSH = 'scancode_push', // 扫码推事件
|
||||
SCANCODE_WAITMSG = 'scancode_waitmsg', // 扫码带提示
|
||||
VIEW = 'view', // 跳转URL
|
||||
VIEW_LIMITED = 'view_limited', // 跳转图文消息URL
|
||||
}
|
||||
|
||||
export namespace MpMenuApi {
|
||||
/** 菜单按钮信息 */
|
||||
export interface MenuButton {
|
||||
type: MenuType;
|
||||
name: string;
|
||||
key?: string;
|
||||
url?: string;
|
||||
mediaId?: string;
|
||||
appId?: string;
|
||||
pagePath?: string;
|
||||
subButtons?: MenuButton[];
|
||||
}
|
||||
|
||||
/** 菜单信息 */
|
||||
export interface Menu {
|
||||
accountId: number;
|
||||
menus: MenuButton[];
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询菜单列表 */
|
||||
export function getMenuList(accountId: number) {
|
||||
return requestClient.get<MpMenuApi.MenuButton[]>('/mp/menu/list', {
|
||||
params: { accountId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 保存菜单 */
|
||||
export function saveMenu(accountId: number, menus: MpMenuApi.MenuButton[]) {
|
||||
return requestClient.post('/mp/menu/save', {
|
||||
accountId,
|
||||
menus,
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除菜单 */
|
||||
export function deleteMenu(accountId: number) {
|
||||
return requestClient.delete('/mp/menu/delete', {
|
||||
params: { accountId },
|
||||
});
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/** 消息类型枚举 */
|
||||
export enum MessageType {
|
||||
IMAGE = 'image', // 图片消息
|
||||
MPNEWS = 'mpnews', // 公众号图文消息
|
||||
MUSIC = 'music', // 音乐消息
|
||||
NEWS = 'news', // 图文消息
|
||||
TEXT = 'text', // 文本消息
|
||||
VIDEO = 'video', // 视频消息
|
||||
VOICE = 'voice', // 语音消息
|
||||
WXCARD = 'wxcard', // 卡券消息
|
||||
}
|
||||
|
||||
export namespace MpMessageApi {
|
||||
/** 消息信息 */
|
||||
export interface Message {
|
||||
id?: number;
|
||||
accountId: number;
|
||||
type: MessageType;
|
||||
openid: string;
|
||||
content: string;
|
||||
mediaId?: string;
|
||||
status: number;
|
||||
remark?: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
|
||||
/** 发送消息请求 */
|
||||
export interface SendMessageRequest {
|
||||
accountId: number;
|
||||
openid: string;
|
||||
type: MessageType;
|
||||
content: string;
|
||||
mediaId?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询消息列表 */
|
||||
export function getMessagePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<MpMessageApi.Message>>(
|
||||
'/mp/message/page',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 发送消息 */
|
||||
export function sendMessage(data: MpMessageApi.SendMessageRequest) {
|
||||
return requestClient.post('/mp/message/send', data);
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import type { PageParam } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace MpStatisticsApi {
|
||||
/** 统计查询参数 */
|
||||
export interface StatisticsQuery extends PageParam {
|
||||
accountId: number;
|
||||
beginDate: string;
|
||||
endDate: string;
|
||||
}
|
||||
|
||||
/** 消息发送概况数据 */
|
||||
export interface UpstreamMessage {
|
||||
refDate: string;
|
||||
msgType: string;
|
||||
msgUser: number;
|
||||
msgCount: number;
|
||||
}
|
||||
|
||||
/** 用户增减数据 */
|
||||
export interface UserSummary {
|
||||
refDate: string;
|
||||
userSource: number;
|
||||
newUser: number;
|
||||
cancelUser: number;
|
||||
cumulateUser: number;
|
||||
}
|
||||
|
||||
/** 用户累计数据 */
|
||||
export interface UserCumulate {
|
||||
refDate: string;
|
||||
cumulateUser: number;
|
||||
}
|
||||
|
||||
/** 接口分析数据 */
|
||||
export interface InterfaceSummary {
|
||||
refDate: string;
|
||||
callbackCount: number;
|
||||
failCount: number;
|
||||
totalTimeCost: number;
|
||||
maxTimeCost: number;
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取消息发送概况数据 */
|
||||
export function getUpstreamMessage(params: MpStatisticsApi.StatisticsQuery) {
|
||||
return requestClient.get<MpStatisticsApi.UpstreamMessage[]>(
|
||||
'/mp/statistics/upstream-message',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取用户增减数据 */
|
||||
export function getUserSummary(params: MpStatisticsApi.StatisticsQuery) {
|
||||
return requestClient.get<MpStatisticsApi.UserSummary[]>(
|
||||
'/mp/statistics/user-summary',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取用户累计数据 */
|
||||
export function getUserCumulate(params: MpStatisticsApi.StatisticsQuery) {
|
||||
return requestClient.get<MpStatisticsApi.UserCumulate[]>(
|
||||
'/mp/statistics/user-cumulate',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取接口分析数据 */
|
||||
export function getInterfaceSummary(params: MpStatisticsApi.StatisticsQuery) {
|
||||
return requestClient.get<MpStatisticsApi.InterfaceSummary[]>(
|
||||
'/mp/statistics/interface-summary',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace MpTagApi {
|
||||
/** 标签信息 */
|
||||
export interface Tag {
|
||||
id?: number;
|
||||
accountId: number;
|
||||
name: string;
|
||||
count?: number;
|
||||
createTime?: Date;
|
||||
}
|
||||
|
||||
/** 标签分页查询参数 */
|
||||
export interface TagPageQuery extends PageParam {
|
||||
accountId?: number;
|
||||
name?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 创建公众号标签 */
|
||||
export function createTag(data: MpTagApi.Tag) {
|
||||
return requestClient.post('/mp/tag/create', data);
|
||||
}
|
||||
|
||||
/** 更新公众号标签 */
|
||||
export function updateTag(data: MpTagApi.Tag) {
|
||||
return requestClient.put('/mp/tag/update', data);
|
||||
}
|
||||
|
||||
/** 删除公众号标签 */
|
||||
export function deleteTag(id: number) {
|
||||
return requestClient.delete('/mp/tag/delete', {
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取公众号标签 */
|
||||
export function getTag(id: number) {
|
||||
return requestClient.get<MpTagApi.Tag>('/mp/tag/get', {
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取公众号标签分页 */
|
||||
export function getTagPage(params: MpTagApi.TagPageQuery) {
|
||||
return requestClient.get<PageResult<MpTagApi.Tag>>('/mp/tag/page', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取公众号标签精简信息列表 */
|
||||
export function getSimpleTagList() {
|
||||
return requestClient.get<MpTagApi.Tag[]>('/mp/tag/list-all-simple');
|
||||
}
|
||||
|
||||
/** 同步公众号标签 */
|
||||
export function syncTag(accountId: number) {
|
||||
return requestClient.post('/mp/tag/sync', null, {
|
||||
params: { accountId },
|
||||
});
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace MpUserApi {
|
||||
/** 用户信息 */
|
||||
export interface User {
|
||||
id?: number;
|
||||
accountId: number;
|
||||
openid: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
sex: number;
|
||||
country: string;
|
||||
province: string;
|
||||
city: string;
|
||||
language: string;
|
||||
subscribe: boolean;
|
||||
subscribeTime?: Date;
|
||||
remark?: string;
|
||||
tagIds?: number[];
|
||||
createTime?: Date;
|
||||
}
|
||||
|
||||
/** 用户分页查询参数 */
|
||||
export interface UserPageQuery extends PageParam {
|
||||
accountId?: number;
|
||||
nickname?: string;
|
||||
tagId?: number;
|
||||
}
|
||||
}
|
||||
|
||||
/** 更新公众号粉丝 */
|
||||
export function updateUser(data: MpUserApi.User) {
|
||||
return requestClient.put('/mp/user/update', data);
|
||||
}
|
||||
|
||||
/** 获取公众号粉丝 */
|
||||
export function getUser(id: number) {
|
||||
return requestClient.get<MpUserApi.User>('/mp/user/get', {
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取公众号粉丝分页 */
|
||||
export function getUserPage(params: MpUserApi.UserPageQuery) {
|
||||
return requestClient.get<PageResult<MpUserApi.User>>('/mp/user/page', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 同步公众号粉丝 */
|
||||
export function syncUser(accountId: number) {
|
||||
return requestClient.post('/mp/user/sync', null, {
|
||||
params: { accountId },
|
||||
});
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export { default as OperateLog } from './operate-log.vue';
|
||||
|
||||
export type { OperateLogProps } from './typing';
|
|
@ -0,0 +1,46 @@
|
|||
<script setup lang="ts">
|
||||
import type { OperateLogProps } from './typing';
|
||||
|
||||
import { Timeline } from 'ant-design-vue';
|
||||
|
||||
import { DICT_TYPE, getDictLabel, getDictObj } from '#/utils';
|
||||
|
||||
defineOptions({ name: 'OperateLogV2' });
|
||||
|
||||
withDefaults(defineProps<OperateLogProps>(), {
|
||||
logList: () => [],
|
||||
});
|
||||
|
||||
function getUserTypeColor(userType: number) {
|
||||
const dict = getDictObj(DICT_TYPE.USER_TYPE, userType);
|
||||
switch (dict?.colorType) {
|
||||
case 'danger': {
|
||||
return '#F56C6C';
|
||||
}
|
||||
case 'info': {
|
||||
return '#909399';
|
||||
}
|
||||
case 'success': {
|
||||
return '#67C23A';
|
||||
}
|
||||
case 'warning': {
|
||||
return '#E6A23C';
|
||||
}
|
||||
}
|
||||
return '#409EFF';
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<Timeline>
|
||||
<Timeline.Item
|
||||
v-for="log in logList"
|
||||
:key="log.id"
|
||||
:color="getUserTypeColor(log.userType)"
|
||||
>
|
||||
<p>{{ log.createTime }}</p>
|
||||
<p>{{ getDictLabel(DICT_TYPE.USER_TYPE, log.userType)[0] }}</p>
|
||||
</Timeline.Item>
|
||||
</Timeline>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
export interface OperateLogProps {
|
||||
logList: SystemOperateLogApi.OperateLog[]; // 操作日志列表
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
export { default as DeptSelectModal } from './dept-select-modal.vue';
|
||||
export { default as UserSelectModal } from './user-select-modal.vue';
|
|
@ -0,0 +1,186 @@
|
|||
<script setup lang="ts">
|
||||
import type { SimpleFlowNode } from '../../consts';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import { Input } from 'ant-design-vue';
|
||||
|
||||
import { ConditionType } from '../../consts';
|
||||
import {
|
||||
getConditionShowText,
|
||||
getDefaultConditionNodeName,
|
||||
useFormFieldsAndStartUser,
|
||||
} from '../../helpers';
|
||||
import Condition from './modules/condition.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ConditionNodeConfig',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
conditionNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
required: true,
|
||||
},
|
||||
nodeIndex: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const currentNode = ref<SimpleFlowNode>(props.conditionNode);
|
||||
const condition = ref<any>({
|
||||
conditionType: ConditionType.RULE, // 设置默认值
|
||||
conditionExpression: '',
|
||||
conditionGroups: {
|
||||
and: true,
|
||||
conditions: [
|
||||
{
|
||||
and: true,
|
||||
rules: [
|
||||
{
|
||||
opCode: '==',
|
||||
leftSide: '',
|
||||
rightSide: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// 显示名称输入框
|
||||
const showInput = ref(false);
|
||||
const conditionRef = ref();
|
||||
const fieldOptions = useFormFieldsAndStartUser(); // 流程表单字段和发起人字段
|
||||
|
||||
/** 保存配置 */
|
||||
async function saveConfig() {
|
||||
if (!currentNode.value.conditionSetting?.defaultFlow) {
|
||||
// 校验表单
|
||||
const valid = await conditionRef.value.validate();
|
||||
if (!valid) return false;
|
||||
const showText = getConditionShowText(
|
||||
condition.value?.conditionType,
|
||||
condition.value?.conditionExpression,
|
||||
condition.value.conditionGroups,
|
||||
fieldOptions,
|
||||
);
|
||||
if (!showText) {
|
||||
return false;
|
||||
}
|
||||
currentNode.value.showText = showText;
|
||||
// 使用 cloneDeep 进行深拷贝
|
||||
currentNode.value.conditionSetting = cloneDeep({
|
||||
...currentNode.value.conditionSetting,
|
||||
conditionType: condition.value?.conditionType,
|
||||
conditionExpression:
|
||||
condition.value?.conditionType === ConditionType.EXPRESSION
|
||||
? condition.value?.conditionExpression
|
||||
: undefined,
|
||||
conditionGroups:
|
||||
condition.value?.conditionType === ConditionType.RULE
|
||||
? condition.value?.conditionGroups
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
drawerApi.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
title: currentNode.value.name,
|
||||
onConfirm: saveConfig,
|
||||
});
|
||||
|
||||
function open() {
|
||||
// 使用三元表达式代替 if-else,解决 linter 警告
|
||||
condition.value = currentNode.value.conditionSetting
|
||||
? cloneDeep(currentNode.value.conditionSetting)
|
||||
: {
|
||||
conditionType: ConditionType.RULE,
|
||||
conditionExpression: '',
|
||||
conditionGroups: {
|
||||
and: true,
|
||||
conditions: [
|
||||
{
|
||||
and: true,
|
||||
rules: [
|
||||
{
|
||||
opCode: '==',
|
||||
leftSide: '',
|
||||
rightSide: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.conditionNode,
|
||||
(newValue) => {
|
||||
currentNode.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
function clickIcon() {
|
||||
showInput.value = true;
|
||||
}
|
||||
|
||||
// 输入框失去焦点
|
||||
function blurEvent() {
|
||||
showInput.value = false;
|
||||
currentNode.value.name =
|
||||
currentNode.value.name ||
|
||||
getDefaultConditionNodeName(
|
||||
props.nodeIndex,
|
||||
currentNode.value?.conditionSetting?.defaultFlow,
|
||||
);
|
||||
}
|
||||
|
||||
defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
</script>
|
||||
<template>
|
||||
<Drawer class="w-[580px]">
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<Input
|
||||
v-if="showInput"
|
||||
type="text"
|
||||
class="mr-2 w-48"
|
||||
@blur="blurEvent()"
|
||||
v-model:value="currentNode.name"
|
||||
:placeholder="currentNode.name"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex cursor-pointer items-center"
|
||||
@click="clickIcon()"
|
||||
>
|
||||
{{ currentNode.name }}
|
||||
<IconifyIcon class="ml-1" icon="ep:edit-pen" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="mb-3 text-base"
|
||||
v-if="currentNode.conditionSetting?.defaultFlow"
|
||||
>
|
||||
未满足其它条件时,将进入此分支(该分支不可编辑和删除)
|
||||
</div>
|
||||
<div v-else>
|
||||
<Condition ref="conditionRef" v-model:model-value="condition" />
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
</template>
|
|
@ -0,0 +1,533 @@
|
|||
<script setup lang="ts">
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { SimpleFlowNode } from '../../consts';
|
||||
import type { CopyTaskFormType } from '../../helpers';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Col,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
TabPane,
|
||||
Tabs,
|
||||
Textarea,
|
||||
TreeSelect,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { BpmModelFormType } from '#/utils';
|
||||
|
||||
import {
|
||||
CANDIDATE_STRATEGY,
|
||||
CandidateStrategy,
|
||||
FieldPermissionType,
|
||||
MULTI_LEVEL_DEPT,
|
||||
NodeType,
|
||||
} from '../../consts';
|
||||
import {
|
||||
useFormFieldsPermission,
|
||||
useNodeForm,
|
||||
useNodeName,
|
||||
useWatchNode,
|
||||
} from '../../helpers';
|
||||
|
||||
defineOptions({ name: 'CopyTaskNodeConfig' });
|
||||
|
||||
const props = defineProps({
|
||||
flowNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const deptLevelLabel = computed(() => {
|
||||
let label = '部门负责人来源';
|
||||
label =
|
||||
configForm.value.candidateStrategy ===
|
||||
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
|
||||
? `${label}(指定部门向上)`
|
||||
: `${label}(发起人部门向上)`;
|
||||
return label;
|
||||
});
|
||||
|
||||
// 抽屉配置
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
header: true,
|
||||
closable: true,
|
||||
title: '',
|
||||
onConfirm() {
|
||||
saveConfig();
|
||||
},
|
||||
});
|
||||
|
||||
// 当前节点
|
||||
const currentNode = useWatchNode(props);
|
||||
|
||||
// 节点名称
|
||||
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(
|
||||
NodeType.COPY_TASK_NODE,
|
||||
);
|
||||
|
||||
// 激活的 Tab 标签页
|
||||
const activeTabName = ref('user');
|
||||
|
||||
// 表单字段权限配置
|
||||
const {
|
||||
formType,
|
||||
fieldsPermissionConfig,
|
||||
formFieldOptions,
|
||||
getNodeConfigFormFields,
|
||||
} = useFormFieldsPermission(FieldPermissionType.READ);
|
||||
|
||||
// 表单内用户字段选项, 必须是必填和用户选择器
|
||||
const userFieldOnFormOptions = computed(() => {
|
||||
return formFieldOptions.filter((item) => item.type === 'UserSelect');
|
||||
});
|
||||
|
||||
// 表单内部门字段选项, 必须是必填和部门选择器
|
||||
const deptFieldOnFormOptions = computed(() => {
|
||||
return formFieldOptions.filter((item) => item.type === 'DeptSelect');
|
||||
});
|
||||
|
||||
// 抄送人表单配置
|
||||
const formRef = ref(); // 表单 Ref
|
||||
|
||||
// 表单校验规则
|
||||
const formRules: Record<string, Rule[]> = reactive({
|
||||
candidateStrategy: [
|
||||
{ required: true, message: '抄送人设置不能为空', trigger: 'change' },
|
||||
],
|
||||
userIds: [{ required: true, message: '用户不能为空', trigger: 'change' }],
|
||||
roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }],
|
||||
deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
|
||||
userGroups: [
|
||||
{ required: true, message: '用户组不能为空', trigger: 'change' },
|
||||
],
|
||||
postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
|
||||
formUser: [
|
||||
{ required: true, message: '表单内用户字段不能为空', trigger: 'change' },
|
||||
],
|
||||
formDept: [
|
||||
{ required: true, message: '表单内部门字段不能为空', trigger: 'change' },
|
||||
],
|
||||
expression: [
|
||||
{ required: true, message: '流程表达式不能为空', trigger: 'blur' },
|
||||
],
|
||||
});
|
||||
|
||||
const {
|
||||
configForm: tempConfigForm,
|
||||
roleOptions,
|
||||
postOptions,
|
||||
userOptions,
|
||||
userGroupOptions,
|
||||
deptTreeOptions,
|
||||
getShowText,
|
||||
handleCandidateParam,
|
||||
parseCandidateParam,
|
||||
} = useNodeForm(NodeType.COPY_TASK_NODE);
|
||||
|
||||
const configForm = tempConfigForm as Ref<CopyTaskFormType>;
|
||||
// 抄送人策略, 去掉发起人自选 和 发起人自己
|
||||
const copyUserStrategies = computed(() => {
|
||||
return CANDIDATE_STRATEGY.filter(
|
||||
(item) => item.value !== CandidateStrategy.START_USER,
|
||||
);
|
||||
});
|
||||
|
||||
// 改变抄送人设置策略
|
||||
function changeCandidateStrategy() {
|
||||
configForm.value.userIds = [];
|
||||
configForm.value.deptIds = [];
|
||||
configForm.value.roleIds = [];
|
||||
configForm.value.postIds = [];
|
||||
configForm.value.userGroups = [];
|
||||
configForm.value.deptLevel = 1;
|
||||
configForm.value.formUser = '';
|
||||
configForm.value.formDept = '';
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function saveConfig() {
|
||||
activeTabName.value = 'user';
|
||||
if (!formRef.value) return false;
|
||||
const valid = await formRef.value.validate();
|
||||
if (!valid) return false;
|
||||
const showText = getShowText();
|
||||
if (!showText) return false;
|
||||
currentNode.value.name = nodeName.value!;
|
||||
currentNode.value.candidateParam = handleCandidateParam();
|
||||
currentNode.value.candidateStrategy = configForm.value.candidateStrategy;
|
||||
currentNode.value.showText = showText;
|
||||
currentNode.value.fieldsPermission = fieldsPermissionConfig.value;
|
||||
drawerApi.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 显示抄送节点配置, 由父组件传过来
|
||||
function showCopyTaskNodeConfig(node: SimpleFlowNode) {
|
||||
nodeName.value = node.name;
|
||||
// 抄送人设置
|
||||
configForm.value.candidateStrategy = node.candidateStrategy!;
|
||||
parseCandidateParam(node.candidateStrategy!, node?.candidateParam);
|
||||
// 表单字段权限
|
||||
getNodeConfigFormFields(node.fieldsPermission);
|
||||
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
/** 批量更新权限 */
|
||||
function updatePermission(type: string) {
|
||||
fieldsPermissionConfig.value.forEach((field) => {
|
||||
if (type === 'READ') {
|
||||
field.permission = FieldPermissionType.READ;
|
||||
} else if (type === 'WRITE') {
|
||||
field.permission = FieldPermissionType.WRITE;
|
||||
} else {
|
||||
field.permission = FieldPermissionType.NONE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 在组件初始化时对表单字段进行处理
|
||||
onMounted(() => {
|
||||
// 可以在这里进行初始化操作
|
||||
});
|
||||
|
||||
defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
|
||||
</script>
|
||||
<template>
|
||||
<Drawer class="w-[580px]">
|
||||
<template #title>
|
||||
<div class="config-header">
|
||||
<Input
|
||||
v-if="showInput"
|
||||
type="text"
|
||||
class="config-editable-input"
|
||||
@blur="blurEvent()"
|
||||
v-model:value="nodeName"
|
||||
:placeholder="nodeName"
|
||||
/>
|
||||
<div v-else class="node-name">
|
||||
{{ nodeName }}
|
||||
<IconifyIcon class="ml-1" icon="ep:edit-pen" @click="clickIcon()" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<Tabs v-model:active-key="activeTabName">
|
||||
<TabPane tab="抄送人" key="user">
|
||||
<div>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="configForm"
|
||||
:label-col="{ span: 24 }"
|
||||
:wrapper-col="{ span: 24 }"
|
||||
:rules="formRules"
|
||||
>
|
||||
<FormItem label="抄送人设置" name="candidateStrategy">
|
||||
<RadioGroup
|
||||
v-model:value="configForm.candidateStrategy"
|
||||
@change="changeCandidateStrategy"
|
||||
>
|
||||
<Row :gutter="[0, 8]">
|
||||
<Col
|
||||
v-for="(dict, index) in copyUserStrategies"
|
||||
:key="index"
|
||||
:span="8"
|
||||
>
|
||||
<Radio :value="dict.value" :label="dict.value">
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</Col>
|
||||
</Row>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
v-if="configForm.candidateStrategy === CandidateStrategy.ROLE"
|
||||
label="指定角色"
|
||||
name="roleIds"
|
||||
>
|
||||
<Select
|
||||
v-model:value="configForm.roleIds"
|
||||
clearable
|
||||
mode="multiple"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="item in roleOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.candidateStrategy ===
|
||||
CandidateStrategy.DEPT_MEMBER ||
|
||||
configForm.candidateStrategy ===
|
||||
CandidateStrategy.DEPT_LEADER ||
|
||||
configForm.candidateStrategy ===
|
||||
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
|
||||
"
|
||||
label="指定部门"
|
||||
name="deptIds"
|
||||
>
|
||||
<TreeSelect
|
||||
v-model:value="configForm.deptIds"
|
||||
:tree-data="deptTreeOptions"
|
||||
:field-names="{
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
children: 'children',
|
||||
}"
|
||||
empty-text="加载中,请稍后"
|
||||
multiple
|
||||
:check-strictly="true"
|
||||
allow-clear
|
||||
tree-checkable
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="configForm.candidateStrategy === CandidateStrategy.POST"
|
||||
label="指定岗位"
|
||||
name="postIds"
|
||||
>
|
||||
<Select
|
||||
v-model:value="configForm.postIds"
|
||||
clearable
|
||||
mode="multiple"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="item in postOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id!"
|
||||
>
|
||||
{{ item.name }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="configForm.candidateStrategy === CandidateStrategy.USER"
|
||||
label="指定用户"
|
||||
name="userIds"
|
||||
>
|
||||
<Select
|
||||
v-model:value="configForm.userIds"
|
||||
clearable
|
||||
mode="multiple"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="item in userOptions"
|
||||
:key="item.id"
|
||||
:label="item.nickname"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.nickname }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.candidateStrategy === CandidateStrategy.USER_GROUP
|
||||
"
|
||||
label="指定用户组"
|
||||
name="userGroups"
|
||||
>
|
||||
<Select
|
||||
v-model:value="configForm.userGroups"
|
||||
clearable
|
||||
mode="multiple"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="item in userGroupOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.candidateStrategy === CandidateStrategy.FORM_USER
|
||||
"
|
||||
label="表单内用户字段"
|
||||
name="formUser"
|
||||
>
|
||||
<Select v-model:value="configForm.formUser" clearable>
|
||||
<SelectOption
|
||||
v-for="(item, idx) in userFieldOnFormOptions"
|
||||
:key="idx"
|
||||
:label="item.title"
|
||||
:value="item.field"
|
||||
:disabled="!item.required"
|
||||
>
|
||||
{{ item.title }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.candidateStrategy ===
|
||||
CandidateStrategy.FORM_DEPT_LEADER
|
||||
"
|
||||
label="表单内部门字段"
|
||||
name="formDept"
|
||||
>
|
||||
<Select v-model:value="configForm.formDept" clearable>
|
||||
<SelectOption
|
||||
v-for="(item, idx) in deptFieldOnFormOptions"
|
||||
:key="idx"
|
||||
:label="item.title"
|
||||
:value="item.field"
|
||||
:disabled="!item.required"
|
||||
>
|
||||
{{ item.title }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.candidateStrategy ===
|
||||
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
|
||||
configForm.candidateStrategy ===
|
||||
CandidateStrategy.START_USER_DEPT_LEADER ||
|
||||
configForm.candidateStrategy ===
|
||||
CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
|
||||
configForm.candidateStrategy ===
|
||||
CandidateStrategy.FORM_DEPT_LEADER
|
||||
"
|
||||
:label="deptLevelLabel!"
|
||||
name="deptLevel"
|
||||
>
|
||||
<Select v-model:value="configForm.deptLevel" clearable>
|
||||
<SelectOption
|
||||
v-for="(item, index) in MULTI_LEVEL_DEPT"
|
||||
:key="index"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.candidateStrategy === CandidateStrategy.EXPRESSION
|
||||
"
|
||||
label="流程表达式"
|
||||
name="expression"
|
||||
>
|
||||
<Textarea v-model:value="configForm.expression" clearable />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane
|
||||
tab="表单字段权限"
|
||||
key="fields"
|
||||
v-if="formType === BpmModelFormType.NORMAL"
|
||||
>
|
||||
<div class="p-1">
|
||||
<div class="mb-4 text-[16px] font-bold">字段权限</div>
|
||||
|
||||
<!-- 表头 -->
|
||||
<Row class="border border-gray-200 px-4 py-3">
|
||||
<Col :span="8" class="font-bold">字段名称</Col>
|
||||
<Col :span="16">
|
||||
<Row>
|
||||
<Col :span="8" class="flex items-center justify-center">
|
||||
<span
|
||||
class="cursor-pointer font-bold"
|
||||
@click="updatePermission('READ')"
|
||||
>
|
||||
只读
|
||||
</span>
|
||||
</Col>
|
||||
<Col :span="8" class="flex items-center justify-center">
|
||||
<span
|
||||
class="cursor-pointer font-bold"
|
||||
@click="updatePermission('WRITE')"
|
||||
>
|
||||
可编辑
|
||||
</span>
|
||||
</Col>
|
||||
<Col :span="8" class="flex items-center justify-center">
|
||||
<span
|
||||
class="cursor-pointer font-bold"
|
||||
@click="updatePermission('NONE')"
|
||||
>
|
||||
隐藏
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 表格内容 -->
|
||||
<div v-for="(item, index) in fieldsPermissionConfig" :key="index">
|
||||
<Row class="border border-t-0 border-gray-200 px-4 py-2">
|
||||
<Col :span="8" class="flex items-center truncate">
|
||||
{{ item.title }}
|
||||
</Col>
|
||||
<Col :span="16">
|
||||
<RadioGroup v-model:value="item.permission" class="w-full">
|
||||
<Row>
|
||||
<Col :span="8" class="flex items-center justify-center">
|
||||
<Radio
|
||||
:value="FieldPermissionType.READ"
|
||||
size="large"
|
||||
:label="FieldPermissionType.READ"
|
||||
/>
|
||||
</Col>
|
||||
<Col :span="8" class="flex items-center justify-center">
|
||||
<Radio
|
||||
:value="FieldPermissionType.WRITE"
|
||||
size="large"
|
||||
:label="FieldPermissionType.WRITE"
|
||||
disabled
|
||||
/>
|
||||
</Col>
|
||||
<Col :span="8" class="flex items-center justify-center">
|
||||
<Radio
|
||||
:value="FieldPermissionType.NONE"
|
||||
size="large"
|
||||
:label="FieldPermissionType.NONE"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</RadioGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Drawer>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.config-editable-input {
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-color: #40a9ff;
|
||||
box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,247 @@
|
|||
<script setup lang="ts">
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
|
||||
import type { SimpleFlowNode } from '../../consts';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Col,
|
||||
DatePicker,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
DELAY_TYPE,
|
||||
DelayTypeEnum,
|
||||
NodeType,
|
||||
TIME_UNIT_TYPES,
|
||||
TimeUnitType,
|
||||
} from '../../consts';
|
||||
import { useNodeName, useWatchNode } from '../../helpers';
|
||||
import { convertTimeUnit } from './utils';
|
||||
|
||||
defineOptions({ name: 'DelayTimerNodeConfig' });
|
||||
|
||||
const props = defineProps({
|
||||
flowNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 当前节点
|
||||
const currentNode = useWatchNode(props);
|
||||
// 节点名称
|
||||
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(
|
||||
NodeType.DELAY_TIMER_NODE,
|
||||
);
|
||||
// 抄送人表单配置
|
||||
const formRef = ref(); // 表单 Ref
|
||||
|
||||
// 表单校验规则
|
||||
const formRules: Record<string, Rule[]> = reactive({
|
||||
delayType: [
|
||||
{ required: true, message: '延迟时间不能为空', trigger: 'change' },
|
||||
],
|
||||
timeDuration: [
|
||||
{ required: true, message: '延迟时间不能为空', trigger: 'change' },
|
||||
],
|
||||
dateTime: [
|
||||
{ required: true, message: '延迟时间不能为空', trigger: 'change' },
|
||||
],
|
||||
});
|
||||
|
||||
// 配置表单数据
|
||||
const configForm = ref({
|
||||
delayType: DelayTypeEnum.FIXED_TIME_DURATION,
|
||||
timeDuration: 1,
|
||||
timeUnit: TimeUnitType.HOUR,
|
||||
dateTime: '',
|
||||
});
|
||||
|
||||
// 获取显示文本
|
||||
function getShowText(): string {
|
||||
let showText = '';
|
||||
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
||||
showText = `延迟${configForm.value.timeDuration}${TIME_UNIT_TYPES?.find((item) => item.value === configForm.value.timeUnit)?.label}`;
|
||||
}
|
||||
if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
|
||||
showText = `延迟至${configForm.value.dateTime.replace('T', ' ')}`;
|
||||
}
|
||||
return showText;
|
||||
}
|
||||
|
||||
// 获取ISO时间格式
|
||||
function getIsoTimeDuration() {
|
||||
let strTimeDuration = 'PT';
|
||||
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
||||
strTimeDuration += `${configForm.value.timeDuration}M`;
|
||||
}
|
||||
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
||||
strTimeDuration += `${configForm.value.timeDuration}H`;
|
||||
}
|
||||
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
||||
strTimeDuration += `${configForm.value.timeDuration}D`;
|
||||
}
|
||||
return strTimeDuration;
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function saveConfig() {
|
||||
if (!formRef.value) return false;
|
||||
const valid = await formRef.value.validate();
|
||||
if (!valid) return false;
|
||||
const showText = getShowText();
|
||||
if (!showText) return false;
|
||||
currentNode.value.name = nodeName.value!;
|
||||
currentNode.value.showText = showText;
|
||||
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
||||
currentNode.value.delaySetting = {
|
||||
delayType: configForm.value.delayType,
|
||||
delayTime: getIsoTimeDuration(),
|
||||
};
|
||||
}
|
||||
if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
|
||||
currentNode.value.delaySetting = {
|
||||
delayType: configForm.value.delayType,
|
||||
delayTime: configForm.value.dateTime,
|
||||
};
|
||||
}
|
||||
drawerApi.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
title: nodeName.value,
|
||||
onConfirm: saveConfig,
|
||||
});
|
||||
|
||||
// 显示延迟器节点配置,由父组件调用
|
||||
function openDrawer(node: SimpleFlowNode) {
|
||||
nodeName.value = node.name;
|
||||
if (node.delaySetting) {
|
||||
configForm.value.delayType = node.delaySetting.delayType;
|
||||
// 固定时长
|
||||
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
||||
const strTimeDuration = node.delaySetting.delayTime;
|
||||
const parseTime = strTimeDuration.slice(2, -1);
|
||||
const parseTimeUnit = strTimeDuration.slice(-1);
|
||||
configForm.value.timeDuration = Number.parseInt(parseTime);
|
||||
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
||||
}
|
||||
// 固定日期时间
|
||||
if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
|
||||
configForm.value.dateTime = node.delaySetting.delayTime;
|
||||
}
|
||||
}
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
defineExpose({ openDrawer }); // 暴露方法给父组件
|
||||
</script>
|
||||
<template>
|
||||
<Drawer class="w-[480px]">
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<Input
|
||||
v-if="showInput"
|
||||
type="text"
|
||||
class="mr-2 w-48"
|
||||
@blur="blurEvent()"
|
||||
v-model:value="nodeName"
|
||||
:placeholder="nodeName"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex cursor-pointer items-center"
|
||||
@click="clickIcon()"
|
||||
>
|
||||
{{ nodeName }}
|
||||
<IconifyIcon class="ml-1" icon="ep:edit-pen" :size="16" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="configForm"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 24 }"
|
||||
:wrapper-col="{ span: 24 }"
|
||||
>
|
||||
<FormItem label="延迟时间" name="delayType">
|
||||
<RadioGroup v-model:value="configForm.delayType">
|
||||
<Radio
|
||||
v-for="item in DELAY_TYPE"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="configForm.delayType === DelayTypeEnum.FIXED_TIME_DURATION"
|
||||
>
|
||||
<Row :gutter="8">
|
||||
<Col>
|
||||
<FormItem name="timeDuration">
|
||||
<InputNumber
|
||||
class="w-28"
|
||||
v-model:value="configForm.timeDuration"
|
||||
:min="1"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col>
|
||||
<Select v-model:value="configForm.timeUnit" class="w-28">
|
||||
<SelectOption
|
||||
v-for="item in TIME_UNIT_TYPES"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col>
|
||||
<span class="inline-flex h-8 items-center">后进入下一节点</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="configForm.delayType === DelayTypeEnum.FIXED_DATE_TIME"
|
||||
name="dateTime"
|
||||
>
|
||||
<Row :gutter="8">
|
||||
<Col>
|
||||
<DatePicker
|
||||
class="mr-2"
|
||||
v-model:value="configForm.dateTime"
|
||||
show-time
|
||||
placeholder="请选择日期和时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<span class="inline-flex h-8 items-center">后进入下一节点</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
</Drawer>
|
||||
</template>
|
|
@ -0,0 +1,69 @@
|
|||
<script setup lang="ts">
|
||||
import type { ConditionGroup } from '../../../consts';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { ConditionType, DEFAULT_CONDITION_GROUP_VALUE } from '../../../consts';
|
||||
import Condition from './condition.vue';
|
||||
|
||||
defineOptions({ name: 'ConditionDialog' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
updateCondition: [condition: object];
|
||||
}>();
|
||||
|
||||
const conditionData = ref<{
|
||||
conditionExpression?: string;
|
||||
conditionGroups?: ConditionGroup;
|
||||
conditionType: ConditionType;
|
||||
}>({
|
||||
conditionType: ConditionType.RULE,
|
||||
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
|
||||
});
|
||||
|
||||
// 条件组件的引用
|
||||
const conditionRef = ref();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
title: '条件配置',
|
||||
destroyOnClose: true,
|
||||
draggable: true,
|
||||
async onConfirm() {
|
||||
// 校验表单
|
||||
if (!conditionRef.value) return;
|
||||
const valid = await conditionRef.value.validate().catch(() => false);
|
||||
if (!valid) {
|
||||
message.warning('请完善条件规则');
|
||||
return;
|
||||
}
|
||||
// 设置完的条件传递给父组件
|
||||
emit('updateCondition', conditionData.value);
|
||||
modalApi.close();
|
||||
},
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: jason open 在 useVbenModal 中 onOpenChange 方法
|
||||
function open(conditionObj: any | undefined) {
|
||||
if (conditionObj) {
|
||||
conditionData.value.conditionType = conditionObj.conditionType;
|
||||
conditionData.value.conditionExpression = conditionObj.conditionExpression;
|
||||
conditionData.value.conditionGroups = conditionObj.conditionGroups;
|
||||
}
|
||||
modalApi.open();
|
||||
}
|
||||
// TODO: jason 不需要暴露expose,直接使用modalApi.setData(formSetting).open()
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
<template>
|
||||
<Modal class="w-1/2">
|
||||
<Condition ref="conditionRef" v-model="conditionData" />
|
||||
</Modal>
|
||||
</template>
|
|
@ -0,0 +1,328 @@
|
|||
<script setup lang="ts">
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { computed, inject, reactive, ref } from 'vue';
|
||||
|
||||
import { IconifyIcon, Plus, Trash2 } from '@vben/icons';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
Space,
|
||||
Switch,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { BpmModelFormType } from '#/utils/constants';
|
||||
|
||||
import {
|
||||
COMPARISON_OPERATORS,
|
||||
CONDITION_CONFIG_TYPES,
|
||||
ConditionType,
|
||||
DEFAULT_CONDITION_GROUP_VALUE,
|
||||
} from '../../../consts';
|
||||
import { useFormFieldsAndStartUser } from '../../../helpers';
|
||||
|
||||
defineOptions({
|
||||
name: 'Condition',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const condition = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(newValue) {
|
||||
emit('update:modelValue', newValue);
|
||||
},
|
||||
});
|
||||
|
||||
const formType = inject<Ref<number>>('formType'); // 表单类型
|
||||
const conditionConfigTypes = computed(() => {
|
||||
return CONDITION_CONFIG_TYPES.filter((item) => {
|
||||
// 业务表单暂时去掉条件规则选项
|
||||
return !(
|
||||
formType?.value === BpmModelFormType.CUSTOM &&
|
||||
item.value === ConditionType.RULE
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/** 条件规则可选择的表单字段 */
|
||||
const fieldOptions = useFormFieldsAndStartUser();
|
||||
|
||||
// 表单校验规则
|
||||
const formRules: Record<string, Rule[]> = reactive({
|
||||
conditionType: [
|
||||
{ required: true, message: '配置方式不能为空', trigger: 'change' },
|
||||
],
|
||||
conditionExpression: [
|
||||
{
|
||||
required: true,
|
||||
message: '条件表达式不能为空',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const formRef = ref(); // 表单 Ref
|
||||
|
||||
/** 切换条件配置方式 */
|
||||
function changeConditionType() {
|
||||
if (
|
||||
condition.value.conditionType === ConditionType.RULE &&
|
||||
!condition.value.conditionGroups
|
||||
) {
|
||||
condition.value.conditionGroups = cloneDeep(DEFAULT_CONDITION_GROUP_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteConditionGroup(conditions: any, index: number) {
|
||||
conditions.splice(index, 1);
|
||||
}
|
||||
|
||||
function deleteConditionRule(condition: any, index: number) {
|
||||
condition.rules.splice(index, 1);
|
||||
}
|
||||
|
||||
function addConditionRule(condition: any, index: number) {
|
||||
const rule = {
|
||||
opCode: '==',
|
||||
leftSide: undefined,
|
||||
rightSide: '',
|
||||
};
|
||||
condition.rules.splice(index + 1, 0, rule);
|
||||
}
|
||||
|
||||
function addConditionGroup(conditions: any) {
|
||||
const condition = {
|
||||
and: true,
|
||||
rules: [
|
||||
{
|
||||
opCode: '==',
|
||||
leftSide: undefined,
|
||||
rightSide: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
conditions.push(condition);
|
||||
}
|
||||
|
||||
async function validate() {
|
||||
if (!formRef.value) return false;
|
||||
return await formRef.value.validate();
|
||||
}
|
||||
|
||||
defineExpose({ validate });
|
||||
</script>
|
||||
<template>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="condition"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 24 }"
|
||||
:wrapper-col="{ span: 24 }"
|
||||
>
|
||||
<FormItem label="配置方式" name="conditionType">
|
||||
<RadioGroup
|
||||
v-model:value="condition.conditionType"
|
||||
@change="changeConditionType"
|
||||
>
|
||||
<Radio
|
||||
v-for="(dict, indexConditionType) in conditionConfigTypes"
|
||||
:key="indexConditionType"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
condition.conditionType === ConditionType.RULE &&
|
||||
condition.conditionGroups
|
||||
"
|
||||
>
|
||||
<div class="mb-5 flex w-full justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="mr-4">条件组关系</div>
|
||||
<Switch
|
||||
v-model:checked="condition.conditionGroups.and"
|
||||
checked-children="且"
|
||||
un-checked-children="或"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Space direction="vertical" size="small" class="w-11/12 pl-1">
|
||||
<template #split>
|
||||
{{ condition.conditionGroups.and ? '且' : '或' }}
|
||||
</template>
|
||||
<Card
|
||||
class="group relative w-full hover:border-[#1890ff]"
|
||||
v-for="(equation, cIdx) in condition.conditionGroups.conditions"
|
||||
:key="cIdx"
|
||||
>
|
||||
<div
|
||||
class="absolute left-0 top-0 z-[1] flex cursor-pointer opacity-0 group-hover:opacity-100"
|
||||
v-if="condition.conditionGroups.conditions.length > 1"
|
||||
>
|
||||
<IconifyIcon
|
||||
color="#0089ff"
|
||||
icon="ep:circle-close-filled"
|
||||
class="size-4"
|
||||
@click="
|
||||
deleteConditionGroup(condition.conditionGroups.conditions, cIdx)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<template #extra>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>条件组</div>
|
||||
<div class="flex">
|
||||
<div class="mr-4">规则关系</div>
|
||||
<Switch
|
||||
v-model:checked="equation.and"
|
||||
checked-children="且"
|
||||
un-checked-children="或"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Row
|
||||
:gutter="8"
|
||||
class="mb-2"
|
||||
v-for="(rule, rIdx) in equation.rules"
|
||||
:key="rIdx"
|
||||
>
|
||||
<Col :span="8">
|
||||
<FormItem
|
||||
:name="[
|
||||
'conditionGroups',
|
||||
'conditions',
|
||||
cIdx,
|
||||
'rules',
|
||||
rIdx,
|
||||
'leftSide',
|
||||
]"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '左值不能为空',
|
||||
trigger: 'change',
|
||||
}"
|
||||
>
|
||||
<Select
|
||||
v-model:value="rule.leftSide"
|
||||
allow-clear
|
||||
placeholder="请选择表单字段"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="(field, fIdx) in fieldOptions"
|
||||
:key="fIdx"
|
||||
:label="field.title"
|
||||
:value="field.field"
|
||||
:disabled="!field.required"
|
||||
>
|
||||
<Tooltip
|
||||
title="表单字段非必填时不能作为流程分支条件"
|
||||
placement="right"
|
||||
v-if="!field.required"
|
||||
>
|
||||
<span>{{ field.title }}</span>
|
||||
</Tooltip>
|
||||
<template v-else>{{ field.title }}</template>
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Select v-model:value="rule.opCode" placeholder="请选择操作符">
|
||||
<SelectOption
|
||||
v-for="operator in COMPARISON_OPERATORS"
|
||||
:key="operator.value"
|
||||
:label="operator.label"
|
||||
:value="operator.value"
|
||||
>
|
||||
{{ operator.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col :span="7">
|
||||
<FormItem
|
||||
:name="[
|
||||
'conditionGroups',
|
||||
'conditions',
|
||||
cIdx,
|
||||
'rules',
|
||||
rIdx,
|
||||
'rightSide',
|
||||
]"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '右值不能为空',
|
||||
trigger: ['blur', 'change'],
|
||||
}"
|
||||
>
|
||||
<Input
|
||||
v-model:value="rule.rightSide"
|
||||
placeholder="请输入右值"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="flex h-[32px] items-center">
|
||||
<Trash2
|
||||
v-if="equation.rules.length > 1"
|
||||
class="mr-2 size-4 cursor-pointer text-red-500"
|
||||
@click="deleteConditionRule(equation, rIdx)"
|
||||
/>
|
||||
<Plus
|
||||
class="size-4 cursor-pointer text-blue-500"
|
||||
@click="addConditionRule(equation, rIdx)"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Space>
|
||||
<div title="添加条件组" class="mt-4 cursor-pointer">
|
||||
<Plus
|
||||
class="size-[24px] text-blue-500"
|
||||
@click="addConditionGroup(condition.conditionGroups?.conditions)"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="condition.conditionType === ConditionType.EXPRESSION"
|
||||
label="条件表达式"
|
||||
name="conditionExpression"
|
||||
>
|
||||
<Textarea
|
||||
v-model:value="condition.conditionExpression"
|
||||
placeholder="请输入条件表达式"
|
||||
allow-clear
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
|
@ -0,0 +1,227 @@
|
|||
<script setup lang="ts">
|
||||
import type { HttpRequestParam } from '../../../consts';
|
||||
|
||||
import { Plus, Trash2 } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
FormItem,
|
||||
Input,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
BPM_HTTP_REQUEST_PARAM_TYPES,
|
||||
BpmHttpRequestParamTypeEnum,
|
||||
} from '../../../consts';
|
||||
import { useFormFieldsAndStartUser } from '../../../helpers';
|
||||
|
||||
defineOptions({ name: 'HttpRequestParamSetting' });
|
||||
|
||||
const props = defineProps({
|
||||
header: {
|
||||
type: Array as () => HttpRequestParam[],
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
body: {
|
||||
type: Array as () => HttpRequestParam[],
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
bind: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 流程表单字段,发起人字段
|
||||
const formFieldOptions = useFormFieldsAndStartUser();
|
||||
|
||||
/** 添加请求配置项 */
|
||||
function addHttpRequestParam(arr: HttpRequestParam[]) {
|
||||
arr.push({
|
||||
key: '',
|
||||
type: BpmHttpRequestParamTypeEnum.FIXED_VALUE,
|
||||
value: '',
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除请求配置项 */
|
||||
function deleteHttpRequestParam(arr: HttpRequestParam[], index: number) {
|
||||
arr.splice(index, 1);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<FormItem
|
||||
label="请求头"
|
||||
:label-col="{ span: 24 }"
|
||||
:wrapper-col="{ span: 24 }"
|
||||
>
|
||||
<Row :gutter="8" v-for="(item, index) in props.header" :key="index">
|
||||
<Col :span="7">
|
||||
<FormItem
|
||||
:name="[bind, 'header', index, 'key']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '参数名不能为空',
|
||||
trigger: ['blur', 'change'],
|
||||
}"
|
||||
>
|
||||
<Input placeholder="参数名不能为空" v-model:value="item.key" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="5">
|
||||
<Select v-model:value="item.type">
|
||||
<SelectOption
|
||||
v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
|
||||
:key="types.value"
|
||||
:label="types.label"
|
||||
:value="types.value"
|
||||
>
|
||||
{{ types.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col :span="10">
|
||||
<FormItem
|
||||
:name="[bind, 'header', index, 'value']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '参数值不能为空',
|
||||
trigger: ['blur', 'change'],
|
||||
}"
|
||||
v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE"
|
||||
>
|
||||
<Input placeholder="请求头" v-model:value="item.value" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
:name="[bind, 'header', index, 'value']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '参数值不能为空',
|
||||
trigger: 'change',
|
||||
}"
|
||||
v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM"
|
||||
>
|
||||
<Select v-model:value="item.value" placeholder="请选择表单字段">
|
||||
<SelectOption
|
||||
v-for="(field, fIdx) in formFieldOptions"
|
||||
:key="fIdx"
|
||||
:label="field.title"
|
||||
:value="field.field"
|
||||
:disabled="!field.required"
|
||||
>
|
||||
{{ field.title }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="2">
|
||||
<div class="flex h-[32px] items-center">
|
||||
<Trash2
|
||||
class="size-4 cursor-pointer text-red-500"
|
||||
@click="deleteHttpRequestParam(props.header, index)"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button
|
||||
type="link"
|
||||
@click="addHttpRequestParam(props.header)"
|
||||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<Plus class="size-[18px]" />
|
||||
</template>
|
||||
添加一行
|
||||
</Button>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="请求体"
|
||||
:label-col="{ span: 24 }"
|
||||
:wrapper-col="{ span: 24 }"
|
||||
>
|
||||
<Row :gutter="8" v-for="(item, index) in props.body" :key="index">
|
||||
<Col :span="7">
|
||||
<FormItem
|
||||
:name="[bind, 'body', index, 'key']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '参数名不能为空',
|
||||
trigger: ['blur', 'change'],
|
||||
}"
|
||||
>
|
||||
<Input placeholder="参数名" v-model:value="item.key" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="5">
|
||||
<Select v-model:value="item.type">
|
||||
<SelectOption
|
||||
v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
|
||||
:key="types.value"
|
||||
:label="types.label"
|
||||
:value="types.value"
|
||||
>
|
||||
{{ types.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col :span="10">
|
||||
<FormItem
|
||||
:name="[bind, 'body', index, 'value']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '参数值不能为空',
|
||||
trigger: ['blur', 'change'],
|
||||
}"
|
||||
v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE"
|
||||
>
|
||||
<Input placeholder="参数值" v-model:value="item.value" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
:name="[bind, 'body', index, 'value']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '参数值不能为空',
|
||||
trigger: 'change',
|
||||
}"
|
||||
v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM"
|
||||
>
|
||||
<Select v-model:value="item.value" placeholder="请选择表单字段">
|
||||
<SelectOption
|
||||
v-for="(field, fIdx) in formFieldOptions"
|
||||
:key="fIdx"
|
||||
:label="field.title"
|
||||
:value="field.field"
|
||||
:disabled="!field.required"
|
||||
>
|
||||
{{ field.title }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="2">
|
||||
<div class="flex h-[32px] items-center">
|
||||
<Trash2
|
||||
class="size-4 cursor-pointer text-red-500"
|
||||
@click="deleteHttpRequestParam(props.body, index)"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button
|
||||
type="link"
|
||||
@click="addHttpRequestParam(props.body)"
|
||||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<Plus class="size-[18px]" />
|
||||
</template>
|
||||
添加一行
|
||||
</Button>
|
||||
</FormItem>
|
||||
</template>
|
|
@ -0,0 +1,176 @@
|
|||
<script setup lang="ts">
|
||||
import { toRefs, watch } from 'vue';
|
||||
|
||||
import { Plus, Trash2 } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Col,
|
||||
FormItem,
|
||||
Input,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { useFormFields } from '../../../helpers';
|
||||
import HttpRequestParamSetting from './http-request-param-setting.vue';
|
||||
|
||||
defineOptions({ name: 'HttpRequestSetting' });
|
||||
|
||||
const props = defineProps({
|
||||
setting: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
responseEnable: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
formItemPrefix: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:setting']);
|
||||
|
||||
const { setting } = toRefs(props);
|
||||
|
||||
watch(
|
||||
() => setting,
|
||||
(val) => {
|
||||
emits('update:setting', val);
|
||||
},
|
||||
);
|
||||
|
||||
/** 流程表单字段 */
|
||||
const formFields = useFormFields();
|
||||
|
||||
/** 添加 HTTP 请求返回值设置项 */
|
||||
function addHttpResponseSetting(responseSetting: Record<string, string>[]) {
|
||||
responseSetting.push({
|
||||
key: '',
|
||||
value: '',
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除 HTTP 请求返回值设置项 */
|
||||
function deleteHttpResponseSetting(
|
||||
responseSetting: Record<string, string>[],
|
||||
index: number,
|
||||
) {
|
||||
responseSetting.splice(index, 1);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<FormItem>
|
||||
<Alert
|
||||
message="仅支持 POST 请求,以请求体方式接收参数"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
</FormItem>
|
||||
<!-- 请求地址-->
|
||||
<FormItem
|
||||
label="请求地址"
|
||||
:label-col="{ span: 24 }"
|
||||
:wrapper-col="{ span: 24 }"
|
||||
:name="[formItemPrefix, 'url']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '请求地址不能为空',
|
||||
trigger: ['blur', 'change'],
|
||||
}"
|
||||
>
|
||||
<Input v-model:value="setting.url" placeholder="请输入请求地址" />
|
||||
</FormItem>
|
||||
<!-- 请求头,请求体设置-->
|
||||
<HttpRequestParamSetting
|
||||
:header="setting.header"
|
||||
:body="setting.body"
|
||||
:bind="formItemPrefix"
|
||||
/>
|
||||
<!-- 返回值设置-->
|
||||
<div v-if="responseEnable">
|
||||
<FormItem
|
||||
label="返回值"
|
||||
:label-col="{ span: 24 }"
|
||||
:wrapper-col="{ span: 24 }"
|
||||
>
|
||||
<Alert
|
||||
message="通过请求返回值, 可以修改流程表单的值"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem :wrapper-col="{ span: 24 }">
|
||||
<Row
|
||||
:gutter="8"
|
||||
v-for="(item, index) in setting.response"
|
||||
:key="index"
|
||||
class="mb-2"
|
||||
>
|
||||
<Col :span="10">
|
||||
<FormItem
|
||||
:name="[formItemPrefix, 'response', index, 'key']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '表单字段不能为空',
|
||||
trigger: ['blur', 'change'],
|
||||
}"
|
||||
>
|
||||
<Select
|
||||
v-model:value="item.key"
|
||||
placeholder="请选择表单字段"
|
||||
allow-clear
|
||||
>
|
||||
<SelectOption
|
||||
v-for="(field, fIdx) in formFields"
|
||||
:key="fIdx"
|
||||
:label="field.title"
|
||||
:value="field.field"
|
||||
:disabled="!field.required"
|
||||
>
|
||||
{{ field.title }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<FormItem
|
||||
:name="[formItemPrefix, 'response', index, 'value']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '请求返回字段不能为空',
|
||||
trigger: ['blur', 'change'],
|
||||
}"
|
||||
>
|
||||
<Input v-model:value="item.value" placeholder="请求返回字段" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="2">
|
||||
<div class="flex h-[32px] items-center">
|
||||
<Trash2
|
||||
class="size-4 cursor-pointer text-red-500"
|
||||
@click="deleteHttpResponseSetting(setting.response!, index)"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button
|
||||
type="link"
|
||||
@click="addHttpResponseSetting(setting.response!)"
|
||||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<Plus class="size-[18px]" />
|
||||
</template>
|
||||
添加一行
|
||||
</Button>
|
||||
</FormItem>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,111 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Divider,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Switch,
|
||||
TypographyText,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import HttpRequestParamSetting from './http-request-param-setting.vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
formFieldOptions: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const listenerFormRef = ref();
|
||||
|
||||
const configForm = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(newValue) {
|
||||
emit('update:modelValue', newValue);
|
||||
},
|
||||
});
|
||||
|
||||
const taskListener = ref([
|
||||
{
|
||||
name: '创建任务',
|
||||
type: 'Create',
|
||||
},
|
||||
{
|
||||
name: '指派任务执行人员',
|
||||
type: 'Assign',
|
||||
},
|
||||
{
|
||||
name: '完成任务',
|
||||
type: 'Complete',
|
||||
},
|
||||
]);
|
||||
|
||||
async function validate() {
|
||||
if (!listenerFormRef.value) return false;
|
||||
return await listenerFormRef.value.validate();
|
||||
}
|
||||
|
||||
defineExpose({ validate });
|
||||
</script>
|
||||
<template>
|
||||
<Form ref="listenerFormRef" :model="configForm" :label-col="{ span: 24 }">
|
||||
<div
|
||||
v-for="(listener, listenerIdx) in taskListener"
|
||||
:key="listenerIdx"
|
||||
class="pl-2"
|
||||
>
|
||||
<Divider orientation="left">
|
||||
<TypographyText tag="b" size="large">
|
||||
{{ listener.name }}
|
||||
</TypographyText>
|
||||
</Divider>
|
||||
<FormItem>
|
||||
<Switch
|
||||
v-model:checked="configForm[`task${listener.type}ListenerEnable`]"
|
||||
checked-children="开启"
|
||||
un-checked-children="关闭"
|
||||
/>
|
||||
</FormItem>
|
||||
<div v-if="configForm[`task${listener.type}ListenerEnable`]">
|
||||
<FormItem>
|
||||
<Alert
|
||||
message="仅支持 POST 请求,以请求体方式接收参数"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="请求地址"
|
||||
:name="`task${listener.type}ListenerPath`"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '请求地址不能为空',
|
||||
trigger: ['blur', 'change'],
|
||||
}"
|
||||
>
|
||||
<Input
|
||||
v-model:value="configForm[`task${listener.type}ListenerPath`]"
|
||||
/>
|
||||
</FormItem>
|
||||
<HttpRequestParamSetting
|
||||
:header="configForm[`task${listener.type}Listener`].header"
|
||||
:body="configForm[`task${listener.type}Listener`].body"
|
||||
:bind="`task${listener.type}Listener`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
|
@ -0,0 +1,294 @@
|
|||
<script setup lang="ts">
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { RouterSetting, SimpleFlowNode } from '../../consts';
|
||||
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
message,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { ConditionType, NodeType } from '../../consts';
|
||||
import { useNodeName, useWatchNode } from '../../helpers';
|
||||
import Condition from './modules/condition.vue';
|
||||
|
||||
defineOptions({ name: 'RouterNodeConfig' });
|
||||
|
||||
const props = defineProps({
|
||||
flowNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const processNodeTree = inject<Ref<SimpleFlowNode>>('processNodeTree');
|
||||
|
||||
/** 当前节点 */
|
||||
const currentNode = useWatchNode(props);
|
||||
/** 节点名称 */
|
||||
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(
|
||||
NodeType.ROUTER_BRANCH_NODE,
|
||||
);
|
||||
const routerGroups = ref<RouterSetting[]>([]);
|
||||
const nodeOptions = ref<any[]>([]);
|
||||
const conditionRef = ref<any[]>([]);
|
||||
const formRef = ref();
|
||||
|
||||
/** 校验节点配置 */
|
||||
async function validateConfig() {
|
||||
// 校验路由分支选择
|
||||
const routeIdValid = await formRef.value.validate().catch(() => false);
|
||||
if (!routeIdValid) {
|
||||
message.warning('请配置路由目标节点');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 校验条件规则
|
||||
let valid = true;
|
||||
for (const item of conditionRef.value) {
|
||||
if (item && !(await item.validate())) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
if (!valid) return false;
|
||||
|
||||
// 获取节点显示文本,如果为空,校验不通过
|
||||
const showText = getShowText();
|
||||
if (!showText) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 保存配置 */
|
||||
async function saveConfig() {
|
||||
// 校验配置
|
||||
if (!(await validateConfig())) {
|
||||
return false;
|
||||
}
|
||||
// 保存配置
|
||||
currentNode.value.name = nodeName.value!;
|
||||
currentNode.value.showText = getShowText();
|
||||
currentNode.value.routerGroups = routerGroups.value;
|
||||
drawerApi.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
title: nodeName.value,
|
||||
onConfirm: saveConfig,
|
||||
});
|
||||
|
||||
/** 打开路由节点配置抽屉,由父组件调用 */
|
||||
function openDrawer(node: SimpleFlowNode) {
|
||||
nodeOptions.value = [];
|
||||
getRouterNode(processNodeTree?.value);
|
||||
routerGroups.value = [];
|
||||
nodeName.value = node.name;
|
||||
if (node.routerGroups) {
|
||||
routerGroups.value = node.routerGroups;
|
||||
}
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
/** 获取显示文本 */
|
||||
function getShowText() {
|
||||
if (
|
||||
!routerGroups.value ||
|
||||
!Array.isArray(routerGroups.value) ||
|
||||
routerGroups.value.length <= 0
|
||||
) {
|
||||
message.warning('请配置路由!');
|
||||
return '';
|
||||
}
|
||||
for (const route of routerGroups.value) {
|
||||
if (!route.nodeId || !route.conditionType) {
|
||||
message.warning('请完善路由配置项!');
|
||||
return '';
|
||||
}
|
||||
if (
|
||||
route.conditionType === ConditionType.EXPRESSION &&
|
||||
!route.conditionExpression
|
||||
) {
|
||||
message.warning('请完善路由配置项!');
|
||||
return '';
|
||||
}
|
||||
if (route.conditionType === ConditionType.RULE) {
|
||||
for (const condition of route.conditionGroups.conditions) {
|
||||
for (const rule of condition.rules) {
|
||||
if (!rule.leftSide || !rule.rightSide) {
|
||||
message.warning('请完善路由配置项!');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return `${routerGroups.value.length}条路由分支`;
|
||||
}
|
||||
|
||||
/** 添加路由分支 */
|
||||
function addRouterGroup() {
|
||||
routerGroups.value.push({
|
||||
nodeId: undefined,
|
||||
conditionType: ConditionType.RULE,
|
||||
conditionExpression: '',
|
||||
conditionGroups: {
|
||||
and: true,
|
||||
conditions: [
|
||||
{
|
||||
and: true,
|
||||
rules: [
|
||||
{
|
||||
opCode: '==',
|
||||
leftSide: undefined,
|
||||
rightSide: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除路由分支 */
|
||||
function deleteRouterGroup(index: number) {
|
||||
routerGroups.value.splice(index, 1);
|
||||
}
|
||||
|
||||
/** 递归获取所有节点 */
|
||||
function getRouterNode(node: any) {
|
||||
// TODO 最好还需要满足以下要求
|
||||
// 并行分支、包容分支内部节点不能跳转到外部节点
|
||||
// 条件分支节点可以向上跳转到外部节点
|
||||
while (true) {
|
||||
if (!node) break;
|
||||
if (
|
||||
node.type !== NodeType.ROUTER_BRANCH_NODE &&
|
||||
node.type !== NodeType.CONDITION_NODE
|
||||
) {
|
||||
nodeOptions.value.push({
|
||||
label: node.name,
|
||||
value: node.id,
|
||||
});
|
||||
}
|
||||
if (!node.childNode || node.type === NodeType.END_EVENT_NODE) {
|
||||
break;
|
||||
}
|
||||
if (node.conditionNodes && node.conditionNodes.length > 0) {
|
||||
node.conditionNodes.forEach((item: any) => {
|
||||
getRouterNode(item);
|
||||
});
|
||||
}
|
||||
node = node.childNode;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ openDrawer }); // 暴露方法给父组件
|
||||
</script>
|
||||
<template>
|
||||
<Drawer class="w-[630px]">
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<Input
|
||||
v-if="showInput"
|
||||
type="text"
|
||||
class="mr-2 w-48"
|
||||
@blur="blurEvent()"
|
||||
v-model:value="nodeName"
|
||||
:placeholder="nodeName"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex cursor-pointer items-center"
|
||||
@click="clickIcon()"
|
||||
>
|
||||
{{ nodeName }}
|
||||
<IconifyIcon class="ml-1" icon="ep:edit-pen" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Form ref="formRef" :model="{ routerGroups }">
|
||||
<Card
|
||||
:body-style="{ padding: '10px' }"
|
||||
class="mt-4"
|
||||
v-for="(item, index) in routerGroups"
|
||||
:key="index"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex h-16 w-full items-center justify-between">
|
||||
<div class="flex items-center font-normal">
|
||||
<span class="font-medium">路由{{ index + 1 }}</span>
|
||||
<FormItem
|
||||
class="mb-0 ml-4 inline-block w-[180px]"
|
||||
:name="['routerGroups', index, 'nodeId']"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '路由目标节点不能为空',
|
||||
trigger: 'change',
|
||||
}"
|
||||
>
|
||||
<Select
|
||||
v-model:value="item.nodeId"
|
||||
placeholder="请选择路由目标节点"
|
||||
allow-clear
|
||||
>
|
||||
<SelectOption
|
||||
v-for="node in nodeOptions"
|
||||
:key="node.value"
|
||||
:value="node.value"
|
||||
>
|
||||
{{ node.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</div>
|
||||
<Button
|
||||
v-if="routerGroups.length > 1"
|
||||
shape="circle"
|
||||
class="flex items-center justify-center"
|
||||
@click="deleteRouterGroup(index)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:close" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<Condition
|
||||
:ref="(el) => (conditionRef[index] = el)"
|
||||
:model-value="routerGroups[index] as Record<string, any>"
|
||||
@update:model-value="(val) => (routerGroups[index] = val)"
|
||||
/>
|
||||
</Card>
|
||||
</Form>
|
||||
|
||||
<Row class="mt-4">
|
||||
<Col :span="24">
|
||||
<Button
|
||||
class="flex items-center p-0"
|
||||
type="link"
|
||||
@click="addRouterGroup"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:setting" />
|
||||
</template>
|
||||
新增路由分支
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Drawer>
|
||||
</template>
|
|
@ -37,12 +37,14 @@ import {
|
|||
} from '../../helpers';
|
||||
|
||||
defineOptions({ name: 'StartUserNodeConfig' });
|
||||
|
||||
const props = defineProps({
|
||||
flowNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 可发起流程的用户编号
|
||||
const startUserIds = inject<Ref<any[]>>('startUserIds');
|
||||
// 可发起流程的部门编号
|
||||
|
@ -59,10 +61,12 @@ const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(
|
|||
);
|
||||
// 激活的 Tab 标签页
|
||||
const activeTabName = ref('user');
|
||||
|
||||
// 表单字段权限配置
|
||||
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } =
|
||||
useFormFieldsPermission(FieldPermissionType.WRITE);
|
||||
const getUserNicknames = (userIds: number[]): string => {
|
||||
|
||||
function getUserNicknames(userIds: number[]): string {
|
||||
if (!userIds || userIds.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
@ -74,9 +78,9 @@ const getUserNicknames = (userIds: number[]): string => {
|
|||
}
|
||||
});
|
||||
return nicknames.join(',');
|
||||
};
|
||||
}
|
||||
|
||||
const getDeptNames = (deptIds: number[]): string => {
|
||||
function getDeptNames(deptIds: number[]): string {
|
||||
if (!deptIds || deptIds.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
@ -88,14 +92,14 @@ const getDeptNames = (deptIds: number[]): string => {
|
|||
}
|
||||
});
|
||||
return deptNames.join(',');
|
||||
};
|
||||
}
|
||||
|
||||
// 使用 VbenDrawer
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
header: true,
|
||||
closable: true,
|
||||
onCancel() {
|
||||
drawerApi.close();
|
||||
drawerApi.setState({ isOpen: false });
|
||||
},
|
||||
onConfirm() {
|
||||
saveConfig();
|
||||
|
@ -103,7 +107,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
|||
});
|
||||
|
||||
// 保存配置
|
||||
const saveConfig = async () => {
|
||||
async function saveConfig() {
|
||||
activeTabName.value = 'user';
|
||||
currentNode.value.name = nodeName.value!;
|
||||
currentNode.value.showText = '已设置';
|
||||
|
@ -111,20 +115,20 @@ const saveConfig = async () => {
|
|||
currentNode.value.fieldsPermission = fieldsPermissionConfig.value;
|
||||
// 设置发起人的按钮权限
|
||||
currentNode.value.buttonsSetting = START_USER_BUTTON_SETTING;
|
||||
drawerApi.close();
|
||||
drawerApi.setState({ isOpen: false });
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
// 显示发起人节点配置,由父组件传过来
|
||||
const showStartUserNodeConfig = (node: SimpleFlowNode) => {
|
||||
function showStartUserNodeConfig(node: SimpleFlowNode) {
|
||||
nodeName.value = node.name;
|
||||
// 表单字段权限
|
||||
getNodeConfigFormFields(node.fieldsPermission);
|
||||
drawerApi.open();
|
||||
};
|
||||
}
|
||||
|
||||
/** 批量更新权限 */
|
||||
const updatePermission = (type: string) => {
|
||||
function updatePermission(type: string) {
|
||||
fieldsPermissionConfig.value.forEach((field) => {
|
||||
if (type === 'READ') {
|
||||
field.permission = FieldPermissionType.READ;
|
||||
|
@ -134,7 +138,7 @@ const updatePermission = (type: string) => {
|
|||
field.permission = FieldPermissionType.NONE;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 暴露方法给父组件
|
||||
|
@ -289,4 +293,3 @@ defineExpose({ showStartUserNodeConfig });
|
|||
</Tabs>
|
||||
</Drawer>
|
||||
</template>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -0,0 +1,692 @@
|
|||
<script setup lang="ts">
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { SelectValue } from 'ant-design-vue/es/select';
|
||||
|
||||
import type {
|
||||
FormTriggerSetting,
|
||||
SimpleFlowNode,
|
||||
TriggerSetting,
|
||||
} from '../../consts';
|
||||
|
||||
import { computed, getCurrentInstance, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { IconifyIcon, Trash2 } from '@vben/icons';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
message,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
DEFAULT_CONDITION_GROUP_VALUE,
|
||||
NodeType,
|
||||
TRIGGER_TYPES,
|
||||
TriggerTypeEnum,
|
||||
} from '../../consts';
|
||||
import {
|
||||
getConditionShowText,
|
||||
useFormFields,
|
||||
useFormFieldsAndStartUser,
|
||||
useNodeName,
|
||||
useWatchNode,
|
||||
} from '../../helpers';
|
||||
import ConditionDialog from './modules/condition-dialog.vue';
|
||||
import HttpRequestSetting from './modules/http-request-setting.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'TriggerNodeConfig',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
flowNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
// 抽屉配置
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
header: true,
|
||||
closable: true,
|
||||
title: '',
|
||||
onConfirm() {
|
||||
saveConfig();
|
||||
},
|
||||
});
|
||||
|
||||
// 当前节点
|
||||
const currentNode = useWatchNode(props);
|
||||
// 节点名称
|
||||
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(
|
||||
NodeType.TRIGGER_NODE,
|
||||
);
|
||||
// 触发器表单配置
|
||||
const formRef = ref(); // 表单 Ref
|
||||
|
||||
// 表单校验规则
|
||||
const formRules: Record<string, Rule[]> = reactive({
|
||||
type: [{ required: true, message: '触发器类型不能为空', trigger: 'change' }],
|
||||
'httpRequestSetting.url': [
|
||||
{ required: true, message: '请求地址不能为空', trigger: 'blur' },
|
||||
],
|
||||
});
|
||||
|
||||
// 触发器配置表单数据
|
||||
const configForm = ref<TriggerSetting>({
|
||||
type: TriggerTypeEnum.HTTP_REQUEST,
|
||||
httpRequestSetting: {
|
||||
url: '',
|
||||
header: [],
|
||||
body: [],
|
||||
response: [],
|
||||
},
|
||||
formSettings: [
|
||||
{
|
||||
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
|
||||
updateFormFields: {},
|
||||
deleteFields: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
// 流程表单字段
|
||||
const formFields = useFormFields();
|
||||
|
||||
// 可选的修改的表单字段
|
||||
const optionalUpdateFormFields = computed(() => {
|
||||
return formFields.map((field) => ({
|
||||
title: field.title,
|
||||
field: field.field,
|
||||
disabled: false,
|
||||
}));
|
||||
});
|
||||
|
||||
let originalSetting: TriggerSetting | undefined;
|
||||
|
||||
/** 触发器类型改变了 */
|
||||
function changeTriggerType() {
|
||||
if (configForm.value.type === TriggerTypeEnum.HTTP_REQUEST) {
|
||||
configForm.value.httpRequestSetting =
|
||||
originalSetting?.type === TriggerTypeEnum.HTTP_REQUEST &&
|
||||
originalSetting.httpRequestSetting
|
||||
? originalSetting.httpRequestSetting
|
||||
: {
|
||||
url: '',
|
||||
header: [],
|
||||
body: [],
|
||||
response: [],
|
||||
};
|
||||
configForm.value.formSettings = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (configForm.value.type === TriggerTypeEnum.HTTP_CALLBACK) {
|
||||
configForm.value.httpRequestSetting =
|
||||
originalSetting?.type === TriggerTypeEnum.HTTP_CALLBACK &&
|
||||
originalSetting.httpRequestSetting
|
||||
? originalSetting.httpRequestSetting
|
||||
: {
|
||||
url: '',
|
||||
header: [],
|
||||
body: [],
|
||||
response: [],
|
||||
};
|
||||
configForm.value.formSettings = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (configForm.value.type === TriggerTypeEnum.FORM_UPDATE) {
|
||||
configForm.value.formSettings =
|
||||
originalSetting?.type === TriggerTypeEnum.FORM_UPDATE &&
|
||||
originalSetting.formSettings
|
||||
? originalSetting.formSettings
|
||||
: [
|
||||
{
|
||||
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
|
||||
updateFormFields: {},
|
||||
deleteFields: [],
|
||||
},
|
||||
];
|
||||
configForm.value.httpRequestSetting = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (configForm.value.type === TriggerTypeEnum.FORM_DELETE) {
|
||||
configForm.value.formSettings =
|
||||
originalSetting?.type === TriggerTypeEnum.FORM_DELETE &&
|
||||
originalSetting.formSettings
|
||||
? originalSetting.formSettings
|
||||
: [
|
||||
{
|
||||
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
|
||||
updateFormFields: undefined,
|
||||
deleteFields: [],
|
||||
},
|
||||
];
|
||||
configForm.value.httpRequestSetting = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** 添加新的修改表单设置 */
|
||||
function addFormSetting() {
|
||||
configForm.value.formSettings!.push({
|
||||
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
|
||||
updateFormFields: {},
|
||||
deleteFields: [],
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除修改表单设置 */
|
||||
function deleteFormSetting(index: number) {
|
||||
configForm.value.formSettings!.splice(index, 1);
|
||||
}
|
||||
|
||||
/** 添加条件配置 */
|
||||
function addFormSettingCondition(
|
||||
index: number,
|
||||
formSetting: FormTriggerSetting,
|
||||
) {
|
||||
const conditionDialog = proxy.$refs[`condition-${index}`][0];
|
||||
// TODO: jason Modal 使用 useVbenModal 初始化,弹出使用modalApi.setData(formSetting).open()
|
||||
conditionDialog.open(formSetting);
|
||||
}
|
||||
|
||||
/** 删除条件配置 */
|
||||
function deleteFormSettingCondition(formSetting: FormTriggerSetting) {
|
||||
formSetting.conditionType = undefined;
|
||||
}
|
||||
|
||||
/** 打开条件配置弹窗 */
|
||||
function openFormSettingCondition(
|
||||
index: number,
|
||||
formSetting: FormTriggerSetting,
|
||||
) {
|
||||
const conditionDialog = proxy.$refs[`condition-${index}`][0];
|
||||
conditionDialog.open(formSetting);
|
||||
}
|
||||
|
||||
/** 处理条件配置保存 */
|
||||
function handleConditionUpdate(index: number, condition: any) {
|
||||
if (configForm.value.formSettings![index]) {
|
||||
configForm.value.formSettings![index].conditionType =
|
||||
condition.conditionType;
|
||||
configForm.value.formSettings![index].conditionExpression =
|
||||
condition.conditionExpression;
|
||||
configForm.value.formSettings![index].conditionGroups =
|
||||
condition.conditionGroups;
|
||||
}
|
||||
}
|
||||
// 包含发起人字段的表单字段
|
||||
const includeStartUserFormFields = useFormFieldsAndStartUser();
|
||||
/** 条件配置展示 */
|
||||
function showConditionText(formSetting: FormTriggerSetting) {
|
||||
return getConditionShowText(
|
||||
formSetting.conditionType,
|
||||
formSetting.conditionExpression,
|
||||
formSetting.conditionGroups,
|
||||
includeStartUserFormFields,
|
||||
);
|
||||
}
|
||||
|
||||
/** 添加修改字段设置项 */
|
||||
function addFormFieldSetting(formSetting: FormTriggerSetting) {
|
||||
if (!formSetting) return;
|
||||
if (!formSetting.updateFormFields) {
|
||||
formSetting.updateFormFields = {};
|
||||
}
|
||||
formSetting.updateFormFields[''] = undefined;
|
||||
}
|
||||
|
||||
/** 更新字段 KEY */
|
||||
function updateFormFieldKey(
|
||||
formSetting: FormTriggerSetting,
|
||||
oldKey: string,
|
||||
newKey: SelectValue,
|
||||
) {
|
||||
if (!formSetting?.updateFormFields || !newKey) return;
|
||||
const value = formSetting.updateFormFields[oldKey];
|
||||
delete formSetting.updateFormFields[oldKey];
|
||||
formSetting.updateFormFields[String(newKey)] = value;
|
||||
}
|
||||
|
||||
/** 删除修改字段设置项 */
|
||||
function deleteFormFieldSetting(formSetting: FormTriggerSetting, key: string) {
|
||||
if (!formSetting?.updateFormFields) return;
|
||||
delete formSetting.updateFormFields[key];
|
||||
}
|
||||
|
||||
/** 保存配置 */
|
||||
async function saveConfig() {
|
||||
if (!formRef.value) return false;
|
||||
const valid = await formRef.value.validate();
|
||||
if (!valid) return false;
|
||||
const showText = getShowText();
|
||||
if (!showText) return false;
|
||||
currentNode.value.name = nodeName.value!;
|
||||
currentNode.value.showText = showText;
|
||||
switch (configForm.value.type) {
|
||||
case TriggerTypeEnum.FORM_DELETE: {
|
||||
configForm.value.httpRequestSetting = undefined;
|
||||
// 清理修改字段相关的数据
|
||||
configForm.value.formSettings?.forEach((setting) => {
|
||||
setting.updateFormFields = undefined;
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case TriggerTypeEnum.FORM_UPDATE: {
|
||||
configForm.value.httpRequestSetting = undefined;
|
||||
// 清理删除字段相关的数据
|
||||
configForm.value.formSettings?.forEach((setting) => {
|
||||
setting.deleteFields = undefined;
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case TriggerTypeEnum.HTTP_REQUEST: {
|
||||
configForm.value.formSettings = undefined;
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
currentNode.value.triggerSetting = configForm.value;
|
||||
drawerApi.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 获取节点展示内容 */
|
||||
function getShowText(): string {
|
||||
let showText = '';
|
||||
switch (configForm.value.type) {
|
||||
case TriggerTypeEnum.FORM_DELETE: {
|
||||
for (const [index, setting] of configForm.value.formSettings!.entries()) {
|
||||
if (!setting.deleteFields || setting.deleteFields.length === 0) {
|
||||
message.warning(`请选择表单设置${index + 1}要删除的字段`);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
showText = '删除表单数据';
|
||||
|
||||
break;
|
||||
}
|
||||
case TriggerTypeEnum.FORM_UPDATE: {
|
||||
for (const [index, setting] of configForm.value.formSettings!.entries()) {
|
||||
if (
|
||||
!setting.updateFormFields ||
|
||||
Object.keys(setting.updateFormFields).length === 0
|
||||
) {
|
||||
message.warning(`请添加表单设置${index + 1}的修改字段`);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
showText = '修改表单数据';
|
||||
|
||||
break;
|
||||
}
|
||||
case TriggerTypeEnum.HTTP_CALLBACK:
|
||||
case TriggerTypeEnum.HTTP_REQUEST: {
|
||||
showText = `${configForm.value.httpRequestSetting?.url}`;
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
return showText;
|
||||
}
|
||||
|
||||
/** 显示触发器节点配置, 由父组件传过来 */
|
||||
function showTriggerNodeConfig(node: SimpleFlowNode) {
|
||||
nodeName.value = node.name;
|
||||
originalSetting = node.triggerSetting
|
||||
? cloneDeep(node.triggerSetting)
|
||||
: undefined;
|
||||
if (node.triggerSetting) {
|
||||
configForm.value = {
|
||||
type: node.triggerSetting.type,
|
||||
httpRequestSetting: node.triggerSetting.httpRequestSetting || {
|
||||
url: '',
|
||||
header: [],
|
||||
body: [],
|
||||
response: [],
|
||||
},
|
||||
formSettings: node.triggerSetting.formSettings || [
|
||||
{
|
||||
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
|
||||
updateFormFields: {},
|
||||
deleteFields: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({ showTriggerNodeConfig });
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化可能需要的操作
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Drawer class="w-[580px]">
|
||||
<template #title>
|
||||
<div class="config-header">
|
||||
<Input
|
||||
v-if="showInput"
|
||||
type="text"
|
||||
class="config-editable-input"
|
||||
@blur="blurEvent()"
|
||||
v-model:value="nodeName"
|
||||
:placeholder="nodeName"
|
||||
/>
|
||||
<div v-else class="node-name">
|
||||
{{ nodeName }}
|
||||
<IconifyIcon class="ml-1" icon="ep:edit-pen" @click="clickIcon()" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="configForm"
|
||||
:label-col="{ span: 24 }"
|
||||
:wrapper-col="{ span: 24 }"
|
||||
:rules="formRules"
|
||||
>
|
||||
<FormItem label="触发器类型" name="type">
|
||||
<Select v-model:value="configForm.type" @change="changeTriggerType">
|
||||
<SelectOption
|
||||
v-for="(item, index) in TRIGGER_TYPES"
|
||||
:key="index"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
>
|
||||
{{ item.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<!-- HTTP 请求触发器 -->
|
||||
<div
|
||||
v-if="
|
||||
[
|
||||
TriggerTypeEnum.HTTP_REQUEST,
|
||||
TriggerTypeEnum.HTTP_CALLBACK,
|
||||
].includes(configForm.type) && configForm.httpRequestSetting
|
||||
"
|
||||
>
|
||||
<HttpRequestSetting
|
||||
v-model:setting="configForm.httpRequestSetting"
|
||||
:response-enable="configForm.type === TriggerTypeEnum.HTTP_REQUEST"
|
||||
form-item-prefix="httpRequestSetting"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 表单数据修改触发器 -->
|
||||
<div v-if="configForm.type === TriggerTypeEnum.FORM_UPDATE">
|
||||
<div
|
||||
v-for="(formSetting, index) in configForm.formSettings"
|
||||
:key="index"
|
||||
>
|
||||
<Card class="mt-4">
|
||||
<template #title>
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<span>修改表单设置 {{ index + 1 }}</span>
|
||||
<Button
|
||||
v-if="configForm.formSettings!.length > 1"
|
||||
shape="circle"
|
||||
class="flex items-center justify-center"
|
||||
@click="deleteFormSetting(index)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:close" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ConditionDialog
|
||||
:ref="`condition-${index}`"
|
||||
@update-condition="(val) => handleConditionUpdate(index, val)"
|
||||
/>
|
||||
<Row>
|
||||
<Col :span="24">
|
||||
<div class="cursor-pointer" v-if="formSetting.conditionType">
|
||||
<Tag
|
||||
color="success"
|
||||
closable
|
||||
class="text-sm"
|
||||
@close="deleteFormSettingCondition(formSetting)"
|
||||
@click="openFormSettingCondition(index, formSetting)"
|
||||
>
|
||||
{{ showConditionText(formSetting) }}
|
||||
</Tag>
|
||||
</div>
|
||||
<Button
|
||||
v-else
|
||||
type="link"
|
||||
class="flex items-center p-0"
|
||||
@click="addFormSettingCondition(index, formSetting)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:link" />
|
||||
</template>
|
||||
添加条件
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider>修改表单字段设置</Divider>
|
||||
<!-- 表单字段修改设置 -->
|
||||
<Row
|
||||
:gutter="8"
|
||||
v-for="key in Object.keys(formSetting.updateFormFields || {})"
|
||||
:key="key"
|
||||
>
|
||||
<Col :span="8">
|
||||
<FormItem>
|
||||
<Select
|
||||
:value="key || undefined"
|
||||
@change="
|
||||
(newKey) => updateFormFieldKey(formSetting, key, newKey)
|
||||
"
|
||||
placeholder="请选择表单字段"
|
||||
:disabled="key !== ''"
|
||||
allow-clear
|
||||
>
|
||||
<SelectOption
|
||||
v-for="(field, fIdx) in optionalUpdateFormFields"
|
||||
:key="fIdx"
|
||||
:label="field.title"
|
||||
:value="field.field"
|
||||
:disabled="field.disabled"
|
||||
>
|
||||
{{ field.title }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<FormItem>的值设置为</FormItem>
|
||||
</Col>
|
||||
<Col :span="10">
|
||||
<FormItem
|
||||
:name="['formSettings', index, 'updateFormFields', key]"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '值不能为空',
|
||||
trigger: 'blur',
|
||||
}"
|
||||
>
|
||||
<Input
|
||||
v-model:value="formSetting.updateFormFields![key]"
|
||||
placeholder="请输入值"
|
||||
allow-clear
|
||||
:disabled="!key"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="2">
|
||||
<div class="flex h-[32px] items-center">
|
||||
<Trash2
|
||||
class="size-4 cursor-pointer text-red-500"
|
||||
@click="deleteFormFieldSetting(formSetting, key)"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 添加表单字段按钮 -->
|
||||
<Row>
|
||||
<Col :span="24">
|
||||
<Button
|
||||
type="link"
|
||||
class="flex items-center p-0"
|
||||
@click="addFormFieldSetting(formSetting)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:memo" />
|
||||
</template>
|
||||
添加修改字段
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 添加新的设置 -->
|
||||
<Row class="mt-6">
|
||||
<Col :span="24">
|
||||
<Button
|
||||
class="flex items-center p-0"
|
||||
type="link"
|
||||
@click="addFormSetting"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:setting" />
|
||||
</template>
|
||||
添加设置
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<!-- 表单数据删除触发器 -->
|
||||
<div v-if="configForm.type === TriggerTypeEnum.FORM_DELETE">
|
||||
<div
|
||||
v-for="(formSetting, index) in configForm.formSettings"
|
||||
:key="index"
|
||||
>
|
||||
<Card class="mt-4">
|
||||
<template #title>
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<span>删除表单设置 {{ index + 1 }}</span>
|
||||
<Button
|
||||
v-if="configForm.formSettings!.length > 1"
|
||||
shape="circle"
|
||||
class="flex items-center justify-center"
|
||||
@click="deleteFormSetting(index)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:close" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 条件设置 -->
|
||||
<ConditionDialog
|
||||
:ref="`condition-${index}`"
|
||||
@update-condition="(val) => handleConditionUpdate(index, val)"
|
||||
/>
|
||||
<Row>
|
||||
<Col :span="24">
|
||||
<div class="cursor-pointer" v-if="formSetting.conditionType">
|
||||
<Tag
|
||||
color="success"
|
||||
closable
|
||||
class="text-sm"
|
||||
@close="deleteFormSettingCondition(formSetting)"
|
||||
@click="openFormSettingCondition(index, formSetting)"
|
||||
>
|
||||
{{ showConditionText(formSetting) }}
|
||||
</Tag>
|
||||
</div>
|
||||
<Button
|
||||
v-else
|
||||
type="link"
|
||||
class="flex items-center p-0"
|
||||
@click="addFormSettingCondition(index, formSetting)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:link" />
|
||||
</template>
|
||||
添加条件
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider>删除表单字段设置</Divider>
|
||||
<!-- 表单字段删除设置 -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Select
|
||||
v-model:value="formSetting.deleteFields"
|
||||
mode="multiple"
|
||||
placeholder="请选择要删除的字段"
|
||||
class="w-full"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="field in formFields"
|
||||
:key="field.field"
|
||||
:label="field.title"
|
||||
:value="field.field"
|
||||
>
|
||||
{{ field.title }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 添加新的设置 -->
|
||||
<Row class="mt-6">
|
||||
<Col :span="24">
|
||||
<Button
|
||||
class="flex items-center p-0"
|
||||
type="link"
|
||||
@click="addFormSetting"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:setting" />
|
||||
</template>
|
||||
添加设置
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Drawer>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.config-editable-input {
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-color: #40a9ff;
|
||||
box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -34,7 +34,8 @@ import {
|
|||
TypographyText,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
// TODO import { defaultProps4AntTree } from '#/utils/tree';
|
||||
import { BpmModelFormType } from '#/utils';
|
||||
|
||||
import {
|
||||
APPROVE_METHODS,
|
||||
APPROVE_TYPE,
|
||||
|
@ -65,20 +66,22 @@ import {
|
|||
useNodeName,
|
||||
useWatchNode,
|
||||
} from '../../helpers';
|
||||
import UserTaskListener from './modules/user-task-listener.vue';
|
||||
import { convertTimeUnit, getApproveTypeText } from './utils';
|
||||
|
||||
// TODO import UserTaskListener from './components/UserTaskListener.vue';
|
||||
|
||||
defineOptions({ name: 'UserTaskNodeConfig' });
|
||||
|
||||
const props = defineProps({
|
||||
flowNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits<{
|
||||
findReturnTaskNodes: [nodeList: SimpleFlowNode[]];
|
||||
}>();
|
||||
|
||||
const deptLevelLabel = computed(() => {
|
||||
let label = '部门负责人来源';
|
||||
if (
|
||||
|
@ -95,6 +98,7 @@ const deptLevelLabel = computed(() => {
|
|||
}
|
||||
return label;
|
||||
});
|
||||
|
||||
// 监控节点的变化
|
||||
const currentNode = useWatchNode(props);
|
||||
// 抽屉配置
|
||||
|
@ -102,19 +106,19 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
|||
header: true,
|
||||
closable: true,
|
||||
title: '',
|
||||
onCancel() {
|
||||
drawerApi.close();
|
||||
},
|
||||
onConfirm() {
|
||||
saveConfig();
|
||||
},
|
||||
});
|
||||
|
||||
// 节点名称配置
|
||||
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(
|
||||
NodeType.USER_TASK_NODE,
|
||||
);
|
||||
|
||||
// 激活的 Tab 标签页
|
||||
const activeTabName = ref('user');
|
||||
|
||||
// 表单字段权限设置
|
||||
const {
|
||||
formType,
|
||||
|
@ -122,10 +126,12 @@ const {
|
|||
formFieldOptions,
|
||||
getNodeConfigFormFields,
|
||||
} = useFormFieldsPermission(FieldPermissionType.READ);
|
||||
|
||||
// 表单内用户字段选项, 必须是必填和用户选择器
|
||||
const userFieldOnFormOptions = computed(() => {
|
||||
return formFieldOptions.filter((item) => item.type === 'UserSelect');
|
||||
});
|
||||
|
||||
// 表单内部门字段选项, 必须是必填和部门选择器
|
||||
const deptFieldOnFormOptions = computed(() => {
|
||||
return formFieldOptions.filter((item) => item.type === 'DeptSelect');
|
||||
|
@ -138,7 +144,9 @@ const {
|
|||
changeBtnDisplayName,
|
||||
btnDisplayNameBlurEvent,
|
||||
} = useButtonsSetting();
|
||||
|
||||
const approveType = ref(ApproveType.USER);
|
||||
|
||||
// 审批人表单设置
|
||||
const formRef = ref(); // 表单 Ref
|
||||
// 表单校验规则
|
||||
|
@ -200,7 +208,7 @@ const {
|
|||
const configForm = tempConfigForm as Ref<UserTaskFormType>;
|
||||
|
||||
// 改变审批人设置策略
|
||||
const changeCandidateStrategy = () => {
|
||||
function changeCandidateStrategy() {
|
||||
configForm.value.userIds = [];
|
||||
configForm.value.deptIds = [];
|
||||
configForm.value.roleIds = [];
|
||||
|
@ -210,16 +218,16 @@ const changeCandidateStrategy = () => {
|
|||
configForm.value.formUser = '';
|
||||
configForm.value.formDept = '';
|
||||
configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE;
|
||||
};
|
||||
}
|
||||
|
||||
// 审批方式改变
|
||||
const approveMethodChanged = () => {
|
||||
/** 审批方式改变 */
|
||||
function approveMethodChanged() {
|
||||
configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS;
|
||||
if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) {
|
||||
configForm.value.approveRatio = 100;
|
||||
}
|
||||
formRef.value.clearValidate('approveRatio');
|
||||
};
|
||||
}
|
||||
// 审批拒绝 可退回的节点
|
||||
const returnTaskList = ref<SimpleFlowNode[]>([]);
|
||||
// 审批人超时未处理设置
|
||||
|
@ -233,49 +241,59 @@ const {
|
|||
cTimeoutMaxRemindCount,
|
||||
} = useTimeoutHandler();
|
||||
|
||||
// TODO 监听器待实现
|
||||
// const userTaskListenerRef = ref();
|
||||
const userTaskListenerRef = ref();
|
||||
|
||||
/** 节点类型名称 */
|
||||
const nodeTypeName = computed(() => {
|
||||
return currentNode.value.type === NodeType.TRANSACTOR_NODE ? '办理' : '审批';
|
||||
});
|
||||
|
||||
/** 保存配置 */
|
||||
const saveConfig = async () => {
|
||||
// 设置审批节点名称
|
||||
currentNode.value.name = nodeName.value!;
|
||||
// 设置审批类型
|
||||
currentNode.value.approveType = approveType.value;
|
||||
// 如果不是人工审批。返回
|
||||
if (approveType.value !== ApproveType.USER) {
|
||||
currentNode.value.showText = getApproveTypeText(approveType.value);
|
||||
drawerApi.close();
|
||||
return true;
|
||||
}
|
||||
// TODO 监听器待实现
|
||||
// activeTabName.value = 'listener';
|
||||
// await nextTick();
|
||||
activeTabName.value = 'user';
|
||||
|
||||
/** 校验节点配置 */
|
||||
async function validateConfig() {
|
||||
if (!formRef.value) return false;
|
||||
// TODO 监听器待实现
|
||||
// if (!userTaskListenerRef.value) return false;
|
||||
// const valid =
|
||||
// (await formRef.value.validate()) &&
|
||||
// (await userTaskListenerRef.value.validate());
|
||||
if (!userTaskListenerRef.value) return false;
|
||||
|
||||
if (!(await formRef.value.validate())) {
|
||||
activeTabName.value = 'user';
|
||||
// 先进行表单验证,记录验证结果
|
||||
const userFormValid = await formRef.value.validate().catch(() => false);
|
||||
const listenerValid = await userTaskListenerRef.value.validate().catch(() => {
|
||||
return false;
|
||||
});
|
||||
// 如果监听器有错误,切换到监听器Tab
|
||||
if (!listenerValid) {
|
||||
activeTabName.value = 'listener';
|
||||
return false;
|
||||
}
|
||||
// 如果审批人表单有错误,切换到审批人Tab
|
||||
if (!userFormValid) {
|
||||
activeTabName.value = 'user';
|
||||
return false;
|
||||
}
|
||||
// TODO 监听器待实现
|
||||
// if (!(await userTaskListenerRef.value.validate())) {
|
||||
// activeTabName.value = 'listener';
|
||||
// }
|
||||
|
||||
const showText = getShowText();
|
||||
if (!showText) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 保存配置 */
|
||||
async function saveConfig() {
|
||||
// 如果不是人工审批,不执行校验,直接返回
|
||||
if (approveType.value !== ApproveType.USER) {
|
||||
currentNode.value.name = nodeName.value!;
|
||||
currentNode.value.approveType = approveType.value;
|
||||
currentNode.value.showText = getApproveTypeText(approveType.value);
|
||||
drawerApi.close();
|
||||
return true;
|
||||
}
|
||||
// 执行校验
|
||||
if (!(await validateConfig())) {
|
||||
return false;
|
||||
}
|
||||
// 设置审批节点名称
|
||||
currentNode.value.name = nodeName.value!;
|
||||
// 设置审批类型
|
||||
currentNode.value.approveType = approveType.value;
|
||||
// 设置审批人设置策略
|
||||
currentNode.value.candidateStrategy = configForm.value.candidateStrategy;
|
||||
// 处理 candidateParam 参数
|
||||
currentNode.value.candidateParam = handleCandidateParam();
|
||||
|
@ -338,13 +356,13 @@ const saveConfig = async () => {
|
|||
// 审批意见
|
||||
currentNode.value.reasonRequire = configForm.value.reasonRequire;
|
||||
|
||||
currentNode.value.showText = showText;
|
||||
currentNode.value.showText = getShowText();
|
||||
drawerApi.close();
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/** 显示审批节点配置, 由父组件传过来 */
|
||||
const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
|
||||
function showUserTaskNodeConfig(node: SimpleFlowNode) {
|
||||
nodeName.value = node.name;
|
||||
// 1 审批类型
|
||||
approveType.value =
|
||||
|
@ -423,7 +441,7 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
|
|||
configForm.value.reasonRequire = node?.reasonRequire ?? false;
|
||||
|
||||
drawerApi.open();
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({ showUserTaskNodeConfig }); // 暴露方法给父组件
|
||||
|
||||
|
@ -535,7 +553,7 @@ function useTimeoutHandler() {
|
|||
}
|
||||
|
||||
/** 批量更新权限 */
|
||||
const updatePermission = (type: string) => {
|
||||
function updatePermission(type: string) {
|
||||
fieldsPermissionConfig.value.forEach((field) => {
|
||||
if (type === 'READ') {
|
||||
field.permission = FieldPermissionType.READ;
|
||||
|
@ -545,7 +563,8 @@ const updatePermission = (type: string) => {
|
|||
field.permission = FieldPermissionType.NONE;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// 在组件初始化时记录初始位置
|
||||
onMounted(() => {
|
||||
// 固定添加发起人ID字段
|
||||
|
@ -630,11 +649,9 @@ onMounted(() => {
|
|||
name="roleIds"
|
||||
>
|
||||
<Select
|
||||
filterable
|
||||
v-model:value="configForm.roleIds"
|
||||
clearable
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="item in roleOptions"
|
||||
|
@ -658,16 +675,18 @@ onMounted(() => {
|
|||
label="指定部门"
|
||||
name="deptIds"
|
||||
>
|
||||
<!-- TODO :replace-fields="defaultProps4AntTree" -->
|
||||
<TreeSelect
|
||||
v-model:value="configForm.deptIds"
|
||||
:tree-data="deptTreeOptions"
|
||||
:field-names="{
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
children: 'children',
|
||||
}"
|
||||
empty-text="加载中,请稍后"
|
||||
multiple
|
||||
node-key="id"
|
||||
:check-strictly="true"
|
||||
allow-clear
|
||||
style="width: 100%"
|
||||
tree-checkable
|
||||
/>
|
||||
</FormItem>
|
||||
|
@ -677,11 +696,9 @@ onMounted(() => {
|
|||
name="postIds"
|
||||
>
|
||||
<Select
|
||||
filterable
|
||||
v-model:value="configForm.postIds"
|
||||
clearable
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="item in postOptions"
|
||||
|
@ -699,11 +716,9 @@ onMounted(() => {
|
|||
name="userIds"
|
||||
>
|
||||
<Select
|
||||
filterable
|
||||
v-model:value="configForm.userIds"
|
||||
clearable
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="item in userOptions"
|
||||
|
@ -723,11 +738,9 @@ onMounted(() => {
|
|||
name="userGroups"
|
||||
>
|
||||
<Select
|
||||
filterable
|
||||
v-model:value="configForm.userGroups"
|
||||
clearable
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="item in userGroupOptions"
|
||||
|
@ -746,12 +759,7 @@ onMounted(() => {
|
|||
label="表单内用户字段"
|
||||
name="formUser"
|
||||
>
|
||||
<Select
|
||||
filterable
|
||||
v-model:value="configForm.formUser"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<Select v-model:value="configForm.formUser" clearable>
|
||||
<SelectOption
|
||||
v-for="(item, idx) in userFieldOnFormOptions"
|
||||
:key="idx"
|
||||
|
@ -771,12 +779,7 @@ onMounted(() => {
|
|||
label="表单内部门字段"
|
||||
name="formDept"
|
||||
>
|
||||
<Select
|
||||
filterable
|
||||
v-model:value="configForm.formDept"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<Select v-model:value="configForm.formDept" clearable>
|
||||
<SelectOption
|
||||
v-for="(item, idx) in deptFieldOnFormOptions"
|
||||
:key="idx"
|
||||
|
@ -802,7 +805,7 @@ onMounted(() => {
|
|||
:label="deptLevelLabel!"
|
||||
name="deptLevel"
|
||||
>
|
||||
<Select filterable v-model:value="configForm.deptLevel" clearable>
|
||||
<Select v-model:value="configForm.deptLevel" clearable>
|
||||
<SelectOption
|
||||
v-for="(item, index) in MULTI_LEVEL_DEPT"
|
||||
:key="index"
|
||||
|
@ -821,11 +824,7 @@ onMounted(() => {
|
|||
label="流程表达式"
|
||||
name="expression"
|
||||
>
|
||||
<Textarea
|
||||
v-model:value="configForm.expression"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
<Textarea v-model:value="configForm.expression" clearable />
|
||||
</FormItem>
|
||||
<!-- 多人审批/办理 方式 -->
|
||||
<FormItem :label="`多人${nodeTypeName}方式`" name="approveMethod">
|
||||
|
@ -890,12 +889,7 @@ onMounted(() => {
|
|||
label="驳回节点"
|
||||
name="returnNodeId"
|
||||
>
|
||||
<Select
|
||||
filterable
|
||||
v-model:value="configForm.returnNodeId"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<Select v-model:value="configForm.returnNodeId" clearable>
|
||||
<SelectOption
|
||||
v-for="item in returnTaskList"
|
||||
:key="item.id"
|
||||
|
@ -963,8 +957,7 @@ onMounted(() => {
|
|||
<Col>
|
||||
<FormItem name="timeDuration">
|
||||
<InputNumber
|
||||
class="mr-2"
|
||||
:style="{ width: '100px' }"
|
||||
class="mr-2 mt-0.5"
|
||||
v-model:value="configForm.timeDuration"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
|
@ -973,7 +966,6 @@ onMounted(() => {
|
|||
</Col>
|
||||
<Col>
|
||||
<Select
|
||||
filterable
|
||||
v-model:value="timeUnit"
|
||||
class="mr-2"
|
||||
:style="{ width: '100px' }"
|
||||
|
@ -1040,11 +1032,9 @@ onMounted(() => {
|
|||
name="assignEmptyHandlerUserIds"
|
||||
>
|
||||
<Select
|
||||
filterable
|
||||
v-model:value="configForm.assignEmptyHandlerUserIds"
|
||||
clearable
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="item in userOptions"
|
||||
|
@ -1151,7 +1141,11 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="表单字段权限" key="fields" v-if="formType === 10">
|
||||
<TabPane
|
||||
tab="表单字段权限"
|
||||
key="fields"
|
||||
v-if="formType === BpmModelFormType.NORMAL"
|
||||
>
|
||||
<div class="p-1">
|
||||
<div class="mb-4 text-[16px] font-bold">字段权限</div>
|
||||
|
||||
|
@ -1225,20 +1219,14 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="监听器" key="listener">
|
||||
<!-- TODO 待实现 -->
|
||||
<span>待实现</span>
|
||||
<!-- <UserTaskListener
|
||||
<TabPane tab="监听器" key="listener" :force-render="true">
|
||||
<UserTaskListener
|
||||
ref="userTaskListenerRef"
|
||||
v-model:value="configForm"
|
||||
v-model="configForm"
|
||||
:form-field-options="formFieldOptions"
|
||||
/> -->
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<template #footer>
|
||||
<Button type="primary" @click="saveConfig">确 定</Button>
|
||||
<Button @click="drawerApi.close()">取 消</Button>
|
||||
</template>
|
||||
</Drawer>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
import { APPROVE_TYPE, ApproveType, TimeUnitType } from '../../consts';
|
||||
|
||||
/** 获取条件节点默认的名称 */
|
||||
export const getDefaultConditionNodeName = (
|
||||
export function getDefaultConditionNodeName(
|
||||
index: number,
|
||||
defaultFlow: boolean | undefined,
|
||||
): string => {
|
||||
): string {
|
||||
if (defaultFlow) {
|
||||
return '其它情况';
|
||||
}
|
||||
return `条件${index + 1}`;
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取包容分支条件节点默认的名称 */
|
||||
export const getDefaultInclusiveConditionNodeName = (
|
||||
export function getDefaultInclusiveConditionNodeName(
|
||||
index: number,
|
||||
defaultFlow: boolean | undefined,
|
||||
): string => {
|
||||
): string {
|
||||
if (defaultFlow) {
|
||||
return '其它情况';
|
||||
}
|
||||
return `包容条件${index + 1}`;
|
||||
};
|
||||
}
|
||||
|
||||
/** 转换时间单位字符串为枚举值 */
|
||||
export const convertTimeUnit = (strTimeUnit: string) => {
|
||||
export function convertTimeUnit(strTimeUnit: string) {
|
||||
if (strTimeUnit === 'M') {
|
||||
return TimeUnitType.MINUTE;
|
||||
}
|
||||
|
@ -34,10 +34,10 @@ export const convertTimeUnit = (strTimeUnit: string) => {
|
|||
return TimeUnitType.DAY;
|
||||
}
|
||||
return TimeUnitType.HOUR;
|
||||
};
|
||||
}
|
||||
|
||||
/** 根据审批类型获取对应的文本描述 */
|
||||
export const getApproveTypeText = (approveType: ApproveType): string => {
|
||||
export function getApproveTypeText(approveType: ApproveType): string {
|
||||
let approveTypeText = '';
|
||||
APPROVE_TYPE.forEach((item) => {
|
||||
if (item.value === approveType) {
|
||||
|
@ -45,4 +45,4 @@ export const getApproveTypeText = (approveType: ApproveType): string => {
|
|||
}
|
||||
});
|
||||
return approveTypeText;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
<script setup lang="ts">
|
||||
import type { SimpleFlowNode } from '../../consts';
|
||||
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Input } from 'ant-design-vue';
|
||||
|
||||
import { NODE_DEFAULT_TEXT, NodeType } from '../../consts';
|
||||
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
|
||||
import CopyTaskNodeConfig from '../nodes-config/copy-task-node-config.vue';
|
||||
import NodeHandler from './node-handler.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'CopyTaskNode',
|
||||
});
|
||||
const props = defineProps({
|
||||
flowNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
// 定义事件,更新父组件。
|
||||
const emits = defineEmits<{
|
||||
'update:flowNode': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
// 监控节点的变化
|
||||
const currentNode = useWatchNode(props);
|
||||
// 节点名称编辑
|
||||
const { showInput, blurEvent, clickTitle } = useNodeName2(
|
||||
currentNode,
|
||||
NodeType.COPY_TASK_NODE,
|
||||
);
|
||||
|
||||
const nodeSetting = ref();
|
||||
// 打开节点配置
|
||||
function openNodeConfig() {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
nodeSetting.value.showCopyTaskNodeConfig(currentNode.value);
|
||||
nodeSetting.value.openDrawer();
|
||||
}
|
||||
|
||||
// 删除节点。更新当前节点为孩子节点
|
||||
function deleteNode() {
|
||||
emits('update:flowNode', currentNode.value.childNode);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="node-wrapper">
|
||||
<div class="node-container">
|
||||
<div
|
||||
class="node-box"
|
||||
:class="[
|
||||
{ 'node-config-error': !currentNode.showText },
|
||||
`${useTaskStatusClass(currentNode?.activityStatus)}`,
|
||||
]"
|
||||
>
|
||||
<div class="node-title-container">
|
||||
<div class="node-title-icon copy-task">
|
||||
<span class="iconfont icon-copy"></span>
|
||||
</div>
|
||||
<Input
|
||||
v-if="!readonly && showInput"
|
||||
type="text"
|
||||
class="editable-title-input"
|
||||
@blur="blurEvent()"
|
||||
v-model="currentNode.name"
|
||||
:placeholder="currentNode.name"
|
||||
/>
|
||||
<div v-else class="node-title" @click="clickTitle">
|
||||
{{ currentNode.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-content" @click="openNodeConfig">
|
||||
<div
|
||||
class="node-text"
|
||||
:title="currentNode.showText"
|
||||
v-if="currentNode.showText"
|
||||
>
|
||||
{{ currentNode.showText }}
|
||||
</div>
|
||||
<div class="node-text" v-else>
|
||||
{{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }}
|
||||
</div>
|
||||
<IconifyIcon v-if="!readonly" icon="ep:arrow-right-bold" />
|
||||
</div>
|
||||
<div v-if="!readonly" class="node-toolbar">
|
||||
<div class="toolbar-icon">
|
||||
<IconifyIcon
|
||||
color="#0089ff"
|
||||
icon="ep:circle-close-filled"
|
||||
:size="18"
|
||||
@click="deleteNode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
|
||||
<NodeHandler
|
||||
v-if="currentNode"
|
||||
v-model:child-node="currentNode.childNode"
|
||||
:current-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
<CopyTaskNodeConfig
|
||||
v-if="!readonly && currentNode"
|
||||
ref="nodeSetting"
|
||||
:flow-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,114 @@
|
|||
<script setup lang="ts">
|
||||
import type { SimpleFlowNode } from '../../consts';
|
||||
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Input } from 'ant-design-vue';
|
||||
|
||||
import { NODE_DEFAULT_TEXT, NodeType } from '../../consts';
|
||||
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
|
||||
import DelayTimerNodeConfig from '../nodes-config/delay-timer-node-config.vue';
|
||||
import NodeHandler from './node-handler.vue';
|
||||
|
||||
defineOptions({ name: 'DelayTimerNode' });
|
||||
const props = defineProps({
|
||||
flowNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
// 定义事件,更新父组件。
|
||||
const emits = defineEmits<{
|
||||
'update:flowNode': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
// 监控节点的变化
|
||||
const currentNode = useWatchNode(props);
|
||||
// 节点名称编辑
|
||||
const { showInput, blurEvent, clickTitle } = useNodeName2(
|
||||
currentNode,
|
||||
NodeType.DELAY_TIMER_NODE,
|
||||
);
|
||||
|
||||
const nodeSetting = ref();
|
||||
// 打开节点配置
|
||||
function openNodeConfig() {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
nodeSetting.value.openDrawer(currentNode.value);
|
||||
}
|
||||
|
||||
// 删除节点。更新当前节点为孩子节点
|
||||
function deleteNode() {
|
||||
emits('update:flowNode', currentNode.value.childNode);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="node-wrapper">
|
||||
<div class="node-container">
|
||||
<div
|
||||
class="node-box"
|
||||
:class="[
|
||||
{ 'node-config-error': !currentNode.showText },
|
||||
`${useTaskStatusClass(currentNode?.activityStatus)}`,
|
||||
]"
|
||||
>
|
||||
<div class="node-title-container">
|
||||
<div class="node-title-icon delay-node">
|
||||
<span class="iconfont icon-delay"></span>
|
||||
</div>
|
||||
<Input
|
||||
v-if="!readonly && showInput"
|
||||
type="text"
|
||||
class="editable-title-input"
|
||||
@blur="blurEvent()"
|
||||
v-model="currentNode.name"
|
||||
:placeholder="currentNode.name"
|
||||
/>
|
||||
<div v-else class="node-title" @click="clickTitle">
|
||||
{{ currentNode.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-content" @click="openNodeConfig">
|
||||
<div
|
||||
class="node-text"
|
||||
:title="currentNode.showText"
|
||||
v-if="currentNode.showText"
|
||||
>
|
||||
{{ currentNode.showText }}
|
||||
</div>
|
||||
<div class="node-text" v-else>
|
||||
{{ NODE_DEFAULT_TEXT.get(NodeType.DELAY_TIMER_NODE) }}
|
||||
</div>
|
||||
<IconifyIcon v-if="!readonly" icon="ep:arrow-right-bold" />
|
||||
</div>
|
||||
<div v-if="!readonly" class="node-toolbar">
|
||||
<div class="toolbar-icon">
|
||||
<IconifyIcon
|
||||
color="#0089ff"
|
||||
icon="ep:circle-close-filled"
|
||||
:size="18"
|
||||
@click="deleteNode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
|
||||
<NodeHandler
|
||||
v-if="currentNode"
|
||||
v-model:child-node="currentNode.childNode"
|
||||
:current-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
<DelayTimerNodeConfig
|
||||
v-if="!readonly && currentNode"
|
||||
ref="nodeSetting"
|
||||
:flow-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -22,7 +22,7 @@ const processInstance = inject<Ref<any>>('processInstance', ref({}));
|
|||
|
||||
const processInstanceInfos = ref<any[]>([]); // 流程的审批信息
|
||||
|
||||
const nodeClick = () => {
|
||||
function nodeClick() {
|
||||
if (readonly && processInstance && processInstance.value) {
|
||||
console.warn(
|
||||
'TODO 只读模式,弹窗显示审批信息',
|
||||
|
@ -30,7 +30,7 @@ const nodeClick = () => {
|
|||
processInstanceInfos.value,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="end-node-wrapper">
|
||||
|
@ -44,4 +44,3 @@ const nodeClick = () => {
|
|||
</div>
|
||||
<!-- TODO 审批信息 -->
|
||||
</template>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -0,0 +1,282 @@
|
|||
<script setup lang="ts">
|
||||
import type { SimpleFlowNode } from '../../consts';
|
||||
|
||||
import { getCurrentInstance, inject, ref, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { cloneDeep, buildShortUUID as generateUUID } from '@vben/utils';
|
||||
|
||||
import { Button, Input } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
ConditionType,
|
||||
DEFAULT_CONDITION_GROUP_VALUE,
|
||||
NODE_DEFAULT_TEXT,
|
||||
NodeType,
|
||||
} from '../../consts';
|
||||
import { getDefaultConditionNodeName, useTaskStatusClass } from '../../helpers';
|
||||
import ConditionNodeConfig from '../nodes-config/condition-node-config.vue';
|
||||
import ProcessNodeTree from '../process-node-tree.vue';
|
||||
import NodeHandler from './node-handler.vue';
|
||||
|
||||
defineOptions({ name: 'ExclusiveNode' });
|
||||
|
||||
const props = defineProps({
|
||||
flowNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 定义事件,更新父组件
|
||||
const emits = defineEmits<{
|
||||
findParentNode: [nodeList: SimpleFlowNode[], nodeType: number];
|
||||
recursiveFindParentNode: [
|
||||
nodeList: SimpleFlowNode[],
|
||||
curentNode: SimpleFlowNode,
|
||||
nodeType: number,
|
||||
];
|
||||
'update:modelValue': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
const currentNode = ref<SimpleFlowNode>(props.flowNode);
|
||||
|
||||
watch(
|
||||
() => props.flowNode,
|
||||
(newValue) => {
|
||||
currentNode.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
const showInputs = ref<boolean[]>([]);
|
||||
|
||||
// 失去焦点
|
||||
function blurEvent(index: number) {
|
||||
showInputs.value[index] = false;
|
||||
const conditionNode = currentNode.value.conditionNodes?.at(
|
||||
index,
|
||||
) as SimpleFlowNode;
|
||||
conditionNode.name =
|
||||
conditionNode.name ||
|
||||
getDefaultConditionNodeName(
|
||||
index,
|
||||
conditionNode.conditionSetting?.defaultFlow,
|
||||
);
|
||||
}
|
||||
|
||||
// 点击条件名称
|
||||
function clickEvent(index: number) {
|
||||
showInputs.value[index] = true;
|
||||
}
|
||||
|
||||
function conditionNodeConfig(nodeId: string) {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
const conditionNode = proxy.$refs[nodeId][0];
|
||||
conditionNode.open();
|
||||
}
|
||||
|
||||
// 新增条件
|
||||
function addCondition() {
|
||||
const conditionNodes = currentNode.value.conditionNodes;
|
||||
if (conditionNodes) {
|
||||
const len = conditionNodes.length;
|
||||
const lastIndex = len - 1;
|
||||
const conditionData: SimpleFlowNode = {
|
||||
id: `Flow_${generateUUID()}`,
|
||||
name: `条件${len}`,
|
||||
showText: '',
|
||||
type: NodeType.CONDITION_NODE,
|
||||
childNode: undefined,
|
||||
conditionNodes: [],
|
||||
conditionSetting: {
|
||||
defaultFlow: false,
|
||||
conditionType: ConditionType.RULE,
|
||||
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
|
||||
},
|
||||
};
|
||||
conditionNodes.splice(lastIndex, 0, conditionData);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除条件
|
||||
function deleteCondition(index: number) {
|
||||
const conditionNodes = currentNode.value.conditionNodes;
|
||||
if (conditionNodes) {
|
||||
conditionNodes.splice(index, 1);
|
||||
if (conditionNodes.length === 1) {
|
||||
const childNode = currentNode.value.childNode;
|
||||
// 更新此节点为后续孩子节点
|
||||
emits('update:modelValue', childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动节点
|
||||
function moveNode(index: number, to: number) {
|
||||
// -1 :向左 1: 向右
|
||||
if (
|
||||
currentNode.value.conditionNodes &&
|
||||
currentNode.value.conditionNodes[index]
|
||||
) {
|
||||
currentNode.value.conditionNodes[index] =
|
||||
currentNode.value.conditionNodes.splice(
|
||||
index + to,
|
||||
1,
|
||||
currentNode.value.conditionNodes[index],
|
||||
)[0] as SimpleFlowNode;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归从父节点中查询匹配的节点
|
||||
function recursiveFindParentNode(
|
||||
nodeList: SimpleFlowNode[],
|
||||
node: SimpleFlowNode,
|
||||
nodeType: number,
|
||||
) {
|
||||
if (!node || node.type === NodeType.START_USER_NODE) {
|
||||
return;
|
||||
}
|
||||
if (node.type === nodeType) {
|
||||
nodeList.push(node);
|
||||
}
|
||||
// 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.EXCLUSIVE_NODE) 继续查找
|
||||
emits('findParentNode', nodeList, nodeType);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="branch-node-wrapper">
|
||||
<div class="branch-node-container">
|
||||
<div
|
||||
v-if="readonly"
|
||||
class="branch-node-readonly"
|
||||
:class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
|
||||
>
|
||||
<span class="iconfont icon-exclusive icon-size condition"></span>
|
||||
</div>
|
||||
<Button v-else class="branch-node-add" @click="addCondition">
|
||||
添加条件
|
||||
</Button>
|
||||
<!-- 排他网关节点下面可以多个分支,每个分支第一个节点是条件节点 NodeType.CONDITION_NODE -->
|
||||
<div
|
||||
class="branch-node-item"
|
||||
v-for="(item, index) in currentNode.conditionNodes"
|
||||
:key="index"
|
||||
>
|
||||
<template v-if="index === 0">
|
||||
<div class="branch-line-first-top"></div>
|
||||
<div class="branch-line-first-bottom"></div>
|
||||
</template>
|
||||
<template v-if="index + 1 === currentNode.conditionNodes?.length">
|
||||
<div class="branch-line-last-top"></div>
|
||||
<div class="branch-line-last-bottom"></div>
|
||||
</template>
|
||||
<div class="node-wrapper">
|
||||
<div class="node-container">
|
||||
<div
|
||||
class="node-box"
|
||||
:class="[
|
||||
{ 'node-config-error': !item.showText },
|
||||
`${useTaskStatusClass(item.activityStatus)}`,
|
||||
]"
|
||||
>
|
||||
<div class="branch-node-title-container">
|
||||
<div v-if="!readonly && showInputs[index]">
|
||||
<Input
|
||||
type="text"
|
||||
class="editable-title-input"
|
||||
@blur="blurEvent(index)"
|
||||
v-model="item.name"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="branch-title" @click="clickEvent(index)">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="branch-priority">优先级{{ index + 1 }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="branch-node-content"
|
||||
@click="conditionNodeConfig(item.id)"
|
||||
>
|
||||
<div
|
||||
class="branch-node-text"
|
||||
:title="item.showText"
|
||||
v-if="item.showText"
|
||||
>
|
||||
{{ item.showText }}
|
||||
</div>
|
||||
<div class="branch-node-text" v-else>
|
||||
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="node-toolbar"
|
||||
v-if="
|
||||
!readonly && index + 1 !== currentNode.conditionNodes?.length
|
||||
"
|
||||
>
|
||||
<div class="toolbar-icon">
|
||||
<IconifyIcon
|
||||
color="#0089ff"
|
||||
icon="ep:circle-close-filled"
|
||||
:size="18"
|
||||
@click="deleteCondition(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="branch-node-move move-node-left"
|
||||
v-if="
|
||||
!readonly &&
|
||||
index !== 0 &&
|
||||
index + 1 !== currentNode.conditionNodes?.length
|
||||
"
|
||||
@click="moveNode(index, -1)"
|
||||
>
|
||||
<IconifyIcon icon="ep:arrow-left" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="branch-node-move move-node-right"
|
||||
v-if="
|
||||
!readonly &&
|
||||
currentNode.conditionNodes &&
|
||||
index < currentNode.conditionNodes.length - 2
|
||||
"
|
||||
@click="moveNode(index, 1)"
|
||||
>
|
||||
<IconifyIcon icon="ep:arrow-right" />
|
||||
</div>
|
||||
</div>
|
||||
<NodeHandler
|
||||
v-model:child-node="item.childNode"
|
||||
:current-node="item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 条件节点配置 -->
|
||||
<ConditionNodeConfig
|
||||
:node-index="index"
|
||||
:condition-node="item"
|
||||
:ref="item.id"
|
||||
/>
|
||||
<!-- 递归显示子节点 -->
|
||||
<ProcessNodeTree
|
||||
v-if="item && item.childNode"
|
||||
:parent-node="item"
|
||||
v-model:flow-node="item.childNode"
|
||||
@recursive-find-parent-node="recursiveFindParentNode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NodeHandler
|
||||
v-if="currentNode"
|
||||
v-model:child-node="currentNode.childNode"
|
||||
:current-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,285 @@
|
|||
<script setup lang="ts">
|
||||
import type { SimpleFlowNode } from '../../consts';
|
||||
|
||||
import { getCurrentInstance, inject, ref, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { cloneDeep, buildShortUUID as generateUUID } from '@vben/utils';
|
||||
|
||||
import { Button, Input } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
ConditionType,
|
||||
DEFAULT_CONDITION_GROUP_VALUE,
|
||||
NODE_DEFAULT_TEXT,
|
||||
NodeType,
|
||||
} from '../../consts';
|
||||
import {
|
||||
getDefaultInclusiveConditionNodeName,
|
||||
useTaskStatusClass,
|
||||
} from '../../helpers';
|
||||
import ConditionNodeConfig from '../nodes-config/condition-node-config.vue';
|
||||
import ProcessNodeTree from '../process-node-tree.vue';
|
||||
import NodeHandler from './node-handler.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'InclusiveNode',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
flowNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 定义事件,更新父组件
|
||||
const emits = defineEmits<{
|
||||
findParentNode: [nodeList: SimpleFlowNode[], nodeType: number];
|
||||
recursiveFindParentNode: [
|
||||
nodeList: SimpleFlowNode[],
|
||||
curentNode: SimpleFlowNode,
|
||||
nodeType: number,
|
||||
];
|
||||
'update:modelValue': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
|
||||
const currentNode = ref<SimpleFlowNode>(props.flowNode);
|
||||
|
||||
watch(
|
||||
() => props.flowNode,
|
||||
(newValue) => {
|
||||
currentNode.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
const showInputs = ref<boolean[]>([]);
|
||||
// 失去焦点
|
||||
function blurEvent(index: number) {
|
||||
showInputs.value[index] = false;
|
||||
const conditionNode = currentNode.value.conditionNodes?.at(
|
||||
index,
|
||||
) as SimpleFlowNode;
|
||||
conditionNode.name =
|
||||
conditionNode.name ||
|
||||
getDefaultInclusiveConditionNodeName(
|
||||
index,
|
||||
conditionNode.conditionSetting?.defaultFlow,
|
||||
);
|
||||
}
|
||||
|
||||
// 点击条件名称
|
||||
function clickEvent(index: number) {
|
||||
showInputs.value[index] = true;
|
||||
}
|
||||
|
||||
function conditionNodeConfig(nodeId: string) {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
const conditionNode = proxy.$refs[nodeId][0];
|
||||
conditionNode.open();
|
||||
}
|
||||
|
||||
// 新增条件
|
||||
function addCondition() {
|
||||
const conditionNodes = currentNode.value.conditionNodes;
|
||||
if (conditionNodes) {
|
||||
const len = conditionNodes.length;
|
||||
const lastIndex = len - 1;
|
||||
const conditionData: SimpleFlowNode = {
|
||||
id: `Flow_${generateUUID()}`,
|
||||
name: `包容条件${len}`,
|
||||
showText: '',
|
||||
type: NodeType.CONDITION_NODE,
|
||||
childNode: undefined,
|
||||
conditionNodes: [],
|
||||
conditionSetting: {
|
||||
defaultFlow: false,
|
||||
conditionType: ConditionType.RULE,
|
||||
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
|
||||
},
|
||||
};
|
||||
conditionNodes.splice(lastIndex, 0, conditionData);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除条件
|
||||
function deleteCondition(index: number) {
|
||||
const conditionNodes = currentNode.value.conditionNodes;
|
||||
if (conditionNodes) {
|
||||
conditionNodes.splice(index, 1);
|
||||
if (conditionNodes.length === 1) {
|
||||
const childNode = currentNode.value.childNode;
|
||||
// 更新此节点为后续孩子节点
|
||||
emits('update:modelValue', childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动节点
|
||||
function moveNode(index: number, to: number) {
|
||||
// -1 :向左 1: 向右
|
||||
if (
|
||||
currentNode.value.conditionNodes &&
|
||||
currentNode.value.conditionNodes[index]
|
||||
) {
|
||||
currentNode.value.conditionNodes[index] =
|
||||
currentNode.value.conditionNodes.splice(
|
||||
index + to,
|
||||
1,
|
||||
currentNode.value.conditionNodes[index],
|
||||
)[0] as SimpleFlowNode;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归从父节点中查询匹配的节点
|
||||
function recursiveFindParentNode(
|
||||
nodeList: SimpleFlowNode[],
|
||||
node: SimpleFlowNode,
|
||||
nodeType: number,
|
||||
) {
|
||||
if (!node || node.type === NodeType.START_USER_NODE) {
|
||||
return;
|
||||
}
|
||||
if (node.type === nodeType) {
|
||||
nodeList.push(node);
|
||||
}
|
||||
// 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.INCLUSIVE_BRANCH_NODE) 继续查找
|
||||
emits('findParentNode', nodeList, nodeType);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="branch-node-wrapper">
|
||||
<div class="branch-node-container">
|
||||
<div
|
||||
v-if="readonly"
|
||||
class="branch-node-readonly"
|
||||
:class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
|
||||
>
|
||||
<span class="iconfont icon-inclusive icon-size inclusive"></span>
|
||||
</div>
|
||||
<Button v-else class="branch-node-add" @click="addCondition">
|
||||
添加条件
|
||||
</Button>
|
||||
<div
|
||||
class="branch-node-item"
|
||||
v-for="(item, index) in currentNode.conditionNodes"
|
||||
:key="index"
|
||||
>
|
||||
<template v-if="index === 0">
|
||||
<div class="branch-line-first-top"></div>
|
||||
<div class="branch-line-first-bottom"></div>
|
||||
</template>
|
||||
<template v-if="index + 1 === currentNode.conditionNodes?.length">
|
||||
<div class="branch-line-last-top"></div>
|
||||
<div class="branch-line-last-bottom"></div>
|
||||
</template>
|
||||
<div class="node-wrapper">
|
||||
<div class="node-container">
|
||||
<div
|
||||
class="node-box"
|
||||
:class="[
|
||||
{ 'node-config-error': !item.showText },
|
||||
`${useTaskStatusClass(item.activityStatus)}`,
|
||||
]"
|
||||
>
|
||||
<div class="branch-node-title-container">
|
||||
<div v-if="!readonly && showInputs[index]">
|
||||
<Input
|
||||
type="text"
|
||||
class="editable-title-input"
|
||||
@blur="blurEvent(index)"
|
||||
v-model="item.name"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="branch-title" @click="clickEvent(index)">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="branch-node-content"
|
||||
@click="conditionNodeConfig(item.id)"
|
||||
>
|
||||
<div
|
||||
class="branch-node-text"
|
||||
:title="item.showText"
|
||||
v-if="item.showText"
|
||||
>
|
||||
{{ item.showText }}
|
||||
</div>
|
||||
<div class="branch-node-text" v-else>
|
||||
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="node-toolbar"
|
||||
v-if="
|
||||
!readonly && index + 1 !== currentNode.conditionNodes?.length
|
||||
"
|
||||
>
|
||||
<div class="toolbar-icon">
|
||||
<IconifyIcon
|
||||
color="#0089ff"
|
||||
icon="ep:circle-close-filled"
|
||||
:size="18"
|
||||
@click="deleteCondition(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="branch-node-move move-node-left"
|
||||
v-if="
|
||||
!readonly &&
|
||||
index !== 0 &&
|
||||
index + 1 !== currentNode.conditionNodes?.length
|
||||
"
|
||||
@click="moveNode(index, -1)"
|
||||
>
|
||||
<IconifyIcon icon="ep:arrow-left" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="branch-node-move move-node-right"
|
||||
v-if="
|
||||
!readonly &&
|
||||
currentNode.conditionNodes &&
|
||||
index < currentNode.conditionNodes.length - 2
|
||||
"
|
||||
@click="moveNode(index, 1)"
|
||||
>
|
||||
<IconifyIcon icon="ep:arrow-right" />
|
||||
</div>
|
||||
</div>
|
||||
<NodeHandler
|
||||
v-model:child-node="item.childNode"
|
||||
:current-node="item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 条件节点配置 -->
|
||||
<ConditionNodeConfig
|
||||
:node-index="index"
|
||||
:condition-node="item"
|
||||
:ref="item.id"
|
||||
/>
|
||||
<!-- 递归显示子节点 -->
|
||||
<ProcessNodeTree
|
||||
v-if="item && item.childNode"
|
||||
:parent-node="item"
|
||||
v-model:flow-node="item.childNode"
|
||||
@recursive-find-parent-node="recursiveFindParentNode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NodeHandler
|
||||
v-if="currentNode"
|
||||
v-model:child-node="currentNode.childNode"
|
||||
:current-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -33,11 +33,12 @@ const props = defineProps({
|
|||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:childNode']);
|
||||
const popoverShow = ref(false);
|
||||
const readonly = inject<Boolean>('readonly'); // 是否只读
|
||||
|
||||
const addNode = (type: number) => {
|
||||
function addNode(type: number) {
|
||||
// 校验:条件分支、包容分支后面,不允许直接添加并行分支
|
||||
if (
|
||||
type === NodeType.PARALLEL_BRANCH_NODE &&
|
||||
|
@ -238,7 +239,7 @@ const addNode = (type: number) => {
|
|||
};
|
||||
emits('update:childNode', data);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="node-handler-wrapper">
|
||||
|
@ -334,5 +335,3 @@ const addNode = (type: number) => {
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
<script setup lang="ts">
|
||||
import type { SimpleFlowNode } from '../../consts';
|
||||
|
||||
import { inject, ref, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { buildShortUUID as generateUUID } from '@vben/utils';
|
||||
|
||||
import { Button, Input } from 'ant-design-vue';
|
||||
|
||||
import { NODE_DEFAULT_TEXT, NodeType } from '../../consts';
|
||||
import { useTaskStatusClass } from '../../helpers';
|
||||
import ProcessNodeTree from '../process-node-tree.vue';
|
||||
import NodeHandler from './node-handler.vue';
|
||||
|
||||
defineOptions({ name: 'ParallelNode' });
|
||||
|
||||
const props = defineProps({
|
||||
flowNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 定义事件,更新父组件
|
||||
const emits = defineEmits<{
|
||||
findParnetNode: [nodeList: SimpleFlowNode[], nodeType: number];
|
||||
recursiveFindParentNode: [
|
||||
nodeList: SimpleFlowNode[],
|
||||
curentNode: SimpleFlowNode,
|
||||
nodeType: number,
|
||||
];
|
||||
'update:modelValue': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
|
||||
const currentNode = ref<SimpleFlowNode>(props.flowNode);
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
|
||||
watch(
|
||||
() => props.flowNode,
|
||||
(newValue) => {
|
||||
currentNode.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
const showInputs = ref<boolean[]>([]);
|
||||
|
||||
// 失去焦点
|
||||
function blurEvent(index: number) {
|
||||
showInputs.value[index] = false;
|
||||
const conditionNode = currentNode.value.conditionNodes?.at(
|
||||
index,
|
||||
) as SimpleFlowNode;
|
||||
conditionNode.name = conditionNode.name || `并行${index + 1}`;
|
||||
}
|
||||
|
||||
// 点击条件名称
|
||||
function clickEvent(index: number) {
|
||||
showInputs.value[index] = true;
|
||||
}
|
||||
|
||||
// 新增条件
|
||||
function addCondition() {
|
||||
const conditionNodes = currentNode.value.conditionNodes;
|
||||
if (conditionNodes) {
|
||||
const len = conditionNodes.length;
|
||||
const lastIndex = len - 1;
|
||||
const conditionData: SimpleFlowNode = {
|
||||
id: `Flow_${generateUUID()}`,
|
||||
name: `并行${len}`,
|
||||
showText: '无需配置条件同时执行',
|
||||
type: NodeType.CONDITION_NODE,
|
||||
childNode: undefined,
|
||||
conditionNodes: [],
|
||||
};
|
||||
conditionNodes.splice(lastIndex, 0, conditionData);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除条件
|
||||
function deleteCondition(index: number) {
|
||||
const conditionNodes = currentNode.value.conditionNodes;
|
||||
if (conditionNodes) {
|
||||
conditionNodes.splice(index, 1);
|
||||
if (conditionNodes.length === 1) {
|
||||
const childNode = currentNode.value.childNode;
|
||||
// 更新此节点为后续孩子节点
|
||||
emits('update:modelValue', childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 递归从父节点中查询匹配的节点
|
||||
function recursiveFindParentNode(
|
||||
nodeList: SimpleFlowNode[],
|
||||
node: SimpleFlowNode,
|
||||
nodeType: number,
|
||||
) {
|
||||
if (!node || node.type === NodeType.START_USER_NODE) {
|
||||
return;
|
||||
}
|
||||
if (node.type === nodeType) {
|
||||
nodeList.push(node);
|
||||
}
|
||||
// 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点并行节点(NodeType.PARALLEL_NODE) 继续查找
|
||||
emits('findParnetNode', nodeList, nodeType);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="branch-node-wrapper">
|
||||
<div class="branch-node-container">
|
||||
<div
|
||||
v-if="readonly"
|
||||
class="branch-node-readonly"
|
||||
:class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
|
||||
>
|
||||
<span class="iconfont icon-parallel icon-size parallel"></span>
|
||||
</div>
|
||||
<Button
|
||||
v-else
|
||||
class="branch-node-add"
|
||||
color="#626aef"
|
||||
@click="addCondition"
|
||||
plain
|
||||
>
|
||||
添加分支
|
||||
</Button>
|
||||
<div
|
||||
class="branch-node-item"
|
||||
v-for="(item, index) in currentNode.conditionNodes"
|
||||
:key="index"
|
||||
>
|
||||
<template v-if="index === 0">
|
||||
<div class="branch-line-first-top"></div>
|
||||
<div class="branch-line-first-bottom"></div>
|
||||
</template>
|
||||
<template v-if="index + 1 === currentNode.conditionNodes?.length">
|
||||
<div class="branch-line-last-top"></div>
|
||||
<div class="branch-line-last-bottom"></div>
|
||||
</template>
|
||||
<div class="node-wrapper">
|
||||
<div class="node-container">
|
||||
<div
|
||||
class="node-box"
|
||||
:class="`${useTaskStatusClass(item.activityStatus)}`"
|
||||
>
|
||||
<div class="branch-node-title-container">
|
||||
<div v-if="showInputs[index]">
|
||||
<Input
|
||||
type="text"
|
||||
class="input-max-width editable-title-input"
|
||||
@blur="blurEvent(index)"
|
||||
v-model="item.name"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="branch-title" @click="clickEvent(index)">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="branch-priority">无优先级</div>
|
||||
</div>
|
||||
<div class="branch-node-content">
|
||||
<div
|
||||
class="branch-node-text"
|
||||
:title="item.showText"
|
||||
v-if="item.showText"
|
||||
>
|
||||
{{ item.showText }}
|
||||
</div>
|
||||
<div class="branch-node-text" v-else>
|
||||
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!readonly" class="node-toolbar">
|
||||
<div class="toolbar-icon">
|
||||
<IconifyIcon
|
||||
color="#0089ff"
|
||||
icon="ep:circle-close-filled"
|
||||
@click="deleteCondition(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NodeHandler
|
||||
v-model:child-node="item.childNode"
|
||||
:current-node="item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 递归显示子节点 -->
|
||||
<ProcessNodeTree
|
||||
v-if="item && item.childNode"
|
||||
:parent-node="item"
|
||||
v-model:flow-node="item.childNode"
|
||||
@recursive-find-parent-node="recursiveFindParentNode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NodeHandler
|
||||
v-if="currentNode"
|
||||
v-model:child-node="currentNode.childNode"
|
||||
:current-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,116 @@
|
|||
<script setup lang="ts">
|
||||
import type { SimpleFlowNode } from '../../consts';
|
||||
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Input } from 'ant-design-vue';
|
||||
|
||||
import { NODE_DEFAULT_TEXT, NodeType } from '../../consts';
|
||||
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
|
||||
import RouterNodeConfig from '../nodes-config/router-node-config.vue';
|
||||
import NodeHandler from './node-handler.vue';
|
||||
|
||||
defineOptions({ name: 'RouterNode' });
|
||||
|
||||
const props = defineProps({
|
||||
flowNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 定义事件,更新父组件
|
||||
const emits = defineEmits<{
|
||||
'update:flowNode': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
// 监控节点的变化
|
||||
const currentNode = useWatchNode(props);
|
||||
// 节点名称编辑
|
||||
const { showInput, blurEvent, clickTitle } = useNodeName2(
|
||||
currentNode,
|
||||
NodeType.ROUTER_BRANCH_NODE,
|
||||
);
|
||||
|
||||
const nodeSetting = ref();
|
||||
// 打开节点配置
|
||||
function openNodeConfig() {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
nodeSetting.value.openDrawer(currentNode.value);
|
||||
}
|
||||
|
||||
// 删除节点。更新当前节点为孩子节点
|
||||
function deleteNode() {
|
||||
emits('update:flowNode', currentNode.value.childNode);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="node-wrapper">
|
||||
<div class="node-container">
|
||||
<div
|
||||
class="node-box"
|
||||
:class="[
|
||||
{ 'node-config-error': !currentNode.showText },
|
||||
`${useTaskStatusClass(currentNode?.activityStatus)}`,
|
||||
]"
|
||||
>
|
||||
<div class="node-title-container">
|
||||
<div class="node-title-icon router-node">
|
||||
<span class="iconfont icon-router"></span>
|
||||
</div>
|
||||
<Input
|
||||
v-if="!readonly && showInput"
|
||||
type="text"
|
||||
class="editable-title-input"
|
||||
@blur="blurEvent()"
|
||||
v-model="currentNode.name"
|
||||
:placeholder="currentNode.name"
|
||||
/>
|
||||
<div v-else class="node-title" @click="clickTitle">
|
||||
{{ currentNode.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-content" @click="openNodeConfig">
|
||||
<div
|
||||
class="node-text"
|
||||
:title="currentNode.showText"
|
||||
v-if="currentNode.showText"
|
||||
>
|
||||
{{ currentNode.showText }}
|
||||
</div>
|
||||
<div class="node-text" v-else>
|
||||
{{ NODE_DEFAULT_TEXT.get(NodeType.ROUTER_BRANCH_NODE) }}
|
||||
</div>
|
||||
<IconifyIcon v-if="!readonly" icon="ep:arrow-right-bold" />
|
||||
</div>
|
||||
<div v-if="!readonly" class="node-toolbar">
|
||||
<div class="toolbar-icon">
|
||||
<IconifyIcon
|
||||
color="#0089ff"
|
||||
icon="ep:circle-close-filled"
|
||||
@click="deleteNode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
|
||||
<NodeHandler
|
||||
v-if="currentNode"
|
||||
v-model:child-node="currentNode.childNode"
|
||||
:current-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
<RouterNodeConfig
|
||||
v-if="!readonly && currentNode"
|
||||
ref="nodeSetting"
|
||||
:flow-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -15,17 +15,20 @@ import StartUserNodeConfig from '../nodes-config/start-user-node-config.vue';
|
|||
import NodeHandler from './node-handler.vue';
|
||||
|
||||
defineOptions({ name: 'StartUserNode' });
|
||||
|
||||
const props = defineProps({
|
||||
flowNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
default: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
// 定义事件,更新父组件。
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars, no-unused-vars
|
||||
const emits = defineEmits<{
|
||||
// const emits = defineEmits<{
|
||||
defineEmits<{
|
||||
'update:modelValue': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
|
||||
const readonly = inject<Boolean>('readonly'); // 是否只读
|
||||
const tasks = inject<Ref<any[]>>('tasks', ref([]));
|
||||
// 监控节点变化
|
||||
|
@ -41,7 +44,7 @@ const nodeSetting = ref();
|
|||
// 任务的弹窗显示,用于只读模式
|
||||
const selectTasks = ref<any[] | undefined>([]); // 选中的任务数组
|
||||
|
||||
const nodeClick = () => {
|
||||
function nodeClick() {
|
||||
if (readonly) {
|
||||
// 只读模式,弹窗显示任务信息
|
||||
if (tasks && tasks.value) {
|
||||
|
@ -58,7 +61,7 @@ const nodeClick = () => {
|
|||
);
|
||||
nodeSetting.value.showStartUserNodeConfig(currentNode.value);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="node-wrapper">
|
||||
|
@ -116,4 +119,3 @@ const nodeClick = () => {
|
|||
/>
|
||||
<!-- 审批记录 TODO -->
|
||||
</template>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
<script setup lang="ts">
|
||||
import type { SimpleFlowNode } from '../../consts';
|
||||
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Input } from 'ant-design-vue';
|
||||
|
||||
import { NODE_DEFAULT_TEXT, NodeType } from '../../consts';
|
||||
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
|
||||
import TriggerNodeConfig from '../nodes-config/trigger-node-config.vue';
|
||||
import NodeHandler from './node-handler.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'TriggerNode',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
flowNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 定义事件,更新父组件
|
||||
const emits = defineEmits<{
|
||||
'update:flowNode': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
// 监控节点的变化
|
||||
const currentNode = useWatchNode(props);
|
||||
// 节点名称编辑
|
||||
const { showInput, blurEvent, clickTitle } = useNodeName2(
|
||||
currentNode,
|
||||
NodeType.TRIGGER_NODE,
|
||||
);
|
||||
|
||||
const nodeSetting = ref();
|
||||
// 打开节点配置
|
||||
function openNodeConfig() {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
nodeSetting.value.showTriggerNodeConfig(currentNode.value);
|
||||
}
|
||||
|
||||
// 删除节点。更新当前节点为孩子节点
|
||||
function deleteNode() {
|
||||
emits('update:flowNode', currentNode.value.childNode);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="node-wrapper">
|
||||
<div class="node-container">
|
||||
<div
|
||||
class="node-box"
|
||||
:class="[
|
||||
{ 'node-config-error': !currentNode.showText },
|
||||
`${useTaskStatusClass(currentNode?.activityStatus)}`,
|
||||
]"
|
||||
>
|
||||
<div class="node-title-container">
|
||||
<div class="node-title-icon trigger-node">
|
||||
<span class="iconfont icon-trigger"></span>
|
||||
</div>
|
||||
<Input
|
||||
v-if="!readonly && showInput"
|
||||
type="text"
|
||||
class="editable-title-input"
|
||||
@blur="blurEvent()"
|
||||
v-model="currentNode.name"
|
||||
:placeholder="currentNode.name"
|
||||
/>
|
||||
<div v-else class="node-title" @click="clickTitle">
|
||||
{{ currentNode.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-content" @click="openNodeConfig">
|
||||
<div
|
||||
class="node-text"
|
||||
:title="currentNode.showText"
|
||||
v-if="currentNode.showText"
|
||||
>
|
||||
{{ currentNode.showText }}
|
||||
</div>
|
||||
<div class="node-text" v-else>
|
||||
{{ NODE_DEFAULT_TEXT.get(NodeType.TRIGGER_NODE) }}
|
||||
</div>
|
||||
<IconifyIcon v-if="!readonly" icon="ep:arrow-right-bold" />
|
||||
</div>
|
||||
<div v-if="!readonly" class="node-toolbar">
|
||||
<div class="toolbar-icon">
|
||||
<IconifyIcon
|
||||
color="#0089ff"
|
||||
icon="ep:circle-close-filled"
|
||||
:size="18"
|
||||
@click="deleteNode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
|
||||
<NodeHandler
|
||||
v-if="currentNode"
|
||||
v-model:child-node="currentNode.childNode"
|
||||
:current-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
<TriggerNodeConfig
|
||||
v-if="!readonly && currentNode"
|
||||
ref="nodeSetting"
|
||||
:flow-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -22,6 +22,7 @@ const props = defineProps({
|
|||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits<{
|
||||
findParentNode: [nodeList: SimpleFlowNode[], nodeType: NodeType];
|
||||
'update:flowNode': [node: SimpleFlowNode | undefined];
|
||||
|
@ -39,7 +40,7 @@ const { showInput, blurEvent, clickTitle } = useNodeName2(
|
|||
);
|
||||
const nodeSetting = ref();
|
||||
|
||||
const nodeClick = () => {
|
||||
function nodeClick() {
|
||||
if (readonly) {
|
||||
if (tasks && tasks.value) {
|
||||
// 只读模式,弹窗显示任务信息 TODO 待实现
|
||||
|
@ -49,18 +50,18 @@ const nodeClick = () => {
|
|||
// 编辑模式,打开节点配置、把当前节点传递给配置组件
|
||||
nodeSetting.value.showUserTaskNodeConfig(currentNode.value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const deleteNode = () => {
|
||||
function deleteNode() {
|
||||
emits('update:flowNode', currentNode.value.childNode);
|
||||
};
|
||||
}
|
||||
// 查找可以驳回用户节点
|
||||
const findReturnTaskNodes = (
|
||||
function findReturnTaskNodes(
|
||||
matchNodeList: SimpleFlowNode[], // 匹配的节点
|
||||
) => {
|
||||
) {
|
||||
// 从父节点查找
|
||||
emits('findParentNode', matchNodeList, NodeType.USER_TASK_NODE);
|
||||
};
|
||||
}
|
||||
|
||||
// const selectTasks = ref<any[] | undefined>([]); // 选中的任务数组
|
||||
</script>
|
||||
|
@ -135,4 +136,3 @@ const findReturnTaskNodes = (
|
|||
/>
|
||||
<!-- TODO 审批记录 -->
|
||||
</template>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -3,11 +3,19 @@ import type { SimpleFlowNode } from '../consts';
|
|||
|
||||
import { NodeType } from '../consts';
|
||||
import { useWatchNode } from '../helpers';
|
||||
import CopyTaskNode from './nodes/copy-task-node.vue';
|
||||
import DelayTimerNode from './nodes/delay-timer-node.vue';
|
||||
import EndEventNode from './nodes/end-event-node.vue';
|
||||
import ExclusiveNode from './nodes/exclusive-node.vue';
|
||||
import InclusiveNode from './nodes/inclusive-node.vue';
|
||||
import ParallelNode from './nodes/parallel-node.vue';
|
||||
import RouterNode from './nodes/router-node.vue';
|
||||
import StartUserNode from './nodes/start-user-node.vue';
|
||||
import TriggerNode from './nodes/trigger-node.vue';
|
||||
import UserTaskNode from './nodes/user-task-node.vue';
|
||||
|
||||
defineOptions({ name: 'ProcessNodeTree' });
|
||||
|
||||
const props = defineProps({
|
||||
parentNode: {
|
||||
type: Object as () => SimpleFlowNode,
|
||||
|
@ -18,6 +26,7 @@ const props = defineProps({
|
|||
default: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits<{
|
||||
recursiveFindParentNode: [
|
||||
nodeList: SimpleFlowNode[],
|
||||
|
@ -40,11 +49,11 @@ const findParentNode = (nodeList: SimpleFlowNode[], nodeType: number) => {
|
|||
};
|
||||
|
||||
// 递归从父节点中查询匹配的节点
|
||||
const recursiveFindParentNode = (
|
||||
function recursiveFindParentNode(
|
||||
nodeList: SimpleFlowNode[],
|
||||
findNode: SimpleFlowNode,
|
||||
nodeType: number,
|
||||
) => {
|
||||
) {
|
||||
if (!findNode) {
|
||||
return;
|
||||
}
|
||||
|
@ -57,7 +66,7 @@ const recursiveFindParentNode = (
|
|||
nodeList.push(findNode);
|
||||
}
|
||||
emits('recursiveFindParentNode', nodeList, props.parentNode, nodeType);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<!-- 发起人节点 -->
|
||||
|
@ -77,50 +86,50 @@ const recursiveFindParentNode = (
|
|||
@find-parent-node="findParentNode"
|
||||
/>
|
||||
<!-- 抄送节点 -->
|
||||
<!-- <CopyTaskNode
|
||||
<CopyTaskNode
|
||||
v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE"
|
||||
:flow-node="currentNode"
|
||||
@update:flow-node="handleModelValueUpdate"
|
||||
/> -->
|
||||
/>
|
||||
<!-- 条件节点 -->
|
||||
<!-- <ExclusiveNode
|
||||
<ExclusiveNode
|
||||
v-if="currentNode && currentNode.type === NodeType.CONDITION_BRANCH_NODE"
|
||||
:flow-node="currentNode"
|
||||
@update:model-value="handleModelValueUpdate"
|
||||
@find:parent-node="findFromParentNode"
|
||||
/> -->
|
||||
@find-parent-node="findParentNode"
|
||||
/>
|
||||
<!-- 并行节点 -->
|
||||
<!-- <ParallelNode
|
||||
<ParallelNode
|
||||
v-if="currentNode && currentNode.type === NodeType.PARALLEL_BRANCH_NODE"
|
||||
:flow-node="currentNode"
|
||||
@update:model-value="handleModelValueUpdate"
|
||||
@find:parent-node="findFromParentNode"
|
||||
/> -->
|
||||
@find-parent-node="findParentNode"
|
||||
/>
|
||||
<!-- 包容分支节点 -->
|
||||
<!-- <InclusiveNode
|
||||
<InclusiveNode
|
||||
v-if="currentNode && currentNode.type === NodeType.INCLUSIVE_BRANCH_NODE"
|
||||
:flow-node="currentNode"
|
||||
@update:model-value="handleModelValueUpdate"
|
||||
@find:parent-node="findFromParentNode"
|
||||
/> -->
|
||||
@find-parent-node="findParentNode"
|
||||
/>
|
||||
<!-- 延迟器节点 -->
|
||||
<!-- <DelayTimerNode
|
||||
<DelayTimerNode
|
||||
v-if="currentNode && currentNode.type === NodeType.DELAY_TIMER_NODE"
|
||||
:flow-node="currentNode"
|
||||
@update:flow-node="handleModelValueUpdate"
|
||||
/> -->
|
||||
/>
|
||||
<!-- 路由分支节点 -->
|
||||
<!-- <RouterNode
|
||||
<RouterNode
|
||||
v-if="currentNode && currentNode.type === NodeType.ROUTER_BRANCH_NODE"
|
||||
:flow-node="currentNode"
|
||||
@update:flow-node="handleModelValueUpdate"
|
||||
/> -->
|
||||
/>
|
||||
<!-- 触发器节点 -->
|
||||
<!-- <TriggerNode
|
||||
<TriggerNode
|
||||
v-if="currentNode && currentNode.type === NodeType.TRIGGER_NODE"
|
||||
:flow-node="currentNode"
|
||||
@update:flow-node="handleModelValueUpdate"
|
||||
/> -->
|
||||
/>
|
||||
<!-- 子流程节点 -->
|
||||
<!-- <ChildProcessNode
|
||||
v-if="currentNode && currentNode.type === NodeType.CHILD_PROCESS_NODE"
|
||||
|
@ -141,4 +150,3 @@ const recursiveFindParentNode = (
|
|||
:flow-node="currentNode"
|
||||
/>
|
||||
</template>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -11,9 +11,10 @@ import type { SystemUserApi } from '#/api/system/user';
|
|||
|
||||
import { inject, onMounted, provide, ref, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { Button, Modal } from 'ant-design-vue';
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { getFormDetail } from '#/api/bpm/form';
|
||||
import { getUserGroupSimpleList } from '#/api/bpm/userGroup';
|
||||
|
@ -112,16 +113,20 @@ provide('tasks', []);
|
|||
provide('processInstance', {});
|
||||
const processNodeTree = ref<SimpleFlowNode | undefined>();
|
||||
provide('processNodeTree', processNodeTree);
|
||||
const errorDialogVisible = ref(false);
|
||||
const errorNodes: SimpleFlowNode[] = [];
|
||||
|
||||
// 创建错误提示弹窗
|
||||
const [ErrorModal, errorModalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
});
|
||||
|
||||
// 添加更新模型的方法
|
||||
const updateModel = () => {
|
||||
function updateModel() {
|
||||
if (!processNodeTree.value) {
|
||||
processNodeTree.value = {
|
||||
name: '发起人',
|
||||
type: NodeType.START_USER_NODE,
|
||||
id: NodeId.START_USER_NODE_ID,
|
||||
showText: '默认配置',
|
||||
childNode: {
|
||||
id: NodeId.END_EVENT_NODE_ID,
|
||||
name: '结束',
|
||||
|
@ -131,11 +136,11 @@ const updateModel = () => {
|
|||
// 初始化时也触发一次保存
|
||||
saveSimpleFlowModel(processNodeTree.value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const saveSimpleFlowModel = async (
|
||||
async function saveSimpleFlowModel(
|
||||
simpleModelNode: SimpleFlowNode | undefined,
|
||||
) => {
|
||||
) {
|
||||
if (!simpleModelNode) {
|
||||
return;
|
||||
}
|
||||
|
@ -146,52 +151,40 @@ const saveSimpleFlowModel = async (
|
|||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验节点设置。 暂时以 showText 为空 未节点错误配置
|
||||
* 校验节点设置。 暂时以 showText 为空作为节点错误配置的判断条件
|
||||
*/
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars, no-unused-vars
|
||||
const validateNode = (
|
||||
function validateNode(
|
||||
node: SimpleFlowNode | undefined,
|
||||
errorNodes: SimpleFlowNode[],
|
||||
) => {
|
||||
) {
|
||||
if (node) {
|
||||
const { type, showText, conditionNodes } = node;
|
||||
if (type === NodeType.END_EVENT_NODE) {
|
||||
return;
|
||||
}
|
||||
if (type === NodeType.START_USER_NODE) {
|
||||
// 发起人节点暂时不用校验,直接校验孩子节点
|
||||
validateNode(node.childNode, errorNodes);
|
||||
}
|
||||
|
||||
if (
|
||||
type === NodeType.USER_TASK_NODE ||
|
||||
type === NodeType.COPY_TASK_NODE ||
|
||||
type === NodeType.CONDITION_NODE
|
||||
) {
|
||||
if (!showText) {
|
||||
errorNodes.push(node);
|
||||
}
|
||||
validateNode(node.childNode, errorNodes);
|
||||
}
|
||||
|
||||
if (
|
||||
type === NodeType.CONDITION_BRANCH_NODE ||
|
||||
type === NodeType.PARALLEL_BRANCH_NODE ||
|
||||
type === NodeType.INCLUSIVE_BRANCH_NODE
|
||||
) {
|
||||
// 分支节点
|
||||
// 1. 先校验各个分支
|
||||
// 1. 分支节点, 先校验各个分支
|
||||
conditionNodes?.forEach((item) => {
|
||||
validateNode(item, errorNodes);
|
||||
});
|
||||
// 2. 校验孩子节点
|
||||
validateNode(node.childNode, errorNodes);
|
||||
} else {
|
||||
if (!showText) {
|
||||
errorNodes.push(node);
|
||||
}
|
||||
validateNode(node.childNode, errorNodes);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
|
@ -220,38 +213,40 @@ onMounted(async () => {
|
|||
}
|
||||
});
|
||||
|
||||
const simpleProcessModelRef = ref();
|
||||
|
||||
defineExpose({});
|
||||
const validate = async () => {
|
||||
const errorNodes: SimpleFlowNode[] = [];
|
||||
validateNode(processNodeTree.value, errorNodes);
|
||||
if (errorNodes.length === 0) {
|
||||
return true;
|
||||
} else {
|
||||
// 设置错误节点数据并打开弹窗
|
||||
errorModalApi.setData(errorNodes);
|
||||
errorModalApi.open();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
defineExpose({ validate });
|
||||
</script>
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<SimpleProcessModel
|
||||
ref="simpleProcessModelRef"
|
||||
v-if="processNodeTree"
|
||||
:flow-node="processNodeTree"
|
||||
:readonly="false"
|
||||
@save="saveSimpleFlowModel"
|
||||
/>
|
||||
<Modal
|
||||
v-model="errorDialogVisible"
|
||||
title="保存失败"
|
||||
width="400"
|
||||
:fullscreen="false"
|
||||
>
|
||||
<div class="mb-2">以下节点内容不完善,请修改后保存</div>
|
||||
<ErrorModal title="流程设计校验不通过" class="w-[600px]">
|
||||
<div class="mb-2 text-base">以下节点配置不完善,请修改相关配置</div>
|
||||
<div
|
||||
class="b-rounded-1 line-height-normal mb-3 bg-gray-100 p-2"
|
||||
v-for="(item, index) in errorNodes"
|
||||
class="mb-3 rounded-md bg-gray-100 p-2 text-sm"
|
||||
v-for="(item, index) in errorModalApi.getData()"
|
||||
:key="index"
|
||||
>
|
||||
{{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button type="primary" @click="errorDialogVisible = false">
|
||||
知道了
|
||||
</Button>
|
||||
<Button type="primary" @click="errorModalApi.close()">知道了</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</ErrorModal>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -49,22 +49,22 @@ const currentY = ref(0);
|
|||
const initialX = ref(0);
|
||||
const initialY = ref(0);
|
||||
|
||||
const setGrabCursor = () => {
|
||||
function setGrabCursor() {
|
||||
document.body.style.cursor = 'grab';
|
||||
};
|
||||
}
|
||||
|
||||
const resetCursor = () => {
|
||||
function resetCursor() {
|
||||
document.body.style.cursor = 'default';
|
||||
};
|
||||
}
|
||||
|
||||
const startDrag = (e: MouseEvent) => {
|
||||
function startDrag(e: MouseEvent) {
|
||||
isDragging.value = true;
|
||||
startX.value = e.clientX - currentX.value;
|
||||
startY.value = e.clientY - currentY.value;
|
||||
setGrabCursor(); // 设置小手光标
|
||||
};
|
||||
}
|
||||
|
||||
const onDrag = (e: MouseEvent) => {
|
||||
function onDrag(e: MouseEvent) {
|
||||
if (!isDragging.value) return;
|
||||
e.preventDefault(); // 禁用文本选择
|
||||
|
||||
|
@ -73,44 +73,44 @@ const onDrag = (e: MouseEvent) => {
|
|||
currentX.value = e.clientX - startX.value;
|
||||
currentY.value = e.clientY - startY.value;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const stopDrag = () => {
|
||||
function stopDrag() {
|
||||
isDragging.value = false;
|
||||
resetCursor(); // 重置光标
|
||||
};
|
||||
}
|
||||
|
||||
const zoomIn = () => {
|
||||
function zoomIn() {
|
||||
if (scaleValue.value === MAX_SCALE_VALUE) {
|
||||
return;
|
||||
}
|
||||
scaleValue.value += 10;
|
||||
};
|
||||
}
|
||||
|
||||
const zoomOut = () => {
|
||||
function zoomOut() {
|
||||
if (scaleValue.value === MIN_SCALE_VALUE) {
|
||||
return;
|
||||
}
|
||||
scaleValue.value -= 10;
|
||||
};
|
||||
}
|
||||
|
||||
const processReZoom = () => {
|
||||
function processReZoom() {
|
||||
scaleValue.value = 100;
|
||||
};
|
||||
}
|
||||
|
||||
const resetPosition = () => {
|
||||
function resetPosition() {
|
||||
currentX.value = initialX.value;
|
||||
currentY.value = initialY.value;
|
||||
};
|
||||
}
|
||||
|
||||
/** 校验节点设置 */
|
||||
const errorDialogVisible = ref(false);
|
||||
let errorNodes: SimpleFlowNode[] = [];
|
||||
|
||||
const validateNode = (
|
||||
function validateNode(
|
||||
node: SimpleFlowNode | undefined,
|
||||
errorNodes: SimpleFlowNode[],
|
||||
) => {
|
||||
) {
|
||||
if (node) {
|
||||
const { type, showText, conditionNodes } = node;
|
||||
if (type === NodeType.END_EVENT_NODE) {
|
||||
|
@ -146,10 +146,10 @@ const validateNode = (
|
|||
validateNode(node.childNode, errorNodes);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取当前流程数据 */
|
||||
const getCurrentFlowData = async () => {
|
||||
async function getCurrentFlowData() {
|
||||
try {
|
||||
errorNodes = [];
|
||||
validateNode(processNodeTree.value, errorNodes);
|
||||
|
@ -162,26 +162,26 @@ const getCurrentFlowData = async () => {
|
|||
console.error('获取流程数据失败:', error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getCurrentFlowData,
|
||||
});
|
||||
|
||||
/** 导出 JSON */
|
||||
const exportJson = () => {
|
||||
function exportJson() {
|
||||
downloadFileFromBlob({
|
||||
fileName: 'model.json',
|
||||
source: new Blob([JSON.stringify(processNodeTree.value)]),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/** 导入 JSON */
|
||||
const refFile = ref();
|
||||
const importJson = () => {
|
||||
function importJson() {
|
||||
refFile.value.click();
|
||||
};
|
||||
const importLocalFile = () => {
|
||||
}
|
||||
function importLocalFile() {
|
||||
const file = refFile.value.files[0];
|
||||
file.text().then((result: any) => {
|
||||
if (isString(result)) {
|
||||
|
@ -189,7 +189,7 @@ const importLocalFile = () => {
|
|||
emits('save', processNodeTree.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// 在组件初始化时记录初始位置
|
||||
onMounted(() => {
|
||||
|
@ -267,4 +267,3 @@ onMounted(() => {
|
|||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -475,9 +475,9 @@ export type ListenerHandler = {
|
|||
* 条件规则结构定义
|
||||
*/
|
||||
export type ConditionRule = {
|
||||
leftSide: string;
|
||||
leftSide: string | undefined;
|
||||
opCode: string;
|
||||
rightSide: string;
|
||||
rightSide: string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -564,7 +564,7 @@ export type RouterSetting = {
|
|||
conditionExpression: string;
|
||||
conditionGroups: ConditionGroup;
|
||||
conditionType: ConditionType;
|
||||
nodeId: string;
|
||||
nodeId: string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -725,7 +725,7 @@ export const DEFAULT_CONDITION_GROUP_VALUE = {
|
|||
rules: [
|
||||
{
|
||||
opCode: '==',
|
||||
leftSide: '',
|
||||
leftSide: undefined,
|
||||
rightSide: '',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -43,7 +43,7 @@ export function useWatchNode(props: {
|
|||
}
|
||||
|
||||
// 解析 formCreate 所有表单字段, 并返回
|
||||
const parseFormCreateFields = (formFields?: string[]) => {
|
||||
function parseFormCreateFields(formFields?: string[]) {
|
||||
const result: Array<Record<string, any>> = [];
|
||||
if (formFields) {
|
||||
formFields.forEach((fieldStr: string) => {
|
||||
|
@ -51,7 +51,7 @@ const parseFormCreateFields = (formFields?: string[]) => {
|
|||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析表单组件的 field, title 等字段(递归,如果组件包含子组件)
|
||||
|
@ -109,20 +109,20 @@ export function useFormFieldsPermission(
|
|||
|
||||
const formFields = inject<Ref<string[]>>('formFields', ref([])); // 流程表单字段
|
||||
|
||||
const getNodeConfigFormFields = (
|
||||
function getNodeConfigFormFields(
|
||||
nodeFormFields?: Array<Record<string, string>>,
|
||||
) => {
|
||||
) {
|
||||
nodeFormFields = toRaw(nodeFormFields);
|
||||
fieldsPermissionConfig.value =
|
||||
!nodeFormFields || nodeFormFields.length === 0
|
||||
? getDefaultFieldsPermission(unref(formFields))
|
||||
: mergeFieldsPermission(nodeFormFields, unref(formFields));
|
||||
};
|
||||
}
|
||||
// 合并已经设置的表单字段权限,当前流程表单字段 (可能新增,或删除了字段)
|
||||
const mergeFieldsPermission = (
|
||||
function mergeFieldsPermission(
|
||||
formFieldsPermisson: Array<Record<string, string>>,
|
||||
formFields?: string[],
|
||||
) => {
|
||||
) {
|
||||
let mergedFieldsPermission: Array<Record<string, any>> = [];
|
||||
if (formFields) {
|
||||
mergedFieldsPermission = parseFormCreateFields(formFields).map((item) => {
|
||||
|
@ -137,10 +137,10 @@ export function useFormFieldsPermission(
|
|||
});
|
||||
}
|
||||
return mergedFieldsPermission;
|
||||
};
|
||||
}
|
||||
|
||||
// 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
|
||||
const getDefaultFieldsPermission = (formFields?: string[]) => {
|
||||
function getDefaultFieldsPermission(formFields?: string[]) {
|
||||
let defaultFieldsPermission: Array<Record<string, any>> = [];
|
||||
if (formFields) {
|
||||
defaultFieldsPermission = parseFormCreateFields(formFields).map(
|
||||
|
@ -154,7 +154,7 @@ export function useFormFieldsPermission(
|
|||
);
|
||||
}
|
||||
return defaultFieldsPermission;
|
||||
};
|
||||
}
|
||||
|
||||
// 获取表单的所有字段,作为下拉框选项
|
||||
const formFieldOptions = parseFormCreateFields(unref(formFields));
|
||||
|
@ -268,7 +268,6 @@ export function useNodeForm(nodeType: NodeType) {
|
|||
const formFields = inject<Ref<string[]>>('formFields', ref([])); // 流程表单字段
|
||||
const configForm = ref<any | CopyTaskFormType | UserTaskFormType>();
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-ternary
|
||||
if (
|
||||
nodeType === NodeType.USER_TASK_NODE ||
|
||||
nodeType === NodeType.TRANSACTOR_NODE
|
||||
|
@ -286,13 +285,12 @@ export function useNodeForm(nodeType: NodeType) {
|
|||
maxRemindCount: 1, // 默认 提醒 1次
|
||||
buttonsSetting: [],
|
||||
};
|
||||
} else {
|
||||
configForm.value = {
|
||||
candidateStrategy: CandidateStrategy.USER,
|
||||
};
|
||||
}
|
||||
configForm.value = {
|
||||
candidateStrategy: CandidateStrategy.USER,
|
||||
};
|
||||
|
||||
const getShowText = (): string => {
|
||||
function getShowText(): string {
|
||||
let showText = '';
|
||||
// 指定成员
|
||||
if (
|
||||
|
@ -428,12 +426,12 @@ export function useNodeForm(nodeType: NodeType) {
|
|||
showText = `流程表达式:${configForm.value.expression}`;
|
||||
}
|
||||
return showText;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理候选人参数的赋值
|
||||
*/
|
||||
const handleCandidateParam = () => {
|
||||
function handleCandidateParam() {
|
||||
let candidateParam: string | undefined;
|
||||
if (!configForm.value) {
|
||||
return candidateParam;
|
||||
|
@ -495,14 +493,14 @@ export function useNodeForm(nodeType: NodeType) {
|
|||
}
|
||||
}
|
||||
return candidateParam;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 解析候选人参数
|
||||
*/
|
||||
const parseCandidateParam = (
|
||||
function parseCandidateParam(
|
||||
candidateStrategy: CandidateStrategy,
|
||||
candidateParam: string | undefined,
|
||||
) => {
|
||||
) {
|
||||
if (!configForm.value || !candidateParam) {
|
||||
return;
|
||||
}
|
||||
|
@ -578,7 +576,7 @@ export function useNodeForm(nodeType: NodeType) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
configForm,
|
||||
roleOptions,
|
||||
|
@ -599,13 +597,13 @@ export function useDrawer() {
|
|||
// 抽屉配置是否可见
|
||||
const settingVisible = ref(false);
|
||||
// 关闭配置抽屉
|
||||
const closeDrawer = () => {
|
||||
function closeDrawer() {
|
||||
settingVisible.value = false;
|
||||
};
|
||||
}
|
||||
// 打开配置抽屉
|
||||
const openDrawer = () => {
|
||||
function openDrawer() {
|
||||
settingVisible.value = true;
|
||||
};
|
||||
}
|
||||
return {
|
||||
settingVisible,
|
||||
closeDrawer,
|
||||
|
@ -622,15 +620,15 @@ export function useNodeName(nodeType: NodeType) {
|
|||
// 节点名称输入框
|
||||
const showInput = ref(false);
|
||||
// 点击节点名称编辑图标
|
||||
const clickIcon = () => {
|
||||
function clickIcon() {
|
||||
showInput.value = true;
|
||||
};
|
||||
}
|
||||
// 节点名称输入框失去焦点
|
||||
const blurEvent = () => {
|
||||
function blurEvent() {
|
||||
showInput.value = false;
|
||||
nodeName.value =
|
||||
nodeName.value || (NODE_DEFAULT_NAME.get(nodeType) as string);
|
||||
};
|
||||
}
|
||||
return {
|
||||
nodeName,
|
||||
showInput,
|
||||
|
@ -643,15 +641,15 @@ export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) {
|
|||
// 显示节点名称输入框
|
||||
const showInput = ref(false);
|
||||
// 节点名称输入框失去焦点
|
||||
const blurEvent = () => {
|
||||
function blurEvent() {
|
||||
showInput.value = false;
|
||||
node.value.name =
|
||||
node.value.name || (NODE_DEFAULT_NAME.get(nodeType) as string);
|
||||
};
|
||||
}
|
||||
// 点击节点标题进行输入
|
||||
const clickTitle = () => {
|
||||
function clickTitle() {
|
||||
showInput.value = true;
|
||||
};
|
||||
}
|
||||
return {
|
||||
showInput,
|
||||
clickTitle,
|
||||
|
@ -722,18 +720,40 @@ export function getConditionShowText(
|
|||
}
|
||||
|
||||
/** 获取表单字段名称*/
|
||||
const getFormFieldTitle = (
|
||||
function getFormFieldTitle(
|
||||
fieldOptions: Array<Record<string, any>>,
|
||||
field: string,
|
||||
) => {
|
||||
) {
|
||||
const item = fieldOptions.find((item) => item.field === field);
|
||||
return item?.title;
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取操作符名称 */
|
||||
const getOpName = (opCode: string): string | undefined => {
|
||||
function getOpName(opCode: string): string | undefined {
|
||||
const opName = COMPARISON_OPERATORS.find(
|
||||
(item: any) => item.value === opCode,
|
||||
);
|
||||
return opName?.label;
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取条件节点默认的名称 */
|
||||
export function getDefaultConditionNodeName(
|
||||
index: number,
|
||||
defaultFlow: boolean | undefined,
|
||||
): string {
|
||||
if (defaultFlow) {
|
||||
return '其它情况';
|
||||
}
|
||||
return `条件${index + 1}`;
|
||||
}
|
||||
|
||||
/** 获取包容分支条件节点默认的名称 */
|
||||
export function getDefaultInclusiveConditionNodeName(
|
||||
index: number,
|
||||
defaultFlow: boolean | undefined,
|
||||
): string {
|
||||
if (defaultFlow) {
|
||||
return '其它情况';
|
||||
}
|
||||
return `包容条件${index + 1}`;
|
||||
}
|
||||
|
|
|
@ -334,12 +334,14 @@
|
|||
margin-top: 4px;
|
||||
line-height: 32px;
|
||||
color: #111f2c;
|
||||
background: rgb(0 0 0 / 3%);
|
||||
border-radius: 4px;
|
||||
|
||||
.branch-node-text {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
|
||||
-webkit-line-clamp: 1; /* 这将限制文本显示为一行 */
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
word-break: break-all;
|
||||
|
@ -478,6 +480,8 @@
|
|||
top: -18px;
|
||||
left: 50%;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
|
@ -644,7 +648,8 @@
|
|||
width: 80px;
|
||||
height: 36px;
|
||||
color: #212121;
|
||||
border: 2px solid #fafafa;
|
||||
background-color: #fff;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%);
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export { default as UserSelectModal } from './user-select-modal.vue';
|
|
@ -123,8 +123,8 @@ export function useGridColumns<T = BpmCategoryApi.CategoryVO>(
|
|||
field: 'userIds',
|
||||
title: '成员',
|
||||
minWidth: 200,
|
||||
formatter: (row) => {
|
||||
return getMemberNames(row.cellValue);
|
||||
formatter: ({ cellValue }) => {
|
||||
return getMemberNames(cellValue);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -216,7 +216,8 @@ const validateAllSteps = async () => {
|
|||
await validateBasic();
|
||||
} catch {
|
||||
currentStep.value = 0;
|
||||
throw new Error('请完善基本信息');
|
||||
message.warning('请完善基本信息');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 表单设计校验
|
||||
|
@ -224,25 +225,19 @@ const validateAllSteps = async () => {
|
|||
await validateForm();
|
||||
} catch {
|
||||
currentStep.value = 1;
|
||||
throw new Error('请完善自定义表单信息');
|
||||
message.warning('请完善自定义表单信息');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 流程设计校验 TODO
|
||||
|
||||
// 流程设计校验
|
||||
try {
|
||||
await validateProcess();
|
||||
} catch {
|
||||
currentStep.value = 2;
|
||||
throw new Error('请设计流程');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 表单设计校验
|
||||
try {
|
||||
await validateProcess();
|
||||
} catch {
|
||||
currentStep.value = 2;
|
||||
throw new Error('请设计流程');
|
||||
}
|
||||
// TODO 更多设置校验
|
||||
|
||||
return true;
|
||||
};
|
||||
|
@ -251,7 +246,10 @@ const validateAllSteps = async () => {
|
|||
const handleSave = async () => {
|
||||
try {
|
||||
// 保存前校验所有步骤的数据
|
||||
await validateAllSteps();
|
||||
const result = await validateAllSteps();
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新表单数据
|
||||
const modelData = {
|
||||
|
@ -297,7 +295,7 @@ const handleSave = async () => {
|
|||
}
|
||||
} catch (error: any) {
|
||||
console.error('保存失败:', error);
|
||||
message.warning(error.message || '请完善所有步骤的必填信息');
|
||||
// message.warning(error.msg || '请完善所有步骤的必填信息');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -338,7 +336,6 @@ const handleDeploy = async () => {
|
|||
/** 步骤切换处理 */
|
||||
const handleStepClick = async (index: number) => {
|
||||
try {
|
||||
console.warn('handleStepClick', index);
|
||||
if (index !== 0) {
|
||||
await validateBasic();
|
||||
}
|
||||
|
@ -348,23 +345,13 @@ const handleStepClick = async (index: number) => {
|
|||
if (index !== 2) {
|
||||
await validateProcess();
|
||||
}
|
||||
|
||||
// 切换步骤
|
||||
currentStep.value = index;
|
||||
|
||||
// 如果切换到流程设计步骤,等待组件渲染完成后刷新设计器
|
||||
if (index === 2) {
|
||||
// TODO 后续加
|
||||
// await nextTick();
|
||||
// // 等待更长时间确保组件完全初始化
|
||||
// await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
// if (processDesignRef.value?.refresh) {
|
||||
// await processDesignRef.value.refresh();
|
||||
// }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('步骤切换失败:', error);
|
||||
message.warning('请先完善当前步骤必填信息');
|
||||
if (currentStep.value !== 2) {
|
||||
message.warning('请先完善当前步骤必填信息');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -22,9 +22,8 @@ import {
|
|||
Tooltip,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { DeptSelectModal } from '#/components/dept-select-modal';
|
||||
import { DeptSelectModal, UserSelectModal } from '#/components/select-modal';
|
||||
import { ImageUpload } from '#/components/upload';
|
||||
import { UserSelectModal } from '#/components/user-select-modal';
|
||||
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '#/utils';
|
||||
|
||||
const props = defineProps({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { computed, inject, nextTick } from 'vue';
|
||||
import { computed, inject, nextTick, ref } from 'vue';
|
||||
|
||||
import { BpmModelType } from '#/utils';
|
||||
|
||||
|
@ -13,12 +13,21 @@ const modelData = defineModel<any>();
|
|||
|
||||
const processData = inject('processData') as Ref;
|
||||
|
||||
const simpleDesign = ref();
|
||||
|
||||
/** 表单校验 */
|
||||
const validate = async () => {
|
||||
// 获取最新的流程数据
|
||||
if (!processData.value) {
|
||||
throw new Error('请设计流程');
|
||||
}
|
||||
if (modelData.value.type === BpmModelType.SIMPLE) {
|
||||
// 简易设计器校验
|
||||
const validateResult = await simpleDesign.value?.validateConfig();
|
||||
if (!validateResult) {
|
||||
throw new Error('请完善设计配置');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
/** 处理设计器保存成功 */
|
||||
|
@ -41,9 +50,7 @@ const handleDesignSuccess = async (data?: any) => {
|
|||
const showDesigner = computed(() => {
|
||||
return Boolean(modelData.value?.key && modelData.value?.name);
|
||||
});
|
||||
defineExpose({
|
||||
validate,
|
||||
});
|
||||
defineExpose({ validate });
|
||||
</script>
|
||||
<template>
|
||||
<div class="h-full">
|
||||
|
@ -61,6 +68,7 @@ defineExpose({
|
|||
:start-user-ids="modelData.startUserIds"
|
||||
:start-dept-ids="modelData.startDeptIds"
|
||||
@success="handleDesignSuccess"
|
||||
ref="simpleDesign"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -17,12 +17,17 @@ defineProps<{
|
|||
const emit = defineEmits(['success']);
|
||||
const designerRef = ref();
|
||||
|
||||
// 修改成功回调
|
||||
/** 保存成功回调 */
|
||||
const handleSuccess = (data?: any) => {
|
||||
if (data) {
|
||||
emit('success', data);
|
||||
}
|
||||
};
|
||||
/** 设计器配置校验 */
|
||||
const validateConfig = async () => {
|
||||
return await designerRef.value.validate();
|
||||
};
|
||||
defineExpose({ validateConfig });
|
||||
</script>
|
||||
<template>
|
||||
<ContentWrap :body-style="{ padding: '20px 16px' }">
|
||||
|
@ -37,4 +42,3 @@ const handleSuccess = (data?: any) => {
|
|||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -1389,4 +1389,3 @@ defineExpose({ loadTodoTask });
|
|||
<!-- 签名弹窗 -->
|
||||
<Signature ref="signRef" @success="handleSignFinish" />
|
||||
</template>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -10,7 +10,7 @@ import { formatDateTime, isEmpty } from '@vben/utils';
|
|||
|
||||
import { Avatar, Button, Image, Timeline, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { UserSelectModal } from '#/components/user-select-modal';
|
||||
import { UserSelectModal } from '#/components/select-modal';
|
||||
import {
|
||||
BpmCandidateStrategyEnum,
|
||||
BpmNodeTypeEnum,
|
||||
|
|
|
@ -133,8 +133,8 @@ export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
|||
field: 'durationInMillis',
|
||||
title: '耗时',
|
||||
minWidth: 180,
|
||||
formatter: ({ row }) => {
|
||||
return `${formatPast2(row.durationInMillis)}`;
|
||||
formatter: ({ cellValue }) => {
|
||||
return `${formatPast2(cellValue)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -89,8 +89,8 @@ export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
|||
field: 'durationInMillis',
|
||||
title: '耗时',
|
||||
minWidth: 180,
|
||||
formatter: ({ row }) => {
|
||||
return `${formatPast2(row.durationInMillis)}`;
|
||||
formatter: ({ cellValue }) => {
|
||||
return `${formatPast2(cellValue)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,16 +1,5 @@
|
|||
import type { Ref } from 'vue';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
import type { CrmReceivableApi } from '#/api/crm/receivable';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { DICT_TYPE } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
export interface LeftSideItem {
|
||||
name: string;
|
||||
menu: string;
|
||||
|
@ -110,786 +99,3 @@ export const useLeftSides = (
|
|||
},
|
||||
];
|
||||
};
|
||||
|
||||
/** 分配给我的线索 列表的搜索表单 */
|
||||
export function useClueFollowFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'followUpStatus',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: FOLLOWUP_STATUS,
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 分配给我的线索 列表的字段 */
|
||||
export function useClueFollowColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '线索名称',
|
||||
minWidth: 160,
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'source',
|
||||
title: '线索来源',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
title: '手机',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
title: '电话',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: '邮箱',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
title: '地址',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
title: '客户行业',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'level',
|
||||
title: '客户级别',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
title: '下次联系时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'contactLastContent',
|
||||
title: '最后跟进记录',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 100,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 合同审核列表的搜索表单 */
|
||||
export function useContractAuditFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'auditStatus',
|
||||
label: '合同状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: AUDIT_STATUS,
|
||||
},
|
||||
defaultValue: 10,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 合同提醒列表的搜索表单 */
|
||||
export function useContractRemindFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'expiryType',
|
||||
label: '到期状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: CONTRACT_EXPIRY_TYPE,
|
||||
},
|
||||
defaultValue: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 合同审核列表的字段 */
|
||||
export function useContractColumns<T = CrmContractApi.Contract>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
title: '合同编号',
|
||||
minWidth: 160,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '合同名称',
|
||||
minWidth: 160,
|
||||
slots: {
|
||||
default: 'name',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
minWidth: 160,
|
||||
slots: {
|
||||
default: 'customerName',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'businessName',
|
||||
title: '商机名称',
|
||||
minWidth: 160,
|
||||
slots: {
|
||||
default: 'businessName',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '合同金额(元)',
|
||||
minWidth: 120,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
field: 'orderDate',
|
||||
title: '下单时间',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'startTime',
|
||||
title: '合同开始时间',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
title: '合同结束时间',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'contactName',
|
||||
title: '客户签约人',
|
||||
minWidth: 130,
|
||||
slots: {
|
||||
default: 'contactName',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'signUserName',
|
||||
title: '公司签约人',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'totalReceivablePrice',
|
||||
title: '已回款金额(元)',
|
||||
minWidth: 140,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
field: 'noReceivablePrice',
|
||||
title: '未回款金额(元)',
|
||||
minWidth: 120,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'auditStatus',
|
||||
title: '合同状态',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 130,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'no',
|
||||
nameTitle: '合同编号',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'processDetail',
|
||||
show: hasAccessByCodes(['crm:contract:update']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 客户跟进列表的搜索表单 */
|
||||
export function useCustomerFollowFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'followUpStatus',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: FOLLOWUP_STATUS,
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 待进入公海客户列表的搜索表单 */
|
||||
export function useCustomerPutPoolFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'sceneType',
|
||||
label: '归属',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: SCENE_TYPES,
|
||||
},
|
||||
defaultValue: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 今日需联系客户列表的搜索表单 */
|
||||
export function useCustomerTodayContactFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'contactStatus',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: CONTACT_STATUS,
|
||||
},
|
||||
defaultValue: 1,
|
||||
},
|
||||
{
|
||||
fieldName: 'sceneType',
|
||||
label: '归属',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: SCENE_TYPES,
|
||||
},
|
||||
defaultValue: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 客户列表的字段 */
|
||||
export function useCustomerColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '客户名称',
|
||||
minWidth: 160,
|
||||
slots: {
|
||||
default: 'name',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'source',
|
||||
title: '客户来源',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
title: '手机',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
title: '电话',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: '邮箱',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'level',
|
||||
title: '客户级别',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
title: '客户行业',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
title: '下次联系时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'lockStatus',
|
||||
title: '锁定状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'dealStatus',
|
||||
title: '成交状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'contactLastContent',
|
||||
title: '最后跟进记录',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
title: '地址',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'poolDay',
|
||||
title: '距离进入公海天数',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 100,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 回款审核列表的搜索表单 */
|
||||
export function useReceivableAuditFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'auditStatus',
|
||||
label: '合同状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: AUDIT_STATUS,
|
||||
},
|
||||
defaultValue: 10,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 回款审核列表的字段 */
|
||||
export function useReceivableAuditColumns<T = CrmReceivableApi.Receivable>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
title: '回款编号',
|
||||
minWidth: 180,
|
||||
fixed: 'left',
|
||||
slots: {
|
||||
default: 'no',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
minWidth: 120,
|
||||
slots: {
|
||||
default: 'customerName',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'contractNo',
|
||||
title: '合同编号',
|
||||
minWidth: 180,
|
||||
slots: {
|
||||
default: 'contractNo',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'returnTime',
|
||||
title: '回款日期',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '回款金额(元)',
|
||||
minWidth: 140,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
field: 'returnType',
|
||||
title: '回款方式',
|
||||
minWidth: 130,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'contract.totalPrice',
|
||||
title: '合同金额(元)',
|
||||
minWidth: 140,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'auditStatus',
|
||||
title: '回款状态',
|
||||
minWidth: 120,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
width: 140,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '角色',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'processDetail',
|
||||
text: '查看审批',
|
||||
show: hasAccessByCodes(['crm:receivable:update']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 回款计划提醒列表的搜索表单 */
|
||||
export function useReceivablePlanRemindFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'remindType',
|
||||
label: '合同状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: RECEIVABLE_REMIND_TYPE,
|
||||
},
|
||||
defaultValue: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 回款计划提醒列表的字段 */
|
||||
export function useReceivablePlanRemindColumns<T = CrmReceivableApi.Receivable>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
minWidth: 160,
|
||||
fixed: 'left',
|
||||
slots: {
|
||||
default: 'customerName',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'contractNo',
|
||||
title: '合同编号',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'period',
|
||||
title: '期数',
|
||||
minWidth: 160,
|
||||
slots: {
|
||||
default: 'period',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '计划回款金额(元)',
|
||||
minWidth: 120,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
field: 'returnTime',
|
||||
title: '计划回款日期',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'remindDays',
|
||||
title: '提前几天提醒',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'remindTime',
|
||||
title: '提醒日期',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'returnType',
|
||||
title: '回款方式',
|
||||
minWidth: 120,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'receivable.price',
|
||||
title: '实际回款金额(元)',
|
||||
minWidth: 160,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
field: 'receivable.returnTime',
|
||||
title: '实际回款日期',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
width: 140,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'customerName',
|
||||
nameTitle: '客户名称',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'receivableForm',
|
||||
text: '创建回款',
|
||||
show: hasAccessByCodes(['crm:receivable:create']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -10,17 +10,16 @@ import * as ContractApi from '#/api/crm/contract';
|
|||
import * as CustomerApi from '#/api/crm/customer';
|
||||
import * as ReceivableApi from '#/api/crm/receivable';
|
||||
import * as ReceivablePlanApi from '#/api/crm/receivable/plan';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
|
||||
import { useLeftSides } from './data';
|
||||
import ClueFollowList from './modules/ClueFollowList.vue';
|
||||
import ContractAuditList from './modules/ContractAuditList.vue';
|
||||
import ContractRemindList from './modules/ContractRemindList.vue';
|
||||
import CustomerFollowList from './modules/CustomerFollowList.vue';
|
||||
import CustomerPutPoolRemindList from './modules/CustomerPutPoolRemindList.vue';
|
||||
import CustomerTodayContactList from './modules/CustomerTodayContactList.vue';
|
||||
import ReceivableAuditList from './modules/ReceivableAuditList.vue';
|
||||
import ReceivablePlanRemindList from './modules/ReceivablePlanRemindList.vue';
|
||||
import ClueFollowList from './modules/clue-follow-list.vue';
|
||||
import ContractAuditList from './modules/contract-audit-list.vue';
|
||||
import ContractRemindList from './modules/contract-remind-list.vue';
|
||||
import CustomerFollowList from './modules/customer-follow-list.vue';
|
||||
import CustomerPutPoolRemindList from './modules/customer-put-pool-remind-list.vue';
|
||||
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' });
|
||||
|
||||
|
@ -92,12 +91,6 @@ onMounted(async () => {
|
|||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【通用】跟进记录、待办事项"
|
||||
url="https://doc.iocoder.cn/crm/follow-up/"
|
||||
/>
|
||||
</template>
|
||||
<div class="flex h-full w-full">
|
||||
<Card class="w-1/5">
|
||||
<List item-layout="horizontal" :data-source="leftSides">
|
||||
|
@ -105,11 +98,17 @@ onMounted(async () => {
|
|||
<List.Item>
|
||||
<List.Item.Meta>
|
||||
<template #title>
|
||||
<a @click="sideClick(item)"> {{ item.name }} </a>
|
||||
<a @click="sideClick(item)">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</template>
|
||||
</List.Item.Meta>
|
||||
<template #extra v-if="item.count.value && item.count.value > 0">
|
||||
<Badge :count="item.count.value" />
|
||||
<template #extra>
|
||||
<Badge
|
||||
:color="item.menu === leftMenu ? 'blue' : 'red'"
|
||||
:count="item.count.value"
|
||||
:show-zero="true"
|
||||
/>
|
||||
</template>
|
||||
</List.Item>
|
||||
</template>
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
<!-- 待回款提醒 -->
|
||||
<script lang="ts" setup>
|
||||
import type { OnActionClickParams } from '#/adapter/vxe-table';
|
||||
import type { CrmReceivableApi } from '#/api/crm/receivable';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getReceivablePage } from '#/api/crm/receivable';
|
||||
|
||||
import {
|
||||
useReceivablePlanRemindColumns,
|
||||
useReceivablePlanRemindFormSchema,
|
||||
} from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 打开回款详情 */
|
||||
function openDetail(row: CrmReceivableApi.Receivable) {
|
||||
push({ name: 'CrmReceivableDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 打开客户详情 */
|
||||
function openCustomerDetail(row: CrmReceivableApi.Receivable) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
/** 创建回款 */
|
||||
function openReceivableForm(row: CrmReceivableApi.Receivable) {
|
||||
// Todo: 打开创建回款
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<CrmReceivableApi.Receivable>) {
|
||||
switch (code) {
|
||||
case 'receivableForm': {
|
||||
openReceivableForm(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useReceivablePlanRemindFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useReceivablePlanRemindColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getReceivablePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid table-title="待回款提醒">
|
||||
<template #customerName="{ row }">
|
||||
<Button type="link" @click="openCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #period="{ row }">
|
||||
<Button type="link" @click="openDetail(row)">{{ row.period }}</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
|
@ -1,5 +1,6 @@
|
|||
<!-- 分配给我的线索 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmClueApi } from '#/api/crm/clue';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
@ -8,22 +9,34 @@ import { Button } from 'ant-design-vue';
|
|||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getCluePage } from '#/api/crm/clue';
|
||||
import { useGridColumns } from '#/views/crm/clue/data';
|
||||
|
||||
import { useClueFollowColumns, useClueFollowFormSchema } from '../data';
|
||||
import { FOLLOWUP_STATUS } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 打开线索详情 */
|
||||
function onDetail(row: CrmClueApi.Clue) {
|
||||
function handleDetail(row: CrmClueApi.Clue) {
|
||||
push({ name: 'CrmClueDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useClueFollowFormSchema(),
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'followUpStatus',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: FOLLOWUP_STATUS,
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useClueFollowColumns(),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
|
@ -45,14 +58,17 @@ const [Grid] = useVbenVxeGrid({
|
|||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
},
|
||||
} as VxeTableGridOptions<CrmClueApi.Clue>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid table-title="分配给我的线索">
|
||||
<Grid>
|
||||
<template #name="{ row }">
|
||||
<Button type="link" @click="onDetail(row)">{{ row.name }}</Button>
|
||||
<Button type="link" @click="handleDetail(row)">{{ row.name }}</Button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<Button type="link" @click="handleDetail(row)">查看详情</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
|
@ -1,21 +1,22 @@
|
|||
<!-- 待审核合同 -->
|
||||
<script lang="ts" setup>
|
||||
import type { OnActionClickParams } from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getContractPage } from '#/api/crm/contract';
|
||||
import { useGridColumns } from '#/views/crm/contract/data';
|
||||
|
||||
import { useContractAuditFormSchema, useContractColumns } from '../data';
|
||||
import { AUDIT_STATUS } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 查看审批 */
|
||||
function openProcessDetail(row: CrmContractApi.Contract) {
|
||||
function handleProcessDetail(row: CrmContractApi.Contract) {
|
||||
push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.processInstanceId },
|
||||
|
@ -23,43 +24,41 @@ function openProcessDetail(row: CrmContractApi.Contract) {
|
|||
}
|
||||
|
||||
/** 打开合同详情 */
|
||||
function openContractDetail(row: CrmContractApi.Contract) {
|
||||
function handleContractDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmContractDetail', params: { id: row.id } });
|
||||
}
|
||||
/** 打开客户详情 */
|
||||
function openCustomerDetail(row: CrmContractApi.Contract) {
|
||||
function handleCustomerDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 打开联系人详情 */
|
||||
function openContactDetail(row: CrmContractApi.Contract) {
|
||||
function handleContactDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmContactDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 打开商机详情 */
|
||||
function openBusinessDetail(row: CrmContractApi.Contract) {
|
||||
function handleBusinessDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmBusinessDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<CrmContractApi.Contract>) {
|
||||
switch (code) {
|
||||
case 'processDetail': {
|
||||
openProcessDetail(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useContractAuditFormSchema(),
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'auditStatus',
|
||||
label: '合同状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: AUDIT_STATUS,
|
||||
},
|
||||
defaultValue: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useContractColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
|
@ -81,31 +80,43 @@ const [Grid] = useVbenVxeGrid({
|
|||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
},
|
||||
} as VxeTableGridOptions<CrmContractApi.Contract>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid table-title="待审核合同">
|
||||
<Grid>
|
||||
<template #name="{ row }">
|
||||
<Button type="link" @click="openContractDetail(row)">
|
||||
<Button type="link" @click="handleContractDetail(row)">
|
||||
{{ row.name }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<Button type="link" @click="openCustomerDetail(row)">
|
||||
<Button type="link" @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #businessName="{ row }">
|
||||
<Button type="link" @click="openBusinessDetail(row)">
|
||||
<Button type="link" @click="handleBusinessDetail(row)">
|
||||
{{ row.businessName }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #contactName="{ row }">
|
||||
<Button type="link" @click="openContactDetail(row)">
|
||||
<Button type="link" @click="handleContactDetail(row)">
|
||||
{{ row.contactName }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '查看审批',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: handleProcessDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
|
@ -1,21 +1,22 @@
|
|||
<!-- 即将到期的合同 -->
|
||||
<script lang="ts" setup>
|
||||
import type { OnActionClickParams } from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getContractPage } from '#/api/crm/contract';
|
||||
import { useGridColumns } from '#/views/crm/contract/data';
|
||||
|
||||
import { useContractColumns, useContractRemindFormSchema } from '../data';
|
||||
import { CONTRACT_EXPIRY_TYPE } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 查看审批 */
|
||||
function openProcessDetail(row: CrmContractApi.Contract) {
|
||||
function handleProcessDetail(row: CrmContractApi.Contract) {
|
||||
push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.processInstanceId },
|
||||
|
@ -23,43 +24,41 @@ function openProcessDetail(row: CrmContractApi.Contract) {
|
|||
}
|
||||
|
||||
/** 打开合同详情 */
|
||||
function openContractDetail(row: CrmContractApi.Contract) {
|
||||
function handleContractDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmContractDetail', params: { id: row.id } });
|
||||
}
|
||||
/** 打开客户详情 */
|
||||
function openCustomerDetail(row: CrmContractApi.Contract) {
|
||||
function handleCustomerDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 打开联系人详情 */
|
||||
function openContactDetail(row: CrmContractApi.Contract) {
|
||||
function handleContactDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmContactDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 打开商机详情 */
|
||||
function openBusinessDetail(row: CrmContractApi.Contract) {
|
||||
function handleBusinessDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmBusinessDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<CrmContractApi.Contract>) {
|
||||
switch (code) {
|
||||
case 'processDetail': {
|
||||
openProcessDetail(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useContractRemindFormSchema(),
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'expiryType',
|
||||
label: '到期状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: CONTRACT_EXPIRY_TYPE,
|
||||
},
|
||||
defaultValue: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useContractColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
|
@ -81,31 +80,43 @@ const [Grid] = useVbenVxeGrid({
|
|||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
},
|
||||
} as VxeTableGridOptions<CrmContractApi.Contract>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid table-title="即将到期的合同">
|
||||
<Grid>
|
||||
<template #name="{ row }">
|
||||
<Button type="link" @click="openContractDetail(row)">
|
||||
<Button type="link" @click="handleContractDetail(row)">
|
||||
{{ row.name }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<Button type="link" @click="openCustomerDetail(row)">
|
||||
<Button type="link" @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #businessName="{ row }">
|
||||
<Button type="link" @click="openBusinessDetail(row)">
|
||||
<Button type="link" @click="handleBusinessDetail(row)">
|
||||
{{ row.businessName }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #contactName="{ row }">
|
||||
<Button type="link" @click="openContactDetail(row)">
|
||||
{{ row.contactName }}
|
||||
<template #signContactName="{ row }">
|
||||
<Button type="link" @click="handleContactDetail(row)">
|
||||
{{ row.signContactName }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '查看审批',
|
||||
type: 'link',
|
||||
auth: ['crm:contract:update'],
|
||||
onClick: handleProcessDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
|
@ -1,5 +1,6 @@
|
|||
<!-- 分配给我的客户 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
@ -8,22 +9,34 @@ import { Button } from 'ant-design-vue';
|
|||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getCustomerPage } from '#/api/crm/customer';
|
||||
import { useGridColumns } from '#/views/crm/customer/data';
|
||||
|
||||
import { useCustomerColumns, useCustomerFollowFormSchema } from '../data';
|
||||
import { FOLLOWUP_STATUS } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 打开客户详情 */
|
||||
function onDetail(row: CrmCustomerApi.Customer) {
|
||||
function handleDetail(row: CrmCustomerApi.Customer) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useCustomerFollowFormSchema(),
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'followUpStatus',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: FOLLOWUP_STATUS,
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useCustomerColumns(),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
|
@ -45,14 +58,17 @@ const [Grid] = useVbenVxeGrid({
|
|||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
},
|
||||
} as VxeTableGridOptions<CrmCustomerApi.Customer>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid table-title="分配给我的客户">
|
||||
<Grid>
|
||||
<template #name="{ row }">
|
||||
<Button type="link" @click="onDetail(row)">{{ row.name }}</Button>
|
||||
<Button type="link" @click="handleDetail(row)">{{ row.name }}</Button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<Button type="link" @click="handleDetail(row)">查看详情</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
|
@ -1,5 +1,6 @@
|
|||
<!-- 待进入公海的客户 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
@ -8,22 +9,34 @@ import { Button } from 'ant-design-vue';
|
|||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getCustomerPage } from '#/api/crm/customer';
|
||||
import { useGridColumns } from '#/views/crm/customer/data';
|
||||
|
||||
import { useCustomerColumns, useCustomerPutPoolFormSchema } from '../data';
|
||||
import { SCENE_TYPES } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 打开客户详情 */
|
||||
function onDetail(row: CrmCustomerApi.Customer) {
|
||||
function handleDetail(row: CrmCustomerApi.Customer) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useCustomerPutPoolFormSchema(),
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'sceneType',
|
||||
label: '归属',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: SCENE_TYPES,
|
||||
},
|
||||
defaultValue: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useCustomerColumns(),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
|
@ -45,14 +58,17 @@ const [Grid] = useVbenVxeGrid({
|
|||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
},
|
||||
} as VxeTableGridOptions<CrmCustomerApi.Customer>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid table-title="待进入公海的客户">
|
||||
<Grid>
|
||||
<template #name="{ row }">
|
||||
<Button type="link" @click="onDetail(row)">{{ row.name }}</Button>
|
||||
<Button type="link" @click="handleDetail(row)">{{ row.name }}</Button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<Button type="link" @click="handleDetail(row)">查看详情</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
|
@ -1,5 +1,6 @@
|
|||
<!-- 今日需联系客户 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
@ -8,22 +9,44 @@ import { Button } from 'ant-design-vue';
|
|||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getCustomerPage } from '#/api/crm/customer';
|
||||
import { useGridColumns } from '#/views/crm/customer/data';
|
||||
|
||||
import { useCustomerColumns, useCustomerTodayContactFormSchema } from '../data';
|
||||
import { CONTACT_STATUS, SCENE_TYPES } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 打开客户详情 */
|
||||
function onDetail(row: CrmCustomerApi.Customer) {
|
||||
function handleDetail(row: CrmCustomerApi.Customer) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useCustomerTodayContactFormSchema(),
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'contactStatus',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: CONTACT_STATUS,
|
||||
},
|
||||
defaultValue: 1,
|
||||
},
|
||||
{
|
||||
fieldName: 'sceneType',
|
||||
label: '归属',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: SCENE_TYPES,
|
||||
},
|
||||
defaultValue: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useCustomerColumns(),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
|
@ -45,14 +68,17 @@ const [Grid] = useVbenVxeGrid({
|
|||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
},
|
||||
} as VxeTableGridOptions<CrmCustomerApi.Customer>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid table-title="今日需联系客户">
|
||||
<Grid>
|
||||
<template #name="{ row }">
|
||||
<Button type="link" @click="onDetail(row)">{{ row.name }}</Button>
|
||||
<Button type="link" @click="handleDetail(row)">{{ row.name }}</Button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<Button type="link" @click="handleDetail(row)">查看详情</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
|
@ -1,6 +1,6 @@
|
|||
<!-- 待审核回款 -->
|
||||
<script lang="ts" setup>
|
||||
import type { OnActionClickParams } from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmReceivableApi } from '#/api/crm/receivable';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
@ -9,16 +9,14 @@ import { Button } from 'ant-design-vue';
|
|||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getReceivablePage } from '#/api/crm/receivable';
|
||||
import { useGridColumns } from '#/views/crm/receivable/data';
|
||||
|
||||
import {
|
||||
useReceivableAuditColumns,
|
||||
useReceivableAuditFormSchema,
|
||||
} from '../data';
|
||||
import { AUDIT_STATUS } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 查看审批 */
|
||||
function openProcessDetail(row: CrmReceivableApi.Receivable) {
|
||||
function handleProcessDetail(row: CrmReceivableApi.Receivable) {
|
||||
push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.processInstanceId },
|
||||
|
@ -26,39 +24,37 @@ function openProcessDetail(row: CrmReceivableApi.Receivable) {
|
|||
}
|
||||
|
||||
/** 打开回款详情 */
|
||||
function openDetail(row: CrmReceivableApi.Receivable) {
|
||||
function handleDetail(row: CrmReceivableApi.Receivable) {
|
||||
push({ name: 'CrmReceivableDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 打开客户详情 */
|
||||
function openCustomerDetail(row: CrmReceivableApi.Receivable) {
|
||||
function handleCustomerDetail(row: CrmReceivableApi.Receivable) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
/** 打开合同详情 */
|
||||
function openContractDetail(row: CrmReceivableApi.Receivable) {
|
||||
function handleContractDetail(row: CrmReceivableApi.Receivable) {
|
||||
push({ name: 'CrmContractDetail', params: { id: row.contractId } });
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<CrmReceivableApi.Receivable>) {
|
||||
switch (code) {
|
||||
case 'processDetail': {
|
||||
openProcessDetail(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useReceivableAuditFormSchema(),
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'auditStatus',
|
||||
label: '合同状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: AUDIT_STATUS,
|
||||
},
|
||||
defaultValue: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useReceivableAuditColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
|
@ -79,26 +75,29 @@ const [Grid] = useVbenVxeGrid({
|
|||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
},
|
||||
} as VxeTableGridOptions<CrmReceivableApi.Receivable>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid table-title="待审核回款">
|
||||
<Grid>
|
||||
<template #no="{ row }">
|
||||
<Button type="link" @click="openDetail(row)">
|
||||
<Button type="link" @click="handleDetail(row)">
|
||||
{{ row.no }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<Button type="link" @click="openCustomerDetail(row)">
|
||||
<Button type="link" @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #contractNo="{ row }">
|
||||
<Button type="link" @click="openContractDetail(row)">
|
||||
<Button type="link" @click="handleContractDetail(row)">
|
||||
{{ row.contractNo }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<Button type="link" @click="handleProcessDetail(row)">查看审批</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
|
@ -0,0 +1,101 @@
|
|||
<!-- 待回款提醒 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmReceivablePlanApi } from '#/api/crm/receivable/plan';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getReceivablePlanPage } from '#/api/crm/receivable/plan';
|
||||
import Form from '#/views/crm/receivable/modules/form.vue';
|
||||
import { useGridColumns } from '#/views/crm/receivable/plan/data';
|
||||
|
||||
import { RECEIVABLE_REMIND_TYPE } from '../data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 打开回款详情 */
|
||||
function handleDetail(row: CrmReceivablePlanApi.Plan) {
|
||||
push({ name: 'CrmReceivableDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 打开客户详情 */
|
||||
function handleCustomerDetail(row: CrmReceivablePlanApi.Plan) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
/** 创建回款 */
|
||||
function handleCreateReceivable(row: CrmReceivablePlanApi.Plan) {
|
||||
formModalApi.setData({ plan: row }).open();
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'remindType',
|
||||
label: '合同状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: RECEIVABLE_REMIND_TYPE,
|
||||
},
|
||||
defaultValue: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getReceivablePlanPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmReceivablePlanApi.Plan>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FormModal />
|
||||
<Grid>
|
||||
<template #customerName="{ row }">
|
||||
<Button type="link" @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #period="{ row }">
|
||||
<Button type="link" @click="handleDetail(row)">{{ row.period }}</Button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<Button type="link" @click="handleCreateReceivable(row)">
|
||||
创建回款
|
||||
</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
|
@ -1,12 +1,5 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmBusinessApi } from '#/api/crm/business';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
|
@ -37,7 +30,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
controlsPosition: 'right',
|
||||
placeholder: '请输入商机金额',
|
||||
},
|
||||
rules: 'required',
|
||||
|
@ -58,16 +50,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
label: '备注',
|
||||
component: 'Textarea',
|
||||
},
|
||||
{
|
||||
fieldName: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
showTime: false,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -79,133 +61,85 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||
label: '商机名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = CrmBusinessApi.Business>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '商机名称',
|
||||
minWidth: 160,
|
||||
fixed: 'left',
|
||||
slots: {
|
||||
default: 'name',
|
||||
},
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
minWidth: 120,
|
||||
fixed: 'left',
|
||||
slots: {
|
||||
default: 'customerName',
|
||||
},
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
title: '商机金额(元)',
|
||||
minWidth: 140,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
field: 'dealTime',
|
||||
title: '预计成交日期',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDate',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
title: '下次联系时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDate',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'statusTypeName',
|
||||
title: '商机状态组',
|
||||
minWidth: 140,
|
||||
fixed: 'right',
|
||||
},
|
||||
{
|
||||
field: 'statusName',
|
||||
title: '商机阶段',
|
||||
minWidth: 120,
|
||||
fixed: 'right',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
width: 130,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '商机',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['crm:business:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['crm:business:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmBusinessApi } from '#/api/crm/business';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Download, Plus } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteBusiness,
|
||||
exportBusiness,
|
||||
|
@ -38,23 +34,23 @@ function onRefresh() {
|
|||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function onExport() {
|
||||
async function handleExport() {
|
||||
const data = await exportBusiness(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '商机.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建商机 */
|
||||
function onCreate() {
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑商机 */
|
||||
function onEdit(row: CrmBusinessApi.Business) {
|
||||
function handleEdit(row: CrmBusinessApi.Business) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除商机 */
|
||||
async function onDelete(row: CrmBusinessApi.Business) {
|
||||
async function handleDelete(row: CrmBusinessApi.Business) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
|
@ -70,38 +66,21 @@ async function onDelete(row: CrmBusinessApi.Business) {
|
|||
}
|
||||
|
||||
/** 查看商机详情 */
|
||||
function onDetail(row: CrmBusinessApi.Business) {
|
||||
function handleDetail(row: CrmBusinessApi.Business) {
|
||||
push({ name: 'CrmBusinessDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 查看客户详情 */
|
||||
function onCustomerDetail(row: CrmBusinessApi.Business) {
|
||||
function handleCustomerDetail(row: CrmBusinessApi.Business) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<CrmBusinessApi.Business>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
|
@ -142,34 +121,59 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="商机列表">
|
||||
<template #toolbar-tools>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['crm:business:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['商机']) }}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['crm:business:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</Button>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['商机']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:business:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['crm:business:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<Button type="link" @click="onDetail(row)">
|
||||
<Button type="link" @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<Button type="link" @click="onCustomerDetail(row)">
|
||||
<Button type="link" @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:business:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['crm:business:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p>待完成</p>
|
||||
</div>
|
||||
<div>businessInfo</div>
|
||||
</template>
|
|
@ -0,0 +1,4 @@
|
|||
<script lang="ts" setup></script>
|
||||
<template>
|
||||
<div>businessList</div>
|
||||
</template>
|
|
@ -1,18 +1,11 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmBusinessStatusApi } from '#/api/crm/business/status';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import {
|
||||
CommonStatusEnum,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
import { getDeptList } from '#/api/system/dept';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
|
@ -34,12 +27,16 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
{
|
||||
fieldName: 'deptIds',
|
||||
label: '应用部门',
|
||||
component: 'TreeSelect',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const data = await getDeptList();
|
||||
return handleTree(data);
|
||||
},
|
||||
multiple: true,
|
||||
treeCheckable: true,
|
||||
showCheckedStrategy: 'SHOW_PARENT',
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
placeholder: '请选择应用部门',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -56,79 +53,33 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '状态组名',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = CrmBusinessStatusApi.BusinessStatus>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '状态组名',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'deptNames',
|
||||
title: '应用部门',
|
||||
minWidth: 200,
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue?.length > 0 ? cellValue.join(' ') : '全公司';
|
||||
},
|
||||
formatter: ({ cellValue }) =>
|
||||
cellValue?.length > 0 ? cellValue.join(' ') : '全公司',
|
||||
},
|
||||
{
|
||||
field: 'creator',
|
||||
title: '创建人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'TableAction',
|
||||
props: {
|
||||
actions: [
|
||||
{
|
||||
label: '编辑',
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['crm:business-status:update']),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['crm:business-status:delete']),
|
||||
},
|
||||
],
|
||||
onActionClick,
|
||||
},
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmBusinessStatusApi } from '#/api/crm/business/status';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteBusinessStatus,
|
||||
getBusinessStatusPage,
|
||||
|
@ -18,7 +14,7 @@ import {
|
|||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import { useGridColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
|
@ -32,12 +28,12 @@ function onRefresh() {
|
|||
}
|
||||
|
||||
/** 创建商机状态 */
|
||||
function onCreate() {
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 删除商机状态 */
|
||||
async function onDelete(row: CrmBusinessStatusApi.BusinessStatus) {
|
||||
async function handleDelete(row: CrmBusinessStatusApi.BusinessStatus) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
|
@ -53,33 +49,13 @@ async function onDelete(row: CrmBusinessStatusApi.BusinessStatus) {
|
|||
}
|
||||
|
||||
/** 编辑商机状态 */
|
||||
function onEdit(row: CrmBusinessStatusApi.BusinessStatus) {
|
||||
function handleEdit(row: CrmBusinessStatusApi.BusinessStatus) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<CrmBusinessStatusApi.BusinessStatus>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
|
@ -120,14 +96,41 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="商机状态列表">
|
||||
<template #toolbar-tools>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['crm:business-status:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['商机状态']) }}
|
||||
</Button>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['商机状态']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['system:post:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:business-status:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['crm:business-status:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmClueApi } from '#/api/crm/clue';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { getAreaTree } from '#/api/system/area';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
|
@ -164,14 +160,11 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = CrmClueApi.Clue>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '线索名称',
|
||||
minWidth: 160,
|
||||
fixed: 'left',
|
||||
slots: {
|
||||
default: 'name',
|
||||
|
@ -180,7 +173,6 @@ export function useGridColumns<T = CrmClueApi.Clue>(
|
|||
{
|
||||
field: 'source',
|
||||
title: '线索来源',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
|
||||
|
@ -189,27 +181,22 @@ export function useGridColumns<T = CrmClueApi.Clue>(
|
|||
{
|
||||
field: 'mobile',
|
||||
title: '手机',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
title: '电话',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: '邮箱',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
title: '地址',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
title: '客户行业',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
|
||||
|
@ -218,7 +205,6 @@ export function useGridColumns<T = CrmClueApi.Clue>(
|
|||
{
|
||||
field: 'level',
|
||||
title: '客户级别',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
|
||||
|
@ -227,66 +213,40 @@ export function useGridColumns<T = CrmClueApi.Clue>(
|
|||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
title: '下次联系时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
width: 130,
|
||||
width: 140,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '线索',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['crm:clue:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['crm:clue:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmClueApi } from '#/api/crm/clue';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Download, Plus } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
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';
|
||||
|
@ -33,55 +29,43 @@ function onRefresh() {
|
|||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function onExport() {
|
||||
const data = await exportClue(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '线索.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建线索 */
|
||||
function onCreate() {
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑线索 */
|
||||
function onEdit(row: CrmClueApi.Clue) {
|
||||
function handleEdit(row: CrmClueApi.Clue) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除线索 */
|
||||
async function onDelete(row: CrmClueApi.Clue) {
|
||||
async function handleDelete(row: CrmClueApi.Clue) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await deleteClue(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看线索详情 */
|
||||
function onDetail(row: CrmClueApi.Clue) {
|
||||
push({ name: 'CrmClueDetail', params: { id: row.id } });
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportClue(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '线索.xls', source: data });
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({ code, row }: OnActionClickParams<CrmClueApi.Clue>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/** 查看线索详情 */
|
||||
function handleDetail(row: CrmClueApi.Clue) {
|
||||
push({ name: 'CrmClueDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
|
@ -89,7 +73,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
|
@ -130,29 +114,54 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="线索列表">
|
||||
<template #toolbar-tools>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['crm:clue:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['线索']) }}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['crm:clue:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</Button>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['线索']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:clue:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['crm:clue:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<Button type="link" @click="onDetail(row)">
|
||||
<Button type="link" @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:clue:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['crm:clue:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
@ -5,16 +5,18 @@ import { defineAsyncComponent, onMounted, ref } from 'vue';
|
|||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { ArrowLeft } from '@vben/icons';
|
||||
|
||||
import { Button, Card, Modal, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { getClue, transformClue } from '#/api/crm/clue';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { PermissionList, TransferForm } from '#/views/crm/permission';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
import ClueForm from './form.vue';
|
||||
import TransferForm from './transfer.vue';
|
||||
|
||||
const ClueDetailsInfo = defineAsyncComponent(() => import('./detail-info.vue'));
|
||||
|
||||
|
@ -22,6 +24,7 @@ const loading = ref(false);
|
|||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const tabs = useTabs();
|
||||
|
||||
const clueId = ref(0);
|
||||
|
||||
|
@ -57,22 +60,23 @@ async function loadClueDetail() {
|
|||
}
|
||||
|
||||
/** 返回列表页 */
|
||||
function onBack() {
|
||||
function handleBack() {
|
||||
tabs.closeCurrentTab();
|
||||
router.push('/crm/clue');
|
||||
}
|
||||
|
||||
/** 编辑线索 */
|
||||
function onEdit() {
|
||||
function handleEdit() {
|
||||
formModalApi.setData({ id: clueId }).open();
|
||||
}
|
||||
|
||||
/** 转移线索 */
|
||||
function onTransfer() {
|
||||
transferModalApi.setData({ id: clueId }).open();
|
||||
function handleTransfer() {
|
||||
transferModalApi.setData({ bizType: BizTypeEnum.CRM_CLUE }).open();
|
||||
}
|
||||
|
||||
/** 转化为客户 */
|
||||
async function onTransform() {
|
||||
async function handleTransform() {
|
||||
try {
|
||||
await Modal.confirm({
|
||||
title: '提示',
|
||||
|
@ -99,14 +103,14 @@ onMounted(async () => {
|
|||
<Page auto-content-height :title="clue?.name" :loading="loading">
|
||||
<template #extra>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button @click="onBack">
|
||||
<Button @click="handleBack">
|
||||
<ArrowLeft class="size-5" />
|
||||
返回
|
||||
</Button>
|
||||
<Button
|
||||
v-if="permissionListRef?.validateWrite"
|
||||
type="primary"
|
||||
@click="onEdit"
|
||||
@click="handleEdit"
|
||||
v-access:code="['crm:clue:update']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.edit') }}
|
||||
|
@ -114,7 +118,7 @@ onMounted(async () => {
|
|||
<Button
|
||||
v-if="permissionListRef?.validateOwnerUser"
|
||||
type="primary"
|
||||
@click="onTransfer"
|
||||
@click="handleTransfer"
|
||||
v-access:code="['crm:clue:update']"
|
||||
>
|
||||
转移
|
||||
|
@ -122,7 +126,7 @@ onMounted(async () => {
|
|||
<Button
|
||||
v-if="permissionListRef?.validateOwnerUser && !clue?.transformStatus"
|
||||
type="primary"
|
||||
@click="onTransform"
|
||||
@click="handleTransform"
|
||||
v-access:code="['crm:clue:update']"
|
||||
>
|
||||
转化为客户
|
||||
|
@ -141,7 +145,13 @@ onMounted(async () => {
|
|||
<ClueDetailsInfo :clue="clue" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="团队成员" key="3">
|
||||
<div>团队成员</div>
|
||||
<PermissionList
|
||||
ref="permissionListRef"
|
||||
:biz-id="clue.id!"
|
||||
:biz-type="BizTypeEnum.CRM_CLUE"
|
||||
:show-action="true"
|
||||
@quit-team="handleBack"
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="操作日志" key="4">
|
||||
<div>操作日志</div>
|
||||
|
|
|
@ -0,0 +1,385 @@
|
|||
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';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '联系人姓名',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'ownerUserId',
|
||||
label: '负责人',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: () => getSimpleUserList(),
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
label: '客户名称',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: () => getCustomerSimpleList(),
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'mobile',
|
||||
label: '手机',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'telephone',
|
||||
label: '电话',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'email',
|
||||
label: '邮箱',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'wechat',
|
||||
label: '微信',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'qq',
|
||||
label: 'QQ',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'post',
|
||||
label: '职位',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'master',
|
||||
label: '关键决策人',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sex',
|
||||
label: '性别',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'parentId',
|
||||
label: '直属上级',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: () => getSimpleContactList(),
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'areaId',
|
||||
label: '地址',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: () => getAreaTree(),
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'detailAddress',
|
||||
label: '详细地址',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '客户',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: () => getCustomerSimpleList(),
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '姓名',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'mobile',
|
||||
label: '手机号',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'telephone',
|
||||
label: '电话',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'wechat',
|
||||
label: '微信',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'email',
|
||||
label: '电子邮箱',
|
||||
component: 'Input',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
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 },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'parentId',
|
||||
title: '直属上级',
|
||||
slots: { default: 'parentId' },
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
title: '下次联系时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [...useDetailBaseSchema(), ...useDetailSystemSchema()];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
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: 'wechat',
|
||||
label: '微信',
|
||||
},
|
||||
{
|
||||
field: 'qq',
|
||||
label: 'QQ',
|
||||
},
|
||||
{
|
||||
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: 'areaName',
|
||||
label: '地址',
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
label: '详细地址',
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
content: (data) => formatDateTime(data?.contactNextTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的系统字段 */
|
||||
export function useDetailSystemSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
label: '所属部门',
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
label: '最后跟进时间',
|
||||
content: (data) => formatDateTime(data?.contactLastTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data) => formatDateTime(data?.createTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
label: '更新时间',
|
||||
content: (data) => formatDateTime(data?.updateTime) as string,
|
||||
},
|
||||
];
|
||||
}
|
|
@ -1,38 +1,207 @@
|
|||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmContactApi } from '#/api/crm/contact';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { Button, message, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteContact,
|
||||
exportContact,
|
||||
getContactPage,
|
||||
} from '#/api/crm/contact';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const { push } = useRouter();
|
||||
const sceneType = ref('1');
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportContact(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '联系人.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建联系人 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑联系人 */
|
||||
function handleEdit(row: CrmContactApi.Contact) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除联系人 */
|
||||
async function handleDelete(row: CrmContactApi.Contact) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await deleteContact(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看联系人详情 */
|
||||
function handleDetail(row: CrmContactApi.Contact) {
|
||||
push({ name: 'CrmContactDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 查看客户详情 */
|
||||
function handleCustomerDetail(row: CrmContactApi.Contact) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getContactPage({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmContactApi.Contact>,
|
||||
});
|
||||
|
||||
function onChangeSceneType(key: number | string) {
|
||||
sceneType.value = key.toString();
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【客户】客户管理、公海客户"
|
||||
url="https://doc.iocoder.cn/crm/customer/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/contact/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/contact/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【客户】客户管理、公海客户"
|
||||
url="https://doc.iocoder.cn/crm/customer/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid>
|
||||
<template #top>
|
||||
<Tabs class="border-none" @change="onChangeSceneType">
|
||||
<Tabs.TabPane tab="我负责的" key="1" />
|
||||
<Tabs.TabPane tab="我参与的" key="2" />
|
||||
<Tabs.TabPane tab="下属负责的" key="3" />
|
||||
</Tabs>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['联系人']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:contact:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['crm:contact:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<Button type="link" @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<Button type="link" @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #parentId="{ row }">
|
||||
<Button type="link" @click="handleDetail(row)">
|
||||
{{ row.parentId }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:contact:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['crm:contact:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<script lang="ts" setup></script>
|
||||
<template>
|
||||
<div>contactInfo</div>
|
||||
</template>
|
|
@ -0,0 +1,4 @@
|
|||
<script lang="ts" setup></script>
|
||||
<template>
|
||||
<div>contactList</div>
|
||||
</template>
|
|
@ -0,0 +1,81 @@
|
|||
<script lang="ts" setup>
|
||||
import type { CrmContactApi } from '#/api/crm/contact';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { createContact, getContact, updateContact } from '#/api/crm/contact';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<CrmContactApi.Contact>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['联系人'])
|
||||
: $t('ui.actionTitle.create', ['联系人']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
// 一共2列
|
||||
wrapperClass: 'grid-cols-2',
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as CrmContactApi.Contact;
|
||||
try {
|
||||
await (formData.value?.id ? updateContact(data) : createContact(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<CrmContactApi.Contact>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getContact(data.id as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-[40%]">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
|
@ -1,38 +1,104 @@
|
|||
<script lang="ts" setup>
|
||||
import type { CrmContractConfigApi } from '#/api/crm/contract/config';
|
||||
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { Card, message } from 'ant-design-vue';
|
||||
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
getContractConfig,
|
||||
saveContractConfig,
|
||||
} from '#/api/crm/contract/config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
labelClass: 'w-2/6',
|
||||
},
|
||||
layout: 'horizontal',
|
||||
wrapperClass: 'grid-cols-1',
|
||||
actionWrapperClass: 'text-center',
|
||||
schema: [
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'notifyEnabled',
|
||||
label: '提前提醒设置',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '提醒', value: true },
|
||||
{ label: '不提醒', value: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'notifyDays',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 0,
|
||||
},
|
||||
renderComponentContent: () => ({
|
||||
addonBefore: () => '提前',
|
||||
addonAfter: () => '天提醒',
|
||||
}),
|
||||
dependencies: {
|
||||
triggerFields: ['notifyEnabled'],
|
||||
trigger(values) {
|
||||
values.notifyDays = undefined;
|
||||
},
|
||||
show: (value) => value.notifyEnabled,
|
||||
},
|
||||
},
|
||||
],
|
||||
// 提交函数
|
||||
handleSubmit: onSubmit,
|
||||
});
|
||||
|
||||
async function onSubmit() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as CrmContractConfigApi.Config;
|
||||
if (!data.notifyEnabled) {
|
||||
data.notifyDays = undefined;
|
||||
}
|
||||
formApi.setValues(data);
|
||||
try {
|
||||
await saveContractConfig(data);
|
||||
// 关闭并提示
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
formApi.setValues(data);
|
||||
}
|
||||
}
|
||||
|
||||
async function getConfigInfo() {
|
||||
try {
|
||||
const res = await getContractConfig();
|
||||
formApi.setValues(res);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getConfigInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【合同】合同管理、合同提醒"
|
||||
url="https://doc.iocoder.cn/crm/contract/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/contract/config/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/contract/config/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<Card title="合同配置设置">
|
||||
<Form class="w-1/4" />
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,276 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { getSimpleBusinessList } from '#/api/crm/business';
|
||||
import { getSimpleContactList } from '#/api/crm/contact';
|
||||
import { getCustomerSimpleList } from '#/api/crm/customer';
|
||||
import { floatToFixed2 } from '#/utils';
|
||||
import { DICT_TYPE } from '#/utils/dict';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'no',
|
||||
label: '合同编号',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入合同编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '合同名称',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入合同名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
label: '客户',
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择客户',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'businessId',
|
||||
label: '商机',
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: getSimpleBusinessList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择商机',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'totalPrice',
|
||||
label: '合同金额',
|
||||
component: 'InputNumber',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入合同金额',
|
||||
min: 0,
|
||||
precision: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'orderDate',
|
||||
label: '下单时间',
|
||||
component: 'DatePicker',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请选择下单时间',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'startTime',
|
||||
label: '合同开始时间',
|
||||
component: 'DatePicker',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请选择合同开始时间',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'endTime',
|
||||
label: '合同结束时间',
|
||||
component: 'DatePicker',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请选择合同结束时间',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'signContactId',
|
||||
label: '客户签约人',
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: getSimpleContactList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择客户签约人',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
rows: 4,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'no',
|
||||
label: '合同编号',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '合同名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
label: '客户',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择客户',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
title: '合同编号',
|
||||
field: 'no',
|
||||
minWidth: 150,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '合同名称',
|
||||
field: 'name',
|
||||
minWidth: 150,
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
title: '客户名称',
|
||||
field: 'customerName',
|
||||
minWidth: 150,
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
title: '商机名称',
|
||||
field: 'businessName',
|
||||
minWidth: 150,
|
||||
slots: { default: 'businessName' },
|
||||
},
|
||||
{
|
||||
title: '合同金额(元)',
|
||||
field: 'totalPrice',
|
||||
minWidth: 150,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
title: '下单时间',
|
||||
field: 'orderDate',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '合同开始时间',
|
||||
field: 'startTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '合同结束时间',
|
||||
field: 'endTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '客户签约人',
|
||||
field: 'signContactName',
|
||||
minWidth: 150,
|
||||
slots: { default: 'signContactName' },
|
||||
},
|
||||
{
|
||||
title: '公司签约人',
|
||||
field: 'signUserName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '已回款金额(元)',
|
||||
field: 'totalReceivablePrice',
|
||||
minWidth: 150,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
title: '未回款金额(元)',
|
||||
field: 'unpaidPrice',
|
||||
minWidth: 150,
|
||||
formatter: ({ row }) => {
|
||||
return floatToFixed2(row.totalPrice - row.totalReceivablePrice);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最后跟进时间',
|
||||
field: 'contactLastTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
field: 'ownerUserName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '所属部门',
|
||||
field: 'ownerUserDeptName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
field: 'updateTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
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 },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
field: 'actions',
|
||||
fixed: 'right',
|
||||
width: 130,
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
|
@ -1,38 +1,265 @@
|
|||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { Button, message, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteContract,
|
||||
exportContract,
|
||||
getContractPage,
|
||||
submitContract,
|
||||
} from '#/api/crm/contract';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const { push } = useRouter();
|
||||
const sceneType = ref('1');
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportContract(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '合同.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建合同 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑合同 */
|
||||
function handleEdit(row: CrmContractApi.Contract) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除合同 */
|
||||
async function handleDelete(row: CrmContractApi.Contract) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await deleteContract(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交审核 */
|
||||
async function handleSubmit(row: CrmContractApi.Contract) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.submitting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await submitContract(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.submitSuccess', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看合同详情 */
|
||||
function handleDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmContractDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 查看客户详情 */
|
||||
function handleCustomerDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
|
||||
}
|
||||
|
||||
/** 查看联系人详情 */
|
||||
function handleContactDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmContactDetail', params: { id: row.signContactId } });
|
||||
}
|
||||
|
||||
/** 查看商机详情 */
|
||||
function handleBusinessDetail(row: CrmContractApi.Contract) {
|
||||
push({ name: 'CrmBusinessDetail', params: { id: row.businessId } });
|
||||
}
|
||||
|
||||
/** 查看审批详情 */
|
||||
function handleProcessDetail(row: CrmContractApi.Contract) {
|
||||
push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.processInstanceId },
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getContractPage({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sceneType: sceneType.value,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmContractApi.Contract>,
|
||||
});
|
||||
|
||||
function onChangeSceneType(key: number | string) {
|
||||
sceneType.value = key.toString();
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【合同】合同管理、合同提醒"
|
||||
url="https://doc.iocoder.cn/crm/contract/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/contract/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/contract/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【合同】合同管理、合同提醒"
|
||||
url="https://doc.iocoder.cn/crm/contract/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid>
|
||||
<template #top>
|
||||
<Tabs class="border-none" @change="onChangeSceneType">
|
||||
<Tabs.TabPane tab="我负责的" key="1" />
|
||||
<Tabs.TabPane tab="我参与的" key="2" />
|
||||
<Tabs.TabPane tab="下属负责的" key="3" />
|
||||
</Tabs>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['合同']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:contract:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['crm:contract:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<Button type="link" @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #customerName="{ row }">
|
||||
<Button type="link" @click="handleCustomerDetail(row)">
|
||||
{{ row.customerName }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #businessName="{ row }">
|
||||
<Button type="link" @click="handleBusinessDetail(row)">
|
||||
{{ row.businessName }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #signContactName="{ row }">
|
||||
<Button type="link" @click="handleContactDetail(row)">
|
||||
{{ row.signContactName }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:contract:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
ifShow: row.auditStatus === 0,
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[
|
||||
{
|
||||
label: '提交审核',
|
||||
type: 'link',
|
||||
auth: ['crm:contract:update'],
|
||||
onClick: handleSubmit.bind(null, row),
|
||||
ifShow: row.auditStatus === 0,
|
||||
},
|
||||
{
|
||||
label: '查看审批',
|
||||
type: 'link',
|
||||
auth: ['crm:contract:update'],
|
||||
onClick: handleProcessDetail.bind(null, row),
|
||||
ifShow: row.auditStatus !== 0,
|
||||
},
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'link',
|
||||
auth: ['crm:contract:query'],
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
auth: ['crm:contract:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<script lang="ts" setup></script>
|
||||
<template>
|
||||
<div>contractInfo</div>
|
||||
</template>
|
|
@ -0,0 +1,4 @@
|
|||
<script lang="ts" setup></script>
|
||||
<template>
|
||||
<div>contractList</div>
|
||||
</template>
|
|
@ -0,0 +1,85 @@
|
|||
<script lang="ts" setup>
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
createContract,
|
||||
getContract,
|
||||
updateContract,
|
||||
} from '#/api/crm/contract';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<CrmContractApi.Contract>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['合同'])
|
||||
: $t('ui.actionTitle.create', ['合同']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
// 一共2列
|
||||
wrapperClass: 'grid-cols-2',
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as CrmContractApi.Contract;
|
||||
try {
|
||||
await (formData.value?.id ? updateContract(data) : createContract(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<CrmContractApi.Contract>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getContract(data.id as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-[40%]">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
|
@ -1,19 +1,16 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
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';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
|
@ -48,9 +45,13 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
{
|
||||
fieldName: 'ownerUserId',
|
||||
label: '负责人',
|
||||
component: 'Select',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: 'getSimpleUserList',
|
||||
api: () => getSimpleUserList(),
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
|
@ -153,14 +154,11 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = CrmCustomerApi.Customer>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '客户名称',
|
||||
minWidth: 160,
|
||||
fixed: 'left',
|
||||
slots: {
|
||||
default: 'name',
|
||||
|
@ -169,7 +167,6 @@ export function useGridColumns<T = CrmCustomerApi.Customer>(
|
|||
{
|
||||
field: 'source',
|
||||
title: '客户来源',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
|
||||
|
@ -178,27 +175,22 @@ export function useGridColumns<T = CrmCustomerApi.Customer>(
|
|||
{
|
||||
field: 'mobile',
|
||||
title: '手机',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
title: '电话',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: '邮箱',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
title: '地址',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
title: '客户行业',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
|
||||
|
@ -207,7 +199,6 @@ export function useGridColumns<T = CrmCustomerApi.Customer>(
|
|||
{
|
||||
field: 'level',
|
||||
title: '客户级别',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
|
||||
|
@ -216,61 +207,36 @@ export function useGridColumns<T = CrmCustomerApi.Customer>(
|
|||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
title: '下次联系时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'contactLastTime',
|
||||
title: '最后跟进时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
width: 130,
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '线索',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['crm:clue:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['crm:clue:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Download, Plus } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { Button, message, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteCustomer,
|
||||
exportCustomer,
|
||||
|
@ -40,23 +36,23 @@ function onRefresh() {
|
|||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function onExport() {
|
||||
async function handleExport() {
|
||||
const data = await exportCustomer(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '客户.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建客户 */
|
||||
function onCreate() {
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑客户 */
|
||||
function onEdit(row: CrmCustomerApi.Customer) {
|
||||
function handleEdit(row: CrmCustomerApi.Customer) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除客户 */
|
||||
async function onDelete(row: CrmCustomerApi.Customer) {
|
||||
async function handleDelete(row: CrmCustomerApi.Customer) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
|
@ -74,33 +70,16 @@ async function onDelete(row: CrmCustomerApi.Customer) {
|
|||
}
|
||||
|
||||
/** 查看客户详情 */
|
||||
function onDetail(row: CrmCustomerApi.Customer) {
|
||||
function handleDetail(row: CrmCustomerApi.Customer) {
|
||||
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<CrmCustomerApi.Customer>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
|
@ -145,9 +124,8 @@ function onChangeSceneType(key: number | string) {
|
|||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid>
|
||||
<template #toolbar-actions>
|
||||
<template #top>
|
||||
<Tabs class="border-none" @change="onChangeSceneType">
|
||||
<Tabs.TabPane tab="我负责的" key="1" />
|
||||
<Tabs.TabPane tab="我参与的" key="2" />
|
||||
|
@ -155,29 +133,60 @@ function onChangeSceneType(key: number | string) {
|
|||
</Tabs>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['crm:customer:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['客户']) }}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['crm:customer:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</Button>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['客户']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:customer:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['crm:customer:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<Button type="link" @click="onDetail(row)">
|
||||
<Button type="link" @click="handleDetail(row)">
|
||||
{{ row.name }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:customer:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['crm:customer:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { LimitConfType } from '#/api/crm/customer/limitConfig';
|
||||
import { getSimpleDeptList } from '#/api/system/dept';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DICT_TYPE } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(confType: LimitConfType): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'userIds',
|
||||
label: '规则适用人群',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
},
|
||||
multiple: true,
|
||||
allowClear: true,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'deptIds',
|
||||
label: '规则适用部门',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const data = await getSimpleDeptList();
|
||||
return handleTree(data);
|
||||
},
|
||||
multiple: true,
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
placeholder: '请选择规则适用部门',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'maxCount',
|
||||
label:
|
||||
confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT
|
||||
? '拥有客户数上限'
|
||||
: '锁定客户数上限',
|
||||
component: 'InputNumber',
|
||||
},
|
||||
{
|
||||
fieldName: 'dealCountEnabled',
|
||||
label: '成交客户是否占用拥有客户数',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
],
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(
|
||||
confType: LimitConfType,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'users',
|
||||
title: '规则适用人群',
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue
|
||||
.map((user: any) => {
|
||||
return user.nickname;
|
||||
})
|
||||
.join(',');
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'depts',
|
||||
title: '规则适用部门',
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue
|
||||
.map((dept: any) => {
|
||||
return dept.name;
|
||||
})
|
||||
.join(',');
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'maxCount',
|
||||
title:
|
||||
confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT
|
||||
? '拥有客户数上限'
|
||||
: '锁定客户数上限',
|
||||
},
|
||||
{
|
||||
field: 'dealCountEnabled',
|
||||
title: '成交客户是否占用拥有客户数',
|
||||
visible: confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
|
@ -1,38 +1,170 @@
|
|||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmCustomerLimitConfigApi } from '#/api/crm/customer/limitConfig';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteCustomerLimitConfig,
|
||||
getCustomerLimitConfigPage,
|
||||
LimitConfType,
|
||||
} from '#/api/crm/customer/limitConfig';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const configType = ref(LimitConfType.CUSTOMER_QUANTITY_LIMIT);
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建规则 */
|
||||
function handleCreate(type: LimitConfType) {
|
||||
formModalApi.setData({ type }).open();
|
||||
}
|
||||
|
||||
/** 编辑规则 */
|
||||
function handleEdit(
|
||||
row: CrmCustomerLimitConfigApi.CustomerLimitConfig,
|
||||
type: LimitConfType,
|
||||
) {
|
||||
formModalApi.setData({ id: row.id, type }).open();
|
||||
}
|
||||
|
||||
/** 删除规则 */
|
||||
async function handleDelete(
|
||||
row: CrmCustomerLimitConfigApi.CustomerLimitConfig,
|
||||
) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await deleteCustomerLimitConfig(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(configType.value),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCustomerLimitConfigPage({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
type: configType.value,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmCustomerLimitConfigApi.CustomerLimitConfig>,
|
||||
});
|
||||
|
||||
function onChangeConfigType(key: number | string) {
|
||||
configType.value = key as LimitConfType;
|
||||
gridApi.setGridOptions({
|
||||
columns: useGridColumns(configType.value),
|
||||
});
|
||||
onRefresh();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【客户】客户管理、公海客户"
|
||||
url="https://doc.iocoder.cn/crm/customer/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/customer/limitConfig/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/customer/limitConfig/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【客户】客户管理、公海客户"
|
||||
url="https://doc.iocoder.cn/crm/customer/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【通用】数据权限"
|
||||
url="https://doc.iocoder.cn/crm/permission/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal />
|
||||
<Grid>
|
||||
<template #top>
|
||||
<Tabs class="border-none" @change="onChangeConfigType">
|
||||
<Tabs.TabPane
|
||||
tab="拥有客户数限制"
|
||||
:key="LimitConfType.CUSTOMER_QUANTITY_LIMIT"
|
||||
/>
|
||||
<Tabs.TabPane
|
||||
tab="锁定客户数限制"
|
||||
:key="LimitConfType.CUSTOMER_LOCK_LIMIT"
|
||||
/>
|
||||
</Tabs>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['规则']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['crm:customer-limit-config:create'],
|
||||
onClick: handleCreate.bind(null, configType),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['crm:customer-limit-config:update'],
|
||||
onClick: handleEdit.bind(null, row, configType),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['crm:customer-limit-config:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
<script lang="ts" setup>
|
||||
import type { CrmCustomerLimitConfigApi } from '#/api/crm/customer/limitConfig';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createCustomerLimitConfig,
|
||||
getCustomerLimitConfig,
|
||||
LimitConfType,
|
||||
updateCustomerLimitConfig,
|
||||
} from '#/api/crm/customer/limitConfig';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<CrmCustomerLimitConfigApi.CustomerLimitConfig>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['规则'])
|
||||
: $t('ui.actionTitle.create', ['规则']);
|
||||
});
|
||||
|
||||
const confType = ref<LimitConfType>(LimitConfType.CUSTOMER_LOCK_LIMIT);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(confType.value),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as CrmCustomerLimitConfigApi.CustomerLimitConfig;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateCustomerLimitConfig(data)
|
||||
: createCustomerLimitConfig(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
let data =
|
||||
modalApi.getData<CrmCustomerLimitConfigApi.CustomerLimitConfig>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.type) {
|
||||
confType.value = data.type as LimitConfType;
|
||||
}
|
||||
formApi.setState({ schema: useFormSchema(confType.value) });
|
||||
modalApi.lock();
|
||||
try {
|
||||
if (data.id) {
|
||||
data = await getCustomerLimitConfig(data.id as number);
|
||||
}
|
||||
formData.value = data;
|
||||
// 设置到 values
|
||||
await formApi.setValues(data);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-[40%]">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
|
@ -1,7 +1,5 @@
|
|||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p>待完成</p>
|
||||
</div>
|
||||
<div>detail-info</div>
|
||||
</template>
|
|
@ -0,0 +1,4 @@
|
|||
<script lang="ts" setup></script>
|
||||
<template>
|
||||
<div>customerList</div>
|
||||
</template>
|
|
@ -1,7 +1,209 @@
|
|||
<script lang="ts" setup></script>
|
||||
<script setup lang="ts">
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { defineAsyncComponent, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, Modal, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { getCustomer, updateCustomerDealStatus } from '#/api/crm/customer';
|
||||
import { getOperateLogPage } from '#/api/crm/operateLog';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { OperateLog } from '#/components/operate-log';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
const CustomerDetailsInfo = defineAsyncComponent(
|
||||
() => import('./detail-info.vue'),
|
||||
);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const customerId = ref(0);
|
||||
|
||||
const customer = ref<CrmCustomerApi.Customer>({} as CrmCustomerApi.Customer);
|
||||
const permissionListRef = ref(); // 团队成员列表 Ref
|
||||
|
||||
const [Description] = useDescription({
|
||||
componentProps: {
|
||||
bordered: false,
|
||||
column: 4,
|
||||
class: 'mx-4',
|
||||
},
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
/** 加载详情 */
|
||||
async function loadCustomerDetail() {
|
||||
loading.value = true;
|
||||
customerId.value = Number(route.params.id);
|
||||
const data = await getCustomer(customerId.value);
|
||||
await getOperateLog();
|
||||
customer.value = data;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit() {
|
||||
// formModalApi.setData({ id: clueId }).open();
|
||||
}
|
||||
|
||||
/** 转移线索 */
|
||||
function handleTransfer() {
|
||||
// transferModalApi.setData({ id: clueId }).open();
|
||||
}
|
||||
|
||||
/** 锁定客户 */
|
||||
function handleLock() {
|
||||
// transferModalApi.setData({ id: clueId }).open();
|
||||
}
|
||||
|
||||
/** 解锁客户 */
|
||||
function handleUnlock() {
|
||||
// transferModalApi.setData({ id: clueId }).open();
|
||||
}
|
||||
|
||||
/** 领取客户 */
|
||||
function handleReceive() {
|
||||
// transferModalApi.setData({ id: clueId }).open();
|
||||
}
|
||||
|
||||
/** 分配客户 */
|
||||
function handleDistributeForm() {
|
||||
// transferModalApi.setData({ id: clueId }).open();
|
||||
}
|
||||
|
||||
/** 客户放入公海 */
|
||||
function handlePutPool() {
|
||||
// transferModalApi.setData({ id: clueId }).open();
|
||||
}
|
||||
|
||||
/** 更新成交状态操作 */
|
||||
async function handleUpdateDealStatus() {
|
||||
const dealStatus = !customer.value.dealStatus;
|
||||
try {
|
||||
await Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确定更新成交状态为【${dealStatus ? '已成交' : '未成交'}】吗?`,
|
||||
});
|
||||
await updateCustomerDealStatus(customerId.value, dealStatus);
|
||||
Modal.success({
|
||||
title: '成功',
|
||||
content: '更新成交状态成功',
|
||||
});
|
||||
await loadCustomerDetail();
|
||||
} catch {
|
||||
// 用户取消操作
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取操作日志 */
|
||||
const logList = ref<SystemOperateLogApi.OperateLog[]>([]); // 操作日志列表
|
||||
async function getOperateLog() {
|
||||
if (!customerId.value) {
|
||||
return;
|
||||
}
|
||||
const data = await getOperateLogPage({
|
||||
bizType: BizTypeEnum.CRM_CUSTOMER,
|
||||
bizId: customerId.value,
|
||||
});
|
||||
logList.value = data.list;
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(async () => {
|
||||
await loadCustomerDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p>待完成</p>
|
||||
</div>
|
||||
<Page auto-content-height :title="customer?.name" :loading="loading">
|
||||
<template #extra>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
v-if="permissionListRef?.validateWrite"
|
||||
type="primary"
|
||||
@click="handleEdit"
|
||||
v-access:code="['crm:customer:update']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="permissionListRef?.validateOwnerUser"
|
||||
type="primary"
|
||||
@click="handleTransfer"
|
||||
>
|
||||
转移
|
||||
</Button>
|
||||
<Button
|
||||
v-if="permissionListRef?.validateWrite"
|
||||
@click="handleUpdateDealStatus"
|
||||
>
|
||||
更改成交状态
|
||||
</Button>
|
||||
<Button
|
||||
v-if="customer.lockStatus && permissionListRef?.validateOwnerUser"
|
||||
@click="handleUnlock"
|
||||
>
|
||||
解锁
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!customer.lockStatus && permissionListRef?.validateOwnerUser"
|
||||
@click="handleLock"
|
||||
>
|
||||
锁定
|
||||
</Button>
|
||||
<Button v-if="!customer.ownerUserId" @click="handleReceive">
|
||||
领取
|
||||
</Button>
|
||||
<Button v-if="!customer.ownerUserId" @click="handleDistributeForm">
|
||||
分配
|
||||
</Button>
|
||||
<Button
|
||||
v-if="customer.ownerUserId && permissionListRef?.validateOwnerUser"
|
||||
@click="handlePutPool"
|
||||
>
|
||||
放入公海
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<Card>
|
||||
<Description :data="customer" />
|
||||
</Card>
|
||||
<Card class="mt-4">
|
||||
<Tabs>
|
||||
<Tabs.TabPane tab="跟进记录" key="1">
|
||||
<div>跟进记录</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="基本信息" key="2">
|
||||
<CustomerDetailsInfo />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="联系人" key="3">
|
||||
<div>联系人</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="团队成员" key="4">
|
||||
<div>团队成员</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="商机" key="5">
|
||||
<div>商机</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="合同" key="6">
|
||||
<div>合同</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="回款" key="7">
|
||||
<div>回款</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="操作日志" key="8">
|
||||
<OperateLog :log-list="logList" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue