This commit is contained in:
2026-05-31 12:09:14 +08:00
commit e4f9eedb80
1121 changed files with 209380 additions and 0 deletions
@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>
@@ -0,0 +1,478 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="1000"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:confirm-loading="loading"
:title="isUpdate ? '编辑配送员' : '添加配送员'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 5, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="选择用户" name="userId">
<!-- SelectUser 组件本身不支持 v-model,只通过 @done 回传选择结果;用 key 强制刷新回显 -->
<SelectUser
:key="String(form.userId ?? '') + selectedUserText"
:value="selectedUserText"
:placeholder="`选择用户`"
@done="onChooseUser"
/>
</a-form-item>
<a-form-item label="所属门店" name="storeId">
<SelectShopStore
:key="String(form.storeId ?? '') + selectedStoreText"
:value="selectedStoreText"
:placeholder="`选择门店`"
@done="onChooseStore"
/>
</a-form-item>
<a-form-item label="配送点(小区)" name="dealerId">
<SelectCommunity
:key="String(form.dealerId ?? '') + selectedCommunityText"
:value="selectedCommunityText"
:placeholder="`选择小区`"
@done="onChooseCommunity"
/>
</a-form-item>
<a-form-item label="骑手编号" name="riderNo">
<a-input
allow-clear
placeholder="请输入骑手编号(可选)"
v-model:value="form.riderNo"
/>
</a-form-item>
<a-form-item label="姓名" name="realName">
<a-input
allow-clear
placeholder="请输入姓名"
v-model:value="form.realName"
/>
</a-form-item>
<a-form-item label="手机号" name="mobile">
<a-input
allow-clear
placeholder="请输入手机号"
v-model:value="form.mobile"
/>
</a-form-item>
<!-- <a-form-item label="头像" name="avatar">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入头像"-->
<!-- v-model:value="form.avatar"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="身份证号" name="idCardNo">
<a-input
allow-clear
placeholder="请输入身份证号"
:value="maskedIdCardNo"
/>
</a-form-item>
<a-form-item label="接单状态" name="workStatus">
<a-input
allow-clear
placeholder="请输入接单状态:0休息/下线;1在线;2忙碌"
v-model:value="form.workStatus"
/>
</a-form-item>
<a-form-item label="自动派单" name="autoDispatchEnabled">
<a-input
allow-clear
placeholder="请输入是否开启自动派单:1是;0否"
v-model:value="form.autoDispatchEnabled"
/>
</a-form-item>
<a-form-item label="派单优先级" name="dispatchPriority">
<a-input
allow-clear
placeholder="请输入派单优先级(同小区多骑手时可用,值越大越优先)"
v-model:value="form.dispatchPriority"
/>
</a-form-item>
<a-form-item label="最大同时配送单数" name="maxOnhandOrders">
<a-input
allow-clear
placeholder="请输入最大同时配送单数(0表示不限制)"
v-model:value="form.maxOnhandOrders"
/>
</a-form-item>
<a-form-item label="是否计算工资" name="commissionCalcEnabled">
<a-input
allow-clear
placeholder="请输入是否计算工资(提成):1计算;0不计算(如三方配送点可设0)"
v-model:value="form.commissionCalcEnabled"
/>
</a-form-item>
<a-form-item label="提成单价(元/桶)" name="waterBucketUnitFee">
<a-input
allow-clear
placeholder="请输入水每桶提成金额(元/桶)"
v-model:value="form.waterBucketUnitFee"
/>
</a-form-item>
<a-form-item label="其他商品提成方式" name="otherGoodsCommissionType">
<a-input
allow-clear
placeholder="请输入其他商品提成方式:1按订单固定金额;2按订单金额比例;3按商品规则(另表)"
v-model:value="form.otherGoodsCommissionType"
/>
</a-form-item>
<a-form-item label="其他商品提成值" name="otherGoodsCommissionValue">
<a-input
allow-clear
placeholder="请输入其他商品提成值:固定金额(元)或比例(%)"
v-model:value="form.otherGoodsCommissionValue"
/>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="1">启用</a-radio>
<a-radio :value="0">禁用</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import {
addShopStoreRider,
updateShopStoreRider
} from '@/api/shop/shopStoreRider';
import { ShopStoreRider } from '@/api/shop/shopStoreRider/model';
import { getUser } from '@/api/system/user';
import { getShopCommunity } from '@/api/shop/shopCommunity';
import { getShopStore } from '@/api/shop/shopStore';
import SelectUser from '@/components/SelectUser/index.vue';
import SelectCommunity from '@/components/SelectCommunity/index.vue';
import SelectShopStore from '@/components/SelectShopStore/index.vue';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
import type { User } from '@/api/system/user/model';
import type { ShopCommunity } from '@/api/shop/shopCommunity/model';
import type { ShopStore } from '@/api/shop/shopStore/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopStoreRider | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
const selectedUserText = ref('');
const selectedCommunityText = ref('');
const selectedStoreText = ref('');
// 用户信息
const form = reactive<ShopStoreRider>({
id: undefined,
storeId: undefined,
dealerId: undefined,
riderNo: undefined,
realName: undefined,
mobile: undefined,
avatar: undefined,
idCardNo: undefined,
workStatus: undefined,
autoDispatchEnabled: undefined,
dispatchPriority: undefined,
maxOnhandOrders: undefined,
commissionCalcEnabled: undefined,
waterBucketUnitFee: undefined,
otherGoodsCommissionType: undefined,
otherGoodsCommissionValue: undefined,
userId: undefined,
isDelete: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
userId: [
{
validator: (_rule: unknown, value: number | undefined) => {
if (!value) {
return Promise.reject(new Error('请选择用户'));
}
return Promise.resolve();
},
trigger: 'change'
}
],
storeId: [
{
validator: (_rule: unknown, value: number | undefined) => {
if (!value) {
return Promise.reject(new Error('请选择门店'));
}
return Promise.resolve();
},
trigger: 'change'
}
]
// userId: [
// {
// required: true,
// type: 'string',
// message: '请选择用户',
// trigger: 'blur'
// }
// ],
// dealerId: [
// {
// required: true,
// type: 'string',
// message: '请选择配送点',
// trigger: 'blur'
// }
// ],
// realName: [
// {
// required: true,
// type: 'string',
// message: '请填写姓名',
// trigger: 'blur'
// }
// ],
});
const _chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.avatar = data.path;
};
const _onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.avatar = '';
};
const maskIdCard = (value?: string) => {
if (!value) return '';
const s = String(value).trim();
const startLen = Math.min(6, Math.max(0, s.length - 4));
const endLen = Math.min(4, s.length);
const starLen = Math.max(4, s.length - startLen - endLen);
if (s.length <= startLen + endLen) return s;
return `${s.slice(0, startLen)}${'*'.repeat(starLen)}${s.slice(-endLen)}`;
};
const maskedIdCardNo = computed(() => maskIdCard(form.idCardNo));
const onChooseUser = (user?: User) => {
if (!user) {
selectedUserText.value = '';
form.userId = undefined;
formRef.value?.validateFields(['userId']).catch(() => {});
return;
}
form.userId = user.userId;
form.realName = user.realName ?? user.nickname;
form.mobile = user.phone ?? user.mobile;
form.avatar = user.avatar ?? user.avatarUrl;
form.idCardNo = user.idCard ?? user.idcard;
const name = user.realName ?? user.nickname ?? '';
const phone = user.phone ?? user.mobile ?? '';
selectedUserText.value = phone ? `${name}(${phone})` : name;
formRef.value?.validateFields(['userId']).catch(() => {});
};
const onChooseCommunity = (
community?: ShopCommunity & { index?: number }
) => {
if (!community) {
selectedCommunityText.value = '';
form.dealerId = undefined;
return;
}
form.dealerId = community.id;
selectedCommunityText.value = community.name ?? String(community.id ?? '');
};
const onChooseStore = (store?: ShopStore) => {
if (!store) {
selectedStoreText.value = '';
form.storeId = undefined;
formRef.value?.validateFields(['storeId']).catch(() => {});
return;
}
form.storeId = store.id;
selectedStoreText.value = store.name ?? String(store.id ?? '');
formRef.value?.validateFields(['storeId']).catch(() => {});
};
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value
? updateShopStoreRider
: addShopStoreRider;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
selectedUserText.value = '';
selectedCommunityText.value = '';
selectedStoreText.value = '';
if (props.data) {
assignObject(form, props.data);
if (props.data.avatar) {
images.value.push({
uid: uuid(),
url: props.data.avatar,
status: 'done'
});
}
isUpdate.value = true;
// 回显展示文本(避免组件内部不响应 props.value 更新)
if (form.userId) {
const uid = form.userId;
getUser(form.userId)
.then((user) => {
if (form.userId !== uid) return;
const name = user.realName ?? user.nickname ?? '';
const phone = user.phone ?? user.mobile ?? '';
selectedUserText.value = phone ? `${name}(${phone})` : name;
})
.catch(() => {
if (form.userId !== uid) return;
selectedUserText.value = String(form.userId ?? '');
});
}
if (form.storeId) {
// 优先使用列表接口返回的 storeName 回显,避免额外请求
if ((props.data as any)?.storeName) {
selectedStoreText.value = String((props.data as any).storeName);
} else {
const sid = form.storeId;
getShopStore(form.storeId)
.then((store) => {
if (form.storeId !== sid) return;
selectedStoreText.value =
store.name ?? String(store.id ?? '');
})
.catch(() => {
if (form.storeId !== sid) return;
selectedStoreText.value = String(form.storeId ?? '');
});
}
}
if (form.dealerId) {
const dealerId = form.dealerId;
getShopCommunity(form.dealerId)
.then((community) => {
if (form.dealerId !== dealerId) return;
selectedCommunityText.value =
community.name ?? String(community.id ?? '');
})
.catch(() => {
if (form.dealerId !== dealerId) return;
selectedCommunityText.value = String(form.dealerId ?? '');
});
}
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
+311
View File
@@ -0,0 +1,311 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'storeName'">
<a-tag v-if="record.storeName" color="orange">{{ record.storeName }}</a-tag>
</template>
<template v-if="column.key === 'workStatus'">
<a-tag v-if="record.status === 2" color="red">忙碌</a-tag>
<a-tag v-if="record.status === 1" color="green">在线</a-tag>
<a-tag v-if="record.status === 0">休息</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 1" color="green">启用</a-tag>
<a-tag v-if="record.status === 0" color="red">禁用</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ShopStoreRiderEdit v-model:visible="showEdit" :data="current" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import {getPageTitle} from '@/utils/common';
import ShopStoreRiderEdit from './components/shopStoreRiderEdit.vue';
import { pageShopStoreRider, removeShopStoreRider, removeBatchShopStoreRider } from '@/api/shop/shopStoreRider';
import type { ShopStoreRider, ShopStoreRiderParam } from '@/api/shop/shopStoreRider/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopStoreRider[]>([]);
// 当前编辑数据
const current = ref<ShopStoreRider | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageShopStoreRider({
...where,
...orders,
page,
limit
});
};
// 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
width: 90
},
{
title: '所属门店',
dataIndex: 'storeName',
key: 'storeName'
},
{
title: '配送员',
dataIndex: 'realName',
key: 'realName'
},
{
title: '手机号',
dataIndex: 'mobile',
key: 'mobile'
},
// {
// title: '头像',
// dataIndex: 'avatar',
// key: 'avatar'
// },
{
title: '身份证号',
dataIndex: 'idCardNo',
key: 'idCardNo'
},
{
title: '接单状态',
dataIndex: 'workStatus',
key: 'workStatus',
align: 'center'
},
{
title: '自动派单',
dataIndex: 'autoDispatchEnabled',
key: 'autoDispatchEnabled',
align: 'center'
},
{
title: '派单优先级',
dataIndex: 'dispatchPriority',
key: 'dispatchPriority',
width: 90,
align: 'center'
},
// {
// title: '最大同时配送单数(0表示不限制)',
// dataIndex: 'maxOnhandOrders',
// key: 'maxOnhandOrders',
// width: 120
// },
// {
// title: '是否计算工资',
// dataIndex: 'commissionCalcEnabled',
// key: 'commissionCalcEnabled',
// width: 90
// },
// {
// title: '水每桶提成金额(元/桶)',
// dataIndex: 'waterBucketUnitFee',
// key: 'waterBucketUnitFee'
// },
// {
// title: '商品提成方式',
// dataIndex: 'otherGoodsCommissionType',
// key: 'otherGoodsCommissionType'
// },
// {
// title: '其他商品提成值:固定金额(元)或比例(%)',
// dataIndex: 'otherGoodsCommissionValue',
// key: 'otherGoodsCommissionValue'
// },
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '排序号',
// dataIndex: 'sortNumber',
// key: 'sortNumber',
// width: 120
// },
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopStoreRiderParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopStoreRider) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopStoreRider) => {
const hide = message.loading('请求中..', 0);
removeShopStoreRider(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchShopStoreRider(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopStoreRider) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopStoreRider'
};
</script>
<style lang="less" scoped></style>