!219 【antd】CRM 迁移彻底完成

Merge pull request !219 from 芋道源码/dev
This commit is contained in:
芋道源码
2025-10-02 08:10:55 +00:00
committed by Gitee
401 changed files with 5489 additions and 3991 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 495 KiB

View File

@@ -50,6 +50,7 @@ export namespace CrmContractApi {
creatorName: string;
updateTime?: Date;
products?: ContractProduct[];
contactName?: string;
}
}

View File

@@ -16,6 +16,7 @@ export namespace CrmCustomerApi {
ownerUserId: number; // 负责人的用户编号
ownerUserName?: string; // 负责人的用户名称
ownerUserDept?: string; // 负责人的部门名称
ownerUserDeptName?: string; // 负责人的部门名称
lockStatus?: boolean;
dealStatus?: boolean;
mobile: string; // 手机号
@@ -34,7 +35,9 @@ export namespace CrmCustomerApi {
creatorName?: string; // 创建人名称
createTime: Date; // 创建时间
updateTime: Date; // 更新时间
poolDay?: number; // 距离进入公海天数
}
export interface CustomerImport {
ownerUserId: number;
file: File;

View File

@@ -98,7 +98,7 @@ export function deleteReceivablePlan(id: number) {
}
/** 导出回款计划 Excel */
export function exportReceivablePlan(params: PageParam) {
export function exportReceivablePlan(params: any) {
return requestClient.download('/crm/receivable-plan/export-excel', {
params,
});

View File

@@ -65,7 +65,7 @@ export namespace InfraCodegenApi {
}
/** 更新代码生成请求 */
export interface CodegenUpdateReq {
export interface CodegenUpdateReqVO {
table: any | CodegenTable;
columns: CodegenColumn[];
}
@@ -106,25 +106,36 @@ export function getCodegenTable(tableId: number) {
}
/** 修改代码生成表定义 */
export function updateCodegenTable(data: InfraCodegenApi.CodegenUpdateReq) {
export function updateCodegenTable(data: InfraCodegenApi.CodegenUpdateReqVO) {
return requestClient.put('/infra/codegen/update', data);
}
/** 基于数据库的表结构,同步数据库的表和字段定义 */
export function syncCodegenFromDB(tableId: number) {
return requestClient.put(`/infra/codegen/sync-from-db?tableId=${tableId}`);
return requestClient.put(
'/infra/codegen/sync-from-db',
{},
{
params: { tableId },
},
);
}
/** 预览生成代码 */
export function previewCodegen(tableId: number) {
return requestClient.get<InfraCodegenApi.CodegenPreview[]>(
`/infra/codegen/preview?tableId=${tableId}`,
'/infra/codegen/preview',
{
params: { tableId },
},
);
}
/** 下载生成代码 */
export function downloadCodegen(tableId: number) {
return requestClient.download(`/infra/codegen/download?tableId=${tableId}`);
return requestClient.download('/infra/codegen/download', {
params: { tableId },
});
}
/** 获得表定义 */

View File

@@ -44,3 +44,10 @@ export function updateDataSourceConfig(
export function deleteDataSourceConfig(id: number) {
return requestClient.delete(`/infra/data-source-config/delete?id=${id}`);
}
/** 批量删除数据源配置 */
export function deleteDataSourceConfigList(ids: number[]) {
return requestClient.delete(
`/infra/data-source-config/delete-list?ids=${ids.join(',')}`,
);
}

View File

@@ -19,7 +19,7 @@ export namespace InfraFileApi {
}
/** 文件预签名地址 */
export interface FilePresignedUrlResp {
export interface FilePresignedUrlRespVO {
configId: number; // 文件配置编号
uploadUrl: string; // 文件上传 URL
url: string; // 文件 URL
@@ -27,7 +27,7 @@ export namespace InfraFileApi {
}
/** 上传文件 */
export interface FileUploadReq {
export interface FileUploadReqVO {
file: globalThis.File;
directory?: string;
}
@@ -52,7 +52,7 @@ export function deleteFileList(ids: number[]) {
/** 获取文件预签名地址 */
export function getFilePresignedUrl(name: string, directory?: string) {
return requestClient.get<InfraFileApi.FilePresignedUrlResp>(
return requestClient.get<InfraFileApi.FilePresignedUrlRespVO>(
'/infra/file/presigned-url',
{
params: { name, directory },
@@ -67,7 +67,7 @@ export function createFile(data: InfraFileApi.File) {
/** 上传文件 */
export function uploadFile(
data: InfraFileApi.FileUploadReq,
data: InfraFileApi.FileUploadReqVO,
onUploadProgress?: AxiosProgressEvent,
) {
// 特殊:由于 upload 内部封装,即使 directory 为 undefined也会传递给后端

View File

@@ -58,11 +58,12 @@ export function exportJob(params: any) {
/** 任务状态修改 */
export function updateJobStatus(id: number, status: number) {
const params = {
id,
status,
};
return requestClient.put('/infra/job/update-status', {}, { params });
return requestClient.put('/infra/job/update-status', undefined, {
params: {
id,
status,
},
});
}
/** 定时任务立即执行一次 */

View File

@@ -62,7 +62,7 @@ export function deleteAccount(id: number) {
/** 生成公众号账号二维码 */
export function generateAccountQrCode(id: number) {
return requestClient.post(`/mp/account/generate-qr-code?id=${id}`);
return requestClient.put(`/mp/account/generate-qr-code?id=${id}`);
}
/** 清空公众号账号 API 配额 */

View File

@@ -58,7 +58,7 @@ export function deleteMailTemplate(id: number) {
return requestClient.delete(`/system/mail-template/delete?id=${id}`);
}
/** 批量删除邮件模 */
/** 批量删除邮件模 */
export function deleteMailTemplateList(ids: number[]) {
return requestClient.delete(
`/system/mail-template/delete-list?ids=${ids.join(',')}`,

View File

@@ -3,7 +3,7 @@ import { requestClient } from '#/api/request';
/** OAuth2.0 授权信息响应 */
export namespace SystemOAuth2ClientApi {
/** 授权信息 */
export interface AuthorizeInfoResp {
export interface AuthorizeInfoRespVO {
client: {
logo: string;
name: string;
@@ -17,7 +17,7 @@ export namespace SystemOAuth2ClientApi {
/** 获得授权信息 */
export function getAuthorize(clientId: string) {
return requestClient.get<SystemOAuth2ClientApi.AuthorizeInfoResp>(
return requestClient.get<SystemOAuth2ClientApi.AuthorizeInfoRespVO>(
`/system/oauth2/authorize?clientId=${clientId}`,
);
}

View File

@@ -32,10 +32,3 @@ export function deleteOAuth2Token(accessToken: string) {
`/system/oauth2-token/delete?accessToken=${accessToken}`,
);
}
/** 批量删除 OAuth2.0 令牌 */
export function deleteOAuth2TokenList(accessTokens: string[]) {
return requestClient.delete(
`/system/oauth2-token/delete-list?accessTokens=${accessTokens.join(',')}`,
);
}

View File

@@ -2,19 +2,19 @@ import { requestClient } from '#/api/request';
export namespace SystemPermissionApi {
/** 分配用户角色请求 */
export interface AssignUserRoleReq {
export interface AssignUserRoleReqVO {
userId: number;
roleIds: number[];
}
/** 分配角色菜单请求 */
export interface AssignRoleMenuReq {
export interface AssignRoleMenuReqVO {
roleId: number;
menuIds: number[];
}
/** 分配角色数据权限请求 */
export interface AssignRoleDataScopeReq {
export interface AssignRoleDataScopeReqVO {
roleId: number;
dataScope: number;
dataScopeDeptIds: number[];
@@ -30,14 +30,14 @@ export async function getRoleMenuList(roleId: number) {
/** 赋予角色菜单权限 */
export async function assignRoleMenu(
data: SystemPermissionApi.AssignRoleMenuReq,
data: SystemPermissionApi.AssignRoleMenuReqVO,
) {
return requestClient.post('/system/permission/assign-role-menu', data);
}
/** 赋予角色数据权限 */
export async function assignRoleDataScope(
data: SystemPermissionApi.AssignRoleDataScopeReq,
data: SystemPermissionApi.AssignRoleDataScopeReqVO,
) {
return requestClient.post('/system/permission/assign-role-data-scope', data);
}
@@ -51,7 +51,7 @@ export async function getUserRoleList(userId: number) {
/** 赋予用户角色 */
export async function assignUserRole(
data: SystemPermissionApi.AssignUserRoleReq,
data: SystemPermissionApi.AssignUserRoleReqVO,
) {
return requestClient.post('/system/permission/assign-user-role', data);
}

View File

@@ -20,14 +20,14 @@ export namespace SystemSocialUserApi {
}
/** 社交绑定请求 */
export interface SocialUserBindReq {
export interface SocialUserBindReqVO {
type: number;
code: string;
state: string;
}
/** 取消社交绑定请求 */
export interface SocialUserUnbindReq {
export interface SocialUserUnbindReqVO {
type: number;
openid: string;
}
@@ -49,12 +49,12 @@ export function getSocialUser(id: number) {
}
/** 社交绑定,使用 code 授权码 */
export function socialBind(data: SystemSocialUserApi.SocialUserBindReq) {
export function socialBind(data: SystemSocialUserApi.SocialUserBindReqVO) {
return requestClient.post<boolean>('/system/social-user/bind', data);
}
/** 取消社交绑定 */
export function socialUnbind(data: SystemSocialUserApi.SocialUserUnbindReq) {
export function socialUnbind(data: SystemSocialUserApi.SocialUserUnbindReqVO) {
return requestClient.delete<boolean>('/system/social-user/unbind', { data });
}

View File

@@ -2,7 +2,7 @@ import { requestClient } from '#/api/request';
export namespace SystemUserProfileApi {
/** 用户个人中心信息 */
export interface UserProfileResp {
export interface UserProfileRespVO {
id: number;
username: string;
nickname: string;
@@ -19,13 +19,13 @@ export namespace SystemUserProfileApi {
}
/** 更新密码请求 */
export interface UpdatePasswordReq {
export interface UpdatePasswordReqVO {
oldPassword: string;
newPassword: string;
}
/** 更新个人信息请求 */
export interface UpdateProfileReq {
export interface UpdateProfileReqVO {
nickname?: string;
email?: string;
mobile?: string;
@@ -36,19 +36,21 @@ export namespace SystemUserProfileApi {
/** 获取登录用户信息 */
export function getUserProfile() {
return requestClient.get<SystemUserProfileApi.UserProfileResp>(
return requestClient.get<SystemUserProfileApi.UserProfileRespVO>(
'/system/user/profile/get',
);
}
/** 修改用户个人信息 */
export function updateUserProfile(data: SystemUserProfileApi.UpdateProfileReq) {
export function updateUserProfile(
data: SystemUserProfileApi.UpdateProfileReqVO,
) {
return requestClient.put('/system/user/profile/update', data);
}
/** 修改用户个人密码 */
export function updateUserPassword(
data: SystemUserProfileApi.UpdatePasswordReq,
data: SystemUserProfileApi.UpdatePasswordReqVO,
) {
return requestClient.put('/system/user/profile/update-password', data);
}

View File

@@ -4,17 +4,7 @@
// import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
// import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'
// import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css' // 右侧框样式
import {
computed,
defineEmits,
defineOptions,
defineProps,
h,
onBeforeUnmount,
onMounted,
provide,
ref,
} from 'vue';
import { computed, h, onBeforeUnmount, onMounted, provide, ref } from 'vue';
import {
AlignLeftOutlined,
@@ -655,7 +645,7 @@ onBeforeUnmount(() => {
type="file"
id="files"
ref="refFile"
style="display: none"
class="hidden"
accept=".xml, .bpmn"
@change="importLocalFile"
/>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { defineProps, h, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { h, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { BpmProcessInstanceStatus, DICT_TYPE } from '@vben/constants';
import { UndoOutlined, ZoomInOutlined, ZoomOutOutlined } from '@vben/icons';

View File

@@ -3,7 +3,11 @@ import 'bpmn-js/dist/assets/diagram-js.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';
// TODO @puhui999样式问题设计器那位置不太对
export { default as MyProcessDesigner } from './designer';
// TODO @puhui999流程发起时预览相关的需要使用
export { default as MyProcessViewer } from './designer/index2';
export { default as MyProcessPenal } from './penal';
// TODO @puhui999【有个迁移的打印】【新增】流程打印由 [@Lesan](https://gitee.com/LesanOuO) 贡献 [#816](https://gitee.com/yudaocode/yudao-ui-admin-vue3/pulls/816/)、[#1418](https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1418/)、[#817](https://gitee.com/yudaocode/yudao-ui-admin-vue3/pulls/817/)、[#1419](https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1419/)、[#1424](https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1424)、[#819](https://gitee.com/yudaocode/yudao-ui-admin-vue3/pulls/819)、[#821](https://gitee.com/yudaocode/yudao-ui-admin-vue3/pulls/821/)

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { Component } from 'vue';
import { defineOptions, defineProps, ref, watch } from 'vue';
import { ref, watch } from 'vue';
import { CustomConfigMap } from './data';

View File

@@ -1,13 +1,5 @@
<script lang="ts" setup>
import {
defineOptions,
defineProps,
inject,
nextTick,
ref,
toRaw,
watch,
} from 'vue';
import { inject, nextTick, ref, toRaw, watch } from 'vue';
import {
Divider,

View File

@@ -153,11 +153,7 @@ watch(
<template>
<div class="panel-tab__content">
<Form
:model="flowConditionForm"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Form :model="flowConditionForm">
<Form.Item label="流转类型">
<Select v-model:value="flowConditionForm.type" @change="updateFlowType">
<Select.Option value="normal">普通流转路径</Select.Option>

View File

@@ -305,7 +305,7 @@ watch(
<template>
<div class="panel-tab__content">
<Form :label-col="{ style: { width: '80px' } }">
<Form>
<FormItem label="流程表单">
<!-- <Input v-model:value="formKey" @change="updateElementFormKey" />-->
<Select

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { inject, nextTick, ref, watch } from 'vue';
import { IconifyIcon, PlusOutlined } from '@vben/icons';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import {
@@ -290,7 +290,7 @@ watch(
<div class="element-drawer__button">
<Button type="primary" size="small" @click="openListenerForm(null, -1)">
<template #icon>
<PlusOutlined />
<IconifyIcon icon="ep:plus" />
</template>
添加监听器
</Button>
@@ -309,12 +309,7 @@ watch(
:width="width as any"
:destroy-on-close="true"
>
<Form
:model="listenerForm"
ref="listenerFormRef"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Form :model="listenerForm" ref="listenerFormRef">
<FormItem
label="事件类型"
name="event"
@@ -462,20 +457,23 @@ watch(
</template>
</Form>
<Divider />
<p class="listener-filed__title">
<span><IconifyIcon icon="ep:menu" />注入字段:</span>
<Button type="primary" @click="openListenerFieldForm(null, -1)">
<div class="mb-2 flex justify-between">
<span class="flex items-center">
<IconifyIcon icon="ep:menu" class="mr-2 text-gray-600" />
注入字段
</span>
<Button
type="primary"
title="添加字段"
@click="openListenerFieldForm(null, -1)"
>
<template #icon>
<IconifyIcon icon="ep:plus" />
</template>
添加字段
</Button>
</p>
<Table
:data-source="fieldsListOfListener"
size="small"
:scroll="{ y: 240 }"
:pagination="false"
bordered
style="flex: none"
>
</div>
<Table :data-source="fieldsListOfListener" size="small" bordered>
<TableColumn title="序号" width="50px">
<template #default="{ index }">
{{ index + 1 }}
@@ -492,12 +490,12 @@ watch(
/>
<TableColumn
title="字段值/表达式"
width="100px"
width="120px"
:custom-render="
({ record }: any) => record.string || record.expression
"
/>
<TableColumn title="操作" width="130px">
<TableColumn title="操作" width="80px" fixed="right">
<template #default="{ record, index }">
<Button
size="small"
@@ -532,13 +530,7 @@ watch(
width="600px"
:destroy-on-close="true"
>
<Form
:model="listenerFieldForm"
ref="listenerFieldFormRef"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
style="height: 136px"
>
<Form :model="listenerFieldForm" ref="listenerFieldFormRef">
<FormItem
label="字段名称"
name="name"

View File

@@ -4,12 +4,12 @@ import type { BpmProcessListenerApi } from '#/api/bpm/processListener';
import { reactive, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { Button, Modal, Pagination, Table } from 'ant-design-vue';
import { getProcessListenerPage } from '#/api/bpm/processListener';
import { ContentWrap } from '#/components/content-wrap';
import { DictTag } from '#/components/dict-tag';
/** BPM 流程 表单 */
@@ -89,7 +89,7 @@ const select = async (row: BpmProcessListenerApi.ProcessListener) => {
</template>
</Table.Column>
<Table.Column title="值" align="center" data-index="value" />
<Table.Column title="操作" align="center">
<Table.Column title="操作" align="center" fixed="right">
<template #default="{ record }">
<Button type="primary" @click="select(record)"> 选择 </Button>
</template>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { inject, nextTick, ref, watch } from 'vue';
import { MenuOutlined, PlusOutlined, SelectOutlined } from '@vben/icons';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import {
@@ -300,11 +300,11 @@ watch(
</Table>
<div class="element-drawer__button">
<Button size="small" type="primary" @click="openListenerForm(null)">
<template #icon><PlusOutlined /></template>
<template #icon> <IconifyIcon icon="ep:plus" /></template>
添加监听器
</Button>
<Button size="small" @click="openProcessListenerDialog">
<template #icon><SelectOutlined /></template>
<template #icon> <IconifyIcon icon="ep:select" /></template>
选择监听器
</Button>
</div>
@@ -316,12 +316,7 @@ watch(
:width="width"
:destroy-on-close="true"
>
<Form
:model="listenerForm"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
ref="listenerFormRef"
>
<Form :model="listenerForm" ref="listenerFormRef">
<FormItem
label="事件类型"
name="event"
@@ -458,16 +453,22 @@ watch(
</Form>
<Divider />
<p class="listener-filed__title">
<span><MenuOutlined />注入字段:</span>
<div class="mb-2 flex justify-between">
<span class="flex items-center">
<IconifyIcon icon="ep:menu" class="mr-2 text-gray-600" />
注入字段
</span>
<Button
size="small"
type="primary"
title="添加字段"
@click="openListenerFieldForm(null)"
>
<template #icon>
<IconifyIcon icon="ep:plus" />
</template>
添加字段
</Button>
</p>
</div>
<Table
:data="fieldsListOfListener"
size="small"
@@ -533,13 +534,7 @@ watch(
:width="600"
:destroy-on-close="true"
>
<Form
:model="listenerFieldForm"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
ref="listenerFieldFormRef"
style="height: 136px"
>
<Form :model="listenerFieldForm" ref="listenerFieldFormRef">
<FormItem
label="字段名称"
name="name"

View File

@@ -421,7 +421,7 @@ watch(
</RadioGroup>
<div v-else>除了UserTask以外节点的多实例待实现</div>
<!-- 与Simple设计器配置合并保留以前的代码 -->
<Form :label-col="{ span: 6 }" style="display: none">
<Form class="hidden">
<FormItem label="快捷配置">
<Button size="small" @click="() => changeConfig('依次审批')">
依次审批
@@ -467,7 +467,7 @@ watch(
/>
</FormItem>
<!-- add by 芋艿:由于「元素变量」暂时用不到,所以这里 display 为 none -->
<FormItem label="元素变量" key="elementVariable" style="display: none">
<FormItem label="元素变量" key="elementVariable" class="hidden">
<Input
v-model:value="loopInstanceForm.elementVariable"
allow-clear
@@ -485,7 +485,7 @@ watch(
/>
</FormItem>
<!-- add by 芋艿:由于「异步状态」暂时用不到,所以这里 display 为 none -->
<FormItem label="异步状态" key="async" style="display: none">
<FormItem label="异步状态" key="async" class="hidden">
<Checkbox
v-model:checked="loopInstanceForm.asyncBefore"
@change="() => updateLoopAsync('asyncBefore')"

View File

@@ -161,25 +161,15 @@ watch(
<template>
<div class="panel-tab__content">
<Table :data="elementPropertyList" :scroll="{ y: 240 }" bordered>
<Table :data="elementPropertyList" size="small" bordered>
<TableColumn title="序号" width="50">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn
title="属性名"
data-index="name"
:min-width="100"
:ellipsis="{ showTitle: true }"
/>
<TableColumn
title="属性值"
data-index="value"
:min-width="100"
:ellipsis="{ showTitle: true }"
/>
<TableColumn title="操作" width="110">
<TableColumn title="属性名" data-index="name" />
<TableColumn title="属性值" data-index="value" />
<TableColumn title="操作">
<template #default="{ record, index }">
<Button
type="link"
@@ -215,11 +205,7 @@ watch(
:width="600"
:destroy-on-close="true"
>
<Form
:model="propertyForm"
ref="attributeFormRef"
:label-col="{ span: 6 }"
>
<Form :model="propertyForm" ref="attributeFormRef">
<FormItem label="属性名:" name="name">
<Input v-model:value="propertyForm.name" allow-clear />
</FormItem>

View File

@@ -84,8 +84,8 @@ onMounted(() => {
<template>
<div class="panel-tab__content">
<div class="panel-tab__content--title">
<span>
<IconifyIcon icon="ep:menu" style="margin-right: 8px; color: #555" />
<span class="flex items-center">
<IconifyIcon icon="ep:menu" class="mr-2 text-gray-600" />
消息列表
</span>
<Button type="primary" title="创建新消息" @click="openModel('message')">
@@ -95,33 +95,19 @@ onMounted(() => {
创建新消息
</Button>
</div>
<Table :data-source="messageList" :bordered="true" :pagination="false">
<Table :data-source="messageList" size="small" bordered>
<TableColumn title="序号" width="60px">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn
title="消息ID"
data-index="id"
:width="300"
:ellipsis="{ showTitle: true }"
/>
<TableColumn
title="消息名称"
data-index="name"
:width="300"
:ellipsis="{ showTitle: true }"
/>
<TableColumn title="消息ID" data-index="id" />
<TableColumn title="消息名称" data-index="name" />
</Table>
<div
class="panel-tab__content--title"
style="padding-top: 8px; margin-top: 8px; border-top: 1px solid #eee"
>
<span>
<IconifyIcon icon="ep:menu" style="margin-right: 8px; color: #555">
信号列表
</IconifyIcon>
<div class="panel-tab__content--title mt-2 border-t border-gray-200 pt-2">
<span class="flex items-center">
<IconifyIcon icon="ep:menu" class="mr-2 text-gray-600" />
信号列表
</span>
<Button type="primary" title="创建新信号" @click="openModel('signal')">
<template #icon>
@@ -130,24 +116,14 @@ onMounted(() => {
创建新信号
</Button>
</div>
<Table :data-source="signalList" :bordered="true" :pagination="false">
<Table :data-source="signalList" size="small" bordered>
<TableColumn title="序号" width="60px">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn
title="信号ID"
data-index="id"
:width="300"
:ellipsis="{ showTitle: true }"
/>
<TableColumn
title="信号名称"
data-index="name"
:width="300"
:ellipsis="{ showTitle: true }"
/>
<TableColumn title="信号ID" data-index="id" />
<TableColumn title="信号名称" data-index="name" />
</Table>
<Modal
@@ -157,11 +133,7 @@ onMounted(() => {
width="400px"
:destroy-on-close="true"
>
<Form
:model="modelObjectForm"
:label-col="{ span: 9 }"
:wrapper-col="{ span: 15 }"
>
<Form :model="modelObjectForm">
<FormItem :label="modelConfig.idLabel">
<Input v-model:value="modelObjectForm.id" allow-clear />
</FormItem>

View File

@@ -63,9 +63,9 @@ watch(
<template>
<div class="panel-tab__content">
<Form :label-col="{ span: 9 }" :wrapper-col="{ span: 15 }">
<Form>
<!-- add by 芋艿由于异步延续暂时用不到所以这里 display none -->
<FormItem label="异步延续" style="display: none">
<FormItem label="异步延续" class="hidden">
<Checkbox
v-model:checked="taskConfigForm.asyncBefore"
@change="changeTaskAsync"

View File

@@ -180,7 +180,7 @@ watch(
<template>
<div>
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<Form>
<FormItem label="实例名称">
<Input
v-model:value="formData.processInstanceName"
@@ -341,12 +341,7 @@ watch(
@ok="saveVariable"
@cancel="variableDialogVisible = false"
>
<Form
:model="varialbeFormData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
ref="varialbeFormRef"
>
<Form :model="varialbeFormData" ref="varialbeFormRef">
<FormItem label="源:" name="source">
<Input v-model:value="varialbeFormData.source" allow-clear />
</FormItem>

View File

@@ -4,12 +4,12 @@ import type { BpmProcessExpressionApi } from '#/api/bpm/processExpression';
import { reactive, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { CommonStatusEnum } from '@vben/constants';
import { Button, Modal, Pagination, Table, TableColumn } from 'ant-design-vue';
import { getProcessExpressionPage } from '#/api/bpm/processExpression';
import { ContentWrap } from '#/components/content-wrap';
/** BPM 流程 表单 */
defineOptions({ name: 'ProcessExpressionDialog' });

View File

@@ -143,7 +143,7 @@ watch(
width="400px"
:destroy-on-close="true"
>
<Form :model="newMessageForm" size="small" :label-col="{ span: 6 }">
<Form :model="newMessageForm" size="small">
<Form.Item label="消息ID">
<Input v-model:value="newMessageForm.id" allow-clear />
</Form.Item>

View File

@@ -1,13 +1,5 @@
<script lang="ts" setup>
import {
defineOptions,
defineProps,
nextTick,
onBeforeUnmount,
ref,
toRaw,
watch,
} from 'vue';
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
import {
FormItem,

View File

@@ -344,7 +344,7 @@ onBeforeUnmount(() => {
</script>
<template>
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<Form>
<FormItem label="规则类型" name="candidateStrategy">
<Select
v-model:value="userTaskForm.candidateStrategy"

View File

@@ -3,12 +3,7 @@ import type { Ref } from 'vue';
import { computed, nextTick, onMounted, ref, toRaw, watch } from 'vue';
import {
CheckCircleFilled,
ExclamationCircleFilled,
IconifyIcon,
QuestionCircleFilled,
} from '@vben/icons';
import { IconifyIcon } from '@vben/icons';
import { Button, DatePicker, Input, Modal, Tooltip } from 'ant-design-vue';
@@ -240,7 +235,11 @@ watch(
循环
</Button>
</Button.Group>
<CheckCircleFilled v-if="valid" style="color: green; margin-left: 8px" />
<IconifyIcon
icon="ant-design:check-circle-filled"
v-if="valid"
style="color: green; margin-left: 8px"
/>
</div>
<div style="display: flex; align-items: center; margin-top: 10px">
<span>条件</span>
@@ -254,11 +253,15 @@ watch(
>
<template #suffix>
<Tooltip v-if="!valid" title="格式错误" placement="top">
<ExclamationCircleFilled style="color: orange" />
<IconifyIcon
icon="ant-design:exclamation-circle-filled"
class="text-orange-400"
/>
</Tooltip>
<Tooltip :title="helpText" placement="top">
<QuestionCircleFilled
style="color: #409eff; cursor: pointer"
<IconifyIcon
icon="ant-design:question-circle-filled"
class="cursor-pointer text-[#409eff]"
@click="showHelp = true"
/>
</Tooltip>
@@ -351,7 +354,3 @@ watch(
</Modal>
</div>
</template>
<style scoped>
/* 相关样式 */
</style>

View File

@@ -1,49 +0,0 @@
<!--
参考自 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/components/ContentWrap/src/ContentWrap.vue
保证和 yudao-ui-admin-vue3 功能的一致性
-->
<script lang="ts" setup>
import type { CSSProperties } from 'vue';
import { ShieldQuestion } from '@vben/icons';
import { Card, Tooltip } from 'ant-design-vue';
defineOptions({ name: 'ContentWrap' });
withDefaults(
defineProps<{
bodyStyle?: CSSProperties;
message?: string;
title?: string;
}>(),
{
bodyStyle: () => ({ padding: '10px' }),
title: '',
message: '',
},
);
</script>
<template>
<Card :body-style="bodyStyle" :title="title" class="mb-4">
<template v-if="title" #title>
<div class="flex items-center">
<span class="text-base font-bold">{{ title }}</span>
<Tooltip placement="right">
<template #title>
<div class="max-w-[200px]">{{ message }}</div>
</template>
<ShieldQuestion :size="14" class="ml-1" />
</Tooltip>
<div class="flex flex-grow pl-5">
<slot name="header"></slot>
</div>
</div>
</template>
<template #extra>
<slot name="extra"></slot>
</template>
<slot></slot>
</Card>
</template>

View File

@@ -1 +0,0 @@
export { default as ContentWrap } from './content-wrap.vue';

View File

@@ -1,9 +1,3 @@
import { defineAsyncComponent } from 'vue';
export const AsyncOperateLog = defineAsyncComponent(
() => import('./operate-log.vue'),
);
export { default as OperateLog } from './operate-log.vue';
export type { OperateLogProps } from './typing';

View File

@@ -34,6 +34,7 @@ function getUserTypeColor(userType: number) {
</script>
<template>
<div>
<!-- TODO @xingyu有没可能美化下 -->
<Timeline>
<Timeline.Item
v-for="log in logList"

View File

@@ -6,7 +6,7 @@ import type { FileUploadProps } from './typing';
import type { AxiosProgressEvent } from '#/api/infra/file';
import { ref, toRefs, watch } from 'vue';
import { computed, ref, toRefs, watch } from 'vue';
import { CloudUpload } from '@vben/icons';
import { $t } from '@vben/locales';
@@ -22,8 +22,10 @@ defineOptions({ name: 'FileUpload', inheritAttrs: false });
const props = withDefaults(defineProps<FileUploadProps>(), {
value: () => [],
modelValue: undefined,
directory: undefined,
disabled: false,
drag: false,
helpText: '',
maxSize: 2,
maxNumber: 1,
@@ -33,7 +35,14 @@ const props = withDefaults(defineProps<FileUploadProps>(), {
resultField: '',
showDescription: false,
});
const emit = defineEmits(['change', 'update:value', 'delete', 'returnText']);
const emit = defineEmits([
'change',
'update:value',
'update:modelValue',
'delete',
'returnText',
'preview',
]);
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
@@ -43,13 +52,25 @@ const { getStringAccept } = useUploadType({
maxSizeRef: maxSize,
});
// 计算当前绑定的值,优先使用 modelValue
const currentValue = computed(() => {
return props.modelValue === undefined ? props.value : props.modelValue;
});
// 判断是否使用 modelValue
const isUsingModelValue = computed(() => {
return props.modelValue !== undefined;
});
const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true); // 文件大小错误提示
const isActMsg = ref<boolean>(true); // 文件类型错误提示
const isFirstRender = ref<boolean>(true); // 是否第一次渲染
const uploadNumber = ref<number>(0); // 上传文件计数器
const uploadList = ref<any[]>([]); // 临时上传列表
watch(
() => props.value,
currentValue,
(v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
@@ -94,15 +115,40 @@ async function handleRemove(file: UploadFile) {
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('update:modelValue', value);
emit('change', value);
emit('delete', file);
}
}
// 处理文件预览
function handlePreview(file: UploadFile) {
emit('preview', file);
}
// 处理文件数量超限
function handleExceed() {
message.error($t('ui.upload.maxNumber', [maxNumber.value]));
}
// 处理上传错误
function handleUploadError(error: any) {
console.error('上传错误:', error);
message.error($t('ui.upload.uploadError'));
// 上传失败时减少计数器
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
}
async function beforeUpload(file: File) {
const fileContent = await file.text();
emit('returnText', fileContent);
// 检查文件数量限制
if (fileList.value!.length >= props.maxNumber) {
message.error($t('ui.upload.maxNumber', [props.maxNumber]));
return Upload.LIST_IGNORE;
}
const { maxSize, accept } = props;
const isAct = checkFileType(file, accept);
if (!isAct) {
@@ -110,6 +156,7 @@ async function beforeUpload(file: File) {
isActMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isActMsg.value = true), 1000);
return Upload.LIST_IGNORE;
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
@@ -117,8 +164,12 @@ async function beforeUpload(file: File) {
isLtMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isLtMsg.value = true), 1000);
return Upload.LIST_IGNORE;
}
return (isAct && !isLt) || Upload.LIST_IGNORE;
// 只有在验证通过后才增加计数器
uploadNumber.value++;
return true;
}
async function customRequest(info: UploadRequestOption<any>) {
@@ -133,17 +184,48 @@ async function customRequest(info: UploadRequestOption<any>) {
info.onProgress!({ percent });
};
const res = await api?.(info.file as File, progressEvent);
// 处理上传成功后的逻辑
handleUploadSuccess(res, info.file as File);
info.onSuccess!(res);
message.success($t('ui.upload.uploadSuccess'));
// 更新文件
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) {
console.error(error);
info.onError!(error);
handleUploadError(error);
}
}
// 处理上传成功
function handleUploadSuccess(res: any, file: File) {
// 删除临时文件
const index = fileList.value?.findIndex((item) => item.name === file.name);
if (index !== -1) {
fileList.value?.splice(index!, 1);
}
// 添加到临时上传列表
const fileUrl = res?.url || res?.data || res;
uploadList.value.push({
name: file.name,
url: fileUrl,
status: UploadResultStatus.DONE,
uid: file.name + Date.now(),
});
// 检查是否所有文件都上传完成
if (uploadList.value.length >= uploadNumber.value) {
fileList.value?.push(...uploadList.value);
uploadList.value = [];
uploadNumber.value = 0;
// 更新值
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('update:modelValue', value);
emit('change', value);
}
}
@@ -156,11 +238,26 @@ function getValue() {
}
return item?.url || item?.response?.url || item?.response;
});
// add by 芋艿:【特殊】单个文件的情况,获取首个元素,保证返回的是 String 类型
// 单个文件的情况,根据输入参数类型决定返回格式
if (props.maxNumber === 1) {
return list.length > 0 ? list[0] : '';
const singleValue = list.length > 0 ? list[0] : '';
// 如果原始值是字符串或 modelValue 是字符串,返回字符串
if (
isString(props.value) ||
(isUsingModelValue.value && isString(props.modelValue))
) {
return singleValue;
}
return singleValue;
}
return list;
// 多文件情况,根据输入参数类型决定返回格式
if (isUsingModelValue.value) {
return Array.isArray(props.modelValue) ? list : list.join(',');
}
return Array.isArray(props.value) ? list : list.join(',');
}
</script>
@@ -177,15 +274,34 @@ function getValue() {
:multiple="multiple"
list-type="text"
:progress="{ showInfo: true }"
:show-upload-list="{
showPreviewIcon: true,
showRemoveIcon: true,
showDownloadIcon: true,
}"
@remove="handleRemove"
@preview="handlePreview"
@reject="handleExceed"
>
<div v-if="fileList && fileList.length < maxNumber">
<div v-if="drag" class="upload-drag-area">
<p class="ant-upload-drag-icon">
<CloudUpload />
</p>
<p class="ant-upload-text">点击或拖拽文件到此区域上传</p>
<p class="ant-upload-hint">
支持{{ accept.join('/') }}格式文件不超过{{ maxSize }}MB
</p>
</div>
<div v-else-if="fileList && fileList.length < maxNumber">
<Button>
<CloudUpload />
{{ $t('ui.upload.upload') }}
</Button>
</div>
<div v-if="showDescription" class="mt-2 flex flex-wrap items-center">
<div
v-if="showDescription && !drag"
class="mt-2 flex flex-wrap items-center"
>
请上传不超过
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
@@ -195,3 +311,35 @@ function getValue() {
</Upload>
</div>
</template>
<style scoped>
.upload-drag-area {
border: 2px dashed #d9d9d9;
border-radius: 8px;
padding: 20px;
text-align: center;
background-color: #fafafa;
transition: border-color 0.3s;
}
.upload-drag-area:hover {
border-color: #1890ff;
}
.ant-upload-drag-icon {
font-size: 48px;
color: #d9d9d9;
margin-bottom: 16px;
}
.ant-upload-text {
font-size: 16px;
color: #666;
margin-bottom: 8px;
}
.ant-upload-hint {
font-size: 14px;
color: #999;
}
</style>

View File

@@ -6,7 +6,7 @@ import type { FileUploadProps } from './typing';
import type { AxiosProgressEvent } from '#/api/infra/file';
import { ref, toRefs, watch } from 'vue';
import { computed, ref, toRefs, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
@@ -22,6 +22,7 @@ defineOptions({ name: 'ImageUpload', inheritAttrs: false });
const props = withDefaults(defineProps<FileUploadProps>(), {
value: () => [],
modelValue: undefined,
directory: undefined,
disabled: false,
listType: 'picture-card',
@@ -34,7 +35,12 @@ const props = withDefaults(defineProps<FileUploadProps>(), {
resultField: '',
showDescription: true,
});
const emit = defineEmits(['change', 'update:value', 'delete']);
const emit = defineEmits([
'change',
'update:value',
'update:modelValue',
'delete',
]);
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
@@ -43,6 +49,16 @@ const { getStringAccept } = useUploadType({
maxNumberRef: maxNumber,
maxSizeRef: maxSize,
});
// 计算当前绑定的值,优先使用 modelValue
const currentValue = computed(() => {
return props.modelValue === undefined ? props.value : props.modelValue;
});
// 判断是否使用 modelValue
const isUsingModelValue = computed(() => {
return props.modelValue !== undefined;
});
const previewOpen = ref<boolean>(false); // 是否展示预览
const previewImage = ref<string>(''); // 预览图片
const previewTitle = ref<string>(''); // 预览标题
@@ -51,9 +67,11 @@ const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true); // 文件大小错误提示
const isActMsg = ref<boolean>(true); // 文件类型错误提示
const isFirstRender = ref<boolean>(true); // 是否第一次渲染
const uploadNumber = ref<number>(0); // 上传文件计数器
const uploadList = ref<any[]>([]); // 临时上传列表
watch(
() => props.value,
currentValue,
async (v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
@@ -122,6 +140,7 @@ async function handleRemove(file: UploadFile) {
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('update:modelValue', value);
emit('change', value);
emit('delete', file);
}
@@ -133,6 +152,12 @@ function handleCancel() {
}
async function beforeUpload(file: File) {
// 检查文件数量限制
if (fileList.value!.length >= props.maxNumber) {
message.error($t('ui.upload.maxNumber', [props.maxNumber]));
return Upload.LIST_IGNORE;
}
const { maxSize, accept } = props;
const isAct = checkImgType(file, accept);
if (!isAct) {
@@ -140,6 +165,7 @@ async function beforeUpload(file: File) {
isActMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isActMsg.value = true), 1000);
return Upload.LIST_IGNORE;
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
@@ -147,8 +173,12 @@ async function beforeUpload(file: File) {
isLtMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isLtMsg.value = true), 1000);
return Upload.LIST_IGNORE;
}
return (isAct && !isLt) || Upload.LIST_IGNORE;
// 只有在验证通过后才增加计数器
uploadNumber.value++;
return true;
}
async function customRequest(info: UploadRequestOption<any>) {
@@ -163,20 +193,59 @@ async function customRequest(info: UploadRequestOption<any>) {
info.onProgress!({ percent });
};
const res = await api?.(info.file as File, progressEvent);
// 处理上传成功后的逻辑
handleUploadSuccess(res, info.file as File);
info.onSuccess!(res);
message.success($t('ui.upload.uploadSuccess'));
// 更新文件
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) {
console.error(error);
info.onError!(error);
handleUploadError(error);
}
}
// 处理上传成功
function handleUploadSuccess(res: any, file: File) {
// 删除临时文件
const index = fileList.value?.findIndex((item) => item.name === file.name);
if (index !== -1) {
fileList.value?.splice(index!, 1);
}
// 添加到临时上传列表
const fileUrl = res?.url || res?.data || res;
uploadList.value.push({
name: file.name,
url: fileUrl,
status: UploadResultStatus.DONE,
uid: file.name + Date.now(),
});
// 检查是否所有文件都上传完成
if (uploadList.value.length >= uploadNumber.value) {
fileList.value?.push(...uploadList.value);
uploadList.value = [];
uploadNumber.value = 0;
// 更新值
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('update:modelValue', value);
emit('change', value);
}
}
// 处理上传错误
function handleUploadError(error: any) {
console.error('上传错误:', error);
message.error($t('ui.upload.uploadError'));
// 上传失败时减少计数器
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
}
function getValue() {
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE)
@@ -186,11 +255,26 @@ function getValue() {
}
return item?.url || item?.response?.url || item?.response;
});
// add by 芋艿:【特殊】单个文件的情况,获取首个元素,保证返回的是 String 类型
// 单个文件的情况,根据输入参数类型决定返回格式
if (props.maxNumber === 1) {
return list.length > 0 ? list[0] : '';
const singleValue = list.length > 0 ? list[0] : '';
// 如果原始值是字符串或 modelValue 是字符串,返回字符串
if (
isString(props.value) ||
(isUsingModelValue.value && isString(props.modelValue))
) {
return singleValue;
}
return singleValue;
}
return list;
// 多文件情况,根据输入参数类型决定返回格式
if (isUsingModelValue.value) {
return Array.isArray(props.modelValue) ? list : list.join(',');
}
return Array.isArray(props.value) ? list : list.join(',');
}
</script>

View File

@@ -21,10 +21,12 @@ export interface FileUploadProps {
// 上传的目录
directory?: string;
disabled?: boolean;
drag?: boolean; // 是否支持拖拽上传
helpText?: string;
listType?: UploadListType;
// 最大数量的文件Infinity不限制
maxNumber?: number;
modelValue?: string | string[]; // v-model 支持
// 文件最大多少MB
maxSize?: number;
// 是否支持多选

View File

@@ -135,7 +135,7 @@ export function getUploadUrl(): string {
* @param file 文件
*/
function createFile0(
vo: InfraFileApi.FilePresignedUrlResp,
vo: InfraFileApi.FilePresignedUrlRespVO,
file: File,
): InfraFileApi.File {
const fileVO = {

View File

@@ -104,7 +104,7 @@ const routes: RouteRecordRaw[] = [
name: 'BpmProcessInstanceReport',
meta: {
title: '数据报表',
activeMenu: '/bpm/manager/model',
activePath: '/bpm/manager/model',
icon: 'carbon:data-2',
hideInMenu: true,
keepAlive: true,

View File

@@ -16,73 +16,72 @@ const routes: RouteRecordRaw[] = [
name: 'CrmClueDetail',
meta: {
title: '线索详情',
activeMenu: '/crm/clue',
activePath: '/crm/clue',
},
component: () => import('#/views/crm/clue/modules/detail.vue'),
component: () => import('#/views/crm/clue/detail/index.vue'),
},
{
path: 'customer/detail/:id',
name: 'CrmCustomerDetail',
meta: {
title: '客户详情',
activeMenu: '/crm/customer',
activePath: '/crm/customer',
},
component: () => import('#/views/crm/customer/modules/detail.vue'),
component: () => import('#/views/crm/customer/detail/index.vue'),
},
{
path: 'business/detail/:id',
name: 'CrmBusinessDetail',
meta: {
title: '商机详情',
activeMenu: '/crm/business',
activePath: '/crm/business',
},
component: () => import('#/views/crm/business/modules/detail.vue'),
component: () => import('#/views/crm/business/detail/index.vue'),
},
{
path: 'contract/detail/:id',
name: 'CrmContractDetail',
meta: {
title: '合同详情',
activeMenu: '/crm/contract',
activePath: '/crm/contract',
},
component: () => import('#/views/crm/contract/modules/detail.vue'),
component: () => import('#/views/crm/contract/detail/index.vue'),
},
{
path: 'receivable-plan/detail/:id',
name: 'CrmReceivablePlanDetail',
meta: {
title: '回款计划详情',
activeMenu: '/crm/receivable-plan',
activePath: '/crm/receivable-plan',
},
component: () =>
import('#/views/crm/receivable/plan/modules/detail.vue'),
component: () => import('#/views/crm/receivable/plan/detail/index.vue'),
},
{
path: 'receivable/detail/:id',
name: 'CrmReceivableDetail',
meta: {
title: '回款详情',
activeMenu: '/crm/receivable',
activePath: '/crm/receivable',
},
component: () => import('#/views/crm/receivable/modules/detail.vue'),
component: () => import('#/views/crm/receivable/detail/index.vue'),
},
{
path: 'contact/detail/:id',
name: 'CrmContactDetail',
meta: {
title: '联系人详情',
activeMenu: '/crm/contact',
activePath: '/crm/contact',
},
component: () => import('#/views/crm/contact/modules/detail.vue'),
component: () => import('#/views/crm/contact/detail/index.vue'),
},
{
path: 'product/detail/:id',
name: 'CrmProductDetail',
meta: {
title: '产品详情',
activeMenu: '/crm/product',
activePath: '/crm/product',
},
component: () => import('#/views/crm/product/modules/detail.vue'),
component: () => import('#/views/crm/product/detail/index.vue'),
},
],
},

View File

@@ -2,7 +2,7 @@ import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/infra/job/job-log',
path: '/infra/job/log',
component: () => import('#/views/infra/job/logger/index.vue'),
name: 'InfraJobLog',
meta: {
@@ -14,25 +14,16 @@ const routes: RouteRecordRaw[] = [
},
},
{
path: '/codegen',
name: 'CodegenEdit',
path: '/infra/codegen/edit',
component: () => import('#/views/infra/codegen/edit/index.vue'),
name: 'InfraCodegenEdit',
meta: {
title: '代码生成',
title: '生成配置修改',
icon: 'ic:baseline-view-in-ar',
activePath: '/infra/codegen',
keepAlive: true,
hideInMenu: true,
},
children: [
{
path: '/codegen/edit',
name: 'InfraCodegenEdit',
component: () => import('#/views/infra/codegen/edit/index.vue'),
meta: {
title: '修改生成配置',
activeMenu: '/infra/codegen',
},
},
],
},
];

View File

@@ -16,7 +16,7 @@ const routes: RouteRecordRaw[] = [
name: 'ProductSpuAdd',
meta: {
title: '商品添加',
activeMenu: '/mall/product/spu',
activePath: '/mall/product/spu',
},
component: () => import('#/views/mall/product/spu/modules/form.vue'),
},
@@ -25,7 +25,7 @@ const routes: RouteRecordRaw[] = [
name: 'ProductSpuEdit',
meta: {
title: '商品编辑',
activeMenu: '/mall/product/spu',
activePath: '/mall/product/spu',
},
component: () => import('#/views/mall/product/spu/modules/form.vue'),
},
@@ -34,7 +34,7 @@ const routes: RouteRecordRaw[] = [
name: 'ProductSpuDetail',
meta: {
title: '商品详情',
activeMenu: '/crm/business',
activePath: '/crm/business',
},
component: () => import('#/views/mall/product/spu/modules/detail.vue'),
},
@@ -55,7 +55,7 @@ const routes: RouteRecordRaw[] = [
name: 'TradeOrderDetail',
meta: {
title: '订单详情',
activeMenu: '/mall/trade/order',
activePath: '/mall/trade/order',
},
component: () => import('#/views/mall/trade/order/modules/detail.vue'),
},
@@ -64,7 +64,7 @@ const routes: RouteRecordRaw[] = [
name: 'TradeAfterSaleDetail',
meta: {
title: '退款详情',
activeMenu: '/mall/trade/after-sale',
activePath: '/mall/trade/after-sale',
},
component: () =>
import('#/views/mall/trade/afterSale/modules/detail.vue'),

View File

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

View File

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

View File

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

View File

@@ -32,13 +32,12 @@ function onRefresh() {
async function handleDelete(row: AiChatConversationApi.ChatConversation) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteChatConversationByAdmin(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -29,13 +29,12 @@ function onRefresh() {
async function handleDelete(row: AiChatConversationApi.ChatConversation) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteChatMessageByAdmin(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -27,13 +27,12 @@ function onRefresh() {
async function handleDelete(row: AiImageApi.Image) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteImage(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -52,13 +52,12 @@ function handleEdit(id: number) {
async function handleDelete(row: AiKnowledgeDocumentApi.KnowledgeDocument) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteKnowledgeDocument(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -42,13 +42,12 @@ function handleEdit(row: AiKnowledgeKnowledgeApi.Knowledge) {
async function handleDelete(row: AiKnowledgeKnowledgeApi.Knowledge) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteKnowledge(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -49,13 +49,12 @@ function handleEdit(row: AiKnowledgeKnowledgeApi.Knowledge) {
async function handleDelete(row: AiKnowledgeKnowledgeApi.Knowledge) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteKnowledgeSegment(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -34,13 +34,12 @@ function onRefresh() {
async function handleDelete(row: AiMindmapApi.MindMap) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteMindMap(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -37,13 +37,12 @@ function handleEdit(row: AiModelApiKeyApi.ApiKey) {
async function handleDelete(row: AiModelApiKeyApi.ApiKey) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteApiKey(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -37,13 +37,12 @@ function handleEdit(row: AiModelChatRoleApi.ChatRole) {
async function handleDelete(row: AiModelChatRoleApi.ChatRole) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteChatRole(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -42,13 +42,12 @@ function handleEdit(row: AiModelModelApi.Model) {
async function handleDelete(row: AiModelModelApi.Model) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteModel(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -37,13 +37,12 @@ function handleEdit(row: AiModelToolApi.Tool) {
async function handleDelete(row: AiModelToolApi.Tool) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteTool(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -27,13 +27,12 @@ function onRefresh() {
async function handleDelete(row: AiMusicApi.Music) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteMusic(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -39,13 +39,12 @@ function handleEdit(row: any) {
async function handleDelete(row: any) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteWorkflow(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -26,13 +26,12 @@ function onRefresh() {
async function handleDelete(row: AiWriteApi.AiWritePageReq) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteWrite(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -37,13 +37,12 @@ function handleEdit(row: BpmCategoryApi.Category) {
async function handleDelete(row: BpmCategoryApi.Category) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.code]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteCategory(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.code]),
key: 'action_key_msg',
});
onRefresh();
} catch {

View File

@@ -60,13 +60,12 @@ function handleCopy(row: BpmFormApi.Form) {
async function handleDelete(row: BpmFormApi.Form) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteForm(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -41,13 +41,12 @@ function handleEdit(row: BpmUserGroupApi.UserGroup) {
async function handleDelete(row: BpmUserGroupApi.UserGroup) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteUserGroup(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} catch {

View File

@@ -314,12 +314,10 @@ defineExpose({ validate });
</Form.Item>
<Form.Item label="流程类型" name="type" class="mb-5">
<Radio.Group v-model:value="modelData.type">
<!-- TODO BPMN 流程类型需要整合暂时禁用 -->
<Radio
v-for="dict in getDictOptions(DICT_TYPE.BPM_MODEL_TYPE, 'number')"
:key="dict.value"
:key="dict.value as number"
:value="dict.value"
:disabled="dict.value === 10"
>
{{ dict.label }}
</Radio>

View File

@@ -5,6 +5,7 @@ import type { BpmModelApi } from '#/api/bpm/model';
import { inject, onBeforeUnmount, provide, ref, shallowRef, watch } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { BpmModelFormType } from '@vben/constants';
import { message } from 'ant-design-vue';
@@ -18,7 +19,6 @@ import {
import CustomContentPadProvider from '#/components/bpmn-process-designer/package/designer/plugins/content-pad';
// 自定义左侧菜单(修改 默认任务 为 用户任务)
import CustomPaletteProvider from '#/components/bpmn-process-designer/package/designer/plugins/palette';
import { ContentWrap } from '#/components/content-wrap';
defineOptions({ name: 'BpmModelEditor' });

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { ref } from 'vue';
import ContentWrap from '#/components/content-wrap/content-wrap.vue';
import { ContentWrap } from '@vben/common-ui';
import { SimpleProcessDesigner } from '#/components/simple-process-design';
defineOptions({ name: 'SimpleModelDesign' });
@@ -30,7 +31,7 @@ async function validateConfig() {
defineExpose({ validateConfig });
</script>
<template>
<ContentWrap :body-style="{ padding: '20px 16px' }">
<ContentWrap class="px-4 py-5">
<SimpleProcessDesigner
:model-form-id="modelFormId"
:model-name="modelName"

View File

@@ -4,8 +4,9 @@ import type { BpmOALeaveApi } from '#/api/bpm/oa/leave';
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { ContentWrap } from '@vben/common-ui';
import { getLeave } from '#/api/bpm/oa/leave';
import { ContentWrap } from '#/components/content-wrap';
import { Description } from '#/components/description';
import { useDetailFormSchema } from './data';

View File

@@ -40,13 +40,12 @@ function handleEdit(row: BpmProcessExpressionApi.ProcessExpression) {
async function handleDelete(row: BpmProcessExpressionApi.ProcessExpression) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteProcessExpression(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {

View File

@@ -2,7 +2,6 @@
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
import type { SystemUserApi } from '#/api/system/user';
// TODO @jason业务表单审批时读取不到界面参见 https://t.zsxq.com/eif2e
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
import { Page } from '@vben/common-ui';
@@ -156,7 +155,6 @@ async function getApprovalDetail() {
});
} else {
// 注意data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
BusinessFormComponent.value = registerComponent(
data?.processDefinition?.formCustomViewPath || '',
);

View File

@@ -40,13 +40,12 @@ function handleEdit(row: BpmProcessListenerApi.ProcessListener) {
async function handleDelete(row: BpmProcessListenerApi.ProcessListener) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
duration: 0,
});
try {
await deleteProcessListener(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} catch {

View File

@@ -42,14 +42,11 @@ export function useGridFormSchema(): VbenFormSchema[] {
},
{
fieldName: 'status',
label: '流程状态',
label: '审批状态',
component: 'Select',
componentProps: {
options: getDictOptions(
DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
'number',
),
placeholder: '请选择流程状态',
options: getDictOptions(DICT_TYPE.BPM_TASK_STATUS, 'number'),
placeholder: '请选择审批状态',
allowClear: true,
},
},

View File

@@ -46,6 +46,7 @@ export const CONTRACT_EXPIRY_TYPE = [
{ label: '已过期', value: 2 },
];
/** 左侧菜单 */
export const useLeftSides = (
customerTodayContactCount: Ref<number>,
clueFollowCount: Ref<number>,

View File

@@ -78,12 +78,12 @@ async function getCount() {
}
/** 激活时 */
onActivated(async () => {
onActivated(() => {
getCount();
});
/** 初始化 */
onMounted(async () => {
onMounted(() => {
getCount();
});
</script>
@@ -104,9 +104,9 @@ onMounted(async () => {
</List.Item.Meta>
<template #extra>
<Badge
v-if="item.count.value > 0"
:color="item.menu === leftMenu ? 'blue' : 'red'"
:count="item.count.value"
:show-zero="true"
/>
</template>
</List.Item>

View File

@@ -53,6 +53,7 @@ const [Grid] = useVbenVxeGrid({
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,

View File

@@ -53,7 +53,7 @@ const [Grid] = useVbenVxeGrid({
allowClear: true,
options: AUDIT_STATUS,
},
defaultValue: 10,
defaultValue: AUDIT_STATUS[0]!.value,
},
],
},
@@ -75,6 +75,7 @@ const [Grid] = useVbenVxeGrid({
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,

View File

@@ -27,6 +27,7 @@ function handleProcessDetail(row: CrmContractApi.Contract) {
function handleContractDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmContractDetail', params: { id: row.id } });
}
/** 打开客户详情 */
function handleCustomerDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
@@ -53,7 +54,7 @@ const [Grid] = useVbenVxeGrid({
allowClear: true,
options: CONTRACT_EXPIRY_TYPE,
},
defaultValue: 1,
defaultValue: CONTRACT_EXPIRY_TYPE[0]!.value,
},
],
},
@@ -75,6 +76,7 @@ const [Grid] = useVbenVxeGrid({
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,

View File

@@ -53,6 +53,7 @@ const [Grid] = useVbenVxeGrid({
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,

View File

@@ -31,7 +31,7 @@ const [Grid] = useVbenVxeGrid({
allowClear: true,
options: SCENE_TYPES,
},
defaultValue: 1,
defaultValue: SCENE_TYPES[0]!.value,
},
],
},
@@ -53,6 +53,7 @@ const [Grid] = useVbenVxeGrid({
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,

View File

@@ -31,7 +31,7 @@ const [Grid] = useVbenVxeGrid({
allowClear: true,
options: CONTACT_STATUS,
},
defaultValue: 1,
defaultValue: CONTACT_STATUS[0]!.value,
},
{
fieldName: 'sceneType',
@@ -41,7 +41,7 @@ const [Grid] = useVbenVxeGrid({
allowClear: true,
options: SCENE_TYPES,
},
defaultValue: 1,
defaultValue: SCENE_TYPES[0]!.value,
},
],
},
@@ -63,6 +63,7 @@ const [Grid] = useVbenVxeGrid({
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,

View File

@@ -49,7 +49,7 @@ const [Grid] = useVbenVxeGrid({
allowClear: true,
options: AUDIT_STATUS,
},
defaultValue: 10,
defaultValue: AUDIT_STATUS[0]!.value,
},
],
},
@@ -70,6 +70,7 @@ const [Grid] = useVbenVxeGrid({
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,

View File

@@ -49,7 +49,7 @@ const [Grid] = useVbenVxeGrid({
allowClear: true,
options: RECEIVABLE_REMIND_TYPE,
},
defaultValue: 1,
defaultValue: RECEIVABLE_REMIND_TYPE[0]!.value,
},
],
},
@@ -70,6 +70,7 @@ const [Grid] = useVbenVxeGrid({
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,

View File

@@ -0,0 +1,52 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
/** 商机关联列表列定义 */
export function useBusinessDetailListColumns(): VxeTableGridOptions['columns'] {
return [
{
type: 'checkbox',
width: 50,
fixed: 'left',
},
{
field: 'name',
title: '商机名称',
fixed: 'left',
slots: { default: 'name' },
},
{
field: 'customerName',
title: '客户名称',
fixed: 'left',
slots: { default: 'customerName' },
},
{
field: 'totalPrice',
title: '商机金额(元)',
formatter: 'formatAmount2',
},
{
field: 'dealTime',
title: '预计成交日期',
formatter: 'formatDate',
},
{
field: 'ownerUserName',
title: '负责人',
},
{
field: 'ownerUserDeptName',
title: '所属部门',
},
{
field: 'statusTypeName',
title: '商机状态组',
fixed: 'right',
},
{
field: 'statusName',
title: '商机阶段',
fixed: 'right',
},
];
}

View File

@@ -1,3 +1,4 @@
<!-- 商机选择对话框用于联系人详情中关联已有商机 -->
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmBusinessApi } from '#/api/crm/business';
@@ -13,8 +14,8 @@ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getBusinessPageByCustomer } from '#/api/crm/business';
import { $t } from '#/locales';
import { useDetailListColumns } from './detail-data';
import Form from './form.vue';
import Form from '../modules/form.vue';
import { useBusinessDetailListColumns } from './data';
const props = defineProps<{
customerId?: number; // customerId
@@ -35,7 +36,7 @@ function setCheckedRows({ records }: { records: CrmBusinessApi.Business[] }) {
}
/** 刷新表格 */
function onRefresh() {
function handleRefresh() {
gridApi.query();
}
@@ -54,6 +55,7 @@ function handleCustomerDetail(row: CrmBusinessApi.Business) {
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
}
/** 商机关联弹窗 */
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (checkedRows.value.length === 0) {
@@ -71,25 +73,9 @@ const [Modal, modalApi] = useVbenModal({
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
//
const data = modalApi.getData<any>();
if (!data) {
return;
}
modalApi.lock();
try {
// values
// await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
/** 商机选择表格 */
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: [
@@ -101,7 +87,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
],
},
gridOptions: {
columns: useDetailListColumns(),
columns: useBusinessDetailListColumns(),
height: 600,
keepSource: true,
proxyConfig: {
@@ -133,7 +119,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
<template>
<Modal title="关联商机" class="w-2/5">
<FormModal @success="onRefresh" />
<FormModal @success="handleRefresh" />
<Grid>
<template #toolbar-tools>
<TableAction

View File

@@ -1,3 +1,4 @@
<!-- 商机列表用于客户联系人详情中展示其关联的商机列表 -->
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmBusinessApi } from '#/api/crm/business';
@@ -22,9 +23,9 @@ import {
import { BizTypeEnum } from '#/api/crm/permission';
import { $t } from '#/locales';
import { useDetailListColumns } from './detail-data';
import Form from '../modules/form.vue';
import { useBusinessDetailListColumns } from './data';
import ListModal from './detail-list-modal.vue';
import Form from './form.vue';
const props = defineProps<{
bizId: number; //
@@ -51,7 +52,7 @@ function setCheckedRows({ records }: { records: CrmBusinessApi.Business[] }) {
}
/** 刷新表格 */
function onRefresh() {
function handleRefresh() {
gridApi.query();
}
@@ -62,10 +63,12 @@ function handleCreate() {
.open();
}
/** 关联商机 */
function handleCreateBusiness() {
detailListModalApi.setData({ customerId: props.customerId }).open();
}
/** 解除商机关联 */
async function handleDeleteContactBusinessList() {
if (checkedRows.value.length === 0) {
message.error('请先选择商机后操作!');
@@ -83,7 +86,7 @@ async function handleDeleteContactBusinessList() {
if (res) {
//
message.success($t('ui.actionMessage.operationSuccess'));
onRefresh();
handleRefresh();
resolve(true);
} else {
reject(new Error($t('ui.actionMessage.operationFailed')));
@@ -105,18 +108,20 @@ function handleCustomerDetail(row: CrmBusinessApi.Business) {
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
}
/** 创建联系人关联的商机 */
async function handleCreateContactBusinessList(businessIds: number[]) {
const data = {
contactId: props.bizId,
businessIds,
} as CrmContactApi.ContactBusinessReq;
await createContactBusinessList(data);
onRefresh();
handleRefresh();
}
/** 商机关联表格 */
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useDetailListColumns(),
columns: useBusinessDetailListColumns(),
height: 600,
keepSource: true,
proxyConfig: {
@@ -144,6 +149,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
@@ -159,7 +165,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
<template>
<div>
<FormModal @success="onRefresh" />
<FormModal @success="handleRefresh" />
<DetailListModal
:customer-id="customerId"
@success="handleCreateContactBusinessList"

View File

@@ -0,0 +1 @@
export { default as BusinessDetailsList } from './detail-list.vue';

View File

@@ -26,17 +26,27 @@ export function useFormSchema(): VbenFormSchema[] {
label: '商机名称',
component: 'Input',
rules: 'required',
componentProps: {
placeholder: '请输入商机名称',
allowClear: true,
},
},
{
fieldName: 'ownerUserId',
label: '负责人',
component: 'ApiSelect',
dependencies: {
triggerFields: ['id'],
disabled: (values) => values.id,
},
componentProps: {
api: () => getSimpleUserList(),
fieldNames: {
label: 'nickname',
value: 'id',
},
placeholder: '请选择负责人',
allowClear: true,
},
defaultValue: userStore.userInfo?.id,
rules: 'required',
@@ -51,6 +61,8 @@ export function useFormSchema(): VbenFormSchema[] {
label: 'name',
value: 'id',
},
placeholder: '请选择客户',
allowClear: true,
},
dependencies: {
triggerFields: ['id'],
@@ -77,6 +89,8 @@ export function useFormSchema(): VbenFormSchema[] {
label: 'name',
value: 'id',
},
placeholder: '请选择商机状态组',
allowClear: true,
},
dependencies: {
triggerFields: ['id'],
@@ -88,11 +102,11 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'dealTime',
label: '预计成交日期',
component: 'DatePicker',
rules: 'required',
componentProps: {
showTime: false,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
placeholder: '请选择预计成交日期',
},
},
{
@@ -100,6 +114,10 @@ export function useFormSchema(): VbenFormSchema[] {
label: '产品清单',
component: 'Input',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入产品清单',
allowClear: true,
},
},
{
fieldName: 'totalProductPrice',
@@ -108,6 +126,8 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
min: 0,
precision: 2,
disabled: true,
placeholder: '请输入产品总金额',
},
rules: z.number().min(0).optional().default(0),
},
@@ -118,6 +138,7 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
min: 0,
precision: 2,
placeholder: '请输入整单折扣',
},
rules: z.number().min(0).max(100).optional().default(0),
},
@@ -129,6 +150,7 @@ export function useFormSchema(): VbenFormSchema[] {
min: 0,
precision: 2,
disabled: true,
placeholder: '请输入折扣后金额',
},
dependencies: {
triggerFields: ['totalProductPrice', 'discountPercent'],
@@ -170,83 +192,83 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
field: 'name',
title: '商机名称',
fixed: 'left',
minWidth: 240,
width: 160,
slots: { default: 'name' },
},
{
field: 'customerName',
title: '客户名称',
fixed: 'left',
minWidth: 240,
width: 120,
slots: { default: 'customerName' },
},
{
field: 'totalPrice',
title: '商机金额(元)',
minWidth: 140,
width: 140,
formatter: 'formatAmount2',
},
{
field: 'dealTime',
title: '预计成交日期',
formatter: 'formatDate',
minWidth: 180,
width: 180,
},
{
field: 'remark',
title: '备注',
minWidth: 200,
width: 200,
},
{
field: 'contactNextTime',
title: '下次联系时间',
formatter: 'formatDate',
minWidth: 180,
formatter: 'formatDateTime',
width: 180,
},
{
field: 'ownerUserName',
title: '负责人',
minWidth: 120,
width: 100,
},
{
field: 'ownerUserDeptName',
title: '所属部门',
minWidth: 120,
width: 100,
},
{
field: 'contactLastTime',
title: '最后跟进时间',
formatter: 'formatDateTime',
minWidth: 180,
},
{
field: 'createTime',
title: '创建时间',
formatter: 'formatDateTime',
minWidth: 180,
},
{
field: 'creatorName',
title: '创建人',
minWidth: 120,
width: 180,
},
{
field: 'updateTime',
title: '更新时间',
formatter: 'formatDateTime',
minWidth: 180,
width: 180,
},
{
field: 'createTime',
title: '创建时间',
formatter: 'formatDateTime',
width: 180,
},
{
field: 'creatorName',
title: '创建人',
width: 100,
},
{
field: 'statusTypeName',
title: '商机状态组',
fixed: 'right',
minWidth: 120,
width: 140,
},
{
field: 'statusName',
title: '商机阶段',
fixed: 'right',
minWidth: 120,
width: 120,
},
{
title: '操作',

View File

@@ -1,8 +1,12 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { Ref } from 'vue';
import type { CrmBusinessApi } from '#/api/crm/business';
import type { DescriptionItemSchema } from '#/components/description';
import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
import { DEFAULT_STATUSES, getBusinessStatusSimpleList } from '#/api/crm/business/status';
/** 详情页的字段 */
export function useDetailSchema(): DescriptionItemSchema[] {
return [
@@ -72,53 +76,62 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] {
];
}
/** 详情列表的字段 */
export function useDetailListColumns(): VxeTableGridOptions['columns'] {
/** 商机状态更新表单 */
export function useStatusFormSchema(
formData: Ref<CrmBusinessApi.Business | undefined>,
): VbenFormSchema[] {
return [
{
type: 'checkbox',
width: 50,
fixed: 'left',
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
field: 'name',
title: '商机名称',
fixed: 'left',
slots: { default: 'name' },
fieldName: 'statusId',
label: '商机状态',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
field: 'customerName',
title: '客户名称',
fixed: 'left',
slots: { default: 'customerName' },
fieldName: 'endStatus',
label: '商机状态',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
field: 'totalPrice',
title: '商机金额(元)',
formatter: 'formatAmount2',
},
{
field: 'dealTime',
title: '预计成交日期',
formatter: 'formatDate',
},
{
field: 'ownerUserName',
title: '负责人',
},
{
field: 'ownerUserDeptName',
title: '所属部门',
},
{
field: 'statusTypeName',
title: '商机状态组',
fixed: 'right',
},
{
field: 'statusName',
title: '商机阶段',
fixed: 'right',
fieldName: 'status',
label: '商机阶段',
component: 'Select',
dependencies: {
triggerFields: [''],
async componentProps() {
const statusList = await getBusinessStatusSimpleList(
formData.value?.statusTypeId ?? 0,
);
const statusOptions = statusList.map((item) => ({
label: `${item.name}(赢单率:${item.percent}%)`,
value: item.id,
}));
const options = DEFAULT_STATUSES.map((item) => ({
label: `${item.name}(赢单率:${item.percent}%)`,
value: item.endStatus,
}));
statusOptions.push(...options);
return {
options: statusOptions,
};
},
},
rules: 'required',
},
];
}

View File

@@ -14,30 +14,27 @@ import { getBusiness } from '#/api/crm/business';
import { getOperateLogPage } from '#/api/crm/operateLog';
import { BizTypeEnum } from '#/api/crm/permission';
import { useDescription } from '#/components/description';
import { AsyncOperateLog } from '#/components/operate-log';
import {
BusinessDetailsInfo,
BusinessForm,
UpStatusForm,
} from '#/views/crm/business';
import { ContactDetailsList } from '#/views/crm/contact';
import { ContractDetailsList } from '#/views/crm/contract';
import { OperateLog } from '#/components/operate-log';
import { $t } from '#/locales';
import { ContactDetailsList } from '#/views/crm/contact/components';
import { ContractDetailsList } from '#/views/crm/contract/components';
import { FollowUp } from '#/views/crm/followup';
import { PermissionList, TransferForm } from '#/views/crm/permission';
import { ProductDetailsList } from '#/views/crm/product';
import { ProductDetailsList } from '#/views/crm/product/components';
import { useDetailSchema } from './detail-data';
const loading = ref(false);
import Form from '../modules/form.vue';
import UpStatusForm from './modules/status-form.vue';
import { useDetailSchema } from './data';
import BusinessDetailsInfo from './modules/info.vue';
const route = useRoute();
const router = useRouter();
const tabs = useTabs();
const businessId = ref(0);
const business = ref<CrmBusinessApi.Business>({} as CrmBusinessApi.Business);
const businessLogList = ref<SystemOperateLogApi.OperateLog[]>([]);
const loading = ref(false); //
const businessId = ref(0); //
const business = ref<CrmBusinessApi.Business>({} as CrmBusinessApi.Business); //
const logList = ref<SystemOperateLogApi.OperateLog[]>([]);
const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // Ref
const [Descriptions] = useDescription({
@@ -50,7 +47,7 @@ const [Descriptions] = useDescription({
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: BusinessForm,
connectedComponent: Form,
destroyOnClose: true,
});
@@ -65,16 +62,19 @@ const [UpStatusModal, upStatusModalApi] = useVbenModal({
});
/** 加载详情 */
async function loadBusinessDetail() {
async function getBusinessDetail() {
loading.value = true;
const data = await getBusiness(businessId.value);
const logList = await getOperateLogPage({
bizType: BizTypeEnum.CRM_BUSINESS,
bizId: businessId.value,
});
businessLogList.value = logList.list;
business.value = data;
loading.value = false;
try {
business.value = await getBusiness(businessId.value);
//
const res = await getOperateLogPage({
bizType: BizTypeEnum.CRM_BUSINESS,
bizId: businessId.value,
});
logList.value = res.list;
} finally {
loading.value = false;
}
}
/** 返回列表页 */
@@ -83,12 +83,12 @@ function handleBack() {
router.push('/crm/business');
}
/** 编辑 */
/** 编辑商机 */
function handleEdit() {
formModalApi.setData({ id: businessId.value }).open();
}
/** 转移线索 */
/** 转移商机 */
function handleTransfer() {
transferModalApi.setData({ bizType: BizTypeEnum.CRM_BUSINESS }).open();
}
@@ -98,18 +98,18 @@ async function handleUpdateStatus() {
upStatusModalApi.setData(business.value).open();
}
//
/** 加载数据 */
onMounted(() => {
businessId.value = Number(route.params.id);
loadBusinessDetail();
getBusinessDetail();
});
</script>
<template>
<Page auto-content-height :title="business?.name" :loading="loading">
<FormModal @success="loadBusinessDetail" />
<TransferModal @success="loadBusinessDetail" />
<UpStatusModal @success="loadBusinessDetail" />
<FormModal @success="getBusinessDetail" />
<TransferModal @success="getBusinessDetail" />
<UpStatusModal @success="getBusinessDetail" />
<template #extra>
<div class="flex items-center gap-2">
<Button
@@ -138,12 +138,12 @@ onMounted(() => {
</Card>
<Card class="mt-4 min-h-[60%]">
<Tabs>
<Tabs.TabPane tab="详细资料" key="1" :force-render="true">
<BusinessDetailsInfo :business="business" />
</Tabs.TabPane>
<Tabs.TabPane tab="跟进记录" key="2" :force-render="true">
<Tabs.TabPane tab="跟进记录" key="1" :force-render="true">
<FollowUp :biz-id="businessId" :biz-type="BizTypeEnum.CRM_BUSINESS" />
</Tabs.TabPane>
<Tabs.TabPane tab="详细资料" key="2" :force-render="true">
<BusinessDetailsInfo :business="business" />
</Tabs.TabPane>
<Tabs.TabPane tab="联系人" key="3" :force-render="true">
<ContactDetailsList
:biz-id="businessId"
@@ -165,7 +165,10 @@ onMounted(() => {
:biz-type="BizTypeEnum.CRM_BUSINESS"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="团队成员" key="6" :force-render="true">
<Tabs.TabPane tab="操作日志" key="6" :force-render="true">
<OperateLog :log-list="logList" />
</Tabs.TabPane>
<Tabs.TabPane tab="团队成员" key="7" :force-render="true">
<PermissionList
ref="permissionListRef"
:biz-id="businessId"
@@ -174,9 +177,6 @@ onMounted(() => {
@quit-team="handleBack"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="操作日志" key="7" :force-render="true">
<AsyncOperateLog :log-list="businessLogList" />
</Tabs.TabPane>
</Tabs>
</Card>
</Page>

View File

@@ -6,7 +6,7 @@ import { Divider } from 'ant-design-vue';
import { useDescription } from '#/components/description';
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
import { useDetailBaseSchema } from './detail-data';
import { useDetailBaseSchema } from '../data';
defineProps<{
business: CrmBusinessApi.Business; //

View File

@@ -9,12 +9,10 @@ import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { updateBusinessStatus } from '#/api/crm/business';
import {
DEFAULT_STATUSES,
getBusinessStatusSimpleList,
} from '#/api/crm/business/status';
import { $t } from '#/locales';
import { useStatusFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<CrmBusinessApi.Business>();
@@ -28,60 +26,7 @@ const [Form, formApi] = useVbenForm({
labelWidth: 120,
},
layout: 'horizontal',
schema: [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'statusId',
label: '商机状态',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'endStatus',
label: '商机状态',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
label: '商机阶段',
component: 'Select',
dependencies: {
triggerFields: [''],
async componentProps() {
const statusList = await getBusinessStatusSimpleList(
formData.value?.statusTypeId ?? 0,
);
const statusOptions = statusList.map((item) => ({
label: `${item.name}(赢单率:${item.percent}%)`,
value: item.id,
}));
const options = DEFAULT_STATUSES.map((item) => ({
label: `${`${item.name}(赢单率:${item.percent}`}%)`,
value: item.endStatus,
}));
statusOptions.push(...options);
return {
options: statusOptions,
};
},
},
rules: 'required',
},
],
schema: useStatusFormSchema(formData),
showDefaultActions: false,
});

View File

@@ -1,25 +0,0 @@
import { defineAsyncComponent } from 'vue';
export const BusinessForm = defineAsyncComponent(
() => import('./modules/form.vue'),
);
export const BusinessDetailsInfo = defineAsyncComponent(
() => import('./modules/detail-info.vue'),
);
export const BusinessDetailsList = defineAsyncComponent(
() => import('./modules/detail-list.vue'),
);
export const BusinessDetails = defineAsyncComponent(
() => import('./modules/detail.vue'),
);
export const BusinessDetailsListModal = defineAsyncComponent(
() => import('./modules/detail-list-modal.vue'),
);
export const UpStatusForm = defineAsyncComponent(
() => import('./modules/up-status-form.vue'),
);

View File

@@ -2,12 +2,13 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmBusinessApi } from '#/api/crm/business';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { Button, message, Tabs } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@@ -21,6 +22,7 @@ import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const { push } = useRouter();
const sceneType = ref('1');
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
@@ -28,13 +30,23 @@ const [FormModal, formModalApi] = useVbenModal({
});
/** 刷新表格 */
function onRefresh() {
function handleRefresh() {
gridApi.query();
}
/** 处理场景类型的切换 */
function handleChangeSceneType(key: number | string) {
sceneType.value = key.toString();
gridApi.query();
}
/** 导出表格 */
async function handleExport() {
const data = await exportBusiness(await gridApi.formApi.getValues());
const formValues = await gridApi.formApi.getValues();
const data = await exportBusiness({
sceneType: sceneType.value,
...formValues,
});
downloadFileFromBlobPart({ fileName: '商机.xls', source: data });
}
@@ -53,13 +65,12 @@ async function handleDelete(row: CrmBusinessApi.Business) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteBusiness(row.id as number);
await deleteBusiness(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
onRefresh();
} catch {
handleRefresh();
} finally {
hideLoading();
}
}
@@ -88,6 +99,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
return await getBusinessPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
sceneType: sceneType.value,
...formValues,
});
},
@@ -95,6 +107,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
@@ -117,8 +130,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
/>
</template>
<FormModal @success="onRefresh" />
<Grid table-title="商机列表">
<FormModal @success="handleRefresh" />
<Grid>
<template #top>
<Tabs class="-mt-11" @change="handleChangeSceneType">
<Tabs.TabPane tab="我负责的" key="1" />
<Tabs.TabPane tab="我参与的" key="2" />
<Tabs.TabPane tab="下属负责的" key="3" />
</Tabs>
</template>
<template #toolbar-tools>
<TableAction
:actions="[
@@ -176,3 +196,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
</Grid>
</Page>
</template>
<style scoped>
:deep(.vxe-toolbar div) {
z-index: 1;
}
</style>

View File

@@ -16,7 +16,7 @@ import {
} from '#/api/crm/business';
import { BizTypeEnum } from '#/api/crm/permission';
import { $t } from '#/locales';
import { ProductEditTable } from '#/views/crm/product';
import { ProductEditTable } from '#/views/crm/product/components';
import { useFormSchema } from '../data';
@@ -56,7 +56,6 @@ const [Form, formApi] = useVbenForm({
},
labelWidth: 120,
},
// 一共3列
wrapperClass: 'grid-cols-3',
layout: 'vertical',
schema: useFormSchema(),
@@ -90,7 +89,7 @@ const [Modal, modalApi] = useVbenModal({
}
// 加载数据
const data = modalApi.getData<CrmBusinessApi.Business>();
if (!data) {
if (!data || !data.id) {
return;
}
modalApi.lock();

View File

@@ -21,6 +21,9 @@ export function useFormSchema(): VbenFormSchema[] {
label: '状态组名',
component: 'Input',
rules: 'required',
componentProps: {
placeholder: '请输入状态组名',
},
},
{
fieldName: 'deptIds',
@@ -77,3 +80,33 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
},
];
}
/** 商机状态阶段列表列配置 */
export function useFormColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'defaultStatus',
title: '阶段',
minWidth: 100,
slots: { default: 'defaultStatus' },
},
{
field: 'name',
title: '阶段名称',
minWidth: 100,
slots: { default: 'name' },
},
{
field: 'percent',
title: '赢单率(%',
minWidth: 100,
slots: { default: 'percent' },
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -22,7 +22,7 @@ const [FormModal, formModalApi] = useVbenModal({
});
/** 刷新表格 */
function onRefresh() {
function handleRefresh() {
gridApi.query();
}
@@ -31,27 +31,26 @@ function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑商机状态 */
function handleEdit(row: CrmBusinessStatusApi.BusinessStatus) {
formModalApi.setData(row).open();
}
/** 删除商机状态 */
async function handleDelete(row: CrmBusinessStatusApi.BusinessStatus) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteBusinessStatus(row.id as number);
await deleteBusinessStatus(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
onRefresh();
handleRefresh();
} catch {
hideLoading();
}
}
/** 编辑商机状态 */
function handleEdit(row: CrmBusinessStatusApi.BusinessStatus) {
formModalApi.setData(row).open();
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
@@ -70,6 +69,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
@@ -92,7 +92,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
/>
</template>
<FormModal @success="onRefresh" />
<FormModal @success="handleRefresh" />
<Grid table-title="商机状态列表">
<template #toolbar-tools>
<TableAction

View File

@@ -17,7 +17,7 @@ import {
} from '#/api/crm/business/status';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
import { useFormColumns, useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<CrmBusinessStatusApi.BusinessStatus>();
@@ -72,7 +72,6 @@ const [Modal, modalApi] = useVbenModal({
}
// 加载数据
const data = modalApi.getData<CrmBusinessStatusApi.BusinessStatus>();
modalApi.lock();
try {
if (!data || !data.id) {
@@ -82,20 +81,19 @@ const [Modal, modalApi] = useVbenModal({
deptIds: [],
statuses: [],
};
addStatus();
await handleAddStatus();
} else {
formData.value = await getBusinessStatus(data.id);
if (
!formData.value?.statuses?.length ||
formData.value?.statuses?.length === 0
) {
addStatus();
await handleAddStatus();
}
}
// 设置到 values
await formApi.setValues(formData.value as any);
gridApi.grid.reloadData(
await gridApi.grid.reloadData(
(formData.value!.statuses =
formData.value?.statuses?.concat(DEFAULT_STATUSES)) as any,
);
@@ -106,20 +104,20 @@ const [Modal, modalApi] = useVbenModal({
});
/** 添加状态 */
async function addStatus() {
async function handleAddStatus() {
formData.value!.statuses!.unshift({
name: '',
percent: undefined,
} as any);
await nextTick();
gridApi.grid.reloadData(formData.value!.statuses as any);
await gridApi.grid.reloadData(formData.value!.statuses as any);
}
/** 删除状态 */
async function deleteStatusArea(row: any, rowIndex: number) {
gridApi.grid.remove(row);
await gridApi.grid.remove(row);
formData.value!.statuses!.splice(rowIndex, 1);
gridApi.grid.reloadData(formData.value!.statuses as any);
await gridApi.grid.reloadData(formData.value!.statuses as any);
}
/** 表格配置 */
@@ -129,32 +127,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
trigger: 'click',
mode: 'cell',
},
columns: [
{
field: 'defaultStatus',
title: '阶段',
minWidth: 100,
slots: { default: 'defaultStatus' },
},
{
field: 'name',
title: '阶段名称',
minWidth: 100,
slots: { default: 'name' },
},
{
field: 'percent',
title: '赢单率(%',
minWidth: 100,
slots: { default: 'percent' },
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
],
columns: useFormColumns(),
data: formData.value?.statuses?.concat(DEFAULT_STATUSES),
border: true,
showOverflow: true,
@@ -162,6 +135,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
keepSource: true,
rowConfig: {
keyField: 'row_id',
isHover: true,
},
pagerConfig: {
enabled: false,
@@ -184,7 +158,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
</span>
</template>
<template #name="{ row }">
<Input v-if="!row.endStatus" v-model:value="row.name" />
<Input
v-if="!row.endStatus"
v-model:value="row.name"
placeholder="请输入状态名"
/>
<span v-else>{{ row.name }}</span>
</template>
<template #percent="{ row }">
@@ -194,6 +172,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
:min="0"
:max="100"
:precision="2"
placeholder="请输入赢单率"
/>
<span v-else>{{ row.percent }}</span>
</template>
@@ -204,7 +183,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
label: $t('ui.actionTitle.create'),
type: 'link',
ifShow: () => !row.endStatus,
onClick: addStatus,
onClick: handleAddStatus,
},
{
label: $t('common.delete'),

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