tourism-system/tourism-admin/src/views/travel/TravelNoteView.vue

245 lines
7.2 KiB
Vue

<template>
<a-card title="游记管理">
<template #extra>
<a-space wrap>
<a-input
v-model:value="query.keyword"
placeholder="搜索标题/标签"
allow-clear
style="width: 200px"
@press-enter="handleSearch"
/>
<a-select v-model:value="query.status" placeholder="状态" allow-clear style="width: 120px" @change="handleSearch">
<a-select-option :value="0">待审核</a-select-option>
<a-select-option :value="1">已发布</a-select-option>
<a-select-option :value="2">已下架</a-select-option>
</a-select>
<a-button type="primary" @click="handleSearch">
<SearchOutlined /> 查询
</a-button>
</a-space>
</template>
<a-table
:data-source="tableData"
:columns="columns"
:loading="loading"
:pagination="pagination"
row-key="id"
@change="handleTableChange"
/>
<a-empty v-if="!loading && tableData.length === 0" description="暂无游记数据" />
</a-card>
<a-modal v-model:open="viewVisible" title="游记详情" width="720px" :footer="null">
<div v-if="currentNote" class="note-detail">
<img v-if="currentNote.coverImage" :src="currentNote.coverImage" alt="封面" class="note-cover" />
<h3>{{ currentNote.title }}</h3>
<div class="meta">
<span>作者:{{ currentNote.userNickname || '-' }}</span>
<span>关联景点:{{ currentNote.attractionName || '-' }}</span>
<span>状态:{{ statusText(currentNote.status) }}</span>
<span>发布时间:{{ currentNote.publishTime || currentNote.createTime }}</span>
</div>
<div v-if="currentNote.tags" class="tags">
<a-tag v-for="t in currentNote.tags.split(/[;,]/)" :key="t">{{ t }}</a-tag>
</div>
<a-divider />
<div class="note-content" v-html="currentNote.content"></div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, h, onMounted } from 'vue'
import { getTravelNotePage, getTravelNoteById, auditTravelNote, deleteTravelNote } from '@/api/travel'
import { message, Modal } from 'ant-design-vue'
const tableData = ref<any[]>([])
const loading = ref(false)
const viewVisible = ref(false)
const currentNote = ref<any>(null)
const query = reactive({
keyword: '',
status: undefined as number | undefined,
})
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total: number) => `${total}`,
})
const statusMap: Record<number, { text: string; color: string }> = {
0: { text: '待审核', color: 'orange' },
1: { text: '已发布', color: 'green' },
2: { text: '已下架', color: 'red' },
}
function statusText(status: number) {
return (statusMap[status] || { text: '未知' }).text
}
const columns = [
{ title: 'ID', dataIndex: 'id', width: 70 },
{ title: '标题', dataIndex: 'title', minWidth: 200, ellipsis: true },
{ title: '关联景点', dataIndex: 'attractionName', width: 120, ellipsis: true, customRender: ({ text }: any) => text || '-' },
{ title: '作者', dataIndex: 'userNickname', width: 100, customRender: ({ text }: any) => text || '-' },
{ title: '浏览量', dataIndex: 'viewCount', width: 80 },
{ title: '点赞', dataIndex: 'likeCount', width: 70 },
{ title: '评论', dataIndex: 'commentCount', width: 70 },
{
title: '状态',
dataIndex: 'status',
width: 100,
customRender: ({ text, record }: any) => {
const info = statusMap[text] || { text: '未知', color: 'default' }
return h('a-tag', { color: info.color, title: record.rejectReason || '' }, info.text)
},
},
{
title: '发布时间',
dataIndex: 'publishTime',
width: 180,
customRender: ({ text, record }: any) => text || record.createTime || '',
},
{
title: '操作',
width: 240,
fixed: 'right',
customRender: ({ record }: any) => {
const btns: any[] = []
btns.push(h('a', { style: 'margin-right: 16px', onClick: () => handleView(record) }, '查看'))
if (record.status === 0) {
btns.push(h('a', { style: 'margin-right: 16px; color: #52c41a', onClick: () => handleAudit(record, 1) }, '通过'))
btns.push(h('a', { style: 'margin-right: 16px; color: #faad14', onClick: () => handleReject(record) }, '驳回'))
} else if (record.status === 1) {
btns.push(h('a', { style: 'margin-right: 16px', onClick: () => handleAudit(record, 2) }, '下架'))
} else if (record.status === 2) {
btns.push(h('a', { style: 'margin-right: 16px; color: #52c41a', onClick: () => handleAudit(record, 1) }, '重新发布'))
}
btns.push(h('a', { style: 'color: #ff4d4f', onClick: () => handleDelete(record) }, '删除'))
return h('span', null, btns)
},
},
]
async function fetchData() {
loading.value = true
try {
const res: any = await getTravelNotePage({
current: pagination.current,
size: pagination.pageSize,
keyword: query.keyword || undefined,
status: query.status,
})
tableData.value = res.data.records || []
pagination.total = res.data.total || 0
} catch (err: any) {
message.error(err.message || '获取游记列表失败')
} finally {
loading.value = false
}
}
async function handleView(row: any) {
try {
const res: any = await getTravelNoteById(row.id)
currentNote.value = res.data
viewVisible.value = true
} catch (err: any) {
message.error(err.message || '获取游记详情失败')
}
}
function handleSearch() {
pagination.current = 1
fetchData()
}
function handleTableChange(pag: any) {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
fetchData()
}
async function handleAudit(row: any, status: number) {
try {
await auditTravelNote({ id: row.id, status })
message.success(status === 1 ? '已发布' : '已下架')
fetchData()
} catch (err: any) {
message.error(err.message || '操作失败')
}
}
function handleReject(row: any) {
Modal.confirm({
title: '确认驳回该游记?',
content: '驳回后该游记将保持待审核状态并记录驳回原因。',
okText: '确认',
cancelText: '取消',
onOk: async () => {
await auditTravelNote({ id: row.id, status: 0, rejectReason: '审核未通过' })
message.success('已驳回')
fetchData()
},
})
}
function handleDelete(row: any) {
Modal.confirm({
title: `确认删除游记「${row.title}」?`,
okText: '确认',
cancelText: '取消',
okButtonProps: { danger: true },
onOk: async () => {
await deleteTravelNote(row.id)
message.success('删除成功')
fetchData()
},
})
}
onMounted(fetchData)
</script>
<style scoped lang="scss">
.note-detail {
h3 { margin-bottom: 12px; }
.note-cover {
width: 100%;
max-height: 300px;
object-fit: cover;
border-radius: 8px;
margin-bottom: 12px;
}
.meta {
font-size: 13px;
color: #666;
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.tags {
margin-top: 10px;
}
.note-content {
line-height: 1.8;
word-break: break-word;
:deep(img), :deep(video) {
max-width: 100%;
border-radius: 8px;
}
}
}
</style>