Merge remote-tracking branch 'refs/remotes/yudao/dev' into develop

pull/87/head
puhui999 2025-05-03 13:49:52 +08:00
commit abb9cfc05f
283 changed files with 10304 additions and 3435 deletions

View File

@ -2,5 +2,5 @@ ports:
- port: 5555
onOpen: open-preview
tasks:
- init: corepack enable && pnpm install
- init: npm i -g corepack && pnpm install
command: pnpm run dev:play

View File

@ -15,6 +15,6 @@ export default {
],
'package.json': ['prettier --cache --write'],
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
'prettier --cache --write--parser json',
'prettier --cache --write --parser json',
],
};

View File

@ -1 +1 @@
20.14.0
22.1.0

18
.vscode/settings.json vendored
View File

@ -14,7 +14,7 @@
"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.cursorBlinking": "expand",
"editor.largeFileOptimizations": false,
"editor.largeFileOptimizations": true,
"editor.accessibilitySupport": "off",
"editor.cursorSmoothCaretAnimation": "on",
"editor.guides.bracketPairs": "active",
@ -91,6 +91,7 @@
"**/bower_components": true,
"**/.turbo": true,
"**/.idea": true,
"**/.vitepress": true,
"**/tmp": true,
"**/.git": true,
"**/.svn": true,
@ -112,6 +113,8 @@
"**/yarn.lock": true
},
"typescript.tsserver.exclude": ["**/node_modules", "**/dist", "**/.turbo"],
// search
"search.searchEditor.singleClickBehaviour": "peekDefinition",
"search.followSymlinks": false,
@ -223,16 +226,5 @@
"commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true,
"typescript.tsdk": "node_modules/typescript/lib",
"oxc.enable": false,
"cSpell.words": [
"archiver",
"axios",
"dotenv",
"isequal",
"jspm",
"napi",
"nolebase",
"rollup",
"vitest"
]
"oxc.enable": false
}

View File

@ -3,6 +3,10 @@ VITE_APP_TITLE=芋道管理系统
# 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=yudao-vben-antd
# 对store进行加密的密钥在将store持久化到localStorage时会使用该密钥进行加密
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
# 是否开启模拟数据
VITE_NITRO_MOCK=false
@ -16,4 +20,7 @@ VITE_APP_CAPTCHA_ENABLE=false
VITE_APP_DOCALERT_ENABLE=true
# 百度统计
VITE_APP_BAIDU_CODE = e98f2eab6ceb8688bc6d8fc5332ff093
VITE_APP_BAIDU_CODE = e98f2eab6ceb8688bc6d8fc5332ff093
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'

View File

@ -1,6 +1,6 @@
{
"name": "@vben/web-antd",
"version": "5.5.4",
"version": "5.5.5",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@ -44,8 +44,8 @@
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"@vueuse/integrations": "catalog:",
"ant-design-vue": "catalog:",
"vxe-table": "catalog:",
"cropperjs": "catalog:",
"crypto-js": "catalog:",
"dayjs": "catalog:",
@ -53,7 +53,8 @@
"pinia": "catalog:",
"vue": "catalog:",
"vue-dompurify-html": "catalog:",
"vue-router": "catalog:"
"vue-router": "catalog:",
"vxe-table": "catalog:"
},
"devDependencies": {
"@types/crypto-js": "catalog:"

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -76,8 +76,8 @@ const withDefaultPlaceholder = <T extends Component>(
componentProps: Recordable<any> = {},
) => {
return defineComponent({
inheritAttrs: false,
name: component.name,
inheritAttrs: false,
setup: (props: any, { attrs, expose, slots }) => {
const placeholder =
props?.placeholder ||
@ -142,20 +142,34 @@ async function initComponentAdapter() {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
component: Select,
loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value',
}),
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
}),
ApiSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiSelect',
},
'select',
{
component: Select,
loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value',
},
),
ApiTreeSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiTreeSelect',
},
'select',
{
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
),
AutoComplete,
Checkbox,
CheckboxGroup,

View File

