Files
admin-vben5/apps/web-antd/src/views/property/clean/cleanOrders/clean-modal.vue
2025-08-22 19:43:32 +08:00

742 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { CleanVO } from '#/api/property/clean/model';
import { h } from 'vue';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep, handleNode, getPopupContainer } from '@vben/utils';
import { Button, Table } from 'ant-design-vue';
import dayjs from 'dayjs';
import { useVbenForm } from '#/adapter/form';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { cleanList } from '#/api/property/clean';
import {
clean_orderAdd,
clean_orderInfo,
clean_orderUpdate,
} from '#/api/property/clean_order';
import { resident_unitList } from '#/api/property/resident/unit';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import cleanDetailModal from './clean-detail-modal.vue';
// import { modalSchema } from './data';
import { communityTree } from '#/api/property/community';
import { getDictOptions } from '#/utils/dict';
const emit = defineEmits<{ reload: [] }>();
// 计算合计费用
const totalSumPeices = computed(() => {
return detailTable.value
.reduce(
(total: number, item: any) => total + (Number(item.sumPeices) || 0),
0,
)
.toFixed(2);
});
const isUpdate = ref(false);
const isReadonly = ref(false);
const isAudit = ref(false);
const isRefund = ref(false);
const rowData = ref<any>({});
const title = computed(() => {
return isUpdate.value
? $t('pages.common.edit')
: isReadonly.value
? '详情'
: $t('pages.common.add');
});
// 用来缓存 cleanList 的完整数据
let cleanListData: CleanVO[] = [];
// 在文件顶部加缓存
let unitListData: { id: any; name: string }[] = [];
const editUnitId = ref('');
const editCleanOrderId = ref('');
const detailModal = ref(null);
const modalSchema = [
{
label: '主键id',
fieldName: 'id',
component: 'Input',
dependencies: {
show: () => false,
triggerFields: [''],
},
},
{
label: '服务地址(房间号)',
component: 'TreeSelect',
defaultValue: undefined,
fieldName: 'location',
rules: 'required',
},
{
label: '开始时间',
fieldName: 'starTime',
component: 'DatePicker',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
placeholder: '请选择开始时间',
},
rules: 'required',
},
{
label: '结束时间',
fieldName: 'endTime',
component: 'DatePicker',
componentProps: (values: any) => ({
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
placeholder: !values.starTime
? '请先选择开始时间'
: `请选择结束时间(结束时间不能早于开始时间)`,
disabled: isReadonly.value || !values.starTime, // 没选开始时间时禁用
disabledDate: (current: any) => {
if (!values.starTime) return false;
// 只允许选择大于等于开始时间的日期
return current && current < dayjs(values.starTime).startOf('second');
},
}),
rules: 'required',
dependencies: {
triggerFields: ['starTime'],
},
},
{
label: '申请人',
fieldName: 'persion',
component: 'Input',
rules: 'required',
},
{
label: '联系电话',
fieldName: 'phone',
component: 'Input',
rules: 'required',
},
{
label: '申请单位',
fieldName: 'unitId',
component: 'ApiSelect',
componentProps: {
api: resident_unitList,
resultField: 'rows',
labelField: 'name',
valueField: 'id',
placeholder: '请选择单位',
},
rules: 'required',
},
{
label: '支付状态',
fieldName: 'payState',
component: 'Select',
componentProps: {
options: [
{
label: '待支付',
value: 0,
},
{
label: '已支付',
value: 1,
},
],
},
rules: 'required',
},
{
label: '保洁类型',
fieldName: 'type',
component: 'Select',
componentProps: {
options: getDictOptions('pro_cleaning_type'),
},
rules: 'selectRequired',
},
{
label: '评价',
fieldName: 'serviceEvalua',
component: 'Rate',
componentProps: {
allowHalf: false,
count: 5,
tooltips: ['1星', '2星', '3星', '4星', '5星'],
defaultValue: 0,
},
dependencies: {
show: () => (isReadonly.value ? true : false),
triggerFields: [''],
},
},
{
label: '评价文本',
fieldName: 'serviceEvaluaText',
component: 'Input',
dependencies: {
show: () => (isReadonly.value ? true : false),
triggerFields: [''],
},
},
{
label: '评价图片',
fieldName: 'imgUrl',
component: 'Input',
dependencies: {
show: () => (isReadonly.value ? true : false),
triggerFields: [''],
},
},
{
label: '签到方式',
fieldName: 'signType',
component: 'Select',
componentProps: {
options: getDictOptions('wy_bjqdfs'),
},
dependencies: {
show: () => (isReadonly.value ? true : false),
triggerFields: [''],
},
},
{
label: '签到图片',
fieldName: 'signImgUrl',
component: 'ImageUpload',
componentProps: {
helpMessage: false,
},
dependencies: {
show: (formValue) =>
isReadonly.value && formValue.signImgUrl ? true : false,
triggerFields: [''],
},
},
];
const [BasicForm, formApi] = useVbenForm({
commonConfig: {
// 默认占满两列
formItemClass: 'col-span-1',
// 默认label宽度 px
labelWidth: 120,
// 通用配置项 会影响到所有表单项
componentProps: computed(() => ({
class: 'w-full',
disabled: isReadonly.value,
})),
},
// 1. 使用正确的属性名 handleValuesChange
handleValuesChange: async (values, fieldsChanged) => {
// 2. fieldsChanged 是一个包含变化字段名的数组
if (fieldsChanged.includes('name')) {
// 如果缓存数据为空,先请求一次并缓存
if (cleanListData.length === 0) {
try {
const res = await cleanList(); // 查询所有
cleanListData = res.rows || [];
} catch (error) {
console.error('获取劳务列表失败:', error);
cleanListData = []; // 出错时清空
}
}
// 3. 从 values 中获取当前选中的 id
const selectedId = values.name;
const selectedItem = cleanListData.find((item) => item.id === selectedId);
if (selectedItem) {
// 4. 使用正确的 formApi.setValues 方法
await formApi.setValues({
prices: selectedItem.peices, // 服务单价
frequency: selectedItem.frequency, // 保洁频率
standard: selectedItem.standard, // 保洁内容
peices: selectedItem.peices, // 保洁标准
});
}
}
},
// schema: computed(() => {
// modalSchema(isReadonly.value);
// }),
schema: modalSchema,
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicModal, modalApi] = useVbenModal({
// 在这里更改宽度
class: 'w-[70%]',
fullscreenButton: false,
onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm,
onOpenChange: async (isOpen) => {
if (!isOpen) {
return null;
}
// 查询服务地址树形结构
setupCommunitySelect();
modalApi.modalLoading(true);
const { id, readonly, audit, refund, row } = modalApi.getData() as {
id?: string;
readonly?: boolean;
audit?: boolean;
refund?: boolean;
row?: any;
};
editCleanOrderId.value = id || '';
isReadonly.value = !!readonly;
isAudit.value = !!audit;
isRefund.value = !!refund;
rowData.value = row;
//判断是否是编辑状态需要先判断是否是只读状态
if (isReadonly.value) {
isUpdate.value = false;
} else {
isUpdate.value = !!id;
}
if ((isUpdate.value || isReadonly.value) && id) {
const record: any = await clean_orderInfo(id);
if (record.starTime) record.starTime = dayjs(record.starTime);
if (record.endTime) record.endTime = dayjs(record.endTime);
editUnitId.value = record.unitId || '';
detailTable.value = record.cleanList || [];
for (const item of record.relationList) {
for (let i = 0; i < detailTable.value.length; i++) {
if (item.cleanId === detailTable.value[i].id) {
detailTable.value[i].area = item.areas;
detailTable.value[i].sumPeices = item.sumPrice;
}
}
}
await formApi.setValues(record);
}
await markInitialized();
modalApi.modalLoading(false);
},
});
// 添加订单详情表格配置
const detailGridOptions: VxeGridProps = {
height: '300px',
columns: [
{
title: '序号',
field: 'index',
width: 'auto',
slots: {
default: ({ rowIndex }) => {
return (rowIndex + 1).toString();
},
},
},
{
title: '劳务名称',
field: 'name',
width: 'auto',
},
{
title: '计量单位',
field: 'measure',
width: 'auto',
},
{
title: '计算方式',
field: 'method',
width: 'auto',
},
{
title: '申报单价含税(元)',
field: 'peices',
width: 'auto',
},
{
title: '保洁频率',
field: 'frequency',
width: 'auto',
},
{
title: '保洁标准',
field: 'standard',
width: 'auto',
},
{
title: '备注',
field: 'remark',
width: 'auto',
},
{
title: '状态',
field: 'stater',
width: 'auto',
slots: {
default: ({ row }) => (row.stater === 1 ? '启用' : '禁用'),
},
},
{
title: '保洁面积',
field: 'area',
width: 'auto',
},
{
title: '合计费用',
field: 'sumPeices',
width: 'auto',
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 'auto',
},
],
data: [],
};
const detailColumns = [
{ title: '序号', key: 'index' },
{ title: '劳务名称', dataIndex: 'name', key: 'name' },
{ title: '计量单位', dataIndex: 'measure', key: 'measure' },
{ title: '计算方式', dataIndex: 'method', key: 'method' },
{
title: '申报单价含税(元)',
dataIndex: 'peices',
key: 'peices',
},
{
title: '保洁频率',
dataIndex: 'frequency',
key: 'frequency',
},
{ title: '保洁标准', dataIndex: 'standard', key: 'standard' },
{ title: '备注', dataIndex: 'remark', key: 'remark' },
{
title: '状态',
dataIndex: 'stater',
key: 'stater',
customRender: ({ value }: { value: number }) =>
value === 1 ? '启用' : '禁用',
},
{ title: '保洁面积', dataIndex: 'area', key: 'area' },
{
title: '合计费用',
dataIndex: 'sumPeices',
key: 'sumPeices',
},
{
title: '操作',
key: 'action',
fixed: 'right' as const,
},
];
const [DetailTable, detailTableApi] = useVbenVxeGrid({
gridOptions: detailGridOptions,
});
const detailTable = ref<any>([]);
const [CleanDetailModal, detailModalApi] = useVbenModal({
connectedComponent: cleanDetailModal,
});
function handleAddDetail() {
detailModalApi.setData({});
detailModalApi.open();
}
// 添加订单服务详情
function handleDetailReload(data: any) {
detailTable.value.push(data);
}
// 编辑订单服务详情
function handleEditDetailReload(data: any) {
detailTable.value[data.index] = data;
}
// 删除订单服务详情
function handleDeleteDetail(record: any, index: number) {
detailTable.value.splice(index, 1);
}
// 查看产品详情
function handleViewDetail(record: any) {
detailModalApi.setData({ ...record, readonly: true });
detailModalApi.open();
}
// 编辑产品详情
function handleEditDetail(record: any, index: number) {
detailModalApi.setData({ ...record, index, readonly: false });
detailModalApi.open();
}
async function handleConfirm() {
if (isReadonly.value) {
detailTable.value = [];
modalApi.close();
return;
}
try {
modalApi.lock(true);
const { valid } = await formApi.validate();
if (!valid) {
return;
}
const data = cloneDeep(await formApi.getValues());
// 单位数据缓存
if (unitListData.length === 0) {
const res = await resident_unitList();
unitListData = res.rows || [];
}
// 劳务数据缓存 cleanListData 已有
// 查找label
const unitObj = unitListData.find((item) => item.id === data.unitId);
const cleanObj = cleanListData.find((item) => item.id === data.name);
data.unit = unitObj ? unitObj.name : data.unit || '';
data.name = cleanObj ? cleanObj.name : data.name || '';
data.unitId = unitObj ? unitObj.id : isUpdate.value ? editUnitId.value : '';
data.sumPeices = Number(totalSumPeices.value);
data.cleanList = detailTable.value;
if (data.starTime)
data.starTime = dayjs(data.starTime).format('YYYY-MM-DD HH:mm:ss');
if (data.endTime)
data.endTime = dayjs(data.endTime).format('YYYY-MM-DD HH:mm:ss');
isUpdate.value
? await clean_orderUpdate({ ...data, id: editCleanOrderId.value })
: await clean_orderAdd(data);
resetInitialized();
emit('reload');
modalApi.close();
} catch (error) {
console.error(error);
} finally {
modalApi.lock(false);
}
}
async function handleClosed() {
await formApi.resetForm();
detailTable.value = []; //清空详情表格
resetInitialized();
}
// 获取服务地址
async function setupCommunitySelect() {
const areaList = await communityTree(4);
// 选中后显示在输入框的值 即父节点 / 子节点
// addFullName(areaList, 'areaName', ' / ');
const splitStr = '/';
handleNode(areaList, 'label', splitStr, function (node: any) {
if (node.level != 4) {
node.disabled = true;
}
});
formApi.updateSchema([
{
componentProps: () => ({
class: 'w-full',
fieldNames: {
key: 'id',
label: 'label',
value: 'code',
children: 'children',
},
getPopupContainer,
placeholder: '请选择服务地址',
showSearch: true,
treeData: areaList,
treeDefaultExpandAll: true,
treeLine: { showLeafIcon: false },
// 筛选的字段
treeNodeFilterProp: 'label',
// 选中后显示在输入框的值
treeNodeLabelProp: 'fullName',
}),
fieldName: 'location',
},
]);
}
async function handleAudit(params: any) {
modalApi.lock(true);
const { valid } = await formApi.validate();
if (!valid) {
return;
}
const data = cloneDeep(await formApi.getValues());
if (rowData.value) {
data.state = rowData.value.state;
data.isUnbooking = rowData.value?.isUnbooking;
}
// 单位数据缓存
if (unitListData.length === 0) {
const res = await resident_unitList();
unitListData = res.rows || [];
}
// 劳务数据缓存 cleanListData 已有
// 查找label
const unitObj = unitListData.find((item) => item.id === data.unitId);
const cleanObj = cleanListData.find((item) => item.id === data.name);
data.unit = unitObj ? unitObj.name : data.unit || '';
data.name = cleanObj ? cleanObj.name : data.name || '';
data.unitId = unitObj ? unitObj.id : isUpdate.value ? editUnitId.value : '';
data.sumPeices = Number(totalSumPeices.value);
data.starTime = dayjs(data.starTime).format('YYYY-MM-DD HH:mm:ss');
data.endTime = dayjs(data.endTime).format('YYYY-MM-DD HH:mm:ss');
// data.sumPeices = parseInt(totalSumPeices.value, 10);
// 组装 cleanIds
// data.cleanIds = detailTable.value.map((item: any) => item.id);
data.cleanList = detailTable.value;
if (params.isRefund) {
data.isUnbooking = 1;
} else if (params.isAudit) {
data.state = 1;
} else {
data.state = 2;
}
// 0未审核 1审核通过 2审核不通过
await clean_orderUpdate({ ...data, id: editCleanOrderId.value });
resetInitialized();
emit('reload');
modalApi.close();
}
</script>
<template>
<BasicModal :title="title">
<BasicForm> </BasicForm>
<!-- 添加订单详情部分 -->
<div class="mt-4">
<div class="mb-2 flex items-center justify-between">
<h3 class="text-lg font-medium">
{{
isUpdate
? '编辑保洁订单详情'
: isReadonly
? '查看保洁订单详情'
: '添加保洁订单详情'
}}
</h3>
<a-button v-if="!isReadonly" type="primary" @click="handleAddDetail">
{{ $t('pages.common.add') }}
</a-button>
</div>
<Table
:data-source="detailTable"
:columns="detailColumns"
:pagination="false"
>
<template #bodyCell="{ column, index, record }">
<template v-if="column.key === 'index'">
{{ index + 1 }}
</template>
<template v-else-if="column.key === 'action'">
<template v-if="isReadonly">
<Button @click="handleViewDetail(record)">查看</Button>
</template>
<template v-else>
<Button
type="primary"
size="small"
style="margin-right: 5px"
@click="handleViewDetail(record)"
>查看</Button
>
<Button
type="primary"
size="small"
style="margin-right: 5px"
@click="handleEditDetail(record, index)"
>编辑</Button
>
<Button
danger
size="small"
@click="handleDeleteDetail(record, index)"
>
删除
</Button>
</template>
</template>
</template>
</Table>
<div>费用合计{{ totalSumPeices }}</div>
</div>
<CleanDetailModal
@reload="handleDetailReload"
@editReload="handleEditDetailReload"
/>
<template #footer v-if="isAudit">
<a-button @click="modalApi.close()">取消</a-button>
<a-button type="primary" @click="handleAudit({ isAudit: true })"
>审核通过</a-button
>
<a-button danger @click="handleAudit({ isAudit: false })"
>审核不通过</a-button
>
</template>
<template #footer v-if="isRefund">
<a-button @click="modalApi.close()">取消</a-button>
<a-button type="primary" @click="handleAudit({ isRefund: true })"
>退定</a-button
>
</template>
</BasicModal>
</template>
<style scoped>
.mt-4 {
margin-top: 1rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.font-medium {
font-weight: 500;
}
/* 使用 :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>