fix: 全面修复项目问题

后端修复:
- 验证码存储与校验机制
- 订单创建事务+库存扣减
- GetStats字段名错误
- VerifyEmail改为POST
- 供应商更新字段白名单
- 文件删除安全检查
- 抽奖安全随机数
- 用户管理CRUD
- 订单取消/确认收货
- 工单回复
- Toggle返回新数据
- 移除死代码

前端修复:
- 404兜底路由
- 401软跳转
- API层统一
- 面包屑补充banners
- 国际化完善
- 购物车并行删除
- 退出清理购物车
- 供应商Dashboard数据
- 工单详情页
- 订单取消/确认收货
This commit is contained in:
2026-05-06 09:16:34 +08:00
parent 739481b59f
commit 3a898e8aa0
52 changed files with 2807 additions and 920 deletions
+166
View File
@@ -0,0 +1,166 @@
<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>