Merge branch 'main' of http://120.76.84.145:10101/gitblit/r/H5/xp-robotaxi-web
| | |
| | | }, |
| | | "route": { |
| | | "appRoot": "Home", |
| | | "cardList": "Card list", |
| | | "draggableList": "Draggable list", |
| | | "transaction": "Transaction", |
| | | "dashboard": "Dashboard", |
| | | "system": "System", |
| | | "accountPermission": "Account Permission", |
| | | "accountManagement": "Account Management", |
| | | "permissionGroup": "Permission Group Setting", |
| | | "permissionGroupForm": "Permission Group Detail", |
| | | "roleManagement": "Role Management", |
| | | "roleDetail": "Role Detail", |
| | | "appVersion": "App Version", |
| | | "hotfix": "Hot Fix", |
| | | "appVersionManage": "App Version Management", |
| | | "systemConfig": "System Config", |
| | | "userContent": "User & Content", |
| | | "whitelistManagement": "Whitelist", |
| | | "invitationCode": "Invitation Code Management", |
| | | "whitelist": "Whitelist Management", |
| | | "whitelistLog": "Whitelist Log", |
| | | "contentCenter": "Content Center", |
| | | "contentCenterIndex": "Content Center", |
| | | "contentCenterActive": "Activity", |
| | | "contentCenterConpon": "Coupon Management", |
| | | "contentCenterConponAdd": "Add Coupon", |
| | | "bannerManagement": "Banner Management", |
| | | "businessOperations": "Operation & City", |
| | | "openCity": "Open City Management", |
| | | "geofenceManagement": "Geofence Management", |
| | | "geofenceDetail": "Geofence Detail", |
| | | "addFence": "Add Geofence", |
| | | "editFence": "Edit Geofence", |
| | | "pointManagement": "Point Management", |
| | | "cruise": "Cruise", |
| | | "patrolPoints": "Special Point Management", |
| | | "patrolPlan": "Cruise Plan", |
| | | "patrolPlanAdd": "Add Cruise Plan", |
| | | "patrolPlanDetail": "Cruise Plan Detail", |
| | | "assetManagement": "Asset", |
| | | "vehicleManagement": "Vehicle", |
| | | "inventoryVehicle": "Inventory Vehicle Management", |
| | | "addInventoryVehicle": "Add Vehicle", |
| | | "operatingVehicle": "Operating Vehicle Management", |
| | | "operatingVehicleDetail": "Operating Vehicle Detail", |
| | | "fleetManagement": "Fleet Management", |
| | | "fleetManagementDetail": "Fleet Detail", |
| | | "ruleConfig": "Operation Rule Configuration", |
| | | "transaction": "Transaction & Pricing", |
| | | "order": "Order", |
| | | "evaluationRecord": "Evaluation record", |
| | | "transactionStrategy": "Transaction strategy", |
| | | "navigationAndRoutePlanning": "Navigation and route planning" |
| | | "orderManagement": "Order Management", |
| | | "orderDetail": "Order Detail", |
| | | "evaluationRecord": "Evaluation Record", |
| | | "pricingManagement": "Pricing Management", |
| | | "setPrice": "Set Price", |
| | | "pricingDetail": "Pricing Detail", |
| | | "rules": "Billing Rules", |
| | | "transactionStrategy": "Transaction Strategy", |
| | | "navigationAndRoutePlanning": "Navigation And Route Planning", |
| | | "operations": "Operation Execution", |
| | | "personnel": "Personnel", |
| | | "operationsPersonnel": "Operation Personnel Management", |
| | | "operationsPersonnelDetail": "Operation Personnel Detail", |
| | | "operationsAttendance": "Schedule Attendance", |
| | | "operationsAttendanceAddress": "Clock-in Address Management", |
| | | "operationsAttendanceStatistics": "Clock-in Statistics", |
| | | "operationsAttendanceStatisticsDetail": "Clock-in Details", |
| | | "operationsAttendanceShift": "Shift Management", |
| | | "operationsAttendancePlan": "Schedule Plan Management", |
| | | "operationsAttendancePlanEdit": "Edit Schedule", |
| | | "operationsAttendancePlanCopy": "Copy Schedule", |
| | | "taskOrder": "Task Order", |
| | | "taskOrderCenter": "Task Order Center", |
| | | "operationsMessage": "Message", |
| | | "service": "Customer Service", |
| | | "serviceReview": "Customer Service Review", |
| | | "serviceReviewPersonnel": "Customer Service Personnel", |
| | | "serviceReviewPolicyApplication": "Policy Application" |
| | | }, |
| | | "transaction": { |
| | | "export": "Export order", |
| | |
| | | "route": { |
| | | "appRoot": "首页", |
| | | "dashboard": "首页", |
| | | "workbench": "首页" |
| | | "system": "系统", |
| | | "accountPermission": "账户权限", |
| | | "accountManagement": "账号管理", |
| | | "permissionGroup": "权限组设置", |
| | | "permissionGroupForm": "权限组详情", |
| | | "roleManagement": "角色管理", |
| | | "roleDetail": "角色详情", |
| | | "appVersion": "APP版本", |
| | | "hotfix": "热修复", |
| | | "appVersionManage": "APP版本管理", |
| | | "systemConfig": "系统配置", |
| | | "userContent": "用户&内容", |
| | | "whitelistManagement": "白名单", |
| | | "invitationCode": "邀请码管理", |
| | | "whitelist": "白名单管理", |
| | | "whitelistLog": "白名单日志", |
| | | "contentCenter": "内容中心", |
| | | "contentCenterIndex": "内容中心", |
| | | "contentCenterActive": "活动", |
| | | "contentCenterConpon": "优惠券管理", |
| | | "contentCenterConponAdd": "添加优惠券", |
| | | "bannerManagement": "Banner管理", |
| | | "businessOperations": "运营&城市", |
| | | "openCity": "开通城市管理", |
| | | "geofenceManagement": "围栏管理", |
| | | "geofenceDetail": "围栏详情", |
| | | "addFence": "新增围栏", |
| | | "editFence": "编辑围栏", |
| | | "pointManagement": "点位管理", |
| | | "cruise": "巡游", |
| | | "patrolPoints": "特殊点位管理", |
| | | "patrolPlan": "巡游计划", |
| | | "patrolPlanAdd": "新增巡游计划", |
| | | "patrolPlanDetail": "巡游计划详情", |
| | | "assetManagement": "资产", |
| | | "vehicleManagement": "车辆", |
| | | "inventoryVehicle": "在库车辆管理", |
| | | "addInventoryVehicle": "新增车辆", |
| | | "operatingVehicle": "运营车辆管理", |
| | | "operatingVehicleDetail": "运营车辆详情", |
| | | "fleetManagement": "车队管理", |
| | | "fleetManagementDetail": "车队详情", |
| | | "ruleConfig": "运维规则配置", |
| | | "transaction": "交易&定价", |
| | | "order": "订单", |
| | | "orderManagement": "订单管理", |
| | | "orderDetail": "订单详情", |
| | | "evaluationRecord": "评价记录", |
| | | "pricingManagement": "定价管理", |
| | | "setPrice": "设置价格", |
| | | "pricingDetail": "定价详情", |
| | | "rules": "计费规则", |
| | | "transactionStrategy": "交易策略", |
| | | "navigationAndRoutePlanning": "路线规划", |
| | | "operations": "运维执行", |
| | | "personnel": "人员", |
| | | "operationsPersonnel": "运维人员管理", |
| | | "operationsPersonnelDetail": "运维人员详情", |
| | | "operationsAttendance": "排班考勤", |
| | | "operationsAttendanceAddress": "打卡地址管理", |
| | | "operationsAttendanceStatistics": "打卡统计", |
| | | "operationsAttendanceStatisticsDetail": "打卡明细", |
| | | "operationsAttendanceShift": "班次管理", |
| | | "operationsAttendancePlan": "排班计划管理", |
| | | "operationsAttendancePlanEdit": "编辑排班", |
| | | "operationsAttendancePlanCopy": "复制排班", |
| | | "taskOrder": "工单", |
| | | "taskOrderCenter": "工单中心", |
| | | "operationsMessage": "消息", |
| | | "service": "客服", |
| | | "serviceReview": "客服审核", |
| | | "serviceReviewPersonnel": "客服人员", |
| | | "serviceReviewPolicyApplication": "政策申请" |
| | | }, |
| | | "transaction": { |
| | | "export": "导出订单", |
| | |
| | | |
| | | if (isArray(permission)) { |
| | | // 角色为数组, 判断是否有交集 或 权限是否有交集 |
| | | return permission.some(i => |
| | | (role && role.includes(i as Entity.RoleType)) || |
| | | return permission.some(i => |
| | | (role && role.includes(i as Entity.RoleType)) || |
| | | (permissions && permissions.some(p => String(p) === String(i))) |
| | | ) |
| | | } |
| | | |
| | | if (isString(permission) || isNumber(permission)) { |
| | | // 角色为字符串/数字, 判断是否包含 或 权限是否包含 |
| | | return (role && role.includes(permission as Entity.RoleType)) || |
| | | return (role && role.includes(permission as Entity.RoleType)) || |
| | | (permissions && permissions.some(p => String(p) === String(permission))) |
| | | } |
| | | |
| | | |
| | | return false |
| | | } |
| | | |
| | |
| | | pid: 702, |
| | | hide: true, |
| | | activeMenu: '/operations/attendance/plan', |
| | | },{ |
| | | }, |
| | | { |
| | | name: 'operationsAttendancePlanCopy', |
| | | path: '/operations/attendance/plan/copy', |
| | | title: '复制排班', |
| | |
| | | pid: 702, |
| | | hide: true, |
| | | activeMenu: '/operations/attendance/plan', |
| | | }, |
| | | { |
| | | name: 'taskOrder', |
| | | path: '/operations/taskOrder', |
| | | title: '任务工单', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'dir', |
| | | componentPath: null, |
| | | id: 705, |
| | | pid: 7, |
| | | }, |
| | | { |
| | | name: 'taskOrderCenter', |
| | | path: '/operations/taskOrderCenter', |
| | | title: '工单中心', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/operations/taskOrder/taskOrderCenter/index.vue', |
| | | id: 7051, |
| | | pid: 705, |
| | | }, |
| | | { |
| | | name: 'operationsMessage', |
| | |
| | | pid: 8, |
| | | }, |
| | | { |
| | | name: 'serviceTicket', |
| | | path: '/service/ticket', |
| | | title: '客服工单', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:message', |
| | | menuType: 'dir', |
| | | componentPath: null, |
| | | id: 802, |
| | | pid: 8, |
| | | }, |
| | | { |
| | | name: 'serviceReviewPersonnel', |
| | | path: '/service/review/personnel', |
| | | title: '人员资质审核', |
| | |
| | | id: 8012, |
| | | pid: 801, |
| | | }, |
| | | { |
| | | name: 'serviceTicketFeedback', |
| | | path: '/service/ticket/feedback', |
| | | title: '意见反馈', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:message', |
| | | menuType: 'page', |
| | | componentPath: '/service/ticket/feedback/index.vue', |
| | | id: 8021, |
| | | pid: 802, |
| | | }, |
| | | { |
| | | name: 'serviceTicketFeedbackType', |
| | | path: '/service/ticket/feedbackType', |
| | | title: '反馈类型', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:message', |
| | | menuType: 'page', |
| | | componentPath: '/service/ticket/feedbackType/index.vue', |
| | | id: 8022, |
| | | pid: 802, |
| | | }, |
| | | { |
| | | name: 'serviceTicketWorkOrder', |
| | | path: '/service/ticket/workOrder', |
| | | title: '工单', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:message', |
| | | menuType: 'page', |
| | | componentPath: '/service/ticket/workOrder/index.vue', |
| | | id: 8023, |
| | | pid: 802, |
| | | }, |
| | | { |
| | | name: 'serviceTicketWorkOrderDetail', |
| | | path: '/service/ticket/workOrder/detail', |
| | | title: '工单详情', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:message', |
| | | menuType: 'page', |
| | | componentPath: '/service/ticket/workOrder/detail.vue', |
| | | id: 80231, |
| | | pid: 802, |
| | | hide: true, |
| | | activeMenu: '/service/ticket/workOrder', |
| | | }, |
| | | |
| | | ] |
| | |
| | | export function deleteShift(id: any) { |
| | | return request.Delete<Service.ResponseResult<any>>('/maintenance/xp-shift/api/v1/shift/deleteById?id=' + id) |
| | | } |
| | | |
| | | |
| | | /** 消息通知管理 */ |
| | | |
| | | |
| | | // 获取消息通知列表 |
| | | export function getNoticeList() { |
| | | return request.Get<Service.ResponseResult<any>>('/operation/api/v1/notification/list') |
| | | } |
| | | |
| | | // 修改消息通知配置 |
| | | export function updateNoticeConfig(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/operation/api/v1/notification/update', params) |
| | | } |
| | | |
| | | |
| | | /** 打卡统计 */ |
| | | |
| | | |
| | | // 获取打卡统计列表 |
| | | export function getClockInStatisticsList(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/clock/getClockInStatistics', params) |
| | | } |
| | | |
| | | // 获取打卡统计详情列表 |
| | | export function getClockInStatisticsDetailList(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/clock/getClockInStatisticsDetails', params) |
| | | } |
| | | |
| | | // 导出打卡统计 |
| | | export function exportClockInStatistics(params: any) { |
| | | // return request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/clock/exportClockInStatistics', params) |
| | | const method = request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/clock/exportClockInStatistics', params, { responseType: 'blob' }) |
| | | method.meta = { ...method.meta, isBlob: true } |
| | | return method |
| | | } |
| | | |
| | | // 导出打卡统计详情 |
| | | export function exportClockInStatisticsDetails(params: any) { |
| | | // return request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/clock/exportClockInStatisticsDetails', params) |
| | | const method = request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/clock/exportClockInStatisticsDetails', params, { responseType: 'blob' }) |
| | | method.meta = { ...method.meta, isBlob: true } |
| | | return method |
| | | } |
| | | |
| | | |
| | | /** 运维人员管理 */ |
| | | |
| | | // 获取运维人员列表 |
| | | export function getOperationsUserList(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/account/api/v1/operationsUser/pageList', params) |
| | | } |
| | | |
| | | // 获取运维人员不分页列表 |
| | | export function getOperationsUserListNoPage() { |
| | | return request.Post<Service.ResponseResult<any>>('/account/api/v1/operationsUser/listAll') |
| | | } |
| | | |
| | | // 获取运维人员详情 |
| | | export function getOperationsUserDetail(id: any) { |
| | | return request.Get<Service.ResponseResult<any>>('/account/api/v1/operationsUser/detailById?id=' + id) |
| | | } |
| | | |
| | | // 新增运维人员 |
| | | export function addOperationsUser(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/account/api/v1/operationsUser/add', params) |
| | | } |
| | | |
| | | // 编辑运维人员 |
| | | export function editOperationsUser(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/account/api/v1/operationsUser/edit', params) |
| | | } |
| | | |
| | | // 删除运维人员 |
| | | export function deleteOperationsUser(id: any) { |
| | | return request.Delete<Service.ResponseResult<any>>('/account/api/v1/operationsUser/deleteById?id=' + id) |
| | | } |
| | | |
| | | // 启用禁用运维人员 |
| | | export function updateOperationsUserStatus(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/account/api/v1/operationsUser/enableDisableById?', params) |
| | | } |
| | | |
| | | // 重置运维人员密码 |
| | | export function resetOperationsUserPassword(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/account/api/v1/operationsUser/rePassword', params) |
| | | } |
| | | |
| | | // 运维人员导出 |
| | | export function exportOperationsUser(params: any) { |
| | | const method = request.Get<Service.ResponseResult<any>>('/account/api/v1/operationsUser/exportOperationsUser', params, { responseType: 'blob' }) |
| | | method.meta = { ...method.meta, isBlob: true } |
| | | return method |
| | | } |
| | | |
| | | // 运维人员导出模板 |
| | | export function exportOperationsUserTemplate() { |
| | | const method = request.Get<Service.ResponseResult<any>>('/account/api/v1/operationsUser/exportTemplate', {}, { responseType: 'blob' }) |
| | | method.meta = { ...method.meta, isBlob: true } |
| | | return method |
| | | } |
| | | |
| | | // 运维人员导入 |
| | | export function importOperationsUser(file: File) { |
| | | const formData = new FormData() |
| | | formData.append('file', file) |
| | | return request.Post<Service.ResponseResult<any>>('/account/api/v1/operationsUser/importOperationsUser', formData) |
| | | } |
| | | |
| | | |
| | | /** 任务工单 */ |
| | | |
| | | // 任务中心列表 |
| | | export function getTaskOrderCenterList(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/taskManagement/pageList', params) |
| | | } |
| | | |
| | | // 任务工单导出 |
| | | export function exportTaskOrderCenter(params: any) { |
| | | const method = request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/taskManagement/exportExcel', params, { responseType: 'blob' }) |
| | | method.meta = { ...method.meta, isBlob: true } |
| | | return method |
| | | } |
| | | |
| | | // 创建任务事故 |
| | | export function createTaskAccident(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/taskManagement/saveTask', params) |
| | | } |
| | | |
| | | // 获取任务详情 |
| | | export function getTaskOrderDetail(id: any) { |
| | | return request.Get<Service.ResponseResult<any>>('/maintenance/api/v1/taskManagement/taskDetail?id=' + id) |
| | | } |
| | | |
| | | // 任务分配 |
| | | export function assignTaskOrder(params: any) { |
| | | return request.Get<Service.ResponseResult<any>>('/maintenance/api/v1/taskManagement/assigneeTask?id=' + params.id + '&handlerUserId=' + params.handlerUserId) |
| | | } |
| | | |
| | | // 删除任务 |
| | | export function deleteTaskOrder(id: any) { |
| | | return request.Delete<Service.ResponseResult<any>>('/maintenance/api/v1/taskManagement/deleteTaskById?id=' + id) |
| | | } |
| | | |
| | | // 事故类型列表 |
| | | export function getTaskAccidentTypeList() { |
| | | return request.Get<Service.ResponseResult<any>>('/maintenance/api/v1/taskManagement/errorTypeList') |
| | | } |
| | | |
| | | // 查询车辆不分页列表 |
| | | export function getVehicleListNoPage() { |
| | | return request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/taskManagement/listVehicle') |
| | | } |
| | |
| | | export function deletePolicyApplicationById(id: number | string) { |
| | | return request.Delete<Service.ResponseResult<any>>(`/vehicleConvoy/api/v1/policyApplication/deleteById/${id}`) |
| | | } |
| | | |
| | | export function fetchFeedbackPageList(data: { |
| | | content?: string |
| | | pageNum: number |
| | | pageSize: number |
| | | }) { |
| | | return request.Post<Service.ResponseResult<any>>('/order/api/v1/orderEvaluateManagement/pageList', data) |
| | | } |
| | | |
| | | export function fetchFeedbackTypeList(data: { |
| | | pageNum: number |
| | | pageSize: number |
| | | }) { |
| | | return request.Get<Service.ResponseResult<any>>('/operation/api/v1/feedbackType/getList', data) |
| | | } |
| | | |
| | | export function saveFeedbackType(data: { feedbackType: string }) { |
| | | return request.Post<Service.ResponseResult<any>>('/operation/api/v1/feedbackType/saveFeedbackType', data) |
| | | } |
| | | |
| | | export function updateFeedbackType(data: { id: number | string; feedbackType: string }) { |
| | | return request.Post<Service.ResponseResult<any>>('/operation/api/v1/feedbackType/updateFeedbackType', data) |
| | | } |
| | | |
| | | export function deleteFeedbackType(id: number | string) { |
| | | return request.Delete<Service.ResponseResult<any>>(`/operation/api/v1/feedbackType/deleteFeedbackType?id=${id}`) |
| | | } |
| | | |
| | | export function fetchWorkOrderPageList(data: { |
| | | feedbackTypeId?: number | string |
| | | feedbackUserPhone?: string |
| | | handleStatus?: number | null |
| | | pageNum: number |
| | | pageSize: number |
| | | priority?: number | null |
| | | problemSource?: number | null |
| | | }) { |
| | | return request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/workOrder/pageList', data) |
| | | } |
| | | |
| | | export function fetchOperationsUserListAll() { |
| | | return request.Get<Service.ResponseResult<any>>('/account/api/v1/sys-user/listAll', {}) |
| | | } |
| | | |
| | | export function saveWorkOrder(data: { |
| | | feedbackTypeId?: number | string |
| | | feedbackUserPhone?: string |
| | | handleStatus?: number | null |
| | | priority?: number | null |
| | | problemSource?: number | null |
| | | problem?: string |
| | | problemDescription?: string |
| | | problemImage?: string |
| | | handleUserId?: number | string |
| | | handleUserName?: string |
| | | }) { |
| | | return request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/workOrder/saveWorkOrder', data) |
| | | } |
| | | |
| | | export function updateWorkOrder(data: { |
| | | id: number | string |
| | | feedbackTypeId?: number | string |
| | | feedbackUserPhone?: string |
| | | handleStatus?: number | null |
| | | priority?: number | null |
| | | problemSource?: number | null |
| | | problem?: string |
| | | problemDescription?: string |
| | | handleUserId?: number | string |
| | | problemImage?: string |
| | | handleUserName?: string |
| | | }) { |
| | | return request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/workOrder/updateWorkOrder', data) |
| | | } |
| | | |
| | | export function deleteWorkOrder(id: number | string) { |
| | | return request.Delete<Service.ResponseResult<any>>(`/maintenance/api/v1/workOrder/deleteWorkOrder?id=${id}`) |
| | | } |
| | | |
| | | export function fetchWorkOrderDetail(id: number | string) { |
| | | return request.Get<Service.ResponseResult<any>>(`/maintenance/api/v1/workOrder/workOrderDetail?id=${id}`) |
| | | } |
| | | |
| | | export function handleWorkOrder(data: { |
| | | handleFile?: string |
| | | handleLog: string |
| | | handleType: number |
| | | handleUserId?: number | string |
| | | handleUserName?: string |
| | | workOrderId: number | string |
| | | }) { |
| | | return request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/workOrder/handleWorkOrder', data) |
| | | } |
| | |
| | | font-size: 13px; |
| | | } |
| | | } |
| | | |
| | | .n-modal { |
| | | max-height: 80vh; |
| | | |
| | | .n-card-header { |
| | | padding: 16px 24px; |
| | | .n-card-header__main { |
| | | font-size: 16px; |
| | | line-height: 24px; |
| | | font-weight: 600; |
| | | } |
| | | } |
| | | |
| | | .n-card__content { |
| | | overflow-y: auto; |
| | | scrollbar-width: none; |
| | | padding: 16px 24px !important; |
| | | |
| | | &::-webkit-scrollbar { |
| | | display: none; |
| | | } |
| | | } |
| | | |
| | | .n-card__action { |
| | | padding: 16px 24px; |
| | | background-color: #FFF; |
| | | box-shadow: 0px -2px 5px 0px rgba(0, 0, 0, 0.1); |
| | | |
| | | button { |
| | | width: 84px; |
| | | } |
| | | } |
| | | } |
| | |
| | | <script setup lang="tsx"> |
| | | import { ref, computed } from 'vue' |
| | | import { useRouter, useRoute } from 'vue-router' |
| | | import { useBoolean } from '@/hooks' |
| | | import { useRoute } from 'vue-router' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NTag } from 'naive-ui' |
| | | import { getClockInStatisticsDetailList, exportClockInStatisticsDetails } from '@/service/api/attendance' |
| | | import { downloadFile } from '@/utils' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | |
| | | const router = useRouter() |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | |
| | | const route = useRoute() |
| | | |
| | | interface AttendanceDetail { |
| | | id: number |
| | | name: string |
| | | role: string |
| | | fleet: string |
| | | date: string |
| | | shift: string |
| | | checkInTime: string |
| | | checkInResult: string |
| | | checkInAddress: string |
| | | } |
| | | |
| | | const searchModel = ref({ |
| | | name: '王美丽', |
| | | role: '', |
| | | dateRange: null as [number, number] | null |
| | | appUserId: null as any, |
| | | statisticDate: null as string[] | null |
| | | }) |
| | | const personnelOptions = [ |
| | | { label: '王美丽', value: '王美丽' }, |
| | | { label: '张三', value: '张三' }, |
| | | { label: '李四', value: '李四' } |
| | | ] |
| | | |
| | | const roleOptions = [ |
| | | { label: '网格员', value: '网格员' }, |
| | | { label: '运维管理', value: '运维管理' }, |
| | | { label: '角色1', value: '角色1' } |
| | | ] |
| | | |
| | | // 统计数据 |
| | | const statistics = ref({ |
| | |
| | | absentDays: 0 |
| | | }) |
| | | |
| | | const detailList = ref<AttendanceDetail[]>([ |
| | | { id: 1, name: '王美丽', role: '网格员', fleet: '网格员一队', date: '2025-05-17', shift: '早 (08:00-18:00)', checkInTime: '2025-05-17 07:59', checkInResult: '正常', checkInAddress: '成都市武侯区石羊街道瑞西二街' }, |
| | | { id: 2, name: '王美丽', role: '网格员', fleet: '网格员一队', date: '2025-05-17', shift: '早 (08:00-18:00)', checkInTime: '2025-05-17 07:59', checkInResult: '正常', checkInAddress: '成都市武侯区石羊街道瑞西二街' }, |
| | | { id: 3, name: '王美丽', role: '网格员', fleet: '网格员一队', date: '2025-05-17', shift: '早 (08:00-18:00)', checkInTime: '2025-05-17 07:59', checkInResult: '迟到', checkInAddress: '成都市武侯区石羊街道瑞西二街' }, |
| | | { id: 4, name: '王美丽', role: '网格员', fleet: '网格员一队', date: '2025-05-17', shift: '早 (08:00-18:00)', checkInTime: '2025-05-17 07:59', checkInResult: '正常', checkInAddress: '成都市武侯区石羊街道瑞西二街' }, |
| | | { id: 5, name: '王美丽', role: '网格员', fleet: '网格员一队', date: '2025-05-17', shift: '早 (08:00-18:00)', checkInTime: '', checkInResult: '缺卡', checkInAddress: '' }, |
| | | { id: 6, name: '王美丽', role: '网格员', fleet: '网格员一队', date: '2025-05-17', shift: '早 (08:00-18:00)', checkInTime: '2025-05-17 07:59', checkInResult: '正常', checkInAddress: '成都市武侯区石羊街道瑞西二街' }, |
| | | { id: 7, name: '王美丽', role: '网格员', fleet: '网格员一队', date: '2025-05-17', shift: '早 (08:00-18:00)', checkInTime: '2025-05-17 07:59', checkInResult: '迟到', checkInAddress: '成都市武侯区石羊街道瑞西二街' }, |
| | | { id: 8, name: '王美丽', role: '网格员', fleet: '网格员一队', date: '2025-05-17', shift: '早 (08:00-18:00)', checkInTime: '2025-05-17 07:59', checkInResult: '迟到', checkInAddress: '成都市武侯区石羊街道瑞西二街' }, |
| | | { id: 9, name: '王美丽', role: '网格员', fleet: '网格员一队', date: '2025-05-17', shift: '早 (08:00-18:00)', checkInTime: '2025-05-17 07:59', checkInResult: '正常', checkInAddress: '成都市武侯区石羊街道瑞西二街' }, |
| | | { id: 10, name: '王美丽', role: '网格员', fleet: '网格员一队', date: '2025-05-17', shift: '早 (08:00-18:00)', checkInTime: '2025-05-17 07:59', checkInResult: '正常', checkInAddress: '成都市武侯区石羊街道瑞西二街' } |
| | | ]) |
| | | const detailList = ref<any[]>([]) |
| | | |
| | | const currentPage = ref(1) |
| | | const pageSize = ref(10) |
| | | const total = ref(800) |
| | | |
| | | const handleSearch = () => { |
| | | console.log('搜索', searchModel.value) |
| | | } |
| | | const total = ref(0) |
| | | const pageParams = ref({ |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | }) |
| | | |
| | | const handleReset = () => { |
| | | searchModel.value = { |
| | | name: '王美丽', |
| | | role: '', |
| | | dateRange: null |
| | | appUserId: null, |
| | | statisticDate: null |
| | | } |
| | | } |
| | | |
| | | const handleExport = () => { |
| | | console.log('导出') |
| | | } |
| | | |
| | | const handleBack = () => { |
| | | router.back() |
| | | const handleExport = async () => { |
| | | const params = { |
| | | ...pageParams.value, |
| | | statisticDate: searchModel.value.statisticDate?.join('-'), |
| | | } |
| | | const response = await exportClockInStatisticsDetails(params) |
| | | if (response instanceof Blob) { |
| | | downloadFile(response, `打卡统计列表_${new Date().getTime()}.xlsx`) |
| | | window.$message.success('导出成功') |
| | | } |
| | | } |
| | | |
| | | const filterItems = computed(() => [ |
| | | { |
| | | label: '人员', |
| | | path: 'name', |
| | | render: () => <n-select v-model:value={searchModel.value.name} options={personnelOptions} placeholder="请选择" clearable /> |
| | | |
| | | }, |
| | | { |
| | | label: '角色', |
| | | path: 'role', |
| | | render: () => <n-select v-model:value={searchModel.value.role} options={roleOptions} placeholder="请选择" clearable /> |
| | | }, |
| | | { |
| | | label: '打卡日期', |
| | | path: 'dateRange', |
| | | render: () => <n-date-picker v-model:value={searchModel.value.dateRange} type="daterange" clearable start-placeholder="开始时间" end-placeholder="结束时间" /> |
| | | path: 'statisticDate', |
| | | render: () => <n-date-picker v-model:value={searchModel.value.statisticDate} type="daterange" clearable start-placeholder="开始时间" end-placeholder="结束时间" /> |
| | | } |
| | | ]) |
| | | |
| | |
| | | return 'default' |
| | | } |
| | | |
| | | const columns = computed<DataTableColumns<AttendanceDetail>>(() => [ |
| | | { title: '序号', key: 'id', width: 80, align: 'center' }, |
| | | { title: '人员', key: 'name', width: 100 }, |
| | | { title: '角色', key: 'role', width: 100 }, |
| | | { title: '所属队伍', key: 'fleet', width: 120 }, |
| | | { title: '日期', key: 'date', width: 120 }, |
| | | { title: '班次', key: 'shift', width: 150 }, |
| | | { title: '打卡时间', key: 'checkInTime', width: 160 }, |
| | | const columns = computed<DataTableColumns<any>>(() => [ |
| | | { title: '序号', key: 'serialNumber', width: 80, resizable: true, fixed: 'left' }, |
| | | { title: '人员', key: 'name', width: 100, resizable: true, fixed: 'left' }, |
| | | { title: '角色', key: 'roleName', width: 120, resizable: true, }, |
| | | { title: '所属队伍', key: 'teamName', width: 120, resizable: true, }, |
| | | { title: '日期', key: 'clockTime', width: 120 }, |
| | | { title: '班次', key: 'shiftName', width: 120 }, |
| | | { title: '上班打卡时间', key: 'clockInTime', minWidth: 120 }, |
| | | { |
| | | title: '打卡结果', |
| | | key: 'checkInResult', |
| | | width: 100, |
| | | align: 'center', |
| | | title: '上班打卡结果', |
| | | key: 'clockInResult', |
| | | minWidth: 120, |
| | | render: (row) => ( |
| | | <NTag type={getResultTagType(row.checkInResult)} size="small"> |
| | | {row.checkInResult} |
| | | <NTag type={getResultTagType(row.clockInResult)} size="small"> |
| | | {row.clockInResult} |
| | | </NTag> |
| | | ) |
| | | }, |
| | | { title: '打卡地址', key: 'checkInAddress', minWidth: 200 } |
| | | { title: '下班打卡时间', key: 'clockOutTime', minWidth: 120 }, |
| | | { |
| | | title: '下班打卡结果', |
| | | key: 'clockOutResult', |
| | | minWidth: 120, |
| | | render: (row) => ( |
| | | <NTag type={getResultTagType(row.clockOutResult)} size="small"> |
| | | {row.clockOutResult} |
| | | </NTag> |
| | | ) |
| | | }, |
| | | { title: '打卡地址', key: 'clockInLocation', minWidth: 200 } |
| | | ]) |
| | | |
| | | async function getList() { |
| | | startLoading() |
| | | const params = { |
| | | ...pageParams.value, |
| | | appUserId: searchModel.value.appUserId, |
| | | statisticDate: searchModel.value.statisticDate?.join('-'), |
| | | } |
| | | getClockInStatisticsDetailList(params).then((res) => { |
| | | // statisticsList.value = res.data.records || [] |
| | | detailList.value.forEach((item: any, index: number) => { |
| | | item.serialNumber = index + 1 |
| | | }) |
| | | total.value = res.data.total || res.data.records?.length |
| | | }).finally(() => { |
| | | endLoading() |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | searchModel.value.appUserId = route.params.id |
| | | getList() |
| | | }) |
| | | |
| | | function changePage(page: number, size: number) { |
| | | pageParams.value.pageNum = page |
| | | pageParams.value.pageSize = size |
| | | getList() |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col"> |
| | | <div class="roboTaxi"> |
| | | <n-card style="border-radius: 6px" content-style="padding: 12px"> |
| | | <n-form :model="searchModel" label-placement="left" inline :show-feedback="false"> |
| | | <SearchPanel> |
| | |
| | | </n-gi> |
| | | <template #action> |
| | | <n-button style="width: 84px; height: 32px" @click="handleReset">重置</n-button> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleSearch">查询</n-button> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="getList">查询</n-button> |
| | | </template> |
| | | </SearchPanel> |
| | | </n-form> |
| | | </n-card> |
| | | |
| | | <n-card style="border-radius: 6px; margin-top: 12px" content-style="padding: 12px"> |
| | | <div class="mb-4 flex items-center justify-between"> |
| | | <div class="flex items-center gap-4 text-sm"> |
| | | <span>应打卡次数:<span class="font-bold">{{ statistics.shouldAttendDays }}次</span></span> |
| | | <span>正常:<span class="font-bold">{{ statistics.normalDays }}次</span></span> |
| | | <span>迟到:<span class="font-bold">{{ statistics.lateDays }}次</span></span> |
| | | <span>缺卡:<span class="font-bold">{{ statistics.absentDays }}次</span></span> |
| | | </div> |
| | | <div class="flex gap-2"> |
| | | <n-button |
| | | style="width: 100px; height: 32px; background-color: #E6F4FF; color: #0068FF; border: 1px solid #7ABDFF" |
| | | @click="handleExport"> |
| | | 导出打卡明细 |
| | | </n-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <n-data-table |
| | | :columns="columns" |
| | | :data="detailList" |
| | | :pagination="{ |
| | | page: currentPage, |
| | | pageSize: pageSize, |
| | | itemCount: total, |
| | | showSizePicker: true, |
| | | pageSizes: [10, 20, 50, 100], |
| | | prefix: ({ itemCount }) => `共 ${itemCount || 0} 条` |
| | | }" |
| | | :scroll-x="1400" |
| | | /> |
| | | <n-card style=" margin-top: 12px"> |
| | | <template #header> |
| | | <n-flex justify="space-between"> |
| | | <div class="flex items-center gap-4 text-sm"> |
| | | <span>应打卡次数:<span class="font-bold">{{ statistics.shouldAttendDays }}次</span></span> |
| | | <span>正常:<span class="font-bold">{{ statistics.normalDays }}次</span></span> |
| | | <span>迟到/早退:<span class="font-bold">{{ statistics.lateDays }}次</span></span> |
| | | <span>缺卡:<span class="font-bold">{{ statistics.absentDays }}次</span></span> |
| | | </div> |
| | | <div class="flex gap-2"> |
| | | <n-button |
| | | style="width: 100px; height: 32px; background-color: #E6F4FF; color: #0068FF; border: 1px solid #7ABDFF" |
| | | @click="handleExport"> |
| | | 导出打卡明细 |
| | | </n-button> |
| | | </div> |
| | | </n-flex> |
| | | </template> |
| | | <NSpace vertical :size="12"> |
| | | <n-data-table :columns="columns" :data="detailList" :loading="loading" :bordered="false" :header-height="18" |
| | | :row-height="18" /> |
| | | <Pagination :count="total" @change="changePage" /> |
| | | </NSpace> |
| | | </n-card> |
| | | </div> |
| | | </template> |
| | |
| | | <script setup lang="tsx"> |
| | | import { ref, computed } from 'vue' |
| | | import { useBoolean } from '@/hooks' |
| | | import { useRouter } from 'vue-router' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { getClockInStatisticsList, exportClockInStatistics } from '@/service/api/attendance' |
| | | import { NButton } from 'naive-ui' |
| | | import { downloadFile } from '@/utils' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | |
| | | interface Statistics { |
| | | id: number |
| | | name: string |
| | | role: string |
| | | fleet: string |
| | | shouldAttendDays: number |
| | | actualAttendDays: number |
| | | lateDays: number |
| | | absentDays: number |
| | | } |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | |
| | | const router = useRouter() |
| | | |
| | | const searchModel = ref({ |
| | | name: null as string | null, |
| | | role: null as string | null, |
| | | dateRange: null as [number, number] | null |
| | | roleId: null as string | null, |
| | | statisticDate: null as [number, number] | null |
| | | }) |
| | | |
| | | const personnelOptions = [ |
| | |
| | | { label: '角色1', value: '角色1' } |
| | | ] |
| | | |
| | | const statisticsList = ref<Statistics[]>([ |
| | | { id: 1, name: '王美丽', role: '网格员', fleet: '网格员一队', shouldAttendDays: 27, actualAttendDays: 25, lateDays: 1, absentDays: 1 }, |
| | | { id: 2, name: '王美丽', role: '网格员', fleet: '网格员一队', shouldAttendDays: 27, actualAttendDays: 24, lateDays: 2, absentDays: 1 }, |
| | | { id: 3, name: '王美丽', role: '网格员', fleet: '网格员一队', shouldAttendDays: 27, actualAttendDays: 25, lateDays: 2, absentDays: 0 }, |
| | | { id: 4, name: '王美丽', role: '网格员', fleet: '网格员一队', shouldAttendDays: 27, actualAttendDays: 25, lateDays: 2, absentDays: 1 }, |
| | | { id: 5, name: '王美丽', role: '网格员', fleet: '网格员一队', shouldAttendDays: 27, actualAttendDays: 25, lateDays: 0, absentDays: 2 }, |
| | | { id: 6, name: '王美丽', role: '网格员', fleet: '网格员一队', shouldAttendDays: 27, actualAttendDays: 26, lateDays: 0, absentDays: 1 }, |
| | | { id: 7, name: '王美丽', role: '网格员', fleet: '网格员一队', shouldAttendDays: 27, actualAttendDays: 27, lateDays: 0, absentDays: 1 }, |
| | | { id: 8, name: '王美丽', role: '网格员', fleet: '网格员一队', shouldAttendDays: 27, actualAttendDays: 26, lateDays: 1, absentDays: 0 }, |
| | | { id: 9, name: '王美丽', role: '网格员', fleet: '网格员一队', shouldAttendDays: 27, actualAttendDays: 26, lateDays: 1, absentDays: 0 }, |
| | | { id: 10, name: '王美丽', role: '网格员', fleet: '网格员一队', shouldAttendDays: 27, actualAttendDays: 26, lateDays: 1, absentDays: 0 } |
| | | ]) |
| | | const statisticsList = ref<any[]>([{ |
| | | serialNumber: 1, |
| | | name: '王美丽', |
| | | roleName: '网格员', |
| | | teamName: '队伍1', |
| | | shouldClockInDays: 10, |
| | | clockInDays: 8, |
| | | notClockInDays: 2, |
| | | lateEarly: 1, |
| | | }]) |
| | | |
| | | const currentPage = ref(1) |
| | | const pageSize = ref(10) |
| | | const total = ref(1000) |
| | | |
| | | const handleSearch = () => { |
| | | console.log('搜索', searchModel.value) |
| | | } |
| | | const total = ref(0) |
| | | const pageParams = ref({ |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | }) |
| | | |
| | | const handleReset = () => { |
| | | searchModel.value = { |
| | | name: null, |
| | | role: null, |
| | | dateRange: null |
| | | roleId: null, |
| | | statisticDate: null |
| | | } |
| | | } |
| | | |
| | | const handleExport = () => { |
| | | console.log('导出') |
| | | const handleExport = async () => { |
| | | const params = { |
| | | ...pageParams.value, |
| | | name: searchModel.value.name, |
| | | roleId: searchModel.value.roleId, |
| | | statisticDate: searchModel.value.statisticDate?.join('-'), |
| | | } |
| | | const response = await exportClockInStatistics(params) |
| | | if (response instanceof Blob) { |
| | | downloadFile(response, `打卡统计列表_${new Date().getTime()}.xlsx`) |
| | | window.$message.success('导出成功') |
| | | } |
| | | } |
| | | |
| | | const handleViewDetail = (row: Statistics) => { |
| | | const handleViewDetail = (row: any) => { |
| | | router.push(`/operations/attendance/statistics/detail/${row.id}`) |
| | | } |
| | | |
| | |
| | | }, |
| | | { |
| | | label: '角色', |
| | | path: 'role', |
| | | render: () => <n-select v-model:value={searchModel.value.role} options={roleOptions} placeholder="请选择" clearable /> |
| | | path: 'searchModel.value.roleId', |
| | | render: () => <n-select v-model:value={searchModel.value.roleId} options={roleOptions} placeholder="请选择" clearable /> |
| | | }, |
| | | { |
| | | label: '打卡日期', |
| | | path: 'dateRange', |
| | | render: () => <n-date-picker v-model:value={searchModel.value.dateRange} type="daterange" clearable start-placeholder="开始时间" end-placeholder="结束时间" /> |
| | | path: 'statisticDate', |
| | | render: () => <n-date-picker v-model:value={searchModel.value.statisticDate} type="daterange" clearable start-placeholder="开始时间" end-placeholder="结束时间" /> |
| | | } |
| | | ]) |
| | | |
| | | const columns = computed<DataTableColumns<Statistics>>(() => [ |
| | | { title: '序号', key: 'id', width: 80, align: 'center' }, |
| | | { title: '人员', key: 'name', width: 120 }, |
| | | { title: '角色', key: 'role', width: 120 }, |
| | | { title: '所属队伍', key: 'fleet', width: 150 }, |
| | | { title: '应打卡天数', key: 'shouldAttendDays', width: 120, align: 'center' }, |
| | | { title: '正常', key: 'actualAttendDays', width: 100, align: 'center' }, |
| | | { title: '缺卡', key: 'absentDays', width: 100, align: 'center' }, |
| | | { title: '迟到', key: 'lateDays', width: 100, align: 'center' }, |
| | | const columns = computed<DataTableColumns<any>>(() => [ |
| | | { title: '序号', key: 'serialNumber', width: 91, resizable: true, fixed: 'left' }, |
| | | { title: '人员', key: 'name', width: 143, resizable: true, fixed: 'left' }, |
| | | { title: '角色', key: 'roleName', width: 134, resizable: true, }, |
| | | { title: '所属队伍', key: 'teamName', width: 142, resizable: true, }, |
| | | { title: '应打卡天数', key: 'shouldClockInDays', minWidth: 100, resizable: true, }, |
| | | { title: '正常', key: 'clockInDays', minWidth: 100, resizable: true, }, |
| | | { title: '缺卡', key: 'notClockInDays', minWidth: 100, resizable: true, }, |
| | | { title: '迟到', key: 'lateEarly', minWidth: 100, resizable: true, }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 120, |
| | | align: 'center', |
| | | width: 134, |
| | | fixed: 'right', |
| | | render: (row) => ( |
| | | <NButton text type="primary" onClick={() => handleViewDetail(row)}> |
| | |
| | | ) |
| | | } |
| | | ]) |
| | | |
| | | async function getList() { |
| | | startLoading() |
| | | const params = { |
| | | ...pageParams.value, |
| | | name: searchModel.value.name, |
| | | roleId: searchModel.value.roleId, |
| | | statisticDate: searchModel.value.statisticDate?.join('-'), |
| | | } |
| | | getClockInStatisticsList(params).then((res) => { |
| | | // statisticsList.value = res.data.records || [] |
| | | statisticsList.value.forEach((item: any, index: number) => { |
| | | item.serialNumber = index + 1 |
| | | }) |
| | | total.value = res.data.total || res.data.records?.length |
| | | }).finally(() => { |
| | | endLoading() |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | |
| | | function changePage(page: number, size: number) { |
| | | pageParams.value.pageNum = page |
| | | pageParams.value.pageSize = size |
| | | getList() |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col"> |
| | | <div class="roboTaxi"> |
| | | <n-card style="border-radius: 6px" content-style="padding: 12px"> |
| | | <n-form :model="searchModel" label-placement="left" inline :show-feedback="false"> |
| | | <SearchPanel> |
| | |
| | | </n-gi> |
| | | <template #action> |
| | | <n-button style="width: 84px; height: 32px" @click="handleReset">重置</n-button> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleSearch">查询</n-button> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="getList">查询</n-button> |
| | | </template> |
| | | </SearchPanel> |
| | | </n-form> |
| | | </n-card> |
| | | |
| | | <n-card style="border-radius: 6px; margin-top: 12px" content-style="padding: 12px"> |
| | | <div class="mb-4 flex justify-end"> |
| | | <n-button style="height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;" @click="handleExport">导出</n-button> |
| | | </div> |
| | | |
| | | <n-data-table |
| | | :columns="columns" |
| | | :data="statisticsList" |
| | | :pagination="{ |
| | | page: currentPage, |
| | | pageSize: pageSize, |
| | | itemCount: total, |
| | | showSizePicker: true, |
| | | pageSizes: [10, 20, 50, 100], |
| | | prefix: ({ itemCount }) => `第 ${(currentPage - 1) * pageSize + 1}-${Math.min(currentPage * pageSize, itemCount || 0)} 条 / 总共 ${itemCount || 0} 条` |
| | | }" |
| | | :scroll-x="1200" |
| | | /> |
| | | <template #header> |
| | | <n-flex justify="flex-end"> |
| | | <NButton type="primary" secondary @click="handleExport"> |
| | | 导出 |
| | | </NButton> |
| | | </n-flex> |
| | | </template> |
| | | <NSpace vertical :size="12"> |
| | | <n-data-table :columns="columns" :data="statisticsList" :loading="loading" :bordered="false" :header-height="18" |
| | | :row-height="18" /> |
| | | <Pagination :count="total" @change="changePage" /> |
| | | </NSpace> |
| | | </n-card> |
| | | </div> |
| | | </template> |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, h } from 'vue' |
| | | import { useBoolean } from '@/hooks' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NCheckbox } from 'naive-ui' |
| | | import { getNoticeList, updateNoticeConfig } from '@/service/api/attendance' |
| | | |
| | | interface MessageItem { |
| | | id: number |
| | | type: string |
| | | pushMethod: boolean |
| | | smsMethod: boolean |
| | | updateTime: string |
| | | } |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | |
| | | const tableData = ref<MessageItem[]>([ |
| | | { id: 1, type: '新任务通知', pushMethod: true, smsMethod: false, updateTime: '2025-05-07 14:53' }, |
| | | { id: 2, type: '任务状态通知', pushMethod: true, smsMethod: false, updateTime: '2025-05-07 14:53' }, |
| | | { id: 3, type: '排班变动通知', pushMethod: true, smsMethod: false, updateTime: '2025-05-07 14:53' }, |
| | | { id: 4, type: '公告通知', pushMethod: true, smsMethod: false, updateTime: '2025-05-07 14:53' }, |
| | | { id: 5, type: '新事故通知', pushMethod: true, smsMethod: false, updateTime: '2025-05-07 14:53' }, |
| | | { id: 6, type: '事故状态通知', pushMethod: true, smsMethod: false, updateTime: '2025-05-07 14:53' } |
| | | ]) |
| | | const tableData = ref<any[]>([]) |
| | | |
| | | const columns: DataTableColumns<MessageItem> = [ |
| | | const columns: DataTableColumns<any> = [ |
| | | { |
| | | title: '序号', |
| | | key: 'id', |
| | | key: 'serialNumber', |
| | | width: 80, |
| | | align: 'center' |
| | | }, |
| | | { |
| | | title: '通知类型', |
| | | key: 'type', |
| | | align: 'center' |
| | | key: 'notificationType', |
| | | render: (row) => { |
| | | switch (row.notificationType) { |
| | | //通知类型 1=新增事故,2=事故即将超时提示,3=事故超时提示,4=排班信息变动,5=事故状态变更,6=新增任务,7=任务过期提示,8=版本更新提示,9=变更协议政策内容,10=变更协议内容并需用户授权,11=打卡成功通知 |
| | | case 1: |
| | | return '新增事故' |
| | | case 2: |
| | | return '事故即将超时提示' |
| | | case 3: |
| | | return '事故超时提示' |
| | | case 4: |
| | | return '排班信息变动' |
| | | case 5: |
| | | return '事故状态变更' |
| | | case 6: |
| | | return '新增任务' |
| | | case 7: |
| | | return '任务过期提示' |
| | | case 8: |
| | | return '版本更新提示' |
| | | case 9: |
| | | return '变更协议政策内容' |
| | | case 10: |
| | | return '变更协议内容并需用户授权' |
| | | case 11: |
| | | return '打卡成功通知' |
| | | default: |
| | | return row.notificationType |
| | | } |
| | | } |
| | | }, |
| | | { |
| | | title: '通知方式', |
| | | key: 'method', |
| | | align: 'center', |
| | | render: (row) => { |
| | | return h('div', { class: 'flex items-center justify-center gap-4' }, [ |
| | | return h('div', { class: 'flex items-center gap-4' }, [ |
| | | h(NCheckbox, { |
| | | checked: row.pushMethod, |
| | | checked: row.isPush, |
| | | onUpdateChecked: (value: boolean) => { |
| | | row.pushMethod = value |
| | | row.isPush = value |
| | | } |
| | | }, { default: () => 'PUSH推送' }), |
| | | h(NCheckbox, { |
| | | checked: row.smsMethod, |
| | | checked: row.isMessage, |
| | | onUpdateChecked: (value: boolean) => { |
| | | row.smsMethod = value |
| | | row.isMessage = value |
| | | } |
| | | }, { default: () => '短信通知' }) |
| | | ]) |
| | |
| | | { |
| | | title: '更新时间', |
| | | key: 'updateTime', |
| | | align: 'center' |
| | | } |
| | | ] |
| | | |
| | | const handleSave = () => { |
| | | console.log('保存', tableData.value) |
| | | startLoading() |
| | | updateNoticeConfig({ notifications: tableData.value }).then((res) => { |
| | | if (res.code === 200) { |
| | | window.$message.success('保存成功') |
| | | } |
| | | }).finally(() => { |
| | | getList() |
| | | endLoading() |
| | | }) |
| | | } |
| | | |
| | | async function getList() { |
| | | startLoading() |
| | | getNoticeList().then((res) => { |
| | | tableData.value = res.data || [] |
| | | tableData.value.forEach((item: any, index: number) => { |
| | | item.serialNumber = index + 1 |
| | | }) |
| | | }).finally(() => { |
| | | endLoading() |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex-col"> |
| | | <n-card style="border-radius: 6px; overflow: hidden;" content-style="padding: 12px; height: 100%; display: flex; flex-direction: column;"> |
| | | <n-card style="border-radius: 6px; overflow: hidden;" |
| | | content-style="padding: 12px; height: 100%; display: flex; flex-direction: column;"> |
| | | <div style="flex: 1; overflow: auto;"> |
| | | <n-data-table |
| | | :columns="columns" |
| | | :data="tableData" |
| | | :bordered="true" |
| | | :single-line="false" |
| | | /> |
| | | <n-data-table :columns="columns" :data="tableData" :loading="loading" :bordered="false" :header-height="18" |
| | | :row-height="18" /> |
| | | </div> |
| | | </n-card> |
| | | |
| | | <div class="flex justify-end absolute bottom-0 left-0 right-0" style="background-color: white; padding: 16px 24px; border-radius: 6px;"> |
| | | <div class="flex justify-end absolute bottom-0 left-0 right-0" |
| | | style="background-color: white; padding: 16px 24px; border-radius: 6px;"> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleSave">保存</n-button> |
| | | </div> |
| | | </div> |
| | |
| | | import type { FormInst, FormRules, UploadFileInfo } from 'naive-ui' |
| | | |
| | | interface FormModel { |
| | | name: string |
| | | phone: string |
| | | nickName: string |
| | | phoneNumber: string |
| | | uniqueNumber: string |
| | | avatar: string |
| | | contactPhone: string |
| | | password: string |
| | | role: string | null |
| | | remark: string |
| | | roleIds: string[] | null |
| | | remarks: string |
| | | } |
| | | |
| | | const props = defineProps<{ |
| | | show: boolean |
| | | mode?: 'add' | 'edit' |
| | | roleOptions: any[] |
| | | }>() |
| | | |
| | | const emit = defineEmits<{ |
| | |
| | | |
| | | const formRef = ref<FormInst | null>(null) |
| | | const formModel = ref<FormModel>({ |
| | | name: '', |
| | | phone: '', |
| | | nickName: '', |
| | | phoneNumber: '', |
| | | uniqueNumber: '', |
| | | avatar: '', |
| | | contactPhone: '', |
| | | password: '', |
| | | role: null, |
| | | remark: '' |
| | | roleIds: null, |
| | | remarks: '' |
| | | }) |
| | | |
| | | const roleOptions = [ |
| | | { label: '运维管理', value: '运维管理' }, |
| | | { label: '角色1', value: '角色1' }, |
| | | { label: '角色2', value: '角色2' } |
| | | ] |
| | | const roleOptions = computed(() => props.roleOptions) |
| | | |
| | | const fileList = ref<UploadFileInfo[]>([]) |
| | | |
| | | const rules: FormRules = { |
| | | name: [ |
| | | nickName: [ |
| | | { required: true, message: '请输入用户名称', trigger: 'blur' } |
| | | ], |
| | | phone: [ |
| | | uniqueNumber: [ |
| | | { required: true, message: '请输入编号', trigger: 'blur' } |
| | | ], |
| | | password: [ |
| | | { required: true, message: '请输入初始密码', trigger: 'blur' } |
| | | ], |
| | | contactPhone: [ |
| | | phoneNumber: [ |
| | | { required: true, message: '请输入联系电话', trigger: 'blur' }, |
| | | { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' } |
| | | ], |
| | | role: [ |
| | | { required: true, message: '请选择角色', trigger: 'change' } |
| | | roleIds: [ |
| | | { type: 'array', required: true, message: '请选择角色', trigger: 'change' } |
| | | ] |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | async function openModal(row: any) { |
| | | formModel.value = {...row} |
| | | formModel.value.roleIds = row.sysRoles.map((item: any) => item.id) |
| | | } |
| | | |
| | | defineExpose({ |
| | | openModal, |
| | | }) |
| | | |
| | | |
| | | const resetForm = () => { |
| | | formModel.value = { |
| | | name: '', |
| | | phone: '', |
| | | nickName: '', |
| | | phoneNumber: '', |
| | | uniqueNumber: '', |
| | | avatar: '', |
| | | contactPhone: '', |
| | | password: '', |
| | | role: null, |
| | | remark: '' |
| | | roleIds: null, |
| | | remarks: '' |
| | | } |
| | | fileList.value = [] |
| | | } |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal |
| | | v-model:show="show" |
| | | :mask-closable="false" |
| | | preset="card" |
| | | :title="mode === 'edit' ? '编辑运维人员' : '新增运维人员'" |
| | | <n-modal |
| | | v-model:show="show" |
| | | :mask-closable="false" |
| | | preset="card" |
| | | :title="mode === 'edit' ? '编辑账号' : '新增账号'" |
| | | class="w-600px" |
| | | :segmented="{ |
| | | content: true, |
| | |
| | | require-mark-placement="left" |
| | | > |
| | | <div class="grid grid-cols-2 gap-4"> |
| | | <n-form-item label="用户名称" path="name"> |
| | | <n-input v-model:value="formModel.name" placeholder="请输入" clearable /> |
| | | <n-form-item label="用户名称" path="nickName"> |
| | | <n-input v-model:value="formModel.nickName" placeholder="请输入" clearable /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="编号" path="phone"> |
| | | <n-input v-model:value="formModel.phone" placeholder="请输入" clearable /> |
| | | <n-form-item label="编号" path="uniqueNumber"> |
| | | <n-input v-model:value="formModel.uniqueNumber" placeholder="请输入" clearable /> |
| | | </n-form-item> |
| | | </div> |
| | | |
| | |
| | | </n-upload> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="初始密码" path="password"> |
| | | <n-form-item label="初始密码" path="password" v-if="mode === 'add'"> |
| | | <n-input v-model:value="formModel.password" type="password" placeholder="请输入" clearable show-password-on="click" /> |
| | | </n-form-item> |
| | | </div> |
| | | |
| | | <div class="grid grid-cols-2 gap-4"> |
| | | <n-form-item label="联系电话" path="contactPhone"> |
| | | <n-input v-model:value="formModel.contactPhone" placeholder="请输入" clearable /> |
| | | <n-form-item label="联系电话" path="phoneNumber"> |
| | | <n-input v-model:value="formModel.phoneNumber" placeholder="请输入" clearable /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="角色" path="role"> |
| | | <n-select v-model:value="formModel.role" :options="roleOptions" placeholder="请选择" clearable /> |
| | | <n-form-item label="角色" path="roleIds"> |
| | | <n-select v-model:value="formModel.roleIds" :options="roleOptions" placeholder="请选择" clearable multiple/> |
| | | </n-form-item> |
| | | </div> |
| | | |
| | | <n-form-item label="备注" path="remark"> |
| | | <n-form-item label="备注" path="remarks"> |
| | | <n-input |
| | | v-model:value="formModel.remark" |
| | | v-model:value="formModel.remarks" |
| | | type="textarea" |
| | | placeholder="请输入" |
| | | :rows="3" |
| | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue' |
| | | import { useTabStore } from '@/store' |
| | | import { useRouter, useRoute } from 'vue-router' |
| | | import { getOperationsUserDetail } from '@/service/api/attendance' |
| | | |
| | | const router = useRouter() |
| | | const route = useRoute() |
| | | const tabStore = useTabStore() |
| | | |
| | | const personnelInfo = ref({ |
| | | name: '王丽丽', |
| | | phone: '10001', |
| | | contactPhone: '14725836905', |
| | | role: '调度员', |
| | | avatar: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg', |
| | | remark: '-', |
| | | idCardFront: '', |
| | | idCardBack: '', |
| | | driverLicense: '', |
| | | noCriminalRecord: '' |
| | | }) |
| | | const personnelInfo = ref() |
| | | |
| | | const handleBack = () => { |
| | | tabStore.closeTab(route.fullPath) |
| | | router.back() |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | const res = await getOperationsUserDetail(route.params.id) |
| | | if (res.code === 200) { |
| | | personnelInfo.value = res.data |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col" style="gap: 12px"> |
| | | <div class="h-full flex flex-col" style="gap: 12px" v-if="personnelInfo?.nickName"> |
| | | <n-card style="border-radius: 6px" content-style="padding: 12px"> |
| | | <div class="flex items-center justify-between mb-4"> |
| | | <div class="text-lg font-bold">员工详情</div> |
| | |
| | | /> |
| | | <div class="flex-1"> |
| | | <div class="flex items-center gap-3 mb-2"> |
| | | <span class="text-lg font-medium">{{ personnelInfo.name }}</span> |
| | | <n-tag type="info" size="small">运维管理员</n-tag> |
| | | <span class="text-lg font-medium">{{ personnelInfo.nickName }}</span> |
| | | </div> |
| | | <div class="text-sm mb-2"> |
| | | <span class="text-gray-600">编号:</span> |
| | | <span>{{ personnelInfo.phone }}</span> |
| | | <span>{{ personnelInfo.uniqueNumber }}</span> |
| | | </div> |
| | | <div class="grid grid-cols-3 gap-4 text-sm"> |
| | | <div> |
| | | <span class="text-gray-600">电话:</span> |
| | | <span>{{ personnelInfo.contactPhone }}</span> |
| | | <span>{{ personnelInfo.phoneNumber }}</span> |
| | | </div> |
| | | <div> |
| | | <span class="text-gray-600">角色:</span> |
| | | <span>{{ personnelInfo.role }}</span> |
| | | <span class="gap-2 inline-flex"> |
| | | <n-tag type="info" size="small" v-for="item in personnelInfo.sysRoles">{{ item.roleName }}</n-tag> |
| | | </span> |
| | | </div> |
| | | <div> |
| | | <span class="text-gray-600">备注:</span> |
| | | <span>{{ personnelInfo.remark }}</span> |
| | | <span>{{ personnelInfo.remarks || '-' }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | <n-image |
| | | width="128" |
| | | height="80" |
| | | src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg" |
| | | v-if="personnelInfo.idCardFront" |
| | | :src="personnelInfo.idCardFront" |
| | | object-fit="cover" |
| | | style="border-radius: 4px" |
| | | /> |
| | | <n-image |
| | | width="128" |
| | | height="80" |
| | | src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg" |
| | | v-if="personnelInfo.idCardBack" |
| | | :src="personnelInfo.idCardBack" |
| | | object-fit="cover" |
| | | style="border-radius: 4px" |
| | | /> |
| | |
| | | <n-image |
| | | width="128" |
| | | height="80" |
| | | src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg" |
| | | v-if="personnelInfo.driverLicense" |
| | | :src="personnelInfo.driverLicense" |
| | | object-fit="cover" |
| | | style="border-radius: 4px" |
| | | /> |
| | |
| | | <n-image |
| | | width="128" |
| | | height="80" |
| | | src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg" |
| | | v-if="personnelInfo.certificateOfNoCriminalRecord" |
| | | :src="personnelInfo.certificateOfNoCriminalRecord" |
| | | object-fit="cover" |
| | | style="border-radius: 4px" |
| | | /> |
| | |
| | | <script setup lang="tsx"> |
| | | import { ref, h, computed } from 'vue' |
| | | import { useBoolean } from '@/hooks' |
| | | import { useRouter } from 'vue-router' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NButton, NTag, NDropdown } from 'naive-ui' |
| | | import { getOperationsUserList, exportOperationsUserTemplate, exportOperationsUser, importOperationsUser, |
| | | addOperationsUser, editOperationsUser, updateOperationsUserStatus, resetOperationsUserPassword, deleteOperationsUser } from '@/service/api/attendance' |
| | | import { fetchRoleList } from '@/service/api/fleet' |
| | | import { downloadFile } from '@/utils' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import PersonnelFormModal from './components/PersonnelFormModal.vue' |
| | | |
| | | const router = useRouter() |
| | | |
| | | interface Personnel { |
| | | id: number |
| | | avatar: string |
| | | name: string |
| | | phone: string |
| | | contactPhone: string |
| | | role: string |
| | | status: string |
| | | createTime: string |
| | | remark: string |
| | | } |
| | | const dialog = useDialog() |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | |
| | | const searchModel = ref({ |
| | | name: '', |
| | | phone: '', |
| | | role: null as string | null, |
| | | nickName: '', |
| | | phoneNumber: '', |
| | | roleId: null as string | null, |
| | | status: null as string | null |
| | | }) |
| | | |
| | | const showFormModal = ref(false) |
| | | const formMode = ref<'add' | 'edit'>('add') |
| | | |
| | | const roleOptions = [ |
| | | { label: '运维管理', value: '运维管理' }, |
| | | { label: '角色1', value: '角色1' } |
| | | ] |
| | | const roleOptions = ref<any[]>([]) |
| | | const modalRef = ref() |
| | | |
| | | const statusOptions = [ |
| | | { label: '启用', value: '启用' }, |
| | | { label: '禁用', value: '禁用' } |
| | | { label: '启用', value: true }, |
| | | { label: '禁用', value: false } |
| | | ] |
| | | |
| | | const personnelList = ref<Personnel[]>([ |
| | | { id: 1, avatar: '', name: '王美丽', phone: '3948716', contactPhone: '14725836905', role: '运维管理', status: '启用', createTime: '2025-06-08 03:15', remark: '前端开发' }, |
| | | { id: 2, avatar: '', name: '王美丽', phone: '3948716', contactPhone: '14725836905', role: '角色1', status: '禁用', createTime: '2025-06-08 03:15', remark: '' }, |
| | | { id: 3, avatar: '', name: '王美丽', phone: '3948716', contactPhone: '14725836905', role: '角色1', status: '启用', createTime: '2025-06-08 03:15', remark: '' }, |
| | | { id: 4, avatar: '', name: '王美丽', phone: '3948716', contactPhone: '14725836905', role: '角色1', status: '启用', createTime: '2025-06-08 03:15', remark: '' }, |
| | | { id: 5, avatar: '', name: '王美丽', phone: '3948716', contactPhone: '14725836905', role: '角色1', status: '启用', createTime: '2025-06-08 03:15', remark: '' }, |
| | | { id: 6, avatar: '', name: '王美丽', phone: '3948716', contactPhone: '14725836905', role: '角色1', status: '启用', createTime: '2025-06-08 03:15', remark: '' }, |
| | | { id: 7, avatar: '', name: '王美丽', phone: '3948716', contactPhone: '14725836905', role: '角色1', status: '启用', createTime: '2025-06-08 03:15', remark: '' }, |
| | | { id: 8, avatar: '', name: '王美丽', phone: '3948716', contactPhone: '14725836905', role: '角色1', status: '启用', createTime: '2025-06-08 03:15', remark: '' }, |
| | | { id: 9, avatar: '', name: '王美丽', phone: '3948716', contactPhone: '14725836905', role: '角色1', status: '启用', createTime: '2025-06-08 03:15', remark: '' }, |
| | | { id: 10, avatar: '', name: '王美丽', phone: '3948716', contactPhone: '14725836905', role: '角色1', status: '启用', createTime: '2025-06-08 03:15', remark: '' } |
| | | ]) |
| | | const personnelList = ref<any[]>([]) |
| | | |
| | | const currentPage = ref(1) |
| | | const pageSize = ref(10) |
| | | const total = ref(800) |
| | | const total = ref(0) |
| | | const pageParams = ref({ |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | }) |
| | | |
| | | const handleSearch = () => { |
| | | console.log('搜索', searchModel.value) |
| | |
| | | |
| | | const handleReset = () => { |
| | | searchModel.value = { |
| | | name: '', |
| | | phone: '', |
| | | role: null, |
| | | nickName: '', |
| | | phoneNumber: '', |
| | | roleId: null, |
| | | status: null |
| | | } |
| | | } |
| | | |
| | | const handleExport = () => { |
| | | console.log('导出') |
| | | const params = { |
| | | ...pageParams.value, |
| | | nickName: searchModel.value.nickName, |
| | | phoneNumber: searchModel.value.phoneNumber, |
| | | roleId: searchModel.value.roleId, |
| | | status: searchModel.value.status, |
| | | } |
| | | exportOperationsUser(params).then(res => { |
| | | if (res instanceof Blob) { |
| | | downloadFile(res, `运维人员列表_${new Date().getTime()}.xlsx`) |
| | | window.$message.success('导出成功') |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const handleImport = () => { |
| | | console.log('导入') |
| | | // 文件导入相关 |
| | | const fileInputRef = ref<HTMLInputElement | null>(null) |
| | | |
| | | // 导入 |
| | | function handleImport() { |
| | | fileInputRef.value?.click() |
| | | } |
| | | |
| | | // 处理文件选择 |
| | | async function handleFileChange(event: Event) { |
| | | const target = event.target as HTMLInputElement |
| | | const file = target.files?.[0] |
| | | if (!file) |
| | | return |
| | | |
| | | try { |
| | | const { code } = await importOperationsUser(file) |
| | | if (code === 200) { |
| | | window.$message.success('导入成功') |
| | | getList() |
| | | } |
| | | } |
| | | catch (error) { |
| | | console.error('导入失败:', error) |
| | | } |
| | | finally { |
| | | // 清空 input 方便下次选择同一个文件 |
| | | target.value = '' |
| | | } |
| | | } |
| | | |
| | | const handleAdd = () => { |
| | |
| | | showFormModal.value = true |
| | | } |
| | | |
| | | const handleEdit = (row: Personnel) => { |
| | | router.push(`/operations/personnel/personnelManagement/detail/${row.id}`) |
| | | const handleEdit = (row: any) => { |
| | | // router.push(`/operations/personnel/personnelManagement/detail/${row.id}`) |
| | | formMode.value = 'edit' |
| | | showFormModal.value = true |
| | | modalRef.value?.openModal(row) |
| | | } |
| | | |
| | | const handleConfirmForm = (data: any) => { |
| | | console.log('保存数据', data) |
| | | // 这里可以调用API保存数据 |
| | | if (data.id) { |
| | | editOperationsUser(data).then(res => { |
| | | if (res.code === 200) { |
| | | showFormModal.value = false |
| | | window.$message.success('编辑成功') |
| | | getList() |
| | | } |
| | | }) |
| | | } else { |
| | | addOperationsUser(data).then(res => { |
| | | if (res.code === 200) { |
| | | showFormModal.value = false |
| | | window.$message.success('新增成功') |
| | | getList() |
| | | } |
| | | }) |
| | | } |
| | | } |
| | | |
| | | const handleMore = (row: Personnel) => { |
| | | console.log('更多', row) |
| | | } |
| | | |
| | | const moreOptions = (row: Personnel) => [ |
| | | const moreOptions = (row: any) => [ |
| | | { |
| | | label: '编辑', |
| | | key: 'edit', |
| | |
| | | } |
| | | }, |
| | | { |
| | | label: row.status === '启用' ? '禁用' : '启用', |
| | | label: row.status ? '禁用' : '启用', |
| | | key: 'toggle', |
| | | props: { |
| | | onClick: () => console.log('切换状态', row) |
| | | onClick: () => { |
| | | updateOperationsUserStatus({ |
| | | id: row.id, |
| | | status: !row.status |
| | | }).then(res => { |
| | | if (res.code === 200) { |
| | | window.$message.success('操作成功') |
| | | getList() |
| | | } |
| | | }) |
| | | } |
| | | } |
| | | }, |
| | | { |
| | | label: '重置密码', |
| | | key: 'password', |
| | | props: { |
| | | onClick: () => console.log('重置密码', row) |
| | | onClick: () => { |
| | | dialog.create({ |
| | | title: '确认重置密码?', |
| | | content: '', |
| | | positiveText: '重置密码', |
| | | negativeText: '取消', |
| | | draggable: true, |
| | | onPositiveClick: () => { |
| | | resetOperationsUserPassword({ |
| | | id: row.id |
| | | }).then(res => { |
| | | if (res.code === 200) { |
| | | window.$message.success('重置密码成功') |
| | | getList() |
| | | } |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | } |
| | | }, |
| | | { |
| | | label: '删除', |
| | | key: 'delete', |
| | | props: { |
| | | onClick: () => console.log('删除', row) |
| | | onClick: () => { |
| | | dialog.create({ |
| | | title: '确认删除吗?', |
| | | content: '', |
| | | positiveText: '删除', |
| | | negativeText: '取消', |
| | | draggable: true, |
| | | onPositiveClick: () => { |
| | | deleteOperationsUser(row.id).then(res => { |
| | | if (res.code === 200) { |
| | | window.$message.success('删除成功') |
| | | getList() |
| | | } |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | } |
| | | } |
| | | ] |
| | |
| | | const filterItems = computed(() => [ |
| | | { |
| | | label: '员工姓名', |
| | | path: 'name', |
| | | render: () => <n-input v-model:value={searchModel.value.name} placeholder="请输入" clearable /> |
| | | path: 'nickName', |
| | | render: () => <n-input v-model:value={searchModel.value.nickName} placeholder="请输入" clearable /> |
| | | }, |
| | | { |
| | | label: '联系电话', |
| | | path: 'phone', |
| | | render: () => <n-input v-model:value={searchModel.value.phone} placeholder="请输入" clearable /> |
| | | path: 'phoneNumber', |
| | | render: () => <n-input v-model:value={searchModel.value.phoneNumber} placeholder="请输入" clearable /> |
| | | }, |
| | | { |
| | | label: '角色', |
| | | path: 'role', |
| | | render: () => <n-select v-model:value={searchModel.value.role} options={roleOptions} placeholder="请选择" clearable /> |
| | | path: 'roleId', |
| | | render: () => <n-select v-model:value={searchModel.value.roleId} options={roleOptions.value} placeholder="请选择" clearable /> |
| | | }, |
| | | { |
| | | label: '账号状态', |
| | | path: 'status', |
| | | render: () => <n-select v-model:value={searchModel.value.status} options={statusOptions} placeholder="请选择" clearable /> |
| | | } |
| | | }, |
| | | ]) |
| | | |
| | | const columns = computed<DataTableColumns<Personnel>>(() => [ |
| | | { title: '序号', key: 'id', width: 80, align: 'center' }, |
| | | const columns = computed<DataTableColumns<any>>(() => [ |
| | | { title: '序号', key: 'serialNumber', width: 80, resizable: true }, |
| | | { |
| | | title: '姓名', |
| | | key: 'name', |
| | | key: 'nickName', |
| | | width: 150, |
| | | resizable: true, |
| | | render: (row) => ( |
| | | <div class="flex items-center gap-2"> |
| | | <div class="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center text-sm">头</div> |
| | | <span>{row.name}</span> |
| | | {row.avatar && <img src={row.avatar} alt="头像" class="w-8 h-8 rounded-full" />} |
| | | {!row.avatar && <div class="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center text-sm">头</div>} |
| | | <span>{row.nickName}</span> |
| | | </div> |
| | | ) |
| | | }, |
| | | { title: '编号', key: 'phone', width: 120 }, |
| | | { title: '联系电话', key: 'contactPhone', width: 150 }, |
| | | { title: '编号', key: 'uniqueNumber', width: 120, resizable: true }, |
| | | { title: '联系电话', key: 'phoneNumber', width: 150, resizable: true }, |
| | | { |
| | | title: '角色', |
| | | key: 'role', |
| | | key: 'sysRoles', |
| | | width: 150, |
| | | resizable: true, |
| | | render: (row) => ( |
| | | <NTag type="info" size="small" style={{ borderRadius: '4px' }}>{row.role}</NTag> |
| | | <div> |
| | | { |
| | | row.sysRoles?.map((role: any) => ( |
| | | <NTag type="primary" size="small" style={{ borderRadius: '4px' }} >{role.roleName}</NTag> |
| | | )) |
| | | } |
| | | </div> |
| | | ) |
| | | }, |
| | | { |
| | | title: '状态', |
| | | key: 'status', |
| | | width: 100, |
| | | render: (row) => { |
| | | const isActive = row.status === '启用' |
| | | return h('span', { |
| | | style: { |
| | | display: 'inline-block', |
| | | width: '52px', |
| | | height: '20px', |
| | | lineHeight: '20px', |
| | | textAlign: 'center', |
| | | fontSize: '12px', |
| | | borderRadius: '4px', |
| | | color: isActive ? '#0068FF' : 'rgba(23, 31, 38, 0.8)', |
| | | backgroundColor: isActive ? '#E6F4FF' : 'rgba(23, 31, 38, 0.05)' |
| | | } |
| | | }, row.status) |
| | | } |
| | | resizable: true, |
| | | render: (row) => ( |
| | | row.status ? <NTag type="primary" size="small" style={{ borderRadius: '4px' }} >启用</NTag> : <NTag type="error" size="small" style={{ borderRadius: '4px' }} >禁用</NTag> |
| | | ) |
| | | }, |
| | | { title: '创建时间', key: 'createTime', width: 180 }, |
| | | { title: '备注', key: 'remark', minWidth: 150 }, |
| | | { title: '创建时间', key: 'createTime', width: 180 , resizable: true }, |
| | | { title: '备注', key: 'remarks', width: 150, resizable: true }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 150, |
| | | width: 120, |
| | | fixed: 'right', |
| | | render: (row) => ( |
| | | <div class="flex gap-3"> |
| | | <a style={{ color: '#0068FF', cursor: 'pointer' }} onClick={() => handleEdit(row)}>详情</a> |
| | | <a style={{ color: '#0068FF', cursor: 'pointer' }} onClick={() => router.push(`/operations/personnel/personnelManagement/detail/${row.id}`)}>详情</a> |
| | | <NDropdown options={moreOptions(row)} trigger="click"> |
| | | <a style={{ color: '#0068FF', cursor: 'pointer' }}>更多</a> |
| | | </NDropdown> |
| | |
| | | ) |
| | | } |
| | | ]) |
| | | |
| | | async function getList() { |
| | | startLoading() |
| | | const params = { |
| | | ...pageParams.value, |
| | | nickName: searchModel.value.nickName, |
| | | phoneNumber: searchModel.value.phoneNumber, |
| | | roleId: searchModel.value.roleId, |
| | | status: searchModel.value.status, |
| | | } |
| | | getOperationsUserList(params).then((res) => { |
| | | personnelList.value = res.data.records || [] |
| | | personnelList.value.forEach((item: any, index: number) => { |
| | | item.serialNumber = index + 1 |
| | | }) |
| | | total.value = res.data.total || res.data.records?.length |
| | | }).finally(() => { |
| | | endLoading() |
| | | }) |
| | | } |
| | | |
| | | async function getRoleList() { |
| | | const res = await fetchRoleList() |
| | | if (res.code === 200) { |
| | | roleOptions.value = res.data.map((item: any) => ({ |
| | | label: item.roleName, |
| | | value: item.id, |
| | | })) |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | getRoleList() |
| | | }) |
| | | |
| | | function changePage(page: number, size: number) { |
| | | pageParams.value.pageNum = page |
| | | pageParams.value.pageSize = size |
| | | getList() |
| | | } |
| | | |
| | | const handleDownloadTemplate = async () => { |
| | | const response = await exportOperationsUserTemplate() |
| | | if (response instanceof Blob) { |
| | | downloadFile(response, `运维人员导入模板.xlsx`) |
| | | window.$message.success('导出成功') |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col"> |
| | | <div class="roboTaxi"> |
| | | <n-card style="border-radius: 6px" content-style="padding: 12px"> |
| | | <n-form :model="searchModel" label-placement="left" inline :show-feedback="false"> |
| | | <SearchPanel> |
| | |
| | | </n-gi> |
| | | <template #action> |
| | | <n-button style="width: 84px; height: 32px" @click="handleReset">重置</n-button> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleSearch">查询</n-button> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="getList">查询</n-button> |
| | | </template> |
| | | </SearchPanel> |
| | | </n-form> |
| | | </n-card> |
| | | |
| | | <n-card style="border-radius: 6px;margin-top: 12px;" content-style="padding: 12px"> |
| | | <div class="mb-4 flex justify-end gap-2"> |
| | | <n-button @click="handleExport" style="width: 84px; height: 32px">导出</n-button> |
| | | <n-button @click="handleImport" style="width: 84px; height: 32px">导入</n-button> |
| | | <n-button style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;" @click="handleAdd">新增</n-button> |
| | | </div> |
| | | |
| | | <n-data-table |
| | | :columns="columns" |
| | | :data="personnelList" |
| | | :pagination="{ |
| | | page: currentPage, |
| | | pageSize: pageSize, |
| | | itemCount: total, |
| | | showSizePicker: true, |
| | | pageSizes: [10, 20, 50, 100], |
| | | prefix: ({ itemCount }) => `共 ${itemCount || 0} 条` |
| | | }" |
| | | :scroll-x="1400" |
| | | /> |
| | | <n-card class="flex-1 mt-12px"> |
| | | <template #header> |
| | | <n-flex justify="flex-end"> |
| | | <n-button @click="handleExport" style="width: 84px; height: 32px">导出</n-button> |
| | | <n-button @click="handleImport" style="width: 84px; height: 32px">导入</n-button> |
| | | <n-button @click="handleDownloadTemplate" style="width: 84px; height: 32px">下载模板</n-button> |
| | | <n-button style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;" |
| | | @click="handleAdd">新增</n-button> |
| | | </n-flex> |
| | | </template> |
| | | <NSpace vertical> |
| | | <n-data-table :columns="columns" :data="personnelList" :loading="loading" :bordered="false" :header-height="18" |
| | | :row-height="18" /> |
| | | <Pagination :count="total" @change="changePage" /> |
| | | </NSpace> |
| | | </n-card> |
| | | |
| | | <PersonnelFormModal v-model:show="showFormModal" :mode="formMode" @confirm="handleConfirmForm" /> |
| | | <PersonnelFormModal v-model:show="showFormModal" ref="modalRef" :mode="formMode" :roleOptions="roleOptions" |
| | | @confirm="handleConfirmForm" /> |
| | | <input |
| | | ref="fileInputRef" |
| | | type="file" |
| | | style="display: none" |
| | | accept=".xlsx, .xls" |
| | | @change="handleFileChange" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, computed, onMounted, watch } from 'vue' |
| | | import type { FormInst, FormRules, UploadFileInfo } from 'naive-ui' |
| | | import { useScriptTag } from '@vueuse/core' |
| | | import { getVehicleListNoPage } from '@/service/api/attendance' |
| | | import { customUploadRequest } from '@/utils/upload' |
| | | |
| | | // main.ts 或 index.html 里(高德脚本加载前) |
| | | (window as any)._AMapSecurityConfig = { |
| | | securityJsCode: import.meta.env.VITE_MAP_SECURITY |
| | | } |
| | | |
| | | const { load } = useScriptTag( |
| | | `https://webapi.amap.com/maps?v=2.0&key=${import.meta.env.VITE_MAP_KEY}&plugin=AMap.PlaceSearch,AMap.Geocoder` |
| | | ) |
| | | |
| | | interface FormModel { |
| | | taskDescription: string |
| | | taskImage: UploadFileInfo[] |
| | | taskTypeId: number | null |
| | | taskUrgencyLevel: number | null |
| | | vehicleIds: number[] |
| | | vehicleLat: number | null |
| | | vehicleLon: number | null |
| | | vehicleLocation: string |
| | | } |
| | | |
| | | const props = defineProps<{ |
| | | show: boolean |
| | | mode?: 'add' | 'edit' |
| | | taskErrorIdOptions: any[] |
| | | taskUrgencyLevelOptions: any[] |
| | | }>() |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:show': [value: boolean] |
| | | confirm: [data: FormModel] |
| | | }>() |
| | | |
| | | const show = computed({ |
| | | get: () => props.show, |
| | | set: (value) => emit('update:show', value) |
| | | }) |
| | | |
| | | const formRef = ref<FormInst | null>(null) |
| | | const formModel = ref<FormModel>({ |
| | | taskDescription: '', |
| | | taskImage: [], |
| | | taskTypeId: null, |
| | | taskUrgencyLevel: null, |
| | | vehicleIds: [], |
| | | vehicleLat: null, |
| | | vehicleLon: null, |
| | | vehicleLocation: '' |
| | | }) |
| | | |
| | | const searchKeyword = ref('') |
| | | const mapContainer = ref<HTMLDivElement | null>(null) |
| | | let map: any = null |
| | | let marker: any = null |
| | | let placeSearch: any = null |
| | | |
| | | const rules: FormRules = { |
| | | taskTypeId: [ |
| | | { |
| | | required: true, |
| | | type: 'array', |
| | | message: '请选择任务类型', |
| | | trigger: ['blur', 'change'] |
| | | } |
| | | ], |
| | | taskUrgencyLevel: [ |
| | | { |
| | | required: true, |
| | | type: 'number', |
| | | message: '请选择任务等级', |
| | | trigger: ['blur', 'change'] |
| | | } |
| | | ], |
| | | vehicleIds: [ |
| | | { |
| | | required: true, |
| | | type: 'array', |
| | | message: '请选择车辆', |
| | | trigger: ['blur', 'change'] |
| | | } |
| | | ], |
| | | taskDescription: [ |
| | | { |
| | | required: true, |
| | | message: '请输入任务描述', |
| | | trigger: ['blur', 'change'] |
| | | } |
| | | ], |
| | | vehicleLocation: [ |
| | | { |
| | | required: true, |
| | | validator: (_rule: any, _value: any) => { |
| | | if (formModel.value.vehicleLon === null || formModel.value.vehicleLon === undefined || |
| | | formModel.value.vehicleLat === null || formModel.value.vehicleLat === undefined) { |
| | | return new Error('请在地图上选择位置') |
| | | } |
| | | return true |
| | | }, |
| | | trigger: 'change' |
| | | } |
| | | ] |
| | | } |
| | | |
| | | const taskErrorIdOptions = computed(() => props.taskErrorIdOptions) |
| | | const taskUrgencyLevelOptions = computed(() => props.taskUrgencyLevelOptions) |
| | | const vehicleOptions = ref([]) |
| | | |
| | | const initMap = async () => { |
| | | if (!mapContainer.value) return |
| | | await load() |
| | | if (typeof (window as any).AMap === 'undefined') { |
| | | return |
| | | } |
| | | map = new AMap.Map(mapContainer.value, { |
| | | zoom: 15, |
| | | center: [116.397428, 39.90923], |
| | | mapStyle: 'amap://styles/whitesmoke' |
| | | }) |
| | | |
| | | if (props.mode === 'edit') { |
| | | // 编辑模式下,根据经纬度初始化地图 |
| | | map.setCenter([formModel.value.vehicleLon, formModel.value.vehicleLat]) |
| | | marker = new AMap.Marker({ |
| | | position: [formModel.value.vehicleLon, formModel.value.vehicleLat], |
| | | draggable: true |
| | | }) |
| | | map.add(marker) |
| | | // 监听标记拖拽 |
| | | marker.on('dragend', (e: any) => { |
| | | const position = e.target.getPosition() |
| | | formModel.value.vehicleLon = position.lng |
| | | formModel.value.vehicleLat = position.lat |
| | | |
| | | // 触发验证 |
| | | formRef.value?.validate( |
| | | (errors) => { |
| | | // 验证完成 |
| | | }, |
| | | (rule) => rule?.key === 'location' |
| | | ) |
| | | |
| | | // 逆地理编码获取地址 |
| | | const geocoder = new AMap.Geocoder() |
| | | geocoder.getAddress([position.lng, position.lat], (status: string, result: any) => { |
| | | if (status === 'complete' && result.info === 'OK') { |
| | | formModel.value.vehicleLocation = result.regeocode.formattedAddress |
| | | } |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | // 点击地图添加标记 |
| | | map.on('click', (e: any) => { |
| | | const lng = e.lnglat.lng |
| | | const lat = e.lnglat.lat |
| | | |
| | | formModel.value.vehicleLon = lng |
| | | formModel.value.vehicleLat = lat |
| | | |
| | | // 触发验证 |
| | | formRef.value?.validate( |
| | | (errors) => { |
| | | // 验证完成 |
| | | }, |
| | | (rule) => rule?.key === 'vehicleLocation' |
| | | ) |
| | | |
| | | // 如果标记已存在,更新位置;否则创建新标记 |
| | | if (marker) { |
| | | marker.setPosition([lng, lat]) |
| | | } else { |
| | | marker = new AMap.Marker({ |
| | | position: [lng, lat], |
| | | draggable: true |
| | | }) |
| | | map.add(marker) |
| | | |
| | | // 监听标记拖拽 |
| | | marker.on('dragend', (e: any) => { |
| | | const position = e.target.getPosition() |
| | | formModel.value.vehicleLon = position.lng |
| | | formModel.value.vehicleLat = position.lat |
| | | |
| | | // 触发验证 |
| | | formRef.value?.validate( |
| | | (errors) => { |
| | | // 验证完成 |
| | | }, |
| | | (rule) => rule?.key === 'location' |
| | | ) |
| | | |
| | | // 逆地理编码获取地址 |
| | | const geocoder = new AMap.Geocoder() |
| | | geocoder.getAddress([position.lng, position.lat], (status: string, result: any) => { |
| | | if (status === 'complete' && result.info === 'OK') { |
| | | formModel.value.vehicleLocation = result.regeocode.formattedAddress |
| | | } |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | // 逆地理编码获取地址 |
| | | const geocoder = new AMap.Geocoder() |
| | | geocoder.getAddress([lng, lat], (status: string, result: any) => { |
| | | console.log(result, '逆地理编码结果') |
| | | if (status === 'complete' && result.info === 'OK') { |
| | | formModel.value.vehicleLocation = result.regeocode.formattedAddress |
| | | } |
| | | }) |
| | | }) |
| | | |
| | | // 添加地图搜索 |
| | | placeSearch = new AMap.PlaceSearch({ |
| | | city: '全国' |
| | | }) |
| | | } |
| | | |
| | | const handleSearch = () => { |
| | | if (!searchKeyword.value || !placeSearch) return |
| | | |
| | | placeSearch.search(searchKeyword.value, (status: string, result: any) => { |
| | | console.log(result, '搜索结果') |
| | | if (status === 'complete' && result.poiList && result.poiList.pois.length > 0) { |
| | | const poi = result.poiList.pois[0] |
| | | formModel.value.vehicleLon = poi.location.lng |
| | | formModel.value.vehicleLat = poi.location.lat |
| | | formModel.value.vehicleLocation = poi.name |
| | | |
| | | // 触发验证 |
| | | formRef.value?.validate( |
| | | (errors) => { |
| | | // 验证完成 |
| | | }, |
| | | (rule) => rule?.key === 'location' |
| | | ) |
| | | |
| | | if (marker) { |
| | | marker.setPosition([poi.location.lng, poi.location.lat]) |
| | | } else { |
| | | marker = new AMap.Marker({ |
| | | position: [poi.location.lng, poi.location.lat], |
| | | draggable: true |
| | | }) |
| | | map.add(marker) |
| | | |
| | | // 监听标记拖拽 |
| | | marker.on('dragend', (e: any) => { |
| | | const position = e.target.getPosition() |
| | | formModel.value.vehicleLon = position.lng |
| | | formModel.value.vehicleLat = position.lat |
| | | |
| | | // 触发验证 |
| | | formRef.value?.validate( |
| | | (errors) => { |
| | | // 验证完成 |
| | | }, |
| | | (rule) => rule?.key === 'location' |
| | | ) |
| | | |
| | | // 逆地理编码获取地址 |
| | | const geocoder = new AMap.Geocoder() |
| | | geocoder.getAddress([position.lng, position.lat], (status: string, result: any) => { |
| | | if (status === 'complete' && result.info === 'OK') { |
| | | formModel.value.vehicleLocation = result.regeocode.formattedAddress |
| | | } |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | map.setCenter([poi.location.lng, poi.location.lat]) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const handleCancel = () => { |
| | | show.value = false |
| | | resetForm() |
| | | } |
| | | |
| | | const handleConfirm = async () => { |
| | | try { |
| | | await formRef.value?.validate() |
| | | emit('confirm', formModel.value) |
| | | show.value = false |
| | | resetForm() |
| | | } catch (error) { |
| | | console.log('验证失败', error) |
| | | } |
| | | } |
| | | |
| | | const resetForm = () => { |
| | | formModel.value = { |
| | | taskDescription: '', |
| | | taskImage: [], |
| | | taskTypeId: null, |
| | | taskUrgencyLevel: null, |
| | | vehicleIds: [], |
| | | vehicleLat: null, |
| | | vehicleLon: null, |
| | | vehicleLocation: '' |
| | | } |
| | | if (marker) { |
| | | map.remove(marker) |
| | | marker = null |
| | | } |
| | | searchKeyword.value = '' |
| | | } |
| | | |
| | | const openEdit = (data: any) => { |
| | | formModel.value = { ...data } |
| | | } |
| | | |
| | | defineExpose({ |
| | | openEdit, |
| | | }) |
| | | |
| | | const fetchVehicleList = async () => { |
| | | try { |
| | | const res = await getVehicleListNoPage() |
| | | if (res.code === 200) { |
| | | vehicleOptions.value = res.data.map((item: any) => ({ |
| | | label: item.plateNumber, |
| | | value: item.id |
| | | })) |
| | | } |
| | | } catch (error) { |
| | | console.log('查询车辆不分页列表失败', error) |
| | | } |
| | | } |
| | | |
| | | // 监听弹窗显示状态 |
| | | watch(show, (newVal) => { |
| | | if (newVal) { |
| | | setTimeout(() => { |
| | | initMap() |
| | | fetchVehicleList() |
| | | }, 100) |
| | | } else { |
| | | // 关闭弹窗时清理标记 |
| | | if (marker) { |
| | | map.remove(marker) |
| | | marker = null |
| | | } |
| | | } |
| | | }) |
| | | |
| | | async function handleUploadImages(options: any) { |
| | | const res = await customUploadRequest(options) |
| | | // 上传成功后,更新文件的 id 为后端返回的 id |
| | | if (res?.id) { |
| | | const fileItem = formModel.value.taskImage.find(f => f.id === options.file.id) |
| | | if (fileItem) { |
| | | fileItem.id = res.id |
| | | } |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | // 加载高德地图 |
| | | // initMap() |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal v-model:show="show" :mask-closable="false" preset="card" title="创建事故任务" class="w-1000px" :segmented="{ |
| | | content: true, |
| | | action: true, |
| | | }"> |
| | | <n-form ref="formRef" :model="formModel" :rules="rules" label-placement="top" require-mark-placement="left" |
| | | show-require-mark> |
| | | <div class="grid grid-cols-3 gap-4"> |
| | | <n-form-item label="任务类型" path="taskTypeId"> |
| | | <n-cascader v-model:value="formModel.taskTypeId" :options="taskErrorIdOptions" value-field="id" |
| | | label-field="errorName" children-field="child" check-strategy="all" placeholder="请选择" clearable multiple |
| | | :max-tag-count="1" /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="任务等级" path="taskTypeId"> |
| | | <n-select v-model:value="formModel.taskUrgencyLevel" :options="taskUrgencyLevelOptions" placeholder="请选择" |
| | | clearable /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="车辆" path="vehicleIds"> |
| | | <n-select v-model:value="formModel.vehicleIds" :options="vehicleOptions" placeholder="请选择" clearable multiple |
| | | :max-tag-count="1" /> |
| | | </n-form-item> |
| | | </div> |
| | | <div class="grid grid-cols-1 gap-12px"> |
| | | <n-form-item label="任务描述" path="taskDescription"> |
| | | <n-input v-model:value="formModel.taskDescription" type="textarea" placeholder="请输入" :rows="3" clearable /> |
| | | </n-form-item> |
| | | <n-upload v-model:file-list="formModel.taskImage" :max="10" list-type="image-card" |
| | | :custom-request="handleUploadImages"> |
| | | <div |
| | | style="width: 100px; height: 100px; display: flex; align-items: center; justify-content: center; font-size: 32px; color: #999;"> |
| | | + |
| | | </div> |
| | | </n-upload> |
| | | </div> |
| | | <div class="grid grid-cols-3 gap-4 mt-4"> |
| | | <n-form-item label="车辆位置" path="vehicleLocation"> |
| | | <n-input :value="formModel.vehicleLocation" placeholder="请在下方地图选择位置" disabled /> |
| | | </n-form-item> |
| | | </div> |
| | | <n-form-item label="" path="map"> |
| | | <div style="width: 100%; height: 400px; position: relative"> |
| | | <div ref="mapContainer" style="width: 100%; height: 100%; border-radius: 4px; border: 1px solid #e5e7eb"> |
| | | <div class="flex items-center justify-center h-full text-gray-400"> |
| | | 加载地图中... |
| | | </div> |
| | | </div> |
| | | <div style="position: absolute; top: 10px; left: 10px; z-index: 1000; width: 300px"> |
| | | <n-input v-model:value="searchKeyword" placeholder="搜索地址" clearable @keyup.enter="handleSearch"> |
| | | </n-input> |
| | | </div> |
| | | </div> |
| | | </n-form-item> |
| | | </n-form> |
| | | |
| | | <template #action> |
| | | <n-space justify="end"> |
| | | <n-button @click="handleCancel">取消</n-button> |
| | | <n-button type="primary" @click="handleConfirm">保存</n-button> |
| | | </n-space> |
| | | </template> |
| | | </n-modal> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | :deep(.amap-logo), |
| | | :deep(.amap-copyright) { |
| | | display: none !important; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { useBoolean } from '@/hooks' |
| | | import { getOperationsUserListNoPage, assignTaskOrder } from '@/service/api/attendance' |
| | | import type { FormItemRule } from 'naive-ui' |
| | | |
| | | const emit = defineEmits<{ |
| | | open: [] |
| | | close: [] |
| | | refresh: [] |
| | | }>() |
| | | |
| | | const { bool: modalVisible, setTrue: showModal, setFalse: hiddenModal } = useBoolean(false) |
| | | |
| | | const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | |
| | | const formDefault = { |
| | | handlerUserId: null, |
| | | id: '', |
| | | } |
| | | const formModel = ref({ ...formDefault }) |
| | | |
| | | async function openModal(id: string) { |
| | | emit('open') |
| | | await getUserList() |
| | | formModel.value.id = id |
| | | showModal() |
| | | } |
| | | |
| | | function closeModal() { |
| | | formModel.value = { ...formDefault } |
| | | hiddenModal() |
| | | endLoading() |
| | | emit('close') |
| | | } |
| | | |
| | | defineExpose({ |
| | | openModal, |
| | | }) |
| | | |
| | | const formRef = ref() |
| | | |
| | | async function submitModal() { |
| | | await formRef.value?.validate() |
| | | startLoading() |
| | | // 分配任务 |
| | | const res = await assignTaskOrder(formModel.value) |
| | | if (res.code === 200) { |
| | | window.$message.success('分配成功') |
| | | emit('refresh') |
| | | closeModal() |
| | | } |
| | | endLoading() |
| | | } |
| | | |
| | | const rules = { |
| | | handlerUserId: { |
| | | required: true, |
| | | message: '请选择处理人', |
| | | trigger: 'change', |
| | | } as FormItemRule, |
| | | } |
| | | |
| | | |
| | | const userList = ref<any[]>([]) |
| | | |
| | | const getUserList = async () => { |
| | | const res = await getOperationsUserListNoPage() |
| | | if (res.code === 200) { |
| | | userList.value = res.data || [] |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal v-model:show="modalVisible" :mask-closable="false" preset="card" title="任务分配" class="w-400px" :segmented="{ |
| | | content: true, |
| | | action: true, |
| | | }"> |
| | | <n-form ref="formRef" :rules="rules" :model="formModel" require-mark-placement="left"> |
| | | <n-form-item label="选择处理人" path="handlerUserId"> |
| | | <n-select v-model:value="formModel.handlerUserId" :options="userList" value-field="id" label-field="nickName" /> |
| | | </n-form-item> |
| | | </n-form> |
| | | <template #action> |
| | | <n-space justify="end"> |
| | | <n-button @click="closeModal"> |
| | | 取消 |
| | | </n-button> |
| | | <n-button type="primary" :loading="submitLoading" @click="submitModal"> |
| | | 保存 |
| | | </n-button> |
| | | </n-space> |
| | | </template> |
| | | </n-modal> |
| | | </template> |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns, FormInst } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui' |
| | | import { getTaskAccidentTypeList, getTaskOrderCenterList, deleteTaskOrder, exportTaskOrderCenter, createTaskAccident } from '@/service/api/attendance' |
| | | import { downloadFile } from '@/utils' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import TableModal from './components/TableModal.vue' |
| | | import AddFormModal from './components/AddFormModal.vue' |
| | | import dayjs from 'dayjs' |
| | | |
| | | const router = useRouter() |
| | | |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | const { t } = useI18n() |
| | | |
| | | const initialModel = { |
| | | plateNumber: '', |
| | | taskCategory: null, |
| | | taskErrorId: null, |
| | | taskGeneralId: null, |
| | | taskUrgencyLevel: null, |
| | | time: null, |
| | | status: null, |
| | | } |
| | | const model = ref({ ...initialModel }) |
| | | function handleResetSearch() { |
| | | model.value = { ...initialModel } |
| | | } |
| | | |
| | | const formRef = ref<FormInst | null>() |
| | | const modalRef = ref() |
| | | const showFormModal = ref(false) |
| | | const formMode = ref<'add' | 'edit'>('add') |
| | | |
| | | const pageParams = ref({ |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | }) |
| | | |
| | | function handleDeltete(id: number) { |
| | | deleteTaskOrder(id).then((res) => { |
| | | if (res.code === 200) { |
| | | window.$message.success('删除成功') |
| | | } |
| | | }).finally(() => { |
| | | getList() |
| | | }) |
| | | } |
| | | |
| | | const options = [ |
| | | { label: '常规', value: 1 }, |
| | | { label: '事故', value: 2 }, |
| | | ] |
| | | const taskGeneralOptions = [ |
| | | { 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 }, |
| | | ] |
| | | const taskUrgencyLevelOptions = [ |
| | | { label: '一级', value: 0, type: 'error' }, |
| | | { label: '二级', value: 1, type: 'error' }, |
| | | { label: '三级', value: 2, type: 'error' }, |
| | | { label: '四级', value: 3, type: 'warning' }, |
| | | ] |
| | | const statusOptions = [ |
| | | { label: '待到达', value: 0, type: 'default' }, |
| | | { label: '待处理', value: 1, type: 'warning' }, |
| | | { label: '已完成', value: 2, type: 'primary' }, |
| | | { label: '已取消', value: 3, type: 'default' }, |
| | | ] |
| | | const taskErrorIdOptions = ref<any[]>([]) |
| | | |
| | | const filterItems = computed(() => [ |
| | | { |
| | | label: '车辆', |
| | | path: 'plateNumber', |
| | | render: () => <n-input v-model:value={model.value.plateNumber} />, |
| | | }, |
| | | { |
| | | label: '任务类别', |
| | | path: 'taskCategory', |
| | | render: () => <n-select v-model:value={model.value.taskCategory} options={options} placeholder="请选择" clearable /> |
| | | }, |
| | | { |
| | | label: '常规任务类型', |
| | | path: 'taskGeneralId', |
| | | render: () => <n-select v-model:value={model.value.taskGeneralId} options={taskGeneralOptions} placeholder="请选择" clearable /> |
| | | }, |
| | | { |
| | | label: '事故任务类型', |
| | | path: 'taskErrorId', |
| | | render: () => <n-cascader v-model:value={model.value.taskErrorId} options={taskErrorIdOptions.value} value-field="id" label-field="errorName" children-field="child" check-strategy="all" clearable /> |
| | | }, |
| | | { |
| | | label: '级别', |
| | | path: 'taskUrgencyLevel', |
| | | render: () => <n-select v-model:value={model.value.taskUrgencyLevel} options={taskUrgencyLevelOptions} placeholder="请选择" clearable /> |
| | | }, |
| | | { |
| | | label: '任务状态', |
| | | path: 'status', |
| | | render: () => <n-select v-model:value={model.value.status} options={statusOptions} placeholder="请选择" clearable /> |
| | | }, |
| | | { |
| | | label: '上报时间', |
| | | path: 'time', |
| | | render: () => <n-date-picker v-model:value={model.value.time} type="daterange" clearable /> |
| | | }, |
| | | ]) |
| | | |
| | | const total = ref(0) |
| | | const listData = ref<any[]>([]) |
| | | |
| | | async function getList() { |
| | | startLoading() |
| | | const params = { |
| | | ...pageParams.value, |
| | | status: model.value.status, |
| | | plateNumber: model.value.plateNumber, |
| | | taskCategory: model.value.taskCategory, |
| | | taskGeneralId: model.value.taskGeneralId, |
| | | taskErrorId: model.value.taskErrorId, |
| | | taskUrgencyLevel: model.value.taskUrgencyLevel, |
| | | startTime: model.value.time ? dayjs(model.value.time?.[0]).format('YYYY-MM-DD HH:mm:ss') : null, |
| | | endTime: model.value.time ? dayjs(model.value.time?.[1]).format('YYYY-MM-DD HH:mm:ss') : null, |
| | | } |
| | | getTaskOrderCenterList(params).then((res) => { |
| | | listData.value = res.data.records || [] |
| | | listData.value.forEach((item: any, index: number) => { |
| | | item.serialNumber = index + 1 |
| | | }) |
| | | total.value = res.data.total || res.data.records?.length |
| | | }).finally(() => { |
| | | endLoading() |
| | | }) |
| | | } |
| | | |
| | | const getAccidentTypeList = () => { |
| | | getTaskAccidentTypeList().then((res) => { |
| | | if (res.code === 200) { |
| | | taskErrorIdOptions.value = res.data || [] |
| | | } |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | getAccidentTypeList() |
| | | }) |
| | | |
| | | function changePage(page: number, size: number) { |
| | | pageParams.value.pageNum = page |
| | | pageParams.value.pageSize = size |
| | | getList() |
| | | } |
| | | |
| | | const handleExport = () => { |
| | | const params = { |
| | | ...pageParams.value, |
| | | status: model.value.status, |
| | | plateNumber: model.value.plateNumber, |
| | | taskCategory: model.value.taskCategory, |
| | | taskGeneralId: model.value.taskGeneralId, |
| | | taskErrorId: model.value.taskErrorId, |
| | | taskUrgencyLevel: model.value.taskUrgencyLevel, |
| | | startTime: model.value.time ? dayjs(model.value.time?.[0]).format('YYYY-MM-DD HH:mm:ss') : null, |
| | | endTime: model.value.time ? dayjs(model.value.time?.[1]).format('YYYY-MM-DD HH:mm:ss') : null, |
| | | } |
| | | exportTaskOrderCenter(params).then(res => { |
| | | if (res instanceof Blob) { |
| | | downloadFile(res, `工单中心列表_${new Date().getTime()}.xlsx`) |
| | | window.$message.success('导出成功') |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const handleAdd = () => { |
| | | showFormModal.value = true |
| | | formMode.value = 'add' |
| | | } |
| | | |
| | | const handleConfirmForm = (data: any) => { |
| | | const params = { |
| | | ...data, |
| | | taskTypeId: data.taskTypeId.join(','), |
| | | } |
| | | createTaskAccident(params).then((res) => { |
| | | if (res.code === 200) { |
| | | window.$message.success('新增成功') |
| | | } |
| | | }).finally(() => { |
| | | getList() |
| | | }) |
| | | } |
| | | |
| | | |
| | | const columns = computed<DataTableColumns<any>>(() =>[ |
| | | { |
| | | title: '序号', |
| | | key: 'serialNumber', |
| | | width: 80, |
| | | resizable: true, |
| | | fixed: 'left' |
| | | }, |
| | | { |
| | | title: '车辆', |
| | | key: 'plateNumber', |
| | | width: 116, |
| | | resizable: true, |
| | | fixed: 'left' |
| | | }, |
| | | { |
| | | title: '任务类别', |
| | | key: 'taskCategory', |
| | | width: 80, |
| | | resizable: true, |
| | | render: (row) => { |
| | | return options.find((item) => item.value === row.taskCategory)?.label || '-' |
| | | } |
| | | }, |
| | | { |
| | | title: '类型', |
| | | key: 'taskTypeName', |
| | | width: 200, |
| | | resizable: true, |
| | | ellipsis: { |
| | | tooltip: true, |
| | | }, |
| | | }, |
| | | { |
| | | title: '紧急程度', |
| | | key: 'taskUrgencyLevel', |
| | | width: 100, |
| | | resizable: true, |
| | | render(row) { |
| | | return h( |
| | | NTag, |
| | | { |
| | | type: taskUrgencyLevelOptions.find(item => item.value === row.taskUrgencyLevel)?.type || 'default', |
| | | bordered: false |
| | | }, |
| | | { |
| | | default: () => taskUrgencyLevelOptions.find(item => item.value === row.taskUrgencyLevel)?.label || '-' |
| | | } |
| | | ) |
| | | } |
| | | }, |
| | | { |
| | | title: '任务描述', |
| | | key: 'taskDescription', |
| | | width: 180, |
| | | resizable: true, |
| | | ellipsis: { |
| | | tooltip: true, |
| | | }, |
| | | }, |
| | | { |
| | | title: '上报时间', |
| | | key: 'createTime', |
| | | width: 160, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '处理状态', |
| | | key: 'status', |
| | | width: 100, |
| | | resizable: true, |
| | | render(row) { |
| | | return h( |
| | | NTag, |
| | | { |
| | | type: statusOptions.find(item => item.value === row.status)?.type || 'default', |
| | | bordered: false |
| | | }, |
| | | { |
| | | default: () => statusOptions.find(item => item.value === row.status)?.label || '-' |
| | | } |
| | | ) |
| | | } |
| | | }, |
| | | { |
| | | title: '处理人', |
| | | key: 'handleUserName', |
| | | width: 110, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 140, |
| | | fixed: 'right', |
| | | render: (row) => { |
| | | return ( |
| | | <NSpace> |
| | | {(row.status === 0 || row.status === 1) && <NButton |
| | | text |
| | | type='primary' |
| | | onClick={() => modalRef.value.openModal(row.id)} |
| | | > |
| | | 分配 |
| | | </NButton>} |
| | | <NButton |
| | | text |
| | | type='primary' |
| | | onClick={() => modalRef.value.openModal('edit', row.id)} |
| | | > |
| | | 详情 |
| | | </NButton> |
| | | <NPopconfirm onPositiveClick={() => handleDeltete(row.id!)}> |
| | | {{ |
| | | default: () => '确认删除', |
| | | trigger: () => <NButton text type="error">删除</NButton>, |
| | | }} |
| | | </NPopconfirm> |
| | | </NSpace> |
| | | ) |
| | | }, |
| | | }, |
| | | ]) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="roboTaxi"> |
| | | <n-card style="border-radius: 6px" content-style="padding: 12px"> |
| | | <n-form :model="model" label-placement="left" inline :show-feedback="false"> |
| | | <SearchPanel> |
| | | <n-gi v-for="item in filterItems" :key="item.path"> |
| | | <n-form-item :label="item.label" :path="item.path"> |
| | | <component :is="item.render" /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | <template #action> |
| | | <n-button style="width: 84px; height: 32px" @click="handleResetSearch">重置</n-button> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="getList">查询</n-button> |
| | | </template> |
| | | </SearchPanel> |
| | | </n-form> |
| | | </n-card> |
| | | |
| | | <n-card style="border-radius: 6px; margin-top: 12px" content-style="padding: 12px"> |
| | | <template #header> |
| | | <n-flex justify="flex-end"> |
| | | <n-button @click="handleExport" style="width: 84px; height: 32px">导出</n-button> |
| | | <NButton type="primary" secondary @click="handleAdd"> |
| | | 新增任务 |
| | | </NButton> |
| | | </n-flex> |
| | | </template> |
| | | <NSpace vertical :size="12"> |
| | | <n-data-table :columns="columns" :data="listData" :loading="loading" :bordered="false" :header-height="18" |
| | | :row-height="18" :scroll-x="1300" /> |
| | | <Pagination :count="total" @change="changePage" /> |
| | | </NSpace> |
| | | </n-card> |
| | | <TableModal ref="modalRef" @refresh="getList" /> |
| | | <AddFormModal v-model:show="showFormModal" :mode="formMode" @confirm="handleConfirmForm" :taskErrorIdOptions="taskErrorIdOptions" :taskUrgencyLevelOptions="taskUrgencyLevelOptions"/> |
| | | </div> |
| | | </template> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, h, onMounted } from "vue"; |
| | | import type { DataTableColumns } from "naive-ui"; |
| | | import { |
| | | NButton, |
| | | NImage, |
| | | NImageGroup, |
| | | NModal, |
| | | NSpace, |
| | | NAvatar, |
| | | } from "naive-ui"; |
| | | import { useBoolean } from "@/hooks"; |
| | | import { fetchFeedbackPageList } from "@/service/api/serviceCenter"; |
| | | |
| | | const { |
| | | bool: loading, |
| | | setTrue: startLoading, |
| | | setFalse: endLoading, |
| | | } = useBoolean(false); |
| | | |
| | | const initialSearchForm = { |
| | | feedbackContent: "", |
| | | }; |
| | | const searchForm = ref({ ...initialSearchForm }); |
| | | |
| | | const tableData = ref<any[]>([]); |
| | | |
| | | const pagination = ref({ |
| | | page: 1, |
| | | pageSize: 10, |
| | | itemCount: 0, |
| | | showSizePicker: true, |
| | | pageSizes: [10, 20, 50, 100], |
| | | }); |
| | | |
| | | const previewVisible = ref(false); |
| | | const previewImages = ref<string[]>([]); |
| | | |
| | | function normalizeImages(value: any) { |
| | | if (!value) return []; |
| | | if (Array.isArray(value)) { |
| | | return value.filter((item) => typeof item === "string" && item); |
| | | } |
| | | if (typeof value === "string") { |
| | | const trimmed = value.trim(); |
| | | if (!trimmed) return []; |
| | | if (trimmed.startsWith("[")) { |
| | | try { |
| | | const parsed = JSON.parse(trimmed); |
| | | if (Array.isArray(parsed)) { |
| | | return parsed.filter((item) => typeof item === "string" && item); |
| | | } |
| | | } catch { |
| | | return [trimmed]; |
| | | } |
| | | } |
| | | return trimmed.includes(",") |
| | | ? trimmed |
| | | .split(",") |
| | | .map((item) => item.trim()) |
| | | .filter(Boolean) |
| | | : [trimmed]; |
| | | } |
| | | return []; |
| | | } |
| | | |
| | | function getImages(row: any) { |
| | | return normalizeImages(row.feedbackPictures); |
| | | } |
| | | |
| | | const columns: DataTableColumns<FeedbackItem> = [ |
| | | { |
| | | title: "序号", |
| | | key: "index", |
| | | width: 80, |
| | | align: "center", |
| | | render: (_row, index) => |
| | | h( |
| | | "span", |
| | | (pagination.value.page - 1) * pagination.value.pageSize + index + 1 |
| | | ), |
| | | }, |
| | | { |
| | | title: "反馈人", |
| | | key: "nickName", |
| | | width: 120, |
| | | align: "center", |
| | | render: (row) => { |
| | | const displayName = row.nickName || "-"; |
| | | return h("div", { class: "flex items-center justify-center gap-2" }, [ |
| | | h( |
| | | NAvatar, |
| | | { |
| | | round: true, |
| | | size: 32, |
| | | style: { backgroundColor: "#e0e0e0" }, |
| | | src: row.avatar || undefined, |
| | | }, |
| | | { default: () => displayName.charAt(0) } |
| | | ), |
| | | h("span", displayName), |
| | | ]); |
| | | }, |
| | | }, |
| | | { |
| | | title: "反馈类型", |
| | | key: "feedbackTypeName", |
| | | width: 120, |
| | | align: "center", |
| | | }, |
| | | { |
| | | title: "反馈内容", |
| | | key: "feedbackContent", |
| | | minWidth: 200, |
| | | align: "center", |
| | | }, |
| | | { |
| | | title: "图片", |
| | | key: "feedbackPictures", |
| | | width: 120, |
| | | align: "center", |
| | | render: (row) => { |
| | | const images = getImages(row); |
| | | if (!images.length) return h("span", "-"); |
| | | return h( |
| | | NButton, |
| | | { |
| | | text: true, |
| | | type: "primary", |
| | | onClick: () => handleViewImages(images), |
| | | }, |
| | | { default: () => "查看" } |
| | | ); |
| | | }, |
| | | }, |
| | | { |
| | | title: "联系方式", |
| | | key: "phoneNumber", |
| | | width: 140, |
| | | align: "center", |
| | | }, |
| | | { |
| | | title: "反馈时间", |
| | | key: "createTime", |
| | | width: 160, |
| | | align: "center", |
| | | }, |
| | | ]; |
| | | |
| | | const handleSearch = () => { |
| | | pagination.value.page = 1; |
| | | getFeedbackList(); |
| | | }; |
| | | |
| | | const handleReset = () => { |
| | | searchForm.value = { ...initialSearchForm }; |
| | | pagination.value.page = 1; |
| | | getFeedbackList(); |
| | | }; |
| | | |
| | | function handleViewImages(images: string[]) { |
| | | previewImages.value = images; |
| | | previewVisible.value = true; |
| | | } |
| | | |
| | | function handlePageChange(page: number) { |
| | | pagination.value.page = page; |
| | | getFeedbackList(); |
| | | } |
| | | |
| | | function handlePageSizeChange(pageSize: number) { |
| | | pagination.value.pageSize = pageSize; |
| | | pagination.value.page = 1; |
| | | getFeedbackList(); |
| | | } |
| | | |
| | | async function getFeedbackList() { |
| | | startLoading(); |
| | | try { |
| | | const res = await fetchFeedbackPageList({ |
| | | content: searchForm.value.feedbackContent || undefined, |
| | | pageNum: pagination.value.page, |
| | | pageSize: pagination.value.pageSize, |
| | | }); |
| | | if (res.code === 200 && res.data) { |
| | | tableData.value = res.data.records || []; |
| | | pagination.value.itemCount = res.data.total || 0; |
| | | } else { |
| | | tableData.value = []; |
| | | pagination.value.itemCount = 0; |
| | | } |
| | | } finally { |
| | | endLoading(); |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getFeedbackList(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col"> |
| | | <n-card style="border-radius: 6px" content-style="padding: 12px"> |
| | | <n-form |
| | | :model="searchForm" |
| | | label-placement="left" |
| | | inline |
| | | :show-feedback="false" |
| | | > |
| | | <div class="flex justify-between items-center w-full"> |
| | | <div class="flex gap-4"> |
| | | <n-form-item label="反馈内容"> |
| | | <n-input |
| | | v-model:value="searchForm.feedbackContent" |
| | | placeholder="请输入" |
| | | clearable |
| | | style="width: 200px" |
| | | /> |
| | | </n-form-item> |
| | | </div> |
| | | <div class="flex gap-2"> |
| | | <n-button style="width: 84px; height: 32px" @click="handleReset" |
| | | >重置</n-button |
| | | > |
| | | <n-button |
| | | type="primary" |
| | | style="width: 84px; height: 32px" |
| | | @click="handleSearch" |
| | | >查询</n-button |
| | | > |
| | | </div> |
| | | </div> |
| | | </n-form> |
| | | </n-card> |
| | | |
| | | <n-card |
| | | style="border-radius: 6px; margin-top: 12px" |
| | | content-style="padding: 12px" |
| | | > |
| | | <n-data-table |
| | | :columns="columns" |
| | | :data="tableData" |
| | | :loading="loading" |
| | | :pagination="{ |
| | | page: pagination.page, |
| | | pageSize: pagination.pageSize, |
| | | itemCount: pagination.itemCount, |
| | | showSizePicker: true, |
| | | pageSizes: pagination.pageSizes, |
| | | prefix: ({ itemCount }) => `共 ${itemCount || 0} 条`, |
| | | }" |
| | | :scroll-x="1200" |
| | | :row-key="(row: FeedbackItem) => row.id" |
| | | @update:page="handlePageChange" |
| | | @update:page-size="handlePageSizeChange" |
| | | /> |
| | | </n-card> |
| | | |
| | | <n-modal |
| | | v-model:show="previewVisible" |
| | | preset="card" |
| | | title="图片预览" |
| | | :style="{ width: '720px' }" |
| | | :bordered="false" |
| | | :segmented="{ content: true, action: true }" |
| | | > |
| | | <n-image-group v-if="previewImages.length"> |
| | | <n-space wrap> |
| | | <n-image |
| | | v-for="(img, index) in previewImages" |
| | | :key="index" |
| | | :src="img" |
| | | :width="120" |
| | | :height="90" |
| | | object-fit="cover" |
| | | /> |
| | | </n-space> |
| | | </n-image-group> |
| | | <span v-else>-</span> |
| | | </n-modal> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | </style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, h, computed, onMounted } from "vue"; |
| | | import type { DataTableColumns, FormInst, FormRules } from "naive-ui"; |
| | | import { NButton } from "naive-ui"; |
| | | import { useBoolean } from "@/hooks"; |
| | | import { |
| | | fetchFeedbackTypeList, |
| | | saveFeedbackType, |
| | | updateFeedbackType, |
| | | deleteFeedbackType, |
| | | } from "@/service/api/serviceCenter"; |
| | | |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = |
| | | useBoolean(false); |
| | | |
| | | const tableData = ref<any[]>([]); |
| | | |
| | | const modalVisible = ref(false); |
| | | const modalMode = ref<"add" | "edit">("add"); |
| | | const formRef = ref<FormInst | null>(null); |
| | | const formModel = ref({ |
| | | id: null as number | null, |
| | | feedbackType: "", |
| | | }); |
| | | |
| | | const rules: FormRules = { |
| | | feedbackType: [{ required: true, message: "请输入反馈类型", trigger: "blur" }], |
| | | }; |
| | | |
| | | const modalTitle = computed(() => |
| | | modalMode.value === "add" ? "新增反馈类型" : "编辑反馈类型" |
| | | ); |
| | | |
| | | const columns: DataTableColumns<any> = [ |
| | | { |
| | | title: "序号", |
| | | key: "index", |
| | | width: 80, |
| | | align: "center", |
| | | render: (_row, index) => h("span", index + 1), |
| | | }, |
| | | { |
| | | title: "反馈类型", |
| | | key: "feedbackType", |
| | | minWidth: 200, |
| | | align: "center", |
| | | }, |
| | | { |
| | | title: "创建时间", |
| | | key: "createTime", |
| | | width: 180, |
| | | align: "center", |
| | | render: (row) => row.createTime || "-", |
| | | }, |
| | | { |
| | | title: "操作", |
| | | key: "actions", |
| | | width: 160, |
| | | align: "center", |
| | | render: (row) => |
| | | h("div", { class: "flex items-center justify-center gap-2" }, [ |
| | | h( |
| | | NButton, |
| | | { |
| | | text: true, |
| | | type: "primary", |
| | | onClick: () => handleEdit(row), |
| | | }, |
| | | { default: () => "编辑" } |
| | | ), |
| | | h( |
| | | NButton, |
| | | { |
| | | text: true, |
| | | type: "error", |
| | | onClick: () => handleDelete(row), |
| | | }, |
| | | { default: () => "删除" } |
| | | ), |
| | | ]), |
| | | }, |
| | | ]; |
| | | |
| | | function handleAdd() { |
| | | modalMode.value = "add"; |
| | | formModel.value = { id: null, feedbackType: "" }; |
| | | modalVisible.value = true; |
| | | formRef.value?.restoreValidation(); |
| | | } |
| | | |
| | | function handleEdit(row: FeedbackTypeItem) { |
| | | modalMode.value = "edit"; |
| | | formModel.value = { id: row.id, feedbackType: row.feedbackType || "" }; |
| | | modalVisible.value = true; |
| | | formRef.value?.restoreValidation(); |
| | | } |
| | | |
| | | function handleClose() { |
| | | modalVisible.value = false; |
| | | } |
| | | |
| | | async function handleSave() { |
| | | await formRef.value?.validate(); |
| | | const payload = { feedbackType: formModel.value.feedbackType }; |
| | | if (modalMode.value === "add") { |
| | | const { error } = await saveFeedbackType(payload); |
| | | if (!error) { |
| | | window.$message?.success("新增成功"); |
| | | modalVisible.value = false; |
| | | getFeedbackTypeList(); |
| | | } else { |
| | | window.$message?.error("新增失败"); |
| | | } |
| | | return; |
| | | } |
| | | const { error } = await updateFeedbackType({ |
| | | id: formModel.value.id as number, |
| | | ...payload, |
| | | }); |
| | | if (!error) { |
| | | window.$message?.success("编辑成功"); |
| | | modalVisible.value = false; |
| | | getFeedbackTypeList(); |
| | | } else { |
| | | window.$message?.error("编辑失败"); |
| | | } |
| | | } |
| | | |
| | | function handleDelete(row: FeedbackTypeItem) { |
| | | window.$dialog?.warning({ |
| | | title: "确认删除", |
| | | content: "确定要删除该反馈类型吗?", |
| | | positiveText: "确定", |
| | | negativeText: "取消", |
| | | onPositiveClick: async () => { |
| | | const { error } = await deleteFeedbackType(row.id); |
| | | if (!error) { |
| | | window.$message?.success("删除成功"); |
| | | getFeedbackTypeList(); |
| | | } else { |
| | | window.$message?.error("删除失败"); |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | async function getFeedbackTypeList() { |
| | | startLoading(); |
| | | try { |
| | | const res = await fetchFeedbackTypeList(); |
| | | if (res.code === 200 && res.data) { |
| | | const list = Array.isArray(res.data) |
| | | ? res.data |
| | | : res.data.records || []; |
| | | tableData.value = list; |
| | | } else { |
| | | tableData.value = []; |
| | | } |
| | | } finally { |
| | | endLoading(); |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getFeedbackTypeList(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col"> |
| | | <n-card |
| | | style="border-radius: 6px; margin-top: 12px" |
| | | content-style="padding: 12px" |
| | | > |
| | | <div class="mb-4 flex justify-end gap-2"> |
| | | <n-button |
| | | style=" |
| | | width: 84px; |
| | | height: 32px; |
| | | color: #0068ff; |
| | | background-color: #e6f4ff; |
| | | border: 1px solid #7abdff; |
| | | " |
| | | @click="handleAdd" |
| | | >新增</n-button |
| | | > |
| | | </div> |
| | | |
| | | <n-data-table |
| | | :columns="columns" |
| | | :data="tableData" |
| | | :loading="loading" |
| | | :pagination="false" |
| | | :scroll-x="800" |
| | | :row-key="(row: FeedbackTypeItem) => row.id" |
| | | /> |
| | | </n-card> |
| | | |
| | | <n-modal |
| | | v-model:show="modalVisible" |
| | | preset="card" |
| | | :title="modalTitle" |
| | | :mask-closable="false" |
| | | :style="{ width: '420px' }" |
| | | :segmented="{ content: true, action: true }" |
| | | > |
| | | <n-form |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="left" |
| | | label-width="80" |
| | | require-mark-placement="left" |
| | | > |
| | | <n-form-item label="反馈类型" path="feedbackType"> |
| | | <n-input |
| | | v-model:value="formModel.feedbackType" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </n-form-item> |
| | | </n-form> |
| | | |
| | | <template #action> |
| | | <n-space justify="end"> |
| | | <n-button @click="handleClose">取消</n-button> |
| | | <n-button type="primary" @click="handleSave">保存</n-button> |
| | | </n-space> |
| | | </template> |
| | | </n-modal> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped></style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, ref, watch } from "vue"; |
| | | import type { FormInst, FormRules, UploadFileInfo } from "naive-ui"; |
| | | import { customUploadRequest } from "@/utils/upload"; |
| | | |
| | | interface HandleFileItem { |
| | | id: string; |
| | | name: string; |
| | | url: string; |
| | | } |
| | | |
| | | interface Props { |
| | | visible: boolean; |
| | | rowData?: any; |
| | | typeOptions: { label: string; value: number }[]; |
| | | problemSourceOptions: { label: string; value: number }[]; |
| | | priorityOptions: { label: string; value: number }[]; |
| | | handleUserOptions: { label: string; value: number | string }[]; |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | visible: false, |
| | | rowData: null, |
| | | typeOptions: () => [], |
| | | problemSourceOptions: () => [], |
| | | priorityOptions: () => [], |
| | | handleUserOptions: () => [], |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:visible", "submit"]); |
| | | |
| | | const showModal = computed({ |
| | | get: () => props.visible, |
| | | set: (val) => emit("update:visible", val), |
| | | }); |
| | | |
| | | const formRef = ref<FormInst | null>(null); |
| | | const formModel = ref({ |
| | | handleLog: "", |
| | | handleUserId: null as number | string | null, |
| | | handleFile: [] as HandleFileItem[], |
| | | }); |
| | | const fileList = ref<UploadFileInfo[]>([]); |
| | | |
| | | const rules: FormRules = { |
| | | handleLog: [{ required: true, message: "请输入处理情况", trigger: "blur" }], |
| | | }; |
| | | |
| | | const disableSave = computed(() => !formModel.value.handleUserId); |
| | | const disableClose = computed(() => !!formModel.value.handleUserId); |
| | | |
| | | const displayType = computed(() => { |
| | | const id = props.rowData?.feedbackTypeId; |
| | | const matched = props.typeOptions.find((item) => item.value === id); |
| | | return matched?.label || props.rowData?.feedbackTypeName || "-"; |
| | | }); |
| | | |
| | | const displayProblemSource = computed(() => { |
| | | const id = props.rowData?.problemSource; |
| | | const matched = props.problemSourceOptions.find((item) => item.value === id); |
| | | return matched?.label || "-"; |
| | | }); |
| | | |
| | | const displayPriority = computed(() => { |
| | | const id = props.rowData?.priority; |
| | | const matched = props.priorityOptions.find((item) => item.value === id); |
| | | return matched?.label || "-"; |
| | | }); |
| | | |
| | | const displayImages = computed<HandleFileItem[]>(() => { |
| | | const images = props.rowData?.problemImage; |
| | | return images ? JSON.parse(images) : []; |
| | | }); |
| | | |
| | | function resetForm() { |
| | | formModel.value = { |
| | | handleLog: "", |
| | | handleUserId: null, |
| | | handleFile: [], |
| | | }; |
| | | fileList.value = []; |
| | | formRef.value?.restoreValidation(); |
| | | } |
| | | |
| | | async function handleUploadFile({ file, onFinish, onError, onProgress }: any) { |
| | | const res = await customUploadRequest({ |
| | | file, |
| | | onFinish, |
| | | onError, |
| | | onProgress, |
| | | }); |
| | | if (res) { |
| | | file.id = res.id; |
| | | file.url = res.url; |
| | | file.status = "finished"; |
| | | const nextItem = { |
| | | id: String(res.id || file.id || Date.now()), |
| | | name: file.name || "附件", |
| | | url: res.url, |
| | | }; |
| | | formModel.value.handleFile = [ |
| | | ...formModel.value.handleFile.filter((item) => item.id !== nextItem.id), |
| | | nextItem, |
| | | ]; |
| | | onFinish(); |
| | | } |
| | | } |
| | | |
| | | function handleRemoveFile( |
| | | _file: UploadFileInfo, |
| | | fileListValue: UploadFileInfo[] |
| | | ) { |
| | | const ids = new Set( |
| | | fileListValue |
| | | .map((item) => String(item.id)) |
| | | .filter((id) => id && id !== "undefined") |
| | | ); |
| | | formModel.value.handleFile = formModel.value.handleFile.filter((item) => |
| | | ids.has(String(item.id)) |
| | | ); |
| | | } |
| | | |
| | | async function handleSave() { |
| | | await formRef.value?.validate(); |
| | | emit("submit", { |
| | | handleType: 1, |
| | | handleLog: formModel.value.handleLog, |
| | | handleUserId: formModel.value.handleUserId, |
| | | handleFile: JSON.stringify(formModel.value.handleFile), |
| | | }); |
| | | } |
| | | |
| | | async function handleClose() { |
| | | await formRef.value?.validate(); |
| | | emit("submit", { |
| | | handleType: 2, |
| | | handleLog: formModel.value.handleLog, |
| | | handleUserId: null, |
| | | handleFile: JSON.stringify(formModel.value.handleFile), |
| | | }); |
| | | } |
| | | |
| | | watch( |
| | | () => props.visible, |
| | | (visible) => { |
| | | if (visible) { |
| | | resetForm(); |
| | | } |
| | | } |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal |
| | | v-model:show="showModal" |
| | | preset="card" |
| | | title="处理工单" |
| | | :mask-closable="false" |
| | | :style="{ width: '860px' }" |
| | | :segmented="{ content: true, action: true }" |
| | | > |
| | | <div class="space-y-4"> |
| | | <div class="rounded border border-[#E6E6E6] p-3"> |
| | | <div class="grid grid-cols-2 gap-4 text-sm text-[#1D2129]"> |
| | | <div>问题:{{ props.rowData?.problem || "-" }}</div> |
| | | <div>反馈用户:{{ props.rowData?.feedbackUserPhone || "-" }}</div> |
| | | <div>来源:{{ displayProblemSource }}</div> |
| | | <div>类型:{{ displayType }}</div> |
| | | <div>优先级:{{ displayPriority }}</div> |
| | | </div> |
| | | <div class="mt-3 flex text-sm text-[#1D2129]"> |
| | | <div>问题描述:</div> |
| | | <div> |
| | | <div class="whitespace-pre-wrap text-[#4E5969]"> |
| | | {{ props.rowData?.problemDescription || "-" }} |
| | | </div> |
| | | <div v-if="displayImages.length" class="mt-3"> |
| | | <n-image-group> |
| | | <div class="flex flex-wrap gap-2"> |
| | | <n-image |
| | | v-for="item in displayImages" |
| | | :key="item.id || item.url" |
| | | :src="item.url" |
| | | width="80" |
| | | height="80" |
| | | /> |
| | | </div> |
| | | </n-image-group> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <n-form |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="left" |
| | | label-width="90" |
| | | require-mark-placement="left" |
| | | > |
| | | <div class="text-sm font-medium text-[#1D2129] mb-2">处理日志</div> |
| | | <n-form-item label="处理情况" path="handleLog"> |
| | | <n-input |
| | | v-model:value="formModel.handleLog" |
| | | type="textarea" |
| | | placeholder="请输入" |
| | | :rows="4" |
| | | clearable |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="相关资料" path="handleFile" :show-feedback="false"> |
| | | <div class="w-full"> |
| | | <n-upload |
| | | v-model:file-list="fileList" |
| | | list-type="text" |
| | | :max="10" |
| | | accept="image/*,audio/*,video/*" |
| | | :custom-request="handleUploadFile" |
| | | @remove="handleRemoveFile" |
| | | > |
| | | <n-button>上传文件</n-button> |
| | | </n-upload> |
| | | <div class="mt-2 text-xs text-[#86909C]"> |
| | | 上传通话录音、处理结果等资料。上传文件类型可选图片、音频、视频 |
| | | </div> |
| | | </div> |
| | | </n-form-item> |
| | | <n-form-item label="移交" path="handleUserId"> |
| | | <n-select |
| | | v-model:value="formModel.handleUserId" |
| | | :options="handleUserOptions" |
| | | placeholder="请选择" |
| | | clearable |
| | | /> |
| | | </n-form-item> |
| | | </n-form> |
| | | </div> |
| | | |
| | | <template #action> |
| | | <n-space justify="end"> |
| | | <n-button @click="showModal = false">取消</n-button> |
| | | <n-button v-if="!disableClose" type="primary" @click="handleClose"> |
| | | 结办 |
| | | </n-button> |
| | | <n-button v-if="!disableSave" type="primary" @click="handleSave"> |
| | | 保存 |
| | | </n-button> |
| | | </n-space> |
| | | </template> |
| | | </n-modal> |
| | | </template> |
| | | |
| | | <style scoped></style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, ref, watch } from "vue"; |
| | | import type { FormInst, FormRules, UploadFileInfo } from "naive-ui"; |
| | | import { customUploadRequest } from "@/utils/upload"; |
| | | |
| | | interface WorkOrderFormModel { |
| | | id?: number | null; |
| | | problem: string; |
| | | problemDescription: string; |
| | | problemImage: { id: string; name: string; url: string }[]; |
| | | feedbackUserPhone: number | string; |
| | | problemSource: number | null; |
| | | feedbackTypeId: number | null; |
| | | priority: number | null; |
| | | handleUserId: number | string | null; |
| | | } |
| | | |
| | | interface Props { |
| | | visible: boolean; |
| | | mode: "add" | "edit"; |
| | | rowData?: any; |
| | | typeOptions: { label: string; value: number }[]; |
| | | problemSourceOptions: { label: string; value: number }[]; |
| | | priorityOptions: { label: string; value: number }[]; |
| | | handleUserOptions: { label: string; value: number | string }[]; |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | visible: false, |
| | | mode: "add", |
| | | rowData: null, |
| | | typeOptions: () => [], |
| | | problemSourceOptions: () => [], |
| | | priorityOptions: () => [], |
| | | handleUserOptions: () => [], |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:visible", "submit"]); |
| | | |
| | | const showModal = computed({ |
| | | get: () => props.visible, |
| | | set: (val) => emit("update:visible", val), |
| | | }); |
| | | |
| | | const formRef = ref<FormInst | null>(null); |
| | | const formModel = ref<WorkOrderFormModel>({ |
| | | id: null, |
| | | problem: "", |
| | | problemDescription: "", |
| | | problemImage: [], |
| | | feedbackUserPhone: "", |
| | | problemSource: null, |
| | | feedbackTypeId: null, |
| | | priority: null, |
| | | handleUserId: null, |
| | | }); |
| | | const fileList = ref<UploadFileInfo[]>([]); |
| | | |
| | | const rules: FormRules = { |
| | | problem: [{ required: true, message: "请输入问题", trigger: "blur" }], |
| | | problemDescription: [ |
| | | { required: true, message: "请输入问题描述", trigger: "blur" }, |
| | | ], |
| | | feedbackUserPhone: [ |
| | | { required: true, message: "请输入反馈用户", trigger: "blur" }, |
| | | ], |
| | | problemSource: [ |
| | | { |
| | | validator: (_rule, value) => |
| | | value === null || value === undefined || value === "" |
| | | ? new Error("请选择来源") |
| | | : true, |
| | | trigger: ["change", "blur"], |
| | | }, |
| | | ], |
| | | feedbackTypeId: [ |
| | | { |
| | | validator: (_rule, value) => |
| | | value === null || value === undefined || value === "" |
| | | ? new Error("请选择类型") |
| | | : true, |
| | | trigger: ["change", "blur"], |
| | | }, |
| | | ], |
| | | priority: [ |
| | | { |
| | | validator: (_rule, value) => |
| | | value === null || value === undefined || value === "" |
| | | ? new Error("请选择优先级") |
| | | : true, |
| | | trigger: ["change", "blur"], |
| | | }, |
| | | ], |
| | | handleUserId: [ |
| | | { |
| | | validator: (_rule, value) => |
| | | value === null || value === undefined || value === "" |
| | | ? new Error("请选择处理人") |
| | | : true, |
| | | trigger: ["change", "blur"], |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | | const modalTitle = computed(() => |
| | | props.mode === "add" ? "新增工单" : "编辑工单" |
| | | ); |
| | | |
| | | function resetForm() { |
| | | formModel.value = { |
| | | id: null, |
| | | problem: "", |
| | | problemDescription: "", |
| | | problemImage: [], |
| | | feedbackUserPhone: "", |
| | | problemSource: null, |
| | | feedbackTypeId: null, |
| | | priority: null, |
| | | handleUserId: null, |
| | | }; |
| | | fileList.value = []; |
| | | formRef.value?.restoreValidation(); |
| | | } |
| | | |
| | | function fillForm() { |
| | | const row = props.rowData || {}; |
| | | formModel.value = { |
| | | problem: row.problem || "", |
| | | problemDescription: row.problemDescription || "", |
| | | problemImage: row.problemImage ? JSON.parse(row.problemImage) : [], |
| | | feedbackUserPhone: row.feedbackUserPhone || "", |
| | | problemSource: row.problemSource ?? null, |
| | | feedbackTypeId: row.feedbackTypeId ?? null, |
| | | priority: row.priority ?? null, |
| | | handleUserId: row.handleUserId || null, |
| | | }; |
| | | if (row.id) { |
| | | formModel.value.id = row.id; |
| | | } |
| | | fileList.value = formModel.value.problemImage.map((item) => ({ |
| | | id: item.id || `image_${row.id || Date.now()}`, |
| | | name: item.name || "图片", |
| | | status: "finished", |
| | | url: item.url, |
| | | })); |
| | | formRef.value?.restoreValidation(); |
| | | } |
| | | |
| | | async function handleUploadImage({ file, onFinish, onError, onProgress }: any) { |
| | | const res = await customUploadRequest({ |
| | | file, |
| | | onFinish, |
| | | onError, |
| | | onProgress, |
| | | }); |
| | | if (res) { |
| | | file.id = res.id; |
| | | file.url = res.url; |
| | | file.status = "finished"; |
| | | const nextItem = { |
| | | id: String(res.id || file.id || Date.now()), |
| | | name: file.name || "附件", |
| | | url: res.url, |
| | | }; |
| | | formModel.value.problemImage = [ |
| | | ...formModel.value.problemImage.filter((item) => item.id !== nextItem.id), |
| | | nextItem, |
| | | ]; |
| | | onFinish(); |
| | | } |
| | | } |
| | | |
| | | function handleRemoveImage( |
| | | _file: UploadFileInfo, |
| | | fileListValue: UploadFileInfo[] |
| | | ) { |
| | | const ids = new Set( |
| | | fileListValue |
| | | .map((item) => String(item.id)) |
| | | .filter((id) => id && id !== "undefined") |
| | | ); |
| | | formModel.value.problemImage = formModel.value.problemImage.filter((item) => |
| | | ids.has(String(item.id)) |
| | | ); |
| | | } |
| | | |
| | | async function handleSave() { |
| | | await formRef.value?.validate(); |
| | | emit("submit", { |
| | | ...formModel.value, |
| | | problemImage: JSON.stringify(formModel.value.problemImage), |
| | | }); |
| | | } |
| | | |
| | | watch( |
| | | () => props.visible, |
| | | (visible) => { |
| | | if (!visible) return; |
| | | if (props.mode === "edit") { |
| | | fillForm(); |
| | | } else { |
| | | resetForm(); |
| | | } |
| | | } |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal |
| | | v-model:show="showModal" |
| | | preset="card" |
| | | :title="modalTitle" |
| | | :mask-closable="false" |
| | | :style="{ width: '720px' }" |
| | | :segmented="{ content: true, action: true }" |
| | | > |
| | | <n-form |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="left" |
| | | label-width="90" |
| | | require-mark-placement="left" |
| | | > |
| | | <n-form-item label="问题" path="problem"> |
| | | <n-input |
| | | v-model:value="formModel.problem" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="问题描述" path="problemDescription"> |
| | | <n-input |
| | | v-model:value="formModel.problemDescription" |
| | | type="textarea" |
| | | placeholder="请输入" |
| | | :rows="4" |
| | | clearable |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="图片" path="problemImage"> |
| | | <div class="flex flex-col"> |
| | | <n-upload |
| | | v-model:file-list="fileList" |
| | | list-type="image-card" |
| | | :max="10" |
| | | :custom-request="handleUploadImage" |
| | | @remove="handleRemoveImage" |
| | | accept="image/*" |
| | | > |
| | | <div |
| | | style=" |
| | | width: 120px; |
| | | height: 120px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: #999; |
| | | " |
| | | > |
| | | <icon-park-outline-plus style="font-size: 20px" /> |
| | | <div style="margin-top: 8px; font-size: 12px">点击上传图片</div> |
| | | </div> |
| | | </n-upload> |
| | | <div style="margin-top: 8px; color: #999; font-size: 12px"> |
| | | 支持jpg、png 格式 建议尺寸:X px Xpx<br /> |
| | | </div> |
| | | </div> |
| | | </n-form-item> |
| | | <n-form-item label="反馈用户" path="feedbackUserPhone"> |
| | | <n-input |
| | | v-model:value="formModel.feedbackUserPhone" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="来源" path="problemSource" required> |
| | | <n-select |
| | | v-model:value="formModel.problemSource" |
| | | :options="problemSourceOptions" |
| | | placeholder="请选择" |
| | | clearable |
| | | /> |
| | | </n-form-item> |
| | | <div class="grid grid-cols-2 gap-4"> |
| | | <n-form-item label="类型" path="feedbackTypeId" required> |
| | | <n-select |
| | | v-model:value="formModel.feedbackTypeId" |
| | | :options="typeOptions" |
| | | placeholder="请选择" |
| | | clearable |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="优先级" path="priority" required> |
| | | <n-select |
| | | v-model:value="formModel.priority" |
| | | :options="priorityOptions" |
| | | placeholder="请选择" |
| | | clearable |
| | | /> |
| | | </n-form-item> |
| | | </div> |
| | | <n-form-item label="处理人" path="handleUserId" required> |
| | | <n-select |
| | | v-model:value="formModel.handleUserId" |
| | | :options="handleUserOptions" |
| | | placeholder="请选择" |
| | | clearable |
| | | /> |
| | | </n-form-item> |
| | | </n-form> |
| | | |
| | | <template #action> |
| | | <n-space justify="end"> |
| | | <n-button @click="showModal = false">取消</n-button> |
| | | <n-button type="primary" @click="handleSave">保存</n-button> |
| | | </n-space> |
| | | </template> |
| | | </n-modal> |
| | | </template> |
| | | |
| | | <style scoped></style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, ref, onMounted } from "vue"; |
| | | import { useRoute, useRouter } from "vue-router"; |
| | | import { useBoolean } from "@/hooks"; |
| | | import { fetchWorkOrderDetail } from "@/service/api/serviceCenter"; |
| | | |
| | | const route = useRoute(); |
| | | const router = useRouter(); |
| | | const { |
| | | bool: loading, |
| | | setTrue: startLoading, |
| | | setFalse: endLoading, |
| | | } = useBoolean(false); |
| | | |
| | | const detailData = ref<any>({}); |
| | | const workOrderId = computed(() => route.query.id); |
| | | |
| | | const problemSourceLabelMap: Record<number, string> = { |
| | | 1: "电话客服", |
| | | }; |
| | | const priorityLabelMap: Record<number, string> = { |
| | | 1: "低", |
| | | 2: "中", |
| | | 3: "高", |
| | | }; |
| | | |
| | | const displayProblemSource = computed( |
| | | () => problemSourceLabelMap[detailData.value.problemSource] || "-" |
| | | ); |
| | | const displayPriority = computed( |
| | | () => priorityLabelMap[detailData.value.priority] || "-" |
| | | ); |
| | | const displayType = computed(() => detailData.value.feedbackTypeName || "-"); |
| | | const displayImages = computed(() => |
| | | parseFileList(detailData.value.problemImage) |
| | | ); |
| | | |
| | | const timelineItems = computed(() => { |
| | | const items: any[] = []; |
| | | if (detailData.value.createTime) { |
| | | items.push({ |
| | | title: detailData.value.createTime, |
| | | handleUserName: detailData.value.createBy, |
| | | typeTxt: "创建工单", |
| | | }); |
| | | } |
| | | const logs = Array.isArray(detailData.value.workOrderLogs) |
| | | ? detailData.value.workOrderLogs |
| | | : []; |
| | | logs.forEach((log: any) => { |
| | | items.push({ |
| | | title: log.handleTime || log.createTime || "-", |
| | | handleUserName: log.handleUserName || "-", |
| | | typeTxt: "处理", |
| | | log, |
| | | }); |
| | | }); |
| | | return items; |
| | | }); |
| | | |
| | | function parseFileList(value: any) { |
| | | if (!value) return []; |
| | | if (Array.isArray(value)) return value; |
| | | if (typeof value === "string") { |
| | | try { |
| | | const parsed = JSON.parse(value); |
| | | return Array.isArray(parsed) ? parsed : []; |
| | | } catch { |
| | | return []; |
| | | } |
| | | } |
| | | return []; |
| | | } |
| | | |
| | | function handleTypeLabel(value: number) { |
| | | if (value === 1) return "移交"; |
| | | if (value === 2) return "办结"; |
| | | return "-"; |
| | | } |
| | | |
| | | function getFileExt(fileName: string) { |
| | | const normalized = fileName || ""; |
| | | const dotIndex = normalized.lastIndexOf("."); |
| | | if (dotIndex === -1) return ""; |
| | | return normalized.slice(dotIndex + 1).toLowerCase(); |
| | | } |
| | | |
| | | function isImageFile(fileName: string) { |
| | | return ["png", "jpg", "jpeg", "gif", "bmp", "webp"].includes( |
| | | getFileExt(fileName) |
| | | ); |
| | | } |
| | | |
| | | function isVideoFile(fileName: string) { |
| | | return ["mp4", "mov", "avi", "wmv", "mkv", "webm"].includes( |
| | | getFileExt(fileName) |
| | | ); |
| | | } |
| | | |
| | | function isAudioFile(fileName: string) { |
| | | return ["mp3", "wav", "aac", "flac", "ogg", "m4a"].includes( |
| | | getFileExt(fileName) |
| | | ); |
| | | } |
| | | |
| | | function isOfficeFile(fileName: string) { |
| | | return ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf"].includes( |
| | | getFileExt(fileName) |
| | | ); |
| | | } |
| | | |
| | | function openNewTab(url: string) { |
| | | if (!url) { |
| | | window.$message?.warning("文件地址不存在"); |
| | | return; |
| | | } |
| | | window.open(url, "_blank", "noopener"); |
| | | } |
| | | |
| | | function handlePreview(file: any) { |
| | | const url = file?.url; |
| | | const name = file?.name || ""; |
| | | if (isOfficeFile(name)) { |
| | | const previewUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent( |
| | | url || "" |
| | | )}`; |
| | | openNewTab(previewUrl); |
| | | return; |
| | | } |
| | | if (isImageFile(name) || isVideoFile(name) || isAudioFile(name)) { |
| | | openNewTab(url); |
| | | return; |
| | | } |
| | | openNewTab(url); |
| | | } |
| | | |
| | | async function handleDownload(file: any) { |
| | | const url = file?.url; |
| | | if (!url) { |
| | | window.$message?.warning("文件地址不存在"); |
| | | return; |
| | | } |
| | | try { |
| | | const response = await fetch(url); |
| | | if (!response.ok) { |
| | | throw new Error("download_failed"); |
| | | } |
| | | const blob = await response.blob(); |
| | | const objectUrl = URL.createObjectURL(blob); |
| | | const link = document.createElement("a"); |
| | | link.href = objectUrl; |
| | | link.download = file?.name || "file"; |
| | | document.body.appendChild(link); |
| | | link.click(); |
| | | document.body.removeChild(link); |
| | | URL.revokeObjectURL(objectUrl); |
| | | } catch { |
| | | const link = document.createElement("a"); |
| | | link.href = url; |
| | | link.download = file?.name || "file"; |
| | | document.body.appendChild(link); |
| | | link.click(); |
| | | document.body.removeChild(link); |
| | | } |
| | | } |
| | | |
| | | function handleBack() { |
| | | router.back(); |
| | | } |
| | | |
| | | async function getDetail() { |
| | | if (!workOrderId.value) return; |
| | | startLoading(); |
| | | try { |
| | | const res = await fetchWorkOrderDetail(workOrderId.value); |
| | | if (res.code === 200 && res.data) { |
| | | detailData.value = res.data; |
| | | } else { |
| | | detailData.value = {}; |
| | | } |
| | | } finally { |
| | | endLoading(); |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col"> |
| | | <n-card style="border-radius: 6px" content-style="padding: 12px"> |
| | | <div class="flex items-center gap-2 mb-3"> |
| | | <div |
| | | class="bg-[#0068FF]" |
| | | style="width: 3px; height: 14px; border-radius: 3px" |
| | | ></div> |
| | | <div |
| | | class="text-[#171F26]" |
| | | style="line-height: 24px; font-size: 16px; font-weight: bold" |
| | | > |
| | | 工单详情 |
| | | </div> |
| | | </div> |
| | | <n-spin :show="loading"> |
| | | <div class="grid grid-cols-2 gap-4 text-sm text-[#1D2129]"> |
| | | <div>问题:{{ detailData.problem || "-" }}</div> |
| | | <div>反馈用户:{{ detailData.feedbackUserPhone || "-" }}</div> |
| | | <div>来源:{{ displayProblemSource }}</div> |
| | | <div>类型:{{ displayType }}</div> |
| | | <div>优先级:{{ displayPriority }}</div> |
| | | <div>创建人:{{ detailData.createBy }}</div> |
| | | <div>创建时间:{{ detailData.createTime || "-" }}</div> |
| | | </div> |
| | | <div class="mt-3 flex text-sm text-[#1D2129]"> |
| | | <div class="shrink-0">问题描述:</div> |
| | | <div class="whitespace-pre-wrap text-[#4E5969]"> |
| | | {{ detailData.problemDescription || "-" }} |
| | | </div> |
| | | </div> |
| | | <div v-if="displayImages.length" class="mt-3"> |
| | | <n-image-group> |
| | | <div class="flex flex-wrap gap-2"> |
| | | <n-image |
| | | v-for="item in displayImages" |
| | | :key="item.id || item.url" |
| | | :src="item.url" |
| | | width="80" |
| | | height="80" |
| | | /> |
| | | </div> |
| | | </n-image-group> |
| | | </div> |
| | | </n-spin> |
| | | </n-card> |
| | | |
| | | <n-card |
| | | style="border-radius: 6px; margin-top: 12px" |
| | | content-style="padding: 12px" |
| | | > |
| | | <div class="flex items-center gap-2 mb-3"> |
| | | <div |
| | | class="bg-[#0068FF]" |
| | | style="width: 3px; height: 14px; border-radius: 3px" |
| | | ></div> |
| | | <div |
| | | class="text-[#171F26]" |
| | | style="line-height: 24px; font-size: 16px; font-weight: bold" |
| | | > |
| | | 处理流程 |
| | | </div> |
| | | </div> |
| | | <n-spin :show="loading"> |
| | | <n-timeline> |
| | | <n-timeline-item |
| | | v-for="(item, index) in timelineItems" |
| | | :key="index" |
| | | :title="item.title" |
| | | type="success" |
| | | > |
| | | <div> |
| | | <div class="flex items-center gap-10"> |
| | | <!-- 增加一个头像 --> |
| | | <div class="flex items-center gap-3"> |
| | | <n-avatar :src="item.handleUserAvatar" size="small" /> |
| | | <div>{{ item.handleUserName || "-" }}</div> |
| | | </div> |
| | | <div>{{ item.typeTxt || "-" }}</div> |
| | | </div> |
| | | <div |
| | | v-if="index != 0" |
| | | class="mt-2 rounded border border-[#E5E6EB] bg-[#F7F8FA] p-3 text-sm text-[#1D2129]" |
| | | > |
| | | <div class="flex"> |
| | | <div class="shrink-0">处理内容:</div> |
| | | <div> |
| | | <div>{{ item.log.handleLog || "-" }}</div> |
| | | <div class="mt-2"> |
| | | <div |
| | | v-if="parseFileList(item.log.handleFile).length" |
| | | class="mt-2 space-y-2" |
| | | > |
| | | <div |
| | | v-for="file in parseFileList(item.log.handleFile)" |
| | | :key="file.id || file.url" |
| | | class="flex items-center gap-3" |
| | | > |
| | | <div class="max-w-[320px] truncate"> |
| | | {{ file.name || "-" }} |
| | | </div> |
| | | <n-button |
| | | type="primary" |
| | | :text="true" |
| | | @click="handlePreview(file)" |
| | | > |
| | | 预览 |
| | | </n-button> |
| | | <n-button |
| | | type="primary" |
| | | :text="true" |
| | | @click="handleDownload(file)" |
| | | > |
| | | 下载 |
| | | </n-button> |
| | | </div> |
| | | </div> |
| | | <div v-else class="text-[#4E5969]">-</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="mt-2"> |
| | | 操作类型: |
| | | <n-tag type="primary">{{ |
| | | handleTypeLabel(item.log.handleType) |
| | | }}</n-tag> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </n-timeline-item> |
| | | </n-timeline> |
| | | </n-spin> |
| | | <div class="mt-5 flex justify-center"> |
| | | <n-button @click="handleBack">返回</n-button> |
| | | </div> |
| | | </n-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped></style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, h, onMounted } from "vue"; |
| | | import type { DataTableColumns } from "naive-ui"; |
| | | import { NButton } from "naive-ui"; |
| | | import { useRouter } from "vue-router"; |
| | | import { useBoolean } from "@/hooks"; |
| | | import { |
| | | fetchWorkOrderPageList, |
| | | fetchFeedbackTypeList, |
| | | fetchOperationsUserListAll, |
| | | saveWorkOrder, |
| | | updateWorkOrder, |
| | | deleteWorkOrder, |
| | | handleWorkOrder, |
| | | } from "@/service/api/serviceCenter"; |
| | | import WorkOrderModal from "./components/WorkOrderModal.vue"; |
| | | import WorkOrderHandleModal from "./components/WorkOrderHandleModal.vue"; |
| | | |
| | | interface WorkOrderItem { |
| | | id: number; |
| | | feedbackUserPhone?: string; |
| | | problemSource?: number; |
| | | feedbackTypeId?: number; |
| | | feedbackTypeName?: string; |
| | | problem?: string; |
| | | problemDescription?: string; |
| | | problemImage?: string; |
| | | priority?: number; |
| | | handleStatus?: number; |
| | | handleUserId?: number | string; |
| | | handleUserName?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | const { |
| | | bool: loading, |
| | | setTrue: startLoading, |
| | | setFalse: endLoading, |
| | | } = useBoolean(false); |
| | | const router = useRouter(); |
| | | |
| | | const initialSearchForm = { |
| | | feedbackUserPhone: "", |
| | | problemSource: null as number | null, |
| | | feedbackTypeId: null as number | null, |
| | | priority: null as number | null, |
| | | handleStatus: null as number | null, |
| | | }; |
| | | const searchForm = ref({ ...initialSearchForm }); |
| | | |
| | | const tableData = ref<WorkOrderItem[]>([]); |
| | | const currentRow = ref<WorkOrderItem | null>(null); |
| | | const modalVisible = ref(false); |
| | | const modalMode = ref<"add" | "edit">("add"); |
| | | const handleModalVisible = ref(false); |
| | | const handleRow = ref<WorkOrderItem | null>(null); |
| | | |
| | | const pagination = ref({ |
| | | page: 1, |
| | | pageSize: 10, |
| | | itemCount: 0, |
| | | showSizePicker: true, |
| | | pageSizes: [10, 20, 50, 100], |
| | | }); |
| | | |
| | | const typeOptions = ref<{ label: string; value: number }[]>([]); |
| | | const handleUserOptions = ref<{ label: string; value: number | string }[]>([]); |
| | | |
| | | const problemSourceOptions = [{ label: "电话客服", value: 1 }]; |
| | | |
| | | const priorityOptions = [ |
| | | { label: "低", value: 1 }, |
| | | { label: "中", value: 2 }, |
| | | { label: "高", value: 3 }, |
| | | ]; |
| | | |
| | | const statusOptions = [ |
| | | { label: "待处理", value: 1 }, |
| | | { label: "处理中", value: 2 }, |
| | | { label: "已处理", value: 3 }, |
| | | ]; |
| | | |
| | | const priorityLabelMap: Record<number, string> = { |
| | | 1: "低", |
| | | 2: "中", |
| | | 3: "高", |
| | | }; |
| | | |
| | | const statusLabelMap: Record<number, string> = { |
| | | 1: "待处理", |
| | | 2: "处理中", |
| | | 3: "已处理", |
| | | }; |
| | | |
| | | const problemSourceLabelMap: Record<number, string> = { |
| | | 1: "电话客服", |
| | | }; |
| | | |
| | | const columns: DataTableColumns<WorkOrderItem> = [ |
| | | { |
| | | title: "序号", |
| | | key: "index", |
| | | width: 80, |
| | | align: "center", |
| | | render: (_row, index) => |
| | | h( |
| | | "span", |
| | | (pagination.value.page - 1) * pagination.value.pageSize + index + 1 |
| | | ), |
| | | }, |
| | | { |
| | | title: "提交用户", |
| | | key: "feedbackUserPhone", |
| | | width: 140, |
| | | align: "center", |
| | | render: (row) => row.feedbackUserPhone || "-", |
| | | }, |
| | | { |
| | | title: "来源", |
| | | key: "problemSource", |
| | | width: 120, |
| | | align: "center", |
| | | render: (row) => problemSourceLabelMap[row.problemSource || 0] || "-", |
| | | }, |
| | | { |
| | | title: "类型", |
| | | key: "feedbackTypeName", |
| | | width: 140, |
| | | align: "center", |
| | | render: (row) => row.feedbackTypeName || "-", |
| | | }, |
| | | { |
| | | title: "问题", |
| | | key: "problem", |
| | | minWidth: 200, |
| | | align: "center", |
| | | render: (row) => row.problem || "-", |
| | | }, |
| | | { |
| | | title: "优先级", |
| | | key: "priority", |
| | | width: 120, |
| | | align: "center", |
| | | render: (row) => priorityLabelMap[row.priority || 0] || "-", |
| | | }, |
| | | { |
| | | title: "处理状态", |
| | | key: "handleStatus", |
| | | width: 120, |
| | | align: "center", |
| | | render: (row) => statusLabelMap[row.handleStatus || 0] || "-", |
| | | }, |
| | | { |
| | | title: "处理人", |
| | | key: "handleUserName", |
| | | width: 120, |
| | | align: "center", |
| | | render: (row) => row.handleUserName || "-", |
| | | }, |
| | | { |
| | | title: "添加时间", |
| | | key: "createTime", |
| | | width: 180, |
| | | align: "center", |
| | | render: (row) => row.createTime || "-", |
| | | }, |
| | | { |
| | | title: "操作", |
| | | key: "actions", |
| | | width: 260, |
| | | align: "center", |
| | | render: (row) => { |
| | | const buttons = [ |
| | | h( |
| | | NButton, |
| | | { text: true, type: "primary", onClick: () => handleView(row) }, |
| | | { default: () => "详情" } |
| | | ), |
| | | ]; |
| | | if (row.handleStatus === 1) { |
| | | buttons.push( |
| | | h( |
| | | NButton, |
| | | { text: true, type: "primary", onClick: () => handleStart(row) }, |
| | | { default: () => "开始处理" } |
| | | ) |
| | | ); |
| | | } |
| | | if (row.handleStatus === 2) { |
| | | buttons.push( |
| | | h( |
| | | NButton, |
| | | { text: true, type: "primary", onClick: () => handleProcess(row) }, |
| | | { default: () => "处理" } |
| | | ) |
| | | ); |
| | | } |
| | | buttons.push( |
| | | h( |
| | | NButton, |
| | | { text: true, type: "primary", onClick: () => handleEdit(row) }, |
| | | { default: () => "编辑" } |
| | | ), |
| | | h( |
| | | NButton, |
| | | { text: true, type: "error", onClick: () => handleDelete(row) }, |
| | | { default: () => "删除" } |
| | | ) |
| | | ); |
| | | return h( |
| | | "div", |
| | | { class: "flex items-center justify-center gap-2" }, |
| | | buttons |
| | | ); |
| | | }, |
| | | }, |
| | | ]; |
| | | |
| | | function handleSearch() { |
| | | pagination.value.page = 1; |
| | | getWorkOrderList(); |
| | | } |
| | | |
| | | function handleReset() { |
| | | searchForm.value = { ...initialSearchForm }; |
| | | pagination.value.page = 1; |
| | | getWorkOrderList(); |
| | | } |
| | | |
| | | function handlePageChange(page: number) { |
| | | pagination.value.page = page; |
| | | getWorkOrderList(); |
| | | } |
| | | |
| | | function handlePageSizeChange(pageSize: number) { |
| | | pagination.value.pageSize = pageSize; |
| | | pagination.value.page = 1; |
| | | getWorkOrderList(); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | modalMode.value = "add"; |
| | | currentRow.value = null; |
| | | modalVisible.value = true; |
| | | } |
| | | |
| | | function handleEdit(row: WorkOrderItem) { |
| | | modalMode.value = "edit"; |
| | | currentRow.value = row; |
| | | modalVisible.value = true; |
| | | } |
| | | |
| | | function handleView(row: WorkOrderItem) { |
| | | router.push({ |
| | | path: "/service/ticket/workOrder/detail", |
| | | query: { id: row.id }, |
| | | }); |
| | | } |
| | | |
| | | function handleStart(row: WorkOrderItem) { |
| | | handleRow.value = row; |
| | | handleModalVisible.value = true; |
| | | } |
| | | |
| | | function handleProcess(row: WorkOrderItem) { |
| | | handleRow.value = row; |
| | | handleModalVisible.value = true; |
| | | } |
| | | |
| | | async function handleSave(formData: any) { |
| | | const handleUser = handleUserOptions.value.find( |
| | | (item) => item.value === formData.handleUserId |
| | | ); |
| | | const savePayload = { |
| | | feedbackTypeId: formData.feedbackTypeId || undefined, |
| | | feedbackUserPhone: formData.feedbackUserPhone || undefined, |
| | | priority: formData.priority ?? undefined, |
| | | problemSource: formData.problemSource ?? undefined, |
| | | problem: formData.problem || undefined, |
| | | problemDescription: formData.problemDescription || undefined, |
| | | problemImage: Array.isArray(formData.problemImage) |
| | | ? formData.problemImage |
| | | : undefined, |
| | | handleUserId: formData.handleUserId ?? undefined, |
| | | handleUserName: handleUser?.label || undefined, |
| | | }; |
| | | if (modalMode.value === "add") { |
| | | const { code } = await saveWorkOrder(savePayload); |
| | | if (code === 200) { |
| | | window.$message?.success("新增成功"); |
| | | modalVisible.value = false; |
| | | getWorkOrderList(); |
| | | } else { |
| | | window.$message?.error("新增失败"); |
| | | } |
| | | return; |
| | | } |
| | | const { code } = await updateWorkOrder({ |
| | | id: formData.id as number, |
| | | ...savePayload, |
| | | }); |
| | | if (code === 200) { |
| | | window.$message?.success("编辑成功"); |
| | | modalVisible.value = false; |
| | | getWorkOrderList(); |
| | | } else { |
| | | window.$message?.error("编辑失败"); |
| | | } |
| | | } |
| | | |
| | | async function handleSubmitHandle(payload: { |
| | | handleType: number; |
| | | handleLog: string; |
| | | handleUserId: number | string | null; |
| | | handleFile: { id: string; name: string; url: string }[]; |
| | | }) { |
| | | if (!handleRow.value?.id) return; |
| | | const selectedUser = handleUserOptions.value.find( |
| | | (item) => item.value === payload.handleUserId |
| | | ); |
| | | const handleUserId = |
| | | payload.handleUserId ?? handleRow.value.handleUserId ?? undefined; |
| | | const handleUserName = |
| | | selectedUser?.label || handleRow.value.handleUserName || undefined; |
| | | const submitData = { |
| | | handleFile: payload.handleFile || undefined, |
| | | handleLog: payload.handleLog, |
| | | handleType: payload.handleType, |
| | | handleUserId, |
| | | handleUserName, |
| | | workOrderId: handleRow.value.id, |
| | | }; |
| | | const execSubmit = async () => { |
| | | const { code } = await handleWorkOrder(submitData); |
| | | if (code === 200) { |
| | | window.$message?.success("操作成功"); |
| | | handleModalVisible.value = false; |
| | | getWorkOrderList(); |
| | | } else { |
| | | window.$message?.error("操作失败"); |
| | | } |
| | | }; |
| | | if (payload.handleType === 2) { |
| | | window.$dialog?.warning({ |
| | | title: "确认结办", |
| | | content: "是否确认结办?", |
| | | positiveText: "确定", |
| | | negativeText: "取消", |
| | | onPositiveClick: execSubmit, |
| | | }); |
| | | return; |
| | | } |
| | | execSubmit(); |
| | | } |
| | | |
| | | function handleDelete(row: WorkOrderItem) { |
| | | window.$dialog?.warning({ |
| | | title: "确认删除", |
| | | content: "确定要删除该工单吗?", |
| | | positiveText: "确定", |
| | | negativeText: "取消", |
| | | onPositiveClick: async () => { |
| | | const { error } = await deleteWorkOrder(row.id); |
| | | if (!error) { |
| | | window.$message?.success("删除成功"); |
| | | getWorkOrderList(); |
| | | } else { |
| | | window.$message?.error("删除失败"); |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | async function getTypeOptions() { |
| | | const res = await fetchFeedbackTypeList({ pageNum: 1, pageSize: 9999 }); |
| | | if (res.code === 200 && res.data) { |
| | | const list = Array.isArray(res.data) ? res.data : res.data.records || []; |
| | | typeOptions.value = list.map((item: any) => ({ |
| | | label: item.feedbackType, |
| | | value: item.id, |
| | | })); |
| | | } else { |
| | | typeOptions.value = []; |
| | | } |
| | | } |
| | | |
| | | async function getHandleUserOptions() { |
| | | const res = await fetchOperationsUserListAll(); |
| | | if (res.code === 200 && res.data) { |
| | | const list = Array.isArray(res.data) ? res.data : res.data.records || []; |
| | | handleUserOptions.value = list |
| | | .filter((item: any) => item.nickName) |
| | | .map((item: any) => ({ |
| | | label: item.nickName, |
| | | value: item.id, |
| | | })); |
| | | } else { |
| | | handleUserOptions.value = []; |
| | | } |
| | | } |
| | | |
| | | async function getWorkOrderList() { |
| | | startLoading(); |
| | | try { |
| | | const res = await fetchWorkOrderPageList({ |
| | | feedbackTypeId: searchForm.value.feedbackTypeId || undefined, |
| | | feedbackUserPhone: searchForm.value.feedbackUserPhone || undefined, |
| | | handleStatus: searchForm.value.handleStatus ?? undefined, |
| | | pageNum: pagination.value.page, |
| | | pageSize: pagination.value.pageSize, |
| | | priority: searchForm.value.priority ?? undefined, |
| | | problemSource: searchForm.value.problemSource ?? undefined, |
| | | }); |
| | | if (res.code === 200 && res.data) { |
| | | tableData.value = res.data.records || []; |
| | | pagination.value.itemCount = res.data.total || 0; |
| | | } else { |
| | | tableData.value = []; |
| | | pagination.value.itemCount = 0; |
| | | } |
| | | } finally { |
| | | endLoading(); |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getTypeOptions(); |
| | | getHandleUserOptions(); |
| | | getWorkOrderList(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col"> |
| | | <n-card style="border-radius: 6px" content-style="padding: 12px"> |
| | | <n-form |
| | | :model="searchForm" |
| | | label-placement="left" |
| | | inline |
| | | :show-feedback="false" |
| | | > |
| | | <div class="flex justify-between items-center w-full"> |
| | | <div class="flex flex-wrap gap-4"> |
| | | <n-form-item label="用户"> |
| | | <n-input |
| | | v-model:value="searchForm.feedbackUserPhone" |
| | | placeholder="请输入" |
| | | clearable |
| | | style="width: 200px" |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="问题来源"> |
| | | <n-select |
| | | v-model:value="searchForm.problemSource" |
| | | :options="problemSourceOptions" |
| | | placeholder="请选择" |
| | | clearable |
| | | style="width: 200px" |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="类型"> |
| | | <n-select |
| | | v-model:value="searchForm.feedbackTypeId" |
| | | :options="typeOptions" |
| | | placeholder="请选择" |
| | | clearable |
| | | style="width: 200px" |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="优先级"> |
| | | <n-select |
| | | v-model:value="searchForm.priority" |
| | | :options="priorityOptions" |
| | | placeholder="请选择" |
| | | clearable |
| | | style="width: 200px" |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="状态"> |
| | | <n-select |
| | | v-model:value="searchForm.handleStatus" |
| | | :options="statusOptions" |
| | | placeholder="请选择" |
| | | clearable |
| | | style="width: 200px" |
| | | /> |
| | | </n-form-item> |
| | | </div> |
| | | <div class="flex gap-2"> |
| | | <n-button style="width: 84px; height: 32px" @click="handleReset" |
| | | >重置</n-button |
| | | > |
| | | <n-button |
| | | type="primary" |
| | | style="width: 84px; height: 32px" |
| | | @click="handleSearch" |
| | | >查询</n-button |
| | | > |
| | | </div> |
| | | </div> |
| | | </n-form> |
| | | </n-card> |
| | | |
| | | <n-card |
| | | style="border-radius: 6px; margin-top: 12px" |
| | | content-style="padding: 12px" |
| | | > |
| | | <div class="mb-4 flex justify-end gap-2"> |
| | | <n-button |
| | | style=" |
| | | width: 96px; |
| | | height: 32px; |
| | | color: #0068ff; |
| | | background-color: #e6f4ff; |
| | | border: 1px solid #7abdff; |
| | | " |
| | | @click="handleCreate" |
| | | >创建工单</n-button |
| | | > |
| | | </div> |
| | | |
| | | <n-data-table |
| | | :columns="columns" |
| | | :data="tableData" |
| | | :loading="loading" |
| | | :pagination="{ |
| | | page: pagination.page, |
| | | pageSize: pagination.pageSize, |
| | | itemCount: pagination.itemCount, |
| | | showSizePicker: true, |
| | | pageSizes: pagination.pageSizes, |
| | | prefix: ({ itemCount }) => `共 ${itemCount || 0} 条`, |
| | | }" |
| | | :scroll-x="1600" |
| | | :row-key="(row: WorkOrderItem) => row.id" |
| | | @update:page="handlePageChange" |
| | | @update:page-size="handlePageSizeChange" |
| | | /> |
| | | </n-card> |
| | | |
| | | <WorkOrderModal |
| | | v-model:visible="modalVisible" |
| | | :mode="modalMode" |
| | | :row-data="currentRow" |
| | | :type-options="typeOptions" |
| | | :problem-source-options="problemSourceOptions" |
| | | :priority-options="priorityOptions" |
| | | :handle-user-options="handleUserOptions" |
| | | @submit="handleSave" |
| | | /> |
| | | |
| | | <WorkOrderHandleModal |
| | | v-model:visible="handleModalVisible" |
| | | :row-data="handleRow" |
| | | :type-options="typeOptions" |
| | | :problem-source-options="problemSourceOptions" |
| | | :priority-options="priorityOptions" |
| | | :handle-user-options="handleUserOptions" |
| | | @submit="handleSubmitHandle" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped></style> |
| | |
| | | width: 120, |
| | | resizable: true, |
| | | render(row) { |
| | | return h( |
| | | NTag, |
| | | { |
| | | type: statusOptions.find(item => item.value === row.status)?.type || 'default', |
| | | style: row.status === 7 ? { |
| | | color: '#764DFF', |
| | | backgroundColor: '#F6F0FF', |
| | | } : {}, |
| | | bordered: false |
| | | }, |
| | | { |
| | | default: () => statusOptions.find(item => item.value === row.status)?.label || '-' |
| | | } |
| | | ) |
| | | } |
| | | return h( |
| | | NTag, |
| | | { |
| | | type: statusOptions.find(item => item.value === row.status)?.type || 'default', |
| | | style: row.status === 7 ? { |
| | | color: '#764DFF', |
| | | backgroundColor: '#F6F0FF', |
| | | } : {}, |
| | | bordered: false |
| | | }, |
| | | { |
| | | default: () => statusOptions.find(item => item.value === row.status)?.label || '-' |
| | | } |
| | | ) |
| | | } |
| | | }, |
| | | { |
| | | title: '操作', |
| | |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { getPricingDetail, setPricing } from '@/service/api/transaction' |
| | | |
| | | |
| | | // 检查对象中是否存在值为null的键 |
| | | const hasNullValue = (obj: Record<string, any>) => { |
| | | for (const key in obj) { |
| | |
| | | num12: null, |
| | | num13: null, |
| | | num14: null, |
| | | num141: null, // 夜间行车时间 |
| | | num15: null, |
| | | num16: null, |
| | | num17: null, |
| | |
| | | num19: null, |
| | | num20: null, |
| | | num21: null, |
| | | num211: null, |
| | | num22: null, |
| | | num221: null, |
| | | num23: null, |
| | | num24: null, |
| | | num25: null, |
| | |
| | | window.$message.error('请配置基础定价') |
| | | return |
| | | } |
| | | let basicPricingJson1= '' |
| | | if (category.value === '1') { |
| | | if (hasNullValue(model.value.basicPricingJson1)) { |
| | | window.$message.error('请配置基础定价') |
| | | return |
| | | } else { |
| | | model.value.basicPricingJson1.num14 = model.value.basicPricingJson1.num14 + ' - ' + model.value.basicPricingJson1.num141 |
| | | model.value.basicPricingJson1.num21 = model.value.basicPricingJson1.num21 + ' - ' + model.value.basicPricingJson1.num211 |
| | | model.value.basicPricingJson1.num22 = model.value.basicPricingJson1.num22 + ' - ' + model.value.basicPricingJson1.num221 |
| | | delete model.value.basicPricingJson1.num141 |
| | | delete model.value.basicPricingJson1.num211 |
| | | delete model.value.basicPricingJson1.num221 |
| | | } |
| | | if (model.value.startingFareJson.length === 0) { |
| | | window.$message.error('请配置起步价') |
| | |
| | | return |
| | | } |
| | | startLoading() |
| | | if (category.value === '2') { |
| | | } else { |
| | | } |
| | | const params = { |
| | | basicPricingJson: category.value === '1' ? JSON.stringify(model.value.basicPricingJson1) : JSON.stringify(model.value.basicPricingJson2), |
| | | dynamicPricingJson: JSON.stringify(model.value.dynamicPricingJson), |
| | |
| | | detail.value = res.data |
| | | if (category.value === '1') { |
| | | const parsedBasicPricing = JSON.parse(detail.value.basicPricingJson) |
| | | const num14 = JSON.parse(JSON.stringify(parsedBasicPricing.num14)) |
| | | const num21 = JSON.parse(JSON.stringify(parsedBasicPricing.num21)) |
| | | const num22 = JSON.parse(JSON.stringify(parsedBasicPricing.num22)) |
| | | parsedBasicPricing.num14 = num14.split(' - ')[0] |
| | | parsedBasicPricing.num141 = num14.split(' - ')[1] |
| | | parsedBasicPricing.num21 = num21.split(' - ')[0] |
| | | parsedBasicPricing.num211 = num21.split(' - ')[1] |
| | | parsedBasicPricing.num22 = num22.split(' - ')[0] |
| | | parsedBasicPricing.num221 = num22.split(' - ')[1] |
| | | model.value.basicPricingJson1 = parsedBasicPricing || { ...initialModel.basicPricingJson1 } |
| | | const parsedStartingFare = JSON.parse(detail.value.startingFareJson) |
| | | model.value.startingFareJson = parsedStartingFare || [] |
| | |
| | | <NSpace vertical :size="16"> |
| | | <NSpace align="center"> |
| | | <n-time-picker v-model:formatted-value="model.basicPricingJson1.num14" format="HH:mm" |
| | | value-format="HH:mm" clearable /> |
| | | value-format="HH:mm" clearable class="w-80px"/>- |
| | | <n-time-picker v-model:formatted-value="model.basicPricingJson1.num141" format="HH:mm" |
| | | value-format="HH:mm" clearable class="w-80px" /> |
| | | 点为夜间行车时间,起步价变为 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num15" min="0" max="999" :precision="0" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | |
| | | <NSpace vertical :size="16"> |
| | | <NSpace align="center"> |
| | | <n-time-picker v-model:formatted-value="model.basicPricingJson1.num21" format="HH:mm" |
| | | value-format="HH:mm" clearable /> |
| | | value-format="HH:mm" clearable class="w-80px" />- |
| | | <n-time-picker v-model:formatted-value="model.basicPricingJson1.num211" format="HH:mm" |
| | | value-format="HH:mm" clearable class="w-80px" /> |
| | | 和 |
| | | <n-time-picker v-model:formatted-value="model.basicPricingJson1.num22" format="HH:mm" |
| | | value-format="HH:mm" clearable /> |
| | | value-format="HH:mm" clearable class="w-80px" />- |
| | | <n-time-picker v-model:formatted-value="model.basicPricingJson1.num221" format="HH:mm" |
| | | value-format="HH:mm" clearable class="w-80px" /> |
| | | 为高峰期行车时间,起步价变为 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num23" min="0" max="999" :precision="0" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |