475c62aefc
- 在团购活动页面中为用户ID添加点击跳转到用户详情的功能 - 在团购管理页面中为用户ID添加点击跳转到用户详情的功能 - 在审核相关页面中为容器ID添加点击跳转到容器详情的功能 - 在营销相关页面中为用户ID和订单ID添加点击跳转功能 - 在订单列表页面中为用户ID和商品ID添加点击跳转功能 - 在商品列表页面中为用户和商品添加点击跳转功能 - 在工单列表页面中为用户名添加点击跳转到用户详情的功能 - 在用户虚拟机列表中为商品和用户添加点击跳转功能 - 在用户余额页面中为支付订单ID添加点击跳转到订单详情的功能 - 统一使用el-link组件实现可点击的链接样式 - 添加useRouter依赖并创建router实例用于页面跳转
928 lines
22 KiB
Vue
928 lines
22 KiB
Vue
<template>
|
||
<div class="ticket-list-page">
|
||
<!-- 顶部状态标签栏 -->
|
||
<div class="status-bar">
|
||
<div class="status-tabs">
|
||
<div class="tab-item pending" :class="{ active: activeStatus === 'pending' }" @click="filterByStatus('pending')">
|
||
待处理 <span class="count">{{ stats.pending }}</span>
|
||
</div>
|
||
<div class="tab-item processing" :class="{ active: activeStatus === 'processing' }" @click="filterByStatus('processing')">
|
||
处理中 <span class="count">{{ stats.processing }}</span>
|
||
</div>
|
||
<div class="tab-item replied" :class="{ active: activeStatus === 'replied' }" @click="filterByStatus('replied')">
|
||
已回复 <span class="count">{{ stats.replied }}</span>
|
||
</div>
|
||
<div class="tab-item completed" :class="{ active: activeStatus === 'completed' }" @click="filterByStatus('completed')">
|
||
已完成 <span class="count">{{ stats.completed }}</span>
|
||
</div>
|
||
<div class="tab-item" :class="{ active: activeStatus === '' }" @click="filterByStatus('')">
|
||
全部 <span class="count">{{ stats.total }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 筛选工具栏 -->
|
||
<div class="filter-bar">
|
||
<el-select v-model="sortBy" placeholder="排序方式" clearable style="width: 140px" @change="handleSortChange">
|
||
<el-option label="不排序" value="" />
|
||
<el-option label="创建时间" value="created_at" />
|
||
<el-option label="更新时间" value="updated_at" />
|
||
<el-option label="工单号" value="id" />
|
||
</el-select>
|
||
<el-select v-model="sortOrder" placeholder="排序顺序" clearable style="width: 100px" @change="handleSortChange">
|
||
<el-option label="默认" value="" />
|
||
<el-option label="降序" value="desc" />
|
||
<el-option label="升序" value="asc" />
|
||
</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
|
||
v-model="searchKeyword"
|
||
placeholder="搜索工单标题/内容"
|
||
clearable
|
||
style="width: 200px"
|
||
@input="handleKeywordSearch"
|
||
@clear="handleKeywordSearch"
|
||
>
|
||
<template #prefix>
|
||
<el-icon><Search /></el-icon>
|
||
</template>
|
||
</el-input>
|
||
<el-button icon="Refresh" @click="refreshList">刷新</el-button>
|
||
</div>
|
||
|
||
<!-- 工单表格(PC端) -->
|
||
<el-table
|
||
v-loading="isLoading"
|
||
:data="filteredTickets"
|
||
stripe
|
||
style="width: 100%"
|
||
@row-click="handleRowClick"
|
||
class="desktop-table"
|
||
>
|
||
<el-table-column prop="id" label="工单号" width="100" />
|
||
<el-table-column label="用户" width="180">
|
||
<template #default="{ row }">
|
||
<div class="user-info">
|
||
<el-avatar :size="32" :src="row.avatar">{{ row.username?.charAt(0) }}</el-avatar>
|
||
<el-link v-if="row.userId" type="primary" :underline="false" @click.stop="router.push({ path: '/user/detail', query: { user_id: row.userId } })">{{ row.username }}</el-link>
|
||
<span v-else class="username">{{ row.username }}</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="title" label="工单标题" min-width="200" show-overflow-tooltip />
|
||
<el-table-column label="状态" width="100">
|
||
<template #default="{ row }">
|
||
<el-tag :type="getStatusType(row.status)" size="small">
|
||
{{ getStatusText(row.status) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||
<el-table-column prop="lastReplyTime" label="最后回复" width="180" />
|
||
<el-table-column label="操作" width="150" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" size="small" @click.stop="goToDetail(row)">
|
||
回复
|
||
</el-button>
|
||
<el-button
|
||
v-if="row.status !== 'completed'"
|
||
type="success"
|
||
size="small"
|
||
@click.stop="handleComplete(row)"
|
||
>
|
||
结束
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 移动端卡片列表 -->
|
||
<div class="mobile-ticket-list" v-loading="isLoading">
|
||
<div
|
||
v-for="ticket in filteredTickets"
|
||
:key="ticket.id"
|
||
class="ticket-card"
|
||
@click="goToDetail(ticket)"
|
||
>
|
||
<div class="ticket-card-header">
|
||
<span class="ticket-card-id">#{{ ticket.id }}</span>
|
||
<el-tag :type="getStatusType(ticket.status)" size="small">
|
||
{{ getStatusText(ticket.status) }}
|
||
</el-tag>
|
||
</div>
|
||
<div class="ticket-card-user">
|
||
<el-avatar :size="28" :src="ticket.avatar">{{ ticket.username?.charAt(0) }}</el-avatar>
|
||
<span class="ticket-card-username">{{ ticket.username }}</span>
|
||
</div>
|
||
<div class="ticket-card-title">{{ ticket.title }}</div>
|
||
<div class="ticket-card-footer">
|
||
<span class="ticket-card-time">{{ ticket.createTime }}</span>
|
||
<div class="ticket-card-actions">
|
||
<el-button type="primary" size="small" @click.stop="goToDetail(ticket)">回复</el-button>
|
||
<el-button
|
||
v-if="ticket.status !== 'completed'"
|
||
type="success"
|
||
size="small"
|
||
@click.stop="handleComplete(ticket)"
|
||
>结束</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-empty v-if="filteredTickets.length === 0 && !isLoading" description="暂无工单数据" />
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div class="pagination-wrapper">
|
||
<el-pagination
|
||
v-model:current-page="currentPage"
|
||
v-model:page-size="pageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
:total="totalCount"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@size-change="handleSizeChange"
|
||
@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, 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()
|
||
|
||
// 分页
|
||
const currentPage = ref(1)
|
||
const pageSize = ref(10)
|
||
const totalCount = ref(0)
|
||
const isLoading = ref(false)
|
||
|
||
// 工单数据
|
||
const ticketList = ref([])
|
||
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 sortOrder = ref('') // 默认不选择排序顺序
|
||
|
||
// 统计数据
|
||
const stats = reactive({
|
||
pending: 0,
|
||
processing: 0,
|
||
replied: 0,
|
||
completed: 0,
|
||
total: 0
|
||
})
|
||
|
||
// 自动刷新定时器
|
||
const autoRefreshTimer = ref(null)
|
||
|
||
|
||
|
||
|
||
|
||
// 状态转换
|
||
const convertStatusToString = (status) => {
|
||
const statusMap = { 0: 'pending', 1: 'processing', 2: 'replied', 3: 'completed' }
|
||
return statusMap[status] || 'processing'
|
||
}
|
||
|
||
const getStatusText = (status) => {
|
||
const statusMap = { pending: '待处理', processing: '处理中', replied: '已回复', completed: '已完成' }
|
||
return statusMap[status] || status
|
||
}
|
||
|
||
const getStatusType = (status) => {
|
||
const typeMap = { pending: 'warning', processing: 'primary', replied: 'info', completed: 'success' }
|
||
return typeMap[status] || ''
|
||
}
|
||
|
||
// 获取工单列表
|
||
const fetchTicketList = async () => {
|
||
try {
|
||
isLoading.value = true
|
||
let statusParam = ''
|
||
if (activeStatus.value) {
|
||
const statusMap = { pending: '0', processing: '1', replied: '2', completed: '3' }
|
||
statusParam = statusMap[activeStatus.value] || ''
|
||
}
|
||
|
||
console.log('调用getTickerList,排序参数:', { sortBy: sortBy.value, sortOrder: sortOrder.value })
|
||
const res = await getTickerList(
|
||
pageSize.value,
|
||
currentPage.value,
|
||
statusParam,
|
||
sortBy.value,
|
||
sortOrder.value,
|
||
selectedUser.value?.user_id,
|
||
searchKeyword.value.trim()
|
||
)
|
||
|
||
if (res.code === 200) {
|
||
ticketList.value = (res.data.data || []).map(item => ({
|
||
id: item.work_id,
|
||
title: item.name,
|
||
username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`,
|
||
userId: item.user?.userId,
|
||
avatar: item.user?.coverUrl || '',
|
||
createTime: new Date(item.created_at).toLocaleString(),
|
||
lastReplyTime: new Date(item.update_time).toLocaleString(),
|
||
status: convertStatusToString(item.status)
|
||
}))
|
||
totalCount.value = res.data.all_count || 0
|
||
} else {
|
||
ElMessage.error(res.message || '获取工单列表失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('获取工单列表出错:', error)
|
||
ElMessage.error('网络错误,请稍后重试')
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 获取统计数据
|
||
const fetchStats = async () => {
|
||
try {
|
||
const res = await getTicketCount()
|
||
if (res.code === 200) {
|
||
const data = res.data
|
||
stats.total = data.all_count
|
||
stats.pending = data.wait_count
|
||
stats.replied = data.reply_count
|
||
stats.completed = data.close_count
|
||
stats.processing = data.all_count - data.wait_count - data.reply_count - data.close_count
|
||
}
|
||
} catch (error) {
|
||
console.error('获取统计数据出错:', error)
|
||
}
|
||
}
|
||
|
||
// 过滤后的工单列表
|
||
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: 10, 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) => {
|
||
if (activeStatus.value === status) return
|
||
activeStatus.value = status
|
||
currentPage.value = 1
|
||
fetchTicketList()
|
||
|
||
// 切换状态时重新设置定时器
|
||
stopAutoRefresh()
|
||
if (status === 'pending') {
|
||
startAutoRefresh()
|
||
}
|
||
}
|
||
|
||
// 排序变化处理
|
||
const handleSortChange = () => {
|
||
currentPage.value = 1
|
||
fetchTicketList()
|
||
}
|
||
|
||
// 分页处理
|
||
const handleSizeChange = () => {
|
||
currentPage.value = 1
|
||
fetchTicketList()
|
||
}
|
||
|
||
const handlePageChange = () => {
|
||
fetchTicketList()
|
||
}
|
||
|
||
// 刷新列表
|
||
const refreshList = () => {
|
||
fetchTicketList()
|
||
fetchStats()
|
||
}
|
||
|
||
// 跳转到详情页
|
||
const goToDetail = (row) => {
|
||
router.push({ path: '/ticket/detail', query: { id: row.id } })
|
||
}
|
||
|
||
const handleRowClick = (row) => {
|
||
goToDetail(row)
|
||
}
|
||
|
||
// 结束工单
|
||
const handleComplete = (ticket) => {
|
||
ElMessageBox.confirm('确定要结束此工单吗?结束后将无法继续回复。', '确认操作', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
try {
|
||
const res = await closeTicket(ticket.id)
|
||
if (res.code === 200) {
|
||
ElMessage.success('工单已成功结束')
|
||
refreshList()
|
||
} else {
|
||
ElMessage.error(res.message || '结束工单失败')
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error('网络错误,请稍后重试')
|
||
}
|
||
}).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()
|
||
}
|
||
})
|
||
|
||
// 当页面被激活时(从详情页返回时)
|
||
onActivated(() => {
|
||
// 跳过首次加载,只在从其他页面返回时刷新
|
||
if (!isFirstLoad) {
|
||
refreshList()
|
||
}
|
||
isFirstLoad = false
|
||
|
||
// 重新启动自动刷新(如果是待处理状态)
|
||
if (activeStatus.value === 'pending') {
|
||
startAutoRefresh()
|
||
}
|
||
})
|
||
|
||
// 组件卸载时清理定时器
|
||
onBeforeUnmount(() => {
|
||
stopAutoRefresh()
|
||
if (userSearchTimer.value) {
|
||
clearTimeout(userSearchTimer.value)
|
||
}
|
||
if (keywordSearchTimer.value) {
|
||
clearTimeout(keywordSearchTimer.value)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.ticket-list-page {
|
||
padding: 0;
|
||
height: calc(100vh - 100px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #fff;
|
||
}
|
||
|
||
.status-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
padding: 14px 20px 0;
|
||
}
|
||
|
||
.status-tabs {
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
|
||
.tab-item {
|
||
padding: 6px 16px;
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
transition: all 0.2s;
|
||
user-select: none;
|
||
}
|
||
|
||
.tab-item:hover {
|
||
background: #f0f2f5;
|
||
}
|
||
|
||
.tab-item.active {
|
||
background: #409eff;
|
||
color: #fff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.tab-item.pending.active { background: #e6a23c; }
|
||
.tab-item.processing.active { background: #409eff; }
|
||
.tab-item.replied.active { background: #909399; }
|
||
.tab-item.completed.active { background: #67c23a; }
|
||
|
||
.tab-item .count {
|
||
margin-left: 4px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.filter-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
padding: 12px 20px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.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;
|
||
gap: 8px;
|
||
}
|
||
|
||
.username {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.pagination-wrapper {
|
||
padding: 12px 20px;
|
||
border-top: 1px solid #ebeef5;
|
||
}
|
||
|
||
:deep(.el-table) {
|
||
flex: 1;
|
||
}
|
||
|
||
:deep(.el-table tr) {
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* 移动端卡片列表 */
|
||
.mobile-ticket-list {
|
||
display: none;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
}
|
||
|
||
.ticket-card {
|
||
background: #fff;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.ticket-card:active {
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
.ticket-card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.ticket-card-id {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
.ticket-card-user {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.ticket-card-username {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #303133;
|
||
}
|
||
|
||
.ticket-card-title {
|
||
font-size: 14px;
|
||
color: #303133;
|
||
margin-bottom: 8px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.ticket-card-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.ticket-card-time {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
.ticket-card-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* 大屏平板尺寸响应式样式 (1020px - 1280px) */
|
||
@media (max-width: 1280px) and (min-width: 1021px) {
|
||
.filter-bar {
|
||
padding: 10px 16px;
|
||
gap: 8px;
|
||
}
|
||
|
||
.filter-bar .el-select {
|
||
width: 120px !important;
|
||
}
|
||
|
||
.filter-bar .el-input {
|
||
min-width: 160px;
|
||
}
|
||
|
||
:deep(.el-table) {
|
||
font-size: 13px;
|
||
}
|
||
|
||
:deep(.el-table .el-table__cell) {
|
||
padding: 10px 8px;
|
||
}
|
||
}
|
||
|
||
/* 平板尺寸响应式样式 (769px - 1020px) */
|
||
@media (max-width: 1020px) and (min-width: 769px) {
|
||
.status-bar {
|
||
padding: 10px 16px 0;
|
||
}
|
||
|
||
.status-tabs {
|
||
width: 100%;
|
||
justify-content: flex-start;
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.status-tabs::-webkit-scrollbar {
|
||
height: 4px;
|
||
}
|
||
|
||
.status-tabs::-webkit-scrollbar-thumb {
|
||
background: #dcdfe6;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.filter-bar {
|
||
padding: 10px 16px;
|
||
gap: 8px;
|
||
}
|
||
|
||
.filter-bar .el-select {
|
||
width: 120px !important;
|
||
}
|
||
|
||
.filter-bar .el-input {
|
||
flex: 1;
|
||
min-width: 150px;
|
||
}
|
||
|
||
:deep(.el-table) {
|
||
font-size: 13px;
|
||
}
|
||
|
||
:deep(.el-table .el-table__cell) {
|
||
padding: 8px 6px;
|
||
}
|
||
}
|
||
|
||
/* 移动端响应式样式 */
|
||
@media (max-width: 768px) {
|
||
.ticket-list-page {
|
||
height: auto;
|
||
min-height: calc(100vh - 60px);
|
||
}
|
||
|
||
.status-bar {
|
||
padding: 10px 12px 0;
|
||
}
|
||
|
||
.status-tabs {
|
||
width: 100%;
|
||
overflow-x: auto;
|
||
padding-bottom: 4px;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.status-tabs::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
.tab-item {
|
||
flex-shrink: 0;
|
||
padding: 8px 12px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.filter-bar {
|
||
padding: 10px 12px;
|
||
gap: 8px;
|
||
}
|
||
|
||
.filter-bar .el-select,
|
||
.filter-bar .el-input {
|
||
flex: 1;
|
||
min-width: 120px;
|
||
}
|
||
|
||
.filter-bar .el-button {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 隐藏PC端表格,显示移动端卡片 */
|
||
:deep(.el-table) {
|
||
display: none !important;
|
||
}
|
||
|
||
.mobile-ticket-list {
|
||
display: flex;
|
||
}
|
||
|
||
.pagination-wrapper {
|
||
padding: 12px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.pagination-wrapper :deep(.el-pagination) {
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pagination-wrapper :deep(.el-pagination__sizes),
|
||
.pagination-wrapper :deep(.el-pagination__jump) {
|
||
display: none;
|
||
}
|
||
|
||
/* 用户选择弹窗移动端适配 */
|
||
:deep(.el-dialog) {
|
||
width: 90% !important;
|
||
margin: 5vh auto !important;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.tab-item {
|
||
padding: 6px 10px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.tab-item .count {
|
||
display: none;
|
||
}
|
||
}
|
||
</style>
|