骑手配送

This commit is contained in:
zjf committed 2026-04-08 15:16:42 +08:00
1 parent 70531640d4
commit 66cce2fc17
8 files changed
+583 -7

No files matched your search

+1 -1
View File
@@ -36,7 +36,7 @@ VITE_VERIFY_ENABLE = false
VITE_VERIFY_IMAGE_ENABLE = true
# 是否开启websocket 消息接受,
VITE_WEBSOCKET_ENABLE = false
VITE_WEBSOCKET_ENABLE = true
# 是否开启注册
VITE_REGISTER_ENABLE = false
+3
View File
@@ -15,6 +15,9 @@ VITE_ADMIN_PROXY_PATH = http://localhost:9999/
# VITE_ADMIN_PROXY_PATH = http://192.168.101.196:9999 # 邓-本地
# VITE_ADMIN_PROXY_PATH = http://59.202.170.152:81/api # 正式环境线上
# 食堂配送:骑手角色编码(用于楼栋/骑手池可选用户列表,与 /admin/user/getUserListByRoleCodeAndDept 的 code 一致)
# VITE_CANTEEN_RIDER_ROLE_CODE = CANTEEN_RIDER
# 图片访问基础地址
# VITE_IMAGE_BASE_PATH = http://123.157.250.162:8089/api/ # 测试线上
# VITE_IMAGE_BASE_PATH = http://123.156.230.15:8089/api/ # 测试线上-过期
+9
View File
@@ -165,3 +165,12 @@ export function getSimpleUserAll(params?: { name?: string; mobile?: string; user
params,
})
}
/** 查询单位下具有指定角色编码的用户(如骑手) */
export function getUserListByRoleCodeAndDept(params: { code: string; deptId: string | number }) {
return request({
url: "/admin/user/getUserListByRoleCodeAndDept",
method: "get",
params,
})
}
+61
View File
@@ -0,0 +1,61 @@
import request from "/@/utils/request"
/** 楼栋 — 骑手绑定列表 */
export function listBuildingBind(params: { deptId: string | number }) {
return request({
url: "/canteen/canteenRiderConfig/building-bind/list",
method: "get",
params,
})
}
/** 新增楼栋绑定(同 deptId + buildingId 唯一) */
export function addBuildingBind(params: {
deptId: string | number
buildingId: string | number
riderUserId: string | number
}) {
return request({
url: "/canteen/canteenRiderConfig/building-bind",
method: "post",
params,
})
}
/** 删除楼栋绑定 */
export function delBuildingBind(id: string | number) {
return request({
url: `/canteen/canteenRiderConfig/building-bind/${id}`,
method: "delete",
})
}
/** 默认骑手池列表 */
export function listRiderPool(params: { deptId: string | number }) {
return request({
url: "/canteen/canteenRiderConfig/pool/list",
method: "get",
params,
})
}
/** 新增默认池骑手(sortOrder 越小越靠前) */
export function addRiderPool(params: {
deptId: string | number
riderUserId: string | number
sortOrder?: number
}) {
return request({
url: "/canteen/canteenRiderConfig/pool",
method: "post",
params,
})
}
/** 移除默认池骑手 */
export function delRiderPool(id: string | number) {
return request({
url: `/canteen/canteenRiderConfig/pool/${id}`,
method: "delete",
})
}
@@ -66,6 +66,15 @@
>
新增楼栋
</el-button>
<el-button
type="success"
size="small"
:disabled="!currentDept"
v-auth="'canteen_canteenDelivery_config'"
@click="openRiderBindDrawer"
>
骑手绑定
</el-button>
</div>
</div>
</template>
@@ -89,7 +98,9 @@
<el-card shadow="hover" class="building-card">
<div class="building-card-header">
<div class="name">{{ item.buildingName }}</div>
<div class="badge">排序:{{ item.sortOrder ?? 0 }}</div>
<el-tag type="info" size="small" effect="plain" class="sort-tag">
排序 {{ item.sortOrder ?? 0 }}
</el-tag>
</div>
<div class="building-card-body">
<div class="line">
@@ -110,7 +121,7 @@
size="small"
icon="edit-pen"
v-auth="'admin_sysDeptBuilding_edit'"
@click="formDialogRef.openDialog(item.id)"
@click.stop="formDialogRef.openDialog(item.id)"
>
编辑
</el-button>
@@ -120,9 +131,7 @@
size="small"
icon="Delete"
v-auth="'admin_sysDeptBuilding_del'"
@click="handleDelete([item.id])"
>
删除
@click.stop="handleDelete([item.id])"
</el-button>
</div>
</div>
@@ -146,6 +155,11 @@
<!-- 单元/房间维护弹窗 -->
<unit-form ref="unitDialogRef" @refresh="loadUnits" />
<room-form ref="roomDialogRef" @refresh="loadRooms" />
<rider-config-drawer
v-model="riderConfigVisible"
:dept-id="currentDept?.id"
:dept-name="currentDept?.name"
/>
<!-- 单元 + 房间 抽屉 -->
<el-drawer
@@ -300,8 +314,10 @@ import { fetchList as fetchRoomPage, delObjs as delRoomObjs } from "/@/api/admin
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
const UnitForm = defineAsyncComponent(() => import("./unit-form.vue"))
const RoomForm = defineAsyncComponent(() => import("./room-form.vue"))
const RiderConfigDrawer = defineAsyncComponent(() => import("./rider-config-drawer.vue"))
const formDialogRef = ref()
const riderConfigVisible = ref(false)
const unitDialogRef = ref()
const roomDialogRef = ref()
@@ -374,6 +390,14 @@ const openAddBuilding = () => {
formDialogRef.value?.openDialog()
}
const openRiderBindDrawer = () => {
if (!currentDept.value) {
useMessage().warning("请先选择单位")
return
}
riderConfigVisible.value = true
}
const selectionChangHandle = (objs: { id: string | number }[]) => {
selectObjs.value = objs.map(({ id }) => id)
multiple.value = !objs.length
@@ -598,13 +622,16 @@ onMounted(() => {
}
.building-card-footer {
display: flex;
align-items: center;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-top: 4px;
}
.building-card-footer .actions {
display: flex;
gap: 6px;
align-items: flex-start;
flex-shrink: 0;
}
.manage-entry {
display: flex;
@@ -0,0 +1,454 @@
<template>
<el-drawer
:model-value="modelValue"
title="骑手配送配置"
size="720px"
:close-on-click-modal="false"
@update:model-value="emit('update:modelValue', $event)"
>
<div v-if="deptId" class="rider-config-wrap">
<el-alert
class="mb12"
type="info"
show-icon
:closable="false"
title="同一单位下每个楼栋仅可绑定一名骑手;无绑定或骑手无效时在默认池中按序号轮询派单。楼栋绑定不受用户性别影响。"
/>
<div class="mb8 text-sm color-info">
当前单位:<span class="color-primary">{{ deptName || deptId }}</span>
<span class="ml12">骑手角色编码:</span>
<el-tag size="small">{{ riderRoleCode }}</el-tag>
</div>
<el-tabs v-model="activeTab">
<el-tab-pane label="楼栋骑手绑定" name="building">
<div class="mb8">
<el-button type="primary" size="small" icon="Refresh" @click="reloadBuildingTab">
刷新
</el-button>
</div>
<el-table v-loading="buildingTabLoading" :data="buildingBindRows" border size="small">
<el-table-column prop="buildingName" label="楼栋" min-width="120" show-overflow-tooltip />
<el-table-column label="绑定骑手" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.bind">{{ riderLabel(row.bind.riderUserId) }}</span>
<span v-else class="color-info">未绑定</span>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
v-if="row.bind"
type="primary"
link
size="small"
@click="openBindDialog(row)"
>
更换
</el-button>
<el-button
v-else
type="primary"
link
size="small"
@click="openBindDialog(row)"
>
绑定
</el-button>
<el-button
v-if="row.bind"
type="danger"
link
size="small"
@click="onUnbindBuilding(row.bind)"
>
解绑
</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="默认骑手池" name="pool">
<div class="mb8 flex gap8 items-center">
<el-button type="primary" size="small" icon="Refresh" @click="reloadPool">
刷新
</el-button>
<el-button type="primary" size="small" icon="Plus" @click="openPoolDialog()">
添加骑手
</el-button>
</div>
<el-table v-loading="poolLoading" :data="poolList" border size="small">
<el-table-column prop="sortOrder" label="序号(越小越靠前)" width="150" />
<el-table-column label="骑手" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
{{ riderLabel(row.riderUserId) }}
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="danger" link size="small" @click="onRemovePool(row)">
移除
</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
<el-empty v-else description="未选择单位" />
</el-drawer>
<!-- 楼栋绑定骑手 -->
<el-dialog
v-model="bindDialogVisible"
:title="bindDialogTitle"
width="480px"
destroy-on-close
@closed="resetBindForm"
>
<el-form label-width="90px">
<el-form-item label="骑手">
<el-select
v-model="bindForm.riderUserId"
filterable
clearable
placeholder="请选择骑手"
class="w100"
>
<el-option
v-for="u in riderOptions"
:key="String(u.userId)"
:label="`${u.name || u.username || ''}${u.phone ? ' / ' + u.phone : ''}`"
:value="u.userId"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="bindDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="bindSubmitting" @click="submitBuildingBind">确定</el-button>
</template>
</el-dialog>
<!-- 默认池添加 -->
<el-dialog
v-model="poolDialogVisible"
title="添加默认池骑手"
width="480px"
destroy-on-close
@closed="resetPoolForm"
>
<el-form label-width="100px">
<el-form-item label="骑手" required>
<el-select
v-model="poolForm.riderUserId"
filterable
clearable
placeholder="请选择骑手"
class="w100"
>
<el-option
v-for="u in riderOptions"
:key="String(u.userId)"
:label="`${u.name || u.username || ''}${u.phone ? ' / ' + u.phone : ''}`"
:value="u.userId"
/>
</el-select>
</el-form-item>
<el-form-item label="排序序号">
<el-input-number v-model="poolForm.sortOrder" :min="0" :max="9999" class="w100" />
<div class="text-xs color-info mt4">越小越靠前参与轮询,默认 0</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="poolDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="poolSubmitting" @click="submitPoolAdd">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="SysDeptBuildingRiderConfigDrawer">
import { getUserListByRoleCodeAndDept } from "/@/api/admin/user"
import {
addBuildingBind,
addRiderPool,
delBuildingBind,
delRiderPool,
listBuildingBind,
listRiderPool,
} from "/@/api/canteen/canteenRiderConfig"
import { fetchList as fetchBuildingList } from "/@/api/admin/sysDeptBuilding"
import { useMessage, useMessageBox } from "/@/hooks/message"
const props = defineProps<{
modelValue: boolean
deptId: string | number | null | undefined
deptName?: string
}>()
const emit = defineEmits<{
(e: "update:modelValue", v: boolean): void
}>()
const riderRoleCode = String(
(import.meta as any).env?.VITE_CANTEEN_RIDER_ROLE_CODE || "CANTEEN_RIDER",
)
const activeTab = ref("building")
const buildingTabLoading = ref(false)
const poolLoading = ref(false)
const riderOptions = ref<any[]>([])
const buildingList = ref<any[]>([])
const bindList = ref<any[]>([])
const poolList = ref<any[]>([])
const bindMap = computed(() => {
const m = new Map<string, any>()
for (const b of bindList.value) {
if (b?.buildingId != null) m.set(String(b.buildingId), b)
}
return m
})
const buildingBindRows = computed(() => {
return (buildingList.value || []).map((b: any) => ({
...b,
bind: b?.id != null ? bindMap.value.get(String(b.id)) || null : null,
}))
})
const riderNameMap = computed(() => {
const m = new Map<string, string>()
for (const u of riderOptions.value) {
const id = u.userId
const label = [u.name || u.username, u.phone].filter(Boolean).join(" / ")
m.set(String(id), label || String(id))
}
for (const row of poolList.value) {
const id = row.riderUserId
if (id != null && !m.has(String(id))) m.set(String(id), String(id))
}
for (const row of bindList.value) {
const id = row.riderUserId
if (id != null && !m.has(String(id))) m.set(String(id), String(id))
}
return m
})
function riderLabel(userId: string | number) {
if (userId == null || userId === "") return "-"
return riderNameMap.value.get(String(userId)) || String(userId)
}
async function loadRiders() {
if (!props.deptId) return
const res: any = await getUserListByRoleCodeAndDept({
code: riderRoleCode,
deptId: props.deptId,
})
const raw = res?.data
riderOptions.value = Array.isArray(raw) ? raw : raw?.records || []
}
async function loadBuildings() {
if (!props.deptId) return
const res: any = await fetchBuildingList({
deptId: props.deptId,
current: 1,
size: 9999,
})
buildingList.value = res?.data?.records || []
}
async function loadBinds() {
if (!props.deptId) return
const res: any = await listBuildingBind({ deptId: props.deptId })
bindList.value = res?.data || []
}
async function loadPool() {
if (!props.deptId) return
const res: any = await listRiderPool({ deptId: props.deptId })
poolList.value = res?.data || []
}
async function reloadBuildingTab() {
buildingTabLoading.value = true
try {
await Promise.all([loadBuildings(), loadBinds(), loadRiders()])
} finally {
buildingTabLoading.value = false
}
}
async function reloadPool() {
poolLoading.value = true
try {
await Promise.all([loadPool(), loadRiders()])
} finally {
poolLoading.value = false
}
}
watch(
() => [props.modelValue, props.deptId] as const,
async ([open, deptId]) => {
if (!open || deptId == null || deptId === "") return
activeTab.value = "building"
await reloadBuildingTab()
await loadPool()
},
)
// ---------- 楼栋绑定弹窗 ----------
const bindDialogVisible = ref(false)
const bindSubmitting = ref(false)
const bindTargetRow = ref<any | null>(null)
const bindForm = reactive({
riderUserId: "" as string | number | "",
})
const bindDialogTitle = computed(() => {
const name = bindTargetRow.value?.buildingName || ""
return name ? `绑定骑手 — ${name}` : "绑定骑手"
})
function resetBindForm() {
bindTargetRow.value = null
bindForm.riderUserId = ""
}
function openBindDialog(row: any) {
bindTargetRow.value = row
bindForm.riderUserId = row.bind?.riderUserId ?? ""
bindDialogVisible.value = true
if (!riderOptions.value.length) loadRiders()
}
async function submitBuildingBind() {
if (!props.deptId || !bindTargetRow.value?.id) return
if (bindForm.riderUserId === "" || bindForm.riderUserId == null) {
useMessage().warning("请选择骑手")
return
}
if (bindTargetRow.value.bind) {
if (
String(bindForm.riderUserId) === String(bindTargetRow.value.bind.riderUserId)
) {
bindDialogVisible.value = false
return
}
try {
await useMessageBox().confirm("确认更换该楼栋绑定的骑手?")
} catch {
return
}
try {
bindSubmitting.value = true
await delBuildingBind(bindTargetRow.value.bind.id)
await addBuildingBind({
deptId: props.deptId,
buildingId: bindTargetRow.value.id,
riderUserId: bindForm.riderUserId,
})
useMessage().success("已更换绑定")
bindDialogVisible.value = false
await reloadBuildingTab()
} catch (e: any) {
useMessage().error(e?.msg || "操作失败")
} finally {
bindSubmitting.value = false
}
return
}
try {
bindSubmitting.value = true
await addBuildingBind({
deptId: props.deptId,
buildingId: bindTargetRow.value.id,
riderUserId: bindForm.riderUserId,
})
useMessage().success("绑定成功")
bindDialogVisible.value = false
await reloadBuildingTab()
} catch (e: any) {
useMessage().error(e?.msg || "绑定失败")
} finally {
bindSubmitting.value = false
}
}
async function onUnbindBuilding(bind: any) {
try {
await useMessageBox().confirm("确认解绑该楼栋骑手?")
} catch {
return
}
try {
await delBuildingBind(bind.id)
useMessage().success("已解绑")
await reloadBuildingTab()
} catch (e: any) {
useMessage().error(e?.msg || "解绑失败")
}
}
// ---------- 默认池 ----------
const poolDialogVisible = ref(false)
const poolSubmitting = ref(false)
const poolForm = reactive({
riderUserId: "" as string | number | "",
sortOrder: 0,
})
function resetPoolForm() {
poolForm.riderUserId = ""
poolForm.sortOrder = 0
}
function openPoolDialog() {
resetPoolForm()
poolDialogVisible.value = true
if (!riderOptions.value.length) loadRiders()
}
async function submitPoolAdd() {
if (!props.deptId) return
if (poolForm.riderUserId === "" || poolForm.riderUserId == null) {
useMessage().warning("请选择骑手")
return
}
try {
poolSubmitting.value = true
await addRiderPool({
deptId: props.deptId,
riderUserId: poolForm.riderUserId,
sortOrder: poolForm.sortOrder ?? 0,
})
useMessage().success("已添加")
poolDialogVisible.value = false
await reloadPool()
} catch (e: any) {
useMessage().error(e?.msg || "添加失败")
} finally {
poolSubmitting.value = false
}
}
async function onRemovePool(row: any) {
try {
await useMessageBox().confirm("确认从默认池移除该骑手?")
} catch {
return
}
try {
await delRiderPool(row.id)
useMessage().success("已移除")
await reloadPool()
} catch (e: any) {
useMessage().error(e?.msg || "移除失败")
}
}
</script>
+15
View File
@@ -26,6 +26,15 @@
<el-input clearable placeholder="请输入手机号" v-model="dataForm.phone"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="dataForm.gender">
<el-radio :label="0">未知</el-radio>
<el-radio :label="1">男</el-radio>
<el-radio :label="2">女</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="!roleCodes.includes('DEPT_ADMIN')" class="mb20">
<el-form-item :label="$t('sysuser.role')" prop="role">
<el-select clearable multiple placeholder="请选择角色" v-model="dataForm.role">
@@ -151,6 +160,8 @@ const dataForm = reactive({
nickname: "",
name: "",
email: "",
/** 0 未知 / 1 男 / 2 女 */
gender: 0 as number,
staffingType: "2",
post: [] as string[],
role: [] as string[],
@@ -210,6 +221,9 @@ const openDialog = async (id: string) => {
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
if (!id) {
dataForm.gender = 0
}
})
// 修改获取用户信息
@@ -269,6 +283,7 @@ const getUserData = async (id: string) => {
loading.value = true
const { data } = await getObj(id)
Object.assign(dataForm, data)
dataForm.gender = data.gender != null && data.gender !== "" ? Number(data.gender) : 0
if (data.roleList) {
dataForm.role = data.roleList.map((item: any) => item.roleId)
}
+7
View File
@@ -65,6 +65,13 @@
<el-table-column label="#" type="index" width="60" fixed="left" />
<el-table-column label="用户名" prop="username" fixed="left" show-overflow-tooltip />
<el-table-column label="姓名" prop="name" show-overflow-tooltip min-width="150" />
<el-table-column label="性别" prop="gender" width="90">
<template #default="{ row }">
<el-tag v-if="Number(row.gender) === 1" type="primary" size="small">男</el-tag>
<el-tag v-else-if="Number(row.gender) === 2" type="danger" size="small">女</el-tag>
<el-tag v-else type="info" size="small">未知</el-tag>
</template>
</el-table-column>
<el-table-column label="手机号" prop="phone" show-overflow-tooltip min-width="150" />
<el-table-column label="部门" prop="deptName" show-overflow-tooltip min-width="150" />
<el-table-column label="启用" show-overflow-tooltip min-width="150">