Merge branch 'master' of http://47.109.37.87:3000/by2025/admin-vben5
Some checks are pending
Gitea Actions Demo / Explore-Gitea-Actions (push) Waiting to run
Some checks are pending
Gitea Actions Demo / Explore-Gitea-Actions (push) Waiting to run
# Conflicts: # apps/web-antd/src/views/sis/acAdmin/dp-tree.vue
This commit is contained in:
@@ -42,6 +42,14 @@ export function applicationAdd(data: ApplicationForm) {
|
||||
return requestClient.postWithMsg<void>('/property/application', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 领用审核
|
||||
* @param data
|
||||
*/
|
||||
export function applicationVerified(data: ApplicationForm) {
|
||||
return requestClient.postWithMsg<void>('/property/application/verified', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新资产领用
|
||||
* @param data
|
||||
|
@@ -12,6 +12,22 @@ import { requestClient } from '#/api/request';
|
||||
export function workOrdersTypeList(params?: WorkOrdersTypeQuery) {
|
||||
return requestClient.get<PageResult<WorkOrdersTypeVO>>('/property/workOrdersType/list', { params });
|
||||
}
|
||||
/**
|
||||
* 查询工单类型不分页
|
||||
* @param params
|
||||
* @returns 工单类型管理列表
|
||||
*/
|
||||
export function workOrdersTypeListAll(params?: WorkOrdersTypeQuery) {
|
||||
return requestClient.get<WorkOrdersTypeVO[]>('/property/workOrdersType/queryList', { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工单类型树结构
|
||||
* @param params
|
||||
*/
|
||||
export function workOrdersTypeTree(params?: WorkOrdersTypeQuery) {
|
||||
return requestClient.get<WorkOrdersTypeVO[]>('/property/workOrdersType/typeTree', { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出工单类型管理列表
|
||||
|
@@ -34,7 +34,9 @@ export interface WorkOrdersTypeVO {
|
||||
/**
|
||||
* 是否支持转单(0支持,1不支持)
|
||||
*/
|
||||
isTransfers: number;
|
||||
isTransfers: string;
|
||||
|
||||
excludeId: string;
|
||||
}
|
||||
|
||||
export interface WorkOrdersTypeForm extends BaseEntity {
|
||||
@@ -72,6 +74,11 @@ export interface WorkOrdersTypeForm extends BaseEntity {
|
||||
* 是否支持转单(0支持,1不支持)
|
||||
*/
|
||||
isTransfers?: number;
|
||||
|
||||
/**
|
||||
* 上级类型id
|
||||
*/
|
||||
parentId?: string;
|
||||
}
|
||||
|
||||
export interface WorkOrdersTypeQuery extends PageQuery {
|
||||
@@ -109,4 +116,9 @@ export interface WorkOrdersTypeQuery extends PageQuery {
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any;
|
||||
|
||||
/**
|
||||
* 是否过滤子级
|
||||
*/
|
||||
filterSubNodes?: boolean;
|
||||
}
|
||||
|
@@ -109,6 +109,7 @@ async function setupPackageSelect() {
|
||||
const assets = await assetList({
|
||||
pageNum: 1,
|
||||
pageSize: 1000,
|
||||
params: {stock: 1} //库存不为0
|
||||
});
|
||||
assetsData.value = assets.rows
|
||||
const options = users.rows.map((item) => ({
|
||||
@@ -145,7 +146,8 @@ async function setupPackageSelect() {
|
||||
if (assetInfo) {
|
||||
formApi.updateSchema([{
|
||||
componentProps: {
|
||||
max:assetInfo.stock
|
||||
max: assetInfo.stock,
|
||||
min: 1
|
||||
},
|
||||
fieldName: 'number',
|
||||
}])
|
||||
|
@@ -28,7 +28,7 @@ export const querySchema: FormSchemaGetter = () => [
|
||||
options:getDictOptions(DictEnum.WY_ZCSHZT)
|
||||
},
|
||||
fieldName: 'state',
|
||||
label: '领用状态',
|
||||
label: '审核状态',
|
||||
},
|
||||
// {
|
||||
// component: 'Input',
|
||||
@@ -78,7 +78,7 @@ export const columns: VxeGridProps['columns'] = [
|
||||
field: 'applicationTime',
|
||||
},
|
||||
{
|
||||
title: '领用状态',
|
||||
title: '审核状态',
|
||||
field: 'state',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
|
@@ -14,7 +14,7 @@ import {
|
||||
import {
|
||||
applicationExport,
|
||||
applicationList,
|
||||
applicationRemove, applicationUpdate,
|
||||
applicationRemove, applicationVerified,
|
||||
} from '#/api/property/assetManage/application';
|
||||
import type {ApplicationForm} from '#/api/property/assetManage/application/model';
|
||||
import {commonDownloadExcel} from '#/utils/file/download';
|
||||
@@ -98,7 +98,7 @@ async function handleAudit(row: Required<ApplicationForm>, status: number) {
|
||||
info.state = status
|
||||
info.acceptanceTime = new Date()
|
||||
info.acceptanceUserId = userStore.userInfo?.userId
|
||||
await applicationUpdate(info)
|
||||
await applicationVerified(info)
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import {getDictOptions} from "#/utils/dict";
|
||||
import {renderDict} from "#/utils/render";
|
||||
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
@@ -69,19 +70,24 @@ export const querySchema: FormSchemaGetter = () => [
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
title: '仓库id',
|
||||
field: 'depotId',
|
||||
title: '仓库',
|
||||
field: 'depotName',
|
||||
minWidth:150,
|
||||
},
|
||||
{
|
||||
title: '资产id',
|
||||
field: 'assetId',
|
||||
title: '资产',
|
||||
field: 'assetName',
|
||||
width:180,
|
||||
},
|
||||
{
|
||||
title: '流转类型',
|
||||
field: 'type',
|
||||
width:120,
|
||||
slots:{
|
||||
default:({row})=>{
|
||||
return renderDict(row.type,'wy_cklzlx')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
@@ -95,8 +101,8 @@ export const columns: VxeGridProps['columns'] = [
|
||||
width:150,
|
||||
},
|
||||
{
|
||||
title: '操作人id',
|
||||
field: 'userId',
|
||||
title: '操作人',
|
||||
field: 'userName',
|
||||
width:150,
|
||||
},
|
||||
{
|
||||
|
@@ -7,6 +7,7 @@ import dayjs from 'dayjs';
|
||||
import arrangementModal from './arrangement-modal.vue';
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { arrangementCalender } from '#/api/property/attendanceManagement/arrangement'; // 导入接口
|
||||
import { Modal } from 'ant-design-vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'changeView', value: boolean): void;
|
||||
@@ -19,6 +20,8 @@ const calendarData = reactive<any[]>([]);
|
||||
const loading = ref(false);
|
||||
// 查询日历数据
|
||||
const fetchCalendarData = async (month?: string) => {
|
||||
console.log(123);
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
const params = {
|
||||
@@ -32,47 +35,150 @@ const fetchCalendarData = async (month?: string) => {
|
||||
month ||
|
||||
selectedDate.value?.format('YYYY-MM') ||
|
||||
dayjs().format('YYYY-MM'); //当前月份的开始日期
|
||||
|
||||
// 清空之前的数据
|
||||
calendarData.length = 0;
|
||||
|
||||
// 生成日历渲染数据结构
|
||||
for (const row of res.rows) {
|
||||
if (row.endDate) {
|
||||
const startDate = dayjs(row.startDate);
|
||||
const endDate = dayjs(row.endDate);
|
||||
const currentMonthStart = dayjs(currentMonth).startOf('month');
|
||||
const currentMonthEnd = dayjs(currentMonth).endOf('month');
|
||||
|
||||
//当开始时间小于当前月份的开始日期
|
||||
if (row.startDate <= currentMonth) {
|
||||
console.log(row, '小');
|
||||
if (startDate.isBefore(currentMonthStart) || startDate.isSame(currentMonthStart, 'day')) {
|
||||
console.log(row, '小 - 开始时间小于等于当前月份开始时间');
|
||||
console.log('开始时间:', row.startDate);
|
||||
console.log('结束时间:', row.endDate);
|
||||
console.log('当前月份开始:', currentMonthStart.format('YYYY-MM-DD'));
|
||||
console.log('当前月份结束:', currentMonthEnd.format('YYYY-MM-DD'));
|
||||
|
||||
//如果结束时间小于当前月份的结束时间,则生成当前月份开始时间到row.endDate结束时间的n条数据
|
||||
//如果结束时间大于等于当前月份的结束时间,则生成当前月份开始时间到当前月份结束时间(即当前月份天数)的n条数据
|
||||
} else {
|
||||
//当开始时间大于当前月份的开始日期
|
||||
//如果结束时间小于当前月份的结束时间,则生成开始时间到结束时间的n条数据
|
||||
console.log(row, '大');
|
||||
}
|
||||
} else {
|
||||
|
||||
// 确定结束日期:取row.endDate和当前月份结束日期的较小值
|
||||
const actualEndDate = endDate.isBefore(currentMonthEnd) ? endDate : currentMonthEnd;
|
||||
console.log('实际结束日期:', actualEndDate.format('YYYY-MM-DD'));
|
||||
|
||||
// 生成从当前月份开始到实际结束日期的数据
|
||||
let currentDate = currentMonthStart;
|
||||
let generatedCount = 0;
|
||||
while (currentDate.isSame(actualEndDate, 'day') || currentDate.isBefore(actualEndDate, 'day')) {
|
||||
calendarData.push({
|
||||
data: row.startDate,
|
||||
date: currentDate.format('YYYY-MM-DD'),
|
||||
item: {
|
||||
id: row.id,
|
||||
groupName: row.attendanceGroup.groupName,
|
||||
groupId: row.attendanceGroup.id,
|
||||
startDate: row.startDate,
|
||||
endDate: row.endDate,
|
||||
},
|
||||
});
|
||||
generatedCount++;
|
||||
currentDate = currentDate.add(1, 'day');
|
||||
}
|
||||
console.log(`生成了 ${generatedCount} 条数据`);
|
||||
} else {
|
||||
//当开始时间大于当前月份的开始日期
|
||||
//如果结束时间小于当前月份的结束时间,则生成开始时间到结束时间的n条数据
|
||||
console.log(row, '大 - 开始时间大于当前月份开始时间');
|
||||
console.log('开始时间:', row.startDate);
|
||||
console.log('结束时间:', row.endDate);
|
||||
console.log('当前月份开始:', currentMonthStart.format('YYYY-MM-DD'));
|
||||
console.log('当前月份结束:', currentMonthEnd.format('YYYY-MM-DD'));
|
||||
|
||||
// 确定结束日期:取row.endDate和当前月份结束日期的较小值
|
||||
const actualEndDate = endDate.isBefore(currentMonthEnd) ? endDate : currentMonthEnd;
|
||||
console.log('实际结束日期:', actualEndDate.format('YYYY-MM-DD'));
|
||||
|
||||
// 生成从开始日期到实际结束日期的数据
|
||||
let currentDate = startDate;
|
||||
let generatedCount = 0;
|
||||
while (currentDate.isSame(actualEndDate, 'day') || currentDate.isBefore(actualEndDate, 'day')) {
|
||||
calendarData.push({
|
||||
date: currentDate.format('YYYY-MM-DD'),
|
||||
item: {
|
||||
id: row.id,
|
||||
groupName: row.attendanceGroup.groupName,
|
||||
groupId: row.attendanceGroup.id,
|
||||
startDate: row.startDate,
|
||||
endDate: row.endDate,
|
||||
},
|
||||
});
|
||||
generatedCount++;
|
||||
currentDate = currentDate.add(1, 'day');
|
||||
}
|
||||
console.log(`生成了 ${generatedCount} 条数据`);
|
||||
}
|
||||
} else {
|
||||
// 没有结束日期的情况,只在开始日期添加一条数据
|
||||
calendarData.push({
|
||||
date: row.startDate,
|
||||
item: {
|
||||
id: row.id,
|
||||
groupName: row.attendanceGroup.groupName,
|
||||
groupId: row.attendanceGroup.id,
|
||||
startDate: row.startDate,
|
||||
endDate: row.endDate,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
// if (response) {
|
||||
// calendarData.value = response.data || [];
|
||||
// console.log('日历数据:', calendarData.value);
|
||||
// }
|
||||
console.log('日历数据:', calendarData);
|
||||
//变量calendarData,日期相同的数据放到一个对象中,数据结构为scheduleData
|
||||
|
||||
// 将calendarData按日期分组,形成scheduleData结构
|
||||
const groupedData = new Map<string, any[]>();
|
||||
|
||||
// 遍历calendarData,按日期分组
|
||||
calendarData.forEach(item => {
|
||||
const date = item.date;
|
||||
if (!groupedData.has(date)) {
|
||||
groupedData.set(date, []);
|
||||
}
|
||||
groupedData.get(date)!.push(item.item);
|
||||
});
|
||||
|
||||
// 转换为scheduleData格式
|
||||
scheduleData.length = 0; // 清空原有数据
|
||||
groupedData.forEach((items, date) => {
|
||||
scheduleData.push({
|
||||
date: date,
|
||||
list: items.map(item => ({
|
||||
type: 'success' as BadgeProps['status'],
|
||||
content: item.groupName || '排班安排',
|
||||
item: item
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
console.log('处理后的scheduleData:', scheduleData);
|
||||
|
||||
// 测试:验证生成的日历数据
|
||||
console.log('=== 日历数据验证 ===');
|
||||
console.log('当前月份:', currentMonth);
|
||||
console.log('原始数据条数:', res.rows.length);
|
||||
console.log('生成的日历数据条数:', calendarData.length);
|
||||
console.log('分组后的数据条数:', scheduleData.length);
|
||||
|
||||
// 验证是否有重复日期
|
||||
const dateCounts = new Map<string, number>();
|
||||
calendarData.forEach(item => {
|
||||
const count = dateCounts.get(item.date) || 0;
|
||||
dateCounts.set(item.date, count + 1);
|
||||
});
|
||||
|
||||
console.log('日期分布:', Object.fromEntries(dateCounts));
|
||||
console.log('=== 验证完成 ===');
|
||||
} catch (error) {
|
||||
console.error('获取日历数据失败:', error);
|
||||
message.error('获取日历数据失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 切换视图模式
|
||||
function handleViewModeChange(e: RadioChangeEvent): void {
|
||||
// 将父组件的isCalenderView变为true
|
||||
emit('changeView', e.target.value);
|
||||
}
|
||||
|
||||
// 日历模拟数据
|
||||
const scheduleData: {
|
||||
date: string;
|
||||
list: { type: BadgeProps['status']; content: string }[];
|
||||
@@ -81,18 +187,25 @@ const scheduleData: {
|
||||
{ date: '2025-07-06', list: [{ type: 'success', content: '8月8日事件' }] },
|
||||
// ...
|
||||
];
|
||||
// 切换视图模式
|
||||
function handleViewModeChange(e: RadioChangeEvent): void {
|
||||
// 将父组件的isCalenderView变为true
|
||||
emit('changeView', e.target.value);
|
||||
}
|
||||
|
||||
const getListData2 = (
|
||||
current: Dayjs,
|
||||
): { type: BadgeProps['status']; content: string }[] => {
|
||||
): { type: BadgeProps['status']; content: string; item?: any }[] => {
|
||||
const dateStr = current.format('YYYY-MM-DD');
|
||||
// 优先使用接口数据
|
||||
if (calendarData.length > 0) {
|
||||
const found = calendarData.find((item) => item.date === dateStr);
|
||||
|
||||
// 使用scheduleData结构
|
||||
if (scheduleData.length > 0) {
|
||||
const found = scheduleData.find((item) => item.date === dateStr);
|
||||
if (found) {
|
||||
return found.list || [];
|
||||
return found.list;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到数据,返回空数组
|
||||
return [];
|
||||
};
|
||||
@@ -130,13 +243,8 @@ const getListData = (
|
||||
return listData || [];
|
||||
};
|
||||
|
||||
const getMonthData = (value: Dayjs) => {
|
||||
if (value.month() === 8) {
|
||||
return 1394;
|
||||
}
|
||||
};
|
||||
function customHeader() {
|
||||
// 返回你想要的VNode或null
|
||||
// 返回想要的VNode或null
|
||||
return null; // 什么都不显示
|
||||
}
|
||||
const [ArrangementModal, modalApi] = useVbenModal({
|
||||
@@ -147,6 +255,57 @@ function handleAdd() {
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
// 编辑排班
|
||||
function handleEdit(item: any) {
|
||||
console.log(815328);
|
||||
|
||||
// modalApi.setData({
|
||||
// id: item.id,
|
||||
// groupId: item.groupId,
|
||||
// startDate: item.startDate,
|
||||
// endDate: item.endDate,
|
||||
// });
|
||||
// modalApi.open();
|
||||
}
|
||||
|
||||
// 删除排班
|
||||
function handleDelete(item: any) {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个排班安排吗?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
// 这里需要调用删除接口
|
||||
// await deleteArrangement(item.id);
|
||||
message.success('删除成功');
|
||||
fetchCalendarData();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
message.error('删除失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
function handleViewDetails(item: any) {
|
||||
// 这里可以打开详情弹窗或跳转到详情页面
|
||||
console.log('查看详情:', item);
|
||||
}
|
||||
|
||||
// 获取指定日期的所有排班项目
|
||||
function getScheduleItemsByDate(date: string) {
|
||||
const found = scheduleData.find((item) => item.date === date);
|
||||
return found ? found.list : [];
|
||||
}
|
||||
|
||||
// 查看某日期的所有排班详情
|
||||
function handleViewDateDetails(date: string) {
|
||||
const items = getScheduleItemsByDate(date);
|
||||
console.log(`${date} 的所有排班安排:`, items);
|
||||
// 这里可以打开详情弹窗显示该日期的所有排班
|
||||
}
|
||||
|
||||
// 页面初始化时加载数据
|
||||
onMounted(() => {
|
||||
fetchCalendarData();
|
||||
@@ -169,6 +328,7 @@ onMounted(() => {
|
||||
picker="month"
|
||||
v-model:value="selectedDate"
|
||||
@change="fetchCalendarData()"
|
||||
@select="fetchCalendarData()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,29 +347,32 @@ onMounted(() => {
|
||||
<ul class="events">
|
||||
<li v-for="item in getListData2(current)" :key="item.content">
|
||||
<span>
|
||||
<!-- <Badge :status="item.type" :text="item.content" /> -->
|
||||
<span>{{ item.content }}</span>
|
||||
<a style="margin-left: 4px; color: #1890ff; cursor: pointer"
|
||||
>编辑</a
|
||||
<div class="action-buttons">
|
||||
<a
|
||||
style="margin-left: 4px; color: #1890ff; cursor: pointer"
|
||||
@click.stop="handleEdit(item.item)"
|
||||
>
|
||||
<a style="margin-left: 4px; color: #ff4d4f; cursor: pointer"
|
||||
>删除</a
|
||||
编辑
|
||||
</a>
|
||||
<a
|
||||
style="margin-left: 4px; color: #ff4d4f; cursor: pointer"
|
||||
@click.stop="handleDelete(item.item)"
|
||||
>
|
||||
删除
|
||||
</a>
|
||||
</div>
|
||||
</span>
|
||||
</li>
|
||||
<a
|
||||
v-if="getListData2(current).length > 0"
|
||||
style="margin-left: 4px; color: #1890ff; cursor: pointer"
|
||||
>详情</a
|
||||
@click.stop="handleViewDateDetails(current.format('YYYY-MM-DD'))"
|
||||
>
|
||||
详情
|
||||
</a>
|
||||
</ul>
|
||||
</template>
|
||||
<template #monthCellRender="{ current }">
|
||||
<div v-if="getMonthData(current)" class="notes-month">
|
||||
<section>{{ getMonthData(current) }}</section>
|
||||
<span>Backlog number</span>
|
||||
</div>
|
||||
</template>
|
||||
</Calendar>
|
||||
</div>
|
||||
<ArrangementModal @reload="fetchCalendarData()" />
|
||||
@@ -228,6 +391,32 @@ onMounted(() => {
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
}
|
||||
.events li {
|
||||
margin-bottom: 4px;
|
||||
padding: 4px;
|
||||
/* background-color: #f6ffed; */
|
||||
/* border: 1px solid #b7eb8f; */
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.events li span {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.events li .action-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.events li a {
|
||||
font-size: 11px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.events li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.notes-month {
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
|
@@ -152,8 +152,7 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
{
|
||||
label: '工单类型',
|
||||
fieldName: 'type',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {},
|
||||
component: 'TreeSelect',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
|
@@ -17,7 +17,10 @@ import workOrdersDetail from './work-orders-detail.vue';
|
||||
import ordersModal from './orders-modal.vue';
|
||||
import {columns, querySchema} from './data';
|
||||
import {onMounted, ref} from "vue";
|
||||
import {workOrdersTypeList} from "#/api/property/businessManagement/workOrdersType";
|
||||
import {
|
||||
workOrdersTypeList,
|
||||
workOrdersTypeListAll
|
||||
} from "#/api/property/businessManagement/workOrdersType";
|
||||
|
||||
const ordersTypeList = ref<any[]>([]);
|
||||
const ordersType = ref<string>('0');
|
||||
@@ -130,11 +133,10 @@ function handleMultiDelete() {
|
||||
|
||||
async function queryOrderType() {
|
||||
let params = {
|
||||
pageSize: 1000,
|
||||
pageNum: 1
|
||||
filterSubNodes:true
|
||||
}
|
||||
const res = await workOrdersTypeList(params)
|
||||
ordersTypeList.value = res.rows.map((item) => ({
|
||||
const res = await workOrdersTypeListAll(params)
|
||||
ordersTypeList.value = res.map((item) => ({
|
||||
label: item.orderTypeName,
|
||||
value: item.id,
|
||||
}));
|
||||
|
@@ -14,9 +14,7 @@ import {
|
||||
import {defaultFormValueGetter, useBeforeCloseDiff} from '#/utils/popup';
|
||||
|
||||
import {modalSchema} from './data';
|
||||
import {personList} from "#/api/property/resident/person";
|
||||
import {renderDictValue} from "#/utils/render";
|
||||
import {workOrdersTypeList} from "#/api/property/businessManagement/workOrdersType";
|
||||
import {workOrdersTypeTree} from "#/api/property/businessManagement/workOrdersType";
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
@@ -61,7 +59,7 @@ const [BasicModal, modalApi] = useVbenModal({
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
await queryPersonData()
|
||||
await queryWorkOrdersType()
|
||||
|
||||
const {id} = modalApi.getData() as { id?: number | string };
|
||||
isUpdate.value = !!id;
|
||||
@@ -106,34 +104,35 @@ async function handleClosed() {
|
||||
resetInitialized();
|
||||
}
|
||||
|
||||
async function queryPersonData() {
|
||||
let params = {
|
||||
pageSize: 1000,
|
||||
pageNum: 1,
|
||||
}
|
||||
const res = await personList(params);
|
||||
const options = res.rows.map((user) => ({
|
||||
label: user.userName + '-' + renderDictValue(user.gender, 'sys_user_sex') + '-' + user.phone,
|
||||
value: user.id,
|
||||
}));
|
||||
formApi.updateSchema([{
|
||||
componentProps: () => ({
|
||||
options: options,
|
||||
showSearch:true,
|
||||
filterOption: filterOption
|
||||
}),
|
||||
fieldName: 'initiatorName',
|
||||
},
|
||||
async function queryWorkOrdersType() {
|
||||
const options = await workOrdersTypeTree()
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: () => ({
|
||||
options: options,
|
||||
class: 'w-full',
|
||||
fieldNames: {
|
||||
key: 'id',
|
||||
label: 'orderTypeName',
|
||||
value: 'id',
|
||||
children: 'children',
|
||||
},
|
||||
placeholder: '请选择工单类型',
|
||||
showSearch: true,
|
||||
filterOption: filterOption
|
||||
treeData: options,
|
||||
treeDefaultExpandAll: true,
|
||||
treeLine: { showLeafIcon: false },
|
||||
treeNodeFilterProp: 'orderTypeName',
|
||||
treeNodeLabelProp: 'orderTypeName',
|
||||
}),
|
||||
fieldName: 'handler',
|
||||
}])
|
||||
fieldName: 'type',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@@ -4,11 +4,11 @@ import {renderDict} from "#/utils/render";
|
||||
import {getDictOptions} from "#/utils/dict";
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'orderTypeNo',
|
||||
label: '工单类型编号',
|
||||
},
|
||||
// {
|
||||
// component: 'Input',
|
||||
// fieldName: 'orderTypeNo',
|
||||
// label: '工单类型编号',
|
||||
// },
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'orderTypeName',
|
||||
@@ -25,7 +25,7 @@ export const querySchema: FormSchemaGetter = () => [
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
// { type: 'checkbox', width: 60 },
|
||||
// {
|
||||
// title: '工单类型编号',
|
||||
// field: 'orderTypeNo',
|
||||
@@ -33,6 +33,8 @@ export const columns: VxeGridProps['columns'] = [
|
||||
{
|
||||
title: '类型名称',
|
||||
field: 'orderTypeName',
|
||||
treeNode: true,
|
||||
minWidth:180,
|
||||
},
|
||||
{
|
||||
title: '运作模式',
|
||||
@@ -42,15 +44,17 @@ export const columns: VxeGridProps['columns'] = [
|
||||
return renderDict(row.operationMode, 'pro_operation_pattern');
|
||||
},
|
||||
},
|
||||
minWidth:'120'
|
||||
width:180,
|
||||
},
|
||||
{
|
||||
title: '排序值',
|
||||
field: 'sort',
|
||||
width:180,
|
||||
},
|
||||
{
|
||||
title: '累计工单数量',
|
||||
field: 'number',
|
||||
width:180,
|
||||
},
|
||||
{
|
||||
title: '是否支持转单',
|
||||
@@ -60,7 +64,7 @@ export const columns: VxeGridProps['columns'] = [
|
||||
return renderDict(row.isTransfers, 'support_transferring_orders');
|
||||
},
|
||||
},
|
||||
minWidth:'120'
|
||||
width:180,
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
@@ -87,6 +91,11 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '父级类型',
|
||||
fieldName: 'parentId',
|
||||
component: 'Select',
|
||||
},
|
||||
{
|
||||
label: '运作模式',
|
||||
fieldName: 'operationMode',
|
||||
|
@@ -8,13 +8,14 @@ import {
|
||||
type VxeGridProps
|
||||
} from '#/adapter/vxe-table';
|
||||
import {
|
||||
workOrdersTypeList,
|
||||
workOrdersTypeListAll,
|
||||
workOrdersTypeRemove,
|
||||
} from '#/api/property/businessManagement/workOrdersType';
|
||||
import type { WorkOrdersTypeForm } from '#/api/property/businessManagement/workOrdersType/model';
|
||||
import workOrdersTypeModal from './workOrdersType-modal.vue';
|
||||
import workOrdersTypeDetail from './workOrdersType-detail.vue';
|
||||
import { columns, querySchema } from './data';
|
||||
import {ref} from "vue";
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
commonConfig: {
|
||||
@@ -39,21 +40,27 @@ const gridOptions: VxeGridProps = {
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
pagerConfig: {
|
||||
enabled:false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
return await workOrdersTypeList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
query: async (_, formValues = {}) => {
|
||||
const resp = await workOrdersTypeListAll({
|
||||
...formValues,
|
||||
});
|
||||
return { rows: resp };
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
transform: true,
|
||||
},
|
||||
// 表格全局唯一表示 保存列配置需要用到
|
||||
id: 'property-workOrdersType-index'
|
||||
};
|
||||
@@ -87,6 +94,7 @@ async function handleEdit(row: Required<WorkOrdersTypeForm>) {
|
||||
}
|
||||
|
||||
async function handleDelete(row: Required<WorkOrdersTypeForm>) {
|
||||
console.log(row,'======row')
|
||||
await workOrdersTypeRemove(row.id);
|
||||
await tableApi.query();
|
||||
}
|
||||
@@ -111,14 +119,14 @@ function handleMultiDelete() {
|
||||
<BasicTable table-title="工单类型列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
:disabled="!vxeCheckboxChecked(tableApi)"
|
||||
danger
|
||||
type="primary"
|
||||
v-access:code="['property:workOrdersType:remove']"
|
||||
@click="handleMultiDelete">
|
||||
{{ $t('pages.common.delete') }}
|
||||
</a-button>
|
||||
<!-- <a-button-->
|
||||
<!-- :disabled="!vxeCheckboxChecked(tableApi)"-->
|
||||
<!-- danger-->
|
||||
<!-- type="primary"-->
|
||||
<!-- v-access:code="['property:workOrdersType:remove']"-->
|
||||
<!-- @click="handleMultiDelete">-->
|
||||
<!-- {{ $t('pages.common.delete') }}-->
|
||||
<!-- </a-button>-->
|
||||
<a-button
|
||||
type="primary"
|
||||
v-access:code="['property:workOrdersType:add']"
|
||||
@@ -151,6 +159,7 @@ function handleMultiDelete() {
|
||||
danger
|
||||
v-access:code="['property:workOrdersType:remove']"
|
||||
@click.stop=""
|
||||
:disabled="row.children?.length>0"
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</ghost-button>
|
||||
|
@@ -30,9 +30,9 @@ async function handleOpenChange(open: boolean) {
|
||||
<template>
|
||||
<BasicModal :footer="false" :fullscreen-button="false" title="工单类型信息" class="w-[70%]">
|
||||
<Descriptions v-if="workOrdersTypeInfoDetail" size="small" :column="2" bordered :labelStyle="{width:'120px'}">
|
||||
<DescriptionsItem label="工单类型编号">
|
||||
{{ workOrdersTypeInfoDetail.orderTypeNo }}
|
||||
</DescriptionsItem>
|
||||
<!-- <DescriptionsItem label="工单类型编号">-->
|
||||
<!-- {{ workOrdersTypeInfoDetail.orderTypeNo }}-->
|
||||
<!-- </DescriptionsItem>-->
|
||||
<DescriptionsItem label="类型名称">
|
||||
{{ workOrdersTypeInfoDetail.orderTypeName }}
|
||||
</DescriptionsItem>
|
||||
|
@@ -4,7 +4,11 @@ import { useVbenModal } from '@vben/common-ui';
|
||||
import {$t} from '@vben/locales';
|
||||
import {cloneDeep} from '@vben/utils';
|
||||
import {useVbenForm} from '#/adapter/form';
|
||||
import { workOrdersTypeAdd, workOrdersTypeInfo, workOrdersTypeUpdate } from '#/api/property/businessManagement/workOrdersType';
|
||||
import {
|
||||
workOrdersTypeAdd,
|
||||
workOrdersTypeInfo, workOrdersTypeListAll,
|
||||
workOrdersTypeUpdate
|
||||
} from '#/api/property/businessManagement/workOrdersType';
|
||||
import {defaultFormValueGetter, useBeforeCloseDiff} from '#/utils/popup';
|
||||
import {modalSchema} from './data';
|
||||
|
||||
@@ -40,7 +44,7 @@ const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
// 在这里更改宽度
|
||||
class: 'w-[60%]',
|
||||
class: 'w-[70%]',
|
||||
fullscreenButton: false,
|
||||
onBeforeClose,
|
||||
onClosed: handleClosed,
|
||||
@@ -50,10 +54,9 @@ const [BasicModal, modalApi] = useVbenModal({
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const {id} = modalApi.getData() as { id?: number | string };
|
||||
isUpdate.value = !!id;
|
||||
|
||||
await initTypeOptions(id)
|
||||
if (isUpdate.value && id) {
|
||||
const record = await workOrdersTypeInfo(id);
|
||||
record.operationMode = record.operationMode?.toString();
|
||||
@@ -90,6 +93,25 @@ async function handleClosed() {
|
||||
await formApi.resetForm();
|
||||
resetInitialized();
|
||||
}
|
||||
|
||||
async function initTypeOptions(id:string) {
|
||||
let params = {
|
||||
excludeId:id,
|
||||
filterSubNodes: true //过滤子级
|
||||
}
|
||||
const typeList = await workOrdersTypeListAll(params)
|
||||
formApi.updateSchema([{
|
||||
componentProps: () => ({
|
||||
options: typeList,
|
||||
showSearch: true,
|
||||
optionFilterProp: 'orderTypeName',
|
||||
fieldNames: {label: 'orderTypeName', value: 'id'},
|
||||
allowClear: true,
|
||||
}),
|
||||
fieldName: 'parentId',
|
||||
},
|
||||
]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@@ -32,49 +32,26 @@ export const querySchema: FormSchemaGetter = () => [
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
// {
|
||||
// title: '任务编号',
|
||||
// field: 'id',
|
||||
// width:'auto'
|
||||
// },
|
||||
// {
|
||||
// title: '巡检计划',
|
||||
// field: 'planName',
|
||||
// minWidth:200
|
||||
// },
|
||||
// {
|
||||
// title: '巡检时间范围',
|
||||
// field: 'planInsTime',
|
||||
// width:'auto'
|
||||
//
|
||||
// },
|
||||
// // {
|
||||
// // title: '实际巡检时间',
|
||||
// // field: 'endDate',
|
||||
// // width:180
|
||||
// //
|
||||
// // },
|
||||
// {
|
||||
// title: '签到状态',
|
||||
// field: 'actInsTime',
|
||||
// width:150,
|
||||
// },
|
||||
// {
|
||||
// title: '巡检人',
|
||||
// field: 'planUserName',
|
||||
// width:'auto'
|
||||
//
|
||||
// },
|
||||
// {
|
||||
// title: '巡检方式',
|
||||
// field: 'taskType',
|
||||
// width:'auto',
|
||||
// slots: {
|
||||
// default: ({ row }) => {
|
||||
// return renderDict(row.taskType, 'wy_xjqdfs');
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: '巡检计划',
|
||||
field: 'planId',
|
||||
minWidth:200
|
||||
},
|
||||
{
|
||||
title: '巡检任务',
|
||||
field: 'taskId',
|
||||
width:150
|
||||
},
|
||||
{
|
||||
title: '巡检路线',
|
||||
field: 'routeId',
|
||||
width:150
|
||||
},
|
||||
{
|
||||
title: '巡检点',
|
||||
field: 'pointId',
|
||||
width:150
|
||||
},
|
||||
{
|
||||
title: '签到类型',
|
||||
field: 'signType',
|
||||
@@ -118,7 +95,7 @@ export const columns: VxeGridProps['columns'] = [
|
||||
{
|
||||
title: '备注',
|
||||
field: 'remark',
|
||||
minWidth:120
|
||||
width:180
|
||||
},
|
||||
// {
|
||||
// field: 'action',
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import {type FormSchemaGetter, z} from '#/adapter/form';
|
||||
import {type FormSchemaGetter} from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
@@ -10,8 +9,6 @@ export const querySchema: FormSchemaGetter = () => [
|
||||
},
|
||||
];
|
||||
|
||||
// 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新
|
||||
// export const columns: () => VxeGridProps['columns'] = () => [
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
@@ -61,19 +58,85 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '巡检点',
|
||||
fieldName: 'pointId',
|
||||
component: 'ApiSelect',
|
||||
componentProps:{
|
||||
mode: 'multiple',
|
||||
},
|
||||
rules: z.array(z.string()).min(1, { message: '请选择' }),
|
||||
formItemClass: 'col-span-2',
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
fieldName: 'remark',
|
||||
component: 'Textarea',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
//!!!!!!
|
||||
export const querySchemaPoint: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'pointId',
|
||||
label: '巡检点名称',
|
||||
componentProps: {},
|
||||
},
|
||||
];
|
||||
|
||||
export const columnsPoint: VxeGridProps['columns'] = [
|
||||
{
|
||||
title: '巡检点ID',
|
||||
field: 'pointId',
|
||||
},
|
||||
{
|
||||
title: '巡检点名称',
|
||||
field: 'pointName',
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
|
||||
export const modalSchemaPoint: FormSchemaGetter = () => [
|
||||
{
|
||||
label: '主键id',
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '巡检点名称',
|
||||
fieldName: 'pointId',
|
||||
rules: 'required',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {},
|
||||
},
|
||||
{
|
||||
label: '开始时间',
|
||||
fieldName: 'startTime',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
placeholder: '请选择开始时间',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '结束时间',
|
||||
fieldName: 'endTime',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
placeholder: '请选择开始时间',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '排序',
|
||||
fieldName: 'sort',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
|
@@ -1,14 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import {
|
||||
useVbenVxeGrid,
|
||||
vxeCheckboxChecked,
|
||||
@@ -33,15 +27,6 @@ const formOptions: VbenFormProps = {
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
// 处理区间选择器RangePicker时间格式 将一个字段映射为两个字段 搜索/导出会用到
|
||||
// 不需要直接删除
|
||||
// fieldMappingTime: [
|
||||
// [
|
||||
// 'createTime',
|
||||
// ['params[beginTime]', 'params[endTime]'],
|
||||
// ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
||||
// ],
|
||||
// ],
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
@@ -114,6 +99,12 @@ function handleMultiDelete() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const parentData = ref({})
|
||||
const handleUpdate = (dataSet) => {
|
||||
parentData.value = dataSet; // 更新父组件的数据源
|
||||
console.log(parentData.value,111123);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -163,6 +154,6 @@ function handleMultiDelete() {
|
||||
</Space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<InspectionRouteModal @reload="tableApi.query()" />
|
||||
<InspectionRouteModal @reload="tableApi.query()" @update-data="handleUpdate"/>
|
||||
</Page>
|
||||
</template>
|
||||
|
@@ -4,7 +4,12 @@ import { useVbenModal } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { inspectionRouteAdd, inspectionRouteInfo, inspectionRouteUpdate } from '#/api/property/inspectionManagement/inspectionRoute';
|
||||
import {
|
||||
inspectionRouteAdd,
|
||||
inspectionRouteInfo,
|
||||
inspectionRouteList,
|
||||
inspectionRouteUpdate
|
||||
} from '#/api/property/inspectionManagement/inspectionRoute';
|
||||
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
|
||||
import {
|
||||
inspectionPointList,
|
||||
@@ -20,11 +25,8 @@ const title = computed(() => {
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-1',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 100,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
}
|
||||
@@ -41,8 +43,8 @@ const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
|
||||
},
|
||||
);
|
||||
|
||||
const pointList = ref<any[]>([]);
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
// 在这里更改宽度
|
||||
class: 'w-[60%]',
|
||||
fullscreenButton: false,
|
||||
onBeforeClose,
|
||||
@@ -59,11 +61,15 @@ const [BasicModal, modalApi] = useVbenModal({
|
||||
|
||||
if (isUpdate.value && id) {
|
||||
const record = await inspectionRouteInfo(id);
|
||||
record.pointId = record.pointId?.split(',')
|
||||
pointList.value = (record.inspectionRoutePointVoList || []).map(item => ({
|
||||
pointId: item.pointId ?? '',
|
||||
pointName: item.pointName ?? '',
|
||||
}));
|
||||
await tableApi.reload();
|
||||
console.log(pointList.value,111)
|
||||
await formApi.setValues(record);
|
||||
}
|
||||
await markInitialized();
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
@@ -75,9 +81,11 @@ async function handleConfirm() {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
// getValues获取为一个readonly的对象 需要修改必须先深拷贝一次
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
data.pointId = data.pointId?.join(',')
|
||||
let data = {
|
||||
...cloneDeep(await formApi.getValues()),
|
||||
inspectionRoutePointBoList:[pointData.value]
|
||||
}
|
||||
console.log(data,333)
|
||||
await (isUpdate.value ? inspectionRouteUpdate(data) : inspectionRouteAdd(data));
|
||||
resetInitialized();
|
||||
emit('reload');
|
||||
@@ -104,12 +112,11 @@ async function queryWorkOrdersType() {
|
||||
label: item.pointName,
|
||||
value: item.id,
|
||||
}));
|
||||
formApi.updateSchema([{
|
||||
tableApi.formApi.updateSchema([{
|
||||
componentProps: () => ({
|
||||
options: options,
|
||||
showSearch: true,
|
||||
filterOption: filterOption,
|
||||
mode: 'multiple',
|
||||
}),
|
||||
fieldName: 'pointId',
|
||||
}])
|
||||
@@ -118,11 +125,100 @@ async function queryWorkOrdersType() {
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
|
||||
//!!!!!!
|
||||
import { Page, type VbenFormProps } from '@vben/common-ui';
|
||||
import { Space } from 'ant-design-vue';
|
||||
import {
|
||||
useVbenVxeGrid,
|
||||
type VxeGridProps
|
||||
} from '#/adapter/vxe-table';
|
||||
import dayjs from 'dayjs';
|
||||
import type { InspectionRouteForm } from '#/api/property/inspectionManagement/inspectionRoute/model';
|
||||
import pointModal from './point-modal.vue';
|
||||
import { columnsPoint, querySchemaPoint } from './data';
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
commonConfig: {
|
||||
labelWidth: 90,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
schema: querySchemaPoint(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
reserve: true,
|
||||
},
|
||||
columns: columnsPoint,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
id: 'property-inspectionRoutePoint-index'
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const [PointModal, pointModalApi] = useVbenModal({
|
||||
connectedComponent: pointModal,
|
||||
});
|
||||
|
||||
function handleAdd() {
|
||||
pointModalApi.setData({});
|
||||
pointModalApi.open();
|
||||
}
|
||||
|
||||
async function handleEdit(row: Required<InspectionRouteForm>) {
|
||||
pointModalApi.setData({ id: row.id });
|
||||
pointModalApi.open();
|
||||
}
|
||||
|
||||
const pointData = ref({})
|
||||
const handlePoint = (data) => {
|
||||
data.startTime = dayjs(data.startTime).format('YYYY-MM-DD HH:mm:ss')
|
||||
data.endTime = dayjs(data.endTime).format('YYYY-MM-DD HH:mm:ss')
|
||||
pointData.value = data;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal :title="title">
|
||||
<BasicForm />
|
||||
<Page :auto-content-height="true" style="background-color: #F1F3F6">
|
||||
<BasicTable table-title="巡检点" :grid-options="gridOptions">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
{{ $t('pages.common.add') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Space>
|
||||
<ghost-button
|
||||
@click.stop="handleEdit(row)"
|
||||
>
|
||||
{{ '巡检点' }}
|
||||
</ghost-button>
|
||||
</Space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<PointModal @reload="tableApi.query()" @update-data="handlePoint"/>
|
||||
</Page>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
|
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { inspectionRouteInfo } from '#/api/property/inspectionManagement/inspectionRoute';
|
||||
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
|
||||
import {
|
||||
inspectionPointList,
|
||||
} from '#/api/property/inspectionManagement/inspectionPoint';
|
||||
import { modalSchemaPoint } from './data';
|
||||
import {ref} from "vue";
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
const isUpdate = ref(false);
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 100,
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
}
|
||||
},
|
||||
schema: modalSchemaPoint(),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
|
||||
{
|
||||
initializedGetter: defaultFormValueGetter(formApi),
|
||||
currentGetter: defaultFormValueGetter(formApi),
|
||||
},
|
||||
);
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
class: 'w-[550px]',
|
||||
fullscreenButton: false,
|
||||
onBeforeClose,
|
||||
onClosed: handleClosed,
|
||||
onConfirm: handleConfirm,
|
||||
onOpenChange: async (isOpen) => {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
await queryWorkOrdersType()
|
||||
const { id } = modalApi.getData() as { id?: number | string };
|
||||
isUpdate.value = !!id;
|
||||
|
||||
if (isUpdate.value && id) {
|
||||
const record = await inspectionRouteInfo(id);
|
||||
await formApi.setValues(record);
|
||||
}
|
||||
await markInitialized();
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
async function handleConfirm() {
|
||||
try {
|
||||
modalApi.lock(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
emit('update-data', data);
|
||||
resetInitialized();
|
||||
emit('reload');
|
||||
modalApi.close();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.lock(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClosed() {
|
||||
await formApi.resetForm();
|
||||
resetInitialized();
|
||||
}
|
||||
|
||||
async function queryWorkOrdersType() {
|
||||
let params = {
|
||||
pageSize: 1000,
|
||||
pageNum: 1
|
||||
}
|
||||
const res = await inspectionPointList(params)
|
||||
const options = res.rows.map((item) => ({
|
||||
label: item.pointName,
|
||||
value: item.id,
|
||||
}));
|
||||
formApi.updateSchema([{
|
||||
componentProps: () => ({
|
||||
options: options,
|
||||
showSearch: true,
|
||||
filterOption: filterOption,
|
||||
}),
|
||||
fieldName: 'pointId',
|
||||
}])
|
||||
}
|
||||
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal title="选择巡检点">
|
||||
<BasicForm />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import {
|
||||
addFullName,
|
||||
cloneDeep,
|
||||
getPopupContainer,
|
||||
listToTree,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
categoryAdd,
|
||||
categoryInfo,
|
||||
categoryList,
|
||||
categoryUpdate,
|
||||
} from '#/api/workflow/category';
|
||||
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
|
||||
|
||||
import { modalSchema } from './data';
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const title = computed(() => {
|
||||
return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
|
||||
});
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-2',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 80,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: modalSchema(),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
async function setupCategorySelect() {
|
||||
const listData = await categoryList();
|
||||
const treeData = listToTree(listData, {
|
||||
id: 'categoryId',
|
||||
pid: 'parentId',
|
||||
});
|
||||
addFullName(treeData, 'categoryName', ' / ');
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'parentId',
|
||||
componentProps: {
|
||||
treeData,
|
||||
treeLine: { showLeafIcon: false },
|
||||
fieldNames: { label: 'categoryName', value: 'categoryId' },
|
||||
treeDefaultExpandAll: true,
|
||||
treeNodeLabelProp: 'fullName',
|
||||
getPopupContainer,
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
|
||||
{
|
||||
initializedGetter: defaultFormValueGetter(formApi),
|
||||
currentGetter: defaultFormValueGetter(formApi),
|
||||
},
|
||||
);
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
onBeforeClose,
|
||||
onClosed: handleClosed,
|
||||
onConfirm: handleConfirm,
|
||||
onOpenChange: async (isOpen) => {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { id, parentId } = modalApi.getData() as {
|
||||
id?: number | string;
|
||||
parentId?: number | string;
|
||||
};
|
||||
isUpdate.value = !!id;
|
||||
|
||||
if (isUpdate.value && id) {
|
||||
const record = await categoryInfo(id);
|
||||
await formApi.setValues(record);
|
||||
}
|
||||
if (parentId) {
|
||||
await formApi.setValues({ parentId });
|
||||
}
|
||||
await setupCategorySelect();
|
||||
await markInitialized();
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
async function handleConfirm() {
|
||||
try {
|
||||
modalApi.lock(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
// getValues获取为一个readonly的对象 需要修改必须先深拷贝一次
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
await (isUpdate.value ? categoryUpdate(data) : categoryAdd(data));
|
||||
resetInitialized();
|
||||
emit('reload');
|
||||
modalApi.close();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.lock(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClosed() {
|
||||
await formApi.resetForm();
|
||||
resetInitialized();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal :title="title" class="min-h-[500px]">
|
||||
<BasicForm />
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,69 @@
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
fieldName: 'categoryName',
|
||||
label: '分类名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'categoryCode',
|
||||
label: '分类编码',
|
||||
component: 'Input',
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{
|
||||
field: 'categoryName',
|
||||
title: '分类名称',
|
||||
treeNode: true,
|
||||
},
|
||||
{
|
||||
field: 'orderNum',
|
||||
title: '排序',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
resizable: false,
|
||||
width: 'auto',
|
||||
},
|
||||
];
|
||||
|
||||
export const modalSchema: FormSchemaGetter = () => [
|
||||
{
|
||||
label: 'categoryId',
|
||||
fieldName: 'categoryId',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'parentId',
|
||||
label: '父级分类',
|
||||
rules: 'required',
|
||||
defaultValue: 100,
|
||||
component: 'TreeSelect',
|
||||
},
|
||||
{
|
||||
fieldName: 'categoryName',
|
||||
label: '分类名称',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'orderNum',
|
||||
label: '排序',
|
||||
component: 'InputNumber',
|
||||
},
|
||||
];
|
@@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Popconfirm, Space } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
|
||||
import { categoryList, categoryRemove } from '#/api/workflow/category';
|
||||
|
||||
import categoryModal from './category-modal.vue';
|
||||
import { columns, querySchema } from './data';
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_, formValues = {}) => {
|
||||
const resp = await categoryList({
|
||||
...formValues,
|
||||
});
|
||||
return { rows: resp };
|
||||
},
|
||||
// 默认请求接口后展开全部 不需要可以删除这段
|
||||
querySuccess: () => {
|
||||
nextTick(() => {
|
||||
expandAll();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 虚拟滚动 默认关闭
|
||||
*/
|
||||
scrollY: {
|
||||
enabled: false,
|
||||
gt: 0,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'categoryId',
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'categoryId',
|
||||
transform: true,
|
||||
},
|
||||
// 表格全局唯一表示 保存列配置需要用到
|
||||
id: 'workflow-category-index',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||
const [CategoryModal, modalApi] = useVbenModal({
|
||||
connectedComponent: categoryModal,
|
||||
});
|
||||
|
||||
function handleAdd(row?: Recordable<any>) {
|
||||
modalApi.setData({ parentId: row?.categoryId });
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
async function handleEdit(row: Recordable<any>) {
|
||||
modalApi.setData({ id: row.categoryId });
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
async function handleDelete(row: Recordable<any>) {
|
||||
await categoryRemove(row.categoryId);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
function expandAll() {
|
||||
tableApi.grid?.setAllTreeExpand(true);
|
||||
}
|
||||
|
||||
function collapseAll() {
|
||||
tableApi.grid?.setAllTreeExpand(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<BasicTable table-title="流程分类列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button @click="collapseAll">
|
||||
{{ $t('pages.common.collapse') }}
|
||||
</a-button>
|
||||
<a-button @click="expandAll">
|
||||
{{ $t('pages.common.expand') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
v-access:code="['workflow:category:add']"
|
||||
@click="handleAdd"
|
||||
>
|
||||
{{ $t('pages.common.add') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Space>
|
||||
<ghost-button
|
||||
v-access:code="['workflow:category:edit']"
|
||||
@click.stop="handleEdit(row)"
|
||||
>
|
||||
{{ $t('pages.common.edit') }}
|
||||
</ghost-button>
|
||||
<ghost-button
|
||||
class="btn-success"
|
||||
v-access:code="['workflow:category:edit']"
|
||||
@click.stop="handleAdd(row)"
|
||||
>
|
||||
{{ $t('pages.common.add') }}
|
||||
</ghost-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<ghost-button
|
||||
danger
|
||||
v-access:code="['workflow:category:remove']"
|
||||
@click.stop=""
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</ghost-button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<CategoryModal @reload="tableApi.query()" />
|
||||
</Page>
|
||||
</template>
|
@@ -0,0 +1,149 @@
|
||||
<!-- 流程发起(启动)的弹窗 -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CompleteTaskReqData } from '#/api/workflow/task/model';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { completeTask, getTaskByTaskId } from '#/api/workflow/task';
|
||||
|
||||
import { CopyComponent } from '.';
|
||||
|
||||
interface Emits {
|
||||
/**
|
||||
* 完成
|
||||
*/
|
||||
complete: [];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
interface ModalProps {
|
||||
taskId: string;
|
||||
taskVariables: Record<string, any>;
|
||||
variables?: any; // 这个干啥的
|
||||
}
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '流程发起',
|
||||
fullscreenButton: false,
|
||||
onConfirm: handleSubmit,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
const { taskId } = modalApi.getData() as ModalProps;
|
||||
|
||||
// 查询是否有按钮权限
|
||||
const resp = await getTaskByTaskId(taskId);
|
||||
const buttonPermissions: Record<string, boolean> = {};
|
||||
resp.buttonList.forEach((item) => {
|
||||
buttonPermissions[item.code] = item.show;
|
||||
});
|
||||
|
||||
// 是否具有抄送权限
|
||||
const copyPermission = buttonPermissions?.copy ?? false;
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'flowCopyList',
|
||||
dependencies: {
|
||||
if: copyPermission,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-2',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 100,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'messageType',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '站内信', value: '1', disabled: true },
|
||||
{ label: '邮件', value: '2' },
|
||||
{ label: '短信', value: '3' },
|
||||
],
|
||||
},
|
||||
label: '通知方式',
|
||||
defaultValue: ['1'],
|
||||
},
|
||||
{
|
||||
fieldName: 'attachment',
|
||||
component: 'FileUpload',
|
||||
componentProps: {
|
||||
maxCount: 10,
|
||||
maxSize: 20,
|
||||
accept: 'png, jpg, jpeg, doc, docx, xlsx, xls, ppt, pdf',
|
||||
},
|
||||
defaultValue: [],
|
||||
label: '附件上传',
|
||||
formItemClass: 'items-start',
|
||||
},
|
||||
{
|
||||
fieldName: 'flowCopyList',
|
||||
component: 'Input',
|
||||
defaultValue: [],
|
||||
label: '抄送人',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
const { messageType, flowCopyList, attachment } = cloneDeep(
|
||||
await formApi.getValues(),
|
||||
);
|
||||
const { taskId, taskVariables, variables } =
|
||||
modalApi.getData() as ModalProps;
|
||||
// 需要转换数据 抄送人员
|
||||
const flowCCList = (flowCopyList as Array<any>).map((item) => ({
|
||||
userId: item.userId,
|
||||
userName: item.nickName,
|
||||
}));
|
||||
const requestData = {
|
||||
fileId: attachment.join(','),
|
||||
messageType,
|
||||
flowCopyList: flowCCList,
|
||||
taskId,
|
||||
taskVariables,
|
||||
variables,
|
||||
} as CompleteTaskReqData;
|
||||
await completeTask(requestData);
|
||||
modalApi.close();
|
||||
emit('complete');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<BasicForm>
|
||||
<template #flowCopyList="slotProps">
|
||||
<CopyComponent v-model:user-list="slotProps.modelValue" />
|
||||
</template>
|
||||
</BasicForm>
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { VbenAvatar } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { Descriptions, DescriptionsItem, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { getDiffTimeString } from './helper';
|
||||
|
||||
interface Props extends TaskInfo {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{ info: Props; rowKey?: string }>(), {
|
||||
rowKey: 'id',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ click: [string] }>();
|
||||
|
||||
/**
|
||||
* TODO: 这里要优化 事件没有用到
|
||||
*/
|
||||
function handleClick() {
|
||||
const idKey = props.rowKey as keyof TaskInfo;
|
||||
emit('click', props.info[idKey]);
|
||||
}
|
||||
|
||||
const diffUpdateTimeString = computed(() => {
|
||||
return getDiffTimeString(props.info.updateTime);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'border-primary': info.active,
|
||||
}"
|
||||
class="cursor-pointer rounded-lg border-[1px] border-solid p-3 transition-shadow duration-300 ease-in-out hover:shadow-lg"
|
||||
@click.stop="handleClick"
|
||||
>
|
||||
<Descriptions :column="1" :title="info.flowName" size="middle">
|
||||
<template #extra>
|
||||
<component
|
||||
:is="renderDict(info.flowStatus, DictEnum.WF_BUSINESS_STATUS)"
|
||||
/>
|
||||
</template>
|
||||
<DescriptionsItem label="当前任务">
|
||||
<div class="font-bold">{{ info.nodeName }}</div>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="提交时间">
|
||||
{{ info.createTime }}
|
||||
</DescriptionsItem>
|
||||
<!-- <DescriptionsItem label="更新时间">
|
||||
{{ info.updateTime }}
|
||||
</DescriptionsItem> -->
|
||||
</Descriptions>
|
||||
<div class="flex w-full items-center justify-between text-[14px]">
|
||||
<div class="flex items-center gap-1 overflow-hidden whitespace-nowrap">
|
||||
<VbenAvatar
|
||||
:alt="info.createByName"
|
||||
class="bg-primary size-[24px] rounded-full text-[10px] text-white"
|
||||
src=""
|
||||
/>
|
||||
<span class="overflow-hidden text-ellipsis opacity-50">
|
||||
{{ info.createByName }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-nowrap opacity-50">
|
||||
<Tooltip placement="top" :title="`更新时间: ${info.updateTime}`">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
|
||||
<span>{{ diffUpdateTimeString }}前更新</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.ant-descriptions .ant-descriptions-header) {
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
|
||||
:deep(.ant-descriptions-item) {
|
||||
padding-bottom: 8px !important;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,26 @@
|
||||
<!-- 审批终止 Modal弹窗的content属性专用 用于填写审批意见 -->
|
||||
<script setup lang="ts">
|
||||
import { Textarea } from 'ant-design-vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ApprovalContent',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
defineProps<{ description: string; value: string }>();
|
||||
|
||||
defineEmits<{ 'update:value': [string] }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>{{ description }}</div>
|
||||
<Textarea
|
||||
:allow-clear="true"
|
||||
:auto-size="true"
|
||||
:value="value"
|
||||
placeholder="审批意见(可选)"
|
||||
@change="(e) => $emit('update:value', e.target.value!)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,41 @@
|
||||
<!--
|
||||
审批详情
|
||||
约定${task.formPath}/frame 为内嵌表单 用于展示 需要在本地路由添加
|
||||
apps/web-antd/src/router/routes/workflow-iframe.ts
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FlowInfoResponse } from '#/api/workflow/instance/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { Divider, Skeleton } from 'ant-design-vue';
|
||||
|
||||
import { ApprovalTimeline } from '.';
|
||||
|
||||
defineOptions({
|
||||
name: 'ApprovalDetails',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
defineProps<{
|
||||
currentFlowInfo: FlowInfoResponse;
|
||||
iframeHeight: number;
|
||||
iframeLoaded: boolean;
|
||||
task: TaskInfo;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 约定${task.formPath}/frame 为内嵌表单 用于展示 需要在本地路由添加 -->
|
||||
<iframe
|
||||
v-show="iframeLoaded"
|
||||
:src="`${task.formPath}/iframe?readonly=true&id=${task.businessId}`"
|
||||
:style="{ height: `${iframeHeight}px` }"
|
||||
class="w-full"
|
||||
></iframe>
|
||||
<Skeleton v-show="!iframeLoaded" :paragraph="{ rows: 6 }" active />
|
||||
<Divider />
|
||||
<ApprovalTimeline :list="currentFlowInfo.list" />
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,227 @@
|
||||
<!-- 审批同意的弹窗 -->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type {
|
||||
CompleteTaskReqData,
|
||||
NextNodeInfo,
|
||||
} from '#/api/workflow/task/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { omit } from 'lodash-es';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { completeTask, getNextNodeList } from '#/api/workflow/task';
|
||||
|
||||
import { CopyComponent } from '.';
|
||||
|
||||
const emit = defineEmits<{ complete: [] }>();
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-2',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 100,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'taskId',
|
||||
component: 'Input',
|
||||
label: '任务ID',
|
||||
dependencies: {
|
||||
show: false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'messageType',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '站内信', value: '1', disabled: true },
|
||||
{ label: '邮件', value: '2' },
|
||||
{ label: '短信', value: '3' },
|
||||
],
|
||||
},
|
||||
label: '通知方式',
|
||||
defaultValue: ['1'],
|
||||
},
|
||||
{
|
||||
fieldName: 'attachment',
|
||||
component: 'FileUpload',
|
||||
componentProps: {
|
||||
maxCount: 10,
|
||||
maxSize: 20,
|
||||
accept: 'png, jpg, jpeg, doc, docx, xlsx, xls, ppt, pdf',
|
||||
},
|
||||
defaultValue: [],
|
||||
label: '附件上传',
|
||||
formItemClass: 'items-start',
|
||||
},
|
||||
{
|
||||
fieldName: 'flowCopyList',
|
||||
component: 'Input',
|
||||
defaultValue: [],
|
||||
label: '抄送人',
|
||||
},
|
||||
{
|
||||
fieldName: 'assigneeMap',
|
||||
component: 'Input',
|
||||
label: '下一步审批人',
|
||||
},
|
||||
{
|
||||
fieldName: 'message',
|
||||
component: 'Textarea',
|
||||
label: '审批意见',
|
||||
formItemClass: 'items-start',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
interface ModalProps {
|
||||
taskId: string;
|
||||
// 是否具有抄送权限
|
||||
copyPermission: boolean;
|
||||
// 是有具有选人权限
|
||||
assignPermission: boolean;
|
||||
}
|
||||
|
||||
// 自定义添加选人属性 给组件v-for绑定
|
||||
const nextNodeInfo = ref<(NextNodeInfo & { selectUserList: User[] })[]>([]);
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '审批通过',
|
||||
fullscreenButton: false,
|
||||
class: 'min-h-[365px]',
|
||||
onConfirm: handleSubmit,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
await formApi.resetForm();
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { taskId, copyPermission, assignPermission } =
|
||||
modalApi.getData() as ModalProps;
|
||||
// 是否显示抄送选择
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'flowCopyList',
|
||||
dependencies: {
|
||||
if: copyPermission,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'assigneeMap',
|
||||
dependencies: {
|
||||
if: assignPermission,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 获取下一节点名称
|
||||
if (assignPermission) {
|
||||
const resp = await getNextNodeList({ taskId });
|
||||
nextNodeInfo.value = resp.map((item) => ({
|
||||
...item,
|
||||
// 用于给组件绑定
|
||||
selectUserList: [],
|
||||
}));
|
||||
}
|
||||
|
||||
await formApi.setFieldValue('taskId', taskId);
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
// 需要转换数据 抄送人员
|
||||
const flowCopyList = (data.flowCopyList as Array<any>).map((item) => ({
|
||||
userId: item.userId,
|
||||
userName: item.nickName,
|
||||
}));
|
||||
const requestData = {
|
||||
...omit(data, ['attachment']),
|
||||
fileId: data.attachment.join(','),
|
||||
taskVariables: {},
|
||||
variables: {},
|
||||
flowCopyList,
|
||||
} as CompleteTaskReqData;
|
||||
|
||||
// 选人
|
||||
if (modalApi.getData()?.assignPermission) {
|
||||
// 判断是否选中
|
||||
for (const item of nextNodeInfo.value) {
|
||||
if (item.selectUserList.length === 0) {
|
||||
message.warn(`未选择节点[${item.nodeName}]审批人`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const assigneeMap: { [key: string]: string } = {};
|
||||
nextNodeInfo.value.forEach((item) => {
|
||||
assigneeMap[item.nodeCode] = item.selectUserList
|
||||
.map((u) => u.userId)
|
||||
.join(',');
|
||||
});
|
||||
requestData.assigneeMap = assigneeMap;
|
||||
}
|
||||
|
||||
await completeTask(requestData);
|
||||
modalApi.close();
|
||||
emit('complete');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<BasicForm>
|
||||
<template #flowCopyList="slotProps">
|
||||
<CopyComponent v-model:user-list="slotProps.modelValue" />
|
||||
</template>
|
||||
<template #assigneeMap>
|
||||
<div
|
||||
v-for="item in nextNodeInfo"
|
||||
:key="item.nodeCode"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<template v-if="item.permissionFlag">
|
||||
<span class="opacity-70">{{ item.nodeName }}</span>
|
||||
<CopyComponent
|
||||
:allow-user-ids="item.permissionFlag"
|
||||
v-model:user-list="item.selectUserList"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-red-500">没有权限, 请联系管理员</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</BasicForm>
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,556 @@
|
||||
<!-- 该文件需要重构 但我没空 -->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/core/user';
|
||||
import type { FlowInfoResponse } from '#/api/workflow/instance/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Fallback, useVbenModal, VbenAvatar } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { CopyOutlined } from '@ant-design/icons-vue';
|
||||
import { useClipboard, useEventListener } from '@vueuse/core';
|
||||
import {
|
||||
Card,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Menu,
|
||||
MenuItem,
|
||||
message,
|
||||
Modal,
|
||||
Space,
|
||||
TabPane,
|
||||
Tabs,
|
||||
} from 'ant-design-vue';
|
||||
import { isObject } from 'lodash-es';
|
||||
|
||||
import {
|
||||
cancelProcessApply,
|
||||
deleteByInstanceIds,
|
||||
flowInfo,
|
||||
} from '#/api/workflow/instance';
|
||||
import {
|
||||
getTaskByTaskId,
|
||||
taskOperation,
|
||||
terminationTask,
|
||||
updateAssignee,
|
||||
} from '#/api/workflow/task';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '.';
|
||||
import ApprovalDetails from './approval-details.vue';
|
||||
import FlowPreview from './flow-preview.vue';
|
||||
import { approveWithReasonModal } from './helper';
|
||||
import userSelectModal from './user-select-modal.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ApprovalPanel',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const props = defineProps<{ task?: TaskInfo; type: ApprovalType }>();
|
||||
|
||||
/**
|
||||
* 下面按钮点击后会触发的事件
|
||||
*/
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
const currentTask = ref<TaskInfo>();
|
||||
/**
|
||||
* 是否显示 加签/减签操作
|
||||
*/
|
||||
const showMultiActions = computed(() => {
|
||||
if (!currentTask.value) {
|
||||
return false;
|
||||
}
|
||||
if (Number(currentTask.value.nodeRatio) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
/**
|
||||
* 按钮权限
|
||||
*/
|
||||
const buttonPermissions = computed(() => {
|
||||
const record: Record<string, boolean> = {};
|
||||
if (!currentTask.value) {
|
||||
return record;
|
||||
}
|
||||
currentTask.value.buttonList.forEach((item) => {
|
||||
record[item.code] = item.show;
|
||||
});
|
||||
return record;
|
||||
});
|
||||
|
||||
// 是否显示 `其他` 按钮
|
||||
const showButtonOther = computed(() => {
|
||||
const moreCollections = new Set(['addSign', 'subSign', 'transfer', 'trust']);
|
||||
return Object.keys(buttonPermissions.value).some(
|
||||
(key) => moreCollections.has(key) && buttonPermissions.value[key],
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* myself 我发起的
|
||||
* readonly 只读 只用于查看
|
||||
* approve 审批
|
||||
* admin 流程监控 - 待办任务使用
|
||||
*/
|
||||
type ApprovalType = 'admin' | 'approve' | 'myself' | 'readonly';
|
||||
const showFooter = computed(() => {
|
||||
if (props.type === 'readonly') {
|
||||
return false;
|
||||
}
|
||||
// 我发起的 && [已完成, 已作废] 不显示
|
||||
if (
|
||||
props.type === 'myself' &&
|
||||
['finish', 'invalid'].includes(props.task?.flowStatus ?? '')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const currentFlowInfo = ref<FlowInfoResponse>();
|
||||
/**
|
||||
* card的loading状态
|
||||
*/
|
||||
const loading = ref(false);
|
||||
const iframeLoaded = ref(false);
|
||||
const iframeHeight = ref(300);
|
||||
useEventListener('message', (event) => {
|
||||
const data = event.data as { [key: string]: any; type: string };
|
||||
if (!isObject(data)) return;
|
||||
/**
|
||||
* iframe通信 加载完毕后才显示表单 解决卡顿问题
|
||||
*/
|
||||
if (data.type === 'mounted') {
|
||||
iframeLoaded.value = true;
|
||||
}
|
||||
/**
|
||||
* 高度与表单高度保持一致
|
||||
*/
|
||||
if (data.type === 'height') {
|
||||
const height = data.height;
|
||||
iframeHeight.value = height;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleLoadInfo(task: TaskInfo | undefined) {
|
||||
try {
|
||||
if (!task) return null;
|
||||
loading.value = true;
|
||||
iframeLoaded.value = false;
|
||||
const resp = await flowInfo(task.businessId);
|
||||
currentFlowInfo.value = resp;
|
||||
|
||||
const taskResp = await getTaskByTaskId(props.task!.id);
|
||||
currentTask.value = taskResp;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.task, handleLoadInfo);
|
||||
|
||||
onUnmounted(() => (currentFlowInfo.value = undefined));
|
||||
|
||||
// 进行中 可以撤销
|
||||
const revocable = computed(() => props.task?.flowStatus === 'waiting');
|
||||
async function handleCancel() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要撤销该申请吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await cancelProcessApply({
|
||||
businessId: props.task!.businessId,
|
||||
message: '申请人撤销流程!',
|
||||
});
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可编辑/删除
|
||||
*/
|
||||
const editableAndRemoveable = computed(() => {
|
||||
if (!props.task) {
|
||||
return false;
|
||||
}
|
||||
return ['back', 'cancel', 'draft'].includes(props.task.flowStatus);
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
function handleEdit() {
|
||||
const path = props.task?.formPath;
|
||||
if (path) {
|
||||
router.push({ path, query: { id: props.task!.businessId } });
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemove() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定删除该申请吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await deleteByInstanceIds([props.task!.id]);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批驳回
|
||||
*/
|
||||
const [RejectionModal, rejectionModalApi] = useVbenModal({
|
||||
connectedComponent: approvalRejectionModal,
|
||||
});
|
||||
function handleRejection() {
|
||||
rejectionModalApi.setData({
|
||||
taskId: props.task?.id,
|
||||
definitionId: props.task?.definitionId,
|
||||
nodeCode: props.task?.nodeCode,
|
||||
});
|
||||
rejectionModalApi.open();
|
||||
}
|
||||
/**
|
||||
* 审批终止
|
||||
*/
|
||||
function handleTermination() {
|
||||
approveWithReasonModal({
|
||||
title: '审批终止',
|
||||
description: '确定终止当前审批流程吗?',
|
||||
onOk: async (reason) => {
|
||||
await terminationTask({ taskId: props.task!.id, comment: reason });
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批通过
|
||||
*/
|
||||
const [ApprovalModal, approvalModalApi] = useVbenModal({
|
||||
connectedComponent: approvalModal,
|
||||
});
|
||||
function handleApproval() {
|
||||
// 是否具有抄送权限
|
||||
const copyPermission = buttonPermissions.value?.copy ?? false;
|
||||
// 是否具有选人权限
|
||||
const assignPermission = buttonPermissions.value?.pop ?? false;
|
||||
approvalModalApi.setData({
|
||||
taskId: props.task?.id,
|
||||
copyPermission,
|
||||
assignPermission,
|
||||
});
|
||||
approvalModalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: 1提取公共函数 2原版是可以填写意见的(message参数)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 委托
|
||||
*/
|
||||
const [DelegationModal, delegationModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleDelegation(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
approveWithReasonModal({
|
||||
title: '委托',
|
||||
description: `确定委托给[${current?.nickName}]吗?`,
|
||||
onOk: async (reason) => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userId: current!.userId, message: reason },
|
||||
'delegateTask',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 转办
|
||||
*/
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleTransfer(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
approveWithReasonModal({
|
||||
title: '转办',
|
||||
description: `确定转办给[${current?.nickName}]吗?`,
|
||||
onOk: async (reason) => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userId: current!.userId, message: reason },
|
||||
'transferTask',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [AddSignatureModal, addSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleAddSignature(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认加签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation({ taskId: props.task!.id, userIds }, 'addSignature');
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [ReductionSignatureModal, reductionSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleReductionSignature(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认减签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userIds },
|
||||
'reductionSignature',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 流程干预
|
||||
const [FlowInterfereModal, flowInterfereModalApi] = useVbenModal({
|
||||
connectedComponent: flowInterfereModal,
|
||||
});
|
||||
function handleFlowInterfere() {
|
||||
flowInterfereModalApi.setData({ taskId: props.task?.id });
|
||||
flowInterfereModalApi.open();
|
||||
}
|
||||
|
||||
// 修改办理人
|
||||
const [UpdateAssigneeModal, updateAssigneeModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleUpdateAssignee(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
if (!current) return;
|
||||
Modal.confirm({
|
||||
title: '修改办理人',
|
||||
content: `确定修改办理人为${current?.nickName}吗?`,
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await updateAssignee([props.task!.id], current.userId);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 不加legacy在本地开发没有问题
|
||||
* 打包后在一些设备会无法复制 使用legacy来保证兼容性
|
||||
*/
|
||||
const { copy } = useClipboard({ legacy: true });
|
||||
async function handleCopy(text: string) {
|
||||
await copy(text);
|
||||
message.success('复制成功');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card
|
||||
v-if="task"
|
||||
:body-style="{ overflowY: 'auto', height: '100%' }"
|
||||
:loading="loading"
|
||||
class="thin-scrollbar flex-1 overflow-y-hidden"
|
||||
size="small"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2">
|
||||
<div>编号: {{ task.id }}</div>
|
||||
<CopyOutlined class="cursor-pointer" @click="handleCopy(task.id)" />
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-button size="small" @click="() => handleLoadInfo(task)">
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[material-symbols--refresh] size-24px"></span>
|
||||
</div>
|
||||
</a-button>
|
||||
</template>
|
||||
<div class="flex flex-col gap-5 p-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-2xl font-bold">{{ task.flowName }}</div>
|
||||
<div>
|
||||
<component
|
||||
:is="renderDict(task.flowStatus, DictEnum.WF_BUSINESS_STATUS)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<VbenAvatar
|
||||
:alt="task.createByName"
|
||||
class="bg-primary size-[28px] rounded-full text-white"
|
||||
src=""
|
||||
/>
|
||||
<span>{{ task.createByName }}</span>
|
||||
<div class="flex items-center opacity-50">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[bxs--category-alt] size-[16px]"></span>
|
||||
流程分类: {{ task.categoryName }}
|
||||
</div>
|
||||
<Divider type="vertical" />
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
|
||||
提交时间: {{ task.createTime }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs v-if="currentFlowInfo" class="flex-1">
|
||||
<TabPane key="1" tab="审批详情">
|
||||
<ApprovalDetails
|
||||
:current-flow-info="currentFlowInfo"
|
||||
:iframe-loaded="iframeLoaded"
|
||||
:iframe-height="iframeHeight"
|
||||
:task="task"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane key="2" tab="审批流程图">
|
||||
<FlowPreview :instance-id="currentFlowInfo.instanceId" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
<!-- 固定底部 -->
|
||||
<div class="h-[57px]"></div>
|
||||
<div
|
||||
v-if="showFooter"
|
||||
class="border-t-solid bg-background absolute bottom-0 left-0 w-full border-t-[1px] p-3"
|
||||
>
|
||||
<div class="flex justify-end">
|
||||
<Space v-if="type === 'myself'">
|
||||
<a-button
|
||||
v-if="revocable"
|
||||
danger
|
||||
type="primary"
|
||||
@click="handleCancel"
|
||||
>
|
||||
撤销申请
|
||||
</a-button>
|
||||
<a-button v-if="editableAndRemoveable" @click="handleEdit">
|
||||
重新编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="editableAndRemoveable"
|
||||
danger
|
||||
type="primary"
|
||||
@click="handleRemove"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</Space>
|
||||
<Space v-if="type === 'approve'">
|
||||
<a-button type="primary" @click="handleApproval">通过</a-button>
|
||||
<a-button
|
||||
v-if="buttonPermissions?.termination"
|
||||
danger
|
||||
type="primary"
|
||||
@click="handleTermination"
|
||||
>
|
||||
终止
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="buttonPermissions?.back"
|
||||
danger
|
||||
type="primary"
|
||||
@click="handleRejection"
|
||||
>
|
||||
驳回
|
||||
</a-button>
|
||||
<Dropdown
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<MenuItem
|
||||
v-if="buttonPermissions?.trust"
|
||||
key="1"
|
||||
@click="() => delegationModalApi.open()"
|
||||
>
|
||||
委托
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="buttonPermissions?.transfer"
|
||||
key="2"
|
||||
@click="() => transferModalApi.open()"
|
||||
>
|
||||
转办
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions && buttonPermissions?.addSign"
|
||||
key="3"
|
||||
@click="() => addSignatureModalApi.open()"
|
||||
>
|
||||
加签
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions && buttonPermissions?.subSign"
|
||||
key="4"
|
||||
@click="() => reductionSignatureModalApi.open()"
|
||||
>
|
||||
减签
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
<a-button v-if="showButtonOther"> 其他 </a-button>
|
||||
</Dropdown>
|
||||
<ApprovalModal @complete="$emit('reload')" />
|
||||
<RejectionModal @complete="$emit('reload')" />
|
||||
<DelegationModal mode="single" @finish="handleDelegation" />
|
||||
<TransferModal mode="single" @finish="handleTransfer" />
|
||||
<AddSignatureModal mode="multiple" @finish="handleAddSignature" />
|
||||
<ReductionSignatureModal
|
||||
mode="multiple"
|
||||
@finish="handleReductionSignature"
|
||||
/>
|
||||
</Space>
|
||||
<Space v-if="type === 'admin'">
|
||||
<a-button @click="handleFlowInterfere"> 流程干预 </a-button>
|
||||
<a-button @click="() => updateAssigneeModalApi.open()">
|
||||
修改办理人
|
||||
</a-button>
|
||||
<FlowInterfereModal @complete="$emit('reload')" />
|
||||
<UpdateAssigneeModal mode="single" @finish="handleUpdateAssignee" />
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Fallback v-else title="点击左侧选择" />
|
||||
</template>
|
@@ -0,0 +1,146 @@
|
||||
<!-- 审批驳回窗口 -->
|
||||
<script setup lang="ts">
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { cloneDeep, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { backProcess, getBackTaskNode } from '#/api/workflow/task';
|
||||
|
||||
const emit = defineEmits<{ complete: [] }>();
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-2',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 100,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'taskId',
|
||||
component: 'Input',
|
||||
label: '任务ID',
|
||||
dependencies: {
|
||||
show: false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'messageType',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '站内信', value: '1', disabled: true },
|
||||
{ label: '邮件', value: '2' },
|
||||
{ label: '短信', value: '3' },
|
||||
],
|
||||
},
|
||||
label: '通知方式',
|
||||
defaultValue: ['1'],
|
||||
},
|
||||
{
|
||||
fieldName: 'nodeCode',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
getPopupContainer,
|
||||
},
|
||||
label: '驳回节点',
|
||||
},
|
||||
{
|
||||
fieldName: 'attachment',
|
||||
component: 'FileUpload',
|
||||
componentProps: {
|
||||
maxCount: 10,
|
||||
maxSize: 20,
|
||||
accept: 'png, jpg, jpeg, doc, docx, xlsx, xls, ppt, pdf',
|
||||
},
|
||||
defaultValue: [],
|
||||
label: '附件上传',
|
||||
formItemClass: 'items-start',
|
||||
},
|
||||
{
|
||||
fieldName: 'message',
|
||||
component: 'Textarea',
|
||||
label: '审批意见',
|
||||
formItemClass: 'items-start',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
interface ModalProps {
|
||||
taskId: string;
|
||||
definitionId: string;
|
||||
nodeCode: string;
|
||||
}
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '审批驳回',
|
||||
fullscreenButton: false,
|
||||
class: 'min-h-[365px]',
|
||||
onConfirm: handleSubmit,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
await formApi.resetForm();
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { taskId, definitionId, nodeCode } = modalApi.getData() as ModalProps;
|
||||
await formApi.setFieldValue('taskId', taskId);
|
||||
|
||||
const resp = await getBackTaskNode(definitionId, nodeCode);
|
||||
const options = resp.map((item) => ({
|
||||
label: item.nodeName,
|
||||
value: item.nodeCode,
|
||||
}));
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'nodeCode',
|
||||
componentProps: {
|
||||
options,
|
||||
},
|
||||
},
|
||||
]);
|
||||
// 默认选中第一个节点
|
||||
if (options.length > 0) {
|
||||
formApi.setFieldValue('nodeCode', options[0]?.value);
|
||||
}
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
// 附件join
|
||||
data.fileId = data.attachment?.join?.(',');
|
||||
// 取消attachment参数的传递
|
||||
data.attachment = undefined;
|
||||
await backProcess(data);
|
||||
modalApi.close();
|
||||
emit('complete');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<BasicForm />
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import type { Flow } from '#/api/workflow/instance/model';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { VbenAvatar } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { TimelineItem } from 'ant-design-vue';
|
||||
|
||||
import { ossInfo } from '#/api/system/oss';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
defineOptions({
|
||||
name: 'ApprovalTimelineItem',
|
||||
});
|
||||
|
||||
const props = defineProps<{ item: Flow }>();
|
||||
|
||||
interface AttachmentInfo {
|
||||
ossId: string;
|
||||
url: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理附件信息
|
||||
*/
|
||||
const attachmentInfo = ref<AttachmentInfo[]>([]);
|
||||
onMounted(async () => {
|
||||
if (!props.item.ext) {
|
||||
return null;
|
||||
}
|
||||
const resp = await ossInfo(props.item.ext.split(','));
|
||||
attachmentInfo.value = resp.map((item) => ({
|
||||
ossId: item.ossId,
|
||||
url: item.url,
|
||||
name: item.originalName,
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TimelineItem>
|
||||
<template #dot>
|
||||
<div class="relative rounded-full border">
|
||||
<VbenAvatar
|
||||
:alt="item.approveName"
|
||||
class="bg-primary size-[36px] rounded-full text-white"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ml-2 flex flex-col gap-0.5">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="font-bold">{{ item.nodeName }}</div>
|
||||
<component :is="renderDict(item.flowStatus, DictEnum.WF_TASK_STATUS)" />
|
||||
</div>
|
||||
<div>{{ item.approveName }}</div>
|
||||
<div>{{ item.updateTime }}</div>
|
||||
<div v-if="item.message" class="rounded-lg border p-1">
|
||||
<div class="break-all opacity-70">{{ item.message }}</div>
|
||||
</div>
|
||||
<div v-if="attachmentInfo.length > 0" class="flex flex-wrap gap-2">
|
||||
<!-- 这里下载的文件名不是原始文件名 -->
|
||||
<a
|
||||
v-for="attachment in attachmentInfo"
|
||||
:key="attachment.ossId"
|
||||
:href="attachment.url"
|
||||
class="text-primary"
|
||||
target="_blank"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[mingcute--attachment-line] size-[18px]"></span>
|
||||
<span>{{ attachment.name }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</TimelineItem>
|
||||
</template>
|
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { Flow } from '#/api/workflow/instance/model';
|
||||
|
||||
import { Timeline } from 'ant-design-vue';
|
||||
|
||||
import ApprovalTimelineItem from './approval-timeline-item.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
list: Flow[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Timeline v-if="props.list.length > 0">
|
||||
<ApprovalTimelineItem
|
||||
v-for="item in props.list"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
/>
|
||||
</Timeline>
|
||||
</template>
|
@@ -0,0 +1,98 @@
|
||||
<!--抄送组件-->
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { User } from '#/api/system/user/model';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useVbenModal, VbenAvatar } from '@vben/common-ui';
|
||||
|
||||
import { Avatar, AvatarGroup, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { userSelectModal } from '.';
|
||||
|
||||
defineOptions({
|
||||
name: 'CopyComponent',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ allowUserIds?: string; ellipseNumber?: number }>(),
|
||||
{
|
||||
/**
|
||||
* 最大显示的头像数量 超过显示为省略号头像
|
||||
*/
|
||||
ellipseNumber: 3,
|
||||
/**
|
||||
* 允许选择允许选择的人员ID 会当做参数拼接在uselist接口
|
||||
*/
|
||||
allowUserIds: '',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{ cancel: []; finish: [User[]] }>();
|
||||
|
||||
const [UserSelectModal, modalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
|
||||
const userListModel = defineModel('userList', {
|
||||
type: Array as PropType<User[]>,
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
function handleOpen() {
|
||||
modalApi.setData({ userList: userListModel.value });
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
function handleFinish(userList: User[]) {
|
||||
// 清空 直接赋值[]会丢失响应性
|
||||
userListModel.value.splice(0, userListModel.value.length);
|
||||
userListModel.value.push(...userList);
|
||||
emit('finish', userList);
|
||||
}
|
||||
|
||||
const displayedList = computed(() => {
|
||||
return userListModel.value.slice(0, props.ellipseNumber);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<AvatarGroup v-if="userListModel.length > 0">
|
||||
<Tooltip
|
||||
v-for="user in displayedList"
|
||||
:key="user.userId"
|
||||
:title="user.nickName"
|
||||
placement="top"
|
||||
>
|
||||
<div>
|
||||
<VbenAvatar
|
||||
:alt="user.nickName"
|
||||
class="bg-primary size-[36px] cursor-pointer rounded-full border text-white"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
:title="`等${userListModel.length - props.ellipseNumber}人`"
|
||||
placement="top"
|
||||
>
|
||||
<Avatar
|
||||
v-if="userListModel.length > ellipseNumber"
|
||||
class="flex size-[36px] cursor-pointer items-center justify-center rounded-full border bg-[gray] text-white"
|
||||
>
|
||||
+{{ userListModel.length - props.ellipseNumber }}
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
</AvatarGroup>
|
||||
<a-button size="small" @click="handleOpen">选择人员</a-button>
|
||||
<UserSelectModal
|
||||
:allow-user-ids="allowUserIds"
|
||||
@cancel="$emit('cancel')"
|
||||
@finish="handleFinish"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { useAppConfig, useTabs } from '@vben/hooks';
|
||||
import { stringify } from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
|
||||
defineOptions({ name: 'FlowDesigner' });
|
||||
|
||||
const route = useRoute();
|
||||
const definitionId = route.query.definitionId as string;
|
||||
const disabled = route.query.disabled === 'true';
|
||||
|
||||
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const params = {
|
||||
Authorization: `Bearer ${accessStore.accessToken}`,
|
||||
id: definitionId,
|
||||
clientid: clientId,
|
||||
disabled,
|
||||
};
|
||||
|
||||
/**
|
||||
* iframe设计器的地址
|
||||
*/
|
||||
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
|
||||
|
||||
const { closeCurrentTab } = useTabs();
|
||||
const router = useRouter();
|
||||
|
||||
function messageHandler(event: MessageEvent) {
|
||||
switch (event.data.method) {
|
||||
case 'close': {
|
||||
// 关闭当前tab
|
||||
closeCurrentTab();
|
||||
// 跳转到流程定义列表
|
||||
router.push('/workflow/processDefinition');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// iframe监听组件内设计器保存事件
|
||||
useEventListener('message', messageHandler);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<iframe :src="url" class="size-full"></iframe>
|
||||
</template>
|
@@ -0,0 +1,38 @@
|
||||
<!-- 弹窗查看流程信息 -->
|
||||
<script setup lang="ts">
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { getTaskByBusinessId } from '#/api/workflow/instance';
|
||||
|
||||
import { ApprovalPanel } from '.';
|
||||
|
||||
interface ModalProps {
|
||||
businessId: string;
|
||||
}
|
||||
|
||||
const taskInfo = ref<TaskInfo>();
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '流程信息',
|
||||
class: 'w-[1000px]',
|
||||
footer: false,
|
||||
onOpenChange: async (isOpen) => {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
const { businessId } = modalApi.getData() as ModalProps;
|
||||
const taskResp = await getTaskByBusinessId(businessId);
|
||||
taskInfo.value = taskResp;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<ApprovalPanel :task="taskInfo" type="readonly" />
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,171 @@
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Descriptions, DescriptionsItem, Modal } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getTaskByTaskId,
|
||||
taskOperation,
|
||||
terminationTask,
|
||||
} from '#/api/workflow/task';
|
||||
|
||||
import { userSelectModal } from '.';
|
||||
|
||||
const emit = defineEmits<{ complete: [] }>();
|
||||
|
||||
const taskInfo = ref<TaskInfo>();
|
||||
|
||||
/**
|
||||
* 是否显示 加签/减签操作
|
||||
*/
|
||||
const showMultiActions = computed(() => {
|
||||
if (!taskInfo.value) {
|
||||
return false;
|
||||
}
|
||||
if (Number(taskInfo.value.nodeRatio) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '流程干预',
|
||||
class: 'w-[800px]',
|
||||
fullscreenButton: false,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
const { taskId } = modalApi.getData() as { taskId: string };
|
||||
taskInfo.value = await getTaskByTaskId(taskId);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 转办
|
||||
*/
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleTransfer(userList: User[]) {
|
||||
if (userList.length === 0 || !taskInfo.value) return;
|
||||
const current = userList[0];
|
||||
Modal.confirm({
|
||||
title: '转办',
|
||||
content: `确定转办给${current?.nickName}吗?`,
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: taskInfo.value!.id, userId: current!.userId },
|
||||
'transferTask',
|
||||
);
|
||||
emit('complete');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批终止
|
||||
*/
|
||||
function handleTermination() {
|
||||
if (!taskInfo.value) {
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '审批终止',
|
||||
content: '确定终止当前审批流程吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await terminationTask({ taskId: taskInfo.value!.id });
|
||||
emit('complete');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [AddSignatureModal, addSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleAddSignature(userList: User[]) {
|
||||
if (userList.length === 0 || !taskInfo.value) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认加签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: taskInfo.value!.id, userIds },
|
||||
'addSignature',
|
||||
);
|
||||
emit('complete');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [ReductionSignatureModal, reductionSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleReductionSignature(userList: User[]) {
|
||||
if (userList.length === 0 || !taskInfo.value) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认减签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: taskInfo.value!.id, userIds },
|
||||
'reductionSignature',
|
||||
);
|
||||
emit('complete');
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<Descriptions v-if="taskInfo" :column="2" bordered size="small">
|
||||
<DescriptionsItem label="任务名称">
|
||||
{{ taskInfo.nodeName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="节点编码">
|
||||
{{ taskInfo.nodeCode }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="开始时间">
|
||||
{{ taskInfo.createTime }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="流程实例ID">
|
||||
{{ taskInfo.instanceId }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="版本号">
|
||||
{{ taskInfo.version }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="业务ID">
|
||||
{{ taskInfo.businessId }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
<TransferModal mode="single" @finish="handleTransfer" />
|
||||
<AddSignatureModal mode="multiple" @finish="handleAddSignature" />
|
||||
<ReductionSignatureModal
|
||||
mode="multiple"
|
||||
@finish="handleReductionSignature"
|
||||
/>
|
||||
<template #footer>
|
||||
<template v-if="showMultiActions">
|
||||
<a-button @click="() => addSignatureModalApi.open()">加签</a-button>
|
||||
<a-button @click="() => reductionSignatureModalApi.open()">
|
||||
减签
|
||||
</a-button>
|
||||
</template>
|
||||
<a-button @click="() => transferModalApi.open()">转办</a-button>
|
||||
<a-button danger type="primary" @click="handleTermination">终止</a-button>
|
||||
</template>
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { stringify } from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
defineOptions({ name: 'FlowPreview' });
|
||||
|
||||
const props = defineProps<{ instanceId: string }>();
|
||||
|
||||
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const params = {
|
||||
Authorization: `Bearer ${accessStore.accessToken}`,
|
||||
id: props.instanceId,
|
||||
clientid: clientId,
|
||||
type: 'FlowChart',
|
||||
};
|
||||
|
||||
/**
|
||||
* iframe地址
|
||||
*/
|
||||
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<iframe :src="url" class="h-[500px] w-full border"></iframe>
|
||||
</template>
|
@@ -0,0 +1,60 @@
|
||||
import { defineComponent, h, ref } from 'vue';
|
||||
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
import ApprovalContent from './approval-content.vue';
|
||||
|
||||
export interface ApproveWithReasonModalProps {
|
||||
title: string;
|
||||
description: string;
|
||||
onOk: (reason: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 带审批意见的confirm
|
||||
* @param props props
|
||||
*/
|
||||
export function approveWithReasonModal(props: ApproveWithReasonModalProps) {
|
||||
const { onOk, title, description } = props;
|
||||
const content = ref('');
|
||||
Modal.confirm({
|
||||
title,
|
||||
content: h(
|
||||
defineComponent({
|
||||
setup() {
|
||||
return () =>
|
||||
h(ApprovalContent, {
|
||||
description,
|
||||
value: content.value,
|
||||
'onUpdate:value': (v) => (content.value = v),
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => onOk(content.value),
|
||||
});
|
||||
}
|
||||
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
/**
|
||||
* 计算相差的时间
|
||||
* @param dateTime 时间字符串
|
||||
* @returns 相差的时间
|
||||
*/
|
||||
export function getDiffTimeString(dateTime: string) {
|
||||
// 计算相差秒数
|
||||
const diffSeconds = dayjs().diff(dayjs(dateTime), 'second');
|
||||
/**
|
||||
* 转为时间显示(x月 x天)
|
||||
* https://dayjs.fenxianglu.cn/category/duration.html#%E4%BA%BA%E6%80%A7%E5%8C%96
|
||||
*
|
||||
*/
|
||||
const diffText = dayjs.duration(diffSeconds, 'seconds').humanize();
|
||||
return diffText;
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
export { default as applyModal } from './apply-modal.vue';
|
||||
export { default as ApprovalCard } from './approval-card.vue';
|
||||
/**
|
||||
* 审批同意
|
||||
*/
|
||||
export { default as approvalModal } from './approval-modal.vue';
|
||||
export { default as ApprovalPanel } from './approval-panel.vue';
|
||||
/**
|
||||
* 审批驳回
|
||||
*/
|
||||
export { default as approvalRejectionModal } from './approval-rejection-modal.vue';
|
||||
export { default as ApprovalTimeline } from './approval-timeline.vue';
|
||||
/**
|
||||
* 选择抄送人
|
||||
*/
|
||||
export { default as CopyComponent } from './copy-component.vue';
|
||||
|
||||
/**
|
||||
* 详情信息 modal
|
||||
*/
|
||||
export { default as flowInfoModal } from './flow-info-modal.vue';
|
||||
/**
|
||||
* 流程干预 modal
|
||||
*/
|
||||
export { default as flowInterfereModal } from './flow-interfere-modal.vue';
|
||||
|
||||
/**
|
||||
* 选人 支持单选/多选
|
||||
*/
|
||||
export { default as userSelectModal } from './user-select-modal.vue';
|
@@ -0,0 +1,378 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { User } from '#/api';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal, VbenAvatar } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { userList } from '#/api/system/user';
|
||||
import DeptTree from '#/views/system/user/dept-tree.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'UserSelectModal',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ allowUserIds?: string; mode?: 'multiple' | 'single' }>(),
|
||||
{
|
||||
mode: 'multiple',
|
||||
/**
|
||||
* 允许选择允许选择的人员ID 会当做参数拼接在uselist接口
|
||||
*/
|
||||
allowUserIds: '',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* 取消的事件
|
||||
*/
|
||||
cancel: [];
|
||||
/**
|
||||
* 选择完成的事件
|
||||
*/
|
||||
finish: [User[]];
|
||||
}>();
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '选择人员',
|
||||
class: 'w-[1060px]',
|
||||
fullscreenButton: false,
|
||||
onClosed: () => emit('cancel'),
|
||||
onConfirm: handleSubmit,
|
||||
async onOpened() {
|
||||
const { userList = [] } = modalApi.getData() as { userList: User[] };
|
||||
// 暂时只处理多选 目前并没有单选的情况
|
||||
if (props.mode === 'multiple') {
|
||||
// 左边选中
|
||||
await tableApi.grid.setCheckboxRow(userList, true);
|
||||
// 右边赋值
|
||||
await rightTableApi.grid.loadData(userList);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 左边部门用
|
||||
const selectDeptId = ref<string[]>([]);
|
||||
const formOptions: VbenFormProps = {
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'userName',
|
||||
label: '用户账号',
|
||||
hideLabel: true,
|
||||
componentProps: {
|
||||
placeholder: '请输入账号',
|
||||
},
|
||||
},
|
||||
],
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
handleReset: async () => {
|
||||
selectDeptId.value = [];
|
||||
const { formApi, reload } = tableApi;
|
||||
await formApi.resetForm();
|
||||
const formValues = formApi.form.values;
|
||||
formApi.setLatestSubmissionValues(formValues);
|
||||
await reload(formValues);
|
||||
},
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
// 点击行选中
|
||||
trigger: 'row',
|
||||
},
|
||||
radioConfig: {
|
||||
trigger: 'row',
|
||||
strict: true,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
type: props.mode === 'single' ? 'radio' : 'checkbox',
|
||||
width: 60,
|
||||
resizable: false,
|
||||
},
|
||||
{
|
||||
field: 'userName',
|
||||
title: '用户',
|
||||
headerAlign: 'left',
|
||||
resizable: false,
|
||||
slots: {
|
||||
default: 'user',
|
||||
},
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
layouts: ['PrevPage', 'Number', 'NextPage', 'Sizes', 'Total'],
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 部门树选择处理
|
||||
if (selectDeptId.value.length === 1) {
|
||||
formValues.deptId = selectDeptId.value[0];
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'deptId');
|
||||
}
|
||||
|
||||
// 加载完毕需要设置选中的行
|
||||
if (props.mode === 'multiple') {
|
||||
const records = rightTableApi.grid.getData();
|
||||
await tableApi.grid.setCheckboxRow(records, true);
|
||||
}
|
||||
if (props.mode === 'single') {
|
||||
const records = rightTableApi.grid.getData();
|
||||
if (records.length === 1) {
|
||||
await tableApi.grid.setRadioRow(records[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const params: any = {
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
};
|
||||
// 添加参数
|
||||
if (props.allowUserIds) {
|
||||
params.userIds = props.allowUserIds;
|
||||
}
|
||||
|
||||
return await userList(params);
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'userId',
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
// 需要控制不同的事件 radio也会触发checkbox事件
|
||||
checkboxChange: checkBoxEvent,
|
||||
checkboxAll: checkBoxEvent,
|
||||
radioChange: radioEvent,
|
||||
},
|
||||
});
|
||||
|
||||
function checkBoxEvent() {
|
||||
if (props.mode !== 'multiple') {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 给右边表格赋值
|
||||
* records拿到的是当前页的选中数据
|
||||
* reserveRecords拿到的是其他页选中的数据
|
||||
*/
|
||||
const records = tableApi.grid.getCheckboxRecords();
|
||||
const reserveRecords = tableApi.grid.getCheckboxReserveRecords();
|
||||
const realRecords = [...records, ...reserveRecords];
|
||||
rightTableApi.grid.loadData(realRecords);
|
||||
}
|
||||
|
||||
function radioEvent() {
|
||||
if (props.mode !== 'single') {
|
||||
return;
|
||||
}
|
||||
// 给右边表格赋值
|
||||
const records = tableApi.grid.getRadioRecord();
|
||||
rightTableApi.grid.loadData([records]);
|
||||
}
|
||||
|
||||
const rightGridOptions: VxeGridProps = {
|
||||
checkboxConfig: {},
|
||||
columns: [
|
||||
{
|
||||
field: 'nickName',
|
||||
title: '昵称',
|
||||
width: 200,
|
||||
resizable: false,
|
||||
slots: {
|
||||
default: 'user',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
slots: { default: 'action' },
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'userId',
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
|
||||
const [RightBasicTable, rightTableApi] = useVbenVxeGrid({
|
||||
gridOptions: rightGridOptions,
|
||||
});
|
||||
|
||||
async function handleRemoveItem(row: any) {
|
||||
if (props.mode === 'multiple') {
|
||||
await tableApi.grid.setCheckboxRow(row, false);
|
||||
}
|
||||
if (props.mode === 'single') {
|
||||
await tableApi.grid.clearRadioRow();
|
||||
}
|
||||
const data = rightTableApi.grid.getData();
|
||||
await rightTableApi.grid.loadData(data.filter((item) => item !== row));
|
||||
// 这个方法有问题
|
||||
// await rightTableApi.grid.remove(row);
|
||||
}
|
||||
|
||||
function handleRemoveAll() {
|
||||
if (props.mode === 'multiple') {
|
||||
tableApi.grid.clearCheckboxRow();
|
||||
tableApi.grid.clearCheckboxReserve();
|
||||
}
|
||||
if (props.mode === 'single') {
|
||||
tableApi.grid.clearRadioRow();
|
||||
}
|
||||
rightTableApi.grid.loadData([]);
|
||||
}
|
||||
|
||||
async function handleDeptQuery() {
|
||||
await tableApi.reload();
|
||||
// 重置后恢复 保存勾选的数据
|
||||
const records = rightTableApi.grid.getData();
|
||||
if (props.mode === 'multiple') {
|
||||
tableApi?.grid.setCheckboxRow(records, true);
|
||||
}
|
||||
if (props.mode === 'single' && records.length === 1) {
|
||||
tableApi.grid.setRadioRow(records[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
const records = rightTableApi.grid.getData();
|
||||
console.log(records);
|
||||
emit('finish', records);
|
||||
modalApi.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<div class="flex min-h-[600px]">
|
||||
<DeptTree
|
||||
v-model:select-dept-id="selectDeptId"
|
||||
:show-search="false"
|
||||
class="w-[230px]"
|
||||
@reload="() => tableApi.reload()"
|
||||
@select="handleDeptQuery"
|
||||
/>
|
||||
<div class="h-[600px] w-[450px]">
|
||||
<BasicTable>
|
||||
<template #user="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<VbenAvatar
|
||||
:alt="row.nickName"
|
||||
:src="row.avatar ?? ''"
|
||||
:class="{ 'bg-primary': !row.avatar }"
|
||||
class="size-[32px] rounded-full text-white"
|
||||
/>
|
||||
<div class="flex flex-col items-baseline text-[12px]">
|
||||
<div>{{ row.nickName }}</div>
|
||||
<div class="opacity-50">
|
||||
{{ row.phonenumber || '暂无手机号' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
<div class="flex h-[600px] flex-col">
|
||||
<div class="flex w-full px-4">
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<div>已选中人员</div>
|
||||
<div>
|
||||
<a-button size="small" @click="handleRemoveAll">
|
||||
清空选中
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RightBasicTable id="user-select-right-table">
|
||||
<template #user="{ row }">
|
||||
<div class="flex items-center gap-2 overflow-hidden">
|
||||
<VbenAvatar
|
||||
:alt="row.nickName"
|
||||
:src="row.avatar ?? ''"
|
||||
:class="{ 'bg-primary': !row.avatar }"
|
||||
class="size-[32px] rounded-full text-white"
|
||||
/>
|
||||
<div class="flex flex-col items-baseline text-[12px]">
|
||||
<div class="overflow-ellipsis whitespace-nowrap">
|
||||
{{ row.nickName }}
|
||||
</div>
|
||||
<div class="opacity-50">
|
||||
{{ row.phonenumber || '暂无手机号' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<a-button size="small" @click="handleRemoveItem(row)">
|
||||
移除
|
||||
</a-button>
|
||||
</template>
|
||||
</RightBasicTable>
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(div.vben-link) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.vxe-body--row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/**
|
||||
默认显示右边的滚动条 防止出现滚动条被挤压
|
||||
*/
|
||||
#user-select-right-table {
|
||||
div.vxe-table--body-wrapper.body--wrapper {
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,62 @@
|
||||
import type { LeaveForm, LeaveQuery, LeaveVO } from './model';
|
||||
|
||||
import type { ID, IDS, PageResult } from '#/api/common';
|
||||
|
||||
import { commonExport } from '#/api/helper';
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 查询请假申请列表
|
||||
* @param params
|
||||
* @returns 请假申请列表
|
||||
*/
|
||||
export function leaveList(params?: LeaveQuery) {
|
||||
return requestClient.get<PageResult<LeaveVO>>('/workflow/leave/list', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出请假申请列表
|
||||
* @param params
|
||||
* @returns 请假申请列表
|
||||
*/
|
||||
export function leaveExport(params?: LeaveQuery) {
|
||||
return commonExport('/workflow/leave/export', params ?? {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询请假申请详情
|
||||
* @param id id
|
||||
* @returns 请假申请详情
|
||||
*/
|
||||
export function leaveInfo(id: ID) {
|
||||
return requestClient.get<LeaveVO>(`/workflow/leave/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增请假申请
|
||||
* @param data
|
||||
* @returns void
|
||||
*/
|
||||
export function leaveAdd(data: LeaveForm) {
|
||||
return requestClient.postWithMsg<LeaveVO>('/workflow/leave', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新请假申请
|
||||
* @param data
|
||||
* @returns void
|
||||
*/
|
||||
export function leaveUpdate(data: LeaveForm) {
|
||||
return requestClient.putWithMsg<LeaveVO>('/workflow/leave', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除请假申请
|
||||
* @param id id
|
||||
* @returns void
|
||||
*/
|
||||
export function leaveRemove(id: ID | IDS) {
|
||||
return requestClient.deleteWithMsg<void>(`/workflow/leave/${id}`);
|
||||
}
|
107
apps/web-antd/src/views/property/personalCenter/leave/api/model.d.ts
vendored
Normal file
107
apps/web-antd/src/views/property/personalCenter/leave/api/model.d.ts
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { BaseEntity, PageQuery } from '#/api/common';
|
||||
|
||||
export interface LeaveVO {
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
id: number | string;
|
||||
|
||||
/**
|
||||
* 请假类型
|
||||
*/
|
||||
leaveType: string;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
startDate: string;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
endDate: string;
|
||||
|
||||
/**
|
||||
* 请假天数
|
||||
*/
|
||||
leaveDays: number;
|
||||
|
||||
/**
|
||||
* 请假原因
|
||||
*/
|
||||
remark: string;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface LeaveForm extends BaseEntity {
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
id?: number | string;
|
||||
|
||||
/**
|
||||
* 请假类型
|
||||
*/
|
||||
leaveType?: string;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
startDate?: string;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
endDate?: string;
|
||||
|
||||
/**
|
||||
* 请假天数
|
||||
*/
|
||||
leaveDays?: number;
|
||||
|
||||
/**
|
||||
* 请假原因
|
||||
*/
|
||||
remark?: string;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface LeaveQuery extends PageQuery {
|
||||
/**
|
||||
* 请假类型
|
||||
*/
|
||||
leaveType?: string;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
startDate?: string;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
endDate?: string;
|
||||
|
||||
/**
|
||||
* 请假天数
|
||||
*/
|
||||
leaveDays?: number;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any;
|
||||
}
|
175
apps/web-antd/src/views/property/personalCenter/leave/data.tsx
Normal file
175
apps/web-antd/src/views/property/personalCenter/leave/data.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import type { FormSchemaGetter, VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { DictEnum } from '@vben/constants';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { OptionsTag } from '#/components/table';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
export const leaveTypeOptions = [
|
||||
{ label: '病假', value: '1' },
|
||||
{ label: '事假', value: '2' },
|
||||
{ label: '年假', value: '3' },
|
||||
{ label: '婚假', value: '4' },
|
||||
{ label: '产假', value: '5' },
|
||||
{ label: '其他', value: '7' },
|
||||
];
|
||||
|
||||
export const leaveFlowOptions = [
|
||||
{ label: '请假流程-普通', value: 'leave1' },
|
||||
{ label: '请假流程-排他网关', value: 'leave2' },
|
||||
{ label: '请假流程-并行网关', value: 'leave3' },
|
||||
{ label: '请假流程-会签', value: 'leave4' },
|
||||
{ label: '请假申请-并行会签网关', value: 'leave5' },
|
||||
{ label: '请假申请-排他并行网关', value: 'leave6' },
|
||||
];
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 1,
|
||||
},
|
||||
fieldName: 'startLeaveDays',
|
||||
label: '请假天数',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 1,
|
||||
},
|
||||
fieldName: 'endLeaveDays',
|
||||
label: '至',
|
||||
labelClass: 'justify-center',
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
title: '请假类型',
|
||||
field: 'leaveType',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return <OptionsTag options={leaveTypeOptions} value={row.leaveType} />;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
field: 'startDate',
|
||||
formatter: ({ cellValue }) => dayjs(cellValue).format('YYYY-MM-DD'),
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
field: 'endDate',
|
||||
formatter: ({ cellValue }) => dayjs(cellValue).format('YYYY-MM-DD'),
|
||||
},
|
||||
{
|
||||
title: '请假天数',
|
||||
field: 'leaveDays',
|
||||
formatter: ({ cellValue }) => `${cellValue}天`,
|
||||
},
|
||||
{
|
||||
title: '请假原因',
|
||||
field: 'remark',
|
||||
},
|
||||
{
|
||||
title: '流程状态',
|
||||
field: 'status',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.status, DictEnum.WF_BUSINESS_STATUS);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
resizable: false,
|
||||
width: 'auto',
|
||||
},
|
||||
];
|
||||
|
||||
export const modalSchema: (isEdit: boolean) => VbenFormSchema[] = (
|
||||
isEdit: boolean,
|
||||
) => [
|
||||
{
|
||||
label: '主键',
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '流程类型',
|
||||
fieldName: 'flowType',
|
||||
component: 'Select',
|
||||
help: '这里仅仅为了发起流程方便, 实际不应该包含此字段',
|
||||
componentProps: {
|
||||
options: leaveFlowOptions,
|
||||
getPopupContainer,
|
||||
},
|
||||
defaultValue: 'leave1',
|
||||
rules: 'selectRequired',
|
||||
dependencies: {
|
||||
show: () => isEdit,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '请假类型',
|
||||
fieldName: 'leaveType',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: leaveTypeOptions,
|
||||
getPopupContainer,
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
formItemClass: 'col-span-1',
|
||||
},
|
||||
{
|
||||
label: '开始时间',
|
||||
fieldName: 'dateRange',
|
||||
component: 'RangePicker',
|
||||
componentProps(model) {
|
||||
return {
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
onChange: (dates: [string, string]) => {
|
||||
if (!dates) {
|
||||
model.leaveDays = null;
|
||||
return;
|
||||
}
|
||||
const [start, end] = dates;
|
||||
const leaveDays = dayjs(end).diff(dayjs(start), 'day') + 1;
|
||||
model.leaveDays = leaveDays;
|
||||
},
|
||||
};
|
||||
},
|
||||
rules: 'required',
|
||||
formItemClass: 'col-span-1',
|
||||
},
|
||||
{
|
||||
label: '请假天数',
|
||||
fieldName: 'leaveDays',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '请假原因',
|
||||
fieldName: 'remark',
|
||||
component: 'Textarea',
|
||||
formItemClass: 'items-start',
|
||||
},
|
||||
];
|
200
apps/web-antd/src/views/property/personalCenter/leave/index.vue
Normal file
200
apps/web-antd/src/views/property/personalCenter/leave/index.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import type { LeaveForm } from './api/model';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import { cancelProcessApply } from '#/api/workflow/instance';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import { flowInfoModal } from '../components';
|
||||
import { leaveExport, leaveList, leaveRemove } from './api';
|
||||
import { columns, querySchema } from './data';
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
// 选中 需要根据状态判断
|
||||
checkMethod: ({ row }) => ['back', 'cancel', 'draft'].includes(row.status),
|
||||
},
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
return await leaveList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
// 表格全局唯一表示 保存列配置需要用到
|
||||
id: 'workflow-leave-index',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
function handleAdd() {
|
||||
router.push('/workflow/leaveEdit/index');
|
||||
}
|
||||
|
||||
async function handleEdit(row: Required<LeaveForm>) {
|
||||
router.push({ path: '/workflow/leaveEdit/index', query: { id: row.id } });
|
||||
}
|
||||
|
||||
async function handleDelete(row: Required<LeaveForm>) {
|
||||
await leaveRemove(row.id);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
async function handleRevoke(row: Required<LeaveForm>) {
|
||||
await cancelProcessApply({
|
||||
businessId: row.id,
|
||||
message: '申请人撤销流程!',
|
||||
});
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
function handleMultiDelete() {
|
||||
const rows = tableApi.grid.getCheckboxRecords();
|
||||
const ids = rows.map((row: Required<LeaveForm>) => row.id);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
okType: 'danger',
|
||||
content: `确认删除选中的${ids.length}条记录吗?`,
|
||||
onOk: async () => {
|
||||
await leaveRemove(ids);
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(
|
||||
leaveExport,
|
||||
'请假申请数据',
|
||||
tableApi.formApi.form.values,
|
||||
{
|
||||
fieldMappingTime: formOptions.fieldMappingTime,
|
||||
},
|
||||
);
|
||||
}
|
||||
const [FlowInfoModal, flowInfoModalApi] = useVbenModal({
|
||||
connectedComponent: flowInfoModal,
|
||||
});
|
||||
function handleInfo(row: Required<LeaveForm>) {
|
||||
flowInfoModalApi.setData({ businessId: row.id });
|
||||
flowInfoModalApi.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<BasicTable table-title="请假申请列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['workflow:leave:export']"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
:disabled="!vxeCheckboxChecked(tableApi)"
|
||||
danger
|
||||
type="primary"
|
||||
v-access:code="['workflow:leave:remove']"
|
||||
@click="handleMultiDelete"
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
v-access:code="['workflow:leave:add']"
|
||||
@click="handleAdd"
|
||||
>
|
||||
{{ $t('pages.common.add') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Space>
|
||||
<ghost-button
|
||||
v-if="['draft', 'cancel', 'back'].includes(row.status)"
|
||||
v-access:code="['workflow:leave:edit']"
|
||||
@click.stop="handleEdit(row)"
|
||||
>
|
||||
{{ $t('pages.common.edit') }}
|
||||
</ghost-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认撤销?"
|
||||
@confirm="handleRevoke(row)"
|
||||
>
|
||||
<ghost-button
|
||||
v-if="['waiting'].includes(row.status)"
|
||||
v-access:code="['workflow:leave:edit']"
|
||||
@click.stop=""
|
||||
>
|
||||
撤销
|
||||
</ghost-button>
|
||||
</Popconfirm>
|
||||
<ghost-button v-if="row.status !== 'draft'" @click="handleInfo(row)">
|
||||
详情
|
||||
</ghost-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<ghost-button
|
||||
v-if="['draft', 'cancel', 'back'].includes(row.status)"
|
||||
danger
|
||||
v-access:code="['workflow:leave:remove']"
|
||||
@click.stop=""
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</ghost-button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<FlowInfoModal />
|
||||
</Page>
|
||||
</template>
|
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { LeaveVO } from './api/model';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Descriptions, DescriptionsItem } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { leaveTypeOptions } from './data';
|
||||
|
||||
defineOptions({
|
||||
name: 'LeaveDescription',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = defineProps<{ data: LeaveVO }>();
|
||||
|
||||
const leaveType = computed(() => {
|
||||
return (
|
||||
leaveTypeOptions.find((item) => item.value === props.data.leaveType)
|
||||
?.label ?? '未知'
|
||||
);
|
||||
});
|
||||
|
||||
function formatDate(date: string) {
|
||||
return dayjs(date).format('YYYY-MM-DD');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Descriptions :column="1" size="middle">
|
||||
<DescriptionsItem label="请假类型">
|
||||
{{ leaveType }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="请假时间">
|
||||
{{ formatDate(data.startDate) }} - {{ formatDate(data.endDate) }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="请假时长">
|
||||
{{ data.leaveDays }}天
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="请假原因">
|
||||
{{ data.remark || '无' }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</template>
|
@@ -0,0 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
import type { LeaveVO } from './api/model';
|
||||
|
||||
import type { StartWorkFlowReqData } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Card } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { cloneDeep, omit } from 'lodash-es';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { startWorkFlow } from '#/api/workflow/task';
|
||||
|
||||
import { applyModal } from '../components';
|
||||
import { leaveAdd, leaveInfo, leaveUpdate } from './api';
|
||||
import { modalSchema } from './data';
|
||||
import LeaveDescription from './leave-description.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const readonly = route.query?.readonly === 'true';
|
||||
const id = route.query?.id as string;
|
||||
|
||||
/**
|
||||
* id存在&readonly时候
|
||||
*/
|
||||
const showActionBtn = computed(() => {
|
||||
return !readonly;
|
||||
});
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-2',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 100,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
disabled: readonly,
|
||||
},
|
||||
},
|
||||
schema: modalSchema(!readonly),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
const leaveDescription = ref<LeaveVO>();
|
||||
const showDescription = computed(() => {
|
||||
return readonly && leaveDescription.value;
|
||||
});
|
||||
const cardRef = useTemplateRef('cardRef');
|
||||
onMounted(async () => {
|
||||
// 只读 获取信息赋值
|
||||
if (id) {
|
||||
const resp = await leaveInfo(id);
|
||||
leaveDescription.value = resp;
|
||||
await formApi.setValues(resp);
|
||||
const dateRange = [dayjs(resp.startDate), dayjs(resp.endDate)];
|
||||
await formApi.setFieldValue('dateRange', dateRange);
|
||||
|
||||
/**
|
||||
* window.parent(最近的上一级父页面)
|
||||
* 主要解决内嵌iframe卡顿的问题
|
||||
*/
|
||||
if (readonly) {
|
||||
// 渲染完毕才显示表单
|
||||
window.parent.postMessage({ type: 'mounted' }, '*');
|
||||
// 获取表单高度 内嵌时保持一致
|
||||
setTimeout(() => {
|
||||
const el = cardRef.value?.$el as HTMLDivElement;
|
||||
// 获取高度
|
||||
const height = el?.offsetHeight ?? 0;
|
||||
if (height) {
|
||||
window.parent.postMessage({ type: 'height', height }, '*');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
/**
|
||||
* 提取通用逻辑
|
||||
*/
|
||||
async function handleSaveOrUpdate() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
let data = cloneDeep(await formApi.getValues()) as any;
|
||||
data = omit(data, 'flowType');
|
||||
// 处理日期
|
||||
data.startDate = dayjs(data.dateRange[0]).format('YYYY-MM-DD HH:mm:ss');
|
||||
data.endDate = dayjs(data.dateRange[1]).format('YYYY-MM-DD HH:mm:ss');
|
||||
if (id) {
|
||||
data.id = id;
|
||||
return await leaveUpdate(data);
|
||||
} else {
|
||||
return await leaveAdd(data);
|
||||
}
|
||||
}
|
||||
|
||||
const [ApplyModal, applyModalApi] = useVbenModal({
|
||||
connectedComponent: applyModal,
|
||||
});
|
||||
/**
|
||||
* 暂存 草稿状态
|
||||
*/
|
||||
async function handleTempSave() {
|
||||
try {
|
||||
await handleSaveOrUpdate();
|
||||
router.push('/demo/leave');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存业务 & 发起流程
|
||||
*/
|
||||
async function handleStartWorkFlow() {
|
||||
try {
|
||||
// 保存业务
|
||||
const leaveResp = await handleSaveOrUpdate();
|
||||
// 启动流程
|
||||
const taskVariables = {
|
||||
leaveDays: leaveResp!.leaveDays,
|
||||
userList: ['1', '3', '4'],
|
||||
};
|
||||
const formValues = await formApi.getValues();
|
||||
const flowCode = formValues?.flowType ?? 'leave1';
|
||||
const startWorkFlowData: StartWorkFlowReqData = {
|
||||
businessId: leaveResp!.id,
|
||||
flowCode,
|
||||
variables: taskVariables,
|
||||
};
|
||||
const { taskId } = await startWorkFlow(startWorkFlowData);
|
||||
// 打开窗口
|
||||
applyModalApi.setData({
|
||||
taskId,
|
||||
taskVariables,
|
||||
variables: {},
|
||||
});
|
||||
applyModalApi.open();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleComplete() {
|
||||
formApi.resetForm();
|
||||
router.push('/demo/leave');
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示详情时 需要较小的padding
|
||||
*/
|
||||
const cardSize = computed(() => {
|
||||
return showDescription.value ? 'small' : 'default';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card ref="cardRef" :size="cardSize">
|
||||
<div id="leave-form">
|
||||
<!-- 使用v-if会影响生命周期 -->
|
||||
<BasicForm v-show="!showDescription" />
|
||||
<LeaveDescription v-if="showDescription" :data="leaveDescription!" />
|
||||
<div v-if="showActionBtn" class="flex justify-end gap-2">
|
||||
<a-button @click="handleTempSave">暂存</a-button>
|
||||
<a-button type="primary" @click="handleStartWorkFlow">提交</a-button>
|
||||
</div>
|
||||
<ApplyModal @complete="handleComplete" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
html:has(#leave-form) {
|
||||
/**
|
||||
去除顶部进度条样式
|
||||
*/
|
||||
#nprogress {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,11 @@
|
||||
<!--
|
||||
后端版本>=5.4.0 这个从本地路由变为从后台返回
|
||||
未修改文件名 而是新加了这个文件
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import LeaveFormPage from './leave-form.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LeaveFormPage />
|
||||
</template>
|
@@ -3,7 +3,8 @@ import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { getDictOptions } from '#/utils/dict';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import {resident_unitList} from '#/api/property/resident/unit/index'
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const modalSchema: FormSchemaGetter = () => [
|
||||
{
|
||||
@@ -25,6 +26,15 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
label: '部门',
|
||||
fieldName: 'departmentId',
|
||||
component: 'ApiSelect',
|
||||
componentProps:{
|
||||
api: async () => {
|
||||
const res = await resident_unitList({pageSize:1000000000,pageNum:1})
|
||||
return res;
|
||||
},
|
||||
resultField:'rows',
|
||||
labelField:'name',
|
||||
valueField:'id'
|
||||
},
|
||||
rules:'required'
|
||||
},
|
||||
{
|
||||
@@ -32,43 +42,126 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
fieldName: 'leaveType',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options:getDictOptions('wy_qjlx')
|
||||
},
|
||||
rules:'required'
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
fieldName: 'Placeholder',
|
||||
component: ''
|
||||
},
|
||||
{
|
||||
label: '开始时间',
|
||||
fieldName: 'startTime',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
componentProps: (values)=>({
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
rules:'required'
|
||||
onChange: (value: string) => {
|
||||
if (value && values.endTime) {
|
||||
const start = dayjs(value);
|
||||
const end = dayjs(values.endTime);
|
||||
const diffMinutes = end.diff(start, 'minute');
|
||||
|
||||
if (diffMinutes < 0) {
|
||||
values.totalDuration = '时间计算错误';
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(diffMinutes / (24 * 60));
|
||||
const remainingMinutes = diffMinutes % (24 * 60);
|
||||
const hours = Math.floor(remainingMinutes / 60);
|
||||
const minutes = remainingMinutes % 60;
|
||||
|
||||
let durationText = '';
|
||||
if (days > 0) {
|
||||
durationText += `${days}天`;
|
||||
}
|
||||
if (hours > 0 || days > 0) {
|
||||
durationText += `${hours}小时`;
|
||||
}
|
||||
durationText += `${minutes}分钟`;
|
||||
|
||||
// 更新合计时间字段
|
||||
values.totalDuration = durationText;
|
||||
} else {
|
||||
// 如果开始时间被清空,也清空合计时间
|
||||
values.totalDuration = '';
|
||||
}
|
||||
},
|
||||
}),
|
||||
rules:'required'
|
||||
},
|
||||
{
|
||||
label: '结束时间',
|
||||
fieldName: 'endTime',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
componentProps: (values)=>({
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
placeholder:!values.startTime?'请先选择开始时间':'请选择结束时间(结束时间不能早于开始时间)',
|
||||
disabled:!values.startTime,
|
||||
disabledDate: (current: any) => {
|
||||
if (!values.startTime) return false;
|
||||
// 只允许选择大于等于开始时间的日期,将小于等于的时间禁用
|
||||
return current && current <= dayjs(values.startTime).startOf('second');
|
||||
},
|
||||
rules:'required'
|
||||
onChange: (value: string) => {
|
||||
if (values.startTime && value) {
|
||||
const start = dayjs(values.startTime);
|
||||
const end = dayjs(value);
|
||||
const diffMinutes = end.diff(start, 'minute');
|
||||
|
||||
if (diffMinutes < 0) {
|
||||
values.totalDuration = '时间计算错误';
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(diffMinutes / (24 * 60));
|
||||
const remainingMinutes = diffMinutes % (24 * 60);
|
||||
const hours = Math.floor(remainingMinutes / 60);
|
||||
const minutes = remainingMinutes % 60;
|
||||
|
||||
let durationText = '';
|
||||
if (days > 0) {
|
||||
durationText += `${days}天`;
|
||||
}
|
||||
if (hours > 0 || days > 0) {
|
||||
durationText += `${hours}小时`;
|
||||
}
|
||||
durationText += `${minutes}分钟`;
|
||||
|
||||
// 更新合计时间字段
|
||||
values.totalDuration = durationText;
|
||||
} else {
|
||||
// 如果结束时间被清空,也清空合计时间
|
||||
values.totalDuration = '';
|
||||
}
|
||||
},
|
||||
}),
|
||||
rules:'required',
|
||||
dependencies:{
|
||||
triggerFields:['startTime']
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '合计时间',
|
||||
fieldName: 'totalDuration',
|
||||
component: 'Input',
|
||||
disabled:true,
|
||||
rules:'required'
|
||||
rules:'required',
|
||||
componentProps:{
|
||||
placeholder:'请选择开始时间与结束时间'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '请假事由',
|
||||
fieldName: 'reason',
|
||||
component: 'Textarea',
|
||||
rules:'required'
|
||||
rules:'required',
|
||||
formItemClass: 'col-span-2'
|
||||
}
|
||||
];
|
||||
|
@@ -12,6 +12,7 @@ import { useVbenForm } from '#/adapter/form';
|
||||
import { useVbenModal} from '@vben/common-ui';
|
||||
import {modalSchema} from "./data";
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
@@ -25,8 +26,30 @@ const [BasicForm, formApi] = useVbenForm({
|
||||
},
|
||||
schema: modalSchema(),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2 gap-x-10 gap-y-2',
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
// 提交处理函数
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
message.error('请完善表单信息');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = await formApi.getValues();
|
||||
formData.startTime = dayjs(formData.startTime).format('YYYY-MM-DD HH:mm:ss');
|
||||
formData.endTime = dayjs(formData.endTime).format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
console.log('提交数据:', formData);
|
||||
// await leaveApplicationAdd(formData);
|
||||
|
||||
message.success('提交成功');
|
||||
await formApi.resetForm();
|
||||
}
|
||||
async function cleanForm () {
|
||||
await formApi.resetForm();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -34,6 +57,23 @@ const [BasicForm, formApi] = useVbenForm({
|
||||
<div class="text-2xl font-bold">请假申请</div>
|
||||
<div class="m-6 bg-white p-4">
|
||||
<BasicForm/>
|
||||
<div class="flex justify-center mt-6 gap-2">
|
||||
<Button type="primary" @click="handleSubmit">提交申请</Button>
|
||||
<Button @click="cleanForm">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
/* 使用 :deep() 穿透 scoped 样式,影响子组件 */
|
||||
:deep(.ant-input[disabled]),
|
||||
:deep(.ant-input-number-disabled .ant-input-number-input),
|
||||
:deep(.ant-select-disabled .ant-select-selection-item),
|
||||
:deep(.ant-picker-disabled .ant-picker-input > input) {
|
||||
/* 设置一个更深的颜色 */
|
||||
color: rgb(0 0 0 / 65%) !important;
|
||||
|
||||
/* 有些浏览器需要这个来覆盖默认颜色 */
|
||||
-webkit-text-fill-color: rgb(0 0 0 / 65%) !important;
|
||||
}
|
||||
</style>
|
||||
|
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { CategoryTree } from '#/api/workflow/category/model';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { SyncOutlined } from '@ant-design/icons-vue';
|
||||
import { InputSearch, Skeleton, Tree } from 'ant-design-vue';
|
||||
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* 点击刷新按钮的事件
|
||||
*/
|
||||
reload: [];
|
||||
/**
|
||||
* 点击节点的事件
|
||||
*/
|
||||
select: [];
|
||||
}>();
|
||||
|
||||
const selectCode = defineModel('selectCode', {
|
||||
required: true,
|
||||
type: Array as PropType<number[] | string[]>,
|
||||
});
|
||||
|
||||
const searchValue = defineModel('searchValue', {
|
||||
type: String,
|
||||
default: '',
|
||||
});
|
||||
|
||||
const categoryTreeArray = ref<CategoryTree[]>([]);
|
||||
/** 骨架屏加载 */
|
||||
const showTreeSkeleton = ref<boolean>(true);
|
||||
|
||||
async function loadTree() {
|
||||
showTreeSkeleton.value = true;
|
||||
searchValue.value = '';
|
||||
selectCode.value = [];
|
||||
|
||||
const treeData = await categoryTree();
|
||||
|
||||
categoryTreeArray.value = treeData;
|
||||
showTreeSkeleton.value = false;
|
||||
}
|
||||
|
||||
async function handleReload() {
|
||||
await loadTree();
|
||||
emit('reload');
|
||||
}
|
||||
|
||||
onMounted(loadTree);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$attrs.class">
|
||||
<Skeleton
|
||||
:loading="showTreeSkeleton"
|
||||
:paragraph="{ rows: 8 }"
|
||||
active
|
||||
class="p-[8px]"
|
||||
>
|
||||
<div
|
||||
class="bg-background flex h-full flex-col overflow-y-auto rounded-lg"
|
||||
>
|
||||
<!-- 固定在顶部 必须加上bg-background背景色 否则会产生'穿透'效果 -->
|
||||
<div class="bg-background z-100 sticky left-0 top-0 p-[8px]">
|
||||
<InputSearch
|
||||
v-model:value="searchValue"
|
||||
:placeholder="$t('pages.common.search')"
|
||||
size="small"
|
||||
>
|
||||
<template #enterButton>
|
||||
<a-button @click="handleReload">
|
||||
<SyncOutlined class="text-primary" />
|
||||
</a-button>
|
||||
</template>
|
||||
</InputSearch>
|
||||
</div>
|
||||
<div class="h-full overflow-x-hidden px-[8px]">
|
||||
<Tree
|
||||
v-bind="$attrs"
|
||||
v-if="categoryTreeArray.length > 0"
|
||||
v-model:selected-keys="selectCode"
|
||||
:class="$attrs.class"
|
||||
:field-names="{ title: 'label', key: 'id' }"
|
||||
:show-line="{ showLeafIcon: false }"
|
||||
:tree-data="categoryTreeArray"
|
||||
:virtual="false"
|
||||
default-expand-all
|
||||
@select="$emit('select')"
|
||||
>
|
||||
<template #title="{ label }">
|
||||
<span v-if="label.indexOf(searchValue) > -1">
|
||||
{{ label.substring(0, label.indexOf(searchValue)) }}
|
||||
<span style="color: #f50">{{ searchValue }}</span>
|
||||
{{
|
||||
label.substring(
|
||||
label.indexOf(searchValue) + searchValue.length,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else>{{ label }}</span>
|
||||
</template>
|
||||
</Tree>
|
||||
</div>
|
||||
</div>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,36 @@
|
||||
import { optionsToEnum } from '@vben/utils';
|
||||
|
||||
export const activityStatusOptions = [
|
||||
{
|
||||
label: '激活',
|
||||
value: 1,
|
||||
color: 'success',
|
||||
enumName: 'Active',
|
||||
},
|
||||
{
|
||||
label: '挂起',
|
||||
value: 0,
|
||||
color: 'error',
|
||||
enumName: 'Suspended',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const ActivityStatusEnum = optionsToEnum(activityStatusOptions);
|
||||
|
||||
export const publishStatusOptions = [
|
||||
{
|
||||
label: '已发布',
|
||||
value: 1,
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
label: '未发布',
|
||||
value: 0,
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
label: '失效',
|
||||
value: 9,
|
||||
color: 'error',
|
||||
},
|
||||
];
|
@@ -0,0 +1,103 @@
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { OptionsTag } from '#/components/table';
|
||||
|
||||
import { publishStatusOptions } from './constant';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'flowName',
|
||||
label: '流程名称',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'flowCode',
|
||||
label: '流程code',
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
field: 'flowName',
|
||||
title: '流程名称',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'flowCode',
|
||||
title: '流程code',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'version',
|
||||
title: '版本号',
|
||||
minWidth: 80,
|
||||
formatter: ({ cellValue }) => `V${cellValue}.0`,
|
||||
},
|
||||
{
|
||||
field: 'activityStatus',
|
||||
title: '激活状态',
|
||||
minWidth: 100,
|
||||
slots: {
|
||||
default: 'activityStatus',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'isPublish',
|
||||
title: '发布状态',
|
||||
minWidth: 100,
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
const cellValue = row.isPublish;
|
||||
return (
|
||||
<OptionsTag options={publishStatusOptions as any} value={cellValue} />
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
resizable: false,
|
||||
width: 'auto',
|
||||
},
|
||||
];
|
||||
|
||||
export const modalSchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
fieldName: 'id',
|
||||
},
|
||||
{
|
||||
component: 'TreeSelect',
|
||||
fieldName: 'category',
|
||||
label: '流程分类',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'flowCode',
|
||||
label: '流程code',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'flowName',
|
||||
label: '流程名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'formPath',
|
||||
label: '表单路径',
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
@@ -0,0 +1,11 @@
|
||||
<!--
|
||||
后端版本>=5.4.0 这个从本地路由变为从后台返回
|
||||
未修改文件名 而是新加了这个文件
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import FlowDesignerPage from '../components/flow-designer.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FlowDesignerPage />
|
||||
</template>
|
@@ -0,0 +1,378 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import type { RadioChangeEvent } from 'ant-design-vue';
|
||||
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import {
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
RadioGroup,
|
||||
Space,
|
||||
Switch,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import {
|
||||
unPublishList,
|
||||
workflowDefinitionActive,
|
||||
workflowDefinitionCopy,
|
||||
workflowDefinitionDelete,
|
||||
workflowDefinitionExport,
|
||||
workflowDefinitionList,
|
||||
workflowDefinitionPublish,
|
||||
} from '#/api/workflow/definition';
|
||||
import { downloadByData } from '#/utils/file/download';
|
||||
|
||||
import CategoryTree from './category-tree.vue';
|
||||
import { columns, querySchema } from './data';
|
||||
import processDefinitionDeployModal from './process-definition-deploy-modal.vue';
|
||||
import processDefinitionModal from './process-definition-modal.vue';
|
||||
|
||||
// 左边部门用
|
||||
const selectedCode = ref<number[] | string[]>([]);
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
schema: querySchema(),
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
handleReset: async () => {
|
||||
selectedCode.value = [];
|
||||
const { formApi, reload } = tableApi;
|
||||
await formApi.resetForm();
|
||||
const formValues = formApi.form.values;
|
||||
formApi.setLatestSubmissionValues(formValues);
|
||||
await reload(formValues);
|
||||
},
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
// 点击行选中
|
||||
trigger: 'default',
|
||||
},
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 部门树选择处理
|
||||
if (selectedCode.value.length === 1) {
|
||||
formValues.category = selectedCode.value[0];
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'category');
|
||||
}
|
||||
|
||||
return await currentTableApi.value({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
headerCellConfig: {
|
||||
height: 44,
|
||||
},
|
||||
cellConfig: {
|
||||
height: 100,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
id: 'workflow-definition-index',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
// 左边的切换
|
||||
const statusOptions = [
|
||||
{ label: '已发布流程', value: 1 },
|
||||
{ label: '未发布流程', value: 0 },
|
||||
];
|
||||
const currentStatus = ref(1);
|
||||
const currentTableApi = computed(() => {
|
||||
if (currentStatus.value === 1) {
|
||||
return workflowDefinitionList;
|
||||
}
|
||||
return unPublishList;
|
||||
});
|
||||
async function handleStatusChange(e: RadioChangeEvent) {
|
||||
currentStatus.value = e.target.value as number;
|
||||
await tableApi.reload();
|
||||
}
|
||||
|
||||
async function handleDelete(row: Recordable<any>) {
|
||||
await workflowDefinitionDelete(row.id);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
function handleMultiDelete() {
|
||||
const rows = tableApi.grid.getCheckboxRecords();
|
||||
const ids = rows.map((row: any) => row.id);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
okType: 'danger',
|
||||
content: `确认删除选中的${ids.length}条记录吗?`,
|
||||
onOk: async () => {
|
||||
await workflowDefinitionDelete(ids);
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
/**
|
||||
* 流程设计/预览
|
||||
* @param row row
|
||||
* @param disabled true为预览,false为设计
|
||||
*/
|
||||
function handleDesign(row: any, disabled: boolean) {
|
||||
router.push({
|
||||
path: '/workflow/design/index',
|
||||
query: { definitionId: row.id, disabled: String(disabled) },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活/挂起流程
|
||||
* @param row row
|
||||
*/
|
||||
async function handleActive(row: any, status: boolean | number | string) {
|
||||
const lastStatus = status === 1 ? 0 : 1;
|
||||
try {
|
||||
await workflowDefinitionActive(row.id, !!status);
|
||||
await tableApi.query();
|
||||
} catch (error) {
|
||||
row.activityStatus = lastStatus;
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布流程
|
||||
* @param row row
|
||||
*/
|
||||
async function handlePublish(row: any) {
|
||||
await workflowDefinitionPublish(row.id);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制流程
|
||||
* @param row row
|
||||
*/
|
||||
async function handleCopy(row: any) {
|
||||
await workflowDefinitionCopy(row.id);
|
||||
// 跳转到未发布流程tab
|
||||
currentStatus.value = 0;
|
||||
await tableApi.reload();
|
||||
}
|
||||
|
||||
const [ProcessDefinitionModal, modalApi] = useVbenModal({
|
||||
connectedComponent: processDefinitionModal,
|
||||
});
|
||||
|
||||
/**
|
||||
* 新增流程
|
||||
*/
|
||||
function handleAdd() {
|
||||
modalApi.setData({});
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑流程
|
||||
*/
|
||||
function handleEdit(row: any) {
|
||||
modalApi.setData({ id: row.id });
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出xml
|
||||
* @param row row
|
||||
*/
|
||||
async function handleExportXml(row: any) {
|
||||
const hideLoading = message.loading($t('pages.common.downloadLoading'), 0);
|
||||
try {
|
||||
const blob = await workflowDefinitionExport(row.id);
|
||||
downloadByData(blob, `${row.flowName}-${Date.now()}.json`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const [ProcessDefinitionDeployModal, deployModalApi] = useVbenModal({
|
||||
connectedComponent: processDefinitionDeployModal,
|
||||
});
|
||||
|
||||
/**
|
||||
* 部署流程xml
|
||||
*/
|
||||
function handleDeploy() {
|
||||
if (selectedCode.value.length === 0) {
|
||||
message.warning('请先选择流程分类');
|
||||
return;
|
||||
}
|
||||
const selectedCategory = selectedCode.value[0];
|
||||
if (selectedCategory === 0) {
|
||||
message.warning('不可选择根目录进行部署, 请选择子分类');
|
||||
return;
|
||||
}
|
||||
deployModalApi.setData({ category: selectedCategory });
|
||||
deployModalApi.open();
|
||||
}
|
||||
|
||||
// 部署流程json
|
||||
async function handleDeploySuccess() {
|
||||
// 跳转到未发布
|
||||
currentStatus.value = 0;
|
||||
await tableApi.reload();
|
||||
}
|
||||
|
||||
// 新增完成需要跳转到未发布
|
||||
async function handleReload(type: 'add' | 'update') {
|
||||
if (type === 'add') {
|
||||
currentStatus.value = 0;
|
||||
}
|
||||
await tableApi.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-[8px]">
|
||||
<CategoryTree
|
||||
v-model:select-code="selectedCode"
|
||||
class="w-[260px]"
|
||||
@reload="() => tableApi.reload()"
|
||||
@select="() => tableApi.reload()"
|
||||
/>
|
||||
<BasicTable class="flex-1 overflow-hidden">
|
||||
<template #toolbar-actions>
|
||||
<RadioGroup
|
||||
v-model:value="currentStatus"
|
||||
:options="statusOptions"
|
||||
button-style="solid"
|
||||
option-type="button"
|
||||
@change="handleStatusChange"
|
||||
/>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
:disabled="!vxeCheckboxChecked(tableApi)"
|
||||
danger
|
||||
type="primary"
|
||||
v-access:code="['system:user:remove']"
|
||||
@click="handleMultiDelete"
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</a-button>
|
||||
<a-button v-access:code="['system:user:add']" @click="handleDeploy">
|
||||
部署
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
v-access:code="['system:user:add']"
|
||||
@click="handleAdd"
|
||||
>
|
||||
{{ $t('pages.common.add') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #activityStatus="{ row }">
|
||||
<Switch
|
||||
v-model:checked="row.activityStatus"
|
||||
:checked-value="1"
|
||||
:unchecked-value="0"
|
||||
checked-children="激活"
|
||||
un-checked-children="挂起"
|
||||
@change="(status) => handleActive(row, status)"
|
||||
/>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div>
|
||||
<a-button size="small" type="link" @click="handleEdit(row)">
|
||||
编辑信息
|
||||
</a-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<a-button danger size="small" type="link" @click.stop="">
|
||||
删除流程
|
||||
</a-button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
<div>
|
||||
<a-button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleDesign(row, !!row.isPublish)"
|
||||
>
|
||||
{{ row.isPublish ? '查看流程' : '设计流程' }}
|
||||
</a-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
:title="`确认发布流程[${row.flowName}]?`"
|
||||
placement="left"
|
||||
@confirm="handlePublish(row)"
|
||||
>
|
||||
<a-button v-if="!row.isPublish" size="small" type="link">
|
||||
发布流程
|
||||
</a-button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
<div>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
:title="`确认复制流程[${row.flowName}]?`"
|
||||
placement="left"
|
||||
@confirm="handleCopy(row)"
|
||||
>
|
||||
<a-button size="small" type="link"> 复制流程 </a-button>
|
||||
</Popconfirm>
|
||||
<a-button size="small" type="link" @click="handleExportXml(row)">
|
||||
导出流程
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
<ProcessDefinitionModal @reload="handleReload" />
|
||||
<ProcessDefinitionDeployModal @reload="handleDeploySuccess" />
|
||||
</Page>
|
||||
</template>
|
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadFile } from 'ant-design-vue/es/upload/interface';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { InBoxIcon } from '@vben/icons';
|
||||
|
||||
import { Upload } from 'ant-design-vue';
|
||||
|
||||
import { workflowDefinitionImport } from '#/api/workflow/definition';
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
const UploadDragger = Upload.Dragger;
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
onCancel: handleCancel,
|
||||
onConfirm: handleSubmit,
|
||||
});
|
||||
|
||||
const fileList = ref<UploadFile[]>([]);
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
if (fileList.value.length !== 1) {
|
||||
handleCancel();
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
file: fileList.value[0]!.originFileObj as Blob,
|
||||
category: modalApi.getData().category,
|
||||
};
|
||||
await workflowDefinitionImport(data);
|
||||
emit('reload');
|
||||
handleCancel();
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
modalApi.close();
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalApi.close();
|
||||
fileList.value = [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal
|
||||
:close-on-click-modal="false"
|
||||
:fullscreen-button="false"
|
||||
title="流程部署"
|
||||
>
|
||||
<!-- z-index不设置会遮挡模板下载loading -->
|
||||
<!-- 手动处理 而不是放入文件就上传 -->
|
||||
<UploadDragger
|
||||
v-model:file-list="fileList"
|
||||
:before-upload="() => false"
|
||||
:max-count="1"
|
||||
:show-upload-list="true"
|
||||
accept="application/json"
|
||||
>
|
||||
<p class="ant-upload-drag-icon flex items-center justify-center">
|
||||
<InBoxIcon class="text-primary size-[48px]" />
|
||||
</p>
|
||||
<p class="ant-upload-text">点击或者拖拽到此处上传[json]文件</p>
|
||||
</UploadDragger>
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { addFullName, cloneDeep, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
import {
|
||||
workflowDefinitionAdd,
|
||||
workflowDefinitionInfo,
|
||||
workflowDefinitionUpdate,
|
||||
} from '#/api/workflow/definition';
|
||||
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
|
||||
|
||||
import { modalSchema } from './data';
|
||||
|
||||
const emit = defineEmits<{ reload: [type: 'add' | 'update'] }>();
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const title = computed(() => {
|
||||
return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
|
||||
});
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 90,
|
||||
},
|
||||
schema: modalSchema(),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
async function setupCategorySelect() {
|
||||
// menu
|
||||
const tree = await categoryTree();
|
||||
addFullName(tree, 'label', ' / ');
|
||||
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
fieldNames: {
|
||||
label: 'label',
|
||||
value: 'id',
|
||||
},
|
||||
getPopupContainer,
|
||||
// 设置弹窗滚动高度 默认256
|
||||
listHeight: 300,
|
||||
showSearch: true,
|
||||
treeData: tree,
|
||||
treeDefaultExpandAll: true,
|
||||
// 默认展开的树节点
|
||||
// treeDefaultExpandedKeys: [0],
|
||||
treeLine: { showLeafIcon: false },
|
||||
// 筛选的字段
|
||||
treeNodeFilterProp: 'label',
|
||||
treeNodeLabelProp: 'fullName',
|
||||
},
|
||||
fieldName: 'category',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
|
||||
{
|
||||
initializedGetter: defaultFormValueGetter(formApi),
|
||||
currentGetter: defaultFormValueGetter(formApi),
|
||||
},
|
||||
);
|
||||
|
||||
const [BasicDrawer, modalApi] = useVbenModal({
|
||||
onBeforeClose,
|
||||
onClosed: handleClosed,
|
||||
onConfirm: handleConfirm,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { id } = modalApi.getData() as { id?: number | string };
|
||||
isUpdate.value = !!id;
|
||||
|
||||
// 加载分类树选择
|
||||
await setupCategorySelect();
|
||||
if (isUpdate.value && id) {
|
||||
const record = await workflowDefinitionInfo(id);
|
||||
await formApi.setValues(record);
|
||||
}
|
||||
await markInitialized();
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
async function handleConfirm() {
|
||||
try {
|
||||
modalApi.lock(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
if (isUpdate.value) {
|
||||
await workflowDefinitionUpdate(data);
|
||||
emit('reload', 'update');
|
||||
} else {
|
||||
await workflowDefinitionAdd(data);
|
||||
emit('reload', 'add');
|
||||
}
|
||||
resetInitialized();
|
||||
modalApi.close();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.lock(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClosed() {
|
||||
await formApi.resetForm();
|
||||
resetInitialized();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicDrawer :fullscreen-button="false" :title="title" class="w-[550px]">
|
||||
<div class="min-h-[400px]">
|
||||
<BasicForm />
|
||||
</div>
|
||||
</BasicDrawer>
|
||||
</template>
|
@@ -0,0 +1,91 @@
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { OptionsTag } from '#/components/table';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { activityStatusOptions } from '../processDefinition/constant';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
label: '任务名称',
|
||||
fieldName: 'nodeName',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
label: '流程名称',
|
||||
fieldName: 'flowName',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
label: '流程编码',
|
||||
fieldName: 'flowCode',
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
field: 'flowName',
|
||||
title: '流程名称',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'nodeName',
|
||||
title: '任务名称',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'flowCode',
|
||||
title: '流程编码',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'createByName',
|
||||
title: '申请人',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'version',
|
||||
title: '版本号',
|
||||
minWidth: 150,
|
||||
formatter: ({ cellValue }) => `V${cellValue}.0`,
|
||||
},
|
||||
{
|
||||
field: 'activityStatus',
|
||||
title: '状态',
|
||||
minWidth: 100,
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
const cellValue = row.activityStatus;
|
||||
return (
|
||||
<OptionsTag
|
||||
options={activityStatusOptions as any}
|
||||
value={cellValue}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'flowStatus',
|
||||
title: '流程状态',
|
||||
minWidth: 100,
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
resizable: false,
|
||||
width: 200,
|
||||
},
|
||||
];
|
@@ -0,0 +1,240 @@
|
||||
<script setup lang="ts">
|
||||
import type { RadioChangeEvent } from 'ant-design-vue';
|
||||
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, RadioGroup, Space } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteByInstanceIds,
|
||||
pageByFinish,
|
||||
pageByRunning,
|
||||
} from '#/api/workflow/instance';
|
||||
import CategoryTree from '#/views/workflow/processDefinition/category-tree.vue';
|
||||
|
||||
import { flowInfoModal } from '../components';
|
||||
import { columns, querySchema } from './data';
|
||||
import instanceInvalidModal from './instance-invalid-modal.vue';
|
||||
import instanceVariableModal from './instance-variable-modal.vue';
|
||||
|
||||
// 左边分类用
|
||||
const selectedCode = ref<number[] | string[]>([]);
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
schema: querySchema(),
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
handleReset: async () => {
|
||||
selectedCode.value = [];
|
||||
|
||||
const { formApi, reload } = tableApi;
|
||||
await formApi.resetForm();
|
||||
const formValues = formApi.form.values;
|
||||
formApi.setLatestSubmissionValues(formValues);
|
||||
await reload(formValues);
|
||||
},
|
||||
};
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '运行中', value: 'process_running' },
|
||||
{ label: '已完成', value: 'process_completed' },
|
||||
];
|
||||
let currentTypeApi = pageByRunning;
|
||||
const currentType = ref('process_running');
|
||||
async function handleTypeChange(e: RadioChangeEvent) {
|
||||
const { value } = e.target;
|
||||
switch (value) {
|
||||
case 'process_completed': {
|
||||
currentTypeApi = pageByFinish;
|
||||
break;
|
||||
}
|
||||
case 'process_running': {
|
||||
currentTypeApi = pageByRunning;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await tableApi.reload();
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
// 点击行选中
|
||||
trigger: 'default',
|
||||
},
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 部门树选择处理
|
||||
if (selectedCode.value.length === 1) {
|
||||
formValues.category = selectedCode.value[0];
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'category');
|
||||
}
|
||||
|
||||
return await currentTypeApi({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
headerCellConfig: {
|
||||
height: 44,
|
||||
},
|
||||
cellConfig: {
|
||||
height: 66,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
id: 'workflow-definition-index',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const [InstanceInvalidModal, instanceInvalidModalApi] = useVbenModal({
|
||||
connectedComponent: instanceInvalidModal,
|
||||
});
|
||||
async function handleInvalid(row: Recordable<any>) {
|
||||
instanceInvalidModalApi.setData({ id: row.id });
|
||||
instanceInvalidModalApi.open();
|
||||
}
|
||||
|
||||
async function handleDelete(row: Recordable<any>) {
|
||||
await deleteByInstanceIds(row.id);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
function handleMultiDelete() {
|
||||
const rows = tableApi.grid.getCheckboxRecords();
|
||||
const ids = rows.map((row: any) => row.id);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
okType: 'danger',
|
||||
content: `确认删除选中的${ids.length}条记录吗?`,
|
||||
onOk: async () => {
|
||||
await deleteByInstanceIds(ids);
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
const [InstanceVariableModal, instanceVariableModalApi] = useVbenModal({
|
||||
connectedComponent: instanceVariableModal,
|
||||
});
|
||||
function handleVariable(row: Recordable<any>) {
|
||||
instanceVariableModalApi.setData({ record: row.variable });
|
||||
instanceVariableModalApi.open();
|
||||
}
|
||||
const [FlowInfoModal, flowInfoModalApi] = useVbenModal({
|
||||
connectedComponent: flowInfoModal,
|
||||
});
|
||||
function handleInfo(row: any) {
|
||||
console.log(row);
|
||||
flowInfoModalApi.setData({ businessId: row.businessId });
|
||||
flowInfoModalApi.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-[8px]">
|
||||
<CategoryTree
|
||||
v-model:select-code="selectedCode"
|
||||
class="w-[260px]"
|
||||
@reload="() => tableApi.reload()"
|
||||
@select="() => tableApi.reload()"
|
||||
/>
|
||||
<BasicTable class="flex-1 overflow-hidden">
|
||||
<template #toolbar-actions>
|
||||
<RadioGroup
|
||||
v-model:value="currentType"
|
||||
:options="typeOptions"
|
||||
button-style="solid"
|
||||
option-type="button"
|
||||
@change="handleTypeChange"
|
||||
/>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
:disabled="!vxeCheckboxChecked(tableApi)"
|
||||
danger
|
||||
type="primary"
|
||||
v-access:code="['system:user:remove']"
|
||||
@click="handleMultiDelete"
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<div class="flex flex-col">
|
||||
<div v-if="currentType === 'process_running'">
|
||||
<a-button
|
||||
danger
|
||||
size="small"
|
||||
type="link"
|
||||
@click.stop="handleInvalid(row)"
|
||||
>
|
||||
作废流程
|
||||
</a-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<a-button danger size="small" type="link" @click.stop="">
|
||||
删除流程
|
||||
</a-button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
<div>
|
||||
<a-button size="small" type="link" @click.stop="handleInfo(row)">
|
||||
流程预览
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
type="link"
|
||||
@click.stop="handleVariable(row)"
|
||||
>
|
||||
变量查看
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
<InstanceInvalidModal @reload="() => tableApi.reload()" />
|
||||
<InstanceVariableModal />
|
||||
<FlowInfoModal />
|
||||
</Page>
|
||||
</template>
|
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { workflowInstanceInvalid } from '#/api/workflow/instance';
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
onConfirm: handleSubmit,
|
||||
onCancel: handleCancel,
|
||||
fullscreenButton: false,
|
||||
title: '作废原因',
|
||||
});
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-2',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'vertical',
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'comment',
|
||||
label: '作废原因',
|
||||
component: 'Textarea',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
async function handleCancel() {
|
||||
modalApi.close();
|
||||
await formApi.resetForm();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
data.id = modalApi.getData().id;
|
||||
await workflowInstanceInvalid(data as any);
|
||||
emit('reload');
|
||||
handleCancel();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<BasicForm />
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { JsonPreview, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
const data = ref({});
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '流程变量',
|
||||
fullscreenButton: false,
|
||||
footer: false,
|
||||
onOpenChange: (visible) => {
|
||||
if (!visible) {
|
||||
data.value = {};
|
||||
return null;
|
||||
}
|
||||
const recordString = modalApi.getData().record;
|
||||
data.value = JSON.parse(recordString);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<div class="min-h-[400px] overflow-y-auto">
|
||||
<JsonPreview :data="data" />
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,360 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Popover,
|
||||
Segmented,
|
||||
Spin,
|
||||
Tooltip,
|
||||
TreeSelect,
|
||||
} from 'ant-design-vue';
|
||||
import { cloneDeep, debounce, uniqueId } from 'lodash-es';
|
||||
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
import { pageByAllTaskFinish, pageByAllTaskWait } from '#/api/workflow/task';
|
||||
|
||||
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||
import { bottomOffset } from './constant';
|
||||
|
||||
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
/**
|
||||
* 流程监控 - 待办任务页面的id不唯一 改为前端处理
|
||||
*/
|
||||
interface TaskItem extends TaskInfo {
|
||||
active: boolean;
|
||||
randomId: string;
|
||||
}
|
||||
|
||||
const taskList = ref<TaskItem[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '待办任务', value: 'todo' },
|
||||
{ label: '已办任务', value: 'done' },
|
||||
];
|
||||
const currentType = ref('todo');
|
||||
const currentApi = computed(() => {
|
||||
if (currentType.value === 'todo') {
|
||||
return pageByAllTaskWait;
|
||||
}
|
||||
return pageByAllTaskFinish;
|
||||
});
|
||||
const approvalType = computed(() => {
|
||||
if (currentType.value === 'done') {
|
||||
return 'readonly';
|
||||
}
|
||||
return 'admin';
|
||||
});
|
||||
async function handleTypeChange() {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
page.value = 1;
|
||||
|
||||
taskList.value = [];
|
||||
await nextTick();
|
||||
await reload(true);
|
||||
}
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
createByIds: [] as string[], // 创建人
|
||||
category: null as null | number, // 流程分类
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
selectedUserList.value = [];
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await currentApi.value({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({
|
||||
...item,
|
||||
active: false,
|
||||
randomId: uniqueId(),
|
||||
}));
|
||||
taskTotal.value = resp.total;
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||
// e.target.clientHeight 是元素的可视高度。
|
||||
// e.target.scrollHeight 是元素的总高度。
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
|
||||
console.log('scrollTop + clientHeight', scrollTop + clientHeight);
|
||||
console.log('scrollHeight', scrollHeight);
|
||||
|
||||
// 滚动到底部且没有加载完成
|
||||
if (isBottom && !isLoadComplete.value) {
|
||||
loading.value = true;
|
||||
page.value += 1;
|
||||
const resp = await currentApi.value({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value.push(
|
||||
...resp.rows.map((item) => ({
|
||||
...item,
|
||||
active: false,
|
||||
randomId: uniqueId(),
|
||||
})),
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const lastSelectId = ref('');
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskItem) {
|
||||
const { randomId } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === randomId) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.randomId === randomId;
|
||||
});
|
||||
lastSelectId.value = randomId;
|
||||
}
|
||||
|
||||
const { refreshTab } = useTabs();
|
||||
|
||||
// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示
|
||||
const popoverOpen = ref(false);
|
||||
const selectedUserList = ref<User[]>([]);
|
||||
function handleFinish(userList: User[]) {
|
||||
popoverOpen.value = true;
|
||||
selectedUserList.value = userList;
|
||||
formData.value.createByIds = userList.map((item) => item.userId);
|
||||
}
|
||||
|
||||
const treeData = ref<any[]>([]);
|
||||
onMounted(async () => {
|
||||
// menu
|
||||
const tree = await categoryTree();
|
||||
addFullName(tree, 'label', ' / ');
|
||||
treeData.value = tree;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<Segmented
|
||||
v-model:value="currentType"
|
||||
:options="typeOptions"
|
||||
block
|
||||
class="mb-2"
|
||||
@change="handleTypeChange"
|
||||
/>
|
||||
<div class="flex items-center gap-1">
|
||||
<InputSearch
|
||||
v-model:value="formData.flowName"
|
||||
placeholder="流程名称搜索"
|
||||
@search="reload(false)"
|
||||
/>
|
||||
<Tooltip placement="top" title="重置">
|
||||
<a-button @click="reload(true)">
|
||||
<RedoOutlined />
|
||||
</a-button>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
v-model:open="popoverOpen"
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="rightTop"
|
||||
trigger="click"
|
||||
>
|
||||
<template #title>
|
||||
<div class="w-full border-b pb-[12px] text-[16px]">搜索</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<Form
|
||||
:colon="false"
|
||||
:label-col="{ span: 6 }"
|
||||
:model="formData"
|
||||
autocomplete="off"
|
||||
class="w-[300px]"
|
||||
@finish="() => reload(false)"
|
||||
>
|
||||
<FormItem label="申请人">
|
||||
<!-- 弹窗关闭后仍然显示表单浮层 -->
|
||||
<CopyComponent
|
||||
v-model:user-list="selectedUserList"
|
||||
@cancel="() => (popoverOpen = true)"
|
||||
@finish="handleFinish"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程分类">
|
||||
<TreeSelect
|
||||
v-model:value="formData.category"
|
||||
:allow-clear="true"
|
||||
:field-names="{ label: 'label', value: 'id' }"
|
||||
:get-popup-container="getPopupContainer"
|
||||
:tree-data="treeData"
|
||||
:tree-default-expand-all="true"
|
||||
:tree-line="{ showLeafIcon: false }"
|
||||
placeholder="请选择"
|
||||
tree-node-filter-prop="label"
|
||||
tree-node-label-prop="fullName"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="任务名称">
|
||||
<Input
|
||||
v-model:value="formData.nodeName"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程编码">
|
||||
<Input
|
||||
v-model:value="formData.flowCode"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<div class="flex">
|
||||
<a-button block html-type="submit" type="primary">
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button block class="ml-2" @click="reload(true)">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
||||
<a-button>
|
||||
<FilterOutlined />
|
||||
</a-button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="cardContainerRef"
|
||||
class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<template v-if="taskList.length > 0">
|
||||
<ApprovalCard
|
||||
v-for="item in taskList"
|
||||
:key="item.randomId"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
row-key="randomId"
|
||||
@click="handleCardClick(item)"
|
||||
/>
|
||||
</template>
|
||||
<Empty v-else :image="emptyImage" />
|
||||
<div
|
||||
v-if="isLoadComplete && taskList.length > 0"
|
||||
class="flex items-center justify-center text-[14px] opacity-50"
|
||||
>
|
||||
没有更多数据了
|
||||
</div>
|
||||
<!-- 遮罩loading层 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
|
||||
>
|
||||
<Spin tip="加载中..." />
|
||||
</div>
|
||||
</div>
|
||||
<!-- total显示 -->
|
||||
<div
|
||||
class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
共 {{ taskTotal }} 条记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ApprovalPanel
|
||||
:task="currentTask"
|
||||
:type="approvalType"
|
||||
@reload="refreshTab"
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
@apply thin-scrollbar;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 底部偏移量
|
||||
* 在缩放时会差大概0.5px 导致触底逻辑不会触发
|
||||
* 在这里设置手动补偿
|
||||
* @see https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC28RE#note_40175381
|
||||
*/
|
||||
export const bottomOffset = 2;
|
@@ -0,0 +1,257 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Popover,
|
||||
Spin,
|
||||
Tooltip,
|
||||
} from 'ant-design-vue';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
|
||||
import { pageByCurrent } from '#/api/workflow/instance';
|
||||
|
||||
import { ApprovalCard, ApprovalPanel } from '../components';
|
||||
import { bottomOffset } from './constant';
|
||||
|
||||
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
category: null as null | number, // 流程分类
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await pageByCurrent({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
|
||||
taskTotal.value = resp.total;
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||
// e.target.clientHeight 是元素的可视高度。
|
||||
// e.target.scrollHeight 是元素的总高度。
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
|
||||
|
||||
// 滚动到底部且没有加载完成
|
||||
if (isBottom && !isLoadComplete.value) {
|
||||
loading.value = true;
|
||||
page.value += 1;
|
||||
const resp = await pageByCurrent({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value.push(
|
||||
...resp.rows.map((item) => ({ ...item, active: false })),
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const lastSelectId = ref('');
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskInfo) {
|
||||
const { id } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === id) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.id === id;
|
||||
});
|
||||
lastSelectId.value = id;
|
||||
}
|
||||
|
||||
const { refreshTab } = useTabs();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<InputSearch
|
||||
v-model:value="formData.flowName"
|
||||
placeholder="流程名称搜索"
|
||||
@search="reload(false)"
|
||||
/>
|
||||
<Tooltip placement="top" title="重置">
|
||||
<a-button @click="reload(true)">
|
||||
<RedoOutlined />
|
||||
</a-button>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="rightTop"
|
||||
trigger="click"
|
||||
>
|
||||
<template #title>
|
||||
<div class="w-full border-b pb-[12px] text-[16px]">搜索</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<Form
|
||||
:colon="false"
|
||||
:label-col="{ span: 6 }"
|
||||
:model="formData"
|
||||
autocomplete="off"
|
||||
class="w-[300px]"
|
||||
@finish="() => reload(false)"
|
||||
>
|
||||
<FormItem label="任务名称">
|
||||
<Input
|
||||
v-model:value="formData.nodeName"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程编码">
|
||||
<Input
|
||||
v-model:value="formData.flowCode"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<div class="flex">
|
||||
<a-button block html-type="submit" type="primary">
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button block class="ml-2" @click="reload(true)">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
||||
<a-button>
|
||||
<FilterOutlined />
|
||||
</a-button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="cardContainerRef"
|
||||
class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<template v-if="taskList.length > 0">
|
||||
<ApprovalCard
|
||||
v-for="item in taskList"
|
||||
:key="item.id"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
@click="handleCardClick(item)"
|
||||
/>
|
||||
</template>
|
||||
<Empty v-else :image="emptyImage" />
|
||||
<div
|
||||
v-if="isLoadComplete && taskList.length > 0"
|
||||
class="flex items-center justify-center text-[14px] opacity-50"
|
||||
>
|
||||
没有更多数据了
|
||||
</div>
|
||||
<!-- 遮罩loading层 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
|
||||
>
|
||||
<Spin tip="加载中..." />
|
||||
</div>
|
||||
</div>
|
||||
<!-- total显示 -->
|
||||
<div
|
||||
class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
共 {{ taskTotal }} 条记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ApprovalPanel :task="currentTask" type="myself" @reload="refreshTab" />
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
@apply thin-scrollbar;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,299 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Popover,
|
||||
Spin,
|
||||
Tooltip,
|
||||
TreeSelect,
|
||||
} from 'ant-design-vue';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
import { pageByTaskCopy } from '#/api/workflow/task';
|
||||
|
||||
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||
import { bottomOffset } from './constant';
|
||||
|
||||
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
createByIds: [] as string[], // 创建人
|
||||
category: null as null | number, // 流程分类
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
selectedUserList.value = [];
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await pageByTaskCopy({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
|
||||
taskTotal.value = resp.total;
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||
// e.target.clientHeight 是元素的可视高度。
|
||||
// e.target.scrollHeight 是元素的总高度。
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
|
||||
|
||||
// 滚动到底部且没有加载完成
|
||||
if (isBottom && !isLoadComplete.value) {
|
||||
loading.value = true;
|
||||
page.value += 1;
|
||||
const resp = await pageByTaskCopy({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value.push(
|
||||
...resp.rows.map((item) => ({ ...item, active: false })),
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const lastSelectId = ref('');
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskInfo) {
|
||||
const { id } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === id) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.id === id;
|
||||
});
|
||||
lastSelectId.value = id;
|
||||
}
|
||||
|
||||
// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示
|
||||
const popoverOpen = ref(false);
|
||||
const selectedUserList = ref<User[]>([]);
|
||||
function handleFinish(userList: User[]) {
|
||||
popoverOpen.value = true;
|
||||
selectedUserList.value = userList;
|
||||
formData.value.createByIds = userList.map((item) => item.userId);
|
||||
}
|
||||
|
||||
const treeData = ref<any[]>([]);
|
||||
onMounted(async () => {
|
||||
// menu
|
||||
const tree = await categoryTree();
|
||||
addFullName(tree, 'label', ' / ');
|
||||
treeData.value = tree;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<InputSearch
|
||||
v-model:value="formData.flowName"
|
||||
placeholder="流程名称搜索"
|
||||
@search="reload(false)"
|
||||
/>
|
||||
<Tooltip placement="top" title="重置">
|
||||
<a-button @click="reload(true)">
|
||||
<RedoOutlined />
|
||||
</a-button>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
v-model:open="popoverOpen"
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="rightTop"
|
||||
trigger="click"
|
||||
>
|
||||
<template #title>
|
||||
<div class="w-full border-b pb-[12px] text-[16px]">搜索</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<Form
|
||||
:colon="false"
|
||||
:label-col="{ span: 6 }"
|
||||
:model="formData"
|
||||
autocomplete="off"
|
||||
class="w-[300px]"
|
||||
@finish="() => reload(false)"
|
||||
>
|
||||
<FormItem label="申请人">
|
||||
<!-- 弹窗关闭后仍然显示表单浮层 -->
|
||||
<CopyComponent
|
||||
v-model:user-list="selectedUserList"
|
||||
@cancel="() => (popoverOpen = true)"
|
||||
@finish="handleFinish"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程分类">
|
||||
<TreeSelect
|
||||
v-model:value="formData.category"
|
||||
:allow-clear="true"
|
||||
:field-names="{ label: 'label', value: 'id' }"
|
||||
:get-popup-container="getPopupContainer"
|
||||
:tree-data="treeData"
|
||||
:tree-default-expand-all="true"
|
||||
:tree-line="{ showLeafIcon: false }"
|
||||
placeholder="请选择"
|
||||
tree-node-filter-prop="label"
|
||||
tree-node-label-prop="fullName"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="任务名称">
|
||||
<Input
|
||||
v-model:value="formData.nodeName"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程编码">
|
||||
<Input
|
||||
v-model:value="formData.flowCode"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<div class="flex">
|
||||
<a-button block html-type="submit" type="primary">
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button block class="ml-2" @click="reload(true)">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
||||
<a-button>
|
||||
<FilterOutlined />
|
||||
</a-button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="cardContainerRef"
|
||||
class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<template v-if="taskList.length > 0">
|
||||
<ApprovalCard
|
||||
v-for="item in taskList"
|
||||
:key="item.id"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
@click="handleCardClick(item)"
|
||||
/>
|
||||
</template>
|
||||
<Empty v-else :image="emptyImage" />
|
||||
<div
|
||||
v-if="isLoadComplete && taskList.length > 0"
|
||||
class="flex items-center justify-center text-[14px] opacity-50"
|
||||
>
|
||||
没有更多数据了
|
||||
</div>
|
||||
<!-- 遮罩loading层 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
|
||||
>
|
||||
<Spin tip="加载中..." />
|
||||
</div>
|
||||
</div>
|
||||
<!-- total显示 -->
|
||||
<div
|
||||
class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
共 {{ taskTotal }} 条记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ApprovalPanel :task="currentTask" type="readonly" />
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
@apply thin-scrollbar;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,299 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Popover,
|
||||
Spin,
|
||||
Tooltip,
|
||||
TreeSelect,
|
||||
} from 'ant-design-vue';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
import { pageByTaskFinish } from '#/api/workflow/task';
|
||||
|
||||
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||
import { bottomOffset } from './constant';
|
||||
|
||||
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
createByIds: [] as string[], // 创建人
|
||||
category: null as null | number, // 流程分类
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
selectedUserList.value = [];
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await pageByTaskFinish({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
|
||||
taskTotal.value = resp.total;
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||
// e.target.clientHeight 是元素的可视高度。
|
||||
// e.target.scrollHeight 是元素的总高度。
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
|
||||
|
||||
// 滚动到底部且没有加载完成
|
||||
if (isBottom && !isLoadComplete.value) {
|
||||
loading.value = true;
|
||||
page.value += 1;
|
||||
const resp = await pageByTaskFinish({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value.push(
|
||||
...resp.rows.map((item) => ({ ...item, active: false })),
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const lastSelectId = ref('');
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskInfo) {
|
||||
const { id } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === id) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.id === id;
|
||||
});
|
||||
lastSelectId.value = id;
|
||||
}
|
||||
|
||||
// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示
|
||||
const popoverOpen = ref(false);
|
||||
const selectedUserList = ref<User[]>([]);
|
||||
function handleFinish(userList: User[]) {
|
||||
popoverOpen.value = true;
|
||||
selectedUserList.value = userList;
|
||||
formData.value.createByIds = userList.map((item) => item.userId);
|
||||
}
|
||||
|
||||
const treeData = ref<any[]>([]);
|
||||
onMounted(async () => {
|
||||
// menu
|
||||
const tree = await categoryTree();
|
||||
addFullName(tree, 'label', ' / ');
|
||||
treeData.value = tree;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<InputSearch
|
||||
v-model:value="formData.flowName"
|
||||
placeholder="流程名称搜索"
|
||||
@search="reload(false)"
|
||||
/>
|
||||
<Tooltip placement="top" title="重置">
|
||||
<a-button @click="reload(true)">
|
||||
<RedoOutlined />
|
||||
</a-button>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
v-model:open="popoverOpen"
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="rightTop"
|
||||
trigger="click"
|
||||
>
|
||||
<template #title>
|
||||
<div class="w-full border-b pb-[12px] text-[16px]">搜索</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<Form
|
||||
:colon="false"
|
||||
:label-col="{ span: 6 }"
|
||||
:model="formData"
|
||||
autocomplete="off"
|
||||
class="w-[300px]"
|
||||
@finish="() => reload(false)"
|
||||
>
|
||||
<FormItem label="申请人">
|
||||
<!-- 弹窗关闭后仍然显示表单浮层 -->
|
||||
<CopyComponent
|
||||
v-model:user-list="selectedUserList"
|
||||
@cancel="() => (popoverOpen = true)"
|
||||
@finish="handleFinish"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程分类">
|
||||
<TreeSelect
|
||||
v-model:value="formData.category"
|
||||
:allow-clear="true"
|
||||
:field-names="{ label: 'label', value: 'id' }"
|
||||
:get-popup-container="getPopupContainer"
|
||||
:tree-data="treeData"
|
||||
:tree-default-expand-all="true"
|
||||
:tree-line="{ showLeafIcon: false }"
|
||||
placeholder="请选择"
|
||||
tree-node-filter-prop="label"
|
||||
tree-node-label-prop="fullName"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="任务名称">
|
||||
<Input
|
||||
v-model:value="formData.nodeName"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程编码">
|
||||
<Input
|
||||
v-model:value="formData.flowCode"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<div class="flex">
|
||||
<a-button block html-type="submit" type="primary">
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button block class="ml-2" @click="reload(true)">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
||||
<a-button>
|
||||
<FilterOutlined />
|
||||
</a-button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="cardContainerRef"
|
||||
class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<template v-if="taskList.length > 0">
|
||||
<ApprovalCard
|
||||
v-for="item in taskList"
|
||||
:key="item.id"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
@click="handleCardClick(item)"
|
||||
/>
|
||||
</template>
|
||||
<Empty v-else :image="emptyImage" />
|
||||
<div
|
||||
v-if="isLoadComplete && taskList.length > 0"
|
||||
class="flex items-center justify-center text-[14px] opacity-50"
|
||||
>
|
||||
没有更多数据了
|
||||
</div>
|
||||
<!-- 遮罩loading层 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
|
||||
>
|
||||
<Spin tip="加载中..." />
|
||||
</div>
|
||||
</div>
|
||||
<!-- total显示 -->
|
||||
<div
|
||||
class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
共 {{ taskTotal }} 条记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ApprovalPanel :task="currentTask" type="readonly" />
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
@apply thin-scrollbar;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,302 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Popover,
|
||||
Spin,
|
||||
Tooltip,
|
||||
TreeSelect,
|
||||
} from 'ant-design-vue';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
import { pageByTaskWait } from '#/api/workflow/task';
|
||||
|
||||
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||
import { bottomOffset } from './constant';
|
||||
|
||||
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
createByIds: [] as string[], // 创建人
|
||||
category: null as null | number, // 流程分类
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
selectedUserList.value = [];
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await pageByTaskWait({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
|
||||
taskTotal.value = resp.total;
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||
// e.target.clientHeight 是元素的可视高度。
|
||||
// e.target.scrollHeight 是元素的总高度。
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
|
||||
|
||||
// 滚动到底部且没有加载完成
|
||||
if (isBottom && !isLoadComplete.value) {
|
||||
loading.value = true;
|
||||
page.value += 1;
|
||||
const resp = await pageByTaskWait({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value.push(
|
||||
...resp.rows.map((item) => ({ ...item, active: false })),
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const lastSelectId = ref('');
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskInfo) {
|
||||
const { id } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === id) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.id === id;
|
||||
});
|
||||
lastSelectId.value = id;
|
||||
}
|
||||
|
||||
const { refreshTab } = useTabs();
|
||||
|
||||
// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示
|
||||
const popoverOpen = ref(false);
|
||||
const selectedUserList = ref<User[]>([]);
|
||||
function handleFinish(userList: User[]) {
|
||||
popoverOpen.value = true;
|
||||
selectedUserList.value = userList;
|
||||
formData.value.createByIds = userList.map((item) => item.userId);
|
||||
}
|
||||
|
||||
const treeData = ref<any[]>([]);
|
||||
onMounted(async () => {
|
||||
// menu
|
||||
const tree = await categoryTree();
|
||||
addFullName(tree, 'label', ' / ');
|
||||
treeData.value = tree;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<InputSearch
|
||||
v-model:value="formData.flowName"
|
||||
placeholder="流程名称搜索"
|
||||
@search="reload(false)"
|
||||
/>
|
||||
<Tooltip placement="top" title="重置">
|
||||
<a-button @click="reload(true)">
|
||||
<RedoOutlined />
|
||||
</a-button>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
v-model:open="popoverOpen"
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="rightTop"
|
||||
trigger="click"
|
||||
>
|
||||
<template #title>
|
||||
<div class="w-full border-b pb-[12px] text-[16px]">搜索</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<Form
|
||||
:colon="false"
|
||||
:label-col="{ span: 6 }"
|
||||
:model="formData"
|
||||
autocomplete="off"
|
||||
class="w-[300px]"
|
||||
@finish="() => reload(false)"
|
||||
>
|
||||
<FormItem label="申请人">
|
||||
<!-- 弹窗关闭后仍然显示表单浮层 -->
|
||||
<CopyComponent
|
||||
v-model:user-list="selectedUserList"
|
||||
@cancel="() => (popoverOpen = true)"
|
||||
@finish="handleFinish"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程分类">
|
||||
<TreeSelect
|
||||
v-model:value="formData.category"
|
||||
:allow-clear="true"
|
||||
:field-names="{ label: 'label', value: 'id' }"
|
||||
:get-popup-container="getPopupContainer"
|
||||
:tree-data="treeData"
|
||||
:tree-default-expand-all="true"
|
||||
:tree-line="{ showLeafIcon: false }"
|
||||
placeholder="请选择"
|
||||
tree-node-filter-prop="label"
|
||||
tree-node-label-prop="fullName"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="任务名称">
|
||||
<Input
|
||||
v-model:value="formData.nodeName"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程编码">
|
||||
<Input
|
||||
v-model:value="formData.flowCode"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<div class="flex">
|
||||
<a-button block html-type="submit" type="primary">
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button block class="ml-2" @click="reload(true)">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
||||
<a-button>
|
||||
<FilterOutlined />
|
||||
</a-button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="cardContainerRef"
|
||||
class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<template v-if="taskList.length > 0">
|
||||
<ApprovalCard
|
||||
v-for="item in taskList"
|
||||
:key="item.id"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
@click="handleCardClick(item)"
|
||||
/>
|
||||
</template>
|
||||
<Empty v-else :image="emptyImage" />
|
||||
<div
|
||||
v-if="isLoadComplete && taskList.length > 0"
|
||||
class="flex items-center justify-center text-[14px] opacity-50"
|
||||
>
|
||||
没有更多数据了
|
||||
</div>
|
||||
<!-- 遮罩loading层 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
|
||||
>
|
||||
<Spin tip="加载中..." />
|
||||
</div>
|
||||
</div>
|
||||
<!-- total显示 -->
|
||||
<div
|
||||
class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
共 {{ taskTotal }} 条记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ApprovalPanel :task="currentTask" type="approve" @reload="refreshTab" />
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
@apply thin-scrollbar;
|
||||
}
|
||||
</style>
|
@@ -111,11 +111,12 @@ async function handleConfirm() {
|
||||
const data = cloneDeep(await formApi.getValues())
|
||||
// 通道信息
|
||||
const filteredChannels = dynamicValidateForm.floor
|
||||
.filter(item => !(item.out.length === 0 && item.in.length === 0))
|
||||
.filter(item => !(item.outUp.length === 0 && item.outDown.length === 0 && item.in.length === 0))
|
||||
.map(item => ({
|
||||
floorId: item.id,
|
||||
inChannel: item.in,
|
||||
outChannel: item.out
|
||||
upChannel: item.outUp,
|
||||
downChannel: item.outDown,
|
||||
}))
|
||||
|
||||
data.channels = filteredChannels
|
||||
@@ -223,7 +224,8 @@ async function setupCommunitySelect() {
|
||||
}
|
||||
|
||||
interface floor {
|
||||
out: string
|
||||
outUp: string
|
||||
outDown: string
|
||||
in: string
|
||||
num: string | number
|
||||
id: string | number
|
||||
@@ -238,7 +240,8 @@ async function handleGetFloor(unitId: string | number) {
|
||||
floorList.value = []
|
||||
res.forEach((item) => {
|
||||
floorList.value.push({
|
||||
out: '',
|
||||
outUp: '',
|
||||
outDown: '',
|
||||
in: '',
|
||||
num: item.floorNumber,
|
||||
id: item.id,
|
||||
@@ -268,10 +271,14 @@ async function handleClosed() {
|
||||
<Form :model="dynamicValidateForm" layout="inline">
|
||||
<Space v-for="(floor, index) in dynamicValidateForm.floor" :key="floor.id"
|
||||
style="display: flex; margin-bottom: 8px" align="baseline">
|
||||
<FormItem :label="'楼层' + (floor.num)" :name="['floor', index, 'out']">
|
||||
<Input v-model:value="floor.out" placeholder="外部按键通道" />
|
||||
<span>{{ "楼层"+floor.num }}</span>
|
||||
<FormItem label="上键通道" :name="['floor', index, 'outUp']">
|
||||
<Input v-model:value="floor.outUp" placeholder="外部按键通道" />
|
||||
</FormItem>
|
||||
<FormItem :name="['floor', index, 'in']">
|
||||
<FormItem label="下键通道" :name="['floor', index, 'outDown']">
|
||||
<Input v-model:value="floor.outDown" placeholder="内部按键通道" />
|
||||
</FormItem>
|
||||
<FormItem label="楼层通道" :name="['floor', index, 'in']">
|
||||
<Input v-model:value="floor.in" placeholder="内部按键通道" />
|
||||
</FormItem>
|
||||
</Space>
|
||||
|
Reference in New Issue
Block a user