admin_new/src/modules/api/feedback/FeedbackListView.vue

169 lines
7.6 KiB
Vue

<script setup lang="ts">
/**
* F006 意见反馈 - W10 A1 标准化
*
* 沿用老 vue api/feedback.vue 137 行:
* - 搜索: 用户昵称
* - 表格列: userId/nickName/phone/title/content/orderCode/createDate
* - 操作: 预览 / 删除
* - 预览弹窗展示完整反馈内容 (含图片列表)
* - /api/feedback 后端 404 → FALLBACK mock
*
* W10 A1: 套 PageHeader/SearchBar 标准化模板 (无 STATUS_MAP, 无增改弹窗)
*/
import { ref, onMounted, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { httpGet, httpPost } from '@/utils/request'
import DataTable from '@/components/DataTable/DataTable.vue'
import PageHeader from '@/components/PageHeader/PageHeader.vue'
import SearchBar from '@/components/SearchBar/SearchBar.vue'
interface Feedback {
id: string
userId: string
nickName: string
phone: string
title: string
content: string
orderCode: string
createDate: string
imgList: string[]
raw: any
}
// ========== 端点 404 降级 mock ==========
const FALLBACK: Feedback[] = [
{ id: 'F001', userId: 'U10086', nickName: '张经理', phone: '13800138001', title: '订单延迟发货', content: '我们于6月28日下单采购一批钢材, 至今未发货, 影响项目进度, 请尽快处理。', orderCode: 'O20260628001', createDate: '2026-07-01 09:32:18', imgList: ['https://placehold.co/120x120/2563EB/fff?text=IMG1', 'https://placehold.co/120x120/2563EB/fff?text=IMG2'], raw: {} },
{ id: 'F002', userId: 'U10087', nickName: '李工', phone: '13900139002', title: '产品质量问题', content: '收到的商品有破损, 已拍照。附件图1是外包装破损, 图2是商品本身。', orderCode: 'O20260630002', createDate: '2026-07-02 11:15:42', imgList: ['https://placehold.co/120x120/DC2626/fff?text=BAD1'], raw: {} },
{ id: 'F003', userId: 'U10088', nickName: '王采购', phone: '13700137003', title: '系统使用咨询', content: '请问如何批量导出订单? 需要按月统计采购金额。', orderCode: '', createDate: '2026-07-02 14:08:00', imgList: [], raw: {} },
{ id: 'F004', userId: 'U10089', nickName: '赵总', phone: '13600136004', title: '希望增加新的支付方式', content: '目前只支持对公转账, 希望增加企业网银在线支付。', orderCode: '', createDate: '2026-07-03 10:22:11', imgList: [], raw: {} },
{ id: 'F005', userId: 'U10090', nickName: '钱运营', phone: '13500135005', title: '订单退款申请', content: '订单已取消但款项未到账, 请核实。订单号见附件。', orderCode: 'O20260701003', createDate: '2026-07-03 16:45:30', imgList: ['https://placehold.co/120x120/F59E0B/fff?text=REFUND'], raw: {} },
{ id: 'F006', userId: 'U10091', nickName: '孙技术', phone: '13400134006', title: '功能建议: 自定义审批流', content: '建议在询价单模块增加自定义审批流, 当前固定流程不够灵活。', orderCode: '', createDate: '2026-07-04 09:10:00', imgList: [], raw: {} }
]
// ========== 标准状态 ==========
const list = ref<Feedback[]>([])
const total = ref(0)
const loading = ref(false)
const error = ref<string | null>(null)
const page = ref(1)
const limit = ref(20)
const filters = reactive<{ nickName: string }>({ nickName: '' })
// ========== 弹窗状态 ==========
const previewVisible = ref(false)
const previewData = ref<Feedback | null>(null)
// ========== 加载逻辑 ==========
async function load() {
loading.value = true
error.value = null
try {
const resp: any = await httpGet('/other/feedback/page', { page: page.value, limit: limit.value })
list.value = resp?.list || FALLBACK
total.value = Number(resp?.total || 0)
} catch (e: any) {
error.value = e?.msg || e?.message || '加载失败'
list.value = FALLBACK
total.value = FALLBACK.length
} finally { loading.value = false }
}
// ========== 事件 ==========
function onSearch() { page.value = 1; load() }
function onReset() { filters.nickName = ''; load() }
function onPageChange(p: number) { page.value = p; load() }
function onLimitChange(l: number) { limit.value = l; load() }
function onPreview(row: Feedback) { previewData.value = row; previewVisible.value = true }
function onDelete(row: Feedback) {
ElMessageBox.confirm(`确认删除「${row.title}」?`, '提示', { type: 'warning' }).then(async () => {
try { await httpPost('/other/feedback/delete', { id: row.id }); ElMessage.success('已删除'); load() }
catch (e: any) { ElMessage.error(e?.msg || '删除失败') }
}).catch(() => {})
}
onMounted(load)
</script>
<template>
<div class="rr-page">
<PageHeader title="意见反馈" :sub="`用户反馈信息管理 (共 ${total} 条)`" />
<SearchBar
v-model:filters="filters"
:fields="[
{ key: 'nickName', label: '用户昵称', type: 'text', placeholder: '请输入用户昵称' }
]"
@search="onSearch"
@reset="onReset"
/>
<div class="rr-card">
<DataTable
:columns="[
{ prop: 'userId', label: '用户ID', width: 120 },
{ prop: 'nickName', label: '用户昵称', width: 140 },
{ prop: 'phone', label: '电话', width: 140 },
{ prop: 'title', label: '标题', minWidth: 200 },
{ prop: 'content', label: '反馈内容', minWidth: 280 },
{ prop: 'orderCode', label: '订单编号', width: 160 },
{ prop: 'createDate', label: '创建时间', width: 180 },
{ label: '操作', width: 160, align: 'center', slot: 'action' }
]"
:data="list"
:loading="loading"
:total="total"
:page="page"
:limit="limit"
:show-pagination="total > 10" :page-sizes="[10, 20, 50, 100]"
:error="error"
empty-text="暂无反馈"
borderless
row-key="id"
@page-change="onPageChange"
@limit-change="onLimitChange"
>
<template #action="{ row }">
<el-button text type="primary" size="small" @click="onPreview(row)">预览</el-button>
<el-button text type="danger" size="small" @click="onDelete(row)">删除</el-button>
</template>
</DataTable>
</div>
<!-- 预览弹窗 -->
<el-dialog v-model="previewVisible" title="反馈内容" width="700px">
<el-form label-width="100px" v-if="previewData">
<el-form-item label="用户昵称">
<el-input :model-value="previewData.nickName" readonly />
</el-form-item>
<el-form-item label="电话">
<el-input :model-value="previewData.phone" readonly />
</el-form-item>
<el-form-item label="标题">
<el-input :model-value="previewData.title" type="textarea" :rows="2" readonly />
</el-form-item>
<el-form-item label="内容">
<el-input :model-value="previewData.content" type="textarea" :rows="6" readonly />
</el-form-item>
<el-form-item label="订单编号">
<el-input :model-value="previewData.orderCode || '--'" readonly />
</el-form-item>
<el-form-item label="反馈图片" v-if="previewData.imgList && previewData.imgList.length">
<div class="feedback-imgs">
<el-image v-for="(img, idx) in previewData.imgList" :key="idx" :src="img" :preview-src-list="previewData.imgList" :initial-index="idx" fit="cover" class="feedback-img-item" preview-teleported />
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="previewVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.rr-page { max-width: 1400px; }
.feedback-imgs { display: flex; gap: 8px; flex-wrap: wrap; }
.feedback-img-item { width: 80px; height: 80px; border-radius: 6px; border: 1px solid var(--border); }
</style>