Files
medical-mall/pages/mall/admin/setting/delivery/management/components/DriverEditDrawer.uvue

634 lines
16 KiB
Plaintext
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.
<template>
<view v-if="visible" class="drawer-mask" :class="{ closing: isClosing }" @click="close">
<view class="drawer-container" :class="{ closing: isClosing }" @click.stop>
<!-- 头部 -->
<view class="drawer-header">
<text class="drawer-title">编辑配送员</text>
<view class="close-btn" @click="close">
<text class="close-icon">×</text>
</view>
</view>
<!-- 滚动内容区 -->
<scroll-view class="drawer-body" scroll-y>
<!-- 加载中 -->
<view v-if="formLoading" class="drawer-center">
<text class="tip-text">加载中...</text>
</view>
<!-- 加载失败 -->
<view v-else-if="loadError !== ''" class="drawer-center">
<text class="tip-text-err">{{ loadError }}</text>
</view>
<!-- 表单 -->
<view v-else class="form-content">
<!-- 基本信息 -->
<view class="section-card">
<view class="section-title">
<view class="blue-bar"></view>
<text class="section-name">基本信息</text>
</view>
<view class="form-grid">
<view class="form-row">
<text class="form-label"><text class="required-star">*</text> 真实姓名</text>
<input
class="form-input"
:value="formRealName"
@input="onRealNameInput"
placeholder="请输入真实姓名"
maxlength="100"
/>
</view>
<view class="form-row">
<text class="form-label">驾驶证号</text>
<input
class="form-input"
:value="formDriverLicense"
@input="onDriverLicenseInput"
placeholder="请输入驾驶证号(选填)"
maxlength="50"
/>
</view>
</view>
<view class="hint-box">
<text class="hint-text">※ 手机号 / 注册账号属于用户账号信息,如需修改请前往"用户管理"</text>
</view>
</view>
<!-- 车辆信息 -->
<view class="section-card">
<view class="section-title">
<view class="blue-bar"></view>
<text class="section-name">车辆信息</text>
</view>
<view class="form-grid">
<view class="form-row">
<text class="form-label">车辆类型</text>
<picker
class="form-picker"
:value="formVehicleType - 1"
:range="vehicleTypeLabels"
@change="onVehicleTypeChange"
>
<view class="picker-display">
<text class="picker-text">{{ vehicleTypeLabels[formVehicleType - 1] ?? '选择类型' }}</text>
<text class="picker-arrow">▾</text>
</view>
</picker>
</view>
<view class="form-row">
<text class="form-label">车牌号码</text>
<input
class="form-input"
:value="formVehicleNumber"
@input="onVehicleNumberInput"
placeholder="请输入车牌号(选填)"
maxlength="20"
/>
</view>
<view class="form-row form-row-full">
<text class="form-label">服务区域</text>
<input
class="form-input"
:value="formServiceAreas"
@input="onServiceAreasInput"
placeholder="多个区域用逗号分隔,如:朝阳区,海淀区"
/>
</view>
</view>
</view>
<!-- 状态设置 -->
<view class="section-card">
<view class="section-title">
<view class="blue-bar"></view>
<text class="section-name">状态设置</text>
</view>
<view class="form-grid">
<view class="form-row">
<text class="form-label">账号状态</text>
<picker
class="form-picker"
:value="formStatus - 1"
:range="statusLabels"
@change="onStatusChange"
>
<view class="picker-display">
<text class="picker-text">{{ statusLabels[formStatus - 1] ?? '选择状态' }}</text>
<text class="picker-arrow">▾</text>
</view>
</picker>
</view>
<view class="form-row">
<text class="form-label">接单状态</text>
<picker
class="form-picker"
:value="formWorkStatus - 1"
:range="workStatusLabels"
@change="onWorkStatusChange"
>
<view class="picker-display">
<text class="picker-text">{{ workStatusLabels[formWorkStatus - 1] ?? '选择状态' }}</text>
<text class="picker-arrow">▾</text>
</view>
</picker>
</view>
</view>
</view>
<!-- 保存错误提示 -->
<view v-if="saveError !== ''" class="save-error-box">
<text class="save-error-text">{{ saveError }}</text>
</view>
</view>
</scroll-view>
<!-- 底部固定按钮栏 -->
<view v-if="!formLoading && loadError === ''" class="drawer-footer">
<button class="btn btn-cancel" :disabled="saving" @click="close">取消</button>
<button class="btn btn-save" :disabled="saving" @click="onSave">
{{ saving ? '保存中...' : '保存' }}
</button>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, watch } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
const props = defineProps({
visible: { type: Boolean, default: false },
driverId: { type: String, default: '' }
})
const emit = defineEmits(['update:visible', 'saved'])
// ========== 抽屉动画状态 ==========
const isClosing = ref(false)
// ========== 表单加载状态 ==========
const formLoading = ref(false)
const loadError = ref('')
const saving = ref(false)
const saveError = ref('')
// ========== Picker 选项 ==========
const vehicleTypeLabels = ['电动车', '摩托车', '汽车']
const statusLabels = ['正常', '暂停', '离职']
const workStatusLabels = ['在线', '忙碌', '离线']
// ========== 表单字段(独立 refUTS 类型安全) ==========
const formRealName = ref('')
const formDriverLicense = ref('')
const formVehicleType = ref(1) // 1:电动车 2:摩托车 3:汽车
const formVehicleNumber = ref('')
const formServiceAreas = ref('') // 逗号分隔的服务区域文本
const formWorkStatus = ref(1) // 1:在线 2:忙碌 3:离线
const formStatus = ref(1) // 1:正常 2:暂停 3:离职
// ========== 服务区域 raw <-> 文本 转换 ==========
function areasToText(raw: any): string {
if (raw == null) return ''
if (typeof raw === 'string') {
if (raw === '' || raw === '[]') return ''
try {
const parsed = JSON.parse(raw) as any
if (Array.isArray(parsed)) return (parsed as string[]).filter((s: string) => s !== '').join(',')
} catch (_) {
return raw
}
}
if (Array.isArray(raw)) {
return (raw as string[]).filter((s: string) => s !== '').join(',')
}
return ''
}
function textToAreas(text: string): string[] {
if (text.trim() === '') return []
return text.split(',').map((s: string) => s.trim()).filter((s: string) => s !== '')
}
// ========== 加载当前配送员数据到表单 ==========
const loadForm = async (id: string) => {
if (id === '' || id == null) return
formLoading.value = true
loadError.value = ''
saveError.value = ''
try {
const res = await supa.select(
'ml_delivery_drivers',
`id=eq.${id}`,
{
// 只查询可编辑字段,不取敏感经纬度、评分等只读统计字段
columns: 'id,real_name,driver_license,vehicle_type,vehicle_number,service_areas,work_status,status',
limit: 1
}
)
if (res.status >= 200 && res.status < 300 && res.data != null) {
const rows = res.data as UTSJSONObject[]
if (rows.length === 0) {
loadError.value = '未找到配送员数据'
return
}
const row = rows[0]
formRealName.value = row.getString('real_name') ?? ''
formDriverLicense.value = row.getString('driver_license') ?? ''
formVehicleType.value = row.getNumber('vehicle_type') ?? 1
formVehicleNumber.value = row.getString('vehicle_number') ?? ''
formServiceAreas.value = areasToText(row['service_areas'])
formWorkStatus.value = row.getNumber('work_status') ?? 1
formStatus.value = row.getNumber('status') ?? 1
} else {
loadError.value = '加载数据失败,请重试'
}
} catch (e) {
loadError.value = '请求异常,请稍后重试'
} finally {
formLoading.value = false
}
}
// ========== 保存 ==========
const onSave = async () => {
if (saving.value) return
saveError.value = ''
// 校验必填
const name = formRealName.value.trim()
if (name === '') {
saveError.value = '真实姓名不能为空'
return
}
saving.value = true
try {
const areas = textToAreas(formServiceAreas.value)
const drvLic = formDriverLicense.value.trim()
const vehNum = formVehicleNumber.value.trim()
// 构建更新 payloadservice_areas 以 as any 绕过 UTSJSONObject 类型,确保传入 JSON array
const payload = {
real_name: name,
driver_license: drvLic !== '' ? drvLic : null,
vehicle_type: formVehicleType.value,
vehicle_number: vehNum !== '' ? vehNum : null,
service_areas: areas as any,
work_status: formWorkStatus.value,
status: formStatus.value
} as UTSJSONObject
const res = await supa.from('ml_delivery_drivers')
.update(payload)
.eq('id', props.driverId)
.execute()
if (res.status >= 200 && res.status < 300) {
uni.showToast({ title: '保存成功', icon: 'success' })
emit('saved') // 通知父组件刷新列表
close()
} else {
saveError.value = '保存失败(' + String(res.status) + '),请检查权限配置'
}
} catch (e) {
saveError.value = '请求异常,请稍后重试'
} finally {
saving.value = false
}
}
// ========== 关闭 ==========
const close = () => {
if (isClosing.value) return
isClosing.value = true
setTimeout(() => {
isClosing.value = false
emit('update:visible', false)
}, 280)
}
// ========== 监听打开 ==========
watch(() => props.visible, (newVal: boolean) => {
if (newVal) {
isClosing.value = false
saveError.value = ''
saving.value = false
loadForm(props.driverId)
}
})
// ========== input/change 事件处理方法 ==========
// 注意:在 uni-app-x 模板内ref 会被自动解包为原始值,
// 必须在 script 方法中修改 .value不能在模板内联函数里操作
function onRealNameInput(e: any) { formRealName.value = e.detail.value as string }
function onDriverLicenseInput(e: any) { formDriverLicense.value = e.detail.value as string }
function onVehicleTypeChange(e: any) { formVehicleType.value = Number(e.detail.value) + 1 }
function onVehicleNumberInput(e: any) { formVehicleNumber.value = e.detail.value as string }
function onServiceAreasInput(e: any) { formServiceAreas.value = e.detail.value as string }
function onStatusChange(e: any) { formStatus.value = Number(e.detail.value) + 1 }
function onWorkStatusChange(e: any) { formWorkStatus.value = Number(e.detail.value) + 1 }
</script>
<style scoped>
/* ---- 遮罩 ---- */
.drawer-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.45);
z-index: 2000;
display: flex;
flex-direction: row;
justify-content: flex-end;
animation: maskFadeIn 0.3s ease-out;
}
.drawer-mask.closing {
animation: maskFadeOut 0.28s ease-in forwards;
}
/* ---- 容器 ---- */
.drawer-container {
width: 50%;
min-width: 360px;
height: 100vh;
background-color: #fff;
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
animation: slideIn 0.32s cubic-bezier(0.23, 1, 0.32, 1);
}
.drawer-container.closing {
animation: slideOut 0.28s cubic-bezier(0.755, 0.05, 0.855, 0.06) forwards;
}
@keyframes slideIn {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
@keyframes slideOut {
from { transform: translateX(0); }
to { transform: translateX(100%); }
}
@keyframes maskFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes maskFadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
/* ---- 头部 ---- */
.drawer-header {
height: 56px;
padding: 0 24px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
}
.drawer-title {
font-size: 16px;
font-weight: 600;
color: #262626;
}
.close-btn {
width: 32px;
height: 32px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 4px;
}
.close-icon {
font-size: 22px;
color: #8c8c8c;
line-height: 1;
}
/* ---- 内容区 ---- */
.drawer-body {
flex: 1;
background-color: #f5f7f9;
}
.drawer-center {
padding: 60px 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.tip-text {
font-size: 14px;
color: #8c8c8c;
}
.tip-text-err {
font-size: 14px;
color: #ff4d4f;
}
/* ---- 表单内容 ---- */
.form-content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* ---- 信息卡片 ---- */
.section-card {
background-color: #fff;
border-radius: 6px;
padding: 16px 24px 20px;
}
.section-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.blue-bar {
width: 3px;
height: 14px;
background-color: #1890ff;
border-radius: 2px;
}
.section-name {
font-size: 14px;
font-weight: 600;
color: #262626;
}
/* ---- 表单行 ---- */
.form-grid {
display: flex;
flex-direction: column;
gap: 14px;
}
.form-row {
display: flex;
flex-direction: row;
align-items: center;
}
.form-row-full {
align-items: flex-start;
}
.form-label {
font-size: 13px;
color: #595959;
width: 72px;
flex-shrink: 0;
line-height: 34px;
}
.required-star {
color: #ff4d4f;
margin-right: 2px;
}
/* ---- 输入框 ---- */
.form-input {
flex: 1;
height: 34px;
line-height: 34px;
padding: 0 10px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 13px;
color: #262626;
background-color: #fff;
}
/* ---- Picker ---- */
.form-picker {
flex: 1;
}
.picker-display {
height: 34px;
padding: 0 10px;
border: 1px solid #d9d9d9;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: #fff;
}
.picker-text {
font-size: 13px;
color: #262626;
}
.picker-arrow {
font-size: 12px;
color: #bfbfbf;
}
/* ---- 提示框 ---- */
.hint-box {
margin-top: 10px;
padding: 8px 10px;
background-color: #f0f7ff;
border-radius: 4px;
border-left: 3px solid #91caff;
}
.hint-text {
font-size: 12px;
color: #1677ff;
line-height: 1.5;
}
/* ---- 保存错误 ---- */
.save-error-box {
padding: 10px 14px;
background-color: #fff2f0;
border: 1px solid #ffccc7;
border-radius: 4px;
}
.save-error-text {
font-size: 13px;
color: #ff4d4f;
}
/* ---- 底部按钮 ---- */
.drawer-footer {
height: 64px;
padding: 0 24px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 12px;
border-top: 1px solid #f0f0f0;
background-color: #fff;
flex-shrink: 0;
}
.btn {
height: 34px;
line-height: 32px;
padding: 0 20px;
font-size: 14px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.btn-cancel {
background-color: #fff;
color: #595959;
border: 1px solid #d9d9d9;
}
.btn-save {
background-color: #1890ff;
color: #fff;
}
.btn-save[disabled] {
background-color: #91caff;
cursor: not-allowed;
}
.btn-cancel[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
</style>