Files
medical-mall/pages/mall/admin/setting/delivery/staff.uvue

327 lines
14 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 class="admin-page-container">
<view class="page-card">
<view class="action-wrap">
<button class="btn btn-primary" @click="onAdd">添加配送员</button>
</view>
<!-- 表格区域 -->
<view class="table-wrap border-shadow">
<view class="table-header">
<view class="th" style="flex: 1;">序号</view>
<view class="th" style="flex: 1.5;">头像</view>
<view class="th" style="flex: 1.8;">名称</view>
<view class="th" style="flex: 1.8;">编号</view>
<view class="th" style="flex: 2.5;">机构</view>
<view class="th" style="flex: 2.3;">手机号码</view>
<view class="th" style="flex: 1.5;">在线</view>
<view class="th" style="flex: 1.7;">资质</view>
<view class="th" style="flex: 1.3;">状态</view>
<view class="th" style="flex: 3;">添加时间</view>
<view class="th" style="flex: 2;">操作</view>
</view>
<view class="table-body">
<view v-if="loading" class="loading-box">
<text>加载中...</text>
</view>
<view v-else-if="staffList.length === 0" class="no-data">
<text class="no-data-text">暂无配送员数据</text>
</view>
<view v-else v-for="(item, index) in staffList" :key="item.id" class="tr">
<view class="td" style="flex: 1;"><text class="td-txt">{{ (page - 1) * pageSize + index + 1 }}</text></view>
<view class="td" style="flex: 1.5;">
<image v-if="item.avatar" class="avatar" :src="item.avatar" mode="aspectFill" />
<view v-else class="avatar-placeholder"><text>👤</text></view>
</view>
<view class="td" style="flex: 1.8;"><text class="td-txt">{{ item.nickname }}</text></view>
<view class="td" style="flex: 1.8;"><text class="td-txt">{{ item.staff_no || '-' }}</text></view>
<view class="td" style="flex: 2.5;"><text class="td-txt">{{ item.station_name || item.station_id || '-' }}</text></view>
<view class="td" style="flex: 2.3;"><text class="td-txt">{{ item.phone }}</text></view>
<view class="td" style="flex: 1.5;"><text class="td-txt">{{ item.online_status || '-' }}</text></view>
<view class="td" style="flex: 1.7;"><text class="td-txt">{{ item.certificate_status || '-' }}</text></view>
<view class="td" style="flex: 1.3;">
<switch :checked="item.status === 1" color="#1890ff" scale="0.7" @change="onToggleStatus(item)" />
</view>
<view class="td" style="flex: 3;"><text class="td-txt-small">{{ formatTime(item.created_at) }}</text></view>
<view class="td" style="flex: 2;">
<text class="action-btn" @click="onEdit(item)">编辑</text>
<view class="divider"></view>
<text class="action-btn danger" @click="onDelete(item)">删除</text>
</view>
</view>
</view>
</view>
<!-- 分页栏 -->
<view class="pagination-footer">
<text class="total-txt">共 {{ total }} 条</text>
<view class="page-btns">
<text :class="['p-btn', page <= 1 ? 'disabled' : '']" @click="prevPage"> < </text>
<text class="p-btn active">{{ page }}</text>
<text :class="['p-btn', staffList.length < pageSize ? 'disabled' : '']" @click="nextPage"> > </text>
</view>
</view>
</view>
<!-- 添加/编辑 弹窗 -->
<view v-if="showModal" class="modal-overlay" @click="closeModal">
<view class="modal-card" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ isEdit ? '编辑配送员' : '添加配送员' }}</text>
<text class="close-btn" @click="closeModal">×</text>
</view>
<view class="modal-body">
<view class="form-item">
<text class="f-label">名称:</text>
<input class="f-input" v-model="form.nickname" placeholder="请输入名称" />
</view>
<view class="form-item">
<text class="f-label">手机号:</text>
<input class="f-input" v-model="form.phone" type="number" placeholder="请输入手机号" />
</view>
<view class="form-item">
<text class="f-label">编号:</text>
<input class="f-input" v-model="form.staff_no" placeholder="请输入服务人员编号" />
</view>
<view class="form-item">
<text class="f-label">机构ID</text>
<input class="f-input" v-model="form.station_id" placeholder="请输入服务机构/站点 ID" />
</view>
<view class="form-item">
<text class="f-label">服务区域:</text>
<input class="f-input" v-model="form.service_area" placeholder="请输入服务区域" />
</view>
<view class="form-item">
<text class="f-label">在线状态:</text>
<input class="f-input" v-model="form.online_status" placeholder="online / resting / busy" />
</view>
<view class="form-item">
<text class="f-label">资质状态:</text>
<input class="f-input" v-model="form.certificate_status" placeholder="valid / expired / pending" />
</view>
<view class="form-item">
<text class="f-label">资质到期:</text>
<input class="f-input" v-model="form.certificate_expire_at" placeholder="YYYY-MM-DD" />
</view>
<view class="form-item">
<text class="f-label">技能标签:</text>
<input class="f-input" v-model="form.skillsText" placeholder="多个标签请用英文逗号分隔" />
</view>
<view class="form-item">
<text class="f-label">头像:</text>
<view class="upload-placeholder" @click="handleUpload">
<image v-if="form.avatar" :src="form.avatar" mode="aspectFill" class="avatar-preview" />
<text v-else>+</text>
</view>
</view>
</view>
<view class="modal-footer">
<button class="btn" @click="closeModal">取消</button>
<button class="btn btn-primary" @click="handleSave">保存</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted } from 'vue'
import { fetchDeliveryStaffPage, saveDeliveryStaff, deleteDeliveryStaff, type DeliveryStaff } from '@/services/admin/deliveryService.uts'
const staffList = ref<DeliveryStaff[]>([])
const loading = ref(false)
const total = ref(0)
const page = ref(1)
const pageSize = 15
// Modal state
const showModal = ref(false)
const isEdit = ref(false)
const form = reactive({
id: '' as string | null,
uid: '' as string | null,
station_id: '' as string | null,
staff_no: '',
nickname: '',
phone: '',
avatar: '',
status: 1,
online_status: 'resting',
certificate_status: 'pending',
certificate_expire_at: '',
service_area: '',
skillsText: ''
})
onMounted(() => {
loadData()
})
async function loadData() {
loading.value = true
try {
const res = await fetchDeliveryStaffPage(page.value, pageSize)
staffList.value = res.items
total.value = res.total
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
function onAdd() {
isEdit.value = false
form.id = null
form.uid = null
form.station_id = ''
form.staff_no = ''
form.nickname = ''
form.phone = ''
form.avatar = ''
form.status = 1
form.online_status = 'resting'
form.certificate_status = 'pending'
form.certificate_expire_at = ''
form.service_area = ''
form.skillsText = ''
showModal.value = true
}
function onEdit(item : DeliveryStaff) {
isEdit.value = true
form.id = item.id
form.uid = item.uid
form.station_id = item.station_id || ''
form.staff_no = item.staff_no || ''
form.nickname = item.nickname
form.phone = item.phone
form.avatar = item.avatar || ''
form.status = item.status
form.online_status = item.online_status || 'resting'
form.certificate_status = item.certificate_status || 'pending'
form.certificate_expire_at = item.certificate_expire_at || ''
form.service_area = item.service_area || ''
form.skillsText = item.skills && item.skills.length > 0 ? item.skills.join(',') : ''
showModal.value = true
}
function closeModal() {
showModal.value = false
}
async function handleSave() {
if (!form.nickname || !form.phone) {
uni.showToast({ title: '请填写姓名和手机号', icon: 'none' })
return
}
if (form.online_status != 'online' && form.online_status != 'resting' && form.online_status != 'busy') {
uni.showToast({ title: '在线状态不合法', icon: 'none' })
return
}
if (form.certificate_status != 'valid' && form.certificate_status != 'expired' && form.certificate_status != 'pending') {
uni.showToast({ title: '资质状态不合法', icon: 'none' })
return
}
loading.value = true
try {
const resId = await saveDeliveryStaff(form)
if (resId != null) {
uni.showToast({ title: '保存成功' })
closeModal()
loadData()
}
} finally {
loading.value = false
}
}
async function onDelete(item : DeliveryStaff) {
uni.showModal({
title: '提示',
content: `确定要删除配送员 "${item.nickname}" 吗?`,
success: async (res) => {
if (res.confirm) {
const ok = await deleteDeliveryStaff(item.id)
if (ok) {
uni.showToast({ title: '删除成功' })
loadData()
}
}
}
})
}
async function onToggleStatus(item : DeliveryStaff) {
const nextStatus = item.status === 1 ? 0 : 1
const ok = await saveDeliveryStaff({ ...item, status: nextStatus })
if (ok != null) {
item.status = nextStatus
uni.showToast({ title: '状态已更新' })
}
}
function prevPage() { if (page.value > 1) { page.value--; loadData(); } }
function nextPage() { if (staffList.value.length >= pageSize) { page.value++; loadData(); } }
function formatTime(iso : string | null) : string {
if (!iso) return '-'
return iso.substring(0, 16).replace('T', ' ')
}
function handleUpload() {
uni.showToast({ title: '上传功能开发中', icon: 'none' })
}
</script>
<style scoped lang="scss">
.admin-page-container { padding: 24px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 24px; }
.action-wrap { margin-bottom: 24px; }
.btn { height: 32px; padding: 0 20px; border-radius: 4px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; }
.btn-primary { background-color: #1890ff; color: #fff; }
.table-wrap { border: 1px solid #f0f0f0; border-radius: 4px; }
.table-header { display: flex; flex-direction: row; background-color: #f8f8f9; }
.th { padding: 12px 10px; font-size: 14px; font-weight: bold; color: #515a6e; border-bottom: 1px solid #f0f0f0; text-align: center; }
.tr { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; min-height: 60px; align-items: center; }
.td { padding: 10px; display: flex; align-items: center; justify-content: center; }
.td-txt { font-size: 13px; color: #606266; }
.td-txt-small { font-size: 12px; color: #999; }
.avatar { width: 40px; height: 40px; border-radius: 4px; }
.avatar-placeholder { width: 40px; height: 40px; border-radius: 4px; background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; font-size: 20px; }
.action-btn { color: #1890ff; font-size: 13px; cursor: pointer; }
.danger { color: #ff4d4f; }
.divider { width: 1px; height: 12px; background-color: #e8e8e8; margin: 0 8px; }
.no-data, .loading-box { padding: 60px 0; text-align: center; width: 100%; }
.no-data-text { font-size: 14px; color: #ccc; }
.pagination-footer { margin-top: 24px; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 12px; }
.total-txt { font-size: 13px; color: #999; }
.page-btns { display: flex; flex-direction: row; gap: 8px; }
.p-btn { width: 30px; height: 30px; border: 1px solid #dcdee2; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; font-size: 13px; }
.p-btn.active { background-color: #1890ff; color: #fff; border-color: #1890ff; }
.p-btn.disabled { opacity: 0.5; cursor: not-allowed; }
/* Modal */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; }
.modal-card { width: 450px; background-color: #fff; border-radius: 8px; overflow: hidden; }
.modal-header { padding: 16px 20px; border-bottom: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
.modal-title { font-size: 16px; font-weight: bold; color: #333; }
.close-btn { font-size: 24px; color: #999; cursor: pointer; }
.modal-body { padding: 24px; }
.form-item { margin-bottom: 20px; display: flex; flex-direction: row; align-items: center; }
.f-label { width: 80px; font-size: 14px; color: #666; text-align: right; margin-right: 15px; }
.f-input { flex: 1; border: 1px solid #dcdfe6; height: 36px; padding: 0 12px; border-radius: 4px; font-size: 14px; }
.upload-placeholder { width: 64px; height: 64px; border: 1px dashed #dcdfe6; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 24px; color: #ccc; cursor: pointer; }
.avatar-preview { width: 100%; height: 100%; border-radius: 4px; }
.modal-footer { padding: 16px 24px; border-top: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: flex-end; gap: 12px; }
</style>