@ -30,7 +30,7 @@ setupVbenVxeTable({
},
toolbarConfig: {
import: false, // 是否导入
export: false, // 否导出
export: false, // 否导出
refresh: true, // 是否刷新
print: false, // 是否打印
zoom: true, // 是否缩放
@ -259,6 +259,22 @@ setupVbenVxeTable({
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add
// add by 星语:数量格式化,例如说:金额
vxeUI.formats.add('formatAmount', {
cellFormatMethod({ cellValue }, digits = 2) {
if (cellValue === null || cellValue === undefined) {
return '';
}
if (isString(cellValue)) {
cellValue = Number.parseFloat(cellValue);
}
// 如果非 number则直接返回空串
if (Number.isNaN(cellValue)) {
return '';
}
return cellValue.toFixed(digits);
},
});
},
useVbenForm,
});

View File

@ -1,5 +1,7 @@
import type { PageParam, PageResult } from '@vben/request';
import type { BpmModelApi } from '#/api/bpm/model';
import { requestClient } from '#/api/request';
export namespace BpmCategoryApi {
@ -11,6 +13,14 @@ export namespace BpmCategoryApi {
status: number;
sort: number; // 分类排序
}
/** 模型分类信息 */
// TODO @jason这个应该非 api 的,可以考虑抽到页面里哈。
export interface ModelCategoryInfo {
id: number;
name: string;
modelList: BpmModelApi.ModelVO[];
}
}
/** 查询流程分类分页 */
@ -30,15 +40,30 @@ export async function getCategory(id: number) {
/** 新增流程分类 */
export async function createCategory(data: BpmCategoryApi.CategoryVO) {
return requestClient.post('/bpm/category/create', data);
return requestClient.post<number>('/bpm/category/create', data);
}
/** 修改流程分类 */
export async function updateCategory(data: BpmCategoryApi.CategoryVO) {
return requestClient.put('/bpm/category/update', data);
return requestClient.put<boolean>('/bpm/category/update', data);
}
/** 删除流程分类 */
export async function deleteCategory(id: number) {
return requestClient.delete(`/bpm/category/delete?id=${id}`);
return requestClient.delete<boolean>(`/bpm/category/delete?id=${id}`);
}
/** 查询流程分类列表 */
export async function getCategorySimpleList() {
return requestClient.get<BpmCategoryApi.CategoryVO[]>(
`/bpm/category/simple-list`,
);
}
/** 批量修改流程分类的排序 */
export async function updateCategorySortBatch(ids: number[]) {
const params = ids.join(',');
return requestClient.put<boolean>(
`/bpm/category/update-sort-batch?ids=${params}`,
);
}

View File

@ -0,0 +1,109 @@
import { requestClient } from '#/api/request';
export namespace BpmModelApi {
/** 用户信息 TODO 这个是不是可以抽取出来定义在公共模块 */
// TODO @芋艿:一起看看。
export interface UserInfo {
id: number;
nickname: string;
avatar?: string;
deptId?: number;
deptName?: string;
}
/** 流程定义 VO */
export interface ProcessDefinitionVO {
id: string;
version: number;
deploymentTime: number;
suspensionState: number;
formType?: number;
}
/** 流程模型 VO */
export interface ModelVO {
id: number;
key: string;
name: string;
icon?: string;
description: string;
category: string;
formName: string;
formType: number;
formId: number;
formCustomCreatePath: string;
formCustomViewPath: string;
processDefinition: ProcessDefinitionVO;
status: number;
remark: string;
createTime: string;
bpmnXml: string;
startUsers?: UserInfo[];
}
/** 模型分类信息 */
export interface ModelCategoryInfo {
id: number;
name: string;
modelList: ModelVO[];
}
}
/** 获取流程模型列表 */
export async function getModelList(name: string | undefined) {
return requestClient.get<BpmModelApi.ModelVO[]>('/bpm/model/list', {
params: { name },
});
}
/** 获取流程模型详情 */
export async function getModel(id: string) {
return requestClient.get<BpmModelApi.ModelVO>(`/bpm/model/get?id=${id}`);
}
/** 更新流程模型 */
export async function updateModel(data: BpmModelApi.ModelVO) {
return requestClient.put('/bpm/model/update', data);
}
/** 批量修改流程模型排序 */
export async function updateModelSortBatch(ids: number[]) {
const params = ids.join(',');
return requestClient.put<boolean>(
`/bpm/model/update-sort-batch?ids=${params}`,
);
}
/** 更新流程模型的 BPMN XML */
export async function updateModelBpmn(data: BpmModelApi.ModelVO) {
return requestClient.put('/bpm/model/update-bpmn', data);
}
/** 更新流程模型状态 */
export async function updateModelState(id: number, state: number) {
const data = {
id,
state,
};
return requestClient.put('/bpm/model/update-state', data);
}
/** 创建流程模型 */
export async function createModel(data: BpmModelApi.ModelVO) {
return requestClient.post('/bpm/model/create', data);
}
/** 删除流程模型 */
export async function deleteModel(id: number) {
return requestClient.delete(`/bpm/model/delete?id=${id}`);
}
/** 部署流程模型 */
export async function deployModel(id: number) {
return requestClient.post(`/bpm/model/deploy?id=${id}`);
}
/** 清理流程模型 */
export async function cleanModel(id: number) {
return requestClient.delete(`/bpm/model/clean?id=${id}`);
}

View File

@ -0,0 +1,118 @@
import type { PageParam, PageResult } from '@vben/request';
import type { CrmPermissionApi } from '#/api/crm/permission';
import { requestClient } from '#/api/request';
export namespace CrmBusinessApi {
/** 商机产品信息 */
export interface BusinessProduct {
id: number;
productId: number;
productName: string;
productNo: string;
productUnit: number;
productPrice: number;
businessPrice: number;
count: number;
totalPrice: number;
}
/** 商机信息 */
export interface Business {
id: number;
name: string;
customerId: number;
customerName?: string;
followUpStatus: boolean;
contactLastTime: Date;
contactNextTime: Date;
ownerUserId: number;
ownerUserName?: string; // 负责人的用户名称
ownerUserDept?: string; // 负责人的部门名称
statusTypeId: number;
statusTypeName?: string;
statusId: number;
statusName?: string;
endStatus: number;
endRemark: string;
dealTime: Date;
totalProductPrice: number;
totalPrice: number;
discountPercent: number;
remark: string;
creator: string; // 创建人
creatorName?: string; // 创建人名称
createTime: Date; // 创建时间
updateTime: Date; // 更新时间
products?: BusinessProduct[];
}
}
/** 查询商机列表 */
export function getBusinessPage(params: PageParam) {
return requestClient.get<PageResult<CrmBusinessApi.Business>>(
'/crm/business/page',
{ params },
);
}
/** 查询商机列表,基于指定客户 */
export function getBusinessPageByCustomer(params: PageParam) {
return requestClient.get<PageResult<CrmBusinessApi.Business>>(
'/crm/business/page-by-customer',
{ params },
);
}
/** 查询商机详情 */
export function getBusiness(id: number) {
return requestClient.get<CrmBusinessApi.Business>(
`/crm/business/get?id=${id}`,
);
}
/** 获得商机列表(精简) */
export function getSimpleBusinessList() {
return requestClient.get<CrmBusinessApi.Business[]>(
'/crm/business/simple-all-list',
);
}
/** 新增商机 */
export function createBusiness(data: CrmBusinessApi.Business) {
return requestClient.post('/crm/business/create', data);
}
/** 修改商机 */
export function updateBusiness(data: CrmBusinessApi.Business) {
return requestClient.put('/crm/business/update', data);
}
/** 修改商机状态 */
export function updateBusinessStatus(data: CrmBusinessApi.Business) {
return requestClient.put('/crm/business/update-status', data);
}
/** 删除商机 */
export function deleteBusiness(id: number) {
return requestClient.delete(`/crm/business/delete?id=${id}`);
}
/** 导出商机 */
export function exportBusiness(params: any) {
return requestClient.download('/crm/business/export-excel', params);
}
/** 联系人关联商机列表 */
export function getBusinessPageByContact(params: PageParam) {
return requestClient.get<PageResult<CrmBusinessApi.Business>>(
'/crm/business/page-by-contact',
{ params },
);
}
/** 商机转移 */
export function transferBusiness(data: CrmPermissionApi.TransferReq) {
return requestClient.put('/crm/business/transfer', data);
}

View File

@ -0,0 +1,91 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmBusinessStatusApi {
/** 商机状态信息 */
export interface BusinessStatus {
id: number;
name: string;
percent: number;
}
/** 商机状态组信息 */
export interface BusinessStatusType {
id: number;
name: string;
deptIds: number[];
statuses?: BusinessStatus[];
}
/** 默认商机状态 */
export const DEFAULT_STATUSES = [
{
endStatus: 1,
key: '结束',
name: '赢单',
percent: 100,
},
{
endStatus: 2,
key: '结束',
name: '输单',
percent: 0,
},
{
endStatus: 3,
key: '结束',
name: '无效',
percent: 0,
},
] as const;
}
/** 查询商机状态组列表 */
export function getBusinessStatusPage(params: PageParam) {
return requestClient.get<PageResult<CrmBusinessStatusApi.BusinessStatusType>>(
'/crm/business-status/page',
{ params },
);
}
/** 新增商机状态组 */
export function createBusinessStatus(
data: CrmBusinessStatusApi.BusinessStatusType,
) {
return requestClient.post('/crm/business-status/create', data);
}
/** 修改商机状态组 */
export function updateBusinessStatus(
data: CrmBusinessStatusApi.BusinessStatusType,
) {
return requestClient.put('/crm/business-status/update', data);
}
/** 查询商机状态类型详情 */
export function getBusinessStatus(id: number) {
return requestClient.get<CrmBusinessStatusApi.BusinessStatusType>(
`/crm/business-status/get?id=${id}`,
);
}
/** 删除商机状态 */
export function deleteBusinessStatus(id: number) {
return requestClient.delete(`/crm/business-status/delete?id=${id}`);
}
/** 获得商机状态组列表 */
export function getBusinessStatusTypeSimpleList() {
return requestClient.get<CrmBusinessStatusApi.BusinessStatusType[]>(
'/crm/business-status/type-simple-list',
);
}
/** 获得商机阶段列表 */
export function getBusinessStatusSimpleList(typeId: number) {
return requestClient.get<CrmBusinessStatusApi.BusinessStatus[]>(
'/crm/business-status/status-simple-list',
{ params: { typeId } },
);
}

View File

@ -0,0 +1,86 @@
import type { PageParam, PageResult } from '@vben/request';
import type { CrmPermissionApi } from '#/api/crm/permission';
import { requestClient } from '#/api/request';
export namespace CrmClueApi {
/** 线索信息 */
export interface Clue {
id: number; // 编号
name: string; // 线索名称
followUpStatus: boolean; // 跟进状态
contactLastTime: Date; // 最后跟进时间
contactLastContent: string; // 最后跟进内容
contactNextTime: Date; // 下次联系时间
ownerUserId: number; // 负责人的用户编号
ownerUserName?: string; // 负责人的用户名称
ownerUserDept?: string; // 负责人的部门名称
transformStatus: boolean; // 转化状态
customerId: number; // 客户编号
customerName?: string; // 客户名称
mobile: string; // 手机号
telephone: string; // 电话
qq: string; // QQ
wechat: string; // wechat
email: string; // email
areaId: number; // 所在地
areaName?: string; // 所在地名称
detailAddress: string; // 详细地址
industryId: number; // 所属行业
level: number; // 客户等级
source: number; // 客户来源
remark: string; // 备注
creator: string; // 创建人
creatorName?: string; // 创建人名称
createTime: Date; // 创建时间
updateTime: Date; // 更新时间
}
}
/** 查询线索列表 */
export function getCluePage(params: PageParam) {
return requestClient.get<PageResult<CrmClueApi.Clue>>('/crm/clue/page', {
params,
});
}
/** 查询线索详情 */
export function getClue(id: number) {
return requestClient.get<CrmClueApi.Clue>(`/crm/clue/get?id=${id}`);
}
/** 新增线索 */
export function createClue(data: CrmClueApi.Clue) {
return requestClient.post('/crm/clue/create', data);
}
/** 修改线索 */
export function updateClue(data: CrmClueApi.Clue) {
return requestClient.put('/crm/clue/update', data);
}
/** 删除线索 */
export function deleteClue(id: number) {
return requestClient.delete(`/crm/clue/delete?id=${id}`);
}
/** 导出线索 */
export function exportClue(params: any) {
return requestClient.download('/crm/clue/export-excel', params);
}
/** 线索转移 */
export function transferClue(data: CrmPermissionApi.TransferReq) {
return requestClient.put('/crm/clue/transfer', data);
}
/** 线索转化为客户 */
export function transformClue(id: number) {
return requestClient.put('/crm/clue/transform', { id });
}
/** 获得分配给我的、待跟进的线索数量 */
export function getFollowClueCount() {
return requestClient.get<number>('/crm/clue/follow-count');
}

View File

@ -0,0 +1,140 @@
import type { PageParam, PageResult } from '@vben/request';
import type { CrmPermissionApi } from '#/api/crm/permission';
import { requestClient } from '#/api/request';
export namespace CrmContactApi {
/** 联系人信息 */
export interface Contact {
id: number; // 编号
name: string; // 联系人名称
customerId: number; // 客户编号
customerName?: string; // 客户名称
contactLastTime: Date; // 最后跟进时间
contactLastContent: string; // 最后跟进内容
contactNextTime: Date; // 下次联系时间
ownerUserId: number; // 负责人的用户编号
ownerUserName?: string; // 负责人的用户名称
ownerUserDept?: string; // 负责人的部门名称
mobile: string; // 手机号
telephone: string; // 电话
qq: string; // QQ
wechat: string; // wechat
email: string; // email
areaId: number; // 所在地
areaName?: string; // 所在地名称
detailAddress: string; // 详细地址
sex: number; // 性别
master: boolean; // 是否主联系人
post: string; // 职务
parentId: number; // 上级联系人编号
parentName?: string; // 上级联系人名称
remark: string; // 备注
creator: string; // 创建人
creatorName?: string; // 创建人名称
createTime: Date; // 创建时间
updateTime: Date; // 更新时间
}
/** 联系人商机关联请求 */
export interface ContactBusinessReq {
contactId: number;
businessIds: number[];
}
/** 商机联系人关联请求 */
export interface BusinessContactReq {
businessId: number;
contactIds: number[];
}
}
/** 查询联系人列表 */
export function getContactPage(params: PageParam) {
return requestClient.get<PageResult<CrmContactApi.Contact>>(
'/crm/contact/page',
{ params },
);
}
/** 查询联系人列表,基于指定客户 */
export function getContactPageByCustomer(params: PageParam) {
return requestClient.get<PageResult<CrmContactApi.Contact>>(
'/crm/contact/page-by-customer',
{ params },
);
}
/** 查询联系人列表,基于指定商机 */
export function getContactPageByBusiness(params: PageParam) {
return requestClient.get<PageResult<CrmContactApi.Contact>>(
'/crm/contact/page-by-business',
{ params },
);
}
/** 查询联系人详情 */
export function getContact(id: number) {
return requestClient.get<CrmContactApi.Contact>(`/crm/contact/get?id=${id}`);
}
/** 新增联系人 */
export function createContact(data: CrmContactApi.Contact) {
return requestClient.post('/crm/contact/create', data);
}
/** 修改联系人 */
export function updateContact(data: CrmContactApi.Contact) {
return requestClient.put('/crm/contact/update', data);
}
/** 删除联系人 */
export function deleteContact(id: number) {
return requestClient.delete(`/crm/contact/delete?id=${id}`);
}
/** 导出联系人 */
export function exportContact(params: any) {
return requestClient.download('/crm/contact/export-excel', params);
}
/** 获得联系人列表(精简) */
export function getSimpleContactList() {
return requestClient.get<CrmContactApi.Contact[]>(
'/crm/contact/simple-all-list',
);
}
/** 批量新增联系人商机关联 */
export function createContactBusinessList(
data: CrmContactApi.ContactBusinessReq,
) {
return requestClient.post('/crm/contact/create-business-list', data);
}
/** 批量新增商机联系人关联 */
export function createBusinessContactList(
data: CrmContactApi.BusinessContactReq,
) {
return requestClient.post('/crm/contact/create-business-list2', data);
}
/** 解除联系人商机关联 */
export function deleteContactBusinessList(
data: CrmContactApi.ContactBusinessReq,
) {
return requestClient.delete('/crm/contact/delete-business-list', { data });
}
/** 解除商机联系人关联 */
export function deleteBusinessContactList(
data: CrmContactApi.BusinessContactReq,
) {
return requestClient.delete('/crm/contact/delete-business-list2', { data });
}
/** 联系人转移 */
export function transferContact(data: CrmPermissionApi.TransferReq) {
return requestClient.put('/crm/contact/transfer', data);
}

View File

@ -0,0 +1,21 @@
import { requestClient } from '#/api/request';
export namespace CrmContractConfigApi {
/** 合同配置信息 */
export interface Config {
notifyEnabled?: boolean;
notifyDays?: number;
}
}
/** 获取合同配置 */
export function getContractConfig() {
return requestClient.get<CrmContractConfigApi.Config>(
'/crm/contract-config/get',
);
}
/** 更新合同配置 */
export function saveContractConfig(data: CrmContractConfigApi.Config) {
return requestClient.put('/crm/contract-config/save', data);
}

View File

@ -0,0 +1,132 @@
import type { PageParam, PageResult } from '@vben/request';
import type { CrmPermissionApi } from '#/api/crm/permission';
import { requestClient } from '#/api/request';
export namespace CrmContractApi {
/** 合同产品信息 */
export interface ContractProduct {
id: number;
productId: number;
productName: string;
productNo: string;
productUnit: number;
productPrice: number;
contractPrice: number;
count: number;
totalPrice: number;
}
/** 合同信息 */
export interface Contract {
id: number;
name: string;
no: string;
customerId: number;
customerName?: string;
businessId: number;
businessName: string;
contactLastTime: Date;
ownerUserId: number;
ownerUserName?: string;
ownerUserDeptName?: string;
processInstanceId: number;
auditStatus: number;
orderDate: Date;
startTime: Date;
endTime: Date;
totalProductPrice: number;
discountPercent: number;
totalPrice: number;
totalReceivablePrice: number;
signContactId: number;
signContactName?: string;
signUserId: number;
signUserName: string;
remark: string;
createTime?: Date;
creator: string;
creatorName: string;
updateTime?: Date;
products?: ContractProduct[];
}
}
/** 查询合同列表 */
export function getContractPage(params: PageParam) {
return requestClient.get<PageResult<CrmContractApi.Contract>>(
'/crm/contract/page',
{ params },
);
}
/** 查询合同列表,基于指定客户 */
export function getContractPageByCustomer(params: PageParam) {
return requestClient.get<PageResult<CrmContractApi.Contract>>(
'/crm/contract/page-by-customer',
{ params },
);
}
/** 查询合同列表,基于指定商机 */
export function getContractPageByBusiness(params: PageParam) {
return requestClient.get<PageResult<CrmContractApi.Contract>>(
'/crm/contract/page-by-business',
{ params },
);
}
/** 查询合同详情 */
export function getContract(id: number) {
return requestClient.get<CrmContractApi.Contract>(
`/crm/contract/get?id=${id}`,
);
}
/** 查询合同下拉列表 */
export function getContractSimpleList(customerId: number) {
return requestClient.get<CrmContractApi.Contract[]>(
`/crm/contract/simple-list?customerId=${customerId}`,
);
}
/** 新增合同 */
export function createContract(data: CrmContractApi.Contract) {
return requestClient.post('/crm/contract/create', data);
}
/** 修改合同 */
export function updateContract(data: CrmContractApi.Contract) {
return requestClient.put('/crm/contract/update', data);
}
/** 删除合同 */
export function deleteContract(id: number) {
return requestClient.delete(`/crm/contract/delete?id=${id}`);
}
/** 导出合同 */
export function exportContract(params: any) {
return requestClient.download('/crm/contract/export-excel', params);
}
/** 提交审核 */
export function submitContract(id: number) {
return requestClient.put(`/crm/contract/submit?id=${id}`);
}
/** 合同转移 */
export function transferContract(data: CrmPermissionApi.TransferReq) {
return requestClient.put('/crm/contract/transfer', data);
}
/** 获得待审核合同数量 */
export function getAuditContractCount() {
return requestClient.get<number>('/crm/contract/audit-count');
}
/** 获得即将到期(提醒)的合同数量 */
export function getRemindContractCount() {
return requestClient.get<number>('/crm/contract/remind-count');
}

View File

@ -0,0 +1,146 @@
import type { PageParam, PageResult } from '@vben/request';
import type { CrmPermissionApi } from '#/api/crm/permission';
import { requestClient } from '#/api/request';
export namespace CrmCustomerApi {
/** 客户信息 */
export interface Customer {
id: number; // 编号
name: string; // 客户名称
followUpStatus: boolean; // 跟进状态
contactLastTime: Date; // 最后跟进时间
contactLastContent: string; // 最后跟进内容
contactNextTime: Date; // 下次联系时间
ownerUserId: number; // 负责人的用户编号
ownerUserName?: string; // 负责人的用户名称
ownerUserDept?: string; // 负责人的部门名称
lockStatus?: boolean;
dealStatus?: boolean;
mobile: string; // 手机号
telephone: string; // 电话
qq: string; // QQ
wechat: string; // wechat
email: string; // email
areaId: number; // 所在地
areaName?: string; // 所在地名称
detailAddress: string; // 详细地址
industryId: number; // 所属行业
level: number; // 客户等级
source: number; // 客户来源
remark: string; // 备注
creator: string; // 创建人
creatorName?: string; // 创建人名称
createTime: Date; // 创建时间
updateTime: Date; // 更新时间
}
}
/** 查询客户列表 */
export function getCustomerPage(params: PageParam) {
return requestClient.get<PageResult<CrmCustomerApi.Customer>>(
'/crm/customer/page',
{ params },
);
}
/** 查询客户详情 */
export function getCustomer(id: number) {
return requestClient.get<CrmCustomerApi.Customer>(
`/crm/customer/get?id=${id}`,
);
}
/** 新增客户 */
export function createCustomer(data: CrmCustomerApi.Customer) {
return requestClient.post('/crm/customer/create', data);
}
/** 修改客户 */
export function updateCustomer(data: CrmCustomerApi.Customer) {
return requestClient.put('/crm/customer/update', data);
}
/** 删除客户 */
export function deleteCustomer(id: number) {
return requestClient.delete(`/crm/customer/delete?id=${id}`);
}
/** 导出客户 */
export function exportCustomer(params: any) {
return requestClient.download('/crm/customer/export-excel', params);
}
/** 下载客户导入模板 */
export function importCustomerTemplate() {
return requestClient.download('/crm/customer/get-import-template');
}
/** 导入客户 */
export function importCustomer(file: File) {
return requestClient.upload('/crm/customer/import', { file });
}
/** 获取客户精简信息列表 */
export function getCustomerSimpleList() {
return requestClient.get<CrmCustomerApi.Customer[]>(
'/crm/customer/simple-list',
);
}
/** 客户转移 */
export function transferCustomer(data: CrmPermissionApi.TransferReq) {
return requestClient.put('/crm/customer/transfer', data);
}
/** 锁定/解锁客户 */
export function lockCustomer(id: number, lockStatus: boolean) {
return requestClient.put('/crm/customer/lock', { id, lockStatus });
}
/** 领取公海客户 */
export function receiveCustomer(ids: number[]) {
return requestClient.put('/crm/customer/receive', { ids: ids.join(',') });
}
/** 分配公海给对应负责人 */
export function distributeCustomer(ids: number[], ownerUserId: number) {
return requestClient.put('/crm/customer/distribute', { ids, ownerUserId });
}
/** 客户放入公海 */
export function putCustomerPool(id: number) {
return requestClient.put(`/crm/customer/put-pool?id=${id}`);
}
/** 更新客户的成交状态 */
export function updateCustomerDealStatus(id: number, dealStatus: boolean) {
return requestClient.put('/crm/customer/update-deal-status', {
id,
dealStatus,
});
}
/** 进入公海客户提醒的客户列表 */
export function getPutPoolRemindCustomerPage(params: PageParam) {
return requestClient.get<PageResult<CrmCustomerApi.Customer>>(
'/crm/customer/put-pool-remind-page',
{ params },
);
}
/** 获得待进入公海客户数量 */
export function getPutPoolRemindCustomerCount() {
return requestClient.get<number>('/crm/customer/put-pool-remind-count');
}
/** 获得今日需联系客户数量 */
export function getTodayContactCustomerCount() {
return requestClient.get<number>('/crm/customer/today-contact-count');
}
/** 获得分配给我、待跟进的线索数量的客户数量 */
export function getFollowCustomerCount() {
return requestClient.get<number>('/crm/customer/follow-count');
}

View File

@ -0,0 +1,58 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmCustomerLimitConfigApi {
/** 客户限制配置 */
export interface CustomerLimitConfig {
id?: number;
type?: number;
userIds?: string;
deptIds?: string;
maxCount?: number;
dealCountEnabled?: boolean;
}
/**
*
*/
export enum LimitConfType {
/** 锁定客户数限制 */
CUSTOMER_LOCK_LIMIT = 2,
/** 拥有客户数限制 */
CUSTOMER_QUANTITY_LIMIT = 1,
}
}
/** 查询客户限制配置列表 */
export function getCustomerLimitConfigPage(params: PageParam) {
return requestClient.get<
PageResult<CrmCustomerLimitConfigApi.CustomerLimitConfig>
>('/crm/customer-limit-config/page', { params });
}
/** 查询客户限制配置详情 */
export function getCustomerLimitConfig(id: number) {
return requestClient.get<CrmCustomerLimitConfigApi.CustomerLimitConfig>(
`/crm/customer-limit-config/get?id=${id}`,
);
}
/** 新增客户限制配置 */
export function createCustomerLimitConfig(
data: CrmCustomerLimitConfigApi.CustomerLimitConfig,
) {
return requestClient.post('/crm/customer-limit-config/create', data);
}
/** 修改客户限制配置 */
export function updateCustomerLimitConfig(
data: CrmCustomerLimitConfigApi.CustomerLimitConfig,
) {
return requestClient.put('/crm/customer-limit-config/update', data);
}
/** 删除客户限制配置 */
export function deleteCustomerLimitConfig(id: number) {
return requestClient.delete(`/crm/customer-limit-config/delete?id=${id}`);
}

View File

@ -0,0 +1,26 @@
import { requestClient } from '#/api/request';
export namespace CrmCustomerPoolConfigApi {
/** 客户公海规则设置 */
export interface CustomerPoolConfig {
enabled?: boolean;
contactExpireDays?: number;
dealExpireDays?: number;
notifyEnabled?: boolean;
notifyDays?: number;
}
}
/** 获取客户公海规则设置 */
export function getCustomerPoolConfig() {
return requestClient.get<CrmCustomerPoolConfigApi.CustomerPoolConfig>(
'/crm/customer-pool-config/get',
);
}
/** 更新客户公海规则设置 */
export function saveCustomerPoolConfig(
data: CrmCustomerPoolConfigApi.CustomerPoolConfig,
) {
return requestClient.put('/crm/customer-pool-config/save', data);
}

View File

@ -0,0 +1,53 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmFollowUpApi {
/** 关联商机信息 */
export interface Business {
id: number;
name: string;
}
/** 关联联系人信息 */
export interface Contact {
id: number;
name: string;
}
/** 跟进记录信息 */
export interface FollowUpRecord {
id: number; // 编号
bizType: number; // 数据类型
bizId: number; // 数据编号
type: number; // 跟进类型
content: string; // 跟进内容
picUrls: string[]; // 图片
fileUrls: string[]; // 附件
nextTime: Date; // 下次联系时间
businessIds: number[]; // 关联的商机编号数组
businesses: Business[]; // 关联的商机数组
contactIds: number[]; // 关联的联系人编号数组
contacts: Contact[]; // 关联的联系人数组
creator: string;
creatorName?: string;
}
}
/** 查询跟进记录分页 */
export function getFollowUpRecordPage(params: PageParam) {
return requestClient.get<PageResult<CrmFollowUpApi.FollowUpRecord>>(
'/crm/follow-up-record/page',
{ params },
);
}
/** 新增跟进记录 */
export function createFollowUpRecord(data: CrmFollowUpApi.FollowUpRecord) {
return requestClient.post('/crm/follow-up-record/create', data);
}
/** 删除跟进记录 */
export function deleteFollowUpRecord(id: number) {
return requestClient.delete(`/crm/follow-up-record/delete?id=${id}`);
}

View File

@ -0,0 +1,31 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmOperateLogApi {
/** 操作日志查询参数 */
export interface OperateLogQuery extends PageParam {
bizType: number;
bizId: number;
}
/** 操作日志信息 */
export interface OperateLog {
id: number;
bizType: number;
bizId: number;
type: number;
content: string;
creator: string;
creatorName?: string;
createTime: Date;
}
}
/** 获得操作日志 */
export function getOperateLogPage(params: CrmOperateLogApi.OperateLogQuery) {
return requestClient.get<PageResult<CrmOperateLogApi.OperateLog>>(
'/crm/operate-log/page',
{ params },
);
}

View File

@ -0,0 +1,79 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmPermissionApi {
/** 数据权限信息 */
export interface Permission {
id?: number; // 数据权限编号
userId: number; // 用户编号
bizType: number; // Crm 类型
bizId: number; // Crm 类型数据编号
level: number; // 权限级别
toBizTypes?: number[]; // 同时添加至
deptName?: string; // 部门名称
nickname?: string; // 用户昵称
postNames?: string[]; // 岗位名称数组
createTime?: Date;
ids?: number[];
}
/** 数据权限转移请求 */
export interface TransferReq {
id: number; // 模块编号
newOwnerUserId: number; // 新负责人的用户编号
oldOwnerPermissionLevel?: number; // 老负责人加入团队后的权限级别
toBizTypes?: number[]; // 转移客户时,需要额外有【联系人】【商机】【合同】的 checkbox 选择
}
/**
* CRM
*/
export enum BizType {
CRM_BUSINESS = 4, // 商机
CRM_CLUE = 1, // 线索
CRM_CONTACT = 3, // 联系人
CRM_CONTRACT = 5, // 合同
CRM_CUSTOMER = 2, // 客户
CRM_PRODUCT = 6, // 产品
CRM_RECEIVABLE = 7, // 回款
CRM_RECEIVABLE_PLAN = 8, // 回款计划
}
/**
* CRM
*/
export enum PermissionLevel {
OWNER = 1, // 负责人
READ = 2, // 只读
WRITE = 3, // 读写
}
}
/** 获得数据权限列表(查询团队成员列表) */
export function getPermissionList(params: PageParam) {
return requestClient.get<PageResult<CrmPermissionApi.Permission>>(
'/crm/permission/list',
{ params },
);
}
/** 创建数据权限(新增团队成员) */
export function createPermission(data: CrmPermissionApi.Permission) {
return requestClient.post('/crm/permission/create', data);
}
/** 编辑数据权限(修改团队成员权限级别) */
export function updatePermission(data: CrmPermissionApi.Permission) {
return requestClient.put('/crm/permission/update', data);
}
/** 删除数据权限(删除团队成员) */
export function deletePermissionBatch(ids: number[]) {
return requestClient.delete(`/crm/permission/delete?ids=${ids.join(',')}`);
}
/** 删除自己的数据权限(退出团队) */
export function deleteSelfPermission(id: number) {
return requestClient.delete(`/crm/permission/delete-self?id=${id}`);
}

View File

@ -0,0 +1,46 @@
import type { PageParam } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmProductCategoryApi {
/** 产品分类信息 */
export interface ProductCategory {
id: number;
name: string;
parentId: number;
}
}
/** 查询产品分类详情 */
export function getProductCategory(id: number) {
return requestClient.get<CrmProductCategoryApi.ProductCategory>(
`/crm/product-category/get?id=${id}`,
);
}
/** 新增产品分类 */
export function createProductCategory(
data: CrmProductCategoryApi.ProductCategory,
) {
return requestClient.post('/crm/product-category/create', data);
}
/** 修改产品分类 */
export function updateProductCategory(
data: CrmProductCategoryApi.ProductCategory,
) {
return requestClient.put('/crm/product-category/update', data);
}
/** 删除产品分类 */
export function deleteProductCategory(id: number) {
return requestClient.delete(`/crm/product-category/delete?id=${id}`);
}
/** 产品分类列表 */
export function getProductCategoryList(params?: PageParam) {
return requestClient.get<CrmProductCategoryApi.ProductCategory[]>(
'/crm/product-category/list',
{ params },
);
}

View File

@ -0,0 +1,57 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmProductApi {
/** 产品信息 */
export interface Product {
id: number;
name: string;
no: string;
unit: number;
price: number;
status: number;
categoryId: number;
categoryName?: string;
description: string;
ownerUserId: number;
}
}
/** 查询产品列表 */
export function getProductPage(params: PageParam) {
return requestClient.get<PageResult<CrmProductApi.Product>>(
'/crm/product/page',
{ params },
);
}
/** 获得产品精简列表 */
export function getProductSimpleList() {
return requestClient.get<CrmProductApi.Product[]>('/crm/product/simple-list');
}
/** 查询产品详情 */
export function getProduct(id: number) {
return requestClient.get<CrmProductApi.Product>(`/crm/product/get?id=${id}`);
}
/** 新增产品 */
export function createProduct(data: CrmProductApi.Product) {
return requestClient.post('/crm/product/create', data);
}
/** 修改产品 */
export function updateProduct(data: CrmProductApi.Product) {
return requestClient.put('/crm/product/update', data);
}
/** 删除产品 */
export function deleteProduct(id: number) {
return requestClient.delete(`/crm/product/delete?id=${id}`);
}
/** 导出产品 */
export function exportProduct(params: any) {
return requestClient.download('/crm/product/export-excel', params);
}

View File

@ -0,0 +1,90 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmReceivableApi {
/** 合同信息 */
export interface Contract {
id?: number;
name?: string;
no: string;
totalPrice: number;
}
/** 回款信息 */
export interface Receivable {
id: number;
no: string;
planId?: number;
period?: number;
customerId?: number;
customerName?: string;
contractId?: number;
contract?: Contract;
auditStatus: number;
processInstanceId: number;
returnTime: Date;
returnType: number;
price: number;
ownerUserId: number;
ownerUserName?: string;
remark: string;
creator: string; // 创建人
creatorName?: string; // 创建人名称
createTime: Date; // 创建时间
updateTime: Date; // 更新时间
}
}
/** 查询回款列表 */
export function getReceivablePage(params: PageParam) {
return requestClient.get<PageResult<CrmReceivableApi.Receivable>>(
'/crm/receivable/page',
{ params },
);
}
/** 查询回款列表,基于指定客户 */
export function getReceivablePageByCustomer(params: PageParam) {
return requestClient.get<PageResult<CrmReceivableApi.Receivable>>(
'/crm/receivable/page-by-customer',
{ params },
);
}
/** 查询回款详情 */
export function getReceivable(id: number) {
return requestClient.get<CrmReceivableApi.Receivable>(
`/crm/receivable/get?id=${id}`,
);
}
/** 新增回款 */
export function createReceivable(data: CrmReceivableApi.Receivable) {
return requestClient.post('/crm/receivable/create', data);
}
/** 修改回款 */
export function updateReceivable(data: CrmReceivableApi.Receivable) {
return requestClient.put('/crm/receivable/update', data);
}
/** 删除回款 */
export function deleteReceivable(id: number) {
return requestClient.delete(`/crm/receivable/delete?id=${id}`);
}
/** 导出回款 */
export function exportReceivable(params: any) {
return requestClient.download('/crm/receivable/export-excel', params);
}
/** 提交审核 */
export function submitReceivable(id: number) {
return requestClient.put(`/crm/receivable/submit?id=${id}`);
}
/** 获得待审核回款数量 */
export function getAuditReceivableCount() {
return requestClient.get<number>('/crm/receivable/audit-count');
}

View File

@ -0,0 +1,98 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmReceivablePlanApi {
/** 回款计划信息 */
export interface Plan {
id: number;
period: number;
receivableId: number;
price: number;
returnTime: Date;
remindDays: number;
returnType: number;
remindTime: Date;
customerId: number;
customerName?: string;
contractId?: number;
contractNo?: string;
ownerUserId: number;
ownerUserName?: string;
remark: string;
creator: string;
creatorName?: string;
createTime: Date;
updateTime: Date;
receivable?: {
price: number;
returnTime: Date;
};
}
}
/** 查询回款计划列表 */
export function getReceivablePlanPage(params: PageParam) {
return requestClient.get<PageResult<CrmReceivablePlanApi.Plan>>(
'/crm/receivable-plan/page',
{ params },
);
}
/** 查询回款计划列表(按客户) */
export function getReceivablePlanPageByCustomer(params: PageParam) {
return requestClient.get<PageResult<CrmReceivablePlanApi.Plan>>(
'/crm/receivable-plan/page-by-customer',
{ params },
);
}
/** 查询回款计划详情 */
export function getReceivablePlan(id: number) {
return requestClient.get<CrmReceivablePlanApi.Plan>(
'/crm/receivable-plan/get',
{ params: { id } },
);
}
/** 查询回款计划下拉数据 */
export function getReceivablePlanSimpleList(
customerId: number,
contractId: number,
) {
return requestClient.get<CrmReceivablePlanApi.Plan[]>(
'/crm/receivable-plan/simple-list',
{
params: { customerId, contractId },
},
);
}
/** 新增回款计划 */
export function createReceivablePlan(data: CrmReceivablePlanApi.Plan) {
return requestClient.post('/crm/receivable-plan/create', data);
}
/** 修改回款计划 */
export function updateReceivablePlan(data: CrmReceivablePlanApi.Plan) {
return requestClient.put('/crm/receivable-plan/update', data);
}
/** 删除回款计划 */
export function deleteReceivablePlan(id: number) {
return requestClient.delete('/crm/receivable-plan/delete', {
params: { id },
});
}
/** 导出回款计划 Excel */
export function exportReceivablePlan(params: PageParam) {
return requestClient.download('/crm/receivable-plan/export-excel', {
params,
});
}
/** 获得待回款提醒数量 */
export function getReceivablePlanRemindCount() {
return requestClient.get<number>('/crm/receivable-plan/remind-count');
}

View File

@ -0,0 +1,191 @@
import type { PageParam } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmStatisticsCustomerApi {
/** 客户总量分析(按日期) */
export interface CustomerSummaryByDate {
time: string;
customerCreateCount: number;
customerDealCount: number;
}
/** 客户总量分析(按用户) */
export interface CustomerSummaryByUser {
ownerUserName: string;
customerCreateCount: number;
customerDealCount: number;
contractPrice: number;
receivablePrice: number;
}
/** 客户跟进次数分析(按日期) */
export interface FollowUpSummaryByDate {
time: string;
followUpRecordCount: number;
followUpCustomerCount: number;
}
/** 客户跟进次数分析(按用户) */
export interface FollowUpSummaryByUser {
ownerUserName: string;
followupRecordCount: number;
followupCustomerCount: number;
}
/** 客户跟进方式统计 */
export interface FollowUpSummaryByType {
followUpType: string;
followUpRecordCount: number;
}
/** 合同摘要信息 */
export interface CustomerContractSummary {
customerName: string;
contractName: string;
totalPrice: number;
receivablePrice: number;
customerType: string;
customerSource: string;
ownerUserName: string;
creatorUserName: string;
createTime: Date;
orderDate: Date;
}
/** 客户公海分析(按日期) */
export interface PoolSummaryByDate {
time: string;
customerPutCount: number;
customerTakeCount: number;
}
/** 客户公海分析(按用户) */
export interface PoolSummaryByUser {
ownerUserName: string;
customerPutCount: number;
customerTakeCount: number;
}
/** 客户成交周期(按日期) */
export interface CustomerDealCycleByDate {
time: string;
customerDealCycle: number;
}
/** 客户成交周期(按用户) */
export interface CustomerDealCycleByUser {
ownerUserName: string;
customerDealCycle: number;
customerDealCount: number;
}
/** 客户成交周期(按地区) */
export interface CustomerDealCycleByArea {
areaName: string;
customerDealCycle: number;
customerDealCount: number;
}
/** 客户成交周期(按产品) */
export interface CustomerDealCycleByProduct {
productName: string;
customerDealCycle: number;
customerDealCount: number;
}
}
/** 客户总量分析(按日期) */
export function getCustomerSummaryByDate(params: PageParam) {
return requestClient.get<CrmStatisticsCustomerApi.CustomerSummaryByDate[]>(
'/crm/statistics-customer/get-customer-summary-by-date',
{ params },
);
}
/** 客户总量分析(按用户) */
export function getCustomerSummaryByUser(params: PageParam) {
return requestClient.get<CrmStatisticsCustomerApi.CustomerSummaryByUser[]>(
'/crm/statistics-customer/get-customer-summary-by-user',
{ params },
);
}
/** 客户跟进次数分析(按日期) */
export function getFollowUpSummaryByDate(params: PageParam) {
return requestClient.get<CrmStatisticsCustomerApi.FollowUpSummaryByDate[]>(
'/crm/statistics-customer/get-follow-up-summary-by-date',
{ params },
);
}
/** 客户跟进次数分析(按用户) */
export function getFollowUpSummaryByUser(params: PageParam) {
return requestClient.get<CrmStatisticsCustomerApi.FollowUpSummaryByUser[]>(
'/crm/statistics-customer/get-follow-up-summary-by-user',
{ params },
);
}
/** 获取客户跟进方式统计数 */
export function getFollowUpSummaryByType(params: PageParam) {
return requestClient.get<CrmStatisticsCustomerApi.FollowUpSummaryByType[]>(
'/crm/statistics-customer/get-follow-up-summary-by-type',
{ params },
);
}
/** 合同摘要信息(客户转化率页面) */
export function getContractSummary(params: PageParam) {
return requestClient.get<CrmStatisticsCustomerApi.CustomerContractSummary[]>(
'/crm/statistics-customer/get-contract-summary',
{ params },
);
}
/** 获取客户公海分析(按日期) */
export function getPoolSummaryByDate(params: PageParam) {
return requestClient.get<CrmStatisticsCustomerApi.PoolSummaryByDate[]>(
'/crm/statistics-customer/get-pool-summary-by-date',
{ params },
);
}
/** 获取客户公海分析(按用户) */
export function getPoolSummaryByUser(params: PageParam) {
return requestClient.get<CrmStatisticsCustomerApi.PoolSummaryByUser[]>(
'/crm/statistics-customer/get-pool-summary-by-user',
{ params },
);
}
/** 获取客户成交周期(按日期) */
export function getCustomerDealCycleByDate(params: PageParam) {
return requestClient.get<CrmStatisticsCustomerApi.CustomerDealCycleByDate[]>(
'/crm/statistics-customer/get-customer-deal-cycle-by-date',
{ params },
);
}
/** 获取客户成交周期(按用户) */
export function getCustomerDealCycleByUser(params: PageParam) {
return requestClient.get<CrmStatisticsCustomerApi.CustomerDealCycleByUser[]>(
'/crm/statistics-customer/get-customer-deal-cycle-by-user',
{ params },
);
}
/** 获取客户成交周期(按地区) */
export function getCustomerDealCycleByArea(params: PageParam) {
return requestClient.get<CrmStatisticsCustomerApi.CustomerDealCycleByArea[]>(
'/crm/statistics-customer/get-customer-deal-cycle-by-area',
{ params },
);
}
/** 获取客户成交周期(按产品) */
export function getCustomerDealCycleByProduct(params: PageParam) {
return requestClient.get<
CrmStatisticsCustomerApi.CustomerDealCycleByProduct[]
>('/crm/statistics-customer/get-customer-deal-cycle-by-product', { params });
}

View File

@ -0,0 +1,67 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmStatisticsFunnelApi {
/** 销售漏斗统计数据 */
export interface FunnelSummary {
customerCount: number; // 客户数
businessCount: number; // 商机数
businessWinCount: number; // 赢单数
}
/** 商机分析(按日期) */
export interface BusinessSummaryByDate {
time: string; // 时间
businessCreateCount: number; // 商机数
totalPrice: number | string; // 商机金额
}
/** 商机转化率分析(按日期) */
export interface BusinessInversionRateSummaryByDate {
time: string; // 时间
businessCount: number; // 商机数量
businessWinCount: number; // 赢单商机数
}
}
/** 获取销售漏斗统计数据 */
export function getFunnelSummary(params: PageParam) {
return requestClient.get<CrmStatisticsFunnelApi.FunnelSummary>(
'/crm/statistics-funnel/get-funnel-summary',
{ params },
);
}
/** 获取商机结束状态统计 */
export function getBusinessSummaryByEndStatus(params: PageParam) {
return requestClient.get<Record<string, number>>(
'/crm/statistics-funnel/get-business-summary-by-end-status',
{ params },
);
}
/** 获取新增商机分析(按日期) */
export function getBusinessSummaryByDate(params: PageParam) {
return requestClient.get<CrmStatisticsFunnelApi.BusinessSummaryByDate[]>(
'/crm/statistics-funnel/get-business-summary-by-date',
{ params },
);
}
/** 获取商机转化率分析(按日期) */
export function getBusinessInversionRateSummaryByDate(params: PageParam) {
return requestClient.get<
CrmStatisticsFunnelApi.BusinessInversionRateSummaryByDate[]
>('/crm/statistics-funnel/get-business-inversion-rate-summary-by-date', {
params,
});
}
/** 获取商机列表(按日期) */
export function getBusinessPageByDate(params: PageParam) {
return requestClient.get<PageResult<any>>(
'/crm/statistics-funnel/get-business-page-by-date',
{ params },
);
}

View File

@ -0,0 +1,37 @@
import type { PageParam } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmStatisticsPerformanceApi {
/** 员工业绩统计 */
export interface Performance {
time: string;
currentMonthCount: number;
lastMonthCount: number;
lastYearCount: number;
}
}
/** 员工获得合同金额统计 */
export function getContractPricePerformance(params: PageParam) {
return requestClient.get<CrmStatisticsPerformanceApi.Performance[]>(
'/crm/statistics-performance/get-contract-price-performance',
{ params },
);
}
/** 员工获得回款统计 */
export function getReceivablePricePerformance(params: PageParam) {
return requestClient.get<CrmStatisticsPerformanceApi.Performance[]>(
'/crm/statistics-performance/get-receivable-price-performance',
{ params },
);
}
/** 员工获得签约合同数量统计 */
export function getContractCountPerformance(params: PageParam) {
return requestClient.get<CrmStatisticsPerformanceApi.Performance[]>(
'/crm/statistics-performance/get-contract-count-performance',
{ params },
);
}

View File

@ -0,0 +1,69 @@
import type { PageParam } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmStatisticsPortraitApi {
/** 客户基础统计信息 */
export interface CustomerBase {
customerCount: number;
dealCount: number;
dealPortion: number | string;
}
/** 客户行业统计信息 */
export interface CustomerIndustry extends CustomerBase {
industryId: number;
industryPortion: number | string;
}
/** 客户来源统计信息 */
export interface CustomerSource extends CustomerBase {
source: number;
sourcePortion: number | string;
}
/** 客户级别统计信息 */
export interface CustomerLevel extends CustomerBase {
level: number;
levelPortion: number | string;
}
/** 客户地区统计信息 */
export interface CustomerArea extends CustomerBase {
areaId: number;
areaName: string;
areaPortion: number | string;
}
}
/** 获取客户行业统计数据 */
export function getCustomerIndustry(params: PageParam) {
return requestClient.get<CrmStatisticsPortraitApi.CustomerIndustry[]>(
'/crm/statistics-portrait/get-customer-industry-summary',
{ params },
);
}
/** 获取客户来源统计数据 */
export function getCustomerSource(params: PageParam) {
return requestClient.get<CrmStatisticsPortraitApi.CustomerSource[]>(
'/crm/statistics-portrait/get-customer-source-summary',
{ params },
);
}
/** 获取客户级别统计数据 */
export function getCustomerLevel(params: PageParam) {
return requestClient.get<CrmStatisticsPortraitApi.CustomerLevel[]>(
'/crm/statistics-portrait/get-customer-level-summary',
{ params },
);
}
/** 获取客户地区统计数据 */
export function getCustomerArea(params: PageParam) {
return requestClient.get<CrmStatisticsPortraitApi.CustomerArea[]>(
'/crm/statistics-portrait/get-customer-area-summary',
{ params },
);
}

View File

@ -0,0 +1,76 @@
import type { PageParam } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmStatisticsRankApi {
/** 排行统计数据 */
export interface Rank {
count: number;
nickname: string;
deptName: string;
}
}
/** 获得合同排行榜 */
export function getContractPriceRank(params: PageParam) {
return requestClient.get<CrmStatisticsRankApi.Rank[]>(
'/crm/statistics-rank/get-contract-price-rank',
{ params },
);
}
/** 获得回款排行榜 */
export function getReceivablePriceRank(params: PageParam) {
return requestClient.get<CrmStatisticsRankApi.Rank[]>(
'/crm/statistics-rank/get-receivable-price-rank',
{ params },
);
}
/** 签约合同排行 */
export function getContractCountRank(params: PageParam) {
return requestClient.get<CrmStatisticsRankApi.Rank[]>(
'/crm/statistics-rank/get-contract-count-rank',
{ params },
);
}
/** 产品销量排行 */
export function getProductSalesRank(params: PageParam) {
return requestClient.get<CrmStatisticsRankApi.Rank[]>(
'/crm/statistics-rank/get-product-sales-rank',
{ params },
);
}
/** 新增客户数排行 */
export function getCustomerCountRank(params: PageParam) {
return requestClient.get<CrmStatisticsRankApi.Rank[]>(
'/crm/statistics-rank/get-customer-count-rank',
{ params },
);
}
/** 新增联系人数排行 */
export function getContactsCountRank(params: PageParam) {
return requestClient.get<CrmStatisticsRankApi.Rank[]>(
'/crm/statistics-rank/get-contacts-count-rank',
{ params },
);
}
/** 跟进次数排行 */
export function getFollowCountRank(params: PageParam) {
return requestClient.get<CrmStatisticsRankApi.Rank[]>(
'/crm/statistics-rank/get-follow-count-rank',
{ params },
);
}
/** 跟进客户数排行 */
export function getFollowCustomerCountRank(params: PageParam) {
return requestClient.get<CrmStatisticsRankApi.Rank[]>(
'/crm/statistics-rank/get-follow-customer-count-rank',
{ params },
);
}

View File

@ -23,12 +23,13 @@ export namespace InfraFileApi {
configId: number; // 文件配置编号
uploadUrl: string; // 文件上传 URL
url: string; // 文件 URL
path: string; // 文件路径
}
/** 上传文件 */
export interface FileUploadReqVO {
file: globalThis.File;
path?: string;
directory?: string;
}
}
@ -45,11 +46,11 @@ export function deleteFile(id: number) {
}
/** 获取文件预签名地址 */
export function getFilePresignedUrl(path: string) {
export function getFilePresignedUrl(name: string, directory?: string) {
return requestClient.get<InfraFileApi.FilePresignedUrlRespVO>(
'/infra/file/presigned-url',
{
params: { path },
params: { name, directory },
},
);
}
@ -64,5 +65,9 @@ export function uploadFile(
data: InfraFileApi.FileUploadReqVO,
onUploadProgress?: AxiosProgressEvent,
) {
// 特殊:由于 upload 内部封装,即使 directory 为 undefined也会传递给后端
if (!data.directory) {
delete data.directory;
}
return requestClient.upload('/infra/file/upload', data, { onUploadProgress });
}

View File

@ -0,0 +1,63 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace PayAppApi {
/** 支付应用信息 */
export interface App {
id?: number;
appKey: string;
name: string;
status: number;
remark: string;
payNotifyUrl: string;
refundNotifyUrl: string;
transferNotifyUrl: string;
merchantId: number;
merchantName: string;
createTime?: Date;
}
/** 更新状态请求 */
export interface UpdateStatusReq {
id: number;
status: number;
}
}
/** 查询支付应用列表 */
export function getAppPage(params: PageParam) {
return requestClient.get<PageResult<PayAppApi.App>>('/pay/app/page', {
params,
});
}
/** 查询支付应用详情 */
export function getApp(id: number) {
return requestClient.get<PayAppApi.App>(`/pay/app/get?id=${id}`);
}
/** 新增支付应用 */
export function createApp(data: PayAppApi.App) {
return requestClient.post('/pay/app/create', data);
}
/** 修改支付应用 */
export function updateApp(data: PayAppApi.App) {
return requestClient.put('/pay/app/update', data);
}
/** 修改支付应用状态 */
export function changeAppStatus(data: PayAppApi.UpdateStatusReq) {
return requestClient.put('/pay/app/update-status', data);
}
/** 删除支付应用 */
export function deleteApp(id: number) {
return requestClient.delete(`/pay/app/delete?id=${id}`);
}
/** 获取支付应用列表 */
export function getAppList() {
return requestClient.get<PayAppApi.App[]>('/pay/app/list');
}

View File

@ -0,0 +1,54 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace PayChannelApi {
/** 支付渠道信息 */
export interface Channel {
id: number;
code: string;
config: string;
status: number;
remark: string;
feeRate: number;
appId: number;
createTime: Date;
}
}
/** 查询支付渠道列表 */
export function getChannelPage(params: PageParam) {
return requestClient.get<PageResult<PayChannelApi.Channel>>(
'/pay/channel/page',
{
params,
},
);
}
/** 查询支付渠道详情 */
export function getChannel(appId: string, code: string) {
return requestClient.get<PayChannelApi.Channel>('/pay/channel/get', {
params: { appId, code },
});
}
/** 新增支付渠道 */
export function createChannel(data: PayChannelApi.Channel) {
return requestClient.post('/pay/channel/create', data);
}
/** 修改支付渠道 */
export function updateChannel(data: PayChannelApi.Channel) {
return requestClient.put('/pay/channel/update', data);
}
/** 删除支付渠道 */
export function deleteChannel(id: number) {
return requestClient.delete(`/pay/channel/delete?id=${id}`);
}
/** 导出支付渠道 */
export function exportChannel(params: PageParam) {
return requestClient.download('/pay/channel/export-excel', { params });
}

View File

@ -0,0 +1,38 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace PayDemoApi {
/** 示例订单信息 */
export interface DemoOrder {
spuId: number;
createTime: Date;
}
}
/** 创建示例订单 */
export function createDemoOrder(data: PayDemoApi.DemoOrder) {
return requestClient.post('/pay/demo-order/create', data);
}
/** 获得示例订单 */
export function getDemoOrder(id: number) {
return requestClient.get<PayDemoApi.DemoOrder>(
`/pay/demo-order/get?id=${id}`,
);
}
/** 获得示例订单分页 */
export function getDemoOrderPage(params: PageParam) {
return requestClient.get<PageResult<PayDemoApi.DemoOrder>>(
'/pay/demo-order/page',
{
params,
},
);
}
/** 退款示例订单 */
export function refundDemoOrder(id: number) {
return requestClient.put(`/pay/demo-order/refund?id=${id}`);
}

View File

@ -0,0 +1,29 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace PayDemoTransferApi {
/** 示例转账单信息 */
export interface DemoTransfer {
price: number;
type: number;
userName: string;
alipayLogonId: string;
openid: string;
}
}
/** 创建示例转账单 */
export function createDemoTransfer(data: PayDemoTransferApi.DemoTransfer) {
return requestClient.post('/pay/demo-transfer/create', data);
}
/** 获得示例转账单分页 */
export function getDemoTransferPage(params: PageParam) {
return requestClient.get<PageResult<PayDemoTransferApi.DemoTransfer>>(
'/pay/demo-transfer/page',
{
params,
},
);
}

View File

@ -0,0 +1,15 @@
import type { PageParam } from '@vben/request';
import { requestClient } from '#/api/request';
/** 获得支付通知明细 */
export function getNotifyTaskDetail(id: number) {
return requestClient.get(`/pay/notify/get-detail?id=${id}`);
}
/** 获得支付通知分页 */
export function getNotifyTaskPage(params: PageParam) {
return requestClient.get('/pay/notify/page', {
params,
});
}

View File

@ -0,0 +1,118 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace PayOrderApi {
/** 支付订单信息 */
export interface Order {
id: number;
merchantId: number;
appId: number;
channelId: number;
channelCode: string;
merchantOrderId: string;
subject: string;
body: string;
notifyUrl: string;
notifyStatus: number;
amount: number;
channelFeeRate: number;
channelFeeAmount: number;
status: number;
userIp: string;
expireTime: Date;
successTime: Date;
notifyTime: Date;
successExtensionId: number;
refundStatus: number;
refundTimes: number;
refundAmount: number;
channelUserId: string;
channelOrderNo: string;
createTime: Date;
}
/** 支付订单分页请求 */
export interface OrderPageReqVO extends PageParam {
merchantId?: number;
appId?: number;
channelId?: number;
channelCode?: string;
merchantOrderId?: string;
subject?: string;
body?: string;
notifyUrl?: string;
notifyStatus?: number;
amount?: number;
channelFeeRate?: number;
channelFeeAmount?: number;
status?: number;
expireTime?: Date[];
successTime?: Date[];
notifyTime?: Date[];
successExtensionId?: number;
refundStatus?: number;
refundTimes?: number;
channelUserId?: string;
channelOrderNo?: string;
createTime?: Date[];
}
/** 支付订单导出请求 */
export interface OrderExportReqVO {
merchantId?: number;
appId?: number;
channelId?: number;
channelCode?: string;
merchantOrderId?: string;
subject?: string;
body?: string;
notifyUrl?: string;
notifyStatus?: number;
amount?: number;
channelFeeRate?: number;
channelFeeAmount?: number;
status?: number;
expireTime?: Date[];
successTime?: Date[];
notifyTime?: Date[];
successExtensionId?: number;
refundStatus?: number;
refundTimes?: number;
channelUserId?: string;
channelOrderNo?: string;
createTime?: Date[];
}
}
/** 查询支付订单列表 */
export function getOrderPage(params: PayOrderApi.OrderPageReqVO) {
return requestClient.get<PageResult<PayOrderApi.Order>>('/pay/order/page', {
params,
});
}
/** 查询支付订单详情 */
export function getOrder(id: number, sync?: boolean) {
return requestClient.get<PayOrderApi.Order>('/pay/order/get', {
params: {
id,
sync,
},
});
}
/** 获得支付订单的明细 */
export function getOrderDetail(id: number) {
return requestClient.get<PayOrderApi.Order>(`/pay/order/get-detail?id=${id}`);
}
/** 提交支付订单 */
export function submitOrder(data: any) {
return requestClient.post('/pay/order/submit', data);
}
/** 导出支付订单 */
export function exportOrder(params: PayOrderApi.OrderExportReqVO) {
return requestClient.download('/pay/order/export-excel', { params });
}

View File

@ -0,0 +1,129 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace PayRefundApi {
/** 退款订单信息 */
export interface Refund {
id: number;
merchantId: number;
appId: number;
channelId: number;
channelCode: string;
orderId: string;
tradeNo: string;
merchantOrderId: string;
merchantRefundNo: string;
notifyUrl: string;
notifyStatus: number;
status: number;
type: number;
payAmount: number;
refundAmount: number;
reason: string;
userIp: string;
channelOrderNo: string;
channelRefundNo: string;
channelErrorCode: string;
channelErrorMsg: string;
channelExtras: string;
expireTime: Date;
successTime: Date;
notifyTime: Date;
createTime: Date;
updateTime: Date;
}
/** 退款订单分页请求 */
export interface RefundPageReqVO extends PageParam {
merchantId?: number;
appId?: number;
channelId?: number;
channelCode?: string;
orderId?: string;
tradeNo?: string;
merchantOrderId?: string;
merchantRefundNo?: string;
notifyUrl?: string;
notifyStatus?: number;
status?: number;
type?: number;
payAmount?: number;
refundAmount?: number;
reason?: string;
userIp?: string;
channelOrderNo?: string;
channelRefundNo?: string;
channelErrorCode?: string;
channelErrorMsg?: string;
channelExtras?: string;
expireTime?: Date[];
successTime?: Date[];
notifyTime?: Date[];
createTime?: Date[];
}
/** 退款订单导出请求 */
export interface RefundExportReqVO {
merchantId?: number;
appId?: number;
channelId?: number;
channelCode?: string;
orderId?: string;
tradeNo?: string;
merchantOrderId?: string;
merchantRefundNo?: string;
notifyUrl?: string;
notifyStatus?: number;
status?: number;
type?: number;
payAmount?: number;
refundAmount?: number;
reason?: string;
userIp?: string;
channelOrderNo?: string;
channelRefundNo?: string;
channelErrorCode?: string;
channelErrorMsg?: string;
channelExtras?: string;
expireTime?: Date[];
successTime?: Date[];
notifyTime?: Date[];
createTime?: Date[];
}
}
/** 查询退款订单列表 */
export function getRefundPage(params: PayRefundApi.RefundPageReqVO) {
return requestClient.get<PageResult<PayRefundApi.Refund>>(
'/pay/refund/page',
{
params,
},
);
}
/** 查询退款订单详情 */
export function getRefund(id: number) {
return requestClient.get<PayRefundApi.Refund>(`/pay/refund/get?id=${id}`);
}
/** 创建退款订单 */
export function createRefund(data: PayRefundApi.Refund) {
return requestClient.post('/pay/refund/create', data);
}
/** 更新退款订单 */
export function updateRefund(data: PayRefundApi.Refund) {
return requestClient.put('/pay/refund/update', data);
}
/** 删除退款订单 */
export function deleteRefund(id: number) {
return requestClient.delete(`/pay/refund/delete?id=${id}`);
}
/** 导出退款订单 */
export function exportRefund(params: PayRefundApi.RefundExportReqVO) {
return requestClient.download('/pay/refund/export-excel', { params });
}

View File

@ -0,0 +1,58 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace PayTransferApi {
/** 转账单信息 */
export interface Transfer {
id: number;
appId: number;
channelId: number;
channelCode: string;
merchantTransferId: string;
type: number;
price: number;
subject: string;
userName: string;
alipayLogonId: string;
openid: string;
status: number;
createTime: Date;
}
/** 转账单分页请求 */
export interface TransferPageReqVO extends PageParam {
appId?: number;
channelId?: number;
channelCode?: string;
merchantTransferId?: string;
type?: number;
price?: number;
subject?: string;
userName?: string;
status?: number;
createTime?: Date[];
}
}
/** 查询转账单列表 */
export function getTransferPage(params: PayTransferApi.TransferPageReqVO) {
return requestClient.get<PageResult<PayTransferApi.Transfer>>(
'/pay/transfer/page',
{
params,
},
);
}
/** 查询转账单详情 */
export function getTransfer(id: number) {
return requestClient.get<PayTransferApi.Transfer>(
`/pay/transfer/get?id=${id}`,
);
}
/** 创建转账单 */
export function createTransfer(data: PayTransferApi.Transfer) {
return requestClient.post('/pay/transfer/create', data);
}

View File

@ -0,0 +1,53 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace PayWalletApi {
/** 用户钱包查询参数 */
export interface PayWalletUserReqVO {
userId: number;
}
/** 钱包信息 */
export interface WalletVO {
id: number;
userId: number;
userType: number;
balance: number;
totalExpense: number;
totalRecharge: number;
freezePrice: number;
}
/** 钱包分页请求 */
export interface WalletPageReqVO extends PageParam {
userId?: number;
userType?: number;
balance?: number;
totalExpense?: number;
totalRecharge?: number;
freezePrice?: number;
}
}
/** 查询用户钱包详情 */
export function getWallet(params: PayWalletApi.PayWalletUserReqVO) {
return requestClient.get<PayWalletApi.WalletVO>('/pay/wallet/get', {
params,
});
}
/** 查询会员钱包列表 */
export function getWalletPage(params: PayWalletApi.WalletPageReqVO) {
return requestClient.get<PageResult<PayWalletApi.WalletVO>>(
'/pay/wallet/page',
{
params,
},
);
}
/** 修改会员钱包余额 */
export function updateWalletBalance(data: PayWalletApi.WalletVO) {
return requestClient.put('/pay/wallet/update-balance', data);
}

View File

@ -0,0 +1,46 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace WalletRechargePackageApi {
/** 充值套餐信息 */
export interface Package {
id?: number;
name: string;
payPrice: number;
bonusPrice: number;
status: number;
}
}
/** 查询充值套餐列表 */
export function getPackagePage(params: PageParam) {
return requestClient.get<PageResult<WalletRechargePackageApi.Package>>(
'/pay/wallet-recharge-package/page',
{
params,
},
);
}
/** 查询充值套餐详情 */
export function getPackage(id: number) {
return requestClient.get<WalletRechargePackageApi.Package>(
`/pay/wallet-recharge-package/get?id=${id}`,
);
}
/** 新增充值套餐 */
export function createPackage(data: WalletRechargePackageApi.Package) {
return requestClient.post('/pay/wallet-recharge-package/create', data);
}
/** 修改充值套餐 */
export function updatePackage(data: WalletRechargePackageApi.Package) {
return requestClient.put('/pay/wallet-recharge-package/update', data);
}
/** 删除充值套餐 */
export function deletePackage(id: number) {
return requestClient.delete(`/pay/wallet-recharge-package/delete?id=${id}`);
}

View File

@ -0,0 +1,24 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace WalletTransactionApi {
/** 钱包交易流水信息 */
export interface Transaction {
id: number;
walletId: number;
title: string;
price: number;
balance: number;
}
}
/** 查询钱包交易流水列表 */
export function getTransactionPage(params: PageParam) {
return requestClient.get<PageResult<WalletTransactionApi.Transaction>>(
'/pay/wallet-transaction/page',
{
params,
},
);
}

View File

@ -7,7 +7,6 @@ import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/antd';
import 'vxe-table/styles/cssvar.scss'; // TODO @puhui999这个必须导入哇我看 use-vxe-grid.vue 已经导入了
import { useTitle } from '@vueuse/core';
@ -18,6 +17,8 @@ import { initComponentAdapter } from './adapter/component';
import App from './app.vue';
import { router } from './router';
import 'vxe-table/styles/cssvar.scss'; // TODO @puhui999这个必须导入哇我看 use-vxe-grid.vue 已经导入了
async function bootstrap(namespace: string) {
// 初始化组件适配器
await initComponentAdapter();

View File

@ -4,6 +4,7 @@ import ImageUpload from '#/components/upload/image-upload.vue';
export const useImagesUpload = () => {
return defineComponent({
name: 'ImagesUpload',
props: {
multiple: {
type: Boolean,
@ -20,6 +21,5 @@ export const useImagesUpload = () => {
<ImageUpload maxNumber={props.maxNumber} multiple={props.multiple} />
);
},
name: 'ImagesUpload',
});
};

View File

@ -28,6 +28,8 @@ const props = withDefaults(
file: File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
//
directory?: string;
disabled?: boolean;
helpText?: string;
// Infinity
@ -44,13 +46,14 @@ const props = withDefaults(
}>(),
{
value: () => [],
directory: undefined,
disabled: false,
helpText: '',
maxSize: 2,
maxNumber: 1,
accept: () => [],
multiple: false,
api: useUpload().httpRequest,
api: undefined,
resultField: '',
showDescription: false,
},
@ -141,10 +144,9 @@ const beforeUpload = async (file: File) => {
};
async function customRequest(info: UploadRequestOption<any>) {
const { api } = props;
let { api } = props;
if (!api || !isFunction(api)) {
console.warn('upload api must exist and be a function');
return;
api = useUpload(props.directory).httpRequest;
}
try {
//

View File

@ -30,6 +30,8 @@ const props = withDefaults(
file: File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
//
directory?: string;
disabled?: boolean;
helpText?: string;
listType?: UploadListType;
@ -47,6 +49,7 @@ const props = withDefaults(
}>(),
{
value: () => [],
directory: undefined,
disabled: false,
listType: 'picture-card',
helpText: '',
@ -54,7 +57,7 @@ const props = withDefaults(
maxNumber: 1,
accept: () => defaultImageAccepts,
multiple: false,
api: useUpload().httpRequest,
api: undefined,
resultField: '',
showDescription: true,
},
@ -177,10 +180,9 @@ const beforeUpload = async (file: File) => {
};
async function customRequest(info: UploadRequestOption<any>) {
const { api } = props;
let { api } = props;
if (!api || !isFunction(api)) {
console.warn('upload api must exist and be a function');
return;
api = useUpload(props.directory).httpRequest;
}
try {
//

View File

@ -7,8 +7,7 @@ import { computed, unref } from 'vue';
import { useAppConfig } from '@vben/hooks';
import { $t } from '@vben/locales';
import CryptoJS from 'crypto-js';
// import CryptoJS from 'crypto-js';
import { createFile, getFilePresignedUrl, uploadFile } from '#/api/infra/file';
import { baseRequestClient } from '#/api/request';
@ -81,7 +80,7 @@ export function useUploadType({
}
// TODO @芋艿:目前保持和 admin-vue3 一致,后续可能重构
export const useUpload = () => {
export const useUpload = (directory?: string) => {
// 后端上传地址
const uploadUrl = getUploadUrl();
// 是否使用前端直连上传
@ -97,7 +96,7 @@ export const useUpload = () => {
// 1.1 生成文件名称
const fileName = await generateFileName(file);
// 1.2 获取文件预签名地址
const presignedInfo = await getFilePresignedUrl(fileName);
const presignedInfo = await getFilePresignedUrl(fileName, directory);
// 1.3 上传文件
return baseRequestClient
.put(presignedInfo.uploadUrl, file, {
@ -107,13 +106,13 @@ export const useUpload = () => {
})
.then(() => {
// 1.4. 记录文件信息到后端(异步)
createFile0(presignedInfo, fileName, file);
createFile0(presignedInfo, file);
// 通知成功,数据格式保持与后端上传的返回结果一致
return { data: presignedInfo.url };
return { url: presignedInfo.url };
});
} else {
// 模式二:后端上传
return uploadFile({ file }, onUploadProgress);
return uploadFile({ file, directory }, onUploadProgress);
}
};
@ -134,18 +133,13 @@ export const getUploadUrl = (): string => {
*
*
* @param vo
* @param name
* @param file
*/
function createFile0(
vo: InfraFileApi.FilePresignedUrlRespVO,
name: string,
file: File,
) {
function createFile0(vo: InfraFileApi.FilePresignedUrlRespVO, file: File) {
const fileVO = {
configId: vo.configId,
url: vo.url,
path: name,
path: vo.path,
name: file.name,
type: file.type,
size: file.size,
@ -160,12 +154,13 @@ function createFile0(
* @param file
*/
async function generateFileName(file: File) {
// 读取文件内容
const data = await file.arrayBuffer();
const wordArray = CryptoJS.lib.WordArray.create(data);
// 计算SHA256
const sha256 = CryptoJS.SHA256(wordArray).toString();
// 拼接后缀
const ext = file.name.slice(Math.max(0, file.name.lastIndexOf('.')));
return `${sha256}${ext}`;
// // 读取文件内容
// const data = await file.arrayBuffer();
// const wordArray = CryptoJS.lib.WordArray.create(data);
// // 计算SHA256
// const sha256 = CryptoJS.SHA256(wordArray).toString();
// // 拼接后缀
// const ext = file.name.slice(Math.max(0, file.name.lastIndexOf('.')));
// return `${sha256}${ext}`;
return file.name;
}

View File

@ -3,7 +3,7 @@ import type { NotificationItem } from '@vben/layouts';
import { computed, onMounted, ref, watch } from 'vue';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks';
import {
@ -33,6 +33,8 @@ import { router } from '#/router';
import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue';
import Help from './components/help.vue';
const userStore = useUserStore();
const authStore = useAuthStore();
const accessStore = useAccessStore();
@ -42,6 +44,10 @@ const notifications = ref<NotificationItem[]>([]);
const unreadCount = ref(0);
const showDot = computed(() => unreadCount.value > 0);
const [HelpModal, helpModalApi] = useVbenModal({
connectedComponent: Help,
});
const menus = computed(() => [
{
handler: () => {
@ -70,9 +76,7 @@ const menus = computed(() => [
},
{
handler: () => {
openWindow(`${VBEN_GITHUB_URL}/issues`, {
target: '_blank',
});
helpModalApi.open();
},
icon: CircleHelp,
text: $t('ui.widgets.qa'),
@ -210,4 +214,5 @@ watch(
<LockScreen :avatar @to-login="handleLogout" />
</template>
</BasicLayout>
<HelpModal />
</template>

View File

@ -0,0 +1,93 @@
<script lang="ts" setup>
import { useVbenModal, VbenButton, VbenButtonGroup } from '@vben/common-ui';
import { Image, Tag } from 'ant-design-vue';
import { $t } from '#/locales';
const [Modal, modalApi] = useVbenModal({
draggable: true,
overlayBlur: 5,
footer: false,
onCancel() {
modalApi.close();
},
});
function openWindow(url: string) {
window.open(url, '_blank');
}
</script>
<template>
<Modal class="w-[40%]" :title="$t('ui.widgets.qa')">
<div class="mt-2 flex flex-col">
<div class="mt-2 flex flex-row">
<VbenButtonGroup class="basis-1/3" :gap="2" border size="large">
<p class="p-2">项目地址:</p>
<VbenButton
variant="link"
@click="
openWindow('https://gitee.com/yudaocode/yudao-ui-admin-vben')
"
>
Gitee
</VbenButton>
<VbenButton
variant="link"
@click="
openWindow('https://github.com/yudaocode/yudao-ui-admin-vben')
"
>
Github
</VbenButton>
</VbenButtonGroup>
<VbenButtonGroup class="basis-1/3" :gap="2" border size="large">
<p class="p-2">issues:</p>
<VbenButton
variant="link"
@click="
openWindow(
'https://gitee.com/yudaocode/yudao-ui-admin-vben/issues',
)
"
>
Gitee
</VbenButton>
<VbenButton
variant="link"
@click="
openWindow(
'https://github.com/yudaocode/yudao-ui-admin-vben/issues',
)
"
>
Github
</VbenButton>
</VbenButtonGroup>
<VbenButtonGroup class="basis-1/3" :gap="2" border size="large">
<p class="p-2">开发文档:</p>
<VbenButton
variant="link"
@click="openWindow('https://doc.iocoder.cn/quick-start/')"
>
项目文档
</VbenButton>
<VbenButton variant="link" @click="openWindow('https://antdv.com/')">
antdv 文档
</VbenButton>
</VbenButtonGroup>
</div>
<p class="mt-2 flex justify-center">
<span>
<Image src="/wx-xingyu.png" alt="数舵科技" />
</span>
</p>
<p class="mt-2 flex justify-center pt-4 text-sm italic">
本项目采用<Tag color="blue">MIT</Tag>开源协议个人与企业可100%
免费使用
</p>
</div>
</Modal>
</template>

View File

@ -1,28 +0,0 @@
import type { RouteRecordRaw } from 'vue-router';
// import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
// {
// meta: {
// icon: 'ic:baseline-view-in-ar',
// keepAlive: true,
// order: 1000,
// title: $t('demos.title'),
// },
// name: 'Demos',
// path: '/demos',
// children: [
// {
// meta: {
// title: $t('demos.antd'),
// },
// name: 'AntDesignDemos',
// path: '/demos/ant-design',
// component: () => import('#/views/demos/antd/index.vue'),
// },
// ],
// },
];
export default routes; // update by 芋艿:不展示

View File

@ -15,14 +15,22 @@ export function getRangePickerDefaultProps(): any {
valueFormat: 'YYYY-MM-DD HH:mm:ss',
format: 'YYYY-MM-DD HH:mm:ss',
placeholder: ['开始时间', '结束时间'],
// prettier-ignore
ranges: {
'今天': [dayjs().startOf('day'), dayjs().endOf('day')],
'昨天': [dayjs().subtract(1, 'day').startOf('day'), dayjs().subtract(1, 'day').endOf('day')],
'本周': [dayjs().startOf('week'), dayjs().endOf('day')],
'本月': [dayjs().startOf('month'), dayjs().endOf('day')],
'最近 7 天': [dayjs().subtract(7, 'day').startOf('day'), dayjs().endOf('day')],
'最近 30 天': [dayjs().subtract(30, 'day').startOf('day'), dayjs().endOf('day')],
: [dayjs().startOf('day'), dayjs().endOf('day')],
: [
dayjs().subtract(1, 'day').startOf('day'),
dayjs().subtract(1, 'day').endOf('day'),
],
: [dayjs().startOf('week'), dayjs().endOf('day')],
: [dayjs().startOf('month'), dayjs().endOf('day')],
'最近 7 天': [
dayjs().subtract(7, 'day').startOf('day'),
dayjs().endOf('day'),
],
'最近 30 天': [
dayjs().subtract(30, 'day').startOf('day'),
dayjs().endOf('day'),
],
},
transformDateFunc: (dates: any) => {
if (dates && dates.length === 2) {

View File

@ -1,17 +1,20 @@
<script setup lang="ts">
import type { SystemUserProfileApi } from '#/api/system/user/profile';
import { Card, Tabs } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import ProfileUser from './modules/profile-user.vue';
import BaseInfo from './modules/base-info.vue';
import ResetPwd from './modules/reset-pwd.vue';
import UserSocial from './modules/user-social.vue';
import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Card, Tabs } from 'ant-design-vue';
import { getUserProfile } from '#/api/system/user/profile';
import { useAuthStore } from '#/store';
import BaseInfo from './modules/base-info.vue';
import ProfileUser from './modules/profile-user.vue';
import ResetPwd from './modules/reset-pwd.vue';
import UserSocial from './modules/user-social.vue';
const authStore = useAuthStore();
const activeName = ref('basicInfo');
@ -46,13 +49,13 @@ onMounted(loadProfile);
<Card class="ml-3 w-3/5">
<Tabs v-model:active-key="activeName" class="-mt-4">
<Tabs.TabPane key="basicInfo" tab="基本设置">
<BaseInfo :profile="profile" @success="refreshProfile" />
<BaseInfo :profile="profile" @success="refreshProfile" />
</Tabs.TabPane>
<Tabs.TabPane key="resetPwd" tab="密码设置">
<ResetPwd />
<ResetPwd />
</Tabs.TabPane>
<Tabs.TabPane key="userSocial" tab="社交绑定" force-render>
<UserSocial @update:active-name="activeName = $event" />
<UserSocial @update:active-name="activeName = $event" />
</Tabs.TabPane>
<!-- TODO @芋艿在线设备 -->
</Tabs>

View File

@ -1,16 +1,21 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { SystemUserProfileApi } from '#/api/system/user/profile';
import { watch } from 'vue';
import { $t } from '@vben/locales';
import { message } from 'ant-design-vue';
import { watch } from 'vue';
import { useVbenForm, z } from '#/adapter/form';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
import { updateUserProfile } from '#/api/system/user/profile';
import { $t } from '@vben/locales';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
const props = defineProps<{ profile?: SystemUserProfileApi.UserProfileRespVO }>();
const props = defineProps<{
profile?: SystemUserProfileApi.UserProfileRespVO;
}>();
const emit = defineEmits<{
(e: 'success'): void;
}>();
@ -87,11 +92,15 @@ async function handleSubmit(values: Recordable<any>) {
}
/** 监听 profile 变化 */
watch(() => props.profile, (newProfile) => {
if (newProfile) {
formApi.setValues(newProfile);
}
}, { immediate: true });
watch(
() => props.profile,
(newProfile) => {
if (newProfile) {
formApi.setValues(newProfile);
}
},
{ immediate: true },
);
</script>
<template>

View File

@ -1,11 +1,12 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import { $t } from '@vben/locales';
import { message } from 'ant-design-vue';
import { useVbenForm, z } from '#/adapter/form';
import { updateUserPassword } from '#/api/system/user/profile';
import { $t } from '@vben/locales';
const [Form, formApi] = useVbenForm({
commonConfig: {

View File

@ -2,26 +2,27 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSocialUserApi } from '#/api/system/social/user';
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { Button, Card, Image, message, Modal } from 'ant-design-vue';
import { computed, ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { $t } from '#/locales';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { socialAuthRedirect } from '#/api/core/auth';
import {
getBindSocialUserList,
socialUnbind,
socialBind,
socialUnbind,
} from '#/api/system/social/user';
import { socialAuthRedirect } from '#/api/core/auth';
import { DICT_TYPE, getDictLabel } from '#/utils/dict';
import { $t } from '#/locales';
import { SystemUserSocialTypeEnum } from '#/utils/constants';
import { DICT_TYPE, getDictLabel } from '#/utils/dict';
const route = useRoute();
const emit = defineEmits<{
(e: 'update:activeName', v: string): void;
}>();
const route = useRoute();
/** 已经绑定的平台 */
const bindList = ref<SystemSocialUserApi.SocialUser[]>([]);
const allBindList = computed<any[]>(() => {
@ -126,8 +127,7 @@ async function onBind(bind: any) {
try {
// redirectUri
// tricky: type encode getUrlValue() 使
const redirectUri =
location.origin + '/profile?' + encodeURIComponent(`type=${type}`);
const redirectUri = `${location.origin}/profile?${encodeURIComponent(`type=${type}`)}`;
//
window.location.href = await socialAuthRedirect(type, redirectUri);

View File

@ -63,7 +63,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber',
componentProps: {
min: 0,
class: 'w-full',
controlsPosition: 'right',
placeholder: '请输入分类排序',
},

View File

@ -50,7 +50,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@ -69,7 +69,7 @@ const [Modal, modalApi] = useVbenModal({
// values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@ -1,18 +1,34 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="审批接入(流程表单)" url="https://doc.iocoder.cn/bpm/use-bpm-form/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<DocAlert
title="审批接入(流程表单)"
url="https://doc.iocoder.cn/bpm/use-bpm-form/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/form/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/form/index pull request
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/form/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/form/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@ -1,18 +1,31 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/group/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/group/index pull request
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/group/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/group/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@ -1,28 +1,242 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type { BpmModelApi } from '#/api/bpm/model';
import { Button } from 'ant-design-vue';
import { onActivated, reactive, ref, useTemplateRef, watch } from 'vue';
import { Page } from '@vben/common-ui';
import { Plus, Search, Settings } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import { refAutoReset } from '@vueuse/core';
import { useSortable } from '@vueuse/integrations/useSortable';
import {
Button,
Card,
Divider,
Dropdown,
Form,
Input,
Menu,
message,
} from 'ant-design-vue';
import {
getCategorySimpleList,
updateCategorySortBatch,
} from '#/api/bpm/category';
import { getModelList } from '#/api/bpm/model';
import CategoryDraggableModel from './modules/category-draggable-model.vue';
//
const modelListSpinning = refAutoReset(false, 3000);
//
const saveSortLoading = ref(false);
// category
const categoryGroup = ref<BpmModelApi.ModelCategoryInfo[]>([]);
//
const originalData = ref<BpmModelApi.ModelCategoryInfo[]>([]);
//
const sortable = useTemplateRef<HTMLElement>('categoryGroupRef');
// 便
const sortableInstance = ref<any>(null);
//
const isCategorySorting = ref(false);
//
const queryParams = reactive({
name: '',
});
//
watch(
() => isCategorySorting.value,
(newValue) => {
if (sortableInstance.value) {
if (newValue) {
//
sortableInstance.value.option('disabled', false);
} else {
//
sortableInstance.value.option('disabled', true);
}
}
},
);
/** 加载数据 */
const getList = async () => {
modelListSpinning.value = true;
try {
const modelList = await getModelList(queryParams.name);
const categoryList = await getCategorySimpleList();
// category
categoryGroup.value = categoryList.map((category: any) => ({
...category,
modelList: modelList.filter(
(model: any) => model.categoryName === category.name,
),
}));
//
sortableInstance.value = null;
} finally {
modelListSpinning.value = false;
}
};
/** 初始化 */
onActivated(() => {
getList();
});
/** 查询方法 */
const handleQuery = () => {
getList();
};
/** 新增模型 */
const createModel = () => {
// TODO
};
/** 处理下拉菜单命令 */
const handleCommand = (command: string) => {
if (command === 'handleCategoryAdd') {
// TODO
} else if (command === 'handleCategorySort') {
originalData.value = cloneDeep(categoryGroup.value);
isCategorySorting.value = true;
//
if (sortableInstance.value) {
//
sortableInstance.value.option('disabled', false);
} else {
sortableInstance.value = useSortable(sortable, categoryGroup, {
disabled: false, //
});
}
}
};
/** 取消分类排序 */
const handleCategorySortCancel = () => {
//
categoryGroup.value = cloneDeep(originalData.value);
isCategorySorting.value = false;
//
if (sortableInstance.value) {
sortableInstance.value.option('disabled', true);
}
};
/** 提交分类排序 */
const handleCategorySortSubmit = async () => {
saveSortLoading.value = true;
try {
//
const ids = categoryGroup.value.map((item: any) => item.id);
await updateCategorySortBatch(ids);
} finally {
saveSortLoading.value = false;
}
message.success('分类排序成功');
isCategorySorting.value = false;
//
await getList();
//
if (sortableInstance.value) {
sortableInstance.value.option('disabled', true);
}
};
</script>
<template>
<Page>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
<Page auto-content-height>
<Card
:body-style="{ padding: '10px' }"
class="mb-4"
v-spinning="modelListSpinning"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/model/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/model/index
代码pull request 贡献给我们
</Button>
<div class="flex items-center justify-between pl-5">
<span class="-mb-4 text-lg font-extrabold">流程模型</span>
<!-- 搜索工作栏 -->
<Form
v-if="!isCategorySorting"
class="-mb-4 mr-2.5 flex"
:model="queryParams"
layout="inline"
>
<Form.Item name="name" class="ml-auto">
<Input
v-model:value="queryParams.name"
placeholder="搜索流程"
allow-clear
@press-enter="handleQuery"
class="!w-60"
>
<template #prefix>
<Search class="mx-2.5" />
</template>
</Input>
</Form.Item>
<!-- 右上角新建模型更多操作 -->
<Form.Item>
<Button type="primary" @click="createModel">
<Plus class="size-5" /> 新建模型
</Button>
</Form.Item>
<Form.Item>
<Dropdown placement="bottomRight">
<Button>
<template #icon>
<Settings class="size-4" />
</template>
</Button>
<template #overlay>
<Menu @click="(e) => handleCommand(e.key as string)">
<Menu.Item key="handleCategoryAdd">
<div class="flex items-center">
<span
class="icon-[ant-design--plus-outlined] mr-1.5 text-[18px]"
></span>
新建分类
</div>
</Menu.Item>
<Menu.Item key="handleCategorySort">
<div class="flex items-center">
<span class="icon-[fa--sort-amount-desc] mr-1.5"></span>
分类排序
</div>
</Menu.Item>
</Menu>
</template>
</Dropdown>
</Form.Item>
</Form>
<div class="-mb-4 mr-6" v-else>
<Button @click="handleCategorySortCancel" class="mr-3">
</Button>
<Button
type="primary"
:loading="saveSortLoading"
@click="handleCategorySortSubmit"
>
保存排序
</Button>
</div>
</div>
<Divider />
<!-- 按照分类展示其所属的模型列表 -->
<div class="px-5" ref="categoryGroupRef">
<CategoryDraggableModel
v-for="element in categoryGroup"
:class="isCategorySorting ? 'cursor-move' : ''"
:key="element.id"
:category-info="element"
:is-category-sorting="isCategorySorting"
@success="getList"
/>
</div>
</Card>
</Page>
</template>

View File

@ -0,0 +1,398 @@
<script lang="ts" setup>
import type { BpmCategoryApi } from '#/api/bpm/category';
import type { BpmModelApi } from '#/api/bpm/model';
import { computed, ref, watchEffect } from 'vue';
import { cloneDeep, formatDateTime, isEqual } from '@vben/utils';
import { useDebounceFn } from '@vueuse/core';
import { useSortable } from '@vueuse/integrations/useSortable';
import {
Button,
Card,
Collapse,
message,
Table,
Tag,
Tooltip,
} from 'ant-design-vue';
import { updateModelSortBatch } from '#/api/bpm/model';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE } from '#/utils/dict';
const props = defineProps<{
categoryInfo: BpmCategoryApi.ModelCategoryInfo;
isCategorySorting: boolean;
}>();
const emit = defineEmits(['success']);
const isModelSorting = ref(false);
const originalData = ref<BpmModelApi.ModelVO[]>([]);
const modelList = ref<BpmModelApi.ModelVO[]>([]);
const isExpand = ref(false);
const tableRef = ref();
// 便
const sortableInstance = ref<any>(null);
/** 解决 v-model 问题,使用计算属性 */
const expandKeys = computed(() => (isExpand.value ? ['1'] : []));
//
const columns = [
{
title: '流程名',
dataIndex: 'name',
key: 'name',
align: 'left' as const,
minWidth: 250,
},
{
title: '可见范围',
dataIndex: 'startUserIds',
key: 'startUserIds',
align: 'center' as const,
minWidth: 150,
},
{
title: '流程类型',
dataIndex: 'type',
key: 'type',
align: 'center' as const,
minWidth: 120,
},
{
title: '表单信息',
dataIndex: 'formType',
key: 'formType',
align: 'center' as const,
minWidth: 150,
},
{
title: '最后发布',
dataIndex: 'deploymentTime',
key: 'deploymentTime',
align: 'center' as const,
minWidth: 250,
},
{
title: '操作',
key: 'operation',
align: 'center' as const,
fixed: 'right' as const,
width: 150,
},
];
/** 处理模型的排序 */
const handleModelSort = () => {
//
originalData.value = cloneDeep(props.categoryInfo.modelList);
//
isExpand.value = true;
isModelSorting.value = true;
//
if (sortableInstance.value) {
//
sortableInstance.value.option('disabled', false);
} else {
const sortableClass = `.category-${props.categoryInfo.id} .ant-table .ant-table-tbody`;
sortableInstance.value = useSortable(sortableClass, modelList, {
disabled: false, //
});
}
};
/** 处理模型的排序提交 */
const handleModelSortSubmit = async () => {
try {
//
const ids = modelList.value.map((item) => item.id);
await updateModelSortBatch(ids);
//
isModelSorting.value = false;
message.success('排序模型成功');
emit('success');
} catch (error) {
console.error('排序保存失败', error);
}
};
/** 处理模型的排序取消 */
const handleModelSortCancel = () => {
//
modelList.value = cloneDeep(originalData.value);
isModelSorting.value = false;
//
if (sortableInstance.value) {
sortableInstance.value.option('disabled', true);
}
};
/** 处理表单详情点击 */
const handleFormDetail = (row: any) => {
// TODO
console.warn('待实现', row);
};
/** 更新 modelList 模型列表 */
const updateModelList = useDebounceFn(() => {
const newModelList = props.categoryInfo.modelList;
if (!isEqual(modelList.value, newModelList)) {
modelList.value = cloneDeep(newModelList);
if (newModelList?.length > 0) {
isExpand.value = true;
}
//
isModelSorting.value = false;
//
sortableInstance.value = null;
}
}, 100);
/** 监听分类信息和排序状态变化 */
watchEffect(() => {
if (props.categoryInfo?.modelList) {
updateModelList();
}
if (props.isCategorySorting) {
isExpand.value = false;
}
});
/** 自定义表格行渲染 */
const customRow = (_record: any) => {
return {
class: isModelSorting.value ? 'cursor-move' : '',
};
};
</script>
<template>
<Card
:body-style="{ padding: 0 }"
class="category-draggable-model mb-5 rounded-lg transition-all duration-300 ease-in-out hover:shadow-xl"
>
<div class="flex h-12 items-center">
<!-- 头部分类名 -->
<div class="flex items-center">
<Tooltip v-if="isCategorySorting" title="拖动排序">
<span
class="icon-[ic--round-drag-indicator] ml-2.5 cursor-move text-2xl text-gray-500"
></span>
</Tooltip>
<div class="ml-4 mr-2 text-lg font-medium">{{ categoryInfo.name }}</div>
<div class="text-gray-500">
({{ categoryInfo.modelList?.length || 0 }})
</div>
</div>
<!-- 头部操作 -->
<div class="flex flex-1 items-center" v-show="!isCategorySorting">
<div
v-if="categoryInfo.modelList.length > 0"
class="ml-3 flex cursor-pointer items-center transition-transform duration-300"
:class="isExpand ? 'rotate-180' : 'rotate-0'"
@click="isExpand = !isExpand"
>
<span
class="icon-[ic--round-expand-more] text-3xl text-gray-400"
></span>
</div>
<div
class="ml-auto flex items-center"
:class="isModelSorting ? 'mr-4' : 'mr-12'"
>
<template v-if="!isModelSorting">
<Button
v-if="categoryInfo.modelList.length > 0"
type="link"
class="mr-5 flex items-center text-[14px]"
@click.stop="handleModelSort"
>
<template #icon>
<span class="icon-[fa--sort-amount-desc] mr-1"></span>
</template>
排序
</Button>
</template>
<template v-else>
<Button @click.stop="handleModelSortCancel" class="mr-2">
</Button>
<Button type="primary" @click.stop="handleModelSortSubmit">
保存排序
</Button>
</template>
</div>
</div>
</div>
<!-- 模型列表 -->
<Collapse :active-key="expandKeys" :bordered="false" class="bg-transparent">
<Collapse.Panel
key="1"
:show-arrow="false"
class="border-0 bg-transparent p-0"
v-show="isExpand"
>
<Table
v-if="modelList && modelList.length > 0"
:class="`category-${categoryInfo.id}`"
ref="tableRef"
:data-source="modelList"
:columns="columns"
:pagination="false"
:custom-row="customRow"
row-key="id"
>
<template #bodyCell="{ column, record }">
<!-- 流程名 -->
<template v-if="column.key === 'name'">
<div class="flex items-center">
<Tooltip v-if="isModelSorting" title="拖动排序">
<span
class="icon-[ic--round-drag-indicator] mr-2.5 cursor-move text-2xl text-gray-500"
></span>
</Tooltip>
<div
v-if="!record.icon"
class="mr-2.5 flex h-9 w-9 items-center justify-center rounded bg-blue-500 text-white"
>
<span style="font-size: 12px">{{
record.name.substring(0, 2)
}}</span>
</div>
<img
v-else
:src="record.icon"
class="mr-2.5 h-9 w-9 rounded"
alt="图标"
/>
{{ record.name }}
</div>
</template>
<!-- 可见范围列-->
<template v-else-if="column.key === 'startUserIds'">
<span
v-if="!record.startUsers?.length && !record.startDepts?.length"
>
全部可见
</span>
<span v-else-if="record.startUsers?.length === 1">
{{ record.startUsers[0].nickname }}
</span>
<span v-else-if="record.startDepts?.length === 1">
{{ record.startDepts[0].name }}
</span>
<span v-else-if="record.startDepts?.length > 1">
<Tooltip
placement="top"
:title="
record.startDepts.map((dept: any) => dept.name).join('、')
"
>
{{ record.startDepts[0].name }}
{{ record.startDepts.length }} 个部门可见
</Tooltip>
</span>
<span v-else-if="record.startUsers?.length > 1">
<Tooltip
placement="top"
:title="
record.startUsers
.map((user: any) => user.nickname)
.join('、')
"
>
{{ record.startUsers[0].nickname }}
{{ record.startUsers.length }} 人可见
</Tooltip>
</span>
</template>
<!-- 流程类型列 -->
<template v-else-if="column.key === 'type'">
<!-- <DictTag :value="record.type" :type="DICT_TYPE.BPM_MODEL_TYPE" /> -->
<!-- <Tag>{{ record.type }}</Tag> -->
<DictTag :type="DICT_TYPE.BPM_MODEL_TYPE" :value="record.type" />
</template>
<!-- 表单信息列 -->
<template v-else-if="column.key === 'formType'">
<!-- TODO BpmModelFormType.NORMAL -->
<Button
v-if="record.formType === 10"
type="link"
@click="handleFormDetail(record)"
>
{{ record.formName }}
</Button>
<!-- TODO BpmModelFormType.CUSTOM -->
<Button
v-else-if="record.formType === 20"
type="link"
@click="handleFormDetail(record)"
>
{{ record.formCustomCreatePath }}
</Button>
<span v-else></span>
</template>
<!-- 最后发布列 -->
<template v-else-if="column.key === 'deploymentTime'">
<div class="flex items-center justify-center">
<span v-if="record.processDefinition" class="w-[150px]">
{{ formatDateTime(record.processDefinition.deploymentTime) }}
</span>
<Tag v-if="record.processDefinition">
v{{ record.processDefinition.version }}
</Tag>
<Tag v-else color="warning">未部署</Tag>
<Tag
v-if="record.processDefinition?.suspensionState === 2"
color="warning"
class="ml-[10px]"
>
已停用
</Tag>
</div>
</template>
<!-- 操作列 -->
<template v-else-if="column.key === 'operation'">
<div class="flex items-center justify-center">待实现</div>
</template>
</template>
</Table>
</Collapse.Panel>
</Collapse>
</Card>
</template>
<style lang="scss" scoped>
.category-draggable-model {
// ant-table-tbody
:deep(.ant-table-tbody > tr > td) {
overflow: hidden;
border-bottom: none;
}
// ant-collapse-header
:deep(.ant-collapse-header) {
padding: 0;
}
//
:deep(.ant-table-tbody) {
transform: translateZ(0);
will-change: transform;
}
//
:deep(.ant-collapse-content-box) {
padding: 0;
}
}
</style>

View File

@ -1,18 +1,34 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="审批接入(业务表单)" url="https://doc.iocoder.cn/bpm/use-business-form/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<DocAlert
title="审批接入(业务表单)"
url="https://doc.iocoder.cn/bpm/use-business-form/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/oa/leave/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/oa/leave/index pull request
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/oa/leave/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/oa/leave/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@ -1,18 +1,31 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="流程表达式" url="https://doc.iocoder.cn/bpm/expression/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processExpression/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processExpression/index pull request
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processExpression/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processExpression/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@ -1,18 +1,34 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<DocAlert
title="流程发起、取消、重新发起"
url="https://doc.iocoder.cn/bpm/process-instance/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/index pull request
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@ -1,18 +1,31 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/manager/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/manager/index pull request
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/manager/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/manager/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@ -1,18 +1,34 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="执行监听器、任务监听器" url="https://doc.iocoder.cn/bpm/listener/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<DocAlert
title="执行监听器、任务监听器"
url="https://doc.iocoder.cn/bpm/listener/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processListener/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processListener/index pull request
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processListener/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processListener/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@ -1,18 +1,34 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="审批转办、委派、抄送" url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<DocAlert
title="审批转办、委派、抄送"
url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/copy/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/copy/index pull request
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/copy/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/copy/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@ -1,21 +1,40 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="审批通过、不通过、驳回" url="https://doc.iocoder.cn/bpm/task-todo-done/" />
<DocAlert
title="审批通过、不通过、驳回"
url="https://doc.iocoder.cn/bpm/task-todo-done/"
/>
<DocAlert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
<DocAlert title="审批转办、委派、抄送" url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" />
<DocAlert
title="审批转办、委派、抄送"
url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
/>
<DocAlert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/done/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/done/index pull request
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/done/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/done/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@ -1,18 +1,31 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/manager/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/manager/index pull request
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/manager/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/manager/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@ -1,21 +1,40 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="审批通过、不通过、驳回" url="https://doc.iocoder.cn/bpm/task-todo-done/" />
<DocAlert
title="审批通过、不通过、驳回"
url="https://doc.iocoder.cn/bpm/task-todo-done/"
/>
<DocAlert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
<DocAlert title="审批转办、委派、抄送" url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" />
<DocAlert
title="审批转办、委派、抄送"
url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
/>
<DocAlert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/todo/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/todo/index pull request
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/todo/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/todo/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@ -0,0 +1,895 @@
import type { Ref } from 'vue';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmContractApi } from '#/api/crm/contract';
import type { CrmReceivableApi } from '#/api/crm/receivable';
import { useAccess } from '@vben/access';
import { DICT_TYPE } from '#/utils/dict';
const { hasAccessByCodes } = useAccess();
export interface LeftSideItem {
name: string;
menu: string;
count: Ref<number>;
}
/** 跟进状态 */
export const FOLLOWUP_STATUS = [
{ label: '待跟进', value: false },
{ label: '已跟进', value: true },
];
/** 归属范围 */
export const SCENE_TYPES = [
{ label: '我负责的', value: 1 },
{ label: '我参与的', value: 2 },
{ label: '下属负责的', value: 3 },
];
/** 联系状态 */
export const CONTACT_STATUS = [
{ label: '今日需联系', value: 1 },
{ label: '已逾期', value: 2 },
{ label: '已联系', value: 3 },
];
/** 审批状态 */
export const AUDIT_STATUS = [
{ label: '待审批', value: 10 },
{ label: '审核通过', value: 20 },
{ label: '审核不通过', value: 30 },
];
/** 回款提醒类型 */
export const RECEIVABLE_REMIND_TYPE = [
{ label: '待回款', value: 1 },
{ label: '已逾期', value: 2 },
{ label: '已回款', value: 3 },
];
/** 合同过期状态 */
export const CONTRACT_EXPIRY_TYPE = [
{ label: '即将过期', value: 1 },
{ label: '已过期', value: 2 },
];
export const useLeftSides = (
customerTodayContactCount: Ref<number>,
clueFollowCount: Ref<number>,
customerFollowCount: Ref<number>,
customerPutPoolRemindCount: Ref<number>,
contractAuditCount: Ref<number>,
contractRemindCount: Ref<number>,
receivableAuditCount: Ref<number>,
receivablePlanRemindCount: Ref<number>,
): LeftSideItem[] => {
return [
{
name: '今日需联系客户',
menu: 'customerTodayContact',
count: customerTodayContactCount,
},
{
name: '分配给我的线索',
menu: 'clueFollow',
count: clueFollowCount,
},
{
name: '分配给我的客户',
menu: 'customerFollow',
count: customerFollowCount,
},
{
name: '待进入公海的客户',
menu: 'customerPutPoolRemind',
count: customerPutPoolRemindCount,
},
{
name: '待审核合同',
menu: 'contractAudit',
count: contractAuditCount,
},
{
name: '待审核回款',
menu: 'receivableAudit',
count: receivableAuditCount,
},
{
name: '待回款提醒',
menu: 'receivablePlanRemind',
count: receivablePlanRemindCount,
},
{
name: '即将到期的合同',
menu: 'contractRemind',
count: contractRemindCount,
},
];
};
/** 分配给我的线索 列表的搜索表单 */
export function useClueFollowFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'followUpStatus',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: FOLLOWUP_STATUS,
},
defaultValue: false,
},
];
}
/** 分配给我的线索 列表的字段 */
export function useClueFollowColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'name',
title: '线索名称',
minWidth: 160,
fixed: 'left',
slots: { default: 'name' },
},
{
field: 'source',
title: '线索来源',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
},
},
{
field: 'mobile',
title: '手机',
minWidth: 120,
},
{
field: 'telephone',
title: '电话',
minWidth: 130,
},
{
field: 'email',
title: '邮箱',
minWidth: 180,
},
{
field: 'detailAddress',
title: '地址',
minWidth: 180,
},
{
field: 'industryId',
title: '客户行业',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
},
},
{
field: 'level',
title: '客户级别',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
},
},
{
field: 'contactNextTime',
title: '下次联系时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'remark',
title: '备注',
minWidth: 200,
},
{
field: 'contactLastTime',
title: '最后跟进时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'contactLastContent',
title: '最后跟进记录',
minWidth: 200,
},
{
field: 'ownerUserName',
title: '负责人',
minWidth: 100,
},
{
field: 'ownerUserDeptName',
title: '所属部门',
minWidth: 100,
},
{
field: 'updateTime',
title: '更新时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'creatorName',
title: '创建人',
minWidth: 100,
},
];
}
/** 合同审核列表的搜索表单 */
export function useContractAuditFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'auditStatus',
label: '合同状态',
component: 'Select',
componentProps: {
allowClear: true,
options: AUDIT_STATUS,
},
defaultValue: 10,
},
];
}
/** 合同提醒列表的搜索表单 */
export function useContractRemindFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'expiryType',
label: '到期状态',
component: 'Select',
componentProps: {
allowClear: true,
options: CONTRACT_EXPIRY_TYPE,
},
defaultValue: 1,
},
];
}
/** 合同审核列表的字段 */
export function useContractColumns<T = CrmContractApi.Contract>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'no',
title: '合同编号',
minWidth: 160,
fixed: 'left',
},
{
field: 'name',
title: '合同名称',
minWidth: 160,
slots: {
default: 'name',
},
},
{
field: 'customerName',
title: '客户名称',
minWidth: 160,
slots: {
default: 'customerName',
},
},
{
field: 'businessName',
title: '商机名称',
minWidth: 160,
slots: {
default: 'businessName',
},
},
{
field: 'price',
title: '合同金额(元)',
minWidth: 120,
formatter: 'formatAmount',
},
{
field: 'orderDate',
title: '下单时间',
minWidth: 120,
formatter: 'formatDateTime',
},
{
field: 'startTime',
title: '合同开始时间',
minWidth: 120,
formatter: 'formatDateTime',
},
{
field: 'endTime',
title: '合同结束时间',
minWidth: 120,
formatter: 'formatDateTime',
},
{
field: 'contactName',
title: '客户签约人',
minWidth: 130,
slots: {
default: 'contactName',
},
},
{
field: 'signUserName',
title: '公司签约人',
minWidth: 130,
},
{
field: 'remark',
title: '备注',
minWidth: 200,
},
{
field: 'totalReceivablePrice',
title: '已回款金额(元)',
minWidth: 140,
formatter: 'formatAmount',
},
{
field: 'noReceivablePrice',
title: '未回款金额(元)',
minWidth: 120,
formatter: 'formatAmount',
},
{
field: 'contactLastTime',
title: '最后跟进时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'ownerUserName',
title: '负责人',
minWidth: 120,
},
{
field: 'ownerUserDeptName',
title: '所属部门',
minWidth: 100,
},
{
field: 'updateTime',
title: '更新时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'creatorName',
title: '创建人',
minWidth: 100,
},
{
field: 'auditStatus',
title: '合同状态',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
},
},
{
field: 'operation',
title: '操作',
minWidth: 130,
align: 'center',
fixed: 'right',
cellRender: {
attrs: {
nameField: 'no',
nameTitle: '合同编号',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'processDetail',
show: hasAccessByCodes(['crm:contract:update']),
},
],
},
},
];
}
/** 客户跟进列表的搜索表单 */
export function useCustomerFollowFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'followUpStatus',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: FOLLOWUP_STATUS,
},
defaultValue: false,
},
];
}
/** 待进入公海客户列表的搜索表单 */
export function useCustomerPutPoolFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'sceneType',
label: '归属',
component: 'Select',
componentProps: {
allowClear: true,
options: SCENE_TYPES,
},
defaultValue: 1,
},
];
}
/** 今日需联系客户列表的搜索表单 */
export function useCustomerTodayContactFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'contactStatus',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: CONTACT_STATUS,
},
defaultValue: 1,
},
{
fieldName: 'sceneType',
label: '归属',
component: 'Select',
componentProps: {
allowClear: true,
options: SCENE_TYPES,
},
defaultValue: 1,
},
];
}
/** 客户列表的字段 */
export function useCustomerColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'name',
title: '客户名称',
minWidth: 160,
slots: {
default: 'name',
},
},
{
field: 'source',
title: '客户来源',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
},
},
{
field: 'mobile',
title: '手机',
minWidth: 120,
},
{
field: 'telephone',
title: '电话',
minWidth: 130,
},
{
field: 'email',
title: '邮箱',
minWidth: 180,
},
{
field: 'level',
title: '客户级别',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
},
},
{
field: 'industryId',
title: '客户行业',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
},
},
{
field: 'contactNextTime',
title: '下次联系时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'remark',
title: '备注',
minWidth: 200,
},
{
field: 'lockStatus',
title: '锁定状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'dealStatus',
title: '成交状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'contactLastTime',
title: '最后跟进时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'contactLastContent',
title: '最后跟进记录',
minWidth: 200,
},
{
field: 'detailAddress',
title: '地址',
minWidth: 200,
},
{
field: 'poolDay',
title: '距离进入公海天数',
minWidth: 180,
},
{
field: 'ownerUserName',
title: '负责人',
minWidth: 100,
},
{
field: 'ownerUserDeptName',
title: '所属部门',
minWidth: 100,
},
{
field: 'updateTime',
title: '更新时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'creatorName',
title: '创建人',
minWidth: 100,
},
];
}
/** 回款审核列表的搜索表单 */
export function useReceivableAuditFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'auditStatus',
label: '合同状态',
component: 'Select',
componentProps: {
allowClear: true,
options: AUDIT_STATUS,
},
defaultValue: 10,
},
];
}
/** 回款审核列表的字段 */
export function useReceivableAuditColumns<T = CrmReceivableApi.Receivable>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'no',
title: '回款编号',
minWidth: 180,
fixed: 'left',
slots: {
default: 'no',
},
},
{
field: 'customerName',
title: '客户名称',
minWidth: 120,
slots: {
default: 'customerName',
},
},
{
field: 'contractNo',
title: '合同编号',
minWidth: 180,
slots: {
default: 'contractNo',
},
},
{
field: 'returnTime',
title: '回款日期',
minWidth: 150,
formatter: 'formatDateTime',
},
{
field: 'price',
title: '回款金额(元)',
minWidth: 140,
formatter: 'formatAmount',
},
{
field: 'returnType',
title: '回款方式',
minWidth: 130,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
},
},
{
field: 'remark',
title: '备注',
minWidth: 200,
},
{
field: 'contract.totalPrice',
title: '合同金额(元)',
minWidth: 140,
formatter: 'formatAmount',
},
{
field: 'ownerUserName',
title: '负责人',
minWidth: 120,
},
{
field: 'ownerUserDeptName',
title: '所属部门',
minWidth: 100,
},
{
field: 'updateTime',
title: '更新时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'creatorName',
title: '创建人',
minWidth: 120,
},
{
field: 'auditStatus',
title: '回款状态',
minWidth: 120,
fixed: 'right',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
},
},
{
field: 'operation',
title: '操作',
width: 140,
fixed: 'right',
align: 'center',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '角色',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'processDetail',
text: '查看审批',
show: hasAccessByCodes(['crm:receivable:update']),
},
],
},
},
];
}
/** 回款计划提醒列表的搜索表单 */
export function useReceivablePlanRemindFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'remindType',
label: '合同状态',
component: 'Select',
componentProps: {
allowClear: true,
options: RECEIVABLE_REMIND_TYPE,
},
defaultValue: 1,
},
];
}
/** 回款计划提醒列表的字段 */
export function useReceivablePlanRemindColumns<T = CrmReceivableApi.Receivable>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'customerName',
title: '客户名称',
minWidth: 160,
fixed: 'left',
slots: {
default: 'customerName',
},
},
{
field: 'contractNo',
title: '合同编号',
minWidth: 200,
},
{
field: 'period',
title: '期数',
minWidth: 160,
slots: {
default: 'period',
},
},
{
field: 'price',
title: '计划回款金额(元)',
minWidth: 120,
formatter: 'formatAmount',
},
{
field: 'returnTime',
title: '计划回款日期',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'remindDays',
title: '提前几天提醒',
minWidth: 150,
},
{
field: 'remindTime',
title: '提醒日期',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'returnType',
title: '回款方式',
minWidth: 120,
fixed: 'right',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
},
},
{
field: 'remark',
title: '备注',
minWidth: 200,
},
{
field: 'ownerUserName',
title: '负责人',
minWidth: 100,
},
{
field: 'receivable.price',
title: '实际回款金额(元)',
minWidth: 160,
formatter: 'formatAmount',
},
{
field: 'receivable.returnTime',
title: '实际回款日期',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'updateTime',
title: '更新时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'creatorName',
title: '创建人',
minWidth: 100,
},
{
field: 'operation',
title: '操作',
width: 140,
fixed: 'right',
align: 'center',
cellRender: {
attrs: {
nameField: 'customerName',
nameTitle: '客户名称',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'receivableForm',
text: '创建回款',
show: hasAccessByCodes(['crm:receivable:create']),
},
],
},
},
];
}

