<script setup lang="tsx">
|
import noDataImg from '@/assets/element-img/no-data.png'
|
import type { FormInst, FormRules } from 'naive-ui'
|
import { NButton, NSpace } from 'naive-ui'
|
import VehicleSelectModal from './VehicleSelectModal.vue'
|
import StaffSelectModal from './StaffSelectModal.vue'
|
import { addFleet, editFleet, fetchCheckLocationList, fetchFleetDetail, fetchOperationsUserList } from '@/service/api/fleet'
|
|
interface FleetFormModel {
|
fleetName: string
|
operationsUserId: number | null
|
phoneNumber: string
|
locationId: number | null
|
}
|
|
interface SelectedStaff {
|
id: number
|
name: string
|
employeeNumber: string
|
position: string
|
isBind?: number | null
|
fleetId?: number | null
|
fleetName?: string
|
}
|
|
interface SelectedVehicle {
|
id: number
|
vehicleCode: string
|
vehicleNumber?: string
|
plateNumber?: string
|
type?: string
|
vehicleModel?: string
|
fleetId?: number | null
|
fleetName?: string
|
}
|
|
interface FleetData {
|
id?: number
|
fleetName: string
|
operationsUserName: string
|
phoneNumber: string
|
electronicName: string
|
staffList?: SelectedStaff[]
|
vehicleList?: SelectedVehicle[]
|
}
|
|
interface Props {
|
mode?: 'add' | 'edit'
|
editData?: FleetData | null
|
}
|
|
const props = withDefaults(defineProps<Props>(), {
|
mode: 'add',
|
editData: null,
|
})
|
|
const emit = defineEmits<{
|
confirm: []
|
}>()
|
const visible = defineModel<boolean>('visible', { default: false })
|
const formRef = ref<FormInst | null>(null)
|
|
// 计算弹窗标题
|
const modalTitle = computed(() => props.mode === 'edit' ? '编辑车队' : '新增车队')
|
|
// 计算成功提示文案
|
const successMessage = computed(() => props.mode === 'edit' ? '编辑车队成功' : '新增车队成功')
|
|
// 表单数据
|
const formModel = ref<FleetFormModel>({
|
fleetName: '',
|
operationsUserId: null,
|
phoneNumber: '',
|
locationId: null,
|
})
|
|
// 选中的人员列表
|
const selectedStaffList = ref<SelectedStaff[]>([])
|
|
// 计算已选 ID 列表
|
const selectedStaffIds = computed(() => selectedStaffList.value.map(item => item.id))
|
|
// 选中的车辆列表
|
const selectedVehicleList = ref<SelectedVehicle[]>([])
|
|
// 计算已选车辆 ID 列表
|
const selectedVehicleIds = computed(() => selectedVehicleList.value.map(item => item.id))
|
|
// 弹窗控制
|
const vehicleModalVisible = ref(false)
|
const staffModalVisible = ref(false)
|
|
// 下拉框展开状态
|
const isManagerSelectOpen = ref(false)
|
const isLocationSelectOpen = ref(false)
|
|
// 打卡地选项
|
const operatingAreaOptions = ref<Array<{ label: string, value: number }>>([])
|
|
// 负责人选项
|
const managerOptions = ref<Array<{ label: string, value: number, phone: string }>>([])
|
|
// 获取打卡地列表
|
async function getCheckLocationList() {
|
try {
|
const res = await fetchCheckLocationList()
|
if (res.code === 200 && res.data) {
|
operatingAreaOptions.value = res.data
|
.filter((item: any) => item.status === 1) // 只显示启用的打卡地
|
.map((item: any) => ({
|
label: item.locationAddress,
|
value: item.id,
|
}))
|
}
|
else {
|
// window.$message?.error(res.message || '获取打卡地列表失败')
|
}
|
}
|
catch (error) {
|
console.error('获取打卡地列表失败', error)
|
}
|
}
|
|
// 获取运维人员列表(负责人选项)
|
async function getManagerList() {
|
try {
|
const res = await fetchOperationsUserList({
|
pageNum: 1,
|
pageSize: 100, // 获取所有人员
|
})
|
if (res.code === 200 && res.data) {
|
managerOptions.value = res.data.records
|
.filter((item: any) => item.status === true) // 只显示启用的人员
|
.map((item: any) => ({
|
label: item.nickName,
|
value: item.id,
|
phone: item.phoneNumber,
|
}))
|
}
|
else {
|
// window.$message?.error(res.message || '获取负责人列表失败')
|
}
|
}
|
catch (error) {
|
console.error('获取负责人列表失败', error)
|
}
|
}
|
|
// 负责人选择变化时,自动填充联系电话
|
function handleManagerChange(value: number) {
|
const manager = managerOptions.value.find(item => item.value === value)
|
if (manager) {
|
formModel.value.phoneNumber = manager.phone
|
}
|
}
|
|
// 表单验证规则
|
const rules = computed<FormRules>(() => ({
|
fleetName: [
|
{ required: true, message: '请输入车队名称', trigger: 'blur' },
|
],
|
operationsUserId: [
|
{ required: true, message: '请输入负责人', trigger: 'blur' },
|
],
|
locationId: [
|
{ required: true, message: '请选择打卡地', trigger: 'change' },
|
],
|
}))
|
|
// 打开添加人员弹窗
|
function handleAddStaff() {
|
staffModalVisible.value = true
|
}
|
|
// 打开添加车辆弹窗
|
function handleAddVehicle() {
|
vehicleModalVisible.value = true
|
}
|
|
// 确认选择人员
|
function handleStaffConfirm(selectedStaff: any[]) {
|
selectedStaffList.value = selectedStaff.map(staff => ({
|
id: staff.id,
|
name: staff.nickName,
|
employeeNumber: staff.uniqueNumber,
|
position: staff.roleName,
|
isBind: staff.isBind ?? null,
|
fleetId: staff.fleetId ?? null,
|
fleetName: staff.fleetName || '',
|
}))
|
|
window.$message.success(`已更新 ${selectedStaff.length} 名人员`)
|
}
|
|
// 确认选择车辆
|
function handleVehicleConfirm(selectedVehicles: any[]) {
|
selectedVehicleList.value = selectedVehicles.map(vehicle => ({
|
id: vehicle.id,
|
vehicleCode: vehicle.vehicleCode,
|
vehicleNumber: vehicle.plateNumber,
|
plateNumber: vehicle.plateNumber,
|
type: vehicle.vehicleModel,
|
vehicleModel: vehicle.vehicleModel,
|
fleetId: vehicle.fleetId ?? null,
|
fleetName: vehicle.fleetName || '',
|
}))
|
|
window.$message.success(`已更新 ${selectedVehicles.length} 辆车辆`)
|
}
|
|
function hasFleetBinding(row: { fleetId?: number | null, fleetName?: string | null }) {
|
return row.fleetId != null || Boolean(row.fleetName?.trim())
|
}
|
|
function hasStaffFleetBinding(row: { isBind?: number | null, fleetId?: number | null, fleetName?: string | null }) {
|
return row.isBind === 1 || hasFleetBinding(row)
|
}
|
|
// 移除人员
|
function handleRemoveStaff(id: number) {
|
const index = selectedStaffList.value.findIndex(item => item.id === id)
|
if (index > -1) {
|
selectedStaffList.value.splice(index, 1)
|
window.$message.success('已移除')
|
}
|
}
|
|
// 移除车辆
|
function handleRemoveVehicle(id: number) {
|
const index = selectedVehicleList.value.findIndex(item => item.id === id)
|
if (index > -1) {
|
selectedVehicleList.value.splice(index, 1)
|
window.$message.success('已移除')
|
}
|
}
|
|
// 取消
|
function handleCancel() {
|
visible.value = false
|
resetForm()
|
}
|
|
// 确认
|
async function handleConfirm() {
|
try {
|
// 表单验证
|
await formRef.value?.validate()
|
|
// 调用接口
|
if (props.mode === 'edit' && props.editData?.id) {
|
// 编辑模式 - 只提交基本信息
|
const res = await editFleet({
|
id: props.editData.id,
|
fleetName: formModel.value.fleetName,
|
locationId: formModel.value.locationId!,
|
operationsUserId: formModel.value.operationsUserId!,
|
})
|
if (res.code !== 200) {
|
// window.$message?.error(res.message || '编辑车队失败')
|
return
|
}
|
}
|
else {
|
// 新增模式 - 需要校验并提交关联人员和车辆
|
if (selectedStaffList.value.length === 0) {
|
window.$message?.warning('请至少关联一名人员')
|
return
|
}
|
|
if (selectedVehicleList.value.length === 0) {
|
window.$message?.warning('请至少关联一辆车辆')
|
return
|
}
|
|
const res = await addFleet({
|
fleetName: formModel.value.fleetName,
|
locationId: formModel.value.locationId!,
|
operationsUserId: formModel.value.operationsUserId!,
|
operationsUserIds: selectedStaffList.value.map(item => item.id),
|
vehicleIds: selectedVehicleList.value.map(item => item.id),
|
})
|
if (res.code !== 200) {
|
// window.$message?.error(res.message || '新增车队失败')
|
return
|
}
|
}
|
|
window.$message?.success(successMessage.value)
|
emit('confirm')
|
visible.value = false
|
resetForm()
|
}
|
catch (error: any) {
|
console.error('保存车队失败', error)
|
if (error?.message) {
|
window.$message?.error(error.message)
|
}
|
}
|
}
|
|
// 重置表单
|
function resetForm() {
|
formModel.value = {
|
fleetName: '',
|
operationsUserId: null,
|
phoneNumber: '',
|
locationId: null,
|
}
|
selectedStaffList.value = []
|
selectedVehicleList.value = []
|
formRef.value?.restoreValidation()
|
}
|
|
// 初始化表单数据(编辑模式)
|
async function initFormData() {
|
if (props.mode === 'edit' && props.editData?.id) {
|
try {
|
const res = await fetchFleetDetail(props.editData.id)
|
if (res.code === 200 && res.data) {
|
const data = res.data
|
|
formModel.value = {
|
fleetName: data.fleetName || '',
|
operationsUserId: data.operationsUserId || null,
|
phoneNumber: props.editData.phoneNumber || '',
|
locationId: data.locationId || null,
|
}
|
}
|
else {
|
// window.$message?.error(res.message || '获取车队详情失败')
|
}
|
}
|
catch (error) {
|
console.error('获取车队详情失败', error)
|
window.$message?.error('获取车队详情失败')
|
}
|
}
|
}
|
|
// 监听弹窗打开
|
watch(visible, async (val) => {
|
if (val) {
|
// 弹窗打开时初始化数据
|
getCheckLocationList() // 获取打卡地列表
|
getManagerList() // 获取负责人列表
|
if (props.mode === 'edit') {
|
await initFormData()
|
}
|
}
|
else {
|
// 弹窗关闭时重置表单
|
resetForm()
|
}
|
})
|
|
// 人员表格列配置
|
const staffColumns = [
|
{ title: '序号', key: 'index', width: 60, render: (_row: any, index: number) => index + 1 },
|
{ title: '姓名', key: 'name', ellipsis: true },
|
{ title: '编号', key: 'employeeNumber', ellipsis: true },
|
{ title: '角色', key: 'position', ellipsis: true },
|
{
|
title: '操作',
|
key: 'actions',
|
width: 80,
|
render: (row: SelectedStaff) => {
|
return (
|
<div style="color: #0068FF; cursor: pointer;" onClick={() => handleRemoveStaff(row.id)}>
|
移除
|
</div>
|
)
|
},
|
},
|
]
|
|
// 车辆表格列配置
|
const vehicleColumns = [
|
{ title: '序号', key: 'index', width: 60, render: (_row: any, index: number) => index + 1 },
|
{ title: '车身编码', key: 'vehicleCode', ellipsis: true },
|
{ title: '车牌号', key: 'vehicleNumber', ellipsis: true },
|
{ title: '型号', key: 'type', ellipsis: true },
|
{
|
title: '操作',
|
key: 'actions',
|
width: 80,
|
render: (row: SelectedVehicle) => {
|
return (
|
<div style="color: #0068FF; cursor: pointer;" onClick={() => handleRemoveVehicle(row.id)}>
|
移除
|
</div>
|
)
|
},
|
},
|
]
|
</script>
|
|
<template>
|
<n-modal
|
v-model:show="visible"
|
:mask-closable="false"
|
preset="card"
|
:title="modalTitle"
|
:style="{ width: '600px' }"
|
:auto-focus="false"
|
:segmented="{
|
content: true,
|
footer: 'soft',
|
}"
|
>
|
<n-form
|
ref="formRef"
|
:model="formModel"
|
:rules="rules"
|
label-placement="top"
|
require-mark-placement="left"
|
>
|
<n-grid :cols="2" :x-gap="24">
|
<n-gi>
|
<n-form-item label="车队名称" path="fleetName">
|
<n-input v-model:value="formModel.fleetName" placeholder="请输入" />
|
</n-form-item>
|
</n-gi>
|
<n-gi>
|
<n-form-item label="负责人" path="operationsUserId">
|
<n-select
|
v-model:value="formModel.operationsUserId"
|
:options="managerOptions"
|
placeholder="请选择"
|
filterable
|
@update:value="handleManagerChange"
|
@update:show="isManagerSelectOpen = $event"
|
>
|
<template #arrow>
|
<img
|
src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
|
class="arrow-icon"
|
:class="{ 'arrow-icon--open': isManagerSelectOpen }"
|
style="width: 16px; height: 16px"
|
alt="arrow"
|
>
|
</template>
|
<template #empty>
|
<div class="flex-center">
|
<img
|
:src="noDataImg"
|
style="width: 50%"
|
alt="no-data"
|
>
|
</div>
|
</template>
|
</n-select>
|
</n-form-item>
|
</n-gi>
|
<n-gi>
|
<n-form-item label="联系电话" path="phoneNumber">
|
<n-input
|
v-model:value="formModel.phoneNumber"
|
placeholder="请选择负责人"
|
disabled
|
/>
|
</n-form-item>
|
</n-gi>
|
<n-gi>
|
<n-form-item label="打卡地" path="locationId">
|
<n-select
|
v-model:value="formModel.locationId"
|
:options="operatingAreaOptions"
|
placeholder="请选择"
|
@update:show="isLocationSelectOpen = $event"
|
>
|
<template #arrow>
|
<img
|
src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
|
class="arrow-icon"
|
:class="{ 'arrow-icon--open': isLocationSelectOpen }"
|
style="width: 16px; height: 16px"
|
alt="arrow"
|
>
|
</template>
|
<template #empty>
|
<div class="flex-center">
|
<img
|
:src="noDataImg"
|
style="width: 50%"
|
alt="no-data"
|
>
|
</div>
|
</template>
|
</n-select>
|
</n-form-item>
|
</n-gi>
|
</n-grid>
|
|
<!-- 只在新增模式下显示关联人员和车辆 -->
|
<template v-if="mode === 'add'">
|
<div class="section-header">
|
<div class="section-title">
|
<span class="required-mark">*</span>关联人员
|
</div>
|
<NSpace>
|
<NButton
|
size="small"
|
style="font-size: 12px"
|
@click="selectedStaffList = []"
|
>
|
重置
|
</NButton>
|
<NButton
|
type="primary"
|
style="
|
background-color: #e6f4ff;
|
font-size: 12px;
|
color: #0068ff;
|
border: 1px solid #7abdff;
|
"
|
secondary
|
size="small"
|
@click="handleAddStaff"
|
>
|
添加
|
</NButton>
|
</NSpace>
|
</div>
|
<n-form-item :show-label="false">
|
<NSpace vertical style="width: 100%">
|
<!-- 人员列表 -->
|
<n-data-table
|
:columns="staffColumns"
|
:data="selectedStaffList"
|
:max-height="200"
|
size="small"
|
bordered
|
>
|
<template #empty>
|
<div class="flex-center">
|
<img
|
:src="noDataImg"
|
style="width: 123px; height: 125px"
|
>
|
</div>
|
</template>
|
</n-data-table>
|
</NSpace>
|
</n-form-item>
|
|
<div class="section-header">
|
<div class="section-title">
|
<span class="required-mark">*</span>关联车辆
|
</div>
|
<NSpace>
|
<NButton
|
size="small"
|
style="font-size: 12px"
|
@click="selectedVehicleList = []"
|
>
|
重置
|
</NButton>
|
<NButton
|
type="primary"
|
style="
|
background-color: #e6f4ff;
|
font-size: 12px;
|
color: #0068ff;
|
border: 1px solid #7abdff;
|
"
|
secondary
|
size="small"
|
@click="handleAddVehicle"
|
>
|
添加
|
</NButton>
|
</NSpace>
|
</div>
|
<n-form-item :show-label="false">
|
<NSpace vertical style="width: 100%">
|
<!-- 车辆列表 -->
|
<n-data-table
|
:columns="vehicleColumns"
|
:data="selectedVehicleList"
|
:max-height="200"
|
size="small"
|
bordered
|
>
|
<template #empty>
|
<div class="flex-center">
|
<img
|
:src="noDataImg"
|
style="width: 123px; height: 125px"
|
>
|
</div>
|
</template>
|
</n-data-table>
|
</NSpace>
|
</n-form-item>
|
</template>
|
</n-form>
|
|
<template #footer>
|
<NSpace justify="end">
|
<NButton class="footer-btn" @click="handleCancel">
|
取消
|
</NButton>
|
<NButton class="footer-btn" type="primary" @click="handleConfirm">
|
保存
|
</NButton>
|
</NSpace>
|
</template>
|
|
<!-- 人员选择弹窗 -->
|
<StaffSelectModal
|
v-model:visible="staffModalVisible"
|
:selected-ids="selectedStaffIds"
|
:request-params="{ isBind: 0 }"
|
:selection-disabled="hasStaffFleetBinding"
|
@confirm="handleStaffConfirm"
|
/>
|
|
<!-- 车辆选择弹窗 -->
|
<VehicleSelectModal
|
v-model:visible="vehicleModalVisible"
|
:selected-ids="selectedVehicleIds"
|
:selection-disabled="hasFleetBinding"
|
:isBind="0"
|
show-vehicle-code-search
|
@confirm="handleVehicleConfirm"
|
/>
|
</n-modal>
|
</template>
|
|
<style scoped lang="scss">
|
:deep(.n-form-item-label) {
|
font-weight: 500;
|
}
|
|
.section-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 12px;
|
margin-top: 8px;
|
}
|
|
.section-title {
|
font-size: 16px;
|
font-weight: 500;
|
display: flex;
|
align-items: center;
|
padding-left: 12px;
|
position: relative;
|
|
&::before {
|
content: "";
|
position: absolute;
|
left: 0;
|
top: 50%;
|
transform: translateY(-50%);
|
width: 4px;
|
height: 16px;
|
background-color: #1890ff;
|
border-radius: 2px;
|
}
|
}
|
|
.required-mark {
|
color: #d03050;
|
margin-right: 4px;
|
}
|
|
:deep(.n-form-item.n-form-item--top-labelled) {
|
margin-bottom: 8px;
|
}
|
|
:deep(.n-data-table .n-data-table-thead .n-data-table-th) {
|
background-color: #f7f8fa;
|
font-weight: 500;
|
}
|
:deep(.n-data-table-th__title) {
|
font-size: 13px;
|
}
|
|
:deep(.n-empty) {
|
background-color: #f7f8fa;
|
border-radius: 4px;
|
}
|
|
.footer-btn {
|
padding: 7px 29px;
|
height: auto;
|
line-height: 18px;
|
}
|
|
.arrow-icon {
|
transition: transform 0.3s ease;
|
&--open {
|
transform: rotate(180deg);
|
}
|
}
|
</style>
|