feat: 工单系统优化 - 修复自动跳转问题并添加用户筛选功能
Build and Deploy Vue3 / build (push) Successful in 1m8s
Build and Deploy Vue3 / deploy (push) Successful in 1m28s

- 修复工单详情页定时刷新导致的自动跳转问题
- 添加用户搜索选择器,支持按用户筛选工单
- 优化用户搜索体验,使用对话框模式
- 修正API响应数据结构解析
This commit is contained in:
2026-01-07 17:21:01 +08:00
parent 1655d86f6b
commit 2ce2c1a31f
3 changed files with 262 additions and 24 deletions
+2 -1
View File
@@ -5,11 +5,12 @@ import request from "@/utils/request.js";
* @returns {Promise}
*/
export function getTickerList(count, page, status, orderBy, order) {
export function getTickerList(count, page, status, orderBy, order, userId) {
const params = { count, page }
if (status !== undefined && status !== '') params.status = status
if (orderBy) params.orderBy = orderBy
if (order) params.order = order
if (userId) params.user_id = userId
console.log('工单列表请求参数:', params) // 调试日志
return request.get('/api/v1/admin/work_order/list', params)
}
+2 -1
View File
@@ -892,7 +892,8 @@ const goToUserDetail = () => {
// 定时刷新
const startAutoRefresh = () => {
refreshTimer.value = setInterval(() => {
if (ticketInfo.value?.status !== 'completed') {
// 只有当前路由仍在工单详情页且工单未完成时才刷新
if (route.path === '/ticket/detail' && route.query.id && ticketInfo.value?.status !== 'completed') {
fetchTicketDetail(false) // 定时刷新时不显示 loading
}
}, 10000)
+258 -22
View File
@@ -31,14 +31,21 @@
<el-option label="降序" value="desc" />
<el-option label="升序" value="asc" />
</el-select>
<!-- 用户筛选输入框 -->
<el-input
v-model="searchKeyword"
placeholder="搜索工单号、标题、用户名"
prefix-icon="Search"
clearable
style="width: 240px"
@input="handleSearch"
/>
:model-value="selectedUser ? selectedUser.user_name : ''"
placeholder="点击选择用户筛选"
readonly
style="width: 180px; cursor: pointer"
@click="showUserDialog = true"
>
<template #prefix>
<el-icon><User /></el-icon>
</template>
<template #suffix v-if="selectedUser">
<el-icon @click.stop="clearUserFilter" style="cursor: pointer"><Close /></el-icon>
</template>
</el-input>
<el-button icon="Refresh" @click="refreshList">刷新</el-button>
</div>
</div>
@@ -99,18 +106,74 @@
@current-change="handlePageChange"
/>
</div>
<!-- 用户选择对话框 -->
<el-dialog
v-model="showUserDialog"
title="选择用户"
width="600px"
destroy-on-close
>
<div class="user-dialog-content">
<el-input
v-model="userSearchKeyword"
placeholder="输入用户名/手机号/邮箱搜索"
clearable
@input="handleUserSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<div class="user-list-container" v-loading="isSearchingUser">
<!-- 调试信息 -->
<div style="padding: 8px; font-size: 12px; color: #909399; border-bottom: 1px solid #eee;">
搜索关键词: {{ userSearchKeyword }} | 用户数量: {{ userList.length }}
</div>
<div v-if="!userSearchKeyword" class="empty-hint">
请输入关键词搜索用户
</div>
<div v-else-if="userSearchKeyword && userList.length === 0 && !isSearchingUser" class="empty-hint">
未找到匹配的用户
</div>
<div v-if="userList.length > 0" class="user-list">
<div
v-for="user in userList"
:key="user.user_id"
class="user-list-item"
@click="selectUser(user)"
>
<el-avatar :size="40" :src="user.cover">{{ user.user_name?.charAt(0) }}</el-avatar>
<div class="user-list-info">
<div class="user-list-name">{{ user.user_name }}</div>
<div class="user-list-sub">
<span v-if="user.phone">手机: {{ user.phone }}</span>
<span v-else-if="user.email">邮箱: {{ user.email }}</span>
<span v-else>UID: {{ user.user_id }}</span>
</div>
</div>
<el-icon class="user-list-arrow"><ArrowRight /></el-icon>
</div>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onActivated } from 'vue'
import { ref, reactive, computed, onMounted, onActivated, onBeforeUnmount, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, User, Close, ArrowRight } from '@element-plus/icons-vue'
import {
getTickerList,
closeTicket,
getTicketCount
} from '@/api/ticket'
import { getUserList } from '@/api/admin/user'
const router = useRouter()
@@ -122,9 +185,16 @@ const isLoading = ref(false)
// 工单数据
const ticketList = ref([])
const searchKeyword = ref('')
const activeStatus = ref('pending') // 默认选中"待处理"
// 用户搜索
const userSearchKeyword = ref('')
const userList = ref([])
const selectedUser = ref(null)
const showUserDialog = ref(false)
const isSearchingUser = ref(false)
const userSearchTimer = ref(null)
// 排序
const sortBy = ref('') // 默认不排序
const sortOrder = ref('') // 默认不选择排序顺序
@@ -138,6 +208,9 @@ const stats = reactive({
total: 0
})
// 自动刷新定时器
const autoRefreshTimer = ref(null)
@@ -174,7 +247,8 @@ const fetchTicketList = async () => {
currentPage.value,
statusParam,
sortBy.value,
sortOrder.value
sortOrder.value,
selectedUser.value?.user_id
)
if (res.code === 200) {
@@ -218,15 +292,61 @@ const fetchStats = async () => {
}
// 过滤后的工单列表
const filteredTickets = computed(() => {
if (!searchKeyword.value) return ticketList.value
const keyword = searchKeyword.value.toLowerCase()
return ticketList.value.filter(ticket =>
ticket.title.toLowerCase().includes(keyword) ||
ticket.username.toLowerCase().includes(keyword) ||
String(ticket.id).includes(keyword)
)
})
const filteredTickets = computed(() => ticketList.value)
// 用户搜索
const handleUserSearch = () => {
if (userSearchTimer.value) {
clearTimeout(userSearchTimer.value)
}
const keyword = userSearchKeyword.value.trim()
if (!keyword) {
userList.value = []
return
}
userSearchTimer.value = setTimeout(async () => {
try {
isSearchingUser.value = true
const res = await getUserList({ page: 1, count: 20, key: keyword })
console.log('用户搜索响应:', res)
if (res.data?.code === 200) {
// 注意:响应结构是 res.data.data.data
userList.value = res.data.data?.data || []
console.log('用户列表更新:', userList.value)
} else {
ElMessage.error(res.data?.message || '搜索用户失败')
userList.value = []
}
} catch (error) {
console.error('搜索用户出错:', error)
ElMessage.error('搜索用户失败')
userList.value = []
} finally {
isSearchingUser.value = false
}
}, 300)
}
// 选择用户
const selectUser = (user) => {
selectedUser.value = user
showUserDialog.value = false
userSearchKeyword.value = ''
userList.value = []
currentPage.value = 1
fetchTicketList()
}
// 清除用户筛选
const clearUserFilter = () => {
selectedUser.value = null
currentPage.value = 1
fetchTicketList()
}
// 按状态过滤
const filterByStatus = (status) => {
@@ -234,6 +354,12 @@ const filterByStatus = (status) => {
activeStatus.value = status
currentPage.value = 1
fetchTicketList()
// 切换状态时重新设置定时器
stopAutoRefresh()
if (status === 'pending') {
startAutoRefresh()
}
}
// 排序变化处理
@@ -242,9 +368,6 @@ const handleSortChange = () => {
fetchTicketList()
}
// 搜索处理
const handleSearch = () => {}
// 分页处理
const handleSizeChange = () => {
currentPage.value = 1
@@ -291,11 +414,49 @@ const handleComplete = (ticket) => {
}).catch(() => {})
}
// 启动自动刷新(仅在待处理状态)
const startAutoRefresh = () => {
if (autoRefreshTimer.value) return
autoRefreshTimer.value = setInterval(() => {
if (activeStatus.value === 'pending') {
// 静默刷新,不显示loading
const originalLoading = isLoading.value
fetchTicketList().finally(() => {
isLoading.value = originalLoading
})
fetchStats()
}
}, 30000) // 30秒
}
// 停止自动刷新
const stopAutoRefresh = () => {
if (autoRefreshTimer.value) {
clearInterval(autoRefreshTimer.value)
autoRefreshTimer.value = null
}
}
let isFirstLoad = true
// 监听对话框关闭,清空搜索状态
watch(showUserDialog, (newVal) => {
if (!newVal) {
// 对话框关闭时清空搜索
userSearchKeyword.value = ''
userList.value = []
}
})
onMounted(() => {
fetchTicketList()
fetchStats()
// 如果默认是待处理状态,启动自动刷新
if (activeStatus.value === 'pending') {
startAutoRefresh()
}
})
// 当页面被激活时(从详情页返回时)
@@ -305,6 +466,19 @@ onActivated(() => {
refreshList()
}
isFirstLoad = false
// 重新启动自动刷新(如果是待处理状态)
if (activeStatus.value === 'pending') {
startAutoRefresh()
}
})
// 组件卸载时清理定时器
onBeforeUnmount(() => {
stopAutoRefresh()
if (userSearchTimer.value) {
clearTimeout(userSearchTimer.value)
}
})
</script>
@@ -365,6 +539,68 @@ onActivated(() => {
gap: 8px;
}
.user-dialog-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.user-list-container {
min-height: 300px;
max-height: 400px;
overflow-y: auto;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
.empty-hint {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
color: #909399;
font-size: 14px;
}
.user-list {
padding: 8px 0;
}
.user-list-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
transition: background 0.2s;
}
.user-list-item:hover {
background: #f5f7fa;
}
.user-list-info {
flex: 1;
min-width: 0;
}
.user-list-name {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.user-list-sub {
font-size: 12px;
color: #909399;
}
.user-list-arrow {
color: #c0c4cc;
font-size: 16px;
}
.user-info {
display: flex;
align-items: center;