refactor(tickets): 删除工单处理功能及相关导航入口
- 从控制台导航中移除工单管理菜单项 - 从开发者导航中移除工单处理菜单项 - 完全删除工单处理页面及其代码实现 - 移除所有与工单列表、详情、回复和分配相关的UI组件和逻辑 - 清理工单处理相关的样式和API调用代码
This commit is contained in:
@@ -1,552 +0,0 @@
|
||||
<template>
|
||||
<div class="tickets-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">🎫 工单处理</h2>
|
||||
<p class="page-desc">处理用户提交的技术支持工单,分配并跟进处理进度</p>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button @click="loadTickets" :loading="loading">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="12" :md="6" v-for="stat in statCards" :key="stat.key">
|
||||
<div
|
||||
class="stat-card"
|
||||
:class="[stat.color, { active: filterStatus === stat.key }]"
|
||||
@click="handleStatFilter(stat.key)"
|
||||
>
|
||||
<div class="stat-icon">{{ stat.icon }}</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 工单列表 -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">📋 工单列表</span>
|
||||
<a-space wrap>
|
||||
<a-select v-model:value="filterStatus" style="width: 130px" @change="handleSearch">
|
||||
<a-select-option value="">全部状态</a-select-option>
|
||||
<a-select-option value="pending">待处理</a-select-option>
|
||||
<a-select-option value="assigned">已分配</a-select-option>
|
||||
<a-select-option value="processing">处理中</a-select-option>
|
||||
<a-select-option value="resolved">已解决</a-select-option>
|
||||
<a-select-option value="closed">已关闭</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="filterPriority" style="width: 110px" @change="handleSearch">
|
||||
<a-select-option value="">全部优先级</a-select-option>
|
||||
<a-select-option value="urgent">紧急</a-select-option>
|
||||
<a-select-option value="high">高</a-select-option>
|
||||
<a-select-option value="normal">普通</a-select-option>
|
||||
<a-select-option value="low">低</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="filterCategory" style="width: 110px" @change="handleSearch">
|
||||
<a-select-option value="">全部分类</a-select-option>
|
||||
<a-select-option value="bug">Bug反馈</a-select-option>
|
||||
<a-select-option value="feature">功能请求</a-select-option>
|
||||
<a-select-option value="consultation">咨询</a-select-option>
|
||||
<a-select-option value="complaint">投诉</a-select-option>
|
||||
<a-select-option value="other">其他</a-select-option>
|
||||
</a-select>
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索工单标题"
|
||||
style="width: 200px"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tickets"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="ticketId"
|
||||
@change="handleTableChange"
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 工单信息 -->
|
||||
<template v-if="column.key === 'ticketInfo'">
|
||||
<div class="ticket-info">
|
||||
<div class="ticket-title">
|
||||
<a-tag v-if="record.hasUnread" color="red" style="font-size:10px;padding:0 4px">NEW</a-tag>
|
||||
{{ record.title }}
|
||||
</div>
|
||||
<div class="ticket-no">{{ record.ticketNo }}</div>
|
||||
<div class="ticket-meta">
|
||||
<span>来自:{{ record.submitUserName || '-' }}</span>
|
||||
<span v-if="record.productName" style="margin-left:8px">应用:{{ record.productName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 分类 -->
|
||||
<template v-if="column.key === 'category'">
|
||||
<a-tag :color="categoryColor(record.category)">{{ categoryText(record.category) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 优先级 -->
|
||||
<template v-if="column.key === 'priority'">
|
||||
<span :class="['priority-badge', 'priority-' + record.priority]">
|
||||
{{ priorityText(record.priority) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- 状态 -->
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="statusColor(record.status)">{{ statusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 负责人 -->
|
||||
<template v-if="column.key === 'assignee'">
|
||||
<div v-if="record.assigneeName" class="assignee-cell">
|
||||
<a-avatar :size="24" :src="record.assigneeAvatar">
|
||||
<template #icon><UserOutlined /></template>
|
||||
</a-avatar>
|
||||
<span>{{ record.assigneeName }}</span>
|
||||
</div>
|
||||
<a-button v-else type="link" size="small" @click="handleAssign(record)">
|
||||
<PlusOutlined /> 分配
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 提交时间 -->
|
||||
<template v-if="column.key === 'createTime'">
|
||||
<span class="text-sm text-gray">{{ record.createTime?.substring(0, 16) || '-' }}</span>
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleView(record)">处理</a-button>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button type="link" size="small">更多 <DownOutlined /></a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }) => handleQuickStatus(key as string, record)">
|
||||
<a-menu-item key="processing" v-if="['pending','assigned'].includes(record.status)">🔄 开始处理</a-menu-item>
|
||||
<a-menu-item key="resolved" v-if="['processing','assigned'].includes(record.status)">✅ 标记已解决</a-menu-item>
|
||||
<a-menu-item key="closed" v-if="record.status !== 'closed'">🔒 关闭工单</a-menu-item>
|
||||
<a-menu-item key="rejected">❌ 拒绝工单</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 工单详情/处理弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showDetailModal"
|
||||
:title="`工单处理:${currentTicket?.ticketNo || ''}`"
|
||||
width="780px"
|
||||
:footer="null"
|
||||
destroy-on-close
|
||||
>
|
||||
<template v-if="currentTicket">
|
||||
<!-- 基本信息 -->
|
||||
<a-descriptions :column="3" size="small" class="mb-4">
|
||||
<a-descriptions-item label="工单标题" :span="3">
|
||||
<strong>{{ currentTicket.title }}</strong>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="statusColor(currentTicket.status)">{{ statusText(currentTicket.status) }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="优先级">
|
||||
<span :class="['priority-badge', 'priority-' + currentTicket.priority]">{{ priorityText(currentTicket.priority) }}</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="分类">
|
||||
<a-tag :color="categoryColor(currentTicket.category)">{{ categoryText(currentTicket.category) }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="提交人">{{ currentTicket.submitUserName || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="关联应用">{{ currentTicket.productName || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="负责人">{{ currentTicket.assigneeName || '未分配' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="提交时间" :span="3">{{ currentTicket.createTime }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 工单内容 -->
|
||||
<div class="ticket-content-box">
|
||||
<div class="ticket-content-title">📝 工单内容</div>
|
||||
<div class="ticket-content-body">{{ currentTicket.content }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 回复区 -->
|
||||
<div class="reply-section">
|
||||
<div class="reply-title">💬 回复记录</div>
|
||||
<a-spin v-if="loadingReplies" style="padding:20px;display:block;text-align:center" />
|
||||
<div v-else-if="replies.length === 0" class="reply-empty">暂无回复</div>
|
||||
<div v-else class="reply-list">
|
||||
<div v-for="reply in replies" :key="reply.replyId" class="reply-item" :class="{ 'reply-staff': reply.isStaff }">
|
||||
<a-avatar :size="32" :src="reply.userAvatar">
|
||||
<template #icon><UserOutlined /></template>
|
||||
</a-avatar>
|
||||
<div class="reply-bubble">
|
||||
<div class="reply-meta">
|
||||
<span class="reply-name">{{ reply.userName }}</span>
|
||||
<a-tag v-if="reply.isStaff" color="blue" style="font-size:10px;padding:0 4px">客服</a-tag>
|
||||
<span class="reply-time">{{ reply.createTime?.substring(0, 16) }}</span>
|
||||
</div>
|
||||
<div class="reply-content">{{ reply.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回复输入框 -->
|
||||
<div class="reply-input-area">
|
||||
<a-textarea
|
||||
v-model:value="replyContent"
|
||||
:rows="3"
|
||||
placeholder="输入回复内容..."
|
||||
:maxlength="2000"
|
||||
show-count
|
||||
/>
|
||||
<div class="reply-actions">
|
||||
<a-space>
|
||||
<a-select v-model:value="quickStatus" style="width: 140px" placeholder="同时更新状态">
|
||||
<a-select-option value="">不更新状态</a-select-option>
|
||||
<a-select-option value="processing">更新为处理中</a-select-option>
|
||||
<a-select-option value="resolved">更新为已解决</a-select-option>
|
||||
<a-select-option value="closed">更新为已关闭</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" :loading="replying" @click="handleSubmitReply">发送回复</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<!-- 分配弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showAssignModal"
|
||||
title="分配工单"
|
||||
:confirm-loading="assigning"
|
||||
@ok="confirmAssign"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="选择负责人" required>
|
||||
<a-select v-model:value="assigneeId" placeholder="请选择技术人员" style="width:100%">
|
||||
<a-select-option v-for="staff in staffList" :key="staff.userId" :value="staff.userId">
|
||||
{{ staff.nickname }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ReloadOutlined, UserOutlined, PlusOutlined, DownOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
getAllTickets, getTicketDetail, getTicketReplies, replyTicket,
|
||||
updateTicketStatus, assignTicket, getTechStaffList, getTicketStats,
|
||||
} from '@/api/ticket/index'
|
||||
import type { Ticket, TicketReply, TicketStatus } from '@/api/ticket/model'
|
||||
|
||||
definePageMeta({ layout: 'admin' })
|
||||
useHead({ title: '工单处理 - 平台管理' })
|
||||
|
||||
const loading = ref(false)
|
||||
const tickets = ref<Ticket[]>([])
|
||||
const filterStatus = ref<TicketStatus | ''>('')
|
||||
const filterPriority = ref('')
|
||||
const filterCategory = ref('')
|
||||
const searchKeyword = ref('')
|
||||
|
||||
const pagination = reactive({ current: 1, pageSize: 20, total: 0, showSizeChanger: true, showQuickJumper: true })
|
||||
|
||||
const statCards = reactive([
|
||||
{ key: 'pending', icon: '⏳', label: '待处理', value: 0, color: 'orange' },
|
||||
{ key: 'processing', icon: '🔄', label: '处理中', value: 0, color: 'blue' },
|
||||
{ key: 'resolved', icon: '✅', label: '已解决', value: 0, color: 'green' },
|
||||
{ key: '', icon: '📋', label: '全部工单', value: 0, color: 'gray' },
|
||||
])
|
||||
|
||||
const columns = [
|
||||
{ title: '工单信息', key: 'ticketInfo', width: 280 },
|
||||
{ title: '分类', key: 'category', width: 100 },
|
||||
{ title: '优先级', key: 'priority', width: 90 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '负责人', key: 'assignee', width: 140 },
|
||||
{ title: '提交时间', key: 'createTime', width: 140 },
|
||||
{ title: '操作', key: 'action', width: 150 },
|
||||
]
|
||||
|
||||
// 详情弹窗
|
||||
const showDetailModal = ref(false)
|
||||
const currentTicket = ref<Ticket | null>(null)
|
||||
const replies = ref<TicketReply[]>([])
|
||||
const loadingReplies = ref(false)
|
||||
const replyContent = ref('')
|
||||
const replying = ref(false)
|
||||
const quickStatus = ref('')
|
||||
|
||||
// 分配弹窗
|
||||
const showAssignModal = ref(false)
|
||||
const assigning = ref(false)
|
||||
const assigneeId = ref<number | null>(null)
|
||||
const assignTarget = ref<Ticket | null>(null)
|
||||
const staffList = ref<{ userId: number; nickname: string; avatar: string }[]>([])
|
||||
|
||||
async function loadTickets() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getAllTickets({
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
status: filterStatus.value as TicketStatus || undefined,
|
||||
priority: filterPriority.value as any || undefined,
|
||||
category: filterCategory.value as any || undefined,
|
||||
keywords: searchKeyword.value || undefined,
|
||||
})
|
||||
// ticket API 直接返回 axios response,data 字段即 {list, count}
|
||||
const data = (res as any)?.data ?? res
|
||||
tickets.value = data?.list || []
|
||||
pagination.total = data?.count || 0
|
||||
loadStats()
|
||||
} catch {
|
||||
message.error('加载工单列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await getTicketStats()
|
||||
const stats = (res as any)?.data ?? res
|
||||
statCards[0].value = stats?.pending || 0
|
||||
statCards[1].value = stats?.processing || 0
|
||||
statCards[2].value = stats?.resolved || 0
|
||||
statCards[3].value = stats?.total || 0
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function handleStatFilter(key: string) {
|
||||
filterStatus.value = key as any
|
||||
pagination.current = 1
|
||||
loadTickets()
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.current = 1
|
||||
loadTickets()
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadTickets()
|
||||
}
|
||||
|
||||
async function handleView(record: Ticket) {
|
||||
currentTicket.value = record
|
||||
showDetailModal.value = true
|
||||
replyContent.value = ''
|
||||
quickStatus.value = ''
|
||||
loadingReplies.value = true
|
||||
try {
|
||||
const res = await getTicketReplies(record.ticketId)
|
||||
const data = (res as any)?.data ?? res
|
||||
replies.value = Array.isArray(data) ? data : (data?.list || [])
|
||||
} catch {
|
||||
replies.value = []
|
||||
} finally {
|
||||
loadingReplies.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmitReply() {
|
||||
if (!replyContent.value.trim()) {
|
||||
message.warning('请输入回复内容')
|
||||
return
|
||||
}
|
||||
replying.value = true
|
||||
try {
|
||||
await replyTicket({ ticketId: currentTicket.value!.ticketId, content: replyContent.value })
|
||||
if (quickStatus.value) {
|
||||
await updateTicketStatus({ ticketId: currentTicket.value!.ticketId, status: quickStatus.value as TicketStatus })
|
||||
}
|
||||
message.success('回复已发送')
|
||||
replyContent.value = ''
|
||||
quickStatus.value = ''
|
||||
// 重新加载回复
|
||||
const res = await getTicketReplies(currentTicket.value!.ticketId)
|
||||
const data = (res as any)?.data ?? res
|
||||
replies.value = Array.isArray(data) ? data : (data?.list || [])
|
||||
loadTickets()
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '发送失败')
|
||||
} finally {
|
||||
replying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleQuickStatus(key: string, record: Ticket) {
|
||||
try {
|
||||
await updateTicketStatus({ ticketId: record.ticketId, status: key as TicketStatus })
|
||||
message.success('状态已更新')
|
||||
loadTickets()
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAssign(record: Ticket) {
|
||||
assignTarget.value = record
|
||||
assigneeId.value = null
|
||||
showAssignModal.value = true
|
||||
try {
|
||||
const res = await getTechStaffList()
|
||||
const data = (res as any)?.data ?? res
|
||||
staffList.value = Array.isArray(data) ? data : []
|
||||
} catch { staffList.value = [] }
|
||||
}
|
||||
|
||||
async function confirmAssign() {
|
||||
if (!assigneeId.value) {
|
||||
message.warning('请选择负责人')
|
||||
return
|
||||
}
|
||||
assigning.value = true
|
||||
try {
|
||||
await assignTicket({ ticketId: assignTarget.value!.ticketId, assigneeId: assigneeId.value })
|
||||
message.success('工单已分配')
|
||||
showAssignModal.value = false
|
||||
loadTickets()
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '分配失败')
|
||||
} finally {
|
||||
assigning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function statusText(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待处理', assigned: '已分配', processing: '处理中',
|
||||
resolved: '已解决', closed: '已关闭', rejected: '已拒绝',
|
||||
}
|
||||
return map[status || ''] || status || '-'
|
||||
}
|
||||
|
||||
function statusColor(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'orange', assigned: 'blue', processing: 'processing',
|
||||
resolved: 'success', closed: 'default', rejected: 'error',
|
||||
}
|
||||
return map[status || ''] || 'default'
|
||||
}
|
||||
|
||||
function priorityText(p?: string) {
|
||||
const map: Record<string, string> = { urgent: '紧急', high: '高', normal: '普通', low: '低' }
|
||||
return map[p || ''] || p || '-'
|
||||
}
|
||||
|
||||
function categoryText(c?: string) {
|
||||
const map: Record<string, string> = { bug: 'Bug反馈', feature: '功能请求', consultation: '咨询', complaint: '投诉', other: '其他' }
|
||||
return map[c || ''] || c || '-'
|
||||
}
|
||||
|
||||
function categoryColor(c?: string) {
|
||||
const map: Record<string, string> = { bug: 'error', feature: 'blue', consultation: 'cyan', complaint: 'warning', other: 'default' }
|
||||
return map[c || ''] || 'default'
|
||||
}
|
||||
|
||||
onMounted(() => loadTickets())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tickets-page { min-height: 100%; }
|
||||
|
||||
.page-header {
|
||||
display: flex; align-items: center;
|
||||
justify-content: space-between; margin-bottom: 20px;
|
||||
}
|
||||
.page-title { font-size: 18px; font-weight: 700; color: #1f2937; margin: 0; }
|
||||
.page-desc { font-size: 13px; color: #9ca3af; margin: 2px 0 0; }
|
||||
|
||||
.stat-card {
|
||||
display: flex; align-items: center; gap: 12px; padding: 16px;
|
||||
border-radius: 12px; border: 2px solid transparent; cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
|
||||
.stat-card.active { box-shadow: 0 4px 12px rgba(0,0,0,0.12); }
|
||||
.stat-card.blue { background: #eff6ff; border-color: #dbeafe; }
|
||||
.stat-card.orange { background: #fff7ed; border-color: #fed7aa; }
|
||||
.stat-card.green { background: #f0fdf4; border-color: #bbf7d0; }
|
||||
.stat-card.gray { background: #f9fafb; border-color: #e5e7eb; }
|
||||
.stat-card.active.blue { border-color: #3b82f6; }
|
||||
.stat-card.active.orange { border-color: #f97316; }
|
||||
.stat-card.active.green { border-color: #22c55e; }
|
||||
.stat-icon { font-size: 28px; flex-shrink: 0; }
|
||||
.stat-value { font-size: 22px; font-weight: 700; color: rgba(0,0,0,0.85); line-height: 1.2; }
|
||||
.stat-label { font-size: 12px; color: rgba(0,0,0,0.45); margin-top: 2px; }
|
||||
|
||||
.panel { background: #fff; border: 1px solid #f0f0f0; border-radius: 12px; overflow: hidden; }
|
||||
.panel-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 14px 18px; border-bottom: 1px solid #f5f5f5; flex-wrap: wrap; gap: 10px;
|
||||
}
|
||||
.panel-title { font-size: 14px; font-weight: 600; color: rgba(0,0,0,0.85); }
|
||||
|
||||
.ticket-info .ticket-title { font-size: 14px; font-weight: 500; color: rgba(0,0,0,0.85); margin-bottom: 3px; }
|
||||
.ticket-no { font-size: 11px; color: #4f46e5; font-family: monospace; }
|
||||
.ticket-meta { font-size: 11px; color: rgba(0,0,0,0.45); margin-top: 2px; }
|
||||
|
||||
.priority-badge { font-size: 12px; font-weight: 600; padding: 2px 8px; border-radius: 4px; }
|
||||
.priority-urgent { color: #fff; background: #ef4444; }
|
||||
.priority-high { color: #fff; background: #f97316; }
|
||||
.priority-normal { color: #1d4ed8; background: #dbeafe; }
|
||||
.priority-low { color: rgba(0,0,0,0.45); background: #f3f4f6; }
|
||||
|
||||
.assignee-cell { display: flex; align-items: center; gap: 6px; font-size: 13px; }
|
||||
|
||||
/* 详情弹窗 */
|
||||
.ticket-content-box {
|
||||
background: #fafafa; border-radius: 8px; padding: 14px;
|
||||
margin-bottom: 20px; border: 1px solid #f0f0f0;
|
||||
}
|
||||
.ticket-content-title { font-size: 13px; font-weight: 600; color: rgba(0,0,0,0.65); margin-bottom: 8px; }
|
||||
.ticket-content-body { font-size: 14px; color: rgba(0,0,0,0.85); white-space: pre-wrap; word-break: break-word; line-height: 1.8; }
|
||||
|
||||
.reply-section { margin-top: 4px; }
|
||||
.reply-title { font-size: 13px; font-weight: 600; color: rgba(0,0,0,0.65); margin-bottom: 12px; }
|
||||
.reply-empty { text-align: center; color: rgba(0,0,0,0.45); padding: 20px; font-size: 13px; }
|
||||
.reply-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 16px; max-height: 300px; overflow-y: auto; }
|
||||
.reply-item { display: flex; gap: 10px; }
|
||||
.reply-staff { flex-direction: row-reverse; }
|
||||
.reply-bubble { flex: 1; }
|
||||
.reply-meta { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; flex-wrap: wrap; }
|
||||
.reply-staff .reply-meta { flex-direction: row-reverse; }
|
||||
.reply-name { font-size: 13px; font-weight: 500; color: rgba(0,0,0,0.65); }
|
||||
.reply-time { font-size: 11px; color: rgba(0,0,0,0.35); }
|
||||
.reply-content {
|
||||
font-size: 13px; line-height: 1.6; padding: 10px 14px;
|
||||
background: #f5f5f5; border-radius: 8px; word-break: break-word;
|
||||
display: inline-block; max-width: 100%;
|
||||
}
|
||||
.reply-staff .reply-content { background: #e0f2fe; }
|
||||
|
||||
.reply-input-area { border-top: 1px solid #f0f0f0; padding-top: 14px; margin-top: 4px; }
|
||||
.reply-actions { display: flex; justify-content: flex-end; margin-top: 10px; }
|
||||
|
||||
.mb-4 { margin-bottom: 16px; }
|
||||
.mb-6 { margin-bottom: 24px; }
|
||||
.text-sm { font-size: 12px; }
|
||||
.text-gray { color: rgba(0,0,0,0.45); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user