refactor: 完成旅游管理系统多模块功能优化与接口迭代

本次提交包含多维度更新:
1. 修复登录页默认密码为123456,优化产品标签分割逻辑
2. 重构文章摘要提取逻辑,使用DOM解析替代正则替换,更准确处理富文本
3. 重构游记服务层登录校验逻辑,新增getLoginUserId方法区分管理员与普通用户
4. 全面升级订单与游记管理页面:
   - 替换列表接口为分页接口,新增搜索、分页、空状态处理
   - 重构表格列配置与状态映射,优化操作按钮逻辑
   - 新增景点订单、商品订单详情查看与多状态操作能力
5. 重构后端接口与接口层代码,统一使用RESTful风格路由与请求方法
6. 优化订单列表展示字段,补充收货信息、商品明细等展示内容
This commit is contained in:
sparksfly 2026-08-08 11:25:20 +08:00
parent 96de3b49e6
commit 1671215071
11 changed files with 557 additions and 120 deletions

View File

@ -1,21 +1,29 @@
import http from './index' import http from './index'
export function getAttractionOrderList() { export function getAttractionOrderPage(params: any) {
return http.get('/order/attraction/list') return http.get('/order/attraction/page', { params })
}
export function processAttractionOrder(id: number) {
return http.post('/order/attraction/process', { id })
}
export function cancelAttractionOrder(id: number) {
return http.post('/order/attraction/cancel', { id })
} }
export function getProductOrderList() { export function markAttractionOrderUsed(id: number) {
return http.get('/order/product/list') return http.put(`/order/attraction/${id}/use`)
} }
export function processProductOrder(id: number) {
return http.post('/order/product/process', { id }) export function refundAttractionOrder(id: number) {
return http.put(`/order/attraction/${id}/refund`)
} }
export function cancelProductOrder(id: number) {
return http.post('/order/product/cancel', { id }) export function getProductOrderPage(params: any) {
return http.get('/order/product/page', { params })
}
export function deliverProductOrder(id: number) {
return http.put(`/order/product/${id}/deliver`)
}
export function receiveProductOrder(id: number) {
return http.put(`/order/product/${id}/receive`)
}
export function refundProductOrder(id: number) {
return http.put(`/order/product/${id}/refund`)
} }

View File

@ -1,14 +1,17 @@
import http from './index' import http from './index'
export function getTravelNoteList() { export function getTravelNotePage(params: any) {
return http.get('/travel/list') return http.get('/travel/page', { params })
} }
export function approveTravelNote(id: number) {
return http.post('/travel/approve', { id }) export function getTravelNoteById(id: number) {
return http.get(`/travel/${id}`)
} }
export function rejectTravelNote(id: number) {
return http.post('/travel/reject', { id }) export function auditTravelNote(data: { id: number; status: number; rejectReason?: string }) {
return http.put('/travel/audit', data)
} }
export function deleteTravelNote(id: number) { export function deleteTravelNote(id: number) {
return http.post('/travel/delete', { id }) return http.delete(`/travel/${id}`)
} }

View File

