feat(admin): migrate admin UI to Vue3
This commit is contained in:
256
admin-frontend/src/pages/FeedbacksPage.vue
Normal file
256
admin-frontend/src/pages/FeedbacksPage.vue
Normal file
@@ -0,0 +1,256 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
import { closeFeedback, deleteFeedback, fetchFeedbacks, replyFeedback } from '../api/feedbacks'
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user