Files
vue-tickets/src/views/TicketManagement.vue

322 lines
8.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="ticket-management">
<div class="stats-container">
<div class="stat-item total">
<div class="stat-label">总工单数</div>
<div class="stat-value">{{ allStats.total }}</div>
</div>
<div class="stat-item pending">
<div class="stat-label">未分配</div>
<div class="stat-value">{{ allStats.pending }}</div>
</div>
<div class="stat-item ongoing">
<div class="stat-label">进行中</div>
<div class="stat-value">{{ allStats.ongoing }}</div>
</div>
<div class="stat-item success">
<div class="stat-label">已完成/已完结</div>
<div class="stat-value">{{ allStats.completed }}</div>
</div>
</div>
<div class="main-content">
<div class="action-bar">
<div class="left">
<el-button type="primary" @click="handleAdd" :icon="Plus">新增任务</el-button>
</div>
<TicketFilter :filters="filters" :operators="operators" @filter-change="handleFilterChange"
@reset="handleFilterReset" />
</div>
<TicketList :tickets="tickets" :loading="loading" @edit="handleEdit" @delete="handleDelete" />
<div class="pagination-container">
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
:total="pagination.total" :page-sizes="[15, 30, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</div>
<TicketForm v-model="showAddDialog" :form-data="currentTicket" :operators="operators" :is-editing="isEditing"
@submit="handleFormSubmit" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { ticketApi } from '@/api/ticket'
import TicketFilter from '@/components/TicketFilter.vue'
import TicketList from '@/components/TicketList.vue'
import TicketForm from '@/components/TicketForm.vue'
// --- 响应式数据 ---
const tickets = ref([])
const operators = ref([])
const loading = ref(false)
const showAddDialog = ref(false)
const isEditing = ref(false)
const currentTicket = ref({})
// 独立统计数据:专门给顶部的卡片使用
const allStats = reactive({
total: 0,
pending: 0,
ongoing: 0,
completed: 0
})
const pagination = reactive({
page: 1,
limit: 15,
total: 0
})
const filters = reactive({
status: null,
operator_name: null,
principalpeople: null,
search: null,
sort_order: 'desc'
})
// --- 方法 ---
/**
* 核心:获取全局统计数据
* 逻辑:发送一个不带任何 filters 的请求,专门计算总数
*/
const initGlobalStats = async () => {
try {
// 请求第一页,且不带任何筛选条件(filters)
// 目的只是为了拿到全量数据来统计卡片
const response = await ticketApi.getTickets({ page: 1, limit: 1000 })
if (response.code) {
const allData = response.data.data
allStats.total = response.data.total
allStats.pending = allData.filter(t => t.is_complete === '未分配').length
allStats.ongoing = allData.filter(t => t.is_complete === '进行中').length
allStats.completed = allData.filter(t => t.is_complete === '已完成' || t.is_complete === '已完结').length
}
} catch (error) {
console.error('统计全局数据失败:', error)
}
}
/**
* 加载列表数据(受分页和筛选影响)
*/
const loadTickets = async () => {
loading.value = true
try {
const params = {
page: pagination.page,
limit: pagination.limit,
...filters
}
const response = await ticketApi.getTickets(params)
// console.log('工单列表响应:', response)
if (response.code) {
tickets.value = response.data.data
pagination.total = response.total
}
} catch (error) {
ElMessage.error('加载列表失败')
} finally {
loading.value = false
}
}
// 筛选逻辑
const handleFilterChange = (newFilters) => {
Object.assign(filters, newFilters)
pagination.page = 1
loadTickets() // 只刷新列表,不刷新卡片
}
const handleFilterReset = () => {
Object.assign(filters, { status: null, operator_name: null, principalpeople: null, search: null, sort_order: 'desc' })
pagination.page = 1
loadTickets()
}
// 新增/编辑/删除后,需要同时刷新列表和统计卡片
const refreshAll = () => {
loadTickets()
initGlobalStats()
}
const handleFormSubmit = async (formData) => {
// ... 提交逻辑 ...
// 成功后调用 refreshAll()
}
const handleDelete = async (id) => {
try {
// 1. 弹出二次确认框,防止用户点错
await ElMessageBox.confirm(
'确定要删除该工单吗?此操作不可逆',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
// 2. 开启 loading(如果有的话)
// loading.value = true
// 3. 调用 API 发送删除请求
const res = await ticketApi.deleteTicket(id)
// console.log('删除响应:', res)
// 4. 根据你的后端返回结构判断是否成功
// 假设后端返回 code 为 200 表示成功
if (res.code) {
ElMessage.success('删除成功')
// 5. 成功后调用刷新列表的方法
if (typeof refreshAll === 'function') {
refreshAll()
}
} else {
ElMessage.error(res.message || '删除失败')
}
} catch (error) {
// 处理取消操作或接口报错
if (error !== 'cancel') {
console.error('删除出错:', error)
ElMessage.error('系统错误,删除失败')
}
} finally {
// loading.value = false
}
}
/**
* 新增任务:清空表单并打开弹窗
*/
const handleAdd = () => {
isEditing.value = false
// 初始化一个空的工单对象
currentTicket.value = {
website: '',
operator: '',
principalpeople: '',
is_complete: '未分配'
}
showAddDialog.value = true
}
/**
* 编辑任务:克隆数据并打开弹窗
*/
const handleEdit = (row) => {
isEditing.value = true
// 使用解构赋值 {...row} 进行浅拷贝,避免修改弹窗时直接影响表格行
currentTicket.value = { ...row }
showAddDialog.value = true
}
/**
* 加载运营人员列表(用于弹窗下拉框)
*/
const loadOperators = async () => {
try {
const response = await ticketApi.getOperators()
console.log('运营人员列表响应:', response.data)
// 这里的判断逻辑要根据你后端的实际返回结构调整
// 如果后端直接返回数组:operators.value = response
// 如果后端返回带 code 的结构:
if (response.code) {
operators.value = response.data || response
}
} catch (error) {
console.error('加载运营人员失败:', error)
}
}
// 分页逻辑
const handleSizeChange = (val) => { pagination.limit = val; loadTickets(); }
const handleCurrentChange = (val) => { pagination.page = val; loadTickets(); }
// 初始化
onMounted(() => {
loadTickets() // 渲染表格
initGlobalStats() // 锁定卡片统计
loadOperators()
})
</script>
<style scoped>
/* 保持原有样式不变 */
.ticket-management {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
}
.stats-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 20px;
}
.stat-item {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
border-left: 5px solid #409eff;
}
.stat-item.pending {
border-left-color: #909399;
}
.stat-item.ongoing {
border-left-color: #e6a23c;
}
.stat-item.success {
border-left-color: #67c23a;
}
.stat-label {
font-size: 14px;
color: #666;
}
.stat-value {
font-size: 26px;
font-weight: bold;
color: #333;
margin-top: 5px;
}
.main-content {
background: white;
padding: 20px;
border-radius: 8px;
}
.action-bar {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>