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

762 lines
19 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 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-row">
<text class="form-label"><text class="required-star">*</text> 手机号码</text>
<input
class="form-input"
:value="searchPhone"
@input="onSearchPhoneInput"
placeholder="输入已注册用户手机号"
maxlength="20"
/>
<button class="search-btn" :disabled="searchLoading" @click="onSearchUser">
{{ searchLoading ? '查询中' : '查找' }}
</button>
</view>
<!-- 搜索错误 -->
<view v-if="searchError !== ''" class="hint-box hint-box-err">
<text class="hint-text-err">{{ searchError }}</text>
</view>
<!-- 找到的用户卡片 -->
<view v-if="foundUser != null" class="user-card">
<image class="user-avatar" :src="foundUser.avatarUrl" mode="aspectFill" />
<view class="user-info">
<text class="user-name">{{ foundUser.username }}</text>
<text class="user-phone">{{ foundUser.phone }}</text>
</view>
<text class="user-ok">✓ 已选择</text>
</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 class="required-star">*</text> 真实姓名</text>
<input
class="form-input"
:value="formRealName"
@input="onRealNameInput"
placeholder="请输入真实姓名"
maxlength="100"
/>
</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 class="required-star">*</text> 身份证号</text>
<input
class="form-input"
:value="formIdCard"
@input="onIdCardInput"
placeholder="请输入身份证号"
maxlength="32"
/>
</view>
<view class="form-row">
<text class="form-label">驾驶证号</text>
<input
class="form-input"
:value="formDriverLicense"
@input="onDriverLicenseInput"
placeholder="请输入驾驶证号(选填)"
maxlength="50"
/>
</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="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 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 }
})
const emit = defineEmits(['update:visible', 'saved'])
// ========== 抽屉动画 ==========
const isClosing = ref(false)
// ========== 搜索用户状态 ==========
const searchPhone = ref('')
const searchLoading = ref(false)
const searchError = ref('')
type FoundUser = {
id: string
username: string
phone: string
avatarUrl: string
}
const foundUser = ref<FoundUser | null>(null)
// ========== 表单状态 ==========
const saving = ref(false)
const saveError = ref('')
// ========== Picker 选项 ==========
const vehicleTypeLabels = ['电动车', '摩托车', '汽车']
const statusLabels = ['正常', '暂停', '离职']
const workStatusLabels = ['在线', '忙碌', '离线']
// ========== 表单字段 ==========
const formRealName = ref('')
const formIdCard = ref('')
const formDriverLicense = ref('')
const formVehicleType = ref(1)
const formVehicleNumber = ref('')
const formServiceAreas = ref('')
const formWorkStatus = ref(1)
const formStatus = ref(1)
// ========== input/change 事件处理方法 ==========
// 注意:在 uni-app-x 模板内ref 会被自动解包为原始值,
// 必须在 script 方法中修改 .value不能在模板内联函数里操作
function onSearchPhoneInput(e: any) {
searchPhone.value = e.detail.value as string
foundUser.value = null
searchError.value = ''
}
function onRealNameInput(e: any) { formRealName.value = e.detail.value as string }
function onIdCardInput(e: any) { formIdCard.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 }
// ========== 服务区域文本转数组 ==========
function textToAreas(text: string): string[] {
if (text.trim() === '') return []
return text.split(',').map((s: string) => s.trim()).filter((s: string) => s !== '')
}
// ========== 重置表单 ==========
function resetForm() {
searchPhone.value = ''
searchError.value = ''
foundUser.value = null
formRealName.value = ''
formIdCard.value = ''
formDriverLicense.value = ''
formVehicleType.value = 1
formVehicleNumber.value = ''
formServiceAreas.value = ''
formWorkStatus.value = 1
formStatus.value = 1
saveError.value = ''
saving.value = false
}
// ========== 搜索用户 ==========
const onSearchUser = async () => {
const ph = searchPhone.value.trim()
if (ph === '') {
searchError.value = '请输入手机号'
return
}
searchLoading.value = true
searchError.value = ''
foundUser.value = null
try {
const res = await supa.select(
'ak_users',
`phone=eq.${ph}`,
{
columns: 'id,username,phone,avatar_url',
limit: 1
}
)
if (res.status >= 200 && res.status < 300 && res.data != null) {
const rows = res.data as UTSJSONObject[]
if (rows.length > 0) {
const u = rows[0]
const avatarUrl = u.getString('avatar_url') ?? ''
foundUser.value = {
id: u.getString('id') ?? '',
username: u.getString('username') ?? '—',
phone: u.getString('phone') ?? ph,
avatarUrl: avatarUrl !== '' ? avatarUrl : '/static/logo.png'
} as FoundUser
} else {
searchError.value = '未找到该手机号对应的用户,请先前往"用户管理"创建账号'
}
} else {
searchError.value = '查询失败,请重试'
}
} catch (e) {
searchError.value = '请求异常,请稍后重试'
} finally {
searchLoading.value = false
}
}
// ========== 保存 ==========
const onSave = async () => {
if (saving.value) return
saveError.value = ''
if (foundUser.value == null) {
saveError.value = '请先通过手机号查找并选择关联的用户账号'
return
}
const name = formRealName.value.trim()
if (name === '') {
saveError.value = '真实姓名不能为空'
return
}
const idCard = formIdCard.value.trim()
if (idCard === '') {
saveError.value = '身份证号不能为空'
return
}
saving.value = true
try {
const areas = textToAreas(formServiceAreas.value)
const drvLic = formDriverLicense.value.trim()
const vehNum = formVehicleNumber.value.trim()
// created_at / updated_at 由数据库 DEFAULT NOW() 自动填充,无需手动传入
const payload = {
user_id: foundUser.value!!.id,
real_name: name,
id_card: idCard,
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').insert(payload).execute()
if (res.status >= 200 && res.status < 300) {
uni.showToast({ title: '添加成功', icon: 'success' })
emit('saved')
close()
} else {
// 常见失败原因该用户已注册为配送员user_id UNIQUE 约束)
if (res.status === 409) {
saveError.value = '添加失败:该用户已注册为配送员,不能重复添加'
} 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
resetForm()
}
})
</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;
}
.close-icon {
font-size: 22px;
color: #8c8c8c;
line-height: 1;
}
/* ---- 内容区 ---- */
.drawer-body {
flex: 1;
background-color: #f5f7f9;
}
.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;
}
/* ---- 搜索按钮 ---- */
.search-btn {
height: 34px;
line-height: 32px;
padding: 0 14px;
margin-left: 8px;
font-size: 13px;
border-radius: 4px;
border: 1px solid #1890ff;
color: #1890ff;
background-color: #fff;
flex-shrink: 0;
}
.search-btn[disabled] {
opacity: 0.6;
}
/* ---- 找到的用户卡片 ---- */
.user-card {
margin-top: 10px;
padding: 10px 12px;
background-color: #f6ffed;
border: 1px solid #b7eb8f;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
.user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
flex-shrink: 0;
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.user-name {
font-size: 13px;
font-weight: 600;
color: #262626;
}
.user-phone {
font-size: 12px;
color: #8c8c8c;
}
.user-ok {
font-size: 13px;
color: #52c41a;
font-weight: 600;
}
/* ---- 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-box-err {
background-color: #fff2f0;
border-left-color: #ff4d4f;
margin-top: 8px;
}
.hint-text {
font-size: 12px;
color: #1677ff;
line-height: 1.5;
}
.hint-text-err {
font-size: 12px;
color: #ff4d4f;
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>