@ -99,7 +99,7 @@
</a-form> </a-form>
<div class="form-footer"> <div class="form-footer">
<p>默认账号: <strong>admin</strong> / 密码: <strong>admin</strong></p> <p>默认账号: <strong>admin</strong> / 密码: <strong>123456</strong></p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,47 +1,101 @@
<template> <template>
<a-card title="景点订单"> <a-card title="景点订单">
<template #extra> <template #extra>
<a-select v-model:value="statusFilter" placeholder="订单状态" allow-clear style="width: 140px" @change="fetchData"> <a-space wrap>
<a-select-option value="待支付">待支付</a-select-option> <a-input
<a-select-option value="已支付">已支付</a-select-option> v-model:value="query.orderNo"
<a-select-option value="已出票">已出票</a-select-option> placeholder="订单号"
<a-select-option value="已完成">已完成</a-select-option> allow-clear
<a-select-option value="已取消">已取消</a-select-option> 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="1">待支付</a-select-option>
<a-select-option :value="2">已支付</a-select-option>
<a-select-option :value="3">已使用</a-select-option>
<a-select-option :value="4">已取消</a-select-option>
<a-select-option :value="5">退款中</a-select-option>
<a-select-option :value="6">已退款</a-select-option>
</a-select> </a-select>
<a-button type="primary" @click="handleSearch">
<SearchOutlined /> 查询
</a-button>
</a-space>
</template> </template>
<a-table :data-source="tableData" :columns="columns" :loading="loading" :pagination="false" row-key="id" />
<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-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, h, onMounted } from 'vue' import { ref, reactive, h, onMounted } from 'vue'
import { getAttractionOrderList, processAttractionOrder, cancelAttractionOrder } from '@/api/order' import { getAttractionOrderPage, markAttractionOrderUsed, refundAttractionOrder } from '@/api/order'
import { message, Modal } from 'ant-design-vue' import { message, Modal } from 'ant-design-vue'
const tableData = ref<any[]>([]) const tableData = ref<any[]>([])
const loading = ref(false) const loading = ref(false)
const statusFilter = ref('')
const colorMap: Record<string, string> = { '待支付': 'orange', '已支付': 'blue', '已出票': 'green', '已完成': '', '已取消': 'default', '已退款': 'red' } const query = reactive({
orderNo: '',
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 }> = {
1: { text: '待支付', color: 'orange' },
2: { text: '已支付', color: 'blue' },
3: { text: '已使用', color: 'green' },
4: { text: '已取消', color: 'default' },
5: { text: '退款中', color: 'gold' },
6: { text: '已退款', color: 'red' },
}
const columns = [ const columns = [
{ title: 'ID', dataIndex: 'id', width: 80 }, { title: 'ID', dataIndex: 'id', width: 70 },
{ title: '订单号', dataIndex: 'orderNo', width: 180 }, { title: '订单号', dataIndex: 'orderNo', width: 180 },
{ title: '用户', dataIndex: 'userName', width: 100 }, { title: '用户', dataIndex: 'username', width: 110, customRender: ({ text }: any) => text || '-' },
{ title: '景点名称', dataIndex: 'attractionName', minWidth: 140 }, { title: '景点名称', dataIndex: 'attractionName', minWidth: 140, ellipsis: true },
{ title: '门票类型', dataIndex: 'ticketName', width: 100 }, { title: '门票', dataIndex: 'ticketName', width: 120, ellipsis: true },
{ title: '数量', dataIndex: 'quantity', width: 60 }, { title: '数量', dataIndex: 'quantity', width: 60 },
{ title: '总价', dataIndex: 'totalPrice', width: 100, customRender: ({ text }: any) => '¥' + text }, { title: '总价', dataIndex: 'totalPrice', width: 100, customRender: ({ text }: any) => '¥' + text },
{ title: '状态', dataIndex: 'status', width: 100, customRender: ({ text }: any) => h('a-tag', { color: colorMap[text] || '' }, text) }, { title: '游玩日期', dataIndex: 'visitDate', width: 110, customRender: ({ text }: any) => text || '-' },
{
title: '状态',
dataIndex: 'status',
width: 90,
customRender: ({ text }: any) => {
const info = statusMap[text] || { text: '未知', color: 'default' }
return h('a-tag', { color: info.color }, info.text)
},
},
{ title: '下单时间', dataIndex: 'createTime', width: 180 }, { title: '下单时间', dataIndex: 'createTime', width: 180 },
{ {
title: '操作', width: 180, fixed: 'right', title: '操作',
width: 180,
fixed: 'right',
customRender: ({ record }: any) => { customRender: ({ record }: any) => {
const btns: any[] = [] if (record.status === 2) {
if (record.status === '待支付') btns.push(h('a', { onClick: () => handleProcess(record) }, '确认出票')) return h('span', null, [
if (record.status !== '已取消' && record.status !== '已完成') btns.push(h('a-divider', { type: 'vertical' }), h('a', { onClick: () => handleCancel(record) }, '取消')) h('a', { style: 'margin-right: 16px; color: #52c41a', onClick: () => handleMarkUsed(record) }, '标记已使用'),
if (!btns.length) return h('span', null, '-') h('a', { style: 'color: #faad14', onClick: () => handleRefund(record) }, '退款'),
return h('span', null, btns) ])
}
return h('span', { style: 'color: #bbb' }, '-')
}, },
}, },
] ]
@ -49,16 +103,54 @@ const columns = [
async function fetchData() { async function fetchData() {
loading.value = true loading.value = true
try { try {
const res: any = await getAttractionOrderList() const res: any = await getAttractionOrderPage({
let list = res.data.records current: pagination.current,
if (statusFilter.value) list = list.filter((r: any) => r.status === statusFilter.value) size: pagination.pageSize,
tableData.value = list orderNo: query.orderNo || undefined,
} finally { loading.value = false } 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 handleProcess(row: any) { await processAttractionOrder(row.id); message.success('已确认出票'); fetchData() } function handleSearch() {
function handleCancel(row: any) { pagination.current = 1
Modal.confirm({ title: '确认取消该订单?', onOk: async () => { await cancelAttractionOrder(row.id); message.success('已取消'); fetchData() } }) fetchData()
}
function handleTableChange(pag: any) {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
fetchData()
}
async function handleMarkUsed(row: any) {
try {
await markAttractionOrderUsed(row.id)
message.success('已标记使用')
fetchData()
} catch (err: any) {
message.error(err.message || '操作失败')
}
}
function handleRefund(row: any) {
Modal.confirm({
title: '确认退款该订单?',
content: `订单号:${row.orderNo},退款后将恢复门票库存。`,
okText: '确认退款',
cancelText: '取消',
onOk: async () => {
await refundAttractionOrder(row.id)
message.success('退款成功')
fetchData()
},
})
} }
onMounted(fetchData) onMounted(fetchData)

View File

@ -1,47 +1,132 @@
<template> <template>
<a-card title="商品订单"> <a-card title="商品订单">
<template #extra> <template #extra>
<a-select v-model:value="statusFilter" placeholder="订单状态" allow-clear style="width: 140px" @change="fetchData"> <a-space wrap>
<a-select-option value="待付款">待付款</a-select-option> <a-input
<a-select-option value="已付款">已付款</a-select-option> v-model:value="query.orderNo"
<a-select-option value="已发货">已发货</a-select-option> placeholder="订单号"
<a-select-option value="已完成">已完成</a-select-option> allow-clear
<a-select-option value="已取消">已取消</a-select-option> 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="1">待支付</a-select-option>
<a-select-option :value="2">已支付</a-select-option>
<a-select-option :value="3">待发货</a-select-option>
<a-select-option :value="4">已发货</a-select-option>
<a-select-option :value="5">待收货</a-select-option>
<a-select-option :value="6">已完成</a-select-option>
<a-select-option :value="7">已取消</a-select-option>
<a-select-option :value="8">退款中</a-select-option>
<a-select-option :value="9">已退款</a-select-option>
</a-select> </a-select>
<a-button type="primary" @click="handleSearch">
<SearchOutlined /> 查询
</a-button>
</a-space>
</template> </template>
<a-table :data-source="tableData" :columns="columns" :loading="loading" :pagination="false" row-key="id" />
<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-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, h, onMounted } from 'vue' import { ref, reactive, h, onMounted } from 'vue'
import { getProductOrderList, processProductOrder, cancelProductOrder } from '@/api/order' import {
getProductOrderPage,
deliverProductOrder,
receiveProductOrder,
refundProductOrder,
} from '@/api/order'
import { message, Modal } from 'ant-design-vue' import { message, Modal } from 'ant-design-vue'
const tableData = ref<any[]>([]) const tableData = ref<any[]>([])
const loading = ref(false) const loading = ref(false)
const statusFilter = ref('')
const colorMap: Record<string, string> = { '待付款': 'orange', '已付款': 'blue', '已发货': 'green', '已完成': '', '已取消': 'default', '已退款': 'red' } const query = reactive({
orderNo: '',
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 }> = {
1: { text: '待支付', color: 'orange' },
2: { text: '已支付', color: 'blue' },
3: { text: '待发货', color: 'default' },
4: { text: '已发货', color: 'green' },
5: { text: '待收货', color: 'cyan' },
6: { text: '已完成', color: 'green' },
7: { text: '已取消', color: 'default' },
8: { text: '退款中', color: 'gold' },
9: { text: '已退款', color: 'red' },
}
function productText(row: any) {
const items = row.items || []
if (items.length === 0) return '-'
const first = items[0].productName || items[0].skuName || '商品'
return items.length > 1 ? `${first}${items.length}` : first
}
const columns = [ const columns = [
{ title: 'ID', dataIndex: 'id', width: 80 }, { title: 'ID', dataIndex: 'id', width: 70 },
{ title: '订单号', dataIndex: 'orderNo', width: 180 }, { title: '订单号', dataIndex: 'orderNo', width: 180 },
{ title: '用户', dataIndex: 'userName', width: 100 }, { title: '用户ID', dataIndex: 'userId', width: 80 },
{ title: '商品名称', dataIndex: 'productName', minWidth: 140 }, { title: '商品', key: 'product', minWidth: 160, ellipsis: true, customRender: ({ record }: any) => productText(record) },
{ title: '数量', dataIndex: 'quantity', width: 60 }, { title: '数量', dataIndex: 'totalQuantity', width: 70 },
{ title: '总价', dataIndex: 'totalPrice', width: 100, customRender: ({ text }: any) => '¥' + text }, { title: '实付金额', dataIndex: 'actualAmount', width: 110, customRender: ({ text, record }: any) => '¥' + (text ?? record.totalPrice) },
{ title: '状态', dataIndex: 'status', width: 100, customRender: ({ text }: any) => h('a-tag', { color: colorMap[text] || '' }, text) }, { title: '收货人', dataIndex: 'consignee', width: 110, customRender: ({ text, record }: any) => text ? `${text} ${record.phone || ''}` : '-' },
{ title: '收货地址', dataIndex: 'address', minWidth: 200, ellipsis: true }, {
title: '收货地址',
key: 'address',
minWidth: 220,
ellipsis: true,
customRender: ({ record }: any) =>
[record.province, record.city, record.district, record.detailAddress].filter(Boolean).join('') || '-',
},
{
title: '状态',
dataIndex: 'status',
width: 90,
customRender: ({ text, record }: any) => {
const info = statusMap[text] || { text: record.statusText || '未知', color: 'default' }
return h('a-tag', { color: info.color }, info.text)
},
},
{ title: '下单时间', dataIndex: 'createTime', width: 180 }, { title: '下单时间', dataIndex: 'createTime', width: 180 },
{ {
title: '操作', width: 180, fixed: 'right', title: '操作',
width: 220,
fixed: 'right',
customRender: ({ record }: any) => { customRender: ({ record }: any) => {
const btns: any[] = [] if (record.status === 2) {
if (record.status === '待付款') btns.push(h('a', { onClick: () => handleCancel(record) }, '取消订单')) return h('span', null, [
if (record.status === '已付款') { if (btns.length) btns.push(h('a-divider', { type: 'vertical' })); btns.push(h('a', { style: 'color: #52c41a', onClick: () => handleProcess(record) }, '确认发货')) } h('a', { style: 'margin-right: 16px; color: #52c41a', onClick: () => handleDeliver(record) }, '发货'),
if (!btns.length) return h('span', null, '-') h('a', { style: 'color: #faad14', onClick: () => handleRefund(record) }, '退款'),
return h('span', null, btns) ])
}
if (record.status === 4) {
return h('span', null, [
h('a', { style: 'margin-right: 16px; color: #52c41a', onClick: () => handleReceive(record) }, '确认收货'),
h('a', { style: 'color: #faad14', onClick: () => handleRefund(record) }, '退款'),
])
}
return h('span', { style: 'color: #bbb' }, '-')
}, },
}, },
] ]
@ -49,16 +134,64 @@ const columns = [
async function fetchData() { async function fetchData() {
loading.value = true loading.value = true
try { try {
const res: any = await getProductOrderList() const res: any = await getProductOrderPage({
let list = res.data.records current: pagination.current,
if (statusFilter.value) list = list.filter((r: any) => r.status === statusFilter.value) size: pagination.pageSize,
tableData.value = list orderNo: query.orderNo || undefined,
} finally { loading.value = false } 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 handleProcess(row: any) { await processProductOrder(row.id); message.success('已确认发货'); fetchData() } function handleSearch() {
function handleCancel(row: any) { pagination.current = 1
Modal.confirm({ title: '确认取消该订单?', onOk: async () => { await cancelProductOrder(row.id); message.success('已取消'); fetchData() } }) fetchData()
}
function handleTableChange(pag: any) {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
fetchData()
}
async function handleDeliver(row: any) {
try {
await deliverProductOrder(row.id)
message.success('已发货')
fetchData()
} catch (err: any) {
message.error(err.message || '操作失败')
}
}
async function handleReceive(row: any) {
try {
await receiveProductOrder(row.id)
message.success('已确认收货')
fetchData()
} catch (err: any) {
message.error(err.message || '操作失败')
}
}
function handleRefund(row: any) {
Modal.confirm({
title: '确认退款该订单?',
content: `订单号:${row.orderNo},退款后将恢复商品库存。`,
okText: '确认退款',
cancelText: '取消',
onOk: async () => {
await refundProductOrder(row.id)
message.success('退款成功')
fetchData()
},
})
} }
onMounted(fetchData) onMounted(fetchData)

View File

@ -1,42 +1,127 @@
<template> <template>
<a-card title="游记管理"> <a-card title="游记管理">
<template #extra> <template #extra>
<a-select v-model:value="statusFilter" placeholder="审核状态" allow-clear style="width: 140px" @change="fetchData"> <a-space wrap>
<a-select-option value="待审核">待审核</a-select-option> <a-input
<a-select-option value="已发布">已发布</a-select-option> v-model:value="query.keyword"
<a-select-option value="已驳回">已驳回</a-select-option> 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-select>
<a-button type="primary" @click="handleSearch">
<SearchOutlined /> 查询
</a-button>
</a-space>
</template> </template>
<a-table :data-source="tableData" :columns="columns" :loading="loading" :pagination="false" row-key="id" />
<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-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> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, h, onMounted } from 'vue' import { ref, reactive, h, onMounted } from 'vue'
import { getTravelNoteList, approveTravelNote, rejectTravelNote, deleteTravelNote } from '@/api/travel' import { getTravelNotePage, getTravelNoteById, auditTravelNote, deleteTravelNote } from '@/api/travel'
import { message, Modal } from 'ant-design-vue' import { message, Modal } from 'ant-design-vue'
const tableData = ref<any[]>([]) const tableData = ref<any[]>([])
const loading = ref(false) const loading = ref(false)
const statusFilter = ref('') const viewVisible = ref(false)
const currentNote = ref<any>(null)
const colorMap: Record<string, string> = { '已发布': 'green', '待审核': 'orange', '已驳回': 'red' } 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 = [ const columns = [
{ title: 'ID', dataIndex: 'id', width: 80 }, { title: 'ID', dataIndex: 'id', width: 70 },
{ title: '标题', dataIndex: 'title', minWidth: 200, ellipsis: true }, { title: '标题', dataIndex: 'title', minWidth: 200, ellipsis: true },
{ title: '作者', dataIndex: 'author', width: 100 }, { 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: 'viewCount', width: 80 },
{ title: '点赞', dataIndex: 'likeCount', width: 70 }, { title: '点赞', dataIndex: 'likeCount', width: 70 },
{ title: '评论', dataIndex: 'commentCount', width: 70 }, { title: '评论', dataIndex: 'commentCount', width: 70 },
{ title: '状态', dataIndex: 'status', width: 100, customRender: ({ text }: any) => h('a-tag', { color: colorMap[text] || '' }, text) },
{ title: '发布时间', dataIndex: 'createTime', width: 180 },
{ {
title: '操作', width: 220, fixed: 'right', 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) => { customRender: ({ record }: any) => {
const btns: any[] = [] const btns: any[] = []
if (record.status === '待审核') { btns.push(h('a', { style: 'color: #52c41a', onClick: () => handleApprove(record) }, '通过')); btns.push(h('a-divider', { type: 'vertical' }), h('a', { style: 'color: #faad14', onClick: () => handleReject(record) }, '驳回')) } btns.push(h('a', { style: 'margin-right: 16px', onClick: () => handleView(record) }, '查看'))
btns.push(h('a-divider', { type: 'vertical' }), h('a', { style: 'color: #ff4d4f', onClick: () => handleDelete(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) return h('span', null, btns)
}, },
}, },
@ -45,18 +130,115 @@ const columns = [
async function fetchData() { async function fetchData() {
loading.value = true loading.value = true
try { try {
const res: any = await getTravelNoteList() const res: any = await getTravelNotePage({
let list = res.data.records current: pagination.current,
if (statusFilter.value) list = list.filter((r: any) => r.status === statusFilter.value) size: pagination.pageSize,
tableData.value = list keyword: query.keyword || undefined,
} finally { loading.value = false } 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()
},
})
} }
async function handleApprove(row: any) { await approveTravelNote(row.id); message.success('游记已通过'); fetchData() }
async function handleReject(row: any) { await rejectTravelNote(row.id); message.success('游记已驳回'); fetchData() }
function handleDelete(row: any) { function handleDelete(row: any) {
Modal.confirm({ title: '确认删除该游记?', onOk: async () => { await deleteTravelNote(row.id); message.success('删除成功'); fetchData() } }) Modal.confirm({
title: `确认删除游记「${row.title}」?`,
okText: '确认',
cancelText: '取消',
okButtonProps: { danger: true },
onOk: async () => {
await deleteTravelNote(row.id)
message.success('删除成功')
fetchData()
},
})
} }
onMounted(fetchData) onMounted(fetchData)
</script> </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>

View File

@ -227,7 +227,9 @@ public class ProductOrderServiceImpl extends ServiceImpl<ProductOrderMapper, Pro
Page<ProductOrder> page = new Page<>(dto.getCurrent(), dto.getSize()); Page<ProductOrder> page = new Page<>(dto.getCurrent(), dto.getSize());
LambdaQueryWrapper<ProductOrder> wrapper = buildQueryWrapper(dto); LambdaQueryWrapper<ProductOrder> wrapper = buildQueryWrapper(dto);
wrapper.orderByDesc(ProductOrder::getCreateTime); wrapper.orderByDesc(ProductOrder::getCreateTime);
return baseMapper.selectPage(page, wrapper).convert(this::convertToVO); IPage<ProductOrderVO> result = baseMapper.selectPage(page, wrapper).convert(this::convertToVO);
result.getRecords().forEach(vo -> vo.setItems(getOrderItems(vo.getId())));
return result;
} }
@Override @Override

View File

@ -37,7 +37,7 @@ public class TravelNoteServiceImpl extends ServiceImpl<TravelNoteMapper, TravelN
@Override @Override
public void create(TravelNoteCreateDTO dto) { public void create(TravelNoteCreateDTO dto) {
Long userId = StpUtil.getLoginIdAsLong(); Long userId = getLoginUserId();
User user = userMapper.selectById(userId); User user = userMapper.selectById(userId);
TravelNote note = new TravelNote(); TravelNote note = new TravelNote();
@ -65,7 +65,7 @@ public class TravelNoteServiceImpl extends ServiceImpl<TravelNoteMapper, TravelN
@Override @Override
public void update(TravelNoteUpdateDTO dto) { public void update(TravelNoteUpdateDTO dto) {
Long userId = StpUtil.getLoginIdAsLong(); Long userId = getLoginUserId();
TravelNote note = baseMapper.selectById(dto.getId()); TravelNote note = baseMapper.selectById(dto.getId());
if (note == null) { if (note == null) {
throw new BusinessException("游记不存在"); throw new BusinessException("游记不存在");
@ -87,7 +87,7 @@ public class TravelNoteServiceImpl extends ServiceImpl<TravelNoteMapper, TravelN
@Override @Override
public void delete(Long id) { public void delete(Long id) {
Long userId = StpUtil.getLoginIdAsLong(); Long userId = getLoginUserId();
TravelNote note = baseMapper.selectById(id); TravelNote note = baseMapper.selectById(id);
if (note == null) { if (note == null) {
throw new BusinessException("游记不存在"); throw new BusinessException("游记不存在");
@ -156,7 +156,7 @@ public class TravelNoteServiceImpl extends ServiceImpl<TravelNoteMapper, TravelN
@Override @Override
public IPage<TravelNoteVO> myPageQuery(Long current, Long size) { public IPage<TravelNoteVO> myPageQuery(Long current, Long size) {
Long userId = StpUtil.getLoginIdAsLong(); Long userId = getLoginUserId();
Page<TravelNote> page = new Page<>(current, size); Page<TravelNote> page = new Page<>(current, size);
LambdaQueryWrapper<TravelNote> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<TravelNote> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TravelNote::getUserId, userId); wrapper.eq(TravelNote::getUserId, userId);
@ -188,7 +188,7 @@ public class TravelNoteServiceImpl extends ServiceImpl<TravelNoteMapper, TravelN
if (!StpUtil.isLogin()) { if (!StpUtil.isLogin()) {
return false; return false;
} }
Long userId = StpUtil.getLoginIdAsLong(); Long userId = getLoginUserId();
LambdaQueryWrapper<Favorite> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<Favorite> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Favorite::getUserId, userId) wrapper.eq(Favorite::getUserId, userId)
.eq(Favorite::getFavoriteType, 3) .eq(Favorite::getFavoriteType, 3)
@ -196,6 +196,17 @@ public class TravelNoteServiceImpl extends ServiceImpl<TravelNoteMapper, TravelN
return favoriteMapper.selectCount(wrapper) > 0; return favoriteMapper.selectCount(wrapper) > 0;
} }
/**
* 获取当前登录用户ID管理员登录 ID 形如 admin:1不能作为用户ID使用
*/
private Long getLoginUserId() {
String loginId = StpUtil.getLoginIdAsString();
if (loginId.startsWith("admin:")) {
throw new BusinessException("请使用用户账号操作游记");
}
return Long.parseLong(loginId);
}
private TravelNoteVO convertToVO(TravelNote note) { private TravelNoteVO convertToVO(TravelNote note) {
TravelNoteVO vo = new TravelNoteVO(); TravelNoteVO vo = new TravelNoteVO();
BeanUtils.copyProperties(note, vo); BeanUtils.copyProperties(note, vo);

View File

@ -177,7 +177,10 @@ function formatDate(dateStr: string | null): string {
function extractExcerpt(content: string): string { function extractExcerpt(content: string): string {
if (!content) return '' if (!content) return ''
const stripped = content.replace(/[#*`\[\]()>|\\-]/g, '').replace(/\s+/g, ' ').trim() const div = document.createElement('div')
div.innerHTML = content
const text = div.textContent || div.innerText || ''
const stripped = text.replace(/\s+/g, ' ').trim()
return stripped.length > 100 ? stripped.slice(0, 100) + '...' : stripped return stripped.length > 100 ? stripped.slice(0, 100) + '...' : stripped
} }

View File

@ -124,7 +124,7 @@ const currentImage = computed(() => imageList.value[currentIndex.value] || image
const tagList = computed(() => { const tagList = computed(() => {
if (!detail.value?.tags) return [] if (!detail.value?.tags) return []
return detail.value.tags.split(',').map((s: string) => s.trim()).filter(Boolean) return detail.value.tags.split(/[;,]/).map((s: string) => s.trim()).filter(Boolean)
}) })
const selectedPrice = computed(() => { const selectedPrice = computed(() => {

View File

@ -100,7 +100,10 @@ function formatDate(dateStr: string | null): string {
function extractExcerpt(content: string): string { function extractExcerpt(content: string): string {
if (!content) return '' if (!content) return ''
const stripped = content.replace(/[#*`\[\]()>|\\-]/g, '').replace(/\s+/g, ' ').trim() const div = document.createElement('div')
div.innerHTML = content
const text = div.textContent || div.innerText || ''
const stripped = text.replace(/\s+/g, ' ').trim()
return stripped.length > 100 ? stripped.slice(0, 100) + '...' : stripped return stripped.length > 100 ? stripped.slice(0, 100) + '...' : stripped
} }