feat:添加admin相关接口

This commit is contained in:
2025-11-13 15:05:54 +08:00
parent 11cb40c86a
commit 067e0539ba
58 changed files with 18736 additions and 273 deletions
+533
View File
@@ -0,0 +1,533 @@
<template>
<div class="discount-code-container">
<!-- 搜索和操作栏 -->
<el-card class="filter-container" shadow="never">
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增优惠码
</el-button>
<el-button type="success" @click="fetchDiscountList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</el-card>
<!-- 优惠码列表 -->
<el-card class="table-container" shadow="never">
<el-table
v-loading="loading"
:data="discountList"
@selection-change="handleSelectionChange"
style="width: 100%"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="code" label="优惠码" min-width="150" />
<el-table-column prop="name" label="名称" min-width="180" />
<el-table-column label="优惠类型" width="120">
<template #default="{ row }">
<el-tag :type="row.percentage ? 'success' : 'primary'">
{{ row.percentage ? '百分比折扣' : '固定金额' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="优惠值" width="120">
<template #default="{ row }">
<span v-if="row.percentage" class="discount-value">{{ (row.percentage / 100).toFixed(0) }}%</span>
<span v-else class="amount">¥{{ (row.amount / 100).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="最低消费" width="120">
<template #default="{ row }">
¥{{ (row.minAmount / 100).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="最大抵扣" width="120">
<template #default="{ row }">
<span v-if="row.maxAmount">¥{{ (row.maxAmount / 100).toFixed(2) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="maxTimes" label="最大使用次数" width="120" />
<el-table-column prop="userTimes" label="单用户次数" width="120" />
<el-table-column label="可叠加" width="100" align="center">
<template #default="{ row }">
<el-icon v-if="row.canStacking" color="#67c23a" :size="20"><SuccessFilled /></el-icon>
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
</template>
</el-table-column>
<el-table-column label="续费可用" width="100" align="center">
<template #default="{ row }">
<el-icon v-if="row.renew" color="#67c23a" :size="20"><SuccessFilled /></el-icon>
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="success" link @click="handleView(row)">查看</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</el-card>
<!-- 优惠码表单对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增优惠码' : '编辑优惠码'"
width="700px"
>
<el-form
ref="discountFormRef"
:model="discountForm"
:rules="discountRules"
label-width="140px"
>
<el-form-item label="优惠码" prop="code">
<el-input v-model="discountForm.code" placeholder="请输入优惠码" />
</el-form-item>
<el-form-item label="优惠码名称" prop="name">
<el-input v-model="discountForm.name" placeholder="请输入优惠码名称" />
</el-form-item>
<el-form-item label="备注" prop="note">
<el-input v-model="discountForm.note" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="优惠类型" prop="discount_mode">
<el-radio-group v-model="discountForm.discount_mode">
<el-radio label="amount">固定金额</el-radio>
<el-radio label="percentage">百分比折扣</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="discountForm.discount_mode === 'amount'" label="优惠金额(元)" prop="amount">
<el-input-number v-model="discountForm.amount" :min="0" :precision="2" :step="0.01" placeholder="请输入优惠金额" style="width: 100%" />
</el-form-item>
<el-form-item v-if="discountForm.discount_mode === 'percentage'" label="优惠百分比(%)" prop="percentage">
<el-input-number v-model="discountForm.percentage" :min="0" :max="100" :precision="0" placeholder="请输入百分比(1-100)" style="width: 100%" />
</el-form-item>
<el-form-item label="最低消费(元)" prop="min_amount">
<el-input-number v-model="discountForm.min_amount" :min="0" :precision="2" :step="0.01" placeholder="满多少可使用" style="width: 100%" />
</el-form-item>
<el-form-item label="最大抵扣(元)" prop="max_amount">
<el-input-number v-model="discountForm.max_amount" :min="0" :precision="2" :step="0.01" placeholder="0表示无限制" style="width: 100%" />
</el-form-item>
<el-form-item label="最大使用次数" prop="max_times">
<el-input-number v-model="discountForm.max_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
</el-form-item>
<el-form-item label="单用户最大次数" prop="user_times">
<el-input-number v-model="discountForm.user_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
</el-form-item>
<el-form-item label="有效期" prop="timeRange">
<el-date-picker
v-model="discountForm.timeRange"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
:teleported="true"
popper-class="discount-date-picker"
placement="top-start"
:editable="true"
:clearable="true"
style="width: 100%"
@keyup.enter="handleDatePickerEnter"
/>
</el-form-item>
<el-form-item label="续费可用" prop="renew">
<el-switch v-model="discountForm.renew" active-text="是" inactive-text="否" />
</el-form-item>
<el-form-item label="同类型可叠加" prop="can_stacking">
<el-switch v-model="discountForm.can_stacking" active-text="是" inactive-text="否" />
</el-form-item>
<el-form-item label="其他类型可叠加" prop="can_combine">
<el-switch v-model="discountForm.can_combine" active-text="是" inactive-text="否" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
<!-- 详情查看对话框 -->
<DiscountDetailDialog
v-model="detailDialogVisible"
type="code"
:detail-data="currentDetail"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Search, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
import {
getDiscountCodeList,
getDiscountCodeDetail,
createDiscountCode,
updateDiscountCode,
deleteDiscountCode
} from '@/api/admin/discount'
import { timeToTimestamp } from '@/utils/tool'
import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue'
// 查询参数
const queryParams = reactive({
discount_type: 'code', // 固定为code表示优惠码
page: 1,
count: 10
})
// 优惠码表单
const discountForm = reactive({
code_id: undefined,
discount_type: 'code', // 固定为code
code: '',
name: '',
note: '',
discount_mode: 'amount', // amount 或 percentage
amount: 0,
percentage: 0,
min_amount: 0,
max_amount: 0,
max_times: 0,
user_times: 0,
timeRange: [],
renew: false,
can_stacking: false,
can_combine: false
})
const discountRules = {
code: [
{ required: true, message: '请输入优惠码', trigger: 'blur' }
],
name: [
{ required: true, message: '请输入优惠码名称', trigger: 'blur' }
],
discount_mode: [
{ required: true, message: '请选择优惠类型', trigger: 'change' }
]
}
// 状态数据
const loading = ref(false)
const discountList = ref([])
const total = ref(0)
const selectedRows = ref([])
const dialogVisible = ref(false)
const dialogType = ref('add')
const discountFormRef = ref(null)
const detailDialogVisible = ref(false)
const currentDetail = ref(null)
// 获取优惠码列表
const fetchDiscountList = async () => {
loading.value = true
try {
const res = await getDiscountCodeList(queryParams)
console.log('优惠码列表数据:', res.data)
if (res.data.code === 200) {
discountList.value = res.data.data?.data || []
total.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取优惠码列表失败:', error)
ElMessage.error('获取优惠码列表失败')
} finally {
loading.value = false
}
}
// 选择项变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
// 分页
const handleSizeChange = (size) => {
queryParams.count = size
fetchDiscountList()
}
const handleCurrentChange = (page) => {
queryParams.page = page
fetchDiscountList()
}
// 新增优惠码
const handleAdd = () => {
dialogType.value = 'add'
dialogVisible.value = true
Object.assign(discountForm, {
code_id: undefined,
discount_type: 'code',
code: '',
name: '',
note: '',
discount_mode: 'amount',
amount: 0,
percentage: 0,
min_amount: 0,
max_amount: 0,
max_times: 0,
user_times: 0,
timeRange: [],
renew: false,
can_stacking: false,
can_combine: false
})
discountFormRef.value?.resetFields()
}
// 编辑优惠码
const handleEdit = (row) => {
dialogType.value = 'edit'
dialogVisible.value = true
// 转换日期字符串为日期选择器格式
const startTime = row.startTime ? new Date(row.startTime).toLocaleString('zh-CN', {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'}).replace(/\//g, '-') : ''
const endTime = row.endTime ? new Date(row.endTime).toLocaleString('zh-CN', {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'}).replace(/\//g, '-') : ''
Object.assign(discountForm, {
code_id: row.id,
discount_type: 'code',
code: row.code,
name: row.name,
note: row.note || '',
discount_mode: row.percentage ? 'percentage' : 'amount',
amount: row.amount ? row.amount / 100 : 0,
percentage: row.percentage ? row.percentage / 100 : 0,
min_amount: row.minAmount ? row.minAmount / 100 : 0,
max_amount: row.maxAmount ? row.maxAmount / 100 : 0,
max_times: row.maxTimes || 0,
user_times: row.userTimes || 0,
timeRange: startTime && endTime ? [startTime, endTime] : [],
renew: row.renew || false,
can_stacking: row.canStacking || false,
can_combine: row.canCombine || false
})
}
// 查看优惠码详情
const handleView = async (row) => {
try {
const res = await getDiscountCodeDetail({ code_id: row.id })
console.log('优惠码详情:', res.data)
if (res.data.code === 200) {
currentDetail.value = res.data.data
detailDialogVisible.value = true
}
} catch (error) {
console.error('获取优惠码详情失败:', error)
ElMessage.error('获取优惠码详情失败')
}
}
// 删除优惠码
const handleDelete = (row) => {
ElMessageBox.confirm(`确认删除优惠码 ${row.code} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteDiscountCode({ code_id: row.id })
console.log('删除响应:', res.data)
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchDiscountList()
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
// 批量删除
const handleBatchDelete = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请至少选择一条记录')
return
}
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
loading.value = true
try {
const deletePromises = selectedRows.value.map(row =>
deleteDiscountCode({ code_id: row.id })
)
const results = await Promise.allSettled(deletePromises)
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.data?.code === 200).length
const failCount = results.length - successCount
if (failCount === 0) {
ElMessage.success(`批量删除成功,共删除 ${successCount} 条记录`)
} else if (successCount === 0) {
ElMessage.error(`批量删除失败,所有 ${failCount} 条记录删除失败`)
} else {
ElMessage.warning(`批量删除完成,成功 ${successCount} 条,失败 ${failCount}`)
}
fetchDiscountList()
} catch (error) {
console.error('批量删除失败:', error)
ElMessage.error('批量删除操作异常')
} finally {
loading.value = false
}
}).catch(() => {})
}
// 处理日期选择器回车事件
const handleDatePickerEnter = (event) => {
// 回车键确认日期选择
const datePicker = event.target.closest('.el-date-editor')
if (datePicker) {
// 触发失焦事件,确认日期选择
event.target.blur()
}
}
// 提交表单
const submitForm = () => {
discountFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
discount_type: 'code',
code: discountForm.code,
name: discountForm.name,
note: discountForm.note,
min_amount: Math.round(discountForm.min_amount * 100),
max_amount: Math.round(discountForm.max_amount * 100),
max_times: discountForm.max_times || 0,
user_times: discountForm.user_times || 0,
renew: discountForm.renew,
can_stacking: discountForm.can_stacking,
can_combine: discountForm.can_combine
}
// 根据优惠类型设置amount或percentage
if (discountForm.discount_mode === 'percentage') {
submitData.percentage = Math.round(discountForm.percentage * 100)
submitData.amount = 0
} else {
submitData.amount = Math.round(discountForm.amount * 100)
submitData.percentage = 0
}
// 处理时间(转换为秒级时间戳)
if (discountForm.timeRange && discountForm.timeRange.length === 2) {
submitData.start_time = timeToTimestamp(discountForm.timeRange[0])
submitData.end_time = timeToTimestamp(discountForm.timeRange[1])
} else {
submitData.start_time = ''
submitData.end_time = ''
}
// 如果是编辑,添加code_id
if (dialogType.value === 'edit') {
submitData.code_id = discountForm.code_id
}
console.log('提交优惠码数据:', submitData)
let res
if (dialogType.value === 'add') {
res = await createDiscountCode(submitData)
} else {
res = await updateDiscountCode(submitData)
}
console.log('提交响应:', res.data)
if (res.data.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
dialogVisible.value = false
fetchDiscountList()
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
// 初始化
onMounted(() => {
fetchDiscountList()
})
</script>
<style scoped>
.discount-code-container {
padding: 0;
}
.filter-container {
margin-bottom: 20px;
border-radius: 8px;
}
.action-bar {
display: flex;
gap: 12px;
}
.table-container {
border-radius: 8px;
}
.amount {
color: #f56c6c;
font-weight: bold;
font-size: 14px;
}
.discount-value {
color: #67c23a;
font-weight: bold;
font-size: 14px;
}
.pagination {
margin-top: 24px;
justify-content: flex-end;
}
</style>
<style>
/* 时间选择器弹出层样式 - 非 scoped */
.discount-date-picker {
z-index: 9999 !important;
}
.discount-date-picker .el-picker-panel {
max-width: 90vw;
}
</style>
+719
View File
@@ -0,0 +1,719 @@
<template>
<div class="discount-goods-container">
<!-- 搜索和操作栏 -->
<el-card class="filter-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="代金卷">
<el-select
v-model="queryParams.code_id"
placeholder="请选择代金券"
filterable
clearable
style="width: 280px"
>
<el-option
v-for="item in voucherListOptions"
:key="item.id"
:label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery" :disabled="!queryParams.code_id">
<el-icon><Search /></el-icon>查询
</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增商品关联
</el-button>
<el-button type="success" @click="fetchGoodsList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</el-card>
<!-- 商品关联列表 -->
<el-card class="table-container" shadow="never">
<el-table
v-loading="loading"
:data="goodsList"
@selection-change="handleSelectionChange"
style="width: 100%"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="discountId" label="代金券ID" width="120" />
<el-table-column label="关联对象ID" width="120">
<template #default="{ row }">
{{ row.goodId || row.goodGroupId || '-' }}
</template>
</el-table-column>
<el-table-column label="名称" min-width="200">
<template #default="{ row }">
{{ row.good?.name || row.goodGroup?.name || '-' }}
</template>
</el-table-column>
<el-table-column label="类型" width="120">
<template #default="{ row }">
<el-tag :type="getGoodsTypeTagByRow(row)">
{{ getGoodsTypeNameByRow(row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" min-width="150">
<template #default="{ row }">
{{ row.good?.table || row.goodGroup?.note || '-' }}
</template>
</el-table-column>
<el-table-column label="商品价格" width="120">
<template #default="{ row }">
<span v-if="row.good?.price" class="price">¥{{ (row.good.price / 100).toFixed(2) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</el-card>
<!-- 添加/编辑商品关联对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增商品关联' : '编辑商品关联'"
width="600px"
>
<el-form
ref="formRef"
:model="form"
:rules="formRules"
label-width="120px"
>
<el-form-item label="代金券" prop="code_id">
<el-select
v-model="form.code_id"
placeholder="请选择代金券"
filterable
clearable
:disabled="dialogType === 'edit'"
style="width: 100%"
>
<el-option
v-for="item in voucherListOptions"
:key="item.id"
:label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="选择类型" prop="select_type" v-if="dialogType === 'add'">
<el-radio-group v-model="form.select_type" @change="handleSelectTypeChange">
<el-radio value="product">商品</el-radio>
<el-radio value="product_group">商品组</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择商品" prop="selected_product" v-if="dialogType === 'add' && form.select_type === 'product'">
<el-select
v-model="form.selected_product"
placeholder="请选择商品"
filterable
clearable
style="width: 100%"
@change="handleProductChange"
>
<el-option
v-for="item in productOptions"
:key="item.id"
:label="`${item.name} (ID: ${item.id})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="选择商品组" prop="selected_group" v-if="dialogType === 'add' && form.select_type === 'product_group'">
<el-select
v-model="form.selected_group"
placeholder="请选择商品组"
filterable
clearable
style="width: 100%"
@change="handleProductGroupChange"
>
<el-option
v-for="item in productGroupOptions"
:key="item.id"
:label="`${item.name} (ID: ${item.id})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<!-- 编辑模式显示字段 -->
<template v-if="dialogType === 'edit'">
<el-form-item label="关联类型">
<el-tag :type="form.goods_type === 'product' ? 'primary' : 'warning'">
{{ form.goods_type === 'product' ? '商品' : '商品组' }}
</el-tag>
</el-form-item>
<el-form-item label="关联对象ID">
<el-select v-model="form.goods_id" style="width: 100%">
<template v-if="form.goods_type === 'product'">
<el-option v-for="item in productOptions" :key="item.id" :label="`${item.name} (ID: ${item.id})`" :value="item.id" />
</template>
<template v-else>
<el-option v-for="item in productGroupOptions" :key="item.id" :label="`${item.name} (ID: ${item.id})`" :value="item.id" />
</template>
</el-select>
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
import {
getDiscountGoodsList,
addDiscountGoods,
updateDiscountGoods,
deleteDiscountGoods,
getDiscountCodeList
} from '@/api/admin/discount'
import {
getProductList,
getProductGroupList
} from '@/api/admin/product'
// 查询参数
const queryParams = reactive({
code_id: '',
page: 1,
count: 10
})
// 表单数据
const form = reactive({
id: undefined,
code_id: undefined,
goods_id: undefined,
goods_name: '',
goods_type: '',
select_type: 'product', // 选择类型:product 或 product_group
selected_product: undefined, // 选中的商品ID
selected_group: undefined // 选中的商品组ID
})
const formRules = {
code_id: [
{ required: true, message: '请选择代金券', trigger: 'change' }
],
select_type: [
{ required: true, message: '请选择类型', trigger: 'change' }
],
selected_product: [
{ required: true, message: '请选择商品', trigger: 'change' }
],
selected_group: [
{ required: true, message: '请选择商品组', trigger: 'change' }
],
goods_id: [
{ required: true, message: '请输入商品ID', trigger: 'blur' }
],
goods_name: [
{ required: true, message: '请输入商品名称', trigger: 'blur' }
],
goods_type: [
{ required: true, message: '请选择商品类型', trigger: 'change' }
]
}
// 状态数据
const loading = ref(false)
const goodsList = ref([])
const total = ref(0)
const selectedRows = ref([])
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref(null)
const voucherListOptions = ref([]) // 代金券列表选项
const productOptions = ref([]) // 商品列表选项
const productGroupOptions = ref([]) // 商品组列表选项
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
// 获取商品类型名称(根据行数据)
const getGoodsTypeNameByRow = (row) => {
// 判断是否有 goodGroup 对象(选择的是商品组)
if (row.goodGroup) {
return '商品组'
}
// 判断是否有 good 对象(选择的是商品)
if (row.good) {
return '商品'
}
return '-'
}
// 获取商品类型标签(根据行数据)
const getGoodsTypeTagByRow = (row) => {
// 商品组用橙色
if (row.goodGroup) {
return 'warning'
}
// 商品用蓝色
if (row.good) {
return 'primary'
}
return 'info'
}
// 获取商品类型名称(兼容旧版)
const getGoodsTypeName = (type) => {
const typeMap = {
'product': '商品',
'product_group': '商品组',
'cloud_server': '云服务器',
'cloud_database': '云数据库',
'cloud_storage': '云存储',
'cdn': 'CDN',
'other': '其他'
}
return typeMap[type] || type
}
// 获取商品类型标签(兼容旧版)
const getGoodsTypeTag = (type) => {
const tagMap = {
'product': 'primary',
'product_group': 'warning',
'cloud_server': 'primary',
'cloud_database': 'success',
'cloud_storage': 'warning',
'cdn': 'info',
'other': 'default'
}
return tagMap[type] || 'default'
}
// 获取代金券列表选项
const fetchVoucherListOptions = async () => {
try {
const res = await getDiscountCodeList({
page: 1,
count: 1000,
discount_type: 'coupon'
})
console.log('获取代金券列表:', res.data)
if (res.data.code === 200) {
voucherListOptions.value = res.data.data?.data || []
}
} catch (error) {
console.error('获取代金券列表失败:', error)
ElMessage.error('获取代金券列表失败')
}
}
// 获取商品列表
const fetchProductList = async () => {
try {
const res = await getProductList({
page: 1,
count: 1000
})
console.log('获取商品列表:', res.data)
if (res.data.code === 200) {
productOptions.value = res.data.data?.data || []
}
} catch (error) {
console.error('获取商品列表失败:', error)
ElMessage.error('获取商品列表失败')
}
}
// 获取商品组列表
const fetchProductGroupList = async () => {
try {
const res = await getProductGroupList({
page: 1,
count: 1000
})
console.log('获取商品组列表:', res.data)
if (res.data.code === 200) {
productGroupOptions.value = res.data.data?.data || []
}
} catch (error) {
console.error('获取商品组列表失败:', error)
ElMessage.error('获取商品组列表失败')
}
}
// 选择类型变化
const handleSelectTypeChange = (type) => {
form.selected_product = undefined
form.selected_group = undefined
form.goods_id = undefined
form.goods_name = ''
form.goods_type = ''
}
// 选择商品变化
const handleProductChange = (productId) => {
const product = productOptions.value.find(item => item.id === productId)
if (product) {
form.goods_id = product.id
form.goods_name = product.goodsName || product.name || ''
form.goods_type = 'product'
}
}
// 选择商品组变化
const handleProductGroupChange = (groupId) => {
const group = productGroupOptions.value.find(item => item.id === groupId)
if (group) {
form.goods_id = group.id
form.goods_name = group.name || ''
form.goods_type = 'product_group'
}
}
// 获取商品关联列表
const fetchGoodsList = async () => {
if (!queryParams.code_id) {
ElMessage.warning('请选择代金券进行查询')
return
}
loading.value = true
try {
const res = await getDiscountGoodsList(queryParams)
console.log('商品关联列表数据:', res.data)
if (res.data.code === 200) {
goodsList.value = res.data.data || []
total.value = res.data.data?.length || 0
}
} catch (error) {
console.error('获取商品关联列表失败:', error)
ElMessage.error('获取商品关联列表失败')
} finally {
loading.value = false
}
}
// 查询
const handleQuery = () => {
queryParams.page = 1
fetchGoodsList()
}
// 重置查询
const resetQuery = () => {
queryParams.code_id = ''
queryParams.page = 1
goodsList.value = []
total.value = 0
}
// 选择项变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
// 分页
const handleSizeChange = (size) => {
queryParams.count = size
fetchGoodsList()
}
const handleCurrentChange = (page) => {
queryParams.page = page
fetchGoodsList()
}
// 新增商品关联
const handleAdd = () => {
dialogType.value = 'add'
dialogVisible.value = true
Object.assign(form, {
id: undefined,
code_id: queryParams.code_id ? Number(queryParams.code_id) : undefined,
goods_id: undefined,
goods_name: '',
goods_type: '',
select_type: 'product',
selected_product: undefined,
selected_group: undefined
})
formRef.value?.resetFields()
// 加载商品和商品组列表
fetchProductList()
fetchProductGroupList()
}
// 编辑商品关联
const handleEdit = (row) => {
dialogType.value = 'edit'
dialogVisible.value = true
// 判断是商品还是商品组
let goodsId, goodsName, goodsType
if (row.goodGroup) {
// 商品组
goodsId = row.goodGroupId
goodsName = row.goodGroup.name
goodsType = 'product_group'
} else if (row.good) {
// 商品
goodsId = row.goodId
goodsName = row.good.name
goodsType = 'product'
}
Object.assign(form, {
id: row.id,
code_id: row.discountId,
goods_id: goodsId,
goods_name: goodsName,
goods_type: goodsType
})
// 加载商品和商品组列表以便编辑
fetchProductList()
fetchProductGroupList()
}
// 删除商品关联
const handleDelete = (row) => {
ElMessageBox.confirm(`确认删除该商品关联吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteDiscountGoods({
discount_good_id: String(row.id),
code_id: String(row.discountId)
})
console.log('删除响应:', res.data)
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchGoodsList()
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
// 批量删除
const handleBatchDelete = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请至少选择一条记录')
return
}
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
loading.value = true
try {
const deletePromises = selectedRows.value.map(row =>
deleteDiscountGoods({
discount_good_id: String(row.id),
code_id: String(row.discountId)
})
)
const results = await Promise.allSettled(deletePromises)
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.data?.code === 200).length
const failCount = results.length - successCount
if (failCount === 0) {
ElMessage.success(`批量删除成功,共删除 ${successCount} 条记录`)
} else if (successCount === 0) {
ElMessage.error(`批量删除失败,所有 ${failCount} 条记录删除失败`)
} else {
ElMessage.warning(`批量删除完成,成功 ${successCount} 条,失败 ${failCount}`)
}
fetchGoodsList()
} catch (error) {
console.error('批量删除失败:', error)
ElMessage.error('批量删除操作异常')
} finally {
loading.value = false
}
}).catch(() => {})
}
// 提交表单
const submitForm = () => {
// 新增模式下的额外验证
if (dialogType.value === 'add') {
if (!form.code_id) {
ElMessage.warning('请选择代金券')
return
}
if (form.select_type === 'product' && !form.selected_product) {
ElMessage.warning('请选择商品')
return
}
if (form.select_type === 'product_group' && !form.selected_group) {
ElMessage.warning('请选择商品组')
return
}
if (!form.goods_id || !form.goods_name || !form.goods_type) {
ElMessage.warning('请先选择商品或商品组')
return
}
}
formRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
code_id: String(form.code_id)
}
// 根据选择类型决定传 good_id 还是 good_group_id
if (dialogType.value === 'add') {
if (form.select_type === 'product') {
// 选择的是商品,传 good_id
submitData.good_ids = String(form.goods_id)
} else if (form.select_type === 'product_group') {
// 选择的是商品组,传 good_group_id
submitData.good_group_ids = String(form.goods_id)
}
} else {
// 编辑模式:传 discount_good_id
submitData.discount_good_id = String(form.id)
// 根据 goods_type 判断传 good_id 还是 good_group_id
if (form.goods_type === 'product') {
submitData.good_id = String(form.goods_id)
} else if (form.goods_type === 'product_group') {
submitData.good_group_id = String(form.goods_id)
} else {
// 其他类型默认使用 good_id
submitData.good_id = String(form.goods_id)
}
}
console.log('提交商品关联数据:', submitData)
let res
if (dialogType.value === 'add') {
res = await addDiscountGoods(submitData)
} else {
res = await updateDiscountGoods(submitData)
}
console.log('提交响应:', res.data)
if (res.data.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
dialogVisible.value = false
fetchGoodsList()
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
// 初始化
onMounted(() => {
fetchVoucherListOptions()
if (queryParams.code_id) {
fetchGoodsList()
}
})
</script>
<style scoped>
.discount-goods-container {
padding: 0;
}
.filter-container {
margin-bottom: 20px;
border-radius: 8px;
}
.search-form {
margin-bottom: 15px;
}
.action-bar {
display: flex;
gap: 12px;
}
.table-container {
border-radius: 8px;
}
.pagination {
margin-top: 24px;
justify-content: flex-end;
}
.price {
color: #f56c6c;
font-weight: bold;
font-size: 14px;
}
</style>
+883
View File
@@ -0,0 +1,883 @@
<template>
<div class="discount-users-container">
<!-- 搜索和操作栏 -->
<el-card class="filter-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="代金卷">
<el-select
v-model="queryParams.code_id"
placeholder="请选择代金券"
filterable
clearable
style="width: 280px"
>
<el-option
v-for="item in voucherListOptions"
:key="item.id"
:label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery" :disabled="!queryParams.code_id">
<el-icon><Search /></el-icon>查询
</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增用户关联
</el-button>
<el-button type="success" @click="fetchUsersList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</el-card>
<!-- 用户关联列表 -->
<el-card class="table-container" shadow="never">
<el-table
v-loading="loading"
:data="usersList"
@selection-change="handleSelectionChange"
style="width: 100%"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="discountId" label="代金券ID" width="120" />
<el-table-column label="关联对象ID" width="130">
<template #default="{ row }">
{{ row.userId || row.userGroupId || '-' }}
</template>
</el-table-column>
<el-table-column label="类型" width="120">
<template #default="{ row }">
<el-tag :type="getUserTypeTagByRow(row)">
{{ getUserTypeNameByRow(row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</el-card>
<!-- 添加/编辑用户关联对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增用户关联' : '编辑用户关联'"
width="600px"
>
<el-form
ref="formRef"
:model="form"
:rules="formRules"
label-width="120px"
>
<el-form-item label="代金券" prop="code_id">
<el-select
v-model="form.code_id"
placeholder="请选择代金券"
filterable
clearable
:disabled="dialogType === 'edit'"
style="width: 100%"
>
<el-option
v-for="item in voucherListOptions"
:key="item.id"
:label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="选择类型" prop="select_type" v-if="dialogType === 'add'">
<el-radio-group v-model="form.select_type" @change="handleSelectTypeChange">
<el-radio value="user">用户</el-radio>
<el-radio value="user_group">用户组</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择用户" prop="selected_user" v-if="dialogType === 'add' && form.select_type === 'user'">
<div class="user-selector-wrapper">
<div class="selected-user-display" v-if="form.selected_user">
<el-tag type="primary" closable @close="clearSelectedUser">
{{ getSelectedUserName() }}
</el-tag>
</div>
<el-button
type="primary"
plain
@click="openUserSelector"
style="width: 100%"
>
<el-icon><User /></el-icon>
{{ form.selected_user ? '重新选择用户' : '选择用户' }}
</el-button>
</div>
</el-form-item>
<el-form-item label="选择用户组" prop="selected_group" v-if="dialogType === 'add' && form.select_type === 'user_group'">
<el-select
v-model="form.selected_group"
placeholder="请选择用户组"
filterable
clearable
style="width: 100%"
@change="handleUserGroupChange"
>
<el-option
v-for="item in userGroupOptions"
:key="item.Id"
:label="`${item.Name} (ID: ${item.Id})`"
:value="item.Id"
/>
</el-select>
</el-form-item>
<!-- 编辑模式显示字段 -->
<template v-if="dialogType === 'edit'">
<el-form-item label="关联类型">
<el-tag :type="form.select_type === 'user' ? 'primary' : 'warning'">
{{ form.select_type === 'user' ? '用户' : '用户组' }}
</el-tag>
</el-form-item>
<el-form-item v-if="form.select_type === 'user'" label="用户ID" prop="user_id">
<div class="user-selector-wrapper">
<div class="selected-user-display" v-if="form.user_id">
<el-tage type="primary" closable @close="clearSelectedUser">
{{ getSelectedUserName() }}
</el-tage>
</div>
<el-button type="primary" plan @click="openUserSelector" style="width: 100%;">
<el-icon><User /></el-icon>
{{ form.user_id ? '重新选择用户' : '选择用户' }}
</el-button>
</div>
</el-form-item>
<el-form-item v-if="form.select_type === 'user_group'" label="用户组ID">
<el-select v-model="form.user_group_id" placeholder="">
<el-option v-for="item in userGroupOptions" :key="item.Id" :label="`${item.Name} (ID: ${item.Id})`" :value="item.Id">
</el-option>
</el-select>
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
<!-- 用户选择弹窗 -->
<el-dialog
v-model="userSelectorVisible"
title="选择用户"
width="800px"
class="user-selector-dialog"
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="userSearchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="searchUsers"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="searchUsers">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="resetUserSearch">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="userSelectorLoading"
:data="userSelectorList"
highlight-current-row
@current-change="handleUserSelectChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
{{ row.Status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="userSearchParams.page"
v-model:page-size="userSearchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="userSelectorTotal"
@size-change="handleUserSelectorSizeChange"
@current-change="handleUserSelectorPageChange"
background
class="selector-pagination"
/>
<template #footer>
<el-button @click="userSelectorVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
确定选择
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue'
import {
getDiscountUsersList,
addDiscountUsers,
updateDiscountUsers,
deleteDiscountUsers,
getDiscountCodeList
} from '@/api/admin/discount'
import {
getUserList,
getUserGroupList
} from '@/api/admin/user'
// 查询参数
const queryParams = reactive({
code_id: '',
page: 1,
count: 10
})
// 表单数据
const form = reactive({
id: undefined,
code_id: undefined,
user_id: undefined,
username: '',
email: '',
status: 1,
select_type: 'user', // 选择类型:user 或 user_group
selected_user: undefined, // 选中的用户ID
selected_group: undefined, // 选中的用户组ID
user_group_id: undefined // 用户组ID(用于提交)
})
const formRules = {
code_id: [
{ required: true, message: '请选择代金券', trigger: 'change' }
],
select_type: [
{ required: true, message: '请选择类型', trigger: 'change' }
],
selected_user: [
{ required: true, message: '请选择用户', trigger: 'change' }
],
selected_group: [
{ required: true, message: '请选择用户组', trigger: 'change' }
]
}
// 状态数据
const loading = ref(false)
const usersList = ref([])
const total = ref(0)
const selectedRows = ref([])
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref(null)
const voucherListOptions = ref([]) // 代金券列表选项
const userOptions = ref([]) // 用户列表选项
const userGroupOptions = ref([]) // 用户组列表选项
// 用户选择弹窗相关
const userSelectorVisible = ref(false)
const userSelectorLoading = ref(false)
const userSelectorList = ref([])
const userSelectorTotal = ref(0)
const selectedUserTemp = ref(null) // 临时存储选中的用户
const userSearchParams = reactive({
key: '',
page: 1,
count: 10
})
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
// 获取用户类型名称(根据行数据)
const getUserTypeNameByRow = (row) => {
// userId 不为 0 说明是用户
if (row.userId && row.userId !== 0) {
return '用户'
}
// userGroupId 不为 0 说明是用户组
if (row.userGroupId && row.userGroupId !== 0) {
return '用户组'
}
return '-'
}
// 获取用户类型标签(根据行数据)
const getUserTypeTagByRow = (row) => {
// 用户用蓝色
if (row.userId && row.userId !== 0) {
return 'primary'
}
// 用户组用橙色
if (row.userGroupId && row.userGroupId !== 0) {
return 'warning'
}
return 'info'
}
// 获取代金券列表选项
const fetchVoucherListOptions = async () => {
try {
const res = await getDiscountCodeList({
page: 1,
count: 1000,
discount_type: 'coupon'
})
console.log('获取代金券列表:', res.data)
if (res.data.code === 200) {
voucherListOptions.value = res.data.data?.data || []
}
} catch (error) {
console.error('获取代金券列表失败:', error)
ElMessage.error('获取代金券列表失败')
}
}
// 获取用户列表
const fetchUserList = async () => {
try {
const res = await getUserList({
page: 1,
count: 10,
key: ''
})
console.log('获取用户列表:', res.data)
if (res.data.code === 200) {
userOptions.value = res.data.data?.data || []
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
}
}
// 获取用户组列表
const fetchUserGroupList = async () => {
try {
const res = await getUserGroupList({
page: 1,
count: 10000,
key: ''
})
console.log('获取用户组列表:', res.data)
if (res.data.code === 200) {
userGroupOptions.value = res.data.data?.data || []
}
} catch (error) {
console.error('获取用户组列表失败:', error)
ElMessage.error('获取用户组列表失败')
}
}
// 打开用户选择器
const openUserSelector = () => {
userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 获取用户选择器列表
const fetchUserSelectorList = async () => {
userSelectorLoading.value = true
try {
const res = await getUserList(userSearchParams)
console.log('用户选择器列表:', res.data)
if (res.data.code === 200) {
userSelectorList.value = res.data.data?.data || []
userSelectorTotal.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
userSelectorLoading.value = false
}
}
// 搜索用户
const searchUsers = () => {
userSearchParams.page = 1
fetchUserSelectorList()
}
// 重置用户搜索
const resetUserSearch = () => {
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 用户选择变化
const handleUserSelectChange = (row) => {
selectedUserTemp.value = row
}
// 用户选择器分页
const handleUserSelectorSizeChange = (size) => {
userSearchParams.count = size
fetchUserSelectorList()
}
const handleUserSelectorPageChange = (page) => {
userSearchParams.page = page
fetchUserSelectorList()
}
// 确认用户选择
const confirmUserSelection = () => {
if (!selectedUserTemp.value) {
ElMessage.warning('请选择一个用户')
return
}
form.selected_user = selectedUserTemp.value.UserId
form.user_id = selectedUserTemp.value.UserId
// 将选中的用户添加到 userOptions 中(如果不存在)
if (!userOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
userOptions.value.push(selectedUserTemp.value)
}
userSelectorVisible.value = false
ElMessage.success('用户选择成功')
}
// 清除选中的用户
const clearSelectedUser = () => {
form.selected_user = undefined
form.user_id = undefined
}
// 获取选中用户的显示名称
const getSelectedUserName = () => {
let user;
user = userOptions.value.find(u => u.UserId === form.selected_user)
console.log("是否有用户的user_id信息",form.user_id)
//需要进行判断是否是编辑的用户通过user_id进行刷选到所需要的用户对象信息
if(form.user_id){
console.log("用户表信息:",userOptions.value)
user = userOptions.value.find(u => u.UserId === form.user_id)
}
console.log("获取用户的名称",user)
return user ? `${user.UserName} (ID: ${user.UserId})` : '未知用户'
}
// 选择类型变化
const handleSelectTypeChange = (type) => {
form.selected_user = undefined
form.selected_group = undefined
form.user_id = undefined
form.user_group_id = undefined
}
// 选择用户变化
const handleUserChange = (userId) => {
const user = userOptions.value.find(item => item.UserId === userId)
if (user) {
form.user_id = user.UserId
}
}
// 选择用户组变化
const handleUserGroupChange = (groupId) => {
const group = userGroupOptions.value.find(item => item.Id === groupId)
if (group) {
form.user_group_id = group.Id
}
}
// 获取用户关联列表
const fetchUsersList = async () => {
if (!queryParams.code_id) {
ElMessage.warning('请选择代金券进行查询')
return
}
loading.value = true
try {
const res = await getDiscountUsersList(queryParams)
console.log('用户关联列表数据:', res.data)
if (res.data.code === 200) {
usersList.value = res.data.data || []
total.value = res.data.data?.length || 0
}
} catch (error) {
console.error('获取用户关联列表失败:', error)
ElMessage.error('获取用户关联列表失败')
} finally {
loading.value = false
}
}
// 查询
const handleQuery = () => {
queryParams.page = 1
fetchUsersList()
}
// 重置查询
const resetQuery = () => {
queryParams.code_id = ''
queryParams.page = 1
usersList.value = []
total.value = 0
}
// 选择项变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
// 分页
const handleSizeChange = (size) => {
queryParams.count = size
fetchUsersList()
}
const handleCurrentChange = (page) => {
queryParams.page = page
fetchUsersList()
}
// 新增用户关联
const handleAdd = () => {
dialogType.value = 'add'
dialogVisible.value = true
Object.assign(form, {
id: undefined,
code_id: queryParams.code_id ? Number(queryParams.code_id) : undefined,
user_id: undefined,
username: '',
email: '',
status: 1,
select_type: 'user',
selected_user: undefined,
selected_group: undefined,
user_group_id: undefined
})
formRef.value?.resetFields()
fetchUserGroupList()
}
// 编辑用户关联
const handleEdit = (row) => {
dialogType.value = 'edit'
dialogVisible.value = true
// 判断是用户还是用户组
let userId, userGroupId, selectType
console.log("获取编辑的当前信息:",row)
if (row.userId && row.userId !== 0) {
// 用户
userId = row.userId
userGroupId = undefined
selectType = 'user'
} else if (row.userGroupId && row.userGroupId !== 0) {
// 用户组
userId = undefined
userGroupId = row.userGroupId
selectType = 'user_group'
}
Object.assign(form, {
id: row.id,
code_id: row.discountId,
user_id: userId,
user_group_id: userGroupId,
select_type: selectType,
username: '',
email: '',
status: 1
})
//点击编辑需要初始化加载用户列表
fetchUserList()
}
// 删除用户关联
const handleDelete = (row) => {
ElMessageBox.confirm(`确认删除该用户关联吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteDiscountUsers({
discount_user_id: String(row.id),
code_id: String(row.discountId)
})
console.log('删除响应:', res.data)
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchUsersList()
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
// 批量删除
const handleBatchDelete = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请至少选择一条记录')
return
}
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
loading.value = true
try {
const deletePromises = selectedRows.value.map(row =>
deleteDiscountUsers({
discount_user_id: String(row.id),
code_id: String(row.discountId)
})
)
const results = await Promise.allSettled(deletePromises)
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.data?.code === 200).length
const failCount = results.length - successCount
if (failCount === 0) {
ElMessage.success(`批量删除成功,共删除 ${successCount} 条记录`)
} else if (successCount === 0) {
ElMessage.error(`批量删除失败,所有 ${failCount} 条记录删除失败`)
} else {
ElMessage.warning(`批量删除完成,成功 ${successCount} 条,失败 ${failCount}`)
}
fetchUsersList()
} catch (error) {
console.error('批量删除失败:', error)
ElMessage.error('批量删除操作异常')
} finally {
loading.value = false
}
}).catch(() => {})
}
// 提交表单
const submitForm = () => {
// 新增模式下的额外验证
if (dialogType.value === 'add') {
if (!form.code_id) {
ElMessage.warning('请选择代金券')
return
}
if (form.select_type === 'user' && !form.selected_user) {
ElMessage.warning('请选择用户')
return
}
if (form.select_type === 'user_group' && !form.selected_group) {
ElMessage.warning('请选择用户组')
return
}
if (!form.user_id && !form.user_group_id) {
ElMessage.warning('请先选择用户或用户组')
return
}
}
formRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
code_id: String(form.code_id)
}
// 根据选择类型决定传 user_id 还是 user_group_id
if (dialogType.value === 'add') {
if (form.select_type === 'user') {
// 选择的是用户,传 user_id
submitData.user_id = String(form.user_id)
} else if (form.select_type === 'user_group') {
// 选择的是用户组,传 user_group_id
submitData.user_group_id = String(form.user_group_id)
}
} else {
// 编辑模式:根据类型传递对应的ID
if (form.select_type === 'user') {
submitData.user_id = String(form.user_id)
} else if (form.select_type === 'user_group') {
submitData.user_group_id = String(form.user_group_id)
}
// 编辑需要传 discount_user_id
submitData.discount_user_id = String(form.id)
}
console.log('提交用户关联数据:', submitData)
let res
if (dialogType.value === 'add') {
res = await addDiscountUsers(submitData)
} else {
res = await updateDiscountUsers(submitData)
}
console.log('提交响应:', res.data)
if (res.data.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
dialogVisible.value = false
fetchUsersList()
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
// 初始化
onMounted(() => {
fetchVoucherListOptions()
if (queryParams.code_id) {
fetchUsersList()
}
})
</script>
<style scoped>
.discount-users-container {
padding: 0;
}
.filter-container {
margin-bottom: 20px;
border-radius: 8px;
}
.search-form {
margin-bottom: 15px;
}
.action-bar {
display: flex;
gap: 12px;
}
.table-container {
border-radius: 8px;
}
.pagination {
margin-top: 24px;
justify-content: flex-end;
}
/* 用户选择器样式 */
.user-selector-wrapper {
width: 100%;
}
.selected-user-display {
margin-bottom: 12px;
padding: 8px 12px;
background: #f5f7fa;
border-radius: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.user-selector-dialog .selector-search {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #ebeef5;
}
.selector-pagination {
margin-top: 16px;
justify-content: flex-end;
}
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row):hover {
background-color: #f5f7fa;
}
:deep(.current-row) {
background-color: #ecf5ff !important;
}
</style>
+892
View File
@@ -0,0 +1,892 @@
<template>
<div class="user-voucher-container">
<!-- 搜索和操作栏 -->
<el-card class="filter-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="代金券">
<el-select
v-model="queryParams.code_id"
placeholder="请选择代金券"
filterable
clearable
style="width: 280px"
@change="handleVoucherSelect"
>
<el-option
v-for="item in voucherListOptions"
:key="item.id"
:label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery" :disabled="!queryParams.code_id">
<el-icon><Search /></el-icon>查询
</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>为用户添加代金券
</el-button>
<el-button type="success" @click="fetchUserVoucherList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</el-card>
<!-- 用户代金券列表 -->
<el-card class="table-container" shadow="never">
<el-table
v-loading="loading"
:data="userVoucherList"
@selection-change="handleSelectionChange"
style="width: 100%"
>
<el-table-column type="selection" width="55" />
<el-table-column label="ID" width="80">
<template #default="{ row }">
{{ row.Id || row.id }}
</template>
</el-table-column>
<el-table-column label="用户ID" width="100">
<template #default="{ row }">
{{ row.UserId || row.userId }}
</template>
</el-table-column>
<el-table-column label="代金券ID" width="100">
<template #default="{ row }">
{{ row.discountId }}
</template>
</el-table-column>
<el-table-column label="代金券名称" min-width="150">
<template #default="{ row }">
{{ row.discount?.name || '-' }}
</template>
</el-table-column>
<el-table-column label="面额" width="120">
<template #default="{ row }">
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
</template>
</el-table-column>
<el-table-column label="已使用/最大次数" width="150">
<template #default="{ row }">
<el-tag type="info">{{ row.useTimes || 0 }} / {{ row.maxUseTimes || row.discount?.maxTimes || 0 }}</el-tag>
</template>
</el-table-column>
<el-table-column label="过期时间" width="180">
<template #default="{ row }">
{{ formatDate(row.expireAt) }}
</template>
</el-table-column>
<el-table-column label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</el-card>
<!-- 添加用户代金券对话框 -->
<el-dialog
v-model="addDialogVisible"
title="为用户分发优惠券/优惠码"
width="600px"
>
<el-form
ref="addFormRef"
:model="addForm"
:rules="addRules"
label-width="140px"
>
<el-form-item label="优惠类型" prop="discount_type">
<el-radio-group v-model="addForm.discount_type" @change="handleDiscountTypeChange">
<el-radio value="coupon">代金券</el-radio>
<!-- <el-radio value="code">优惠码</el-radio> -->
</el-radio-group>
</el-form-item>
<el-form-item label="代金券" prop="voucher_id">
<el-select
v-model="addForm.voucher_id"
placeholder="请选择代金券"
:disabled="addForm.discount_type === 'code'"
filterable
clearable
style="width: 100%"
@change="handleVoucherChange"
>
<el-option
v-for="item in voucherOptions"
:key="item.id"
:label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<!-- <el-form-item label="优惠码" prop="code_id">
<el-select
v-model="addForm.code_id"
placeholder="请选择优惠码"
:disabled="addForm.discount_type === 'coupon'"
filterable
clearable
style="width: 100%"
@change="handleCodeChange"
>
<el-option
v-for="item in codeOptions"
:key="item.id"
:label="`${item.name} (${item.code})`"
:value="item.id"
/>
</el-select>
</el-form-item> -->
<el-divider />
<el-form-item label="分发对象" prop="target_type">
<el-radio-group v-model="addForm.target_type" @change="handleTargetTypeChange">
<el-radio value="user">指定用户</el-radio>
<el-radio value="group">用户组</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="用户" prop="user_id">
<el-select
v-model="addForm.user_id"
placeholder="请选择用户"
:disabled="addForm.target_type === 'group'"
filterable
clearable
remote
:remote-method="searchUsers"
:loading="userSearchLoading"
style="width: 100%"
@change="handleUserChange"
>
<el-option
v-for="item in userOptions"
:key="item.UserId"
:label="`${item.UserName} (ID: ${item.UserId})`"
:value="item.UserId"
/>
</el-select>
</el-form-item>
<el-form-item label="用户组" prop="group_id">
<el-select
v-model="addForm.group_id"
placeholder="请选择用户组"
:disabled="addForm.target_type === 'user'"
filterable
clearable
style="width: 100%"
@change="handleGroupChange"
>
<el-option
v-for="item in groupOptions"
:key="item.Id"
:label="item.Name"
:value="item.Id"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="addDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitAdd" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
<!-- 编辑用户代金券对话框 -->
<el-dialog
v-model="editDialogVisible"
title="编辑用户代金券"
width="600px"
>
<el-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-width="140px"
>
<el-form-item label="用户ID" prop="user_id">
<el-input-number v-model="editForm.user_id" :min="1" disabled style="width: 100%" />
</el-form-item>
<el-form-item label="代金券ID" prop="discount_id">
<el-input-number v-model="editForm.discount_id" :min="1" placeholder="请输入代金券ID" style="width: 100%" />
</el-form-item>
<el-form-item label="已使用次数" prop="use_times">
<el-input-number v-model="editForm.use_times" :min="0" placeholder="请输入已使用次数" style="width: 100%" />
</el-form-item>
<el-form-item label="最大使用次数" prop="max_use_times">
<el-input-number v-model="editForm.max_use_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
</el-form-item>
<el-form-item label="过期时间" prop="expire_at">
<el-date-picker
v-model="editForm.expire_at"
type="datetime"
placeholder="选择过期时间"
value-format="YYYY-MM-DD HH:mm:ss"
:teleported="true"
placement="top-start"
:editable="true"
:clearable="true"
style="width: 100%"
@keyup.enter="handleDatePickerEnter"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitEdit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
import {
getUserVoucherList,
addUserVoucher,
updateUserVoucher,
deleteUserVoucher,
getDiscountCodeList,
getVoucherHolderList,
allocateVoucher
} from '@/api/admin/discount'
import { getUserList, getUserGroupList } from '@/api/admin/user'
// 查询参数
const queryParams = reactive({
code_id: undefined,
page: 1,
count: 10
})
// 添加表单
const addForm = reactive({
discount_type: 'coupon', // 优惠类型:coupon-代金券, code-优惠码
voucher_id: undefined, // 代金券ID
code_id: undefined, // 优惠码ID
target_type: 'user', // 分发对象:user-指定用户, group-用户组
user_id: undefined, // 用户ID
group_id: undefined // 用户组ID
})
const addRules = {
discount_type: [
{ required: true, message: '请选择优惠类型', trigger: 'change' }
],
target_type: [
{ required: true, message: '请选择分发对象', trigger: 'change' }
]
}
// 下拉选项数据
const voucherListOptions = ref([]) // 用于搜索的代金券列表
const voucherOptions = ref([]) // 代金券选项
const codeOptions = ref([]) // 优惠码选项
const userOptions = ref([]) // 用户选项
const groupOptions = ref([]) // 用户组选项
const userSearchLoading = ref(false) // 用户搜索加载状态
const submitLoading = ref(false) // 提交加载状态
const dataList = ref([]) // 优惠列表
// 编辑表单
const editForm = reactive({
id: undefined,
user_id: undefined,
discount_id: undefined,
use_times: 0,
max_use_times: 0,
expire_at: ''
})
const editRules = {
discount_id: [
{ required: true, message: '请输入代金券ID', trigger: 'blur' }
],
use_times: [
{ required: true, message: '请输入已使用次数', trigger: 'blur' }
],
max_use_times: [
{ required: true, message: '请输入最大使用次数', trigger: 'blur' }
],
expire_at: [
{ required: true, message: '请选择过期时间', trigger: 'change' }
]
}
// 状态数据
const loading = ref(false)
const userVoucherList = ref([])
const total = ref(0)
const selectedRows = ref([])
const addDialogVisible = ref(false)
const editDialogVisible = ref(false)
const addFormRef = ref(null)
const editFormRef = ref(null)
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
try {
const date = new Date(dateStr)
// 检查日期是否有效且不是默认的1970年
if (isNaN(date.getTime()) || date.getFullYear() <= 1970) {
return '-'
}
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
} catch (error) {
console.error('日期格式化失败:', error)
return '-'
}
}
// 处理日期选择器回车事件
const handleDatePickerEnter = (event) => {
const datePicker = event.target.closest('.el-date-editor')
if (datePicker) {
event.target.blur()
}
}
// 获取代金券持有者列表
const fetchUserVoucherList = async () => {
if (!queryParams.code_id) {
ElMessage.warning('请先选择代金券')
return
}
loading.value = true
try {
const params = {
code_id: queryParams.code_id,
page: queryParams.page,
count: queryParams.count
}
const res = await getVoucherHolderList(params)
console.log('代金券持有者列表数据:', res.data)
if (res.data.code === 200) {
userVoucherList.value = res.data.data?.data || []
total.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取代金券持有者列表失败:', error)
ElMessage.error('获取代金券持有者列表失败')
} finally {
loading.value = false
}
}
// 查询
const handleQuery = () => {
queryParams.page = 1
fetchUserVoucherList()
}
// 重置查询
const resetQuery = () => {
queryParams.code_id = undefined
queryParams.page = 1
userVoucherList.value = []
total.value = 0
}
// 处理代金券选择
const handleVoucherSelect = (value) => {
if (value) {
queryParams.page = 1
fetchUserVoucherList()
} else {
userVoucherList.value = []
total.value = 0
}
}
// 选择项变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
// 分页
const handleSizeChange = (size) => {
queryParams.count = size
fetchUserVoucherList()
}
const handleCurrentChange = (page) => {
queryParams.page = page
fetchUserVoucherList()
}
// 获取代金券列表(用于搜索下拉框)
const fetchVoucherListOptions = async () => {
try {
const res = await getDiscountCodeList({
page: 1,
count: 1000,
discount_type: 'coupon'
})
console.log('获取代金券列表:', res.data)
if (res.data.code === 200) {
voucherListOptions.value = res.data.data?.data || []
}
} catch (error) {
console.error('获取代金券列表失败:', error)
ElMessage.error('获取代金券列表失败')
}
}
//获取优惠列表
const fetchDiscountList = async () => {
try {
const res = await getDiscountCodeList({
page: 1,
count: 100,
discount_type: 'coupon'
})
console.log('获取代金券列表:', res.data)
if (res.data.code === 200) {
voucherOptions.value = res.data.data?.data || []
dataList.value.push(...res.data.data?.data || [])
}
const res2 = await getDiscountCodeList({
page: 1,
count: 100,
discount_type: 'code'
})
console.log('获取优惠码列表:', res2.data)
if (res2.data.code === 200) {
codeOptions.value = res2.data.data?.data || []
dataList.value.push(...res2.data.data?.data || [])
}
console.log('获取优惠列表最终:', dataList.value)
} catch (error) {
console.error('获取代金券列表失败:', error)
}
}
// 获取代金券列表
const fetchVoucherOptions = async () => {
try {
const res = await getDiscountCodeList({
discount_type: 'coupon',
page: 1,
count: 100
})
if (res.data.code === 200) {
voucherOptions.value = res.data.data?.data || []
}
} catch (error) {
console.error('获取代金券列表失败:', error)
}
}
// 获取优惠码列表
const fetchCodeOptions = async () => {
try {
const res = await getDiscountCodeList({
discount_type: 'code',
page: 1,
count: 100
})
if (res.data.code === 200) {
codeOptions.value = res.data.data?.data || []
}
} catch (error) {
console.error('获取优惠码列表失败:', error)
}
}
//获取用户列表
const fetchUserList = async () => {
try {
const res = await getUserList({
page: 1,
count: 100,
key: ''
})
console.log('获取用户列表:', res.data)
if (res.data.code === 200) {
userOptions.value = res.data.data?.data || []
}
}
catch (error) {
console.error('获取用户列表失败:', error)
}
finally {
userSearchLoading.value = false
}
}
// 获取用户组列表
const fetchGroupOptions = async () => {
try {
const res = await getUserGroupList({
page: 1,
count: 100
})
if (res.data.code === 200) {
groupOptions.value = res.data.data?.data || []
}
} catch (error) {
console.error('获取用户组列表失败:', error)
}
}
// 处理优惠类型变化(代金券/优惠码互斥)
const handleDiscountTypeChange = (val) => {
if (val === 'coupon') {
addForm.code_id = undefined
} else if (val === 'code') {
addForm.voucher_id = undefined
}
}
// 处理代金券选择变化
const handleVoucherChange = (val) => {
if (val) {
addForm.code_id = undefined
addForm.discount_type = 'coupon'
}
}
// 处理优惠码选择变化
const handleCodeChange = (val) => {
if (val) {
addForm.voucher_id = undefined
addForm.discount_type = 'code'
}
}
// 处理分发对象类型变化(用户/用户组互斥)
const handleTargetTypeChange = (val) => {
if (val === 'user') {
addForm.group_id = undefined
} else if (val === 'group') {
addForm.user_id = undefined
}
}
// 处理用户选择变化
const handleUserChange = (val) => {
if (val) {
addForm.group_id = undefined
addForm.target_type = 'user'
}
}
// 处理用户组选择变化
const handleGroupChange = (val) => {
if (val) {
addForm.user_id = undefined
addForm.target_type = 'group'
}
}
// 添加用户代金券
const handleAdd = async () => {
addDialogVisible.value = true
// 重置表单
Object.assign(addForm, {
discount_type: 'coupon',
voucher_id: undefined,
code_id: undefined,
target_type: 'user',
user_id: undefined,
group_id: undefined
})
addFormRef.value?.resetFields()
// 加载下拉选项数据
await Promise.all([
fetchVoucherOptions(),
// fetchCodeOptions(),
fetchGroupOptions(),
fetchUserList()
])
}
// 编辑用户代金券
const handleEdit = (row) => {
editDialogVisible.value = true
// 处理过期时间 - 支持ISO字符串和时间戳
let expireAt = ''
if (row.expireAt) {
try {
// 如果是ISO字符串格式
if (typeof row.expireAt === 'string') {
const date = new Date(row.expireAt)
if (!isNaN(date.getTime()) && date.getFullYear() > 1970) {
expireAt = date.toISOString().slice(0, 19).replace('T', ' ')
}
}
// 如果是时间戳
else if (typeof row.expireAt === 'number' && row.expireAt > 0) {
expireAt = new Date(row.expireAt * 1000).toISOString().slice(0, 19).replace('T', ' ')
}
} catch (error) {
console.error('时间转换失败:', error)
}
}
Object.assign(editForm, {
id: row.Id || row.id,
user_id: row.UserId || row.userId,
discount_id: row.discountId,
use_times: row.useTimes || 0,
max_use_times: row.maxUseTimes || 0,
expire_at: expireAt
})
}
// 删除用户代金券
const handleDelete = (row) => {
ElMessageBox.confirm(`确认删除该用户代金券吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteUserVoucher({
user_id: String(row.UserId || row.userId),
id: String(row.Id || row.id)
})
console.log('删除响应:', res.data)
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchUserVoucherList()
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
// 批量删除
const handleBatchDelete = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请至少选择一条记录')
return
}
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
loading.value = true
try {
const deletePromises = selectedRows.value.map(row =>
deleteUserVoucher({
user_id: String(row.UserId || row.userId),
id: String(row.Id || row.id)
})
)
const results = await Promise.allSettled(deletePromises)
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.data?.code === 200).length
const failCount = results.length - successCount
if (failCount === 0) {
ElMessage.success(`批量删除成功,共删除 ${successCount} 条记录`)
} else if (successCount === 0) {
ElMessage.error(`批量删除失败,所有 ${failCount} 条记录删除失败`)
} else {
ElMessage.warning(`批量删除完成,成功 ${successCount} 条,失败 ${failCount}`)
}
fetchUserVoucherList()
} catch (error) {
console.error('批量删除失败:', error)
ElMessage.error('批量删除操作异常')
} finally {
loading.value = false
}
}).catch(() => {})
}
// 提交添加表单
const submitAdd = () => {
addFormRef.value?.validate(async (valid) => {
if (valid) {
// 验证是否选择了优惠券/优惠码
const discountId = addForm.discount_type === 'coupon' ? addForm.voucher_id : addForm.code_id
if (!discountId) {
ElMessage.warning('请选择代金券或优惠码')
return
}
// 验证是否选择了用户或用户组
const targetId = addForm.target_type === 'user' ? addForm.user_id : addForm.group_id
if (!targetId) {
ElMessage.warning('请选择用户或用户组')
return
}
submitLoading.value = true
try {
const submitData = {
code_id: String(discountId)
}
// 根据分发对象添加不同参数
if (addForm.target_type === 'user') {
submitData.user_id = String(addForm.user_id)
} else {
submitData.user_group_id = String(addForm.group_id)
}
console.log('分发优惠券/优惠码数据:', submitData)
const res = await allocateVoucher(submitData)
console.log('分发响应:', res.data)
if (res.data.code === 200) {
ElMessage.success('分发成功')
addDialogVisible.value = false
// 如果是为当前查询的用户分发,则刷新列表
//if (addForm.target_type === 'user' && queryParams.user_id) {
fetchUserVoucherList()
//}
}
} catch (error) {
console.error('分发失败:', error)
ElMessage.error(error.response?.data?.message || '分发失败')
} finally {
submitLoading.value = false
}
}
})
}
// 提交编辑表单
const submitEdit = () => {
editFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
user_id: Number(editForm.user_id),
id: Number(editForm.id),
discount_id: Number(editForm.discount_id),
use_times: Number(editForm.use_times),
max_use_times: Number(editForm.max_use_times),
expire_at: Math.floor(new Date(editForm.expire_at).getTime() / 1000)
}
console.log('更新用户代金券数据:', submitData)
const res = await updateUserVoucher(submitData)
console.log('更新响应:', res.data)
if (res.data.code === 200) {
ElMessage.success('更新成功')
editDialogVisible.value = false
fetchUserVoucherList()
}
} catch (error) {
console.error('更新失败:', error)
ElMessage.error(error.response?.data?.message || '更新失败')
}
}
})
}
// 初始化
onMounted(() => {
// 加载代金券列表供选择
fetchVoucherListOptions()
fetchDiscountList()
})
</script>
<style scoped>
.user-voucher-container {
padding: 0;
}
.filter-container {
margin-bottom: 20px;
border-radius: 8px;
}
.search-form {
margin-bottom: 15px;
}
.action-bar {
display: flex;
gap: 12px;
}
.table-container {
border-radius: 8px;
}
.user-info {
padding: 4px 0;
}
.username {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
}
.user-id {
font-size: 12px;
color: #999;
}
.amount {
color: #f56c6c;
font-weight: bold;
font-size: 14px;
}
.pagination {
margin-top: 24px;
justify-content: flex-end;
}
</style>
+512
View File
@@ -0,0 +1,512 @@
<template>
<div class="voucher-container">
<!-- 搜索和操作栏 -->
<el-card class="filter-container" shadow="never">
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增代金券
</el-button>
<el-button type="success" @click="fetchVoucherList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</el-card>
<!-- 代金券列表 -->
<el-card class="table-container" shadow="never">
<el-table
v-loading="loading"
:data="voucherList"
@selection-change="handleSelectionChange"
style="width: 100%"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="代金券名称" min-width="200" />
<el-table-column label="面额" width="120">
<template #default="{ row }">
<span class="amount">¥{{ (row.amount / 100).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="最低消费" width="120">
<template #default="{ row }">
¥{{ (row.minAmount / 100).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="最大抵扣" width="120">
<template #default="{ row }">
<span v-if="row.maxAmount">¥{{ (row.maxAmount / 100).toFixed(2) }}</span>
<span v-else>无限制</span>
</template>
</el-table-column>
<el-table-column prop="maxTimes" label="最大使用次数" width="130">
<template #default="{ row }">
{{ row.maxTimes || '无限制' }}
</template>
</el-table-column>
<el-table-column prop="userTimes" label="单用户次数" width="120">
<template #default="{ row }">
{{ row.userTimes || '无限制' }}
</template>
</el-table-column>
<el-table-column label="有效期(天)" width="100">
<template #default="{ row }">
{{ row.duration ? (row.duration / 86400).toFixed(0) : '-' }}
</template>
</el-table-column>
<el-table-column label="续费可用" width="100" align="center">
<template #default="{ row }">
<el-icon v-if="row.renew" color="#67c23a" :size="20"><SuccessFilled /></el-icon>
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="success" link @click="handleView(row)">查看</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</el-card>
<!-- 代金券表单对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增代金券' : '编辑代金券'"
width="700px"
>
<el-form
ref="voucherFormRef"
:model="voucherForm"
:rules="voucherRules"
label-width="140px"
>
<el-form-item label="代金券名称" prop="name">
<el-input v-model="voucherForm.name" placeholder="请输入代金券名称" />
</el-form-item>
<el-form-item label="备注" prop="note">
<el-input v-model="voucherForm.note" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="面额(元)" prop="amount">
<el-input-number v-model="voucherForm.amount" :min="0" :precision="2" :step="0.01" placeholder="请输入面额" style="width: 100%" />
</el-form-item>
<el-form-item label="最低消费(元)" prop="min_amount">
<el-input-number v-model="voucherForm.min_amount" :min="0" :precision="2" :step="0.01" placeholder="满多少可使用" style="width: 100%" />
</el-form-item>
<el-form-item label="最大抵扣(元)" prop="max_amount">
<el-input-number v-model="voucherForm.max_amount" :min="0" :precision="2" :step="0.01" placeholder="0表示无限制" style="width: 100%" />
</el-form-item>
<el-form-item label="最大使用次数" prop="max_times">
<el-input-number v-model="voucherForm.max_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
</el-form-item>
<el-form-item label="单用户最大次数" prop="user_times">
<el-input-number v-model="voucherForm.user_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
</el-form-item>
<el-form-item label="有效期(天)" prop="duration_days">
<el-input-number v-model="voucherForm.duration_days" :min="1" placeholder="代金券有效天数" style="width: 100%" />
<div class="form-tip">代金券领取后的有效持续时间</div>
</el-form-item>
<el-form-item label="发放时间范围" prop="timeRange">
<el-date-picker
v-model="voucherForm.timeRange"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
:teleported="true"
popper-class="voucher-date-picker"
placement="top-start"
:editable="true"
:clearable="true"
style="width: 100%"
@keyup.enter="handleDatePickerEnter"
/>
<div class="form-tip">代金券可以发放给用户的时间范围</div>
</el-form-item>
<el-form-item label="续费可用" prop="renew">
<el-switch v-model="voucherForm.renew" active-text="是" inactive-text="否" />
</el-form-item>
<el-form-item label="同类型可叠加" prop="can_stacking">
<el-switch v-model="voucherForm.can_stacking" active-text="是" inactive-text="否" />
</el-form-item>
<el-form-item label="其他类型可叠加" prop="can_combine">
<el-switch v-model="voucherForm.can_combine" active-text="是" inactive-text="否" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
<!-- 详情查看对话框 -->
<DiscountDetailDialog
v-model="detailDialogVisible"
type="coupon"
:detail-data="currentDetail"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
import {
getDiscountCodeList,
getDiscountCodeDetail,
createDiscountCode,
updateDiscountCode,
deleteDiscountCode
} from '@/api/admin/discount'
import { timeToTimestamp } from '@/utils/tool'
import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue'
// 查询参数
const queryParams = reactive({
discount_type: 'coupon', // 固定为coupon表示代金券
page: 1,
count: 10
})
// 代金券表单
const voucherForm = reactive({
code_id: undefined,
discount_type: 'coupon', // 固定为coupon
name: '',
note: '',
amount: 0,
min_amount: 0,
max_amount: 0,
max_times: 0,
user_times: 0,
duration_days: 30, // 默认30天
timeRange: [],
renew: false,
can_stacking: false,
can_combine: false
})
const voucherRules = {
name: [
{ required: true, message: '请输入代金券名称', trigger: 'blur' }
],
amount: [
{ required: true, message: '请输入面额', trigger: 'blur' }
],
duration_days: [
{ required: true, message: '请输入有效期天数', trigger: 'blur' }
]
}
// 状态数据
const loading = ref(false)
const voucherList = ref([])
const total = ref(0)
const selectedRows = ref([])
const dialogVisible = ref(false)
const dialogType = ref('add')
const voucherFormRef = ref(null)
const detailDialogVisible = ref(false)
const currentDetail = ref(null)
// 获取代金券列表
const fetchVoucherList = async () => {
loading.value = true
try {
const res = await getDiscountCodeList(queryParams)
console.log('代金券列表数据:', res.data)
if (res.data.code === 200) {
voucherList.value = res.data.data?.data || []
total.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取代金券列表失败:', error)
ElMessage.error('获取代金券列表失败')
} finally {
loading.value = false
}
}
// 选择项变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
// 分页
const handleSizeChange = (size) => {
queryParams.count = size
fetchVoucherList()
}
const handleCurrentChange = (page) => {
queryParams.page = page
fetchVoucherList()
}
// 新增代金券
const handleAdd = () => {
dialogType.value = 'add'
dialogVisible.value = true
Object.assign(voucherForm, {
code_id: undefined,
discount_type: 'coupon',
name: '',
note: '',
amount: 0,
min_amount: 0,
max_amount: 0,
max_times: 0,
user_times: 0,
duration_days: 30,
timeRange: [],
renew: false,
can_stacking: false,
can_combine: false
})
voucherFormRef.value?.resetFields()
}
// 编辑代金券
const handleEdit = (row) => {
dialogType.value = 'edit'
dialogVisible.value = true
// 转换日期字符串为日期选择器格式
const startTime = row.startTime ? new Date(row.startTime).toLocaleString('zh-CN', {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'}).replace(/\//g, '-') : ''
const endTime = row.endTime ? new Date(row.endTime).toLocaleString('zh-CN', {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'}).replace(/\//g, '-') : ''
Object.assign(voucherForm, {
code_id: row.id,
discount_type: 'coupon',
name: row.name,
note: row.note || '',
amount: row.amount ? row.amount / 100 : 0,
min_amount: row.minAmount ? row.minAmount / 100 : 0,
max_amount: row.maxAmount ? row.maxAmount / 100 : 0,
max_times: row.maxTimes || 0,
user_times: row.userTimes || 0,
duration_days: row.duration ? row.duration / 86400 : 30, // 秒转天
timeRange: startTime && endTime ? [startTime, endTime] : [],
renew: row.renew || false,
can_stacking: row.canStacking || false,
can_combine: row.canCombine || false
})
}
// 查看代金券详情
const handleView = async (row) => {
try {
const res = await getDiscountCodeDetail({ code_id: row.id })
console.log('代金券详情:', res.data)
if (res.data.code === 200) {
currentDetail.value = res.data.data
detailDialogVisible.value = true
}
} catch (error) {
console.error('获取代金券详情失败:', error)
ElMessage.error('获取代金券详情失败')
}
}
// 删除代金券
const handleDelete = (row) => {
ElMessageBox.confirm(`确认删除代金券 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteDiscountCode({ code_id: row.id })
console.log('删除响应:', res.data)
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchVoucherList()
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
// 批量删除
const handleBatchDelete = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请至少选择一条记录')
return
}
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
loading.value = true
try {
const deletePromises = selectedRows.value.map(row =>
deleteDiscountCode({ code_id: row.id })
)
const results = await Promise.allSettled(deletePromises)
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.data?.code === 200).length
const failCount = results.length - successCount
if (failCount === 0) {
ElMessage.success(`批量删除成功,共删除 ${successCount} 条记录`)
} else if (successCount === 0) {
ElMessage.error(`批量删除失败,所有 ${failCount} 条记录删除失败`)
} else {
ElMessage.warning(`批量删除完成,成功 ${successCount} 条,失败 ${failCount}`)
}
fetchVoucherList()
} catch (error) {
console.error('批量删除失败:', error)
ElMessage.error('批量删除操作异常')
} finally {
loading.value = false
}
}).catch(() => {})
}
// 处理日期选择器回车事件
const handleDatePickerEnter = (event) => {
// 回车键确认日期选择
const datePicker = event.target.closest('.el-date-editor')
if (datePicker) {
// 触发失焦事件,确认日期选择
event.target.blur()
}
}
// 提交代金券表单
const submitForm = () => {
voucherFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
discount_type: 'coupon',
name: voucherForm.name,
note: voucherForm.note,
amount: Math.round(voucherForm.amount * 100),
percentage: 0, // 代金券固定为0
min_amount: Math.round(voucherForm.min_amount * 100),
max_amount: Math.round(voucherForm.max_amount * 100),
max_times: voucherForm.max_times || 0,
user_times: voucherForm.user_times || 0,
duration: voucherForm.duration_days * 86400, // 天转秒
renew: voucherForm.renew,
can_stacking: voucherForm.can_stacking,
can_combine: voucherForm.can_combine
}
// 处理时间(转换为秒级时间戳)
if (voucherForm.timeRange && voucherForm.timeRange.length === 2) {
submitData.start_time = timeToTimestamp(voucherForm.timeRange[0])
submitData.end_time = timeToTimestamp(voucherForm.timeRange[1])
} else {
submitData.start_time = ''
submitData.end_time = ''
}
// 如果是编辑,添加code_id
if (dialogType.value === 'edit') {
submitData.code_id = voucherForm.code_id
}
console.log('提交代金券数据:', submitData)
let res
if (dialogType.value === 'add') {
res = await createDiscountCode(submitData)
} else {
res = await updateDiscountCode(submitData)
}
console.log('提交响应:', res.data)
if (res.data.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
dialogVisible.value = false
fetchVoucherList()
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
// 初始化
onMounted(() => {
fetchVoucherList()
})
</script>
<style scoped>
.voucher-container {
padding: 0;
}
.filter-container {
margin-bottom: 20px;
border-radius: 8px;
}
.action-bar {
display: flex;
gap: 12px;
}
.table-container {
border-radius: 8px;
}
.amount {
color: #f56c6c;
font-weight: bold;
font-size: 14px;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.pagination {
margin-top: 24px;
justify-content: flex-end;
}
</style>
<style>
/* 时间选择器弹出层样式 - 非 scoped */
.voucher-date-picker {
z-index: 9999 !important;
}
.voucher-date-picker .el-picker-panel {
max-width: 90vw;
}
</style>
+581
View File
@@ -0,0 +1,581 @@
<template>
<div class="voucher-history-container">
<!-- 搜索和操作栏 -->
<el-card class="filter-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="用户">
<div class="user_selector-inline">
<el-tag v-if="queryParams.user_id" type="primary" closable @close="clearQueryUser" style="margin-right: 8px;">
{{ getQueryUserName() }}
</el-tag>
<el-button type="primary" plain @click="openQueryUserSelector" size="default">
<el-icon><User /></el-icon>
{{ queryParams.user_id ? '重新选择' : '选择用户' }}
</el-button>
</div>
</el-form-item>
<el-form-item label="关联信息ID">
<el-input v-model="queryParams.id" placeholder="请输入关联信息ID" clearable style="width: 180px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>查询
</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="success" @click="fetchHistoryList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</el-card>
<!-- 使用记录列表 -->
<el-card class="table-container" shadow="never">
<el-table
v-loading="loading"
:data="historyList"
style="width: 100%"
border
stripe
>
<el-table-column prop="id" label="记录ID" width="80" fixed="left" />
<el-table-column prop="user_id" label="用户ID" width="100" />
<el-table-column prop="username" label="用户名" width="150" />
<el-table-column prop="email" label="邮箱" min-width="200" />
<el-table-column prop="discount_id" label="代金券ID" width="120" />
<el-table-column prop="discount_name" label="代金券名称" min-width="180" />
<el-table-column label="优惠金额" width="120">
<template #default="{ row }">
<span class="amount">¥{{ row.discount_amount ? (row.discount_amount / 100).toFixed(2) : '0.00' }}</span>
</template>
</el-table-column>
<el-table-column label="订单金额" width="120">
<template #default="{ row }">
<span>¥{{ row.order_amount ? (row.order_amount / 100).toFixed(2) : '0.00' }}</span>
</template>
</el-table-column>
<el-table-column prop="order_id" label="订单ID" width="150" />
<el-table-column label="使用状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="使用时间" width="180">
<template #default="{ row }">
{{ formatDate(row.used_at) }}
</template>
</el-table-column>
<el-table-column label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.created_at) }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleView(row)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</el-card>
<!-- 详情对话框 -->
<el-dialog
v-model="detailDialogVisible"
title="使用记录详情"
width="800px"
>
<el-descriptions :column="2" border>
<el-descriptions-item label="记录ID">{{ currentDetail.id }}</el-descriptions-item>
<el-descriptions-item label="用户ID">{{ currentDetail.user_id }}</el-descriptions-item>
<el-descriptions-item label="用户名">{{ currentDetail.username }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ currentDetail.email }}</el-descriptions-item>
<el-descriptions-item label="代金券ID">{{ currentDetail.discount_id }}</el-descriptions-item>
<el-descriptions-item label="代金券名称">{{ currentDetail.discount_name }}</el-descriptions-item>
<el-descriptions-item label="优惠金额">
<span class="amount">¥{{ currentDetail.discount_amount ? (currentDetail.discount_amount / 100).toFixed(2) : '0.00' }}</span>
</el-descriptions-item>
<el-descriptions-item label="订单金额">
<span>¥{{ currentDetail.order_amount ? (currentDetail.order_amount / 100).toFixed(2) : '0.00' }}</span>
</el-descriptions-item>
<el-descriptions-item label="订单ID" :span="2">{{ currentDetail.order_id || '-' }}</el-descriptions-item>
<el-descriptions-item label="使用状态">
<el-tag :type="getStatusType(currentDetail.status)">
{{ getStatusText(currentDetail.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="使用时间">{{ formatDate(currentDetail.used_at) }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ formatDate(currentDetail.created_at) }}</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">{{ formatDate(currentDetail.updated_at) }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ currentDetail.remark || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button type="primary" @click="detailDialogVisible = false">确定</el-button>
</template>
</el-dialog>
<!-- 用户选择弹窗 -->
<el-dialog
v-model="userSelectorVisible"
title="选择用户"
width="800px"
class="user-selector-dialog"
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="userSearchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="searchUsers"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="searchUsers">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="resetUserSearch">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="userSelectorLoading"
:data="userSelectorList"
highlight-current-row
@current-change="handleUserSelectChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
{{ row.Status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="userSearchParams.page"
v-model:page-size="userSearchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="userSelectorTotal"
@size-change="handleUserSelectorSizeChange"
@current-change="handleUserSelectorPageChange"
background
class="selector-pagination"
/>
<template #footer>
<el-button @click="userSelectorVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
确定选择
</el-button>
</template>
</el-dialog>
<!-- 统计卡片 -->
<el-row :gutter="20" style="margin-top: 20px">
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="总使用次数" :value="statistics.totalCount">
<template #suffix></template>
</el-statistic>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="总优惠金额" :value="(statistics.totalAmount / 100).toFixed(2)">
<template #prefix>¥</template>
</el-statistic>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="成功使用" :value="statistics.successCount">
<template #suffix></template>
</el-statistic>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="失败/取消" :value="statistics.failedCount">
<template #suffix></template>
</el-statistic>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, Refresh, Download } from '@element-plus/icons-vue'
import { getUserVoucherHistory, getDiscountCodeList } from '@/api/admin/discount'
import { getUserList } from '@/api/admin/user'
// 查询参数
const queryParams = reactive({
user_id: undefined,
id: '',
page: 1,
count: 10
})
// 状态数据
const loading = ref(false)
const historyList = ref([])
const total = ref(0)
const detailDialogVisible = ref(false)
const currentDetail = ref({})
// const userOptions = ref([])
const discountOptions = ref([])
const selectorType = ref('query')
const userSelectorVisible = ref(false)
const userSelectorList = ref([])
const userSelectorTotal = ref(0)
const userSearchParams = reactive({
key: '',
page: 1,
count: 10
})
const selectedUserTemp = ref(null)
const userSelectorLoading = ref(false)
const UserOptions = ref([])
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
0: '未使用',
1: '已使用',
2: '使用失败',
3: '已取消'
}
return statusMap[status] || '未知'
}
// 获取状态标签类型
const getStatusType = (status) => {
const typeMap = {
0: 'info',
1: 'success',
2: 'danger',
3: 'warning'
}
return typeMap[status] || 'info'
}
// 统计数据
const statistics = computed(() => {
const stats = {
totalCount: historyList.value.length,
totalAmount: 0,
successCount: 0,
failedCount: 0
}
historyList.value.forEach(item => {
if (item.status === 1) {
stats.successCount++
stats.totalAmount += item.discount_amount || 0
} else if (item.status === 2 || item.status === 3) {
stats.failedCount++
}
})
return stats
})
// 获取查询用户名称
const getQueryUserName = () => {
const user = UserOptions.value.find(u => u.UserId === queryParams.user_id)
return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${queryParams.user_id}`
}
// 获取使用记录列表
const fetchHistoryList = async () => {
loading.value = true
try {
const params = { ...queryParams }
// 清除空参数
Object.keys(params).forEach(key => {
if (params[key] === '' || params[key] === null || params[key] === undefined) {
delete params[key]
}
})
const res = await getUserVoucherHistory(params)
console.log('使用记录数据:', res.data)
if (res.data.code === 200) {
historyList.value = res.data.data?.list || []
total.value = res.data.data?.all_count || 0
} else {
ElMessage.error(res.data.message || '获取使用记录失败')
}
} catch (error) {
console.error('获取使用记录失败:', error)
ElMessage.error('获取使用记录失败')
} finally {
loading.value = false
}
}
// 清除查询用户
const clearQueryUser = () => {
queryParams.user_id = undefined
}
// 重置用户搜索
const resetUserSearch = () => {
userSearchParams.key = ''
userSearchParams.page = 1
// fetchUserSelectorList()
}
// 确认用户选择
const confirmUserSelection = () => {
if (!selectedUserTemp.value) {
ElMessage.warning('请选择一个用户')
return
}
if (selectorType.value === 'query') {
// 查询表单选择
queryParams.user_id = selectedUserTemp.value.UserId
} else {
// 编辑表单选择
editForm.user_id = selectedUserTemp.value.UserId
}
// 将选中的用户添加到 UserOptions 中(如果不存在)
if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
UserOptions.value.push(selectedUserTemp.value)
}
userSelectorVisible.value = false
ElMessage.success('用户选择成功')
}
// 打开查询用户选择器
const openQueryUserSelector = () => {
selectorType.value = 'query'
userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 打开编辑用户选择器
const openEditUserSelector = () => {
selectorType.value = 'edit'
userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 用户选择变化
const handleUserSelectChange = (row) => {
selectedUserTemp.value = row
}
// 搜索用户
const searchUsers = () => {
userSearchParams.page = 1
fetchUserSelectorList()
}
// 用户选择器分页
const handleUserSelectorSizeChange = (size) => {
userSearchParams.count = size
fetchUserSelectorList()
}
const handleUserSelectorPageChange = (page) => {
userSearchParams.page = page
fetchUserSelectorList()
}
// 获取用户选择器列表
const fetchUserSelectorList = async () => {
userSelectorLoading.value = true
try {
const res = await getUserList(userSearchParams)
console.log('用户选择器列表:', res.data)
if (res.data.code === 200) {
userSelectorList.value = res.data.data?.data || []
userSelectorTotal.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
userSelectorLoading.value = false
}
}
// 查询
const handleQuery = () => {
queryParams.page = 1
fetchHistoryList()
}
// 重置查询
const resetQuery = () => {
queryParams.user_id = undefined
queryParams.discount_id = undefined
queryParams.id = ''
queryParams.page = 1
fetchHistoryList()
}
// 分页
const handleSizeChange = (size) => {
queryParams.count = size
fetchHistoryList()
}
const handleCurrentChange = (page) => {
queryParams.page = page
fetchHistoryList()
}
// 查看详情
const handleView = (row) => {
currentDetail.value = { ...row }
detailDialogVisible.value = true
}
// 导出
const handleExport = () => {
if (historyList.value.length === 0) {
ElMessage.warning('暂无数据可导出')
return
}
ElMessage.info('导出功能开发中...')
}
// 获取用户列表
const fetchUserList = async () => {
try {
const res = await getUserList({
page: 1,
count: 10000,
key: ''
})
UserOptions.value = res.data.data?.data || []
} catch (error) {
console.error('获取用户列表失败:', error)
}
}
// 获取代金券列表
const fetchDiscountList = async () => {
try {
const res = await getDiscountCodeList({
discount_type: 'coupon',
page: 1,
count: 1000
})
if (res.data.code === 200) {
discountOptions.value = res.data.data?.data || []
}
} catch (error) {
console.error('获取代金券列表失败:', error)
}
}
// 初始化
onMounted(() => {
fetchUserList()
fetchDiscountList()
// 默认加载第一页数据
// fetchHistoryList()
})
</script>
<style scoped>
.voucher-history-container {
padding: 0;
}
.filter-container {
margin-bottom: 20px;
border-radius: 8px;
}
.search-form {
margin-bottom: 15px;
}
.action-bar {
display: flex;
gap: 12px;
}
.table-container {
border-radius: 8px;
}
.amount {
color: #f56c6c;
font-weight: bold;
font-size: 14px;
}
.pagination {
margin-top: 24px;
justify-content: flex-end;
}
:deep(.el-statistic__head) {
color: #606266;
font-size: 14px;
}
:deep(.el-statistic__number) {
font-size: 24px;
font-weight: bold;
color: #409eff;
}
</style>
+781
View File
@@ -0,0 +1,781 @@
<template>
<div class="voucher-holders-container">
<!-- 搜索和操作栏 -->
<el-card class="filter-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="用户">
<div class="user-selector-inline">
<el-tag v-if="queryParams.user_id" type="primary" closable @close="clearQueryUser" style="margin-right: 8px">
{{ getQueryUserName() }}
</el-tag>
<el-button type="primary" plain @click="openQueryUserSelector" size="default">
<el-icon><User /></el-icon>
{{ queryParams.user_id ? '重新选择' : '选择用户' }}
</el-button>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>查询
</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>添加代金券
</el-button>
<el-button type="success" @click="fetchHoldersList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</el-card>
<!-- 拥有者列表 -->
<el-card class="table-container" shadow="never">
<el-table
v-loading="loading"
:data="holdersList"
style="width: 100%"
>
<el-table-column prop="Id" label="ID" width="80" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column label="代金券ID" width="120">
<template #default="{ row }">
{{ row.discountId || '-' }}
</template>
</el-table-column>
<el-table-column label="代金券名称" min-width="180">
<template #default="{ row }">
{{ row.discount?.name || '-' }}
</template>
</el-table-column>
<el-table-column label="代金券编码" width="150">
<template #default="{ row }">
{{ row.discount?.code || '-' }}
</template>
</el-table-column>
<el-table-column label="面额" width="120">
<template #default="{ row }">
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
</template>
</el-table-column>
<el-table-column prop="useTimes" label="已使用次数" width="120" />
<el-table-column prop="maxUseTimes" label="最大使用次数" width="120" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row)">
{{ getStatusText(row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="过期时间" width="180">
<template #default="{ row }">
{{ formatDate(row.expireAt) }}
</template>
</el-table-column>
<el-table-column label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleView(row)">查看详情</el-button>
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</el-card>
<!-- 详情对话框 -->
<el-dialog
v-model="detailDialogVisible"
title="拥有者详情"
width="700px"
>
<el-descriptions :column="2" border>
<el-descriptions-item label="记录ID">{{ currentDetail.Id }}</el-descriptions-item>
<el-descriptions-item label="用户ID">{{ currentDetail.UserId }}</el-descriptions-item>
<el-descriptions-item label="代金券ID">{{ currentDetail.discountId }}</el-descriptions-item>
<el-descriptions-item label="代金券编码">{{ currentDetail.discount?.code || '-' }}</el-descriptions-item>
<el-descriptions-item label="代金券名称" :span="2">{{ currentDetail.discount?.name || '-' }}</el-descriptions-item>
<el-descriptions-item label="面额">
<span class="amount">¥{{ currentDetail.discount?.amount ? (currentDetail.discount.amount / 100).toFixed(2) : '0.00' }}</span>
</el-descriptions-item>
<el-descriptions-item label="最低消费">
¥{{ currentDetail.discount?.minAmount ? (currentDetail.discount.minAmount / 100).toFixed(2) : '0.00' }}
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusType(currentDetail)">
{{ getStatusText(currentDetail) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="类型">
{{ currentDetail.discount?.type === 'coupon' ? '代金券' : '优惠码' }}
</el-descriptions-item>
<el-descriptions-item label="已使用次数">{{ currentDetail.useTimes || 0 }}</el-descriptions-item>
<el-descriptions-item label="最大使用次数">{{ currentDetail.maxUseTimes || '无限制' }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ formatDate(currentDetail.CreatedAt) }}</el-descriptions-item>
<el-descriptions-item label="过期时间" :span="2">{{ formatDate(currentDetail.expireAt) }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ currentDetail.discount?.note || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button type="primary" @click="detailDialogVisible = false">确定</el-button>
</template>
</el-dialog>
<!-- 添加/编辑代金券对话框 -->
<el-dialog
v-model="editDialogVisible"
:title="dialogType === 'add' ? '添加代金券' : '编辑代金券'"
width="600px"
>
<el-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-width="120px"
>
<el-form-item label="用户" prop="user_id" v-if="dialogType === 'add'">
<div class="user-selector-wrapper">
<div class="selected-user-display" v-if="editForm.user_id">
<el-tag type="primary" closable @close="clearEditUser">
{{ getEditUserName() }}
</el-tag>
</div>
<el-button
type="primary"
plain
@click="openEditUserSelector"
style="width: 100%"
>
<el-icon><User /></el-icon>
{{ editForm.user_id ? '重新选择用户' : '选择用户' }}
</el-button>
</div>
</el-form-item>
<el-form-item label="代金券" prop="discount_id">
<el-select v-model="editForm.discount_id" placeholder="请选择代金券" filterable style="width: 100%" >
<el-option v-for="item in discountOptions" :key="item.id" :label="`${item.name} (¥${(item.amount/100).toFixed(2)})`" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="已使用次数" prop="use_times" v-if="dialogType === 'edit'">
<el-input-number v-model="editForm.use_times" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="最大使用次数" prop="max_use_times" v-if="dialogType === 'edit'">
<el-input-number v-model="editForm.max_use_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
</el-form-item>
<el-form-item label="过期时间" prop="expire_at" v-if="dialogType === 'edit'">
<el-date-picker
v-model="editForm.expire_at"
type="datetime"
placeholder="选择过期时间"
format="YYYY-MM-DD HH:mm:ss"
:teleported="true"
placement="top-start"
:editable="true"
:clearable="true"
value-format="X"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitEditForm">确定</el-button>
</template>
</el-dialog>
<!-- 用户选择弹窗 -->
<el-dialog
v-model="userSelectorVisible"
title="选择用户"
width="800px"
class="user-selector-dialog"
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="userSearchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="searchUsers"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="searchUsers">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="resetUserSearch">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="userSelectorLoading"
:data="userSelectorList"
highlight-current-row
@current-change="handleUserSelectChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
{{ row.Status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="userSearchParams.page"
v-model:page-size="userSearchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="userSelectorTotal"
@size-change="handleUserSelectorSizeChange"
@current-change="handleUserSelectorPageChange"
background
class="selector-pagination"
/>
<template #footer>
<el-button @click="userSelectorVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
确定选择
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Download, Plus, User } from '@element-plus/icons-vue'
import {
getUserVoucherList,
allocateVoucher,
updateUserVoucher,
deleteUserVoucher,
getDiscountCodeList
} from '@/api/admin/discount'
import { getUserList } from '@/api/admin/user'
// 查询参数
const queryParams = reactive({
user_id: undefined,
page: 1,
count: 10
})
// 状态数据
const loading = ref(false)
const holdersList = ref([])
const total = ref(0)
const detailDialogVisible = ref(false)
const currentDetail = ref({})
const UserOptions = ref([])
const editDialogVisible = ref(false)
const dialogType = ref('add')
const editFormRef = ref(null)
const discountOptions = ref([])
// 用户选择弹窗相关
const userSelectorVisible = ref(false)
const userSelectorLoading = ref(false)
const userSelectorList = ref([])
const userSelectorTotal = ref(0)
const selectedUserTemp = ref(null) // 临时存储选中的用户
const selectorType = ref('query') // 'query' 或 'edit' 用于区分是查询还是编辑
const userSearchParams = reactive({
key: '',
page: 1,
count: 10
})
// 编辑表单
const editForm = reactive({
user_id: undefined,
discount_id: undefined,
id: undefined,
use_times: 0,
max_use_times: 0,
expire_at: undefined
})
// 表单验证规则
const editRules = {
user_id: [{ required: true, message: '请选择用户', trigger: 'change' }],
discount_id: [{ required: true, message: '请选择代金券', trigger: 'change' }]
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr || dateStr === '1970-01-01T08:01:40+08:00' || dateStr === '0001-01-01T00:00:00Z') return '-'
const date = new Date(dateStr)
if (isNaN(date.getTime())) return '-'
const year = date.getFullYear()
// 过滤掉1970年的日期(通常是零值)
if (year === 1970) return '-'
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
// 获取状态文本
const getStatusText = (row) => {
const now = Date.now()
const expireAt = row.expireAt ? new Date(row.expireAt).getTime() : 0
// 检查是否已用完
if (row.useTimes >= row.maxUseTimes && row.maxUseTimes > 0) {
return '已用完'
}
// 检查是否已过期(排除1970年的零值日期)
if (expireAt > 0 && expireAt < now && new Date(row.expireAt).getFullYear() !== 1970) {
return '已过期'
}
return '可使用'
}
// 获取状态标签类型
const getStatusType = (row) => {
const now = Date.now()
const expireAt = row.expireAt ? new Date(row.expireAt).getTime() : 0
if (row.useTimes >= row.maxUseTimes && row.maxUseTimes > 0) {
return 'info'
}
if (expireAt > 0 && expireAt < now && new Date(row.expireAt).getFullYear() !== 1970) {
return 'warning'
}
return 'success'
}
// 获取拥有者列表
const fetchHoldersList = async () => {
if (!queryParams.user_id) {
ElMessage.warning('请选择用户ID进行查询')
return
}
loading.value = true
try {
const params = { ...queryParams }
// 清除空参数
Object.keys(params).forEach(key => {
if (params[key] === '' || params[key] === null || params[key] === undefined) {
delete params[key]
}
})
const res = await getUserVoucherList(params)
console.log('拥有者列表数据:', res.data)
if (res.data.code === 200) {
holdersList.value = res.data.data?.data || []
total.value = res.data.data?.all_count || 0
console.log('解析后的列表数据:', holdersList.value)
}
} catch (error) {
console.error('获取拥有者列表失败:', error)
ElMessage.error('获取拥有者列表失败')
} finally {
loading.value = false
}
}
// 查询
const handleQuery = () => {
queryParams.page = 1
fetchHoldersList()
}
// 重置查询
const resetQuery = () => {
queryParams.code_id = ''
queryParams.username = ''
queryParams.page = 1
holdersList.value = []
total.value = 0
}
// 分页
const handleSizeChange = (size) => {
queryParams.count = size
fetchHoldersList()
}
const handleCurrentChange = (page) => {
queryParams.page = page
fetchHoldersList()
}
// 查看详情
const handleView = (row) => {
currentDetail.value = { ...row }
detailDialogVisible.value = true
}
// 导出
const handleExport = () => {
ElMessage.info('导出功能开发中...')
}
// 获取用户列表
const fetchUserList = async () => {
const res = await getUserList({
page: 1,
count: 10000,
key: ''
})
UserOptions.value = res.data.data?.data || []
}
// 打开查询用户选择器
const openQueryUserSelector = () => {
selectorType.value = 'query'
userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 打开编辑用户选择器
const openEditUserSelector = () => {
selectorType.value = 'edit'
userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 获取用户选择器列表
const fetchUserSelectorList = async () => {
userSelectorLoading.value = true
try {
const res = await getUserList(userSearchParams)
console.log('用户选择器列表:', res.data)
if (res.data.code === 200) {
userSelectorList.value = res.data.data?.data || []
userSelectorTotal.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
userSelectorLoading.value = false
}
}
// 搜索用户
const searchUsers = () => {
userSearchParams.page = 1
fetchUserSelectorList()
}
// 重置用户搜索
const resetUserSearch = () => {
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 用户选择变化
const handleUserSelectChange = (row) => {
selectedUserTemp.value = row
}
// 用户选择器分页
const handleUserSelectorSizeChange = (size) => {
userSearchParams.count = size
fetchUserSelectorList()
}
const handleUserSelectorPageChange = (page) => {
userSearchParams.page = page
fetchUserSelectorList()
}
// 确认用户选择
const confirmUserSelection = () => {
if (!selectedUserTemp.value) {
ElMessage.warning('请选择一个用户')
return
}
if (selectorType.value === 'query') {
// 查询表单选择
queryParams.user_id = selectedUserTemp.value.UserId
} else {
// 编辑表单选择
editForm.user_id = selectedUserTemp.value.UserId
}
// 将选中的用户添加到 UserOptions 中(如果不存在)
if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
UserOptions.value.push(selectedUserTemp.value)
}
userSelectorVisible.value = false
ElMessage.success('用户选择成功')
}
// 清除查询用户
const clearQueryUser = () => {
queryParams.user_id = undefined
}
// 清除编辑用户
const clearEditUser = () => {
editForm.user_id = undefined
}
// 获取查询用户名称
const getQueryUserName = () => {
const user = UserOptions.value.find(u => u.UserId === queryParams.user_id)
return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${queryParams.user_id}`
}
// 获取编辑用户名称
const getEditUserName = () => {
const user = UserOptions.value.find(u => u.UserId === editForm.user_id)
return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${editForm.user_id}`
}
// 获取代金券列表
const fetchDiscountList = async () => {
try {
const res = await getDiscountCodeList({
discount_type: 'coupon',
page: 1,
count: 1000
})
if (res.data.code === 200) {
discountOptions.value = res.data.data?.data || []
}
} catch (error) {
console.error('获取代金券列表失败:', error)
}
}
// 添加代金券
const handleAdd = () => {
dialogType.value = 'add'
editDialogVisible.value = true
Object.assign(editForm, {
user_id: queryParams.user_id,
discount_id: undefined,
id: undefined,
use_times: 0,
max_use_times: 0,
expire_at: undefined
})
editFormRef.value?.resetFields()
}
// 编辑
const handleEdit = (row) => {
dialogType.value = 'edit'
editDialogVisible.value = true
// 处理过期时间
let expireTime = undefined
if (row.expireAt && row.expireAt !== '1970-01-01T08:01:40+08:00' && row.expireAt !== '0001-01-01T00:00:00Z') {
const date = new Date(row.expireAt)
if (!isNaN(date.getTime()) && date.getFullYear() !== 1970) {
expireTime = Math.floor(date.getTime() / 1000)
}
}
Object.assign(editForm, {
user_id: row.UserId,
discount_id: row.discountId,
id: row.Id,
use_times: row.useTimes || 0,
max_use_times: row.maxUseTimes || 0,
expire_at: expireTime
})
}
// 删除
const handleDelete = (row) => {
const discountName = row.discount?.name || '该代金券'
ElMessageBox.confirm(`确认删除用户ID ${row.UserId} 的代金券 ${discountName} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteUserVoucher({
user_id: row.UserId,
id: row.Id
})
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchHoldersList()
} else {
ElMessage.error(res.data.message || '删除失败')
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
// 提交表单
const submitEditForm = () => {
editFormRef.value?.validate(async (valid) => {
if (valid) {
try {
let res
if (dialogType.value === 'add') {
// 使用 allocateVoucher 接口为用户添加代金券
res = await allocateVoucher({
user_id: editForm.user_id,
code_id: editForm.discount_id
})
} else {
res = await updateUserVoucher({
user_id: editForm.user_id,
id: editForm.id,
discount_id: editForm.discount_id,
use_times: editForm.use_times,
max_use_times: editForm.max_use_times,
expire_at: editForm.expire_at
})
}
if (res.data.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '修改成功')
editDialogVisible.value = false
fetchHoldersList()
} else {
ElMessage.error(res.data.message || '操作失败')
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
// 初始化
onMounted(() => {
fetchUserList()
fetchDiscountList()
if (queryParams.user_id) {
fetchHoldersList()
}
})
</script>
<style scoped>
.voucher-holders-container {
padding: 0;
}
.filter-container {
margin-bottom: 20px;
border-radius: 8px;
}
.search-form {
margin-bottom: 15px;
}
.action-bar {
display: flex;
gap: 12px;
}
.table-container {
border-radius: 8px;
}
.amount {
color: #f56c6c;
font-weight: bold;
font-size: 14px;
}
.pagination {
margin-top: 24px;
justify-content: flex-end;
}
/* 用户选择器样式 */
.user-selector-inline {
display: flex;
align-items: center;
}
.user-selector-wrapper {
width: 100%;
}
.selected-user-display {
margin-bottom: 12px;
padding: 8px 12px;
background: #f5f7fa;
border-radius: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.user-selector-dialog .selector-search {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #ebeef5;
}
.selector-pagination {
margin-top: 16px;
justify-content: flex-end;
}
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row):hover {
background-color: #f5f7fa;
}
:deep(.current-row) {
background-color: #ecf5ff !important;
}
</style>