View File

@ -1,34 +1,121 @@
<script lang="ts" setup>
import { computed, onActivated, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { Badge, Card, List } from 'ant-design-vue';
import * as ClueApi from '#/api/crm/clue';
import * as ContractApi from '#/api/crm/contract';
import * as CustomerApi from '#/api/crm/customer';
import * as ReceivableApi from '#/api/crm/receivable';
import * as ReceivablePlanApi from '#/api/crm/receivable/plan';
import { DocAlert } from '#/components/doc-alert';
</script>
import { useLeftSides } from './data';
import ClueFollowList from './modules/ClueFollowList.vue';
import ContractAuditList from './modules/ContractAuditList.vue';
import ContractRemindList from './modules/ContractRemindList.vue';
import CustomerFollowList from './modules/CustomerFollowList.vue';
import CustomerPutPoolRemindList from './modules/CustomerPutPoolRemindList.vue';
import CustomerTodayContactList from './modules/CustomerTodayContactList.vue';
import ReceivableAuditList from './modules/ReceivableAuditList.vue';
import ReceivablePlanRemindList from './modules/ReceivablePlanRemindList.vue';
defineOptions({ name: 'CrmBacklog' });
const leftMenu = ref('customerTodayContact');
const clueFollowCount = ref(0);
const customerFollowCount = ref(0);
const customerPutPoolRemindCount = ref(0);
const customerTodayContactCount = ref(0);
const contractAuditCount = ref(0);
const contractRemindCount = ref(0);
const receivableAuditCount = ref(0);
const receivablePlanRemindCount = ref(0);
const leftSides = useLeftSides(
customerTodayContactCount,
clueFollowCount,
customerFollowCount,
customerPutPoolRemindCount,
contractAuditCount,
contractRemindCount,
receivableAuditCount,
receivablePlanRemindCount,
);
const currentComponent = computed(() => {
const components = {
customerTodayContact: CustomerTodayContactList,
clueFollow: ClueFollowList,
contractAudit: ContractAuditList,
receivableAudit: ReceivableAuditList,
contractRemind: ContractRemindList,
customerFollow: CustomerFollowList,
customerPutPoolRemind: CustomerPutPoolRemindList,
receivablePlanRemind: ReceivablePlanRemindList,
} as const;
return components[leftMenu.value as keyof typeof components];
});
/** 侧边点击 */
function sideClick(item: { menu: string }) {
leftMenu.value = item.menu;
}
/** 获取数量 */
async function getCount() {
customerTodayContactCount.value =
await CustomerApi.getTodayContactCustomerCount();
customerPutPoolRemindCount.value =
await CustomerApi.getPutPoolRemindCustomerCount();
customerFollowCount.value = await CustomerApi.getFollowCustomerCount();
clueFollowCount.value = await ClueApi.getFollowClueCount();
contractAuditCount.value = await ContractApi.getAuditContractCount();
contractRemindCount.value = await ContractApi.getRemindContractCount();
receivableAuditCount.value = await ReceivableApi.getAuditReceivableCount();
receivablePlanRemindCount.value =
await ReceivablePlanApi.getReceivablePlanRemindCount();
}
/** 激活时 */
onActivated(async () => {
getCount();
});
/** 初始化 */
onMounted(async () => {
getCount();
});
</script>
<template>
<Page>
<DocAlert
title="【通用】跟进记录、待办事项"
url="https://doc.iocoder.cn/crm/follow-up/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/backlog/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/backlog/index
代码pull request 贡献给我们
</Button>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【通用】跟进记录、待办事项"
url="https://doc.iocoder.cn/crm/follow-up/"
/>
</template>
<div class="flex h-full w-full">
<Card class="w-1/5">
<List item-layout="horizontal" :data-source="leftSides">
<template #renderItem="{ item }">
<List.Item>
<List.Item.Meta>
<template #title>
<a @click="sideClick(item)"> {{ item.name }} </a>
</template>
</List.Item.Meta>
<template #extra v-if="item.count.value && item.count.value > 0">
<Badge :count="item.count.value" />
</template>
</List.Item>
</template>
</List>
</Card>
<component class="ml-4 w-4/5" :is="currentComponent" />
</div>
</Page>
</template>

View File

@ -0,0 +1,58 @@
<!-- 分配给我的线索 -->
<script lang="ts" setup>
import type { CrmClueApi } from '#/api/crm/clue';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getCluePage } from '#/api/crm/clue';
import { useClueFollowColumns, useClueFollowFormSchema } from '../data';
const { push } = useRouter();
/** 打开线索详情 */
function onDetail(row: CrmClueApi.Clue) {
push({ name: 'CrmClueDetail', params: { id: row.id } });
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useClueFollowFormSchema(),
},
gridOptions: {
columns: useClueFollowColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getCluePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
transformStatus: false,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="线">
<template #name="{ row }">
<Button type="link" @click="onDetail(row)">{{ row.name }}</Button>
</template>
</Grid>
</template>

View File

@ -0,0 +1,111 @@
<!-- 待审核合同 -->
<script lang="ts" setup>
import type { OnActionClickParams } from '#/adapter/vxe-table';
import type { CrmContractApi } from '#/api/crm/contract';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getContractPage } from '#/api/crm/contract';
import { useContractAuditFormSchema, useContractColumns } from '../data';
const { push } = useRouter();
/** 查看审批 */
function openProcessDetail(row: CrmContractApi.Contract) {
push({
name: 'BpmProcessInstanceDetail',
query: { id: row.processInstanceId },
});
}
/** 打开合同详情 */
function openContractDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmContractDetail', params: { id: row.id } });
}
/** 打开客户详情 */
function openCustomerDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
}
/** 打开联系人详情 */
function openContactDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmContactDetail', params: { id: row.id } });
}
/** 打开商机详情 */
function openBusinessDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmBusinessDetail', params: { id: row.id } });
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<CrmContractApi.Contract>) {
switch (code) {
case 'processDetail': {
openProcessDetail(row);
break;
}
}
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useContractAuditFormSchema(),
},
gridOptions: {
columns: useContractColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getContractPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
sceneType: 1, //
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="">
<template #name="{ row }">
<Button type="link" @click="openContractDetail(row)">
{{ row.name }}
</Button>
</template>
<template #customerName="{ row }">
<Button type="link" @click="openCustomerDetail(row)">
{{ row.customerName }}
</Button>
</template>
<template #businessName="{ row }">
<Button type="link" @click="openBusinessDetail(row)">
{{ row.businessName }}
</Button>
</template>
<template #contactName="{ row }">
<Button type="link" @click="openContactDetail(row)">
{{ row.contactName }}
</Button>
</template>
</Grid>
</template>

View File

@ -0,0 +1,111 @@
<!-- 即将到期的合同 -->
<script lang="ts" setup>
import type { OnActionClickParams } from '#/adapter/vxe-table';
import type { CrmContractApi } from '#/api/crm/contract';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getContractPage } from '#/api/crm/contract';
import { useContractColumns, useContractRemindFormSchema } from '../data';
const { push } = useRouter();
/** 查看审批 */
function openProcessDetail(row: CrmContractApi.Contract) {
push({
name: 'BpmProcessInstanceDetail',
query: { id: row.processInstanceId },
});
}
/** 打开合同详情 */
function openContractDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmContractDetail', params: { id: row.id } });
}
/** 打开客户详情 */
function openCustomerDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
}
/** 打开联系人详情 */
function openContactDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmContactDetail', params: { id: row.id } });
}
/** 打开商机详情 */
function openBusinessDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmBusinessDetail', params: { id: row.id } });
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<CrmContractApi.Contract>) {
switch (code) {
case 'processDetail': {
openProcessDetail(row);
break;
}
}
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useContractRemindFormSchema(),
},
gridOptions: {
columns: useContractColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getContractPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
sceneType: 1, //
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="">
<template #name="{ row }">
<Button type="link" @click="openContractDetail(row)">
{{ row.name }}
</Button>
</template>
<template #customerName="{ row }">
<Button type="link" @click="openCustomerDetail(row)">
{{ row.customerName }}
</Button>
</template>
<template #businessName="{ row }">
<Button type="link" @click="openBusinessDetail(row)">
{{ row.businessName }}
</Button>
</template>
<template #contactName="{ row }">
<Button type="link" @click="openContactDetail(row)">
{{ row.contactName }}
</Button>
</template>
</Grid>
</template>

View File

@ -0,0 +1,58 @@
<!-- 分配给我的客户 -->
<script lang="ts" setup>
import type { CrmCustomerApi } from '#/api/crm/customer';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getCustomerPage } from '#/api/crm/customer';
import { useCustomerColumns, useCustomerFollowFormSchema } from '../data';
const { push } = useRouter();
/** 打开客户详情 */
function onDetail(row: CrmCustomerApi.Customer) {
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useCustomerFollowFormSchema(),
},
gridOptions: {
columns: useCustomerColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getCustomerPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
sceneType: 1,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="">
<template #name="{ row }">
<Button type="link" @click="onDetail(row)">{{ row.name }}</Button>
</template>
</Grid>
</template>

View File

@ -0,0 +1,58 @@
<!-- 待进入公海的客户 -->
<script lang="ts" setup>
import type { CrmCustomerApi } from '#/api/crm/customer';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getCustomerPage } from '#/api/crm/customer';
import { useCustomerColumns, useCustomerPutPoolFormSchema } from '../data';
const { push } = useRouter();
/** 打开客户详情 */
function onDetail(row: CrmCustomerApi.Customer) {
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useCustomerPutPoolFormSchema(),
},
gridOptions: {
columns: useCustomerColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getCustomerPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
pool: true, // true
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="">
<template #name="{ row }">
<Button type="link" @click="onDetail(row)">{{ row.name }}</Button>
</template>
</Grid>
</template>

View File

@ -0,0 +1,58 @@
<!-- 今日需联系客户 -->
<script lang="ts" setup>
import type { CrmCustomerApi } from '#/api/crm/customer';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getCustomerPage } from '#/api/crm/customer';
import { useCustomerColumns, useCustomerTodayContactFormSchema } from '../data';
const { push } = useRouter();
/** 打开客户详情 */
function onDetail(row: CrmCustomerApi.Customer) {
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useCustomerTodayContactFormSchema(),
},
gridOptions: {
columns: useCustomerColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getCustomerPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
pool: null, //
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="">
<template #name="{ row }">
<Button type="link" @click="onDetail(row)">{{ row.name }}</Button>
</template>
</Grid>
</template>

View File

@ -0,0 +1,104 @@
<!-- 待审核回款 -->
<script lang="ts" setup>
import type { OnActionClickParams } from '#/adapter/vxe-table';
import type { CrmReceivableApi } from '#/api/crm/receivable';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getReceivablePage } from '#/api/crm/receivable';
import {
useReceivableAuditColumns,
useReceivableAuditFormSchema,
} from '../data';
const { push } = useRouter();
/** 查看审批 */
function openProcessDetail(row: CrmReceivableApi.Receivable) {
push({
name: 'BpmProcessInstanceDetail',
query: { id: row.processInstanceId },
});
}
/** 打开回款详情 */
function openDetail(row: CrmReceivableApi.Receivable) {
push({ name: 'CrmReceivableDetail', params: { id: row.id } });
}
/** 打开客户详情 */
function openCustomerDetail(row: CrmReceivableApi.Receivable) {
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
}
/** 打开合同详情 */
function openContractDetail(row: CrmReceivableApi.Receivable) {
push({ name: 'CrmContractDetail', params: { id: row.contractId } });
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<CrmReceivableApi.Receivable>) {
switch (code) {
case 'processDetail': {
openProcessDetail(row);
break;
}
}
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useReceivableAuditFormSchema(),
},
gridOptions: {
columns: useReceivableAuditColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getReceivablePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="">
<template #no="{ row }">
<Button type="link" @click="openDetail(row)">
{{ row.no }}
</Button>
</template>
<template #customerName="{ row }">
<Button type="link" @click="openCustomerDetail(row)">
{{ row.customerName }}
</Button>
</template>
<template #contractNo="{ row }">
<Button type="link" @click="openContractDetail(row)">
{{ row.contractNo }}
</Button>
</template>
</Grid>
</template>

View File

@ -0,0 +1,90 @@
<!-- 待回款提醒 -->
<script lang="ts" setup>
import type { OnActionClickParams } from '#/adapter/vxe-table';
import type { CrmReceivableApi } from '#/api/crm/receivable';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getReceivablePage } from '#/api/crm/receivable';
import {
useReceivablePlanRemindColumns,
useReceivablePlanRemindFormSchema,
} from '../data';
const { push } = useRouter();
/** 打开回款详情 */
function openDetail(row: CrmReceivableApi.Receivable) {
push({ name: 'CrmReceivableDetail', params: { id: row.id } });
}
/** 打开客户详情 */
function openCustomerDetail(row: CrmReceivableApi.Receivable) {
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
}
/** 创建回款 */
function openReceivableForm(row: CrmReceivableApi.Receivable) {
// Todo:
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<CrmReceivableApi.Receivable>) {
switch (code) {
case 'receivableForm': {
openReceivableForm(row);
break;
}
}
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useReceivablePlanRemindFormSchema(),
},
gridOptions: {
columns: useReceivablePlanRemindColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getReceivablePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="">
<template #customerName="{ row }">
<Button type="link" @click="openCustomerDetail(row)">
{{ row.customerName }}
</Button>
</template>
<template #period="{ row }">
<Button type="link" @click="openDetail(row)">{{ row.period }}</Button>
</template>
</Grid>
</template>

View File

@ -0,0 +1,131 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmBusinessStatusApi } from '#/api/crm/business/status';
import { useAccess } from '@vben/access';
import { getRangePickerDefaultProps } from '@vben/utils';
import { z } from '#/adapter/form';
import { CommonStatusEnum } from '#/utils/constants';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'name',
label: '状态组名',
component: 'Input',
rules: 'required',
},
{
fieldName: 'deptIds',
label: '应用部门',
component: 'TreeSelect',
componentProps: {
multiple: true,
treeCheckable: true,
showCheckedStrategy: 'SHOW_PARENT',
placeholder: '请选择应用部门',
},
},
{
fieldName: 'status',
label: '状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
rules: z.number().default(CommonStatusEnum.ENABLE),
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '状态组名',
component: 'Input',
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns<T = CrmBusinessStatusApi.BusinessStatus>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'name',
title: '状态组名',
minWidth: 200,
},
{
field: 'deptNames',
title: '应用部门',
minWidth: 200,
formatter: ({ cellValue }) => {
return cellValue?.length > 0 ? cellValue.join(' ') : '全公司';
},
},
{
field: 'creator',
title: '创建人',
minWidth: 100,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
width: 160,
fixed: 'right',
align: 'center',
cellRender: {
name: 'TableAction',
props: {
actions: [
{
label: '编辑',
code: 'edit',
show: hasAccessByCodes(['crm:business-status:update']),
},
{
label: '删除',
code: 'delete',
show: hasAccessByCodes(['crm:business-status:delete']),
},
],
onActionClick,
},
},
},
];
}

