供水机的增删功能 #96

Merged
pc8xi2fbj merged 1 commits from zhanghongwei_branch into develop 4 weeks ago

@ -171,4 +171,17 @@ public class WorkOrderController {
return ResultVO.error(500, "派单失败:" + e.getMessage());
}
}
// 获取单个工单详情 - 管理员和维修人员均可访问
@GetMapping("/{orderId}")
@PreAuthorize("hasAnyRole('REPAIRMAN', 'SUPER_ADMIN', 'AREA_ADMIN')")
public ResultVO<WorkOrder> getOrderDetail(@PathVariable String orderId) {
try {
WorkOrder order = workOrderService.getOrderDetail(orderId);
return ResultVO.success(order);
} catch (Exception e) {
return ResultVO.error(500, "获取工单详情失败:" + e.getMessage());
}
}
}

@ -4,6 +4,9 @@ import com.campus.water.entity.WorkOrder;
import java.util.List;
public interface WorkOrderService {
//按ID获取工单详情
WorkOrder getOrderDetail(String orderId);
// 抢单
boolean grabOrder(String orderId, String repairmanId);

@ -18,6 +18,12 @@ public class WorkOrderServiceImpl implements WorkOrderService {
private final WorkOrderRepository workOrderRepository;
private final RepairmanRepository repairmanRepository;
@Override
public WorkOrder getOrderDetail(String orderId) {
return workOrderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("工单不存在"));
}
/**
*

@ -8,7 +8,7 @@
<!-- 操作按钮区 -->
<div class="action-bar">
<button class="btn-add">添加供水机</button>
<button class="btn-add" @click="showAddModal = true">添加供水机</button>
<div class="filters">
<!-- 搜索框 -->
@ -29,8 +29,10 @@
@change="handleSearch"
>
<option value="">全部片区</option>
<option value="市区">市区</option>
<option value="校区">校区</option>
<option value="A">A区</option>
<option value="B">B区</option>
<option value="C">C区</option>
<option value="D">D区</option>
</select>
<!-- 状态筛选 -->
@ -48,13 +50,13 @@
</div>
</div>
<!-- 设备表格 - 新增设备机型列 -->
<!-- 表格 -->
<div class="card">
<table class="equipment-table">
<thead>
<tr>
<th>设备ID</th>
<th>设备机型</th> <!-- 新增机型列 -->
<th>设备机型</th>
<th>所属片区</th>
<th>详细位置</th>
<th>状态</th>
@ -65,7 +67,7 @@
<tbody>
<tr v-for="device in paginatedDevices" :key="device.id">
<td>{{ device.id }}</td>
<td>供水机</td> <!-- 固定显示供水机机型 -->
<td>供水机</td>
<td>{{ device.area }}</td>
<td>{{ device.location }}</td>
<td>
@ -76,10 +78,11 @@
<td>{{ device.lastUploadTime }}</td>
<td class="operation-buttons">
<button class="btn-view" @click="viewDevice(device.id)"></button>
<button class="btn-delete" @click="currentDeviceId = device.id; showDeleteModal = true">删除</button>
</td>
</tr>
<tr v-if="paginatedDevices.length === 0">
<td colspan="7" class="no-data">暂无设备数据</td> <!-- colspan从6改为7 -->
<td colspan="7" class="no-data">暂无设备数据</td>
</tr>
</tbody>
</table>
@ -105,6 +108,52 @@
下一页
</button>
</div>
<!-- 添加设备模态框 -->
<div v-if="showAddModal" class="modal-overlay" @click="showAddModal = false">
<div class="modal-content" @click.stop>
<h3>添加供水机</h3>
<form @submit.prevent="addDevice">
<div class="form-group">
<label>设备ID:</label>
<input v-model="newDevice.deviceId" type="text" required>
</div>
<div class="form-group">
<label>设备名称:</label>
<input v-model="newDevice.deviceName" type="text" required>
</div>
<div class="form-group">
<label>所属片区:</label>
<select v-model="newDevice.areaId" required>
<option value="A">A区</option>
<option value="B">B区</option>
<option value="C">C区</option>
<option value="D">D区</option>
</select>
</div>
<div class="form-group">
<label>安装位置:</label>
<input v-model="newDevice.installLocation" type="text" required>
</div>
<div class="form-actions">
<button type="button" @click="showAddModal = false">取消</button>
<button type="submit">添加</button>
</div>
</form>
</div>
</div>
<!-- 删除确认模态框 -->
<div v-if="showDeleteModal" class="modal-overlay" @click="showDeleteModal = false">
<div class="modal-content" @click.stop>
<h3>确认删除设备</h3>
<p>确定要删除设备 ID: {{ currentDeviceId }} 此操作不可恢复</p>
<div class="form-actions">
<button type="button" @click="showDeleteModal = false">取消</button>
<button type="button" @click="deleteDevice()"></button>
</div>
</div>
</div>
</div>
</template>
@ -137,6 +186,20 @@ const pageSize = 10 // 每页显示数量
const router = useRouter()
const authStore = useAuthStore()
//
const showAddModal = ref(false)
const newDevice = ref({
deviceId: '',
deviceName: '',
areaId: 'A',
installLocation: '',
deviceType: 'water_supply'
})
//
const showDeleteModal = ref(false)
const currentDeviceId = ref('')
//
const loadWaterSuppliers = async () => {
try {
@ -233,6 +296,75 @@ const viewDevice = (id: string) => {
router.push(`/home/equipment/water-supplier/${id}`)
}
//
const addDevice = async () => {
try {
const token = authStore.token
if (!token) {
router.push('/login')
return
}
const deviceToAdd = {
deviceId: newDevice.value.deviceId,
deviceName: newDevice.value.deviceName,
areaId: newDevice.value.areaId,
installLocation: newDevice.value.installLocation,
deviceType: newDevice.value.deviceType
}
const result = await request<ResultVO<any>>('/api/web/device/add', {
method: 'POST',
body: JSON.stringify(deviceToAdd)
})
if (result.code === 200) {
await loadWaterSuppliers()
showAddModal.value = false
alert('设备添加成功')
} else {
alert(`设备添加失败: ${result.message}`)
}
} catch (error) {
console.error('添加设备失败:', error)
alert('添加设备失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
const deleteDevice = async () => {
try {
const token = authStore.token
if (!token) {
router.push('/login')
return
}
const result = await request<ResultVO<boolean>>(`/api/web/device/delete/${currentDeviceId.value}`, {
method: 'DELETE'
})
if (result.code === 200) {
await loadWaterSuppliers()
showDeleteModal.value = false
alert('设备删除成功')
} else {
alert(`设备删除失败: ${result.message}`)
}
} catch (error) {
console.error('删除设备失败:', error)
alert('删除设备失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
onMounted(() => {
loadWaterSuppliers()
@ -285,6 +417,21 @@ onMounted(() => {
background: #359e75;
}
.btn-delete {
background: #cf1322;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.btn-delete:hover {
background: #b80c1a;
}
.filters {
display: flex;
gap: 12px;
@ -394,6 +541,11 @@ onMounted(() => {
color: #1890ff;
}
.btn-delete {
background-color: #ffebe6;
color: #cf1322;
}
.no-data {
text-align: center;
padding: 40px 0;
@ -423,6 +575,80 @@ onMounted(() => {
cursor: not-allowed;
}
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 24px;
border-radius: 8px;
min-width: 400px;
max-width: 500px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.modal-content h3 {
margin-top: 0;
margin-bottom: 20px;
color: #333;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.form-actions button {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.form-actions button[type="button"] {
background: #f5f5f5;
border: 1px solid #ddd;
color: #333;
}
.form-actions button[type="submit"] {
background: #42b983;
border: none;
color: white;
}
/* 响应式调整 */
@media (max-width: 768px) {
.filters {

@ -26,8 +26,10 @@
<label>所属片区</label>
<select v-model="filterForm.area" class="filter-select" @change="handleFilter">
<option value="">全部片区</option>
<option value="市区">市区</option>
<option value="校区">校区</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
</select>
</div>

@ -116,9 +116,12 @@
</div>
</template>
<!-- src/views/workorder/CompletedDetail.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
//
type OrderStatus = 'timeout' | 'pending' | 'processing' | 'reviewing' | 'completed'
@ -164,6 +167,7 @@ const previewImage = ref('')
//
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
//
const formatStatus = (status: OrderStatus): string => {
@ -180,66 +184,73 @@ const formatStatus = (status: OrderStatus): string => {
//
const fetchOrderDetail = async (id: string) => {
try {
// API
// const response = await getCompletedOrderDetailApi(id)
// orderDetail.value = response.data
// API
const mockData: Record<string, OrderDetail> = {
'11': {
id: '11',
orderNo: '1O05',
status: 'completed',
repairman: '王五',
phone: '123456789',
acceptTime: '2025/10/20-16:21',
completeTime: '2025/10/20-18:45',
deviceId: '供水机#A11',
warningItem: '更换滤芯',
location: 'A区图书馆',
processRemark: '原滤芯老旧已损,更换了新滤芯',
actualProcess: '更换滤芯',
totalCost: '110元',
reviewOpinion: '审核通过,已结单',
photos: [
'https://picsum.photos/seed/filter1/400/300',
'https://picsum.photos/seed/filter2/400/300'
]
},
'12': {
id: '12',
orderNo: '1O06',
status: 'completed',
repairman: '赵六',
phone: '987654321',
acceptTime: '2025/10/21-09:15',
completeTime: '2025/10/21-11:30',
deviceId: '制水机#B07',
warningItem: '水泵故障',
location: 'B区教学楼',
processRemark: '水泵已修复,设备运行正常',
actualProcess: '维修水泵,更换损坏零件',
totalCost: '260元',
reviewOpinion: '维修合格,同意结单',
photos: [
'https://picsum.photos/seed/pump1/400/300',
'https://picsum.photos/seed/pump2/400/300'
]
}
// Token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
//
await new Promise(resolve => setTimeout(resolve, 300))
if (mockData[id]) {
orderDetail.value = mockData[id]
console.log('当前 Token:', token.substring(0, 20) + '...')
// fetchOrderDetail
const response = await request<any>(`/api/work-orders/${id}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}` //
}
})
if (response.code === 200) {
const data = response.data
// WorkOrder
orderDetail.value = {
id: data.orderId || '',
orderNo: data.orderId || '',
status: data.status as OrderStatus || 'completed',
repairman: data.assignedRepairmanId || '未知维修员', // ID
phone: '1234567890', //
acceptTime: data.grabbedTime ? new Date(data.grabbedTime).toLocaleString('zh-CN') : '未知',
completeTime: data.completedTime ? new Date(data.completedTime).toLocaleString('zh-CN') : '未知',
deviceId: data.deviceId || '',
warningItem: data.description || '无描述',
location: data.areaId || '未知位置',
processRemark: data.dealNote || '无备注',
actualProcess: data.dealNote || '无处理说明',
totalCost: '0元', //
reviewOpinion: '审核通过', //
photos: data.imgUrl ? [data.imgUrl] : []
}
} else {
throw new Error('未找到工单数据')
const errorMsg = response.msg || `获取失败(错误码:${response.code}`
console.error('获取工单详情失败:', errorMsg)
alert(`获取工单详情失败:${errorMsg}`)
throw new Error(errorMsg)
}
} catch (error) {
} catch (error: any) {
console.error('获取工单详情失败:', error)
alert('获取工单详情详情失败,请重试')
router.push('/home/work-order/completed')
console.error('错误详情:', {
message: error.message,
status: error.status,
response: error.response
})
const errorMsg = error.message.includes('401') || error.message.includes('403')
? '权限不足或登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取数据失败,请稍后重试'
alert(`获取工单详情失败:${errorMsg}`)
if (error.message.includes('401') || error.message.includes('403')) {
authStore.logout()
router.push('/login')
} else {
router.push('/home/work-order/completed')
}
}
}
@ -255,6 +266,7 @@ const handleBack = () => {
//
onMounted(() => {
console.log('Token:', authStore.token)
const id = route.params.id as string
if (id) {
fetchOrderDetail(id)
@ -264,6 +276,8 @@ onMounted(() => {
})
</script>
<style scoped>
.completed-detail-page {
padding: 20px;

@ -25,8 +25,10 @@
<label>所属片区</label>
<select v-model="filterForm.area" class="filter-select" @change="handleFilter">
<option value="">全部片区</option>
<option value="市区">市区</option>
<option value="校区">校区</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
</select>
</div>
@ -257,13 +259,13 @@ const loadAvailableOrders = async () => {
// 使 request 使 axios
// loadAvailableOrders
const response = await request<{
code: number
msg: string
data: any[]
}>(url,{
method: 'GET',
})
const response = await request<{
code: number
msg: string
data: any[]
}>(url,{
method: 'GET',
})
//
@ -288,8 +290,8 @@ const response = await request<{
alert(`获取待抢单工单失败:${errorMsg}`)
}
} catch (error: any) {
console.error('请求异常:', error)
console.error('错误详情:', {
console.error('请求异常:', error)
console.error('错误详情:', {
message: error.message,
status: error.status,
response: error.response

@ -25,8 +25,10 @@
<label>所属片区</label>
<select v-model="filterForm.area" class="filter-select" @change="handleFilter">
<option value="">全部片区</option>
<option value="市区">市区</option>
<option value="校区">校区</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
</select>
</div>

@ -26,8 +26,10 @@
<label>所属片区</label>
<select v-model="filterForm.area" class="filter-select" @change="handleFilter">
<option value="">全部片区</option>
<option value="市区">市区</option>
<option value="校区">校区</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
</select>
</div>

@ -26,8 +26,10 @@
<label>所属片区</label>
<select v-model="filterForm.area" class="filter-select" @change="handleFilter">
<option value="">全部片区</option>
<option value="市区">市区</option>
<option value="校区">校区</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
</select>
</div>
@ -166,7 +168,7 @@
:key="staff.id"
:value="staff.id"
>
{{ staff.name }} ({{ staff.phone }})
{{ staff.repairmanName }} ({{ staff.phone }})
</option>
</select>
</div>
@ -221,10 +223,10 @@ interface TimeoutOrder {
//
interface MaintenanceStaff {
id: string
name: string
repairmanName: string
phone: string
area: string //
status: 'active' | 'disabled' //
areaId: string //
status: 'idle' | 'busy' | 'vacation' //
}
//
@ -299,8 +301,8 @@ const loadTimeoutOrders = async () => {
problemDesc: order.description || '暂无描述',
status: order.status || 'timeout',
createTime: order.createdTime ? new Date(order.createdTime).toLocaleString('zh-CN') : '未知时间',
lastUploadTime: order.lastUploadTime ? new Date(order.lastUploadTime).toLocaleString('zh-CN') : '未知',
location: order.location || '未知位置'
lastUploadTime: order.updatedTime ? new Date(order.updatedTime).toLocaleString('zh-CN') : '未知',
location: order.areaId || '未知位置'
}))
} else {
const errorMsg = response.msg || `获取失败(错误码:${response.code}`
@ -332,12 +334,13 @@ const loadTimeoutOrders = async () => {
}
//
const loadMaintenanceStaff = async () => {
const loadMaintenanceStaff = async (areaId: string) => {
try {
// ID
if (!currentOrder.value) return
const areaId = currentOrder.value.area
const token = authStore.token
if (!token) {
router.push('/login')
return
}
//
const response = await request<{
@ -346,20 +349,26 @@ const loadMaintenanceStaff = async () => {
data: MaintenanceStaff[]
}>('/api/web/repairman/by-area/' + areaId, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
})
console.log('维修人员响应数据:', response) //
console.log('响应数据结构:', typeof response.data, response.data)
if (response.code === 200) {
allStaff.value = response.data || []
allStaff.value = response.data || [] //
} else {
console.error('获取维修人员失败:', response.msg)
console.error('获取维修人员失败:', response.msg) //
alert('获取维修人员失败:' + response.msg)
}
} catch (error: any) {
console.error('请求异常:', error)
console.error('请求异常:', error) //
alert('获取维修人员失败:' + (error.message || '网络错误'))
}
}
}
//
const formatStatus = (status: OrderStatus): string => {
@ -399,11 +408,14 @@ const paginatedOrders = computed(() => {
return filteredOrders.value.slice(start, end)
})
//
//
const filteredStaff = computed(() => {
console.log('当前工单:', currentOrder.value)
console.log('所有维修人员:', allStaff.value)
if (!currentOrder.value) return []
return allStaff.value.filter(staff =>
staff.area === currentOrder.value!.area && staff.status === 'active'
staff.areaId === currentOrder.value!.area
)
})
@ -435,13 +447,17 @@ const resetFilter = () => {
}
//
const openAssignDialog = (order: TimeoutOrder) => {
const openAssignDialog = async (order: TimeoutOrder) => {
currentOrder.value = order
selectedStaffId.value = ''
assignRemark.value = ''
assignDialogVisible.value = true
//
await loadMaintenanceStaff(order.area)
}
//
const closeAssignDialog = () => {
assignDialogVisible.value = false
@ -460,6 +476,12 @@ const confirmAssign = async () => {
if (!currentOrder.value || !selectedStaffId.value) return
try {
const token = authStore.token
if (!token) {
router.push('/login')
return
}
// API
const response = await request<{
code: number
@ -468,6 +490,7 @@ const confirmAssign = async () => {
}>('/api/work-orders/assign', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
@ -494,7 +517,6 @@ const confirmAssign = async () => {
onMounted(() => {
console.log('Token:', authStore.token)
loadTimeoutOrders()
loadMaintenanceStaff()
})
</script>

Loading…
Cancel
Save