refactor: 完成旅游管理系统多模块功能优化与接口迭代
本次提交包含多维度更新: 1. 修复登录页默认密码为123456,优化产品标签分割逻辑 2. 重构文章摘要提取逻辑,使用DOM解析替代正则替换,更准确处理富文本 3. 重构游记服务层登录校验逻辑,新增getLoginUserId方法区分管理员与普通用户 4. 全面升级订单与游记管理页面: - 替换列表接口为分页接口,新增搜索、分页、空状态处理 - 重构表格列配置与状态映射,优化操作按钮逻辑 - 新增景点订单、商品订单详情查看与多状态操作能力 5. 重构后端接口与接口层代码,统一使用RESTful风格路由与请求方法 6. 优化订单列表展示字段,补充收货信息、商品明细等展示内容
This commit is contained in:
parent
96de3b49e6
commit
1671215071
|
|
@ -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`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(() => {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue