3a898e8aa0
后端修复: - 验证码存储与校验机制 - 订单创建事务+库存扣减 - GetStats字段名错误 - VerifyEmail改为POST - 供应商更新字段白名单 - 文件删除安全检查 - 抽奖安全随机数 - 用户管理CRUD - 订单取消/确认收货 - 工单回复 - Toggle返回新数据 - 移除死代码 前端修复: - 404兜底路由 - 401软跳转 - API层统一 - 面包屑补充banners - 国际化完善 - 购物车并行删除 - 退出清理购物车 - 供应商Dashboard数据 - 工单详情页 - 订单取消/确认收货
167 lines
4.2 KiB
Vue
167 lines
4.2 KiB
Vue
<template>
|
||
<div class="ticket-detail-page">
|
||
<BackNav />
|
||
<div v-if="ticket" class="ticket-card">
|
||
<div class="ticket-header">
|
||
<h2>{{ ticket.subject }}</h2>
|
||
<el-tag :type="statusType(ticket.status)">{{ statusText(ticket.status) }}</el-tag>
|
||
</div>
|
||
<div class="ticket-meta">
|
||
<span>分类:{{ ticket.category }}</span>
|
||
<span>创建时间:{{ formatDate(ticket.created_at) }}</span>
|
||
</div>
|
||
<div class="ticket-content">
|
||
<p>{{ ticket.content }}</p>
|
||
</div>
|
||
|
||
<div v-if="ticket.reply" class="reply-section">
|
||
<h3>回复</h3>
|
||
<div class="reply-content" v-html="formatReply(ticket.reply)"></div>
|
||
</div>
|
||
|
||
<div v-if="ticket.status !== 'resolved' && ticket.status !== 'closed'" class="reply-form">
|
||
<h3>追加回复</h3>
|
||
<el-input v-model="replyContent" type="textarea" :rows="4" placeholder="请输入回复内容" />
|
||
<el-button type="primary" @click="submitReply" :loading="submitting" style="margin-top: 12px">提交回复</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import { ticketApi } from '../../api'
|
||
import BackNav from '../../components/BackNav.vue'
|
||
|
||
const route = useRoute()
|
||
const ticket = ref<any>(null)
|
||
const replyContent = ref('')
|
||
const submitting = ref(false)
|
||
|
||
const statusMap: Record<string, string> = {
|
||
pending: '待处理', processing: '处理中', resolved: '已解决', closed: '已关闭',
|
||
}
|
||
const statusTypeMap: Record<string, string> = {
|
||
pending: 'warning', processing: 'primary', resolved: 'success', closed: 'info',
|
||
}
|
||
function statusText(s: string) { return statusMap[s] || s }
|
||
function statusType(s: string) { return statusTypeMap[s] || 'info' }
|
||
|
||
function formatDate(date: string) {
|
||
if (!date) return '-'
|
||
const d = new Date(date)
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||
}
|
||
|
||
function formatReply(reply: string) {
|
||
return reply.replace(/\n/g, '<br>').replace(/---/g, '<hr style="border-color: rgba(255,255,255,0.1); margin: 12px 0;">')
|
||
}
|
||
|
||
async function fetchTicket() {
|
||
try {
|
||
const id = Number(route.params.id)
|
||
const res: any = await ticketApi.getById(id)
|
||
ticket.value = res.data
|
||
} catch {
|
||
ElMessage.error('获取工单失败')
|
||
}
|
||
}
|
||
|
||
async function submitReply() {
|
||
if (!replyContent.value.trim()) {
|
||
ElMessage.warning('请输入回复内容')
|
||
return
|
||
}
|
||
submitting.value = true
|
||
try {
|
||
await ticketApi.reply(Number(route.params.id), { content: replyContent.value })
|
||
ElMessage.success('回复成功')
|
||
replyContent.value = ''
|
||
fetchTicket()
|
||
} catch {
|
||
ElMessage.error('回复失败')
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(fetchTicket)
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.ticket-detail-page { padding: 0; }
|
||
|
||
.ticket-card {
|
||
background: #2d2d44;
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
}
|
||
|
||
.ticket-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
|
||
h2 {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
margin: 0;
|
||
}
|
||
}
|
||
|
||
.ticket-meta {
|
||
display: flex;
|
||
gap: 16px;
|
||
font-size: 13px;
|
||
color: rgba(255, 255, 255, 0.5);
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.ticket-content {
|
||
padding: 16px;
|
||
background: rgba(255, 255, 255, 0.04);
|
||
border-radius: 8px;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
line-height: 1.6;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.reply-section {
|
||
margin-bottom: 20px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||
|
||
h3 {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
margin-bottom: 12px;
|
||
}
|
||
}
|
||
|
||
.reply-content {
|
||
padding: 16px;
|
||
background: rgba(78, 110, 242, 0.08);
|
||
border-radius: 8px;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.reply-form {
|
||
padding-top: 16px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||
|
||
h3 {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
margin-bottom: 12px;
|
||
}
|
||
}
|
||
</style>
|