260 lines
7.5 KiB
Vue
260 lines
7.5 KiB
Vue
<script setup>
|
|
import { inject, onMounted, ref } from 'vue'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
import { closeFeedback, deleteFeedback, fetchFeedbacks, replyFeedback } from '../api/feedbacks'
|
|
|
|
const refreshNavBadges = inject('refreshNavBadges', null)
|
|
|
|
const loading = ref(false)
|
|
const statusFilter = ref('')
|
|
const stats = ref({ total: 0, pending: 0, replied: 0, closed: 0 })
|
|
const list = ref([])
|
|
|
|
const statusOptions = [
|
|
{ label: '全部状态', value: '' },
|
|
{ label: '待处理', value: 'pending' },
|
|
{ label: '已回复', value: 'replied' },
|
|
{ label: '已关闭', value: 'closed' },
|
|
]
|
|
|
|
function statusMeta(status) {
|
|
if (status === 'pending') return { label: '待处理', type: 'warning' }
|
|
if (status === 'replied') return { label: '已回复', type: 'success' }
|
|
if (status === 'closed') return { label: '已关闭', type: 'info' }
|
|
return { label: status || '-', type: 'info' }
|
|
}
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
try {
|
|
const data = await fetchFeedbacks(statusFilter.value)
|
|
list.value = data?.feedbacks || []
|
|
stats.value = data?.stats || { total: 0, pending: 0, replied: 0, closed: 0 }
|
|
} catch {
|
|
list.value = []
|
|
stats.value = { total: 0, pending: 0, replied: 0, closed: 0 }
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
|
|
await refreshNavBadges?.({ pendingFeedbacks: stats.value.pending || 0 })
|
|
}
|
|
|
|
async function onReply(row) {
|
|
let text
|
|
try {
|
|
const res = await ElMessageBox.prompt('请输入回复内容', '回复反馈', {
|
|
inputType: 'textarea',
|
|
inputPlaceholder: '回复内容',
|
|
confirmButtonText: '提交',
|
|
cancelButtonText: '取消',
|
|
inputValidator: (v) => Boolean(String(v || '').trim()),
|
|
inputErrorMessage: '回复内容不能为空',
|
|
})
|
|
text = res.value
|
|
} catch {
|
|
return
|
|
}
|
|
|
|
try {
|
|
const res = await replyFeedback(row.id, String(text || '').trim())
|
|
ElMessage.success(res?.message || '回复成功')
|
|
await load()
|
|
} catch {
|
|
// handled by interceptor
|
|
}
|
|
}
|
|
|
|
async function onClose(row) {
|
|
try {
|
|
await ElMessageBox.confirm('确定要关闭这个反馈吗?', '关闭反馈', {
|
|
confirmButtonText: '关闭',
|
|
cancelButtonText: '取消',
|
|
type: 'warning',
|
|
})
|
|
} catch {
|
|
return
|
|
}
|
|
|
|
try {
|
|
const res = await closeFeedback(row.id)
|
|
ElMessage.success(res?.message || '反馈已关闭')
|
|
await load()
|
|
} catch {
|
|
// handled by interceptor
|
|
}
|
|
}
|
|
|
|
async function onDelete(row) {
|
|
try {
|
|
await ElMessageBox.confirm('确定要删除这个反馈吗?此操作不可恢复!', '删除反馈', {
|
|
confirmButtonText: '删除',
|
|
cancelButtonText: '取消',
|
|
type: 'error',
|
|
})
|
|
} catch {
|
|
return
|
|
}
|
|
|
|
try {
|
|
const res = await deleteFeedback(row.id)
|
|
ElMessage.success(res?.message || '反馈已删除')
|
|
await load()
|
|
} catch {
|
|
// handled by interceptor
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<template>
|
|
<div class="page-stack">
|
|
<div class="app-page-title">
|
|
<h2>反馈管理</h2>
|
|
<div class="toolbar">
|
|
<el-select v-model="statusFilter" style="width: 160px" @change="load">
|
|
<el-option v-for="o in statusOptions" :key="o.value" :label="o.label" :value="o.value" />
|
|
</el-select>
|
|
<el-button @click="load">刷新</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<el-row :gutter="12">
|
|
<el-col :xs="12" :sm="6">
|
|
<el-card shadow="never" class="stat-card" :body-style="{ padding: '14px' }">
|
|
<div class="stat-value">{{ stats.total || 0 }}</div>
|
|
<div class="stat-label">总计</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :xs="12" :sm="6">
|
|
<el-card shadow="never" class="stat-card" :body-style="{ padding: '14px' }">
|
|
<div class="stat-value warn">{{ stats.pending || 0 }}</div>
|
|
<div class="stat-label">待处理</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :xs="12" :sm="6">
|
|
<el-card shadow="never" class="stat-card" :body-style="{ padding: '14px' }">
|
|
<div class="stat-value ok">{{ stats.replied || 0 }}</div>
|
|
<div class="stat-label">已回复</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :xs="12" :sm="6">
|
|
<el-card shadow="never" class="stat-card" :body-style="{ padding: '14px' }">
|
|
<div class="stat-value">{{ stats.closed || 0 }}</div>
|
|
<div class="stat-label">已关闭</div>
|
|
</el-card>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
|
|
<div class="table-wrap">
|
|
<el-table :data="list" v-loading="loading" style="width: 100%">
|
|
<el-table-column prop="id" label="ID" width="80" />
|
|
<el-table-column prop="username" label="用户" width="140" />
|
|
<el-table-column label="标题" min-width="180">
|
|
<template #default="{ row }">
|
|
<el-tooltip :content="row.title" placement="top" :show-after="300">
|
|
<span class="ellipsis">{{ row.title }}</span>
|
|
</el-tooltip>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="描述" min-width="220">
|
|
<template #default="{ row }">
|
|
<el-tooltip :content="row.description" placement="top" :show-after="300">
|
|
<span class="ellipsis">{{ row.description }}</span>
|
|
</el-tooltip>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="contact" label="联系方式" min-width="160">
|
|
<template #default="{ row }">{{ row.contact || '-' }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="状态" width="110">
|
|
<template #default="{ row }">
|
|
<el-tag :type="statusMeta(row.status).type" effect="light">{{ statusMeta(row.status).label }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="created_at" label="提交时间" width="180" />
|
|
<el-table-column label="回复" min-width="180">
|
|
<template #default="{ row }">
|
|
<el-tooltip :content="row.admin_reply || ''" placement="top" :show-after="300">
|
|
<span class="ellipsis">{{ row.admin_reply || '-' }}</span>
|
|
</el-tooltip>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="220" fixed="right">
|
|
<template #default="{ row }">
|
|
<div class="actions">
|
|
<template v-if="row.status !== 'closed'">
|
|
<el-button type="primary" size="small" @click="onReply(row)">回复</el-button>
|
|
<el-button size="small" @click="onClose(row)">关闭</el-button>
|
|
</template>
|
|
<el-button type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</div>
|
|
</el-card>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.page-stack {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
}
|
|
|
|
.card,
|
|
.stat-card {
|
|
border-radius: var(--app-radius);
|
|
border: 1px solid var(--app-border);
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 20px;
|
|
font-weight: 800;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
.stat-label {
|
|
margin-top: 6px;
|
|
font-size: 12px;
|
|
color: var(--app-muted);
|
|
}
|
|
|
|
.warn {
|
|
color: #b45309;
|
|
}
|
|
|
|
.ok {
|
|
color: #047857;
|
|
}
|
|
|
|
.table-wrap {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.ellipsis {
|
|
display: inline-block;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
</style>
|