Merge branch 'master' of https://gitlab.s1f.top/lin/ApiServer-Web-admin_dashboard_pc
This commit is contained in:
+3
-1
@@ -5,11 +5,13 @@ import request from "@/utils/request.js";
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function getTickerList(count, page, status, orderBy, order) {
|
export function getTickerList(count, page, status, orderBy, order, userId, keyword) {
|
||||||
const params = { count, page }
|
const params = { count, page }
|
||||||
if (status !== undefined && status !== '') params.status = status
|
if (status !== undefined && status !== '') params.status = status
|
||||||
if (orderBy) params.orderBy = orderBy
|
if (orderBy) params.orderBy = orderBy
|
||||||
if (order) params.order = order
|
if (order) params.order = order
|
||||||
|
if (userId) params.user_id = userId
|
||||||
|
if (keyword) params.keyword = keyword
|
||||||
console.log('工单列表请求参数:', params) // 调试日志
|
console.log('工单列表请求参数:', params) // 调试日志
|
||||||
return request.get('/api/v1/admin/work_order/list', params)
|
return request.get('/api/v1/admin/work_order/list', params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -892,7 +892,8 @@ const goToUserDetail = () => {
|
|||||||
// 定时刷新
|
// 定时刷新
|
||||||
const startAutoRefresh = () => {
|
const startAutoRefresh = () => {
|
||||||
refreshTimer.value = setInterval(() => {
|
refreshTimer.value = setInterval(() => {
|
||||||
if (ticketInfo.value?.status !== 'completed') {
|
// 只有当前路由仍在工单详情页且工单未完成时才刷新
|
||||||
|
if (route.path === '/ticket/detail' && route.query.id && ticketInfo.value?.status !== 'completed') {
|
||||||
fetchTicketDetail(false) // 定时刷新时不显示 loading
|
fetchTicketDetail(false) // 定时刷新时不显示 loading
|
||||||
}
|
}
|
||||||
}, 10000)
|
}, 10000)
|
||||||
|
|||||||
+288
-20
@@ -31,14 +31,34 @@
|
|||||||
<el-option label="降序" value="desc" />
|
<el-option label="降序" value="desc" />
|
||||||
<el-option label="升序" value="asc" />
|
<el-option label="升序" value="asc" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<!-- 用户筛选输入框 -->
|
||||||
|
<el-input
|
||||||
|
: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-input
|
<el-input
|
||||||
v-model="searchKeyword"
|
v-model="searchKeyword"
|
||||||
placeholder="搜索工单号、标题、用户名"
|
placeholder="搜索工单标题/内容"
|
||||||
prefix-icon="Search"
|
|
||||||
clearable
|
clearable
|
||||||
style="width: 240px"
|
style="width: 200px"
|
||||||
@input="handleSearch"
|
@input="handleKeywordSearch"
|
||||||
/>
|
@clear="handleKeywordSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
<el-button icon="Refresh" @click="refreshList">刷新</el-button>
|
<el-button icon="Refresh" @click="refreshList">刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,18 +119,74 @@
|
|||||||
@current-change="handlePageChange"
|
@current-change="handlePageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Search, User, Close, ArrowRight } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
getTickerList,
|
getTickerList,
|
||||||
closeTicket,
|
closeTicket,
|
||||||
getTicketCount
|
getTicketCount
|
||||||
} from '@/api/ticket'
|
} from '@/api/ticket'
|
||||||
|
import { getUserList } from '@/api/admin/user'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -122,9 +198,20 @@ const isLoading = ref(false)
|
|||||||
|
|
||||||
// 工单数据
|
// 工单数据
|
||||||
const ticketList = ref([])
|
const ticketList = ref([])
|
||||||
const searchKeyword = ref('')
|
|
||||||
const activeStatus = ref('pending') // 默认选中"待处理"
|
const activeStatus = ref('pending') // 默认选中"待处理"
|
||||||
|
|
||||||
|
// 关键词搜索
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const keywordSearchTimer = ref(null)
|
||||||
|
|
||||||
|
// 用户搜索
|
||||||
|
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 sortBy = ref('') // 默认不排序
|
||||||
const sortOrder = ref('') // 默认不选择排序顺序
|
const sortOrder = ref('') // 默认不选择排序顺序
|
||||||
@@ -138,6 +225,9 @@ const stats = reactive({
|
|||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 自动刷新定时器
|
||||||
|
const autoRefreshTimer = ref(null)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -174,7 +264,9 @@ const fetchTicketList = async () => {
|
|||||||
currentPage.value,
|
currentPage.value,
|
||||||
statusParam,
|
statusParam,
|
||||||
sortBy.value,
|
sortBy.value,
|
||||||
sortOrder.value
|
sortOrder.value,
|
||||||
|
selectedUser.value?.user_id,
|
||||||
|
searchKeyword.value.trim()
|
||||||
)
|
)
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
@@ -218,15 +310,72 @@ const fetchStats = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 过滤后的工单列表
|
// 过滤后的工单列表
|
||||||
const filteredTickets = computed(() => {
|
const filteredTickets = computed(() => ticketList.value)
|
||||||
if (!searchKeyword.value) return ticketList.value
|
|
||||||
const keyword = searchKeyword.value.toLowerCase()
|
// 用户搜索
|
||||||
return ticketList.value.filter(ticket =>
|
const handleUserSearch = () => {
|
||||||
ticket.title.toLowerCase().includes(keyword) ||
|
if (userSearchTimer.value) {
|
||||||
ticket.username.toLowerCase().includes(keyword) ||
|
clearTimeout(userSearchTimer.value)
|
||||||
String(ticket.id).includes(keyword)
|
}
|
||||||
)
|
|
||||||
})
|
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 handleKeywordSearch = () => {
|
||||||
|
if (keywordSearchTimer.value) {
|
||||||
|
clearTimeout(keywordSearchTimer.value)
|
||||||
|
}
|
||||||
|
keywordSearchTimer.value = setTimeout(() => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchTicketList()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
// 按状态过滤
|
// 按状态过滤
|
||||||
const filterByStatus = (status) => {
|
const filterByStatus = (status) => {
|
||||||
@@ -234,6 +383,12 @@ const filterByStatus = (status) => {
|
|||||||
activeStatus.value = status
|
activeStatus.value = status
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
fetchTicketList()
|
fetchTicketList()
|
||||||
|
|
||||||
|
// 切换状态时重新设置定时器
|
||||||
|
stopAutoRefresh()
|
||||||
|
if (status === 'pending') {
|
||||||
|
startAutoRefresh()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 排序变化处理
|
// 排序变化处理
|
||||||
@@ -242,9 +397,6 @@ const handleSortChange = () => {
|
|||||||
fetchTicketList()
|
fetchTicketList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索处理
|
|
||||||
const handleSearch = () => {}
|
|
||||||
|
|
||||||
// 分页处理
|
// 分页处理
|
||||||
const handleSizeChange = () => {
|
const handleSizeChange = () => {
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
@@ -291,11 +443,49 @@ const handleComplete = (ticket) => {
|
|||||||
}).catch(() => {})
|
}).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
|
let isFirstLoad = true
|
||||||
|
|
||||||
|
// 监听对话框关闭,清空搜索状态
|
||||||
|
watch(showUserDialog, (newVal) => {
|
||||||
|
if (!newVal) {
|
||||||
|
// 对话框关闭时清空搜索
|
||||||
|
userSearchKeyword.value = ''
|
||||||
|
userList.value = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchTicketList()
|
fetchTicketList()
|
||||||
fetchStats()
|
fetchStats()
|
||||||
|
|
||||||
|
// 如果默认是待处理状态,启动自动刷新
|
||||||
|
if (activeStatus.value === 'pending') {
|
||||||
|
startAutoRefresh()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 当页面被激活时(从详情页返回时)
|
// 当页面被激活时(从详情页返回时)
|
||||||
@@ -305,6 +495,22 @@ onActivated(() => {
|
|||||||
refreshList()
|
refreshList()
|
||||||
}
|
}
|
||||||
isFirstLoad = false
|
isFirstLoad = false
|
||||||
|
|
||||||
|
// 重新启动自动刷新(如果是待处理状态)
|
||||||
|
if (activeStatus.value === 'pending') {
|
||||||
|
startAutoRefresh()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时清理定时器
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopAutoRefresh()
|
||||||
|
if (userSearchTimer.value) {
|
||||||
|
clearTimeout(userSearchTimer.value)
|
||||||
|
}
|
||||||
|
if (keywordSearchTimer.value) {
|
||||||
|
clearTimeout(keywordSearchTimer.value)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -365,6 +571,68 @@ onActivated(() => {
|
|||||||
gap: 8px;
|
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 {
|
.user-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user