Files
ruoyi-vue/src/views/system/giftcoupon/index.vue
2025-10-31 15:45:46 +08:00

597 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<list-layout>
<!-- 搜索区域 -->
<template #search>
<el-form :inline="true" :model="queryParams" label-width="100px">
<el-form-item label="礼金Key">
<el-input v-model="queryParams.giftCouponKey" placeholder="礼金批次ID" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="商品SKU">
<el-input v-model="queryParams.skuId" placeholder="商品SKU ID" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="商品名称">
<el-input v-model="queryParams.skuName" placeholder="商品名称" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="类型">
<el-select v-model="queryParams.owner" placeholder="请选择" clearable size="small">
<el-option label="自营" value="g" />
<el-option label="POP" value="pop" />
</el-select>
</el-form-item>
<el-form-item label="过期状态">
<el-select v-model="queryParams.isExpired" placeholder="请选择" clearable size="small">
<el-option label="未过期" :value="0" />
<el-option label="已过期" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRange"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
size="small"
range-separator=""
@change="handleDateRangeChange"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" icon="el-icon-search" @click="handleQuery">搜索</el-button>
<el-button size="small" icon="el-icon-refresh" @click="resetQuery">重置</el-button>
<el-button type="success" size="small" icon="el-icon-plus" @click="handleCreate">创建礼金</el-button>
<el-button type="warning" size="small" icon="el-icon-download" @click="handleExport" v-hasPermi="['system:giftcoupon:export']">导出</el-button>
</el-form-item>
</el-form>
</template>
<!-- 统计信息卡片 -->
<template #statistics>
<el-row :gutter="20" style="margin-bottom: 20px;">
<el-col :span="8">
<el-card shadow="hover">
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: #409EFF;">{{ statistics.totalCount || 0 }}</div>
<div style="color: #909399; margin-top: 8px;">礼金批次总数</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover">
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: #67C23A;">{{ statistics.totalUseCount || 0 }}</div>
<div style="color: #909399; margin-top: 8px;">总使用次数</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover">
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: #E6A23C;">¥{{ formatMoney(statistics.totalOcsAmount || 0) }}</div>
<div style="color: #909399; margin-top: 8px;">总分摊金额</div>
</div>
</el-card>
</el-col>
</el-row>
</template>
<!-- 表格区域 -->
<template #table>
<el-table :data="list" v-loading="loading" border stripe :default-sort="{prop: 'createTime', order: 'descending'}">
<el-table-column label="礼金Key" prop="giftCouponKey" width="180" sortable>
<template slot-scope="scope">
<div>
<span style="margin-right: 8px;">{{ scope.row.giftCouponKey }}</span>
<el-button
type="text"
size="mini"
icon="el-icon-copy-document"
@click="copyToClipboard(scope.row.giftCouponKey)"
title="复制礼金Key"
>
复制
</el-button>
</div>
</template>
</el-table-column>
<el-table-column label="商品SKU" prop="skuId" width="120" />
<el-table-column label="商品名称" prop="skuName" min-width="200" show-overflow-tooltip />
<el-table-column label="类型" prop="owner" width="80">
<template slot-scope="scope">
<el-tag :type="scope.row.owner === 'g' ? 'success' : 'warning'">
{{ scope.row.owner === 'g' ? '自营' : 'POP' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="使用次数" prop="useCount" width="100" sortable />
<el-table-column label="总分摊金额" prop="totalOcsAmount" width="120" sortable>
<template slot-scope="scope">
¥{{ formatMoney(scope.row.totalOcsAmount || 0) }}
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" width="160" sortable>
<template slot-scope="scope">{{ parseTime(scope.row.createTime) }}</template>
</el-table-column>
<el-table-column label="过期时间" prop="expireTime" width="160">
<template slot-scope="scope">
<span v-if="scope.row.expireTime">{{ parseTime(scope.row.expireTime) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="过期状态" prop="isExpired" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.isExpired === 1 ? 'danger' : 'success'">
{{ scope.row.isExpired === 1 ? '已过期' : '未过期' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="120">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="handleDetail(scope.row)">
详情
</el-button>
</template>
</el-table-column>
</el-table>
</template>
<!-- 分页区域 -->
<template #pagination>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</template>
</list-layout>
<!-- 创建礼金对话框 -->
<el-dialog title="创建礼金" :visible.sync="createDialogVisible" width="500px" append-to-body>
<el-form :model="createForm" :rules="createRules" ref="createForm" label-width="120px">
<el-form-item label="商品链接/SKU" prop="materialUrl">
<el-input v-model="createForm.materialUrl" placeholder="请输入商品链接https://item.jd.com/100012043978.html或SKU ID100012043978" />
<div style="color: #909399; font-size: 12px; margin-top: 5px;">支持京东商品链接或SKU ID纯数字</div>
</el-form-item>
<el-form-item label="商品名称" prop="skuName">
<el-input v-model="createForm.skuName" placeholder="商品名称(可选)" />
</el-form-item>
<el-form-item label="商品类型" prop="owner">
<el-radio-group v-model="createForm.owner">
<el-radio label="g">自营</el-radio>
<el-radio label="pop">POP</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="礼金金额(元)" prop="amount">
<el-input-number v-model="createForm.amount" :min="1" :max="50" :precision="2" :step="0.01" style="width: 100%" />
<div style="color: #909399; font-size: 12px; margin-top: 5px;">范围1-50参考JD项目要求</div>
</el-form-item>
<el-form-item label="礼金数量" prop="quantity">
<el-input-number v-model="createForm.quantity" :min="1" :max="100" style="width: 100%" />
<div style="color: #909399; font-size: 12px; margin-top: 5px;">范围1-100</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="createDialogVisible = false"> </el-button>
<el-button type="primary" :loading="createLoading" @click="submitCreate"> </el-button>
</div>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog title="礼金详情" :visible.sync="detailVisible" width="1000px" append-to-body>
<div v-if="detailData && detailData.giftCoupon">
<el-tabs v-model="activeDetailTab">
<el-tab-pane label="基本信息" name="info">
<el-descriptions :column="2" border style="margin-top: 20px;">
<el-descriptions-item label="礼金Key" :span="2">
<span style="margin-right: 10px;">{{ detailData.giftCoupon.giftCouponKey }}</span>
<el-button type="text" size="mini" icon="el-icon-copy-document" @click="copyToClipboard(detailData.giftCoupon.giftCouponKey)">复制</el-button>
</el-descriptions-item>
<el-descriptions-item label="商品SKU">{{ detailData.giftCoupon.skuId }}</el-descriptions-item>
<el-descriptions-item label="商品名称" :span="2">{{ detailData.giftCoupon.skuName }}</el-descriptions-item>
<el-descriptions-item label="类型">
<el-tag :type="detailData.giftCoupon.owner === 'g' ? 'success' : 'warning'">
{{ detailData.giftCoupon.owner === 'g' ? '自营' : 'POP' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="使用次数">{{ detailData.giftCoupon.useCount }}</el-descriptions-item>
<el-descriptions-item label="总分摊金额">¥{{ formatMoney(detailData.giftCoupon.totalOcsAmount || 0) }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ parseTime(detailData.giftCoupon.createTime) }}</el-descriptions-item>
<el-descriptions-item label="过期时间">
<span v-if="detailData.giftCoupon.expireTime">{{ parseTime(detailData.giftCoupon.expireTime) }}</span>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="过期状态" :span="2">
<el-tag :type="detailData.giftCoupon.isExpired === 1 ? 'danger' : 'success'">
{{ detailData.giftCoupon.isExpired === 1 ? '已过期' : '未过期' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="关联订单" name="orders">
<el-table :data="detailData.orders || []" border style="margin-top: 20px;" max-height="400">
<el-table-column label="订单号" prop="orderId" width="200" />
<el-table-column label="商品名称" prop="skuName" min-width="200" show-overflow-tooltip />
<el-table-column label="商品SKU" prop="skuId" width="120" />
<el-table-column label="订单金额" prop="price" width="100">
<template slot-scope="scope">¥{{ formatMoney(scope.row.price || 0) }}</template>
</el-table-column>
<el-table-column label="礼金分摊" prop="giftCouponOcsAmount" width="100">
<template slot-scope="scope">¥{{ formatMoney(scope.row.giftCouponOcsAmount || 0) }}</template>
</el-table-column>
<el-table-column label="下单时间" prop="orderTime" width="160">
<template slot-scope="scope">{{ parseTime(scope.row.orderTime) }}</template>
</el-table-column>
<el-table-column label="短链" width="200">
<template slot-scope="scope">
<div v-if="scope.row.positionId">
<a :href="getShortUrl(scope.row.positionId)" target="_blank" style="margin-right: 8px;">查看短链</a>
<el-button type="text" size="mini" icon="el-icon-copy-document" @click="copyToClipboard(getShortUrl(scope.row.positionId))">复制</el-button>
</div>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
<div v-if="!detailData.orders || detailData.orders.length === 0" style="text-align: center; padding: 40px; color: #909399;">
暂无关联订单
</div>
</el-tab-pane>
</el-tabs>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="detailVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listGiftCoupons, getGiftCoupon, getGiftCouponStatistics, exportGiftCoupons } from '@/api/system/giftcoupon'
import { createGiftCoupon, transferWithGift } from '@/api/system/jdorder'
import ListLayout from '@/components/ListLayout'
import { parseTime } from '@/utils/ruoyi'
export default {
name: 'GiftCoupon',
components: {
ListLayout
},
data() {
return {
loading: false,
list: [],
total: 0,
dateRange: [],
queryParams: {
pageNum: 1,
pageSize: 50,
giftCouponKey: undefined,
skuId: undefined,
skuName: undefined,
owner: undefined,
isExpired: undefined,
beginTime: null,
endTime: null
},
statistics: {
totalCount: 0,
totalUseCount: 0,
totalOcsAmount: 0
},
detailVisible: false,
detailData: null,
activeDetailTab: 'info',
createDialogVisible: false,
createLoading: false,
createForm: {
materialUrl: '',
skuName: '',
owner: 'g',
amount: null,
quantity: 10
},
createRules: {
materialUrl: [{ required: true, message: '请输入商品链接或SKU', trigger: 'blur' }],
amount: [{ required: true, message: '请输入礼金金额', trigger: 'blur' }],
quantity: [{ required: true, message: '请输入礼金数量', trigger: 'blur' }]
}
}
},
created() {
this.getList()
this.loadStatistics()
},
methods: {
/** 查询列表 */
getList() {
this.loading = true
listGiftCoupons(this.queryParams).then(res => {
this.list = (res.rows || res.data || [])
this.total = res.total || 0
this.loading = false
}).catch(() => {
this.loading = false
})
},
/** 加载统计信息 */
loadStatistics() {
const params = { ...this.queryParams }
// 统计信息不需要分页参数
delete params.pageNum
delete params.pageSize
getGiftCouponStatistics(params).then(res => {
if (res.code === 200 && res.data) {
this.statistics = {
totalCount: res.data.useCount || 0,
totalUseCount: res.data.useCount || 0,
totalOcsAmount: res.data.totalOcsAmount || 0
}
}
})
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
this.loadStatistics()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.queryParams = {
pageNum: 1,
pageSize: 50,
giftCouponKey: undefined,
skuId: undefined,
skuName: undefined,
owner: undefined,
isExpired: undefined,
beginTime: null,
endTime: null
}
this.handleQuery()
},
/** 处理日期范围变化 */
handleDateRangeChange(val) {
if (val && val.length === 2) {
this.queryParams.beginTime = val[0]
this.queryParams.endTime = val[1]
} else {
this.queryParams.beginTime = null
this.queryParams.endTime = null
}
},
/** 导出按钮操作 */
handleExport() {
const params = { ...this.queryParams }
this.$modal.confirm('是否确认导出所有礼金数据项?').then(() => {
this.exportLoading = true
return exportGiftCoupons(params)
}).then(response => {
this.$download.excel(response, '礼金数据.xlsx')
this.exportLoading = false
}).catch(() => {
this.exportLoading = false
})
},
/** 创建礼金 */
handleCreate() {
this.createForm = {
materialUrl: '',
skuName: '',
owner: 'g',
amount: null,
quantity: 10
}
this.createDialogVisible = true
},
/** 从链接中提取SKU ID */
extractSkuIdFromUrl(url) {
if (!url) return null
// 如果是纯数字,直接返回
if (/^\d+$/.test(url.trim())) {
return url.trim()
}
// 从URL中提取SKU ID
// 京东链接格式https://item.jd.com/100012043978.html 或 jd.com/123456.html
const skuMatch = url.match(/(?:item\.jd\.com|jd\.com)\/(\d+)/i)
if (skuMatch && skuMatch[1]) {
return skuMatch[1]
}
// 如果包含skuId参数
const paramMatch = url.match(/[?&]skuId=(\d+)/i)
if (paramMatch && paramMatch[1]) {
return paramMatch[1]
}
// 无法提取,返回原链接(让后端处理)
return url.trim()
},
/** 提交创建礼金 */
async submitCreate() {
this.$refs.createForm.validate(async (valid) => {
if (!valid) return
// 金额校验1-50元参考JD项目中的isValidAmount方法
if (!this.createForm.amount || this.createForm.amount < 1 || this.createForm.amount > 50) {
this.$modal.msgError('礼金金额必须在1-50元之间')
return
}
if (!this.createForm.quantity || this.createForm.quantity < 1 || this.createForm.quantity > 100) {
this.$modal.msgError('礼金数量必须在1-100之间')
return
}
// 提取SKU ID或使用materialUrl
const materialUrl = this.createForm.materialUrl.trim()
if (!materialUrl) {
this.$modal.msgError('请输入商品链接或SKU ID')
return
}
const skuId = this.extractSkuIdFromUrl(materialUrl)
const isUrl = skuId !== materialUrl
this.createLoading = true
try {
// 构建请求参数参考JD项目的参数格式
const params = {
amount: this.createForm.amount,
quantity: this.createForm.quantity,
owner: this.createForm.owner || 'g',
skuName: this.createForm.skuName || ''
}
// 根据提取结果设置skuId或materialUrl
if (isUrl && skuId) {
// 如果从URL中提取到了SKU ID优先使用skuId
params.skuId = skuId
} else if (isUrl) {
// 如果提取不到SKU ID使用原始URL
params.materialUrl = materialUrl
} else {
// 纯数字作为SKU ID
params.skuId = skuId
}
// 调用创建礼金接口
const res = await createGiftCoupon(params)
if (res && res.code === 200) {
// 解析返回的giftCouponKey参考xbmessage中的处理方式
let giftKey = null
if (typeof res.data === 'string') {
giftKey = res.data
} else if (res.data) {
giftKey = res.data.giftCouponKey || res.data.giftKey || res.data.data?.giftCouponKey
}
if (giftKey) {
// 可选自动生成短链参考JD项目的完整流程
try {
const transferParams = { materialUrl: materialUrl }
if (skuId && isUrl) {
transferParams.materialUrl = skuId
} else {
transferParams.materialUrl = materialUrl
}
transferParams.giftCouponKey = giftKey
const tf = await transferWithGift(transferParams)
if (tf && tf.code === 200) {
let shortUrl = ''
if (typeof tf.data === 'string') {
shortUrl = tf.data
} else if (tf.data && tf.data.shortURL) {
shortUrl = tf.data.shortURL
}
if (shortUrl) {
this.$modal.msgSuccess('礼金创建成功,短链已生成')
} else {
this.$modal.msgSuccess('礼金创建成功礼金Key' + giftKey)
}
} else {
this.$modal.msgSuccess('礼金创建成功礼金Key' + giftKey)
}
} catch (e) {
this.$modal.msgSuccess('礼金创建成功礼金Key' + giftKey)
}
} else {
this.$modal.msgWarning('礼金创建完成但未返回礼金Key请检查')
}
this.createDialogVisible = false
// 刷新列表和统计
this.getList()
this.loadStatistics()
} else {
// 错误处理
const errorMsg = res?.msg || res?.data?.msg || '创建失败'
this.$modal.msgError(errorMsg)
}
} catch (e) {
console.error('创建礼金失败', e)
this.$modal.msgError('创建失败:' + (e.message || e.msg || '未知错误'))
} finally {
this.createLoading = false
}
})
},
/** 查看详情 */
async handleDetail(row) {
this.activeDetailTab = 'info'
this.detailVisible = true
try {
const res = await getGiftCoupon(row.giftCouponKey)
if (res && res.code === 200) {
if (res.data.giftCoupon) {
this.detailData = res.data
} else {
// 兼容直接返回GiftCoupon对象的情况
this.detailData = {
giftCoupon: res.data,
orders: []
}
}
} else {
this.$modal.msgError('获取详情失败')
this.detailVisible = false
}
} catch (e) {
this.$modal.msgError('获取详情失败')
this.detailVisible = false
}
},
/** 生成短链URL */
getShortUrl(positionId) {
// 根据positionId生成短链这里需要根据实际情况调整
if (!positionId) return ''
return `https://u.jd.com/${positionId}`
},
/** 复制到剪贴板 */
copyToClipboard(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
this.$modal.msgSuccess('复制成功')
}).catch(() => {
this.fallbackCopyToClipboard(text)
})
} else {
this.fallbackCopyToClipboard(text)
}
},
/** 降级复制方法 */
fallbackCopyToClipboard(text) {
const textArea = document.createElement('textarea')
textArea.value = text
textArea.style.position = 'fixed'
textArea.style.opacity = '0'
document.body.appendChild(textArea)
textArea.select()
try {
document.execCommand('copy')
this.$modal.msgSuccess('复制成功')
} catch (err) {
this.$modal.msgError('复制失败')
}
document.body.removeChild(textArea)
},
/** 格式化金额 */
formatMoney(amount) {
if (!amount) return '0.00'
return Number(amount).toFixed(2)
},
/** 格式化时间 */
parseTime
}
}
</script>
<style scoped>
.el-card {
cursor: pointer;
}
</style>