!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; creatorName: string;
updateTime?: Date; updateTime?: Date;
products?: ContractProduct[]; products?: ContractProduct[];
contactName?: string;
} }
} }

View File

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

View File

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

View File

@@ -65,7 +65,7 @@ export namespace InfraCodegenApi {
} }
/** 更新代码生成请求 */ /** 更新代码生成请求 */
export interface CodegenUpdateReq { export interface CodegenUpdateReqVO {
table: any | CodegenTable; table: any | CodegenTable;
columns: CodegenColumn[]; 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); return requestClient.put('/infra/codegen/update', data);
} }
/** 基于数据库的表结构,同步数据库的表和字段定义 */ /** 基于数据库的表结构,同步数据库的表和字段定义 */
export function syncCodegenFromDB(tableId: number) { 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) { export function previewCodegen(tableId: number) {
return requestClient.get<InfraCodegenApi.CodegenPreview[]>( return requestClient.get<InfraCodegenApi.CodegenPreview[]>(
`/infra/codegen/preview?tableId=${tableId}`, '/infra/codegen/preview',
{
params: { tableId },
},
); );
} }
/** 下载生成代码 */ /** 下载生成代码 */
export function downloadCodegen(tableId: number) { 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) { export function deleteDataSourceConfig(id: number) {
return requestClient.delete(`/infra/data-source-config/delete?id=${id}`); 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; // 文件配置编号 configId: number; // 文件配置编号
uploadUrl: string; // 文件上传 URL uploadUrl: string; // 文件上传 URL
url: string; // 文件 URL url: string; // 文件 URL
@@ -27,7 +27,7 @@ export namespace InfraFileApi {
} }
/** 上传文件 */ /** 上传文件 */
export interface FileUploadReq { export interface FileUploadReqVO {
file: globalThis.File; file: globalThis.File;
directory?: string; directory?: string;
} }
@@ -52,7 +52,7 @@ export function deleteFileList(ids: number[]) {
/** 获取文件预签名地址 */ /** 获取文件预签名地址 */
export function getFilePresignedUrl(name: string, directory?: string) { export function getFilePresignedUrl(name: string, directory?: string) {
return requestClient.get<InfraFileApi.FilePresignedUrlResp>( return requestClient.get<InfraFileApi.FilePresignedUrlRespVO>(
'/infra/file/presigned-url', '/infra/file/presigned-url',
{ {
params: { name, directory }, params: { name, directory },
@@ -67,7 +67,7 @@ export function createFile(data: InfraFileApi.File) {
/** 上传文件 */ /** 上传文件 */
export function uploadFile( export function uploadFile(
data: InfraFileApi.FileUploadReq, data: InfraFileApi.FileUploadReqVO,
onUploadProgress?: AxiosProgressEvent, onUploadProgress?: AxiosProgressEvent,
) { ) {
// 特殊:由于 upload 内部封装,即使 directory 为 undefined也会传递给后端 // 特殊:由于 upload 内部封装,即使 directory 为 undefined也会传递给后端

View File

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

View File

@@ -62,7 +62,7 @@ export function deleteAccount(id: number) {
/** 生成公众号账号二维码 */ /** 生成公众号账号二维码 */
export function generateAccountQrCode(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 配额 */ /** 清空公众号账号 API 配额 */

View File

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

View File

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

View File

@@ -32,10 +32,3 @@ export function deleteOAuth2Token(accessToken: string) {
`/system/oauth2-token/delete?accessToken=${accessToken}`, `/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 namespace SystemPermissionApi {
/** 分配用户角色请求 */ /** 分配用户角色请求 */
export interface AssignUserRoleReq { export interface AssignUserRoleReqVO {
userId: number; userId: number;
roleIds: number[]; roleIds: number[];
} }
/** 分配角色菜单请求 */ /** 分配角色菜单请求 */
export interface AssignRoleMenuReq { export interface AssignRoleMenuReqVO {
roleId: number; roleId: number;
menuIds: number[]; menuIds: number[];
} }
/** 分配角色数据权限请求 */ /** 分配角色数据权限请求 */
export interface AssignRoleDataScopeReq { export interface AssignRoleDataScopeReqVO {
roleId: number; roleId: number;
dataScope: number; dataScope: number;
dataScopeDeptIds: number[]; dataScopeDeptIds: number[];
@@ -30,14 +30,14 @@ export async function getRoleMenuList(roleId: number) {
/** 赋予角色菜单权限 */ /** 赋予角色菜单权限 */
export async function assignRoleMenu( export async function assignRoleMenu(
data: SystemPermissionApi.AssignRoleMenuReq, data: SystemPermissionApi.AssignRoleMenuReqVO,
) { ) {
return requestClient.post('/system/permission/assign-role-menu', data); return requestClient.post('/system/permission/assign-role-menu', data);
} }
/** 赋予角色数据权限 */ /** 赋予角色数据权限 */
export async function assignRoleDataScope( export async function assignRoleDataScope(
data: SystemPermissionApi.AssignRoleDataScopeReq, data: SystemPermissionApi.AssignRoleDataScopeReqVO,
) { ) {
return requestClient.post('/system/permission/assign-role-data-scope', data); return requestClient.post('/system/permission/assign-role-data-scope', data);
} }
@@ -51,7 +51,7 @@ export async function getUserRoleList(userId: number) {
/** 赋予用户角色 */ /** 赋予用户角色 */
export async function assignUserRole( export async function assignUserRole(
data: SystemPermissionApi.AssignUserRoleReq, data: SystemPermissionApi.AssignUserRoleReqVO,
) { ) {
return requestClient.post('/system/permission/assign-user-role', data); 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; type: number;
code: string; code: string;
state: string; state: string;
} }
/** 取消社交绑定请求 */ /** 取消社交绑定请求 */
export interface SocialUserUnbindReq { export interface SocialUserUnbindReqVO {
type: number; type: number;
openid: string; openid: string;
} }
@@ -49,12 +49,12 @@ export function getSocialUser(id: number) {
} }
/** 社交绑定,使用 code 授权码 */ /** 社交绑定,使用 code 授权码 */
export function socialBind(data: SystemSocialUserApi.SocialUserBindReq) { export function socialBind(data: SystemSocialUserApi.SocialUserBindReqVO) {
return requestClient.post<boolean>('/system/social-user/bind', data); 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 }); return requestClient.delete<boolean>('/system/social-user/unbind', { data });
} }

View File

@@ -2,7 +2,7 @@ import { requestClient } from '#/api/request';
export namespace SystemUserProfileApi { export namespace SystemUserProfileApi {
/** 用户个人中心信息 */ /** 用户个人中心信息 */
export interface UserProfileResp { export interface UserProfileRespVO {
id: number; id: number;
username: string; username: string;
nickname: string; nickname: string;
@@ -19,13 +19,13 @@ export namespace SystemUserProfileApi {
} }
/** 更新密码请求 */ /** 更新密码请求 */
export interface UpdatePasswordReq { export interface UpdatePasswordReqVO {
oldPassword: string; oldPassword: string;
newPassword: string; newPassword: string;
} }
/** 更新个人信息请求 */ /** 更新个人信息请求 */
export interface UpdateProfileReq { export interface UpdateProfileReqVO {
nickname?: string; nickname?: string;
email?: string; email?: string;
mobile?: string; mobile?: string;
@@ -36,19 +36,21 @@ export namespace SystemUserProfileApi {
/** 获取登录用户信息 */ /** 获取登录用户信息 */
export function getUserProfile() { export function getUserProfile() {
return requestClient.get<SystemUserProfileApi.UserProfileResp>( return requestClient.get<SystemUserProfileApi.UserProfileRespVO>(
'/system/user/profile/get', '/system/user/profile/get',
); );
} }
/** 修改用户个人信息 */ /** 修改用户个人信息 */
export function updateUserProfile(data: SystemUserProfileApi.UpdateProfileReq) { export function updateUserProfile(
data: SystemUserProfileApi.UpdateProfileReqVO,
) {
return requestClient.put('/system/user/profile/update', data); return requestClient.put('/system/user/profile/update', data);
} }
/** 修改用户个人密码 */ /** 修改用户个人密码 */
export function updateUserPassword( export function updateUserPassword(
data: SystemUserProfileApi.UpdatePasswordReq, data: SystemUserProfileApi.UpdatePasswordReqVO,
) { ) {
return requestClient.put('/system/user/profile/update-password', data); 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-codes.css'
// import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.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 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css' // 右侧框样式
import { import { computed, h, onBeforeUnmount, onMounted, provide, ref } from 'vue';
computed,
defineEmits,
defineOptions,
defineProps,
h,
onBeforeUnmount,
onMounted,
provide,
ref,
} from 'vue';
import { import {
AlignLeftOutlined, AlignLeftOutlined,
@@ -655,7 +645,7 @@ onBeforeUnmount(() => {
type="file" type="file"
id="files" id="files"
ref="refFile" ref="refFile"
style="display: none" class="hidden"
accept=".xml, .bpmn" accept=".xml, .bpmn"
@change="importLocalFile" @change="importLocalFile"
/> />

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <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 { BpmProcessInstanceStatus, DICT_TYPE } from '@vben/constants';
import { UndoOutlined, ZoomInOutlined, ZoomOutOutlined } from '@vben/icons'; 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.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'; 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/dist/assets/bpmn-font/css/bpmn-embedded.css';
// TODO @puhui999样式问题设计器那位置不太对
export { default as MyProcessDesigner } from './designer'; export { default as MyProcessDesigner } from './designer';
// TODO @puhui999流程发起时预览相关的需要使用
export { default as MyProcessViewer } from './designer/index2'; export { default as MyProcessViewer } from './designer/index2';
export { default as MyProcessPenal } from './penal'; 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> <script lang="ts" setup>
import type { Component } from 'vue'; import type { Component } from 'vue';
import { defineOptions, defineProps, ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { CustomConfigMap } from './data'; import { CustomConfigMap } from './data';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -344,7 +344,7 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }"> <Form>
<FormItem label="规则类型" name="candidateStrategy"> <FormItem label="规则类型" name="candidateStrategy">
<Select <Select
v-model:value="userTaskForm.candidateStrategy" 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 { computed, nextTick, onMounted, ref, toRaw, watch } from 'vue';
import { import { IconifyIcon } from '@vben/icons';
CheckCircleFilled,
ExclamationCircleFilled,
IconifyIcon,
QuestionCircleFilled,
} from '@vben/icons';
import { Button, DatePicker, Input, Modal, Tooltip } from 'ant-design-vue'; import { Button, DatePicker, Input, Modal, Tooltip } from 'ant-design-vue';
@@ -240,7 +235,11 @@ watch(
循环 循环
</Button> </Button>
</Button.Group> </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>
<div style="display: flex; align-items: center; margin-top: 10px"> <div style="display: flex; align-items: center; margin-top: 10px">
<span>条件</span> <span>条件</span>
@@ -254,11 +253,15 @@ watch(
> >
<template #suffix> <template #suffix>
<Tooltip v-if="!valid" title="格式错误" placement="top"> <Tooltip v-if="!valid" title="格式错误" placement="top">
<ExclamationCircleFilled style="color: orange" /> <IconifyIcon
icon="ant-design:exclamation-circle-filled"
class="text-orange-400"
/>
</Tooltip> </Tooltip>
<Tooltip :title="helpText" placement="top"> <Tooltip :title="helpText" placement="top">
<QuestionCircleFilled <IconifyIcon
style="color: #409eff; cursor: pointer" icon="ant-design:question-circle-filled"
class="cursor-pointer text-[#409eff]"
@click="showHelp = true" @click="showHelp = true"
/> />
</Tooltip> </Tooltip>
@@ -351,7 +354,3 @@ watch(
</Modal> </Modal>
</div> </div>
</template> </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 { default as OperateLog } from './operate-log.vue';
export type { OperateLogProps } from './typing'; export type { OperateLogProps } from './typing';

View File

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

View File

@@ -6,7 +6,7 @@ import type { FileUploadProps } from './typing';
import type { AxiosProgressEvent } from '#/api/infra/file'; 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 { CloudUpload } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
@@ -22,8 +22,10 @@ defineOptions({ name: 'FileUpload', inheritAttrs: false });
const props = withDefaults(defineProps<FileUploadProps>(), { const props = withDefaults(defineProps<FileUploadProps>(), {
value: () => [], value: () => [],
modelValue: undefined,
directory: undefined, directory: undefined,
disabled: false, disabled: false,
drag: false,
helpText: '', helpText: '',
maxSize: 2, maxSize: 2,
maxNumber: 1, maxNumber: 1,
@@ -33,7 +35,14 @@ const props = withDefaults(defineProps<FileUploadProps>(), {
resultField: '', resultField: '',
showDescription: false, 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 { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false); const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({ const { getStringAccept } = useUploadType({
@@ -43,13 +52,25 @@ const { getStringAccept } = useUploadType({
maxSizeRef: maxSize, 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 fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true); // 文件大小错误提示 const isLtMsg = ref<boolean>(true); // 文件大小错误提示
const isActMsg = ref<boolean>(true); // 文件类型错误提示 const isActMsg = ref<boolean>(true); // 文件类型错误提示
const isFirstRender = ref<boolean>(true); // 是否第一次渲染 const isFirstRender = ref<boolean>(true); // 是否第一次渲染
const uploadNumber = ref<number>(0); // 上传文件计数器
const uploadList = ref<any[]>([]); // 临时上传列表
watch( watch(
() => props.value, currentValue,
(v) => { (v) => {
if (isInnerOperate.value) { if (isInnerOperate.value) {
isInnerOperate.value = false; isInnerOperate.value = false;
@@ -94,15 +115,40 @@ async function handleRemove(file: UploadFile) {
const value = getValue(); const value = getValue();
isInnerOperate.value = true; isInnerOperate.value = true;
emit('update:value', value); emit('update:value', value);
emit('update:modelValue', value);
emit('change', value); emit('change', value);
emit('delete', file); 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) { async function beforeUpload(file: File) {
const fileContent = await file.text(); const fileContent = await file.text();
emit('returnText', fileContent); 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 { maxSize, accept } = props;
const isAct = checkFileType(file, accept); const isAct = checkFileType(file, accept);
if (!isAct) { if (!isAct) {
@@ -110,6 +156,7 @@ async function beforeUpload(file: File) {
isActMsg.value = false; isActMsg.value = false;
// 防止弹出多个错误提示 // 防止弹出多个错误提示
setTimeout(() => (isActMsg.value = true), 1000); setTimeout(() => (isActMsg.value = true), 1000);
return Upload.LIST_IGNORE;
} }
const isLt = file.size / 1024 / 1024 > maxSize; const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) { if (isLt) {
@@ -117,8 +164,12 @@ async function beforeUpload(file: File) {
isLtMsg.value = false; isLtMsg.value = false;
// 防止弹出多个错误提示 // 防止弹出多个错误提示
setTimeout(() => (isLtMsg.value = true), 1000); 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>) { async function customRequest(info: UploadRequestOption<any>) {
@@ -133,17 +184,48 @@ async function customRequest(info: UploadRequestOption<any>) {
info.onProgress!({ percent }); info.onProgress!({ percent });
}; };
const res = await api?.(info.file as File, progressEvent); const res = await api?.(info.file as File, progressEvent);
// 处理上传成功后的逻辑
handleUploadSuccess(res, info.file as File);
info.onSuccess!(res); info.onSuccess!(res);
message.success($t('ui.upload.uploadSuccess')); message.success($t('ui.upload.uploadSuccess'));
// 更新文件
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
info.onError!(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; return item?.url || item?.response?.url || item?.response;
}); });
// add by 芋艿:【特殊】单个文件的情况,获取首个元素,保证返回的是 String 类型
// 单个文件的情况,根据输入参数类型决定返回格式
if (props.maxNumber === 1) { 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 list; return singleValue;
}
// 多文件情况,根据输入参数类型决定返回格式
if (isUsingModelValue.value) {
return Array.isArray(props.modelValue) ? list : list.join(',');
}
return Array.isArray(props.value) ? list : list.join(',');
} }
</script> </script>
@@ -177,15 +274,34 @@ function getValue() {
:multiple="multiple" :multiple="multiple"
list-type="text" list-type="text"
:progress="{ showInfo: true }" :progress="{ showInfo: true }"
:show-upload-list="{
showPreviewIcon: true,
showRemoveIcon: true,
showDownloadIcon: true,
}"
@remove="handleRemove" @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> <Button>
<CloudUpload /> <CloudUpload />
{{ $t('ui.upload.upload') }} {{ $t('ui.upload.upload') }}
</Button> </Button>
</div> </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> <div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
@@ -195,3 +311,35 @@ function getValue() {
</Upload> </Upload>
</div> </div>
</template> </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 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 { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
@@ -22,6 +22,7 @@ defineOptions({ name: 'ImageUpload', inheritAttrs: false });
const props = withDefaults(defineProps<FileUploadProps>(), { const props = withDefaults(defineProps<FileUploadProps>(), {
value: () => [], value: () => [],
modelValue: undefined,
directory: undefined, directory: undefined,
disabled: false, disabled: false,
listType: 'picture-card', listType: 'picture-card',
@@ -34,7 +35,12 @@ const props = withDefaults(defineProps<FileUploadProps>(), {
resultField: '', resultField: '',
showDescription: true, 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 { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false); const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({ const { getStringAccept } = useUploadType({
@@ -43,6 +49,16 @@ const { getStringAccept } = useUploadType({
maxNumberRef: maxNumber, maxNumberRef: maxNumber,
maxSizeRef: maxSize, 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 previewOpen = ref<boolean>(false); // 是否展示预览
const previewImage = ref<string>(''); // 预览图片 const previewImage = ref<string>(''); // 预览图片
const previewTitle = ref<string>(''); // 预览标题 const previewTitle = ref<string>(''); // 预览标题
@@ -51,9 +67,11 @@ const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true); // 文件大小错误提示 const isLtMsg = ref<boolean>(true); // 文件大小错误提示
const isActMsg = ref<boolean>(true); // 文件类型错误提示 const isActMsg = ref<boolean>(true); // 文件类型错误提示
const isFirstRender = ref<boolean>(true); // 是否第一次渲染 const isFirstRender = ref<boolean>(true); // 是否第一次渲染
const uploadNumber = ref<number>(0); // 上传文件计数器
const uploadList = ref<any[]>([]); // 临时上传列表
watch( watch(
() => props.value, currentValue,
async (v) => { async (v) => {
if (isInnerOperate.value) { if (isInnerOperate.value) {
isInnerOperate.value = false; isInnerOperate.value = false;
@@ -122,6 +140,7 @@ async function handleRemove(file: UploadFile) {
const value = getValue(); const value = getValue();
isInnerOperate.value = true; isInnerOperate.value = true;
emit('update:value', value); emit('update:value', value);
emit('update:modelValue', value);
emit('change', value); emit('change', value);
emit('delete', file); emit('delete', file);
} }
@@ -133,6 +152,12 @@ function handleCancel() {
} }
async function beforeUpload(file: File) { 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 { maxSize, accept } = props;
const isAct = checkImgType(file, accept); const isAct = checkImgType(file, accept);
if (!isAct) { if (!isAct) {
@@ -140,6 +165,7 @@ async function beforeUpload(file: File) {
isActMsg.value = false; isActMsg.value = false;
// 防止弹出多个错误提示 // 防止弹出多个错误提示
setTimeout(() => (isActMsg.value = true), 1000); setTimeout(() => (isActMsg.value = true), 1000);
return Upload.LIST_IGNORE;
} }
const isLt = file.size / 1024 / 1024 > maxSize; const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) { if (isLt) {
@@ -147,8 +173,12 @@ async function beforeUpload(file: File) {
isLtMsg.value = false; isLtMsg.value = false;
// 防止弹出多个错误提示 // 防止弹出多个错误提示
setTimeout(() => (isLtMsg.value = true), 1000); 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>) { async function customRequest(info: UploadRequestOption<any>) {
@@ -163,20 +193,59 @@ async function customRequest(info: UploadRequestOption<any>) {
info.onProgress!({ percent }); info.onProgress!({ percent });
}; };
const res = await api?.(info.file as File, progressEvent); const res = await api?.(info.file as File, progressEvent);
// 处理上传成功后的逻辑
handleUploadSuccess(res, info.file as File);
info.onSuccess!(res); info.onSuccess!(res);
message.success($t('ui.upload.uploadSuccess')); message.success($t('ui.upload.uploadSuccess'));
// 更新文件
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
info.onError!(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() { function getValue() {
const list = (fileList.value || []) const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE) .filter((item) => item?.status === UploadResultStatus.DONE)
@@ -186,11 +255,26 @@ function getValue() {
} }
return item?.url || item?.response?.url || item?.response; return item?.url || item?.response?.url || item?.response;
}); });
// add by 芋艿:【特殊】单个文件的情况,获取首个元素,保证返回的是 String 类型
// 单个文件的情况,根据输入参数类型决定返回格式
if (props.maxNumber === 1) { 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 list; return singleValue;
}
// 多文件情况,根据输入参数类型决定返回格式
if (isUsingModelValue.value) {
return Array.isArray(props.modelValue) ? list : list.join(',');
}
return Array.isArray(props.value) ? list : list.join(',');
} }
</script> </script>

View File

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

View File

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

View File

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

View File

@@ -16,73 +16,72 @@ const routes: RouteRecordRaw[] = [
name: 'CrmClueDetail', name: 'CrmClueDetail',
meta: { meta: {
title: '线索详情', 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', path: 'customer/detail/:id',
name: 'CrmCustomerDetail', name: 'CrmCustomerDetail',
meta: { meta: {
title: '客户详情', 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', path: 'business/detail/:id',
name: 'CrmBusinessDetail', name: 'CrmBusinessDetail',
meta: { meta: {
title: '商机详情', 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', path: 'contract/detail/:id',
name: 'CrmContractDetail', name: 'CrmContractDetail',
meta: { meta: {
title: '合同详情', 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', path: 'receivable-plan/detail/:id',
name: 'CrmReceivablePlanDetail', name: 'CrmReceivablePlanDetail',
meta: { meta: {
title: '回款计划详情', title: '回款计划详情',
activeMenu: '/crm/receivable-plan', activePath: '/crm/receivable-plan',
}, },
component: () => component: () => import('#/views/crm/receivable/plan/detail/index.vue'),
import('#/views/crm/receivable/plan/modules/detail.vue'),
}, },
{ {
path: 'receivable/detail/:id', path: 'receivable/detail/:id',
name: 'CrmReceivableDetail', name: 'CrmReceivableDetail',
meta: { meta: {
title: '回款详情', 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', path: 'contact/detail/:id',
name: 'CrmContactDetail', name: 'CrmContactDetail',
meta: { meta: {
title: '联系人详情', 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', path: 'product/detail/:id',
name: 'CrmProductDetail', name: 'CrmProductDetail',
meta: { meta: {
title: '产品详情', 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[] = [ const routes: RouteRecordRaw[] = [
{ {
path: '/infra/job/job-log', path: '/infra/job/log',
component: () => import('#/views/infra/job/logger/index.vue'), component: () => import('#/views/infra/job/logger/index.vue'),
name: 'InfraJobLog', name: 'InfraJobLog',
meta: { meta: {
@@ -14,25 +14,16 @@ const routes: RouteRecordRaw[] = [
}, },
}, },
{ {
path: '/codegen', path: '/infra/codegen/edit',
name: 'CodegenEdit', component: () => import('#/views/infra/codegen/edit/index.vue'),
name: 'InfraCodegenEdit',
meta: { meta: {
title: '代码生成', title: '生成配置修改',
icon: 'ic:baseline-view-in-ar', icon: 'ic:baseline-view-in-ar',
activePath: '/infra/codegen',
keepAlive: true, keepAlive: true,
hideInMenu: 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', name: 'ProductSpuAdd',
meta: { meta: {
title: '商品添加', title: '商品添加',
activeMenu: '/mall/product/spu', activePath: '/mall/product/spu',
}, },
component: () => import('#/views/mall/product/spu/modules/form.vue'), component: () => import('#/views/mall/product/spu/modules/form.vue'),
}, },
@@ -25,7 +25,7 @@ const routes: RouteRecordRaw[] = [
name: 'ProductSpuEdit', name: 'ProductSpuEdit',
meta: { meta: {
title: '商品编辑', title: '商品编辑',
activeMenu: '/mall/product/spu', activePath: '/mall/product/spu',
}, },
component: () => import('#/views/mall/product/spu/modules/form.vue'), component: () => import('#/views/mall/product/spu/modules/form.vue'),
}, },
@@ -34,7 +34,7 @@ const routes: RouteRecordRaw[] = [
name: 'ProductSpuDetail', name: 'ProductSpuDetail',
meta: { meta: {
title: '商品详情', title: '商品详情',
activeMenu: '/crm/business', activePath: '/crm/business',
}, },
component: () => import('#/views/mall/product/spu/modules/detail.vue'), component: () => import('#/views/mall/product/spu/modules/detail.vue'),
}, },
@@ -55,7 +55,7 @@ const routes: RouteRecordRaw[] = [
name: 'TradeOrderDetail', name: 'TradeOrderDetail',
meta: { meta: {
title: '订单详情', title: '订单详情',
activeMenu: '/mall/trade/order', activePath: '/mall/trade/order',
}, },
component: () => import('#/views/mall/trade/order/modules/detail.vue'), component: () => import('#/views/mall/trade/order/modules/detail.vue'),
}, },
@@ -64,7 +64,7 @@ const routes: RouteRecordRaw[] = [
name: 'TradeAfterSaleDetail', name: 'TradeAfterSaleDetail',
meta: { meta: {
title: '退款详情', title: '退款详情',
activeMenu: '/mall/trade/after-sale', activePath: '/mall/trade/after-sale',
}, },
component: () => component: () =>
import('#/views/mall/trade/afterSale/modules/detail.vue'), import('#/views/mall/trade/afterSale/modules/detail.vue'),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -49,7 +49,7 @@ const [Grid] = useVbenVxeGrid({
allowClear: true, allowClear: true,
options: RECEIVABLE_REMIND_TYPE, options: RECEIVABLE_REMIND_TYPE,
}, },
defaultValue: 1, defaultValue: RECEIVABLE_REMIND_TYPE[0]!.value,
}, },
], ],
}, },
@@ -70,6 +70,7 @@ const [Grid] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, 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> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmBusinessApi } from '#/api/crm/business'; 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 { getBusinessPageByCustomer } from '#/api/crm/business';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useDetailListColumns } from './detail-data'; import Form from '../modules/form.vue';
import Form from './form.vue'; import { useBusinessDetailListColumns } from './data';
const props = defineProps<{ const props = defineProps<{
customerId?: number; // customerId customerId?: number; // customerId
@@ -35,7 +36,7 @@ function setCheckedRows({ records }: { records: CrmBusinessApi.Business[] }) {
} }
/** 刷新表格 */ /** 刷新表格 */
function onRefresh() { function handleRefresh() {
gridApi.query(); gridApi.query();
} }
@@ -54,6 +55,7 @@ function handleCustomerDetail(row: CrmBusinessApi.Business) {
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } }); push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
} }
/** 商机关联弹窗 */
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
async onConfirm() { async onConfirm() {
if (checkedRows.value.length === 0) { if (checkedRows.value.length === 0) {
@@ -71,25 +73,9 @@ const [Modal, modalApi] = useVbenModal({
modalApi.unlock(); 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({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: [ schema: [
@@ -101,7 +87,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
], ],
}, },
gridOptions: { gridOptions: {
columns: useDetailListColumns(), columns: useBusinessDetailListColumns(),
height: 600, height: 600,
keepSource: true, keepSource: true,
proxyConfig: { proxyConfig: {
@@ -133,7 +119,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
<template> <template>
<Modal title="关联商机" class="w-2/5"> <Modal title="关联商机" class="w-2/5">
<FormModal @success="onRefresh" /> <FormModal @success="handleRefresh" />
<Grid> <Grid>
<template #toolbar-tools> <template #toolbar-tools>
<TableAction <TableAction

View File

@@ -1,3 +1,4 @@
<!-- 商机列表用于客户联系人详情中展示其关联的商机列表 -->
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmBusinessApi } from '#/api/crm/business'; import type { CrmBusinessApi } from '#/api/crm/business';
@@ -22,9 +23,9 @@ import {
import { BizTypeEnum } from '#/api/crm/permission'; import { BizTypeEnum } from '#/api/crm/permission';
import { $t } from '#/locales'; 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 ListModal from './detail-list-modal.vue';
import Form from './form.vue';
const props = defineProps<{ const props = defineProps<{
bizId: number; // bizId: number; //
@@ -51,7 +52,7 @@ function setCheckedRows({ records }: { records: CrmBusinessApi.Business[] }) {
} }
/** 刷新表格 */ /** 刷新表格 */
function onRefresh() { function handleRefresh() {
gridApi.query(); gridApi.query();
} }
@@ -62,10 +63,12 @@ function handleCreate() {
.open(); .open();
} }
/** 关联商机 */
function handleCreateBusiness() { function handleCreateBusiness() {
detailListModalApi.setData({ customerId: props.customerId }).open(); detailListModalApi.setData({ customerId: props.customerId }).open();
} }
/** 解除商机关联 */
async function handleDeleteContactBusinessList() { async function handleDeleteContactBusinessList() {
if (checkedRows.value.length === 0) { if (checkedRows.value.length === 0) {
message.error('请先选择商机后操作!'); message.error('请先选择商机后操作!');
@@ -83,7 +86,7 @@ async function handleDeleteContactBusinessList() {
if (res) { if (res) {
// //
message.success($t('ui.actionMessage.operationSuccess')); message.success($t('ui.actionMessage.operationSuccess'));
onRefresh(); handleRefresh();
resolve(true); resolve(true);
} else { } else {
reject(new Error($t('ui.actionMessage.operationFailed'))); reject(new Error($t('ui.actionMessage.operationFailed')));
@@ -105,18 +108,20 @@ function handleCustomerDetail(row: CrmBusinessApi.Business) {
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } }); push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
} }
/** 创建联系人关联的商机 */
async function handleCreateContactBusinessList(businessIds: number[]) { async function handleCreateContactBusinessList(businessIds: number[]) {
const data = { const data = {
contactId: props.bizId, contactId: props.bizId,
businessIds, businessIds,
} as CrmContactApi.ContactBusinessReq; } as CrmContactApi.ContactBusinessReq;
await createContactBusinessList(data); await createContactBusinessList(data);
onRefresh(); handleRefresh();
} }
/** 商机关联表格 */
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: { gridOptions: {
columns: useDetailListColumns(), columns: useBusinessDetailListColumns(),
height: 600, height: 600,
keepSource: true, keepSource: true,
proxyConfig: { proxyConfig: {
@@ -144,6 +149,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,
@@ -159,7 +165,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
<template> <template>
<div> <div>
<FormModal @success="onRefresh" /> <FormModal @success="handleRefresh" />
<DetailListModal <DetailListModal
:customer-id="customerId" :customer-id="customerId"
@success="handleCreateContactBusinessList" @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: '商机名称', label: '商机名称',
component: 'Input', component: 'Input',
rules: 'required', rules: 'required',
componentProps: {
placeholder: '请输入商机名称',
allowClear: true,
},
}, },
{ {
fieldName: 'ownerUserId', fieldName: 'ownerUserId',
label: '负责人', label: '负责人',
component: 'ApiSelect', component: 'ApiSelect',
dependencies: {
triggerFields: ['id'],
disabled: (values) => values.id,
},
componentProps: { componentProps: {
api: () => getSimpleUserList(), api: () => getSimpleUserList(),
fieldNames: { fieldNames: {
label: 'nickname', label: 'nickname',
value: 'id', value: 'id',
}, },
placeholder: '请选择负责人',
allowClear: true,
}, },
defaultValue: userStore.userInfo?.id, defaultValue: userStore.userInfo?.id,
rules: 'required', rules: 'required',
@@ -51,6 +61,8 @@ export function useFormSchema(): VbenFormSchema[] {
label: 'name', label: 'name',
value: 'id', value: 'id',
}, },
placeholder: '请选择客户',
allowClear: true,
}, },
dependencies: { dependencies: {
triggerFields: ['id'], triggerFields: ['id'],
@@ -77,6 +89,8 @@ export function useFormSchema(): VbenFormSchema[] {
label: 'name', label: 'name',
value: 'id', value: 'id',
}, },
placeholder: '请选择商机状态组',
allowClear: true,
}, },
dependencies: { dependencies: {
triggerFields: ['id'], triggerFields: ['id'],
@@ -88,11 +102,11 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'dealTime', fieldName: 'dealTime',
label: '预计成交日期', label: '预计成交日期',
component: 'DatePicker', component: 'DatePicker',
rules: 'required',
componentProps: { componentProps: {
showTime: false, showTime: false,
format: 'YYYY-MM-DD HH:mm:ss', format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x', valueFormat: 'x',
placeholder: '请选择预计成交日期',
}, },
}, },
{ {
@@ -100,6 +114,10 @@ export function useFormSchema(): VbenFormSchema[] {
label: '产品清单', label: '产品清单',
component: 'Input', component: 'Input',
formItemClass: 'col-span-3', formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入产品清单',
allowClear: true,
},
}, },
{ {
fieldName: 'totalProductPrice', fieldName: 'totalProductPrice',
@@ -108,6 +126,8 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
min: 0, min: 0,
precision: 2, precision: 2,
disabled: true,
placeholder: '请输入产品总金额',
}, },
rules: z.number().min(0).optional().default(0), rules: z.number().min(0).optional().default(0),
}, },
@@ -118,6 +138,7 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
min: 0, min: 0,
precision: 2, precision: 2,
placeholder: '请输入整单折扣',
}, },
rules: z.number().min(0).max(100).optional().default(0), rules: z.number().min(0).max(100).optional().default(0),
}, },
@@ -129,6 +150,7 @@ export function useFormSchema(): VbenFormSchema[] {
min: 0, min: 0,
precision: 2, precision: 2,
disabled: true, disabled: true,
placeholder: '请输入折扣后金额',
}, },
dependencies: { dependencies: {
triggerFields: ['totalProductPrice', 'discountPercent'], triggerFields: ['totalProductPrice', 'discountPercent'],
@@ -170,83 +192,83 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
field: 'name', field: 'name',
title: '商机名称', title: '商机名称',
fixed: 'left', fixed: 'left',
minWidth: 240, width: 160,
slots: { default: 'name' }, slots: { default: 'name' },
}, },
{ {
field: 'customerName', field: 'customerName',
title: '客户名称', title: '客户名称',
fixed: 'left', fixed: 'left',
minWidth: 240, width: 120,
slots: { default: 'customerName' }, slots: { default: 'customerName' },
}, },
{ {
field: 'totalPrice', field: 'totalPrice',
title: '商机金额(元)', title: '商机金额(元)',
minWidth: 140, width: 140,
formatter: 'formatAmount2', formatter: 'formatAmount2',
}, },
{ {
field: 'dealTime', field: 'dealTime',
title: '预计成交日期', title: '预计成交日期',
formatter: 'formatDate', formatter: 'formatDate',
minWidth: 180, width: 180,
}, },
{ {
field: 'remark', field: 'remark',
title: '备注', title: '备注',
minWidth: 200, width: 200,
}, },
{ {
field: 'contactNextTime', field: 'contactNextTime',
title: '下次联系时间', title: '下次联系时间',
formatter: 'formatDate', formatter: 'formatDateTime',
minWidth: 180, width: 180,
}, },
{ {
field: 'ownerUserName', field: 'ownerUserName',
title: '负责人', title: '负责人',
minWidth: 120, width: 100,
}, },
{ {
field: 'ownerUserDeptName', field: 'ownerUserDeptName',
title: '所属部门', title: '所属部门',
minWidth: 120, width: 100,
}, },
{ {
field: 'contactLastTime', field: 'contactLastTime',
title: '最后跟进时间', title: '最后跟进时间',
formatter: 'formatDateTime', formatter: 'formatDateTime',
minWidth: 180, width: 180,
},
{
field: 'createTime',
title: '创建时间',
formatter: 'formatDateTime',
minWidth: 180,
},
{
field: 'creatorName',
title: '创建人',
minWidth: 120,
}, },
{ {
field: 'updateTime', field: 'updateTime',
title: '更新时间', title: '更新时间',
formatter: 'formatDateTime', formatter: 'formatDateTime',
minWidth: 180, width: 180,
},
{
field: 'createTime',
title: '创建时间',
formatter: 'formatDateTime',
width: 180,
},
{
field: 'creatorName',
title: '创建人',
width: 100,
}, },
{ {
field: 'statusTypeName', field: 'statusTypeName',
title: '商机状态组', title: '商机状态组',
fixed: 'right', fixed: 'right',
minWidth: 120, width: 140,
}, },
{ {
field: 'statusName', field: 'statusName',
title: '商机阶段', title: '商机阶段',
fixed: 'right', fixed: 'right',
minWidth: 120, width: 120,
}, },
{ {
title: '操作', 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 type { DescriptionItemSchema } from '#/components/description';
import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
import { DEFAULT_STATUSES, getBusinessStatusSimpleList } from '#/api/crm/business/status';
/** 详情页的字段 */ /** 详情页的字段 */
export function useDetailSchema(): DescriptionItemSchema[] { export function useDetailSchema(): DescriptionItemSchema[] {
return [ return [
@@ -72,53 +76,62 @@ export function useDetailBaseSchema(): DescriptionItemSchema[] {
]; ];
} }
/** 详情列表的字段 */ /** 商机状态更新表单 */
export function useDetailListColumns(): VxeTableGridOptions['columns'] { export function useStatusFormSchema(
formData: Ref<CrmBusinessApi.Business | undefined>,
): VbenFormSchema[] {
return [ return [
{ {
type: 'checkbox', fieldName: 'id',
width: 50, component: 'Input',
fixed: 'left', dependencies: {
triggerFields: [''],
show: () => false,
},
}, },
{ {
field: 'name', fieldName: 'statusId',
title: '商机名称', label: '商机状态',
fixed: 'left', component: 'Input',
slots: { default: 'name' }, dependencies: {
triggerFields: [''],
show: () => false,
},
}, },
{ {
field: 'customerName', fieldName: 'endStatus',
title: '客户名称', label: '商机状态',
fixed: 'left', component: 'Input',
slots: { default: 'customerName' }, dependencies: {
triggerFields: [''],
show: () => false,
},
}, },
{ {
field: 'totalPrice', fieldName: 'status',
title: '商机金额(元)', label: '商机阶段',
formatter: 'formatAmount2', 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,
};
}, },
{
field: 'dealTime',
title: '预计成交日期',
formatter: 'formatDate',
}, },
{ rules: 'required',
field: 'ownerUserName',
title: '负责人',
},
{
field: 'ownerUserDeptName',
title: '所属部门',
},
{
field: 'statusTypeName',
title: '商机状态组',
fixed: 'right',
},
{
field: 'statusName',
title: '商机阶段',
fixed: 'right',
}, },
]; ];
} }

View File

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

View File

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

View File

@@ -9,12 +9,10 @@ import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form'; import { useVbenForm } from '#/adapter/form';
import { updateBusinessStatus } from '#/api/crm/business'; import { updateBusinessStatus } from '#/api/crm/business';
import {
DEFAULT_STATUSES,
getBusinessStatusSimpleList,
} from '#/api/crm/business/status';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useStatusFormSchema } from '../data';
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
const formData = ref<CrmBusinessApi.Business>(); const formData = ref<CrmBusinessApi.Business>();
@@ -28,60 +26,7 @@ const [Form, formApi] = useVbenForm({
labelWidth: 120, labelWidth: 120,
}, },
layout: 'horizontal', layout: 'horizontal',
schema: [ schema: useStatusFormSchema(formData),
{
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',
},
],
showDefaultActions: false, 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 { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmBusinessApi } from '#/api/crm/business'; import type { CrmBusinessApi } from '#/api/crm/business';
import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui'; import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils'; 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 { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { import {
@@ -21,6 +22,7 @@ import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue'; import Form from './modules/form.vue';
const { push } = useRouter(); const { push } = useRouter();
const sceneType = ref('1');
const [FormModal, formModalApi] = useVbenModal({ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form, 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(); gridApi.query();
} }
/** 导出表格 */ /** 导出表格 */
async function handleExport() { 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 }); downloadFileFromBlobPart({ fileName: '商机.xls', source: data });
} }
@@ -53,13 +65,12 @@ async function handleDelete(row: CrmBusinessApi.Business) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]), content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0, duration: 0,
key: 'action_process_msg',
}); });
try { try {
await deleteBusiness(row.id as number); await deleteBusiness(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name])); message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
onRefresh(); handleRefresh();
} catch { } finally {
hideLoading(); hideLoading();
} }
} }
@@ -88,6 +99,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
return await getBusinessPage({ return await getBusinessPage({
pageNo: page.currentPage, pageNo: page.currentPage,
pageSize: page.pageSize, pageSize: page.pageSize,
sceneType: sceneType.value,
...formValues, ...formValues,
}); });
}, },
@@ -95,6 +107,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,
@@ -117,8 +130,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
/> />
</template> </template>
<FormModal @success="onRefresh" /> <FormModal @success="handleRefresh" />
<Grid table-title="商机列表"> <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> <template #toolbar-tools>
<TableAction <TableAction
:actions="[ :actions="[
@@ -176,3 +196,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
</Grid> </Grid>
</Page> </Page>
</template> </template>
<style scoped>
:deep(.vxe-toolbar div) {
z-index: 1;
}
</style>

View File

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

View File

@@ -21,6 +21,9 @@ export function useFormSchema(): VbenFormSchema[] {
label: '状态组名', label: '状态组名',
component: 'Input', component: 'Input',
rules: 'required', rules: 'required',
componentProps: {
placeholder: '请输入状态组名',
},
}, },
{ {
fieldName: 'deptIds', 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(); gridApi.query();
} }
@@ -31,27 +31,26 @@ function handleCreate() {
formModalApi.setData(null).open(); formModalApi.setData(null).open();
} }
/** 编辑商机状态 */
function handleEdit(row: CrmBusinessStatusApi.BusinessStatus) {
formModalApi.setData(row).open();
}
/** 删除商机状态 */ /** 删除商机状态 */
async function handleDelete(row: CrmBusinessStatusApi.BusinessStatus) { async function handleDelete(row: CrmBusinessStatusApi.BusinessStatus) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]), content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0, duration: 0,
key: 'action_process_msg',
}); });
try { try {
await deleteBusinessStatus(row.id as number); await deleteBusinessStatus(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name])); message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
onRefresh(); handleRefresh();
} catch { } catch {
hideLoading(); hideLoading();
} }
} }
/** 编辑商机状态 */
function handleEdit(row: CrmBusinessStatusApi.BusinessStatus) {
formModalApi.setData(row).open();
}
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: { gridOptions: {
columns: useGridColumns(), columns: useGridColumns(),
@@ -70,6 +69,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,
@@ -92,7 +92,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
/> />
</template> </template>
<FormModal @success="onRefresh" /> <FormModal @success="handleRefresh" />
<Grid table-title="商机状态列表"> <Grid table-title="商机状态列表">
<template #toolbar-tools> <template #toolbar-tools>
<TableAction <TableAction

View File

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

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