View File

@ -1,38 +1,137 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { CrmBusinessStatusApi } from '#/api/crm/business/status';
import { Button } from 'ant-design-vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteBusinessStatus,
getBusinessStatusPage,
} from '#/api/crm/business/status';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 创建商机状态 */
function onCreate() {
formModalApi.setData(null).open();
}
/** 删除商机状态 */
async function onDelete(row: CrmBusinessStatusApi.BusinessStatus) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteBusinessStatus(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
onRefresh();
} catch {
hideLoading();
}
}
/** 编辑商机状态 */
function onEdit(row: CrmBusinessStatusApi.BusinessStatus) {
formModalApi.setData(row).open();
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<CrmBusinessStatusApi.BusinessStatus>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getBusinessStatusPage({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<CrmBusinessStatusApi.BusinessStatus>,
});
</script>
<template>
<Page>
<DocAlert
title="【商机】商机管理、商机状态"
url="https://doc.iocoder.cn/crm/business/"
/>
<DocAlert
title="【通用】数据权限"
url="https://doc.iocoder.cn/crm/permission/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/business/status/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/business/status/index
代码pull request 贡献给我们
</Button>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【商机】商机管理、商机状态"
url="https://doc.iocoder.cn/crm/business/"
/>
<DocAlert
title="【通用】数据权限"
url="https://doc.iocoder.cn/crm/permission/"
/>
</template>
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['crm:business-status:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['商机状态']) }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,94 @@
<script lang="ts" setup>
import type { CrmBusinessStatusApi } from '#/api/crm/business/status';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createBusinessStatus,
getBusinessStatus,
updateBusinessStatus,
} from '#/api/crm/business/status';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<CrmBusinessStatusApi.BusinessStatusType>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['商机状态'])
: $t('ui.actionTitle.create', ['商机状态']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data =
(await formApi.getValues()) as CrmBusinessStatusApi.BusinessStatusType;
try {
await (formData.value?.id
? updateBusinessStatus(data)
: createBusinessStatus(data));
//
await modalApi.close();
emit('success');
message.success({
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_process_msg',
});
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
//
const data = modalApi.getData<CrmBusinessStatusApi.BusinessStatusType>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getBusinessStatus(data.id as number);
// values
if (formData.value) {
await formApi.setValues(formData.value);
}
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-1/2">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -1,66 +0,0 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { Button, Card, message, notification, Space } from 'ant-design-vue';
type NotificationType = 'error' | 'info' | 'success' | 'warning';
function info() {
message.info('How many roads must a man walk down');
}
function error() {
message.error({
content: 'Once upon a time you dressed so fine',
duration: 2500,
});
}
function warning() {
message.warning('How many roads must a man walk down');
}
function success() {
message.success('Cause you walked hand in hand With another man in my place');
}
function notify(type: NotificationType) {
notification[type]({
duration: 2500,
message: '说点啥呢',
type,
});
}
</script>
<template>
<Page
description="支持多语言,主题功能集成切换等"
title="Ant Design Vue组件使用演示"
>
<Card class="mb-5" title="按钮">
<Space>
<Button>Default</Button>
<Button type="primary"> Primary </Button>
<Button> Info </Button>
<Button danger> Error </Button>
</Space>
</Card>
<Card class="mb-5" title="Message">
<Space>
<Button @click="info"> </Button>
<Button danger @click="error"> </Button>
<Button @click="warning"> </Button>
<Button @click="success"> </Button>
</Space>
</Card>
<Card class="mb-5" title="Notification">
<Space>
<Button @click="notify('info')"> </Button>
<Button danger @click="notify('error')"> </Button>
<Button @click="notify('warning')"> </Button>
<Button @click="notify('success')"> </Button>
</Space>
</Card>
</Page>
</template>

View File

@ -28,7 +28,7 @@ const [Modal, modalApi] = useVbenModal({
try {
formData.value = data;
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@ -28,7 +28,7 @@ const [Modal, modalApi] = useVbenModal({
try {
formData.value = data;
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@ -210,14 +210,14 @@ initDataSourceConfig();
<DocAlert
title="代码生成(单表)"
url="https://doc.iocoder.cn/new-feature/"
/>
<DocAlert
title="代码生成(树表)"
url="https://doc.iocoder.cn/new-feature/tree/"
/>
<DocAlert
title="代码生成(主子表)"
url="https://doc.iocoder.cn/new-feature/master-sub/"
/>
<DocAlert
title="代码生成(树表)"
url="https://doc.iocoder.cn/new-feature/tree/"
/>
<DocAlert
title="代码生成(主子表)"
url="https://doc.iocoder.cn/new-feature/master-sub/"
/>
<DocAlert title="单元测试" url="https://doc.iocoder.cn/unit-test/" />
</template>

View File

@ -1,9 +1,10 @@
<script lang="ts" setup>
import type { InfraCodegenApi } from '#/api/infra/codegen';
import { useVbenForm } from '#/adapter/form';
import { watch } from 'vue';
import { useVbenForm } from '#/adapter/form';
import { useBasicInfoFormSchema } from '../data';
const props = defineProps<{

View File

@ -2,11 +2,12 @@
import type { InfraCodegenApi } from '#/api/infra/codegen';
import type { SystemDictTypeApi } from '#/api/system/dict/type';
import { nextTick, onMounted, ref, watch } from 'vue';
import { Checkbox, Input, Select } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSimpleDictTypeList } from '#/api/system/dict/type';
import { nextTick, onMounted, ref, watch } from 'vue';
import { useCodegenColumnTableColumns } from '../data';

View File

@ -110,7 +110,7 @@ const [Modal, modalApi] = useVbenModal({
});
} finally {
hideLoading();
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@ -22,6 +22,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@ -46,7 +53,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@ -65,7 +72,7 @@ const [Modal, modalApi] = useVbenModal({
// values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@ -26,6 +26,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@ -53,7 +60,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@ -72,7 +79,7 @@ const [Modal, modalApi] = useVbenModal({
// values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@ -1,7 +1,5 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo01ContactApi } from '#/api/infra/demo/demo01';
import { useAccess } from '@vben/access';

View File

@ -26,6 +26,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@ -52,7 +59,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@ -71,7 +78,7 @@ const [Modal, modalApi] = useVbenModal({
try {
data = await getDemo01Contact(data.id);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
}
// values

View File

@ -1,7 +1,5 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
import { useAccess } from '@vben/access';
@ -36,7 +34,6 @@ export function useFormSchema(): VbenFormSchema[] {
});
return handleTree(data);
},
class: 'w-full',
labelField: 'name',
valueField: 'id',
childrenField: 'children',

View File

@ -31,6 +31,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@ -58,7 +65,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@ -79,7 +86,7 @@ const [Modal, modalApi] = useVbenModal({
try {
data = await getDemo02Category(data.id);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
}
// values

View File

@ -1,7 +1,5 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
import { useAccess } from '@vben/access';

View File

@ -26,6 +26,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useDemo03CourseFormSchema(),
showDefaultActions: false,
@ -54,7 +61,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@ -73,7 +80,7 @@ const [Modal, modalApi] = useVbenModal({
try {
data = await getDemo03Course(data.id);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
}
// values

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