Merge branch 'deploy-1.0' into feat/deploy-1.0
| | |
| | | { path: '/operations/attendance/address', type: 34 }, |
| | | { path: '/operations/cruiseCheckInManagement/cruiseCheckIn', type: 34 }, |
| | | { path: '/operations/attendance/shift', type: 35 }, |
| | | { path: '/operations/taskOrder/taskOrderCenter', type: 37 }, |
| | | { path: '/operations/taskOrderCenter', type: 37 }, |
| | | { path: '/operations/cruiseCheckInManagement/cruiseCheckInRule', type: 38 }, |
| | | { path: '/service/review/personnel', type: 39 }, |
| | | { path: '/service/review/policyApplication', type: 40 }, |
| | |
| | | { path: '/transaction/order/orderManagement', type: 43 }, |
| | | { path: '/testDemo/testPointManagement/list', type: 44 }, |
| | | { path: '/businessOperations/pointManagement', type: 45 }, |
| | | { path: '/userContent/whitelistManagement/invitationCode', type: 46 }, |
| | | { path: '/userContent/whitelist-management/invitation-code', type: 46 }, |
| | | ] |
| | | |
| | | const resolvedBusinessType = computed<number | undefined>(() => { |
| | |
| | | activeMenu: '/operations/personnel/personnelManagement', |
| | | }, |
| | | { |
| | | name: 'operationsSafetyOfficerInfo', |
| | | path: '/operations/personnel/safetyOfficerInfo', |
| | | title: '安全员管理', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/operations/personnel/safetyOfficerInfo/index.vue', |
| | | id: 7012, |
| | | keepAlive: false, |
| | | pid: 701, |
| | | }, |
| | | { |
| | | name: 'operationsSafetyOfficerInfoAdd', |
| | | path: '/operations/personnel/safetyOfficerInfo/add', |
| | | title: '添加安全员', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/operations/personnel/safetyOfficerInfo/add.vue', |
| | | id: 70121, |
| | | keepAlive: false, |
| | | pid: 701, |
| | | hide: true, |
| | | activeMenu: '/operations/personnel/safetyOfficerInfo', |
| | | fullScreen: true, |
| | | }, |
| | | { |
| | | name: 'operationsAttendance', |
| | | path: '/operations/attendance', |
| | | title: '排班考勤', |
| | |
| | | |
| | | /** 运维人员管理 */ |
| | | |
| | | // 获取安全员列表 |
| | | export function getSafetyOfficerList(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/vehicleConvoy/api/v1/safetyOfficer/pageList', params) |
| | | } |
| | | |
| | | // 新增安全员 |
| | | export function addSafetyOfficer(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/vehicleConvoy/api/v1/safetyOfficer/add', params) |
| | | } |
| | | |
| | | // 编辑安全员 |
| | | export function editSafetyOfficer(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/vehicleConvoy/api/v1/safetyOfficer/edit', params) |
| | | } |
| | | |
| | | // 获取安全员详情 |
| | | export function getSafetyOfficerDetailById(id: number | string) { |
| | | return request.Get<Service.ResponseResult<any>>(`/vehicleConvoy/api/v1/safetyOfficer/detailById?id=${id}`) |
| | | } |
| | | |
| | | // 启用禁用安全员 |
| | | export function updateSafetyOfficerStatus(params: { id: number | string, status: 0 | 1 }) { |
| | | return request.Post<Service.ResponseResult<any>>('/vehicleConvoy/api/v1/safetyOfficer/enableDisableById', params) |
| | | } |
| | | |
| | | // 安全员导出 |
| | | export function exportSafetyOfficer(params: any) { |
| | | const method = request.Post<Service.ResponseResult<any>>('/vehicleConvoy/api/v1/safetyOfficer/exportExcel', params, { responseType: 'blob' }) |
| | | method.meta = { ...method.meta, isBlob: true } |
| | | return method |
| | | } |
| | | |
| | | // 安全员导入模板 |
| | | export function exportSafetyOfficerTemplate() { |
| | | const method = request.Get<Service.ResponseResult<any>>('/vehicleConvoy/api/v1/safetyOfficer/importTemplate', {}, { responseType: 'blob' }) |
| | | method.meta = { ...method.meta, isBlob: true } |
| | | return method |
| | | } |
| | | |
| | | // 安全员导入 |
| | | export function importSafetyOfficer(file: File) { |
| | | const formData = new FormData() |
| | | formData.append('file', file) |
| | | return request.Post<Service.ResponseResult<any>>('/vehicleConvoy/api/v1/safetyOfficer/importExcel', formData) |
| | | } |
| | | |
| | | // 获取运维人员列表 |
| | | export function getOperationsUserList(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/account/api/v1/operationsUser/pageList', params) |
| | |
| | | placeholder="请选择" |
| | | clearable |
| | | multiple |
| | | max-tag-count="responsive" |
| | | max-tag-count="responsive" |
| | | onUpdate:show={(val: boolean) => isStationPointNumOpen.value = val} |
| | | > |
| | | {{ |
| | |
| | | placeholder="请选择" |
| | | clearable |
| | | multiple |
| | | max-tag-count="responsive" |
| | | max-tag-count="responsive" |
| | | onUpdate:show={(val: boolean) => isStationStatusOpen.value = val} |
| | | > |
| | | {{ |
| | |
| | | if (activeTab.value === 'fence') |
| | | return 22 |
| | | if (activeTab.value === 'station') |
| | | return 57 |
| | | return 58 |
| | | return 45 |
| | | }) |
| | | |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { NButton, NRadio, NRadioGroup, NSpace } from 'naive-ui' |
| | | import dayjs from 'dayjs' |
| | | import { addSafetyOfficer, editSafetyOfficer, getSafetyOfficerDetailById } from '@/service/api/attendance' |
| | | |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const formRef = ref<FormInst | null>(null) |
| | | const saving = ref(false) |
| | | |
| | | const isNationSelectOpen = ref(false) |
| | | const isPoliticalStatusSelectOpen = ref(false) |
| | | |
| | | const formModel = reactive({ |
| | | name: '', |
| | | idCardNo: '', |
| | | phoneNumber: '', |
| | | birthDate: null as number | null, |
| | | nation: null as number | null, |
| | | politicalStatus: null as number | null, |
| | | contactAddress: '', |
| | | drivingLicenseNo: '', |
| | | firstLicenseDate: null as number | null, |
| | | drivingLicenseDateRange: null as [number, number] | null, |
| | | gender: 1 as 1 | 2, |
| | | status: 1 as 1 | 2, |
| | | }) |
| | | |
| | | const safetyOfficerId = computed(() => { |
| | | const id = route.query.id |
| | | if (!id) |
| | | return undefined |
| | | return Number(id) |
| | | }) |
| | | |
| | | const isEditMode = computed(() => route.query.mode === 'edit' && safetyOfficerId.value !== undefined && !Number.isNaN(safetyOfficerId.value)) |
| | | |
| | | const nationOptions = [ |
| | | { label: '汉族', value: 1 }, |
| | | { label: '蒙古族', value: 2 }, |
| | | { label: '回族', value: 3 }, |
| | | { label: '藏族', value: 4 }, |
| | | { label: '维吾尔族', value: 5 }, |
| | | { label: '苗族', value: 6 }, |
| | | { label: '彝族', value: 7 }, |
| | | { label: '壮族', value: 8 }, |
| | | { label: '布依族', value: 9 }, |
| | | { label: '朝鲜族', value: 10 }, |
| | | { label: '满族', value: 11 }, |
| | | { label: '侗族', value: 12 }, |
| | | { label: '瑶族', value: 13 }, |
| | | { label: '白族', value: 14 }, |
| | | { label: '土家族', value: 15 }, |
| | | { label: '哈尼族', value: 16 }, |
| | | { label: '哈萨克族', value: 17 }, |
| | | { label: '傣族', value: 18 }, |
| | | { label: '黎族', value: 19 }, |
| | | { label: '傈僳族', value: 20 }, |
| | | { label: '佤族', value: 21 }, |
| | | { label: '畲族', value: 22 }, |
| | | { label: '高山族', value: 23 }, |
| | | { label: '拉祜族', value: 24 }, |
| | | { label: '水族', value: 25 }, |
| | | { label: '东乡族', value: 26 }, |
| | | { label: '纳西族', value: 27 }, |
| | | { label: '景颇族', value: 28 }, |
| | | { label: '柯尔克孜族', value: 29 }, |
| | | { label: '土族', value: 30 }, |
| | | { label: '达斡尔族', value: 31 }, |
| | | { label: '仫佬族', value: 32 }, |
| | | { label: '羌族', value: 33 }, |
| | | { label: '布朗族', value: 34 }, |
| | | { label: '撒拉族', value: 35 }, |
| | | { label: '毛南族', value: 36 }, |
| | | { label: '仡佬族', value: 37 }, |
| | | { label: '锡伯族', value: 38 }, |
| | | { label: '阿昌族', value: 39 }, |
| | | { label: '普米族', value: 40 }, |
| | | { label: '塔吉克族', value: 41 }, |
| | | { label: '怒族', value: 42 }, |
| | | { label: '乌孜别克族', value: 43 }, |
| | | { label: '俄罗斯族', value: 44 }, |
| | | { label: '鄂温克族', value: 45 }, |
| | | { label: '德昂族', value: 46 }, |
| | | { label: '保安族', value: 47 }, |
| | | { label: '裕固族', value: 48 }, |
| | | { label: '京族', value: 49 }, |
| | | { label: '塔塔尔族', value: 50 }, |
| | | { label: '独龙族', value: 51 }, |
| | | { label: '鄂伦春族', value: 52 }, |
| | | { label: '赫哲族', value: 53 }, |
| | | { label: '门巴族', value: 54 }, |
| | | { label: '珞巴族', value: 55 }, |
| | | { label: '基诺族', value: 56 }, |
| | | ] |
| | | |
| | | const politicalStatusOptions = [ |
| | | { label: '中共党员', value: 1 }, |
| | | { label: '中共预备党员', value: 2 }, |
| | | { label: '共青团员', value: 3 }, |
| | | { label: '民革党员', value: 4 }, |
| | | { label: '民盟盟员', value: 5 }, |
| | | ] |
| | | |
| | | const rules: FormRules = { |
| | | name: [ |
| | | { required: true, message: '请输入安全员姓名', trigger: ['blur', 'input'] }, |
| | | ], |
| | | idCardNo: [ |
| | | { required: true, message: '请输入身份证号', trigger: ['blur', 'input'] }, |
| | | ], |
| | | phoneNumber: [ |
| | | { required: true, message: '请输入联系电话', trigger: ['blur', 'input'] }, |
| | | ], |
| | | birthDate: [ |
| | | { required: true, type: 'number', message: '请选择出生日期', trigger: ['change', 'blur'] }, |
| | | ], |
| | | gender: [ |
| | | { required: true, type: 'number', message: '请选择性别', trigger: 'change' }, |
| | | ], |
| | | nation: [ |
| | | { required: true, type: 'number', message: '请选择民族', trigger: 'change' }, |
| | | ], |
| | | politicalStatus: [ |
| | | { required: true, type: 'number', message: '请选择政治面貌', trigger: 'change' }, |
| | | ], |
| | | contactAddress: [ |
| | | { required: true, message: '请输入通信地址', trigger: ['blur', 'input'] }, |
| | | ], |
| | | drivingLicenseNo: [ |
| | | { required: true, message: '请输入机动车驾驶证号', trigger: ['blur', 'input'] }, |
| | | ], |
| | | firstLicenseDate: [ |
| | | { required: true, type: 'number', message: '请选择初次领证日期', trigger: ['change', 'blur'] }, |
| | | ], |
| | | drivingLicenseDateRange: [ |
| | | { required: true, type: 'array', message: '请选择驾驶证有效期', trigger: 'change' }, |
| | | ], |
| | | status: [ |
| | | { required: true, type: 'number', message: '请选择状态', trigger: 'change' }, |
| | | ], |
| | | } |
| | | |
| | | const pageTitle = computed(() => isEditMode.value ? '编辑安全员' : '添加安全员') |
| | | |
| | | function formatDate(value: number | null) { |
| | | if (!value) |
| | | return '' |
| | | return dayjs(value).format('YYYY-MM-DD') |
| | | } |
| | | |
| | | function parseDate(value?: string | null) { |
| | | if (!value) |
| | | return null |
| | | const dateValue = dayjs(value) |
| | | return dateValue.isValid() ? dateValue.valueOf() : null |
| | | } |
| | | |
| | | function setFormData(detail: any) { |
| | | formModel.name = detail?.name || '' |
| | | formModel.idCardNo = detail?.idCardNo || '' |
| | | formModel.phoneNumber = detail?.phoneNumber || '' |
| | | formModel.birthDate = parseDate(detail?.birthDate) |
| | | formModel.nation = detail?.nation ? Number(detail.nation) : null |
| | | formModel.politicalStatus = detail?.politicalStatus ? Number(detail.politicalStatus) : null |
| | | formModel.contactAddress = detail?.contactAddress || '' |
| | | formModel.drivingLicenseNo = detail?.drivingLicenseNo || '' |
| | | formModel.firstLicenseDate = parseDate(detail?.firstLicenseDate) |
| | | formModel.drivingLicenseDateRange = detail?.drivingLicenseStartDate && detail?.drivingLicenseEndDate |
| | | ? [parseDate(detail.drivingLicenseStartDate) as number, parseDate(detail.drivingLicenseEndDate) as number] |
| | | : null |
| | | formModel.gender = detail?.gender === 2 ? 2 : 1 |
| | | |
| | | if (typeof detail?.status === 'number') |
| | | formModel.status = detail.status === 0 ? 2 : 1 |
| | | else if (typeof detail?.deleted === 'boolean') |
| | | formModel.status = detail.deleted ? 2 : 1 |
| | | } |
| | | |
| | | async function getDetail() { |
| | | if (!isEditMode.value || safetyOfficerId.value === undefined) |
| | | return |
| | | |
| | | try { |
| | | const res = await getSafetyOfficerDetailById(safetyOfficerId.value) |
| | | if ((res.code === 200 || res.code === 0) && res.data) { |
| | | setFormData(res.data) |
| | | return |
| | | } |
| | | window.$message?.warning(res.msg || '获取安全员详情失败') |
| | | } |
| | | catch (error) { |
| | | console.error('获取安全员详情失败:', error) |
| | | window.$message?.error('获取安全员详情失败') |
| | | } |
| | | } |
| | | |
| | | async function handleSave() { |
| | | try { |
| | | saving.value = true |
| | | await formRef.value?.validate() |
| | | const [drivingLicenseStartDate, drivingLicenseEndDate] = formModel.drivingLicenseDateRange || [] |
| | | const params = { |
| | | birthDate: formatDate(formModel.birthDate), |
| | | contactAddress: formModel.contactAddress.trim(), |
| | | drivingLicenseEndDate: formatDate(drivingLicenseEndDate ?? null), |
| | | drivingLicenseNo: formModel.drivingLicenseNo.trim(), |
| | | drivingLicenseStartDate: formatDate(drivingLicenseStartDate ?? null), |
| | | firstLicenseDate: formatDate(formModel.firstLicenseDate), |
| | | gender: formModel.gender, |
| | | idCardNo: formModel.idCardNo.trim(), |
| | | name: formModel.name.trim(), |
| | | nation: String(formModel.nation ?? ''), |
| | | phoneNumber: formModel.phoneNumber.trim(), |
| | | politicalStatus: String(formModel.politicalStatus ?? ''), |
| | | status: formModel.status, |
| | | } |
| | | const res = isEditMode.value |
| | | ? await editSafetyOfficer({ |
| | | ...params, |
| | | id: safetyOfficerId.value, |
| | | }) |
| | | : await addSafetyOfficer(params) |
| | | if (res.code === 200 || res.code === 0) { |
| | | window.sessionStorage.setItem('safetyOfficerInfoNeedRefresh', '1') |
| | | window.$message?.success(isEditMode.value ? '编辑安全员成功' : '新增安全员成功') |
| | | router.back() |
| | | } |
| | | } |
| | | finally { |
| | | saving.value = false |
| | | } |
| | | } |
| | | |
| | | function handleCancel() { |
| | | router.back() |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getDetail() |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="w-full h-full relative content flex flex-col"> |
| | | <div class="p-12px main flex flex-col flex-1 overflow-hidden"> |
| | | <n-form |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="left" |
| | | require-mark-placement="left" |
| | | > |
| | | <div class="box"> |
| | | <div class="section-header"> |
| | | <div class="section-title"> |
| | | {{ pageTitle }} |
| | | </div> |
| | | </div> |
| | | <n-grid :cols="3" :x-gap="24" :y-gap="8"> |
| | | <n-gi> |
| | | <n-form-item label="安全员姓名" path="name"> |
| | | <n-input v-model:value="formModel.name" placeholder="请输入" clearable /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | <n-gi> |
| | | <n-form-item label="身份证号" path="idCardNo"> |
| | | <n-input v-model:value="formModel.idCardNo" placeholder="请输入" clearable /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | |
| | | <n-gi> |
| | | <n-form-item label="联系电话" path="phoneNumber"> |
| | | <n-input v-model:value="formModel.phoneNumber" placeholder="请输入" clearable /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | <n-gi> |
| | | <n-form-item label="出生日期" path="birthDate"> |
| | | <n-date-picker |
| | | v-model:value="formModel.birthDate" |
| | | type="date" |
| | | placeholder="请选择" |
| | | clearable |
| | | format="yyyy/MM/dd" |
| | | :actions="[]" |
| | | style="width: 100%" |
| | | /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | <n-gi> |
| | | <n-form-item label="性别" path="gender"> |
| | | <NRadioGroup v-model:value="formModel.gender"> |
| | | <NSpace> |
| | | <NRadio :value="1"> |
| | | 男 |
| | | </NRadio> |
| | | <NRadio :value="2"> |
| | | 女 |
| | | </NRadio> |
| | | </NSpace> |
| | | </NRadioGroup> |
| | | </n-form-item> |
| | | </n-gi> |
| | | |
| | | <n-gi> |
| | | <n-form-item label="民族" path="nation"> |
| | | <n-select |
| | | v-model:value="formModel.nation" |
| | | placeholder="请选择" |
| | | clearable |
| | | :options="nationOptions" |
| | | @update:show="(val: boolean) => (isNationSelectOpen = val)" |
| | | > |
| | | <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': isNationSelectOpen }" |
| | | style="width: 16px; height: 16px" |
| | | alt="arrow" |
| | | > |
| | | </template> |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img |
| | | src="/src/assets/element-img/no-data.png" |
| | | style="width: 50%" |
| | | alt="no-data" |
| | | > |
| | | </div> |
| | | </template> |
| | | </n-select> |
| | | </n-form-item> |
| | | </n-gi> |
| | | <n-gi> |
| | | <n-form-item label="政治面貌" path="politicalStatus"> |
| | | <n-select |
| | | v-model:value="formModel.politicalStatus" |
| | | :options="politicalStatusOptions" |
| | | placeholder="请选择" |
| | | clearable |
| | | @update:show="(val: boolean) => (isPoliticalStatusSelectOpen = val)" |
| | | > |
| | | <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': isPoliticalStatusSelectOpen }" |
| | | style="width: 16px; height: 16px" |
| | | alt="arrow" |
| | | > |
| | | </template> |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img |
| | | src="/src/assets/element-img/no-data.png" |
| | | style="width: 50%" |
| | | alt="no-data" |
| | | > |
| | | </div> |
| | | </template> |
| | | </n-select> |
| | | </n-form-item> |
| | | </n-gi> |
| | | <n-gi> |
| | | <n-form-item label="通信地址" path="contactAddress"> |
| | | <n-input v-model:value="formModel.contactAddress" placeholder="请输入" clearable /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | </n-grid> |
| | | </div> |
| | | |
| | | <div class="box mt-12px"> |
| | | <n-grid :cols="3" :x-gap="24" :y-gap="8"> |
| | | <n-gi> |
| | | <n-form-item label="机动车驾驶证号" path="drivingLicenseNo"> |
| | | <n-input v-model:value="formModel.drivingLicenseNo" placeholder="请输入" clearable /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | <n-gi> |
| | | <n-form-item label="初次领证日期" path="firstLicenseDate"> |
| | | <n-date-picker |
| | | v-model:value="formModel.firstLicenseDate" |
| | | type="date" |
| | | placeholder="请选择" |
| | | clearable |
| | | format="yyyy/MM/dd" |
| | | :actions="[]" |
| | | style="width: 100%" |
| | | /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | <n-gi> |
| | | <n-form-item label="驾驶证有效期" path="drivingLicenseDateRange"> |
| | | <n-date-picker |
| | | v-model:value="formModel.drivingLicenseDateRange" |
| | | type="daterange" |
| | | placeholder="开始时间 - 结束时间" |
| | | clearable |
| | | format="yyyy/MM/dd" |
| | | :actions="[]" |
| | | style="width: 100%" |
| | | close-on-select |
| | | /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | </n-grid> |
| | | </div> |
| | | |
| | | <div class="box mt-12px"> |
| | | <n-form-item label="状态" path="status"> |
| | | <NRadioGroup v-model:value="formModel.status"> |
| | | <NSpace> |
| | | <NRadio :value="1"> |
| | | 启用 |
| | | </NRadio> |
| | | <NRadio :value="2"> |
| | | 停用 |
| | | </NRadio> |
| | | </NSpace> |
| | | </NRadioGroup> |
| | | </n-form-item> |
| | | </div> |
| | | </n-form> |
| | | </div> |
| | | |
| | | <div class="footer"> |
| | | <NSpace justify="end" :size="8"> |
| | | <NButton class="w-84px" @click="handleCancel"> |
| | | 取消 |
| | | </NButton> |
| | | <NButton type="primary" class="w-84px" :loading="saving" @click="handleSave"> |
| | | 保存 |
| | | </NButton> |
| | | </NSpace> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .content { |
| | | height: calc(100vh - 76px); |
| | | width: 100%; |
| | | overflow-y: auto; |
| | | scrollbar-width: none; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .box { |
| | | padding: 24px; |
| | | padding-bottom: 16px; |
| | | border-radius: 6px; |
| | | background: #fff; |
| | | } |
| | | |
| | | .content::-webkit-scrollbar { |
| | | display: none; |
| | | } |
| | | |
| | | .main { |
| | | padding-bottom: 88px; |
| | | } |
| | | |
| | | .section-card { |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .section-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .section-title { |
| | | position: relative; |
| | | padding-left: 10px; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #171f26; |
| | | } |
| | | |
| | | .section-title::before { |
| | | content: ''; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 3px; |
| | | height: 16px; |
| | | background: #0068ff; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .footer { |
| | | position: absolute; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | padding: 12px 24px; |
| | | background: #fff; |
| | | border-top: 1px solid #f0f0f0; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { computed, onActivated, onMounted, ref } from 'vue' |
| | | import dayjs from 'dayjs' |
| | | import { NButton, NTag, useDialog } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | import { useRouter } from 'vue-router' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | import OperationLogModal from '@/components/custom/OperationLogModal.vue' |
| | | import { exportSafetyOfficer, exportSafetyOfficerTemplate, getSafetyOfficerList, importSafetyOfficer, updateSafetyOfficerStatus } from '@/service/api/attendance' |
| | | import { downloadFile } from '@/utils' |
| | | |
| | | interface SafetyOfficerItem { |
| | | id: number |
| | | name: string |
| | | idCardNo: string |
| | | phoneNumber: string |
| | | gender: number |
| | | birthDate: string |
| | | drivingLicenseNo: string |
| | | createBy: string |
| | | createTime: string |
| | | deleted: boolean |
| | | contactAddress?: string |
| | | drivingLicenseEndDate?: string |
| | | drivingLicenseStartDate?: string |
| | | firstLicenseDate?: string |
| | | nation?: string |
| | | politicalStatus?: string |
| | | updateBy?: string |
| | | updateTime?: string |
| | | } |
| | | |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | const router = useRouter() |
| | | const dialog = useDialog() |
| | | |
| | | const searchModel = ref({ |
| | | id: null as number | null, |
| | | name: '', |
| | | idCardNo: '', |
| | | phoneNumber: '', |
| | | drivingLicenseNo: '', |
| | | createTimeRange: null as [number, number] | null, |
| | | status: null as number | null, |
| | | }) |
| | | |
| | | const pageParams = ref({ |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | }) |
| | | |
| | | const isStatusSelectOpen = ref(false) |
| | | const showOperationLogModal = ref(false) |
| | | const checkedRowKeys = ref<Array<string | number>>([]) |
| | | const listData = ref<SafetyOfficerItem[]>([]) |
| | | const total = ref(0) |
| | | const fileInputRef = ref<HTMLInputElement | null>(null) |
| | | |
| | | const statusOptions = [ |
| | | { label: '启用', value: 1 }, |
| | | { label: '停用', value: 2 }, |
| | | ] |
| | | |
| | | function buildQueryParams() { |
| | | return { |
| | | id: searchModel.value.id ?? undefined, |
| | | name: searchModel.value.name?.trim() || undefined, |
| | | idCardNo: searchModel.value.idCardNo?.trim() || undefined, |
| | | phoneNumber: searchModel.value.phoneNumber?.trim() || undefined, |
| | | drivingLicenseNo: searchModel.value.drivingLicenseNo?.trim() || undefined, |
| | | pageNum: pageParams.value.pageNum, |
| | | pageSize: pageParams.value.pageSize, |
| | | } |
| | | } |
| | | |
| | | async function getList() { |
| | | startLoading() |
| | | try { |
| | | const res = await getSafetyOfficerList(buildQueryParams()) |
| | | if (res.code === 200 || res.code === 0) { |
| | | listData.value = res.data?.records || [] |
| | | total.value = Number(res.data?.total ?? 0) |
| | | return |
| | | } |
| | | listData.value = [] |
| | | total.value = 0 |
| | | } |
| | | catch (error) { |
| | | console.error('获取安全员列表失败:', error) |
| | | listData.value = [] |
| | | total.value = 0 |
| | | } |
| | | finally { |
| | | endLoading() |
| | | } |
| | | } |
| | | |
| | | function handleSearch() { |
| | | pageParams.value.pageNum = 1 |
| | | getList() |
| | | } |
| | | |
| | | function handleReset() { |
| | | searchModel.value = { |
| | | id: null, |
| | | name: '', |
| | | idCardNo: '', |
| | | phoneNumber: '', |
| | | drivingLicenseNo: '', |
| | | createTimeRange: null, |
| | | status: null, |
| | | } |
| | | pageParams.value.pageNum = 1 |
| | | getList() |
| | | } |
| | | |
| | | function changePage(page: number, pageSize: number) { |
| | | pageParams.value.pageNum = page |
| | | pageParams.value.pageSize = pageSize |
| | | getList() |
| | | } |
| | | |
| | | function handleCheck(rowKeys: Array<string | number>) { |
| | | checkedRowKeys.value = rowKeys |
| | | } |
| | | |
| | | async function handleBatchExport() { |
| | | if (checkedRowKeys.value.length === 0) { |
| | | window.$message?.warning('请先选择需要导出的记录') |
| | | return |
| | | } |
| | | |
| | | try { |
| | | const response = await exportSafetyOfficer({ |
| | | ...buildQueryParams(), |
| | | // 按批量导出语义优先传勾选 id,同时兼容后端常见 ids 入参 |
| | | ids: checkedRowKeys.value, |
| | | }) |
| | | if (response instanceof Blob) { |
| | | downloadFile(response, `安全员列表_${dayjs().format('YYYYMMDDHHmmss')}.xlsx`) |
| | | window.$message?.success('导出成功') |
| | | } |
| | | } |
| | | catch (error) { |
| | | console.error('导出安全员失败:', error) |
| | | } |
| | | } |
| | | |
| | | function handleBatchImport() { |
| | | fileInputRef.value?.click() |
| | | } |
| | | |
| | | async function handleFileChange(event: Event) { |
| | | const target = event.target as HTMLInputElement |
| | | const file = target.files?.[0] |
| | | if (!file) |
| | | return |
| | | |
| | | try { |
| | | // 复用项目内统一的 excel 导入方式,成功后刷新列表 |
| | | const res = await importSafetyOfficer(file) |
| | | if (res.code === 200 || res.code === 0) { |
| | | window.$message?.success(res.msg || '导入成功') |
| | | getList() |
| | | } |
| | | } |
| | | catch (error) { |
| | | console.error('导入安全员失败:', error) |
| | | } |
| | | finally { |
| | | // 允许重复选择同一个文件时也能触发 change |
| | | target.value = '' |
| | | } |
| | | } |
| | | |
| | | async function handleDownloadTemplate() { |
| | | try { |
| | | const response = await exportSafetyOfficerTemplate() |
| | | if (response instanceof Blob) { |
| | | downloadFile(response, '安全员导入模板.xlsx') |
| | | window.$message?.success('下载成功') |
| | | } |
| | | } |
| | | catch (error) { |
| | | console.error('下载安全员导入模板失败:', error) |
| | | } |
| | | } |
| | | |
| | | function handleOpenOperationLog() { |
| | | showOperationLogModal.value = true |
| | | } |
| | | |
| | | function handleAdd() { |
| | | router.push({ name: 'operationsSafetyOfficerInfoAdd' }) |
| | | } |
| | | |
| | | function handleEdit(row: SafetyOfficerItem) { |
| | | router.push({ |
| | | name: 'operationsSafetyOfficerInfoAdd', |
| | | query: { |
| | | mode: 'edit', |
| | | id: String(row.id), |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | async function handleToggleStatus(row: SafetyOfficerItem) { |
| | | const nextStatus = row.deleted ? 1 : 0 |
| | | const actionText = nextStatus === 1 ? '启用' : '停用' |
| | | dialog.warning({ |
| | | title: `是否确认${actionText}该安全员?`, |
| | | content: ``, |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | maskClosable: false, |
| | | autoFocus: false, |
| | | onPositiveClick: async () => { |
| | | try { |
| | | const res = await updateSafetyOfficerStatus({ |
| | | id: row.id, |
| | | status: nextStatus as 0 | 1, |
| | | }) |
| | | if (res.code === 200 || res.code === 0) { |
| | | window.$message?.success(`${actionText}成功`) |
| | | getList() |
| | | } |
| | | } |
| | | catch (error) { |
| | | console.error(`${actionText}安全员失败:`, error) |
| | | } |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | const filterItems = computed(() => [ |
| | | { |
| | | label: 'ID', |
| | | path: 'id', |
| | | render: () => <n-input v-model:value={searchModel.value.id} placeholder="请输入" clearable />, |
| | | }, |
| | | { |
| | | label: '安全员姓名', |
| | | path: 'name', |
| | | render: () => <n-input v-model:value={searchModel.value.name} placeholder="请输入" clearable />, |
| | | }, |
| | | { |
| | | label: '身份证号', |
| | | path: 'idCardNo', |
| | | render: () => <n-input v-model:value={searchModel.value.idCardNo} placeholder="请输入" clearable />, |
| | | }, |
| | | { |
| | | label: '联系电话', |
| | | path: 'phoneNumber', |
| | | render: () => <n-input v-model:value={searchModel.value.phoneNumber} placeholder="请输入" clearable />, |
| | | }, |
| | | { |
| | | label: '机动车驾驶证号', |
| | | path: 'drivingLicenseNo', |
| | | render: () => <n-input v-model:value={searchModel.value.drivingLicenseNo} placeholder="请输入" clearable />, |
| | | }, |
| | | { |
| | | label: '创建时间', |
| | | path: 'createTimeRange', |
| | | render: () => ( |
| | | <n-date-picker |
| | | v-model:value={searchModel.value.createTimeRange} |
| | | type="daterange" |
| | | clearable |
| | | format="yyyy/MM/dd" |
| | | placeholder="开始时间 - 结束时间" |
| | | style={{ width: '320px' }} |
| | | actions={null} |
| | | /> |
| | | ), |
| | | }, |
| | | { |
| | | label: '状态', |
| | | path: 'status', |
| | | render: () => ( |
| | | <n-select |
| | | v-model:value={searchModel.value.status} |
| | | options={statusOptions} |
| | | placeholder="请选择" |
| | | clearable |
| | | onUpdate:show={(val: boolean) => isStatusSelectOpen.value = val} |
| | | > |
| | | {{ |
| | | arrow: () => ( |
| | | <img |
| | | src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png" |
| | | class={`arrow-icon ${isStatusSelectOpen.value ? 'arrow-icon--open' : ''}`} |
| | | style={{ width: '16px', height: '16px' }} |
| | | alt="arrow" |
| | | /> |
| | | ), |
| | | empty: () => ( |
| | | <div class="flex-center"> |
| | | <img src="/src/assets/element-img/no-data.png" style={{ width: '50%' }} alt="no-data" /> |
| | | </div> |
| | | ), |
| | | }} |
| | | </n-select> |
| | | ), |
| | | }, |
| | | ]) |
| | | |
| | | const columns = computed<DataTableColumns<SafetyOfficerItem>>(() => [ |
| | | { type: 'selection', width: 50, fixed: 'left' }, |
| | | { title: 'ID', key: 'id', width: 80, resizable: true, align: 'left' }, |
| | | { title: '安全员姓名', key: 'name', width: 140, resizable: true, align: 'left' }, |
| | | { title: '身份证号', key: 'idCardNo', width: 160, resizable: true, align: 'left' }, |
| | | { title: '联系电话', key: 'phoneNumber', width: 140, resizable: true, align: 'left' }, |
| | | { |
| | | title: '性别', |
| | | key: 'gender', |
| | | width: 80, |
| | | resizable: true, |
| | | align: 'left', |
| | | render: row => row.gender === 1 ? '男' : row.gender === 2 ? '女' : '-', |
| | | }, |
| | | { |
| | | title: '出生日期', |
| | | key: 'birthDate', |
| | | width: 120, |
| | | resizable: true, |
| | | align: 'left', |
| | | render: row => row.birthDate ? dayjs(row.birthDate).format('YYYY/MM/DD') : '-', |
| | | }, |
| | | { title: '机动车驾驶证号', key: 'drivingLicenseNo', width: 180, resizable: true, align: 'left' }, |
| | | { title: '创建人', key: 'createBy', width: 120, resizable: true, align: 'left' }, |
| | | { |
| | | title: '创建时间', |
| | | key: 'createTime', |
| | | width: 160, |
| | | resizable: true, |
| | | align: 'left', |
| | | render: row => row.createTime ? dayjs(row.createTime).format('YYYY/MM/DD HH:mm') : '-', |
| | | }, |
| | | { |
| | | title: '状态', |
| | | key: 'deleted', |
| | | width: 100, |
| | | resizable: true, |
| | | align: 'left', |
| | | render: row => ( |
| | | <NTag bordered={false} type={row.deleted ? 'default' : 'success'}> |
| | | {row.deleted ? '停用' : '启用'} |
| | | </NTag> |
| | | ), |
| | | }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 140, |
| | | fixed: 'right', |
| | | align: 'left', |
| | | render: row => ( |
| | | <n-space size={12}> |
| | | <NButton text v-permission="70122" type="primary" onClick={() => handleEdit(row)}> |
| | | 编辑 |
| | | </NButton> |
| | | <NButton text v-permission="70123" type="primary" onClick={() => handleToggleStatus(row)}> |
| | | {row.deleted ? '启用' : '停用'} |
| | | </NButton> |
| | | </n-space> |
| | | ), |
| | | }, |
| | | ]) |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | |
| | | onActivated(() => { |
| | | if (window.sessionStorage.getItem('safetyOfficerInfoNeedRefresh') === '1') { |
| | | window.sessionStorage.removeItem('safetyOfficerInfoNeedRefresh') |
| | | getList() |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="roboTaxi w-full h-full"> |
| | | <div class="roboTaxi-form-card mb-12px"> |
| | | <n-form :model="searchModel" label-placement="left" inline :show-feedback="false"> |
| | | <SearchPanel :default-collapsed="false"> |
| | | <n-gi v-for="item in filterItems" :key="item.path" class="n-grid-item"> |
| | | <n-form-item :label="item.label" :path="item.path"> |
| | | <component :is="item.render" /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | <template #action> |
| | | <NButton @click="handleReset"> |
| | | 重置 |
| | | </NButton> |
| | | <NButton type="primary" @click="handleSearch"> |
| | | 查询 |
| | | </NButton> |
| | | </template> |
| | | </SearchPanel> |
| | | </n-form> |
| | | </div> |
| | | |
| | | <n-card |
| | | class="flex-1 robotaxi-table-card" |
| | | content-style="display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0;" |
| | | > |
| | | <template #header> |
| | | <n-flex justify="space-between" align="center"> |
| | | <n-flex align="center"> |
| | | <span v-if="checkedRowKeys.length > 0" style="color: rgba(23, 31, 38, 0.6); font-weight: 400;"> |
| | | 已选择{{ checkedRowKeys.length }}条记录 |
| | | </span> |
| | | <NButton v-permission="70125" :disabled="checkedRowKeys.length === 0" @click="handleBatchExport"> |
| | | 批量导出 |
| | | </NButton> |
| | | <NButton v-permission="70124" @click="handleBatchImport"> |
| | | 批量导入 |
| | | </NButton> |
| | | <NButton v-permission="70124" @click="handleDownloadTemplate"> |
| | | 下载导入模板 |
| | | </NButton> |
| | | <NButton v-permission="70126" @click="handleOpenOperationLog"> |
| | | 操作日志 |
| | | </NButton> |
| | | </n-flex> |
| | | |
| | | <NButton v-permission="70121" type="primary" secondary @click="handleAdd"> |
| | | 新增 |
| | | </NButton> |
| | | </n-flex> |
| | | </template> |
| | | |
| | | <div class="flex-1 flex flex-col h-full" style="min-width: 0; overflow: hidden;"> |
| | | <n-data-table |
| | | class="flex-1" |
| | | :columns="columns" |
| | | :data="listData" |
| | | :loading="loading" |
| | | :bordered="false" |
| | | :row-key="(row: SafetyOfficerItem) => row.id" |
| | | flex-height |
| | | :scroll-x="1560" |
| | | :header-height="18" |
| | | :row-height="18" |
| | | @update:checked-row-keys="handleCheck" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img |
| | | src="/src/assets/element-img/no-data.png" |
| | | style="width: 123px; height: 125px" |
| | | @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }" |
| | | > |
| | | </div> |
| | | </template> |
| | | </n-data-table> |
| | | <Pagination |
| | | :count="total" |
| | | :page="pageParams.pageNum" |
| | | :page-size="pageParams.pageSize" |
| | | class="robotaxi-pagination mt-12px" |
| | | @change="changePage" |
| | | /> |
| | | </div> |
| | | </n-card> |
| | | |
| | | <OperationLogModal v-model:show="showOperationLogModal" :business-type="57" /> |
| | | <input |
| | | ref="fileInputRef" |
| | | type="file" |
| | | style="display: none" |
| | | accept=".xlsx, .xls" |
| | | @change="handleFileChange" |
| | | > |
| | | </div> |
| | | </template> |
| | |
| | | <TableModal ref="modalRef" modal-name="订单改派" @refresh="handleSearch" /> |
| | | <CloseOrderModal ref="closeOrderModalRef" modal-name="关闭订单" @refresh="handleSearch" /> |
| | | <Modal ref="modalRef1" modal-name="改价" @refresh="handleSearch" /> |
| | | <OperationLogModal v-model:show="showOperationLogModal" /> |
| | | </div> |
| | | </template> |
| | |
| | | ] |
| | | |
| | | const statusOptions = [ |
| | | { label: '已绑定', value: 1 }, |
| | | { label: '未绑定', value: 0 }, |
| | | { label: '正常', value: 1 }, |
| | | { label: '冻结', value: 0 }, |
| | | ] |
| | | |
| | | // 下拉框展开状态 |
| | |
| | | <div class="mb-4 flex items-end gap-4" style="flex-shrink: 0;"> |
| | | <div class="w-306px"> |
| | | <div class="mb-8px text-sm"> |
| | | 绑定状态 |
| | | 状态 |
| | | </div> |
| | | <n-select |
| | | v-model:value="searchModel.status" :options="statusOptions" placeholder="请选择" clearable |
| | |
| | | ] |
| | | |
| | | const statusOptions = [ |
| | | { label: '已绑定', value: 1 }, |
| | | { label: '未绑定', value: 0 }, |
| | | { label: '正常', value: 1 }, |
| | | { label: '冻结', value: 0 }, |
| | | ] |
| | | |
| | | // 下拉框展开状态 |