Files
medical-mall/pages/mall/admin/marketing/coupon/receive.uvue
2026-02-15 16:37:37 +08:00

247 lines
8.4 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<AdminLayout currentPage="coupon-receive">
<view class="marketing-coupon-receive">
<!-- 1. 搜索过滤栏 -->
<view class="filter-card border-shadow">
<view class="filter-row">
<view class="filter-item">
<text class="label">搜索记录:</text>
<input class="input-mock" placeholder="用户名/券码" v-model="searchQuery" @confirm="handleSearch" />
</view>
<view class="filter-btns">
<button class="btn-query" @click="handleSearch">查询</button>
<button class="btn-reset" @click="handleReset">重置</button>
</view>
</view>
</view>
<!-- 2. 数据表格 -->
<view class="table-card border-shadow">
<view class="table-container">
<view v-if="isLoading" class="loading-mask">
<text class="loading-text">加载中...</text>
</view>
<view class="table-header">
<view class="th cell-id">ID</view>
<view class="th cell-user">用户信息</view>
<view class="th cell-name">优惠券名称</view>
<view class="th cell-code">优惠券码</view>
<view class="th cell-status">状态</view>
<view class="th cell-time">领取时间</view>
<view class="th cell-time">使用时间</view>
</view>
<view class="table-body">
<view v-if="recordList.length === 0 && !isLoading" class="empty-row">
<text>暂无领取记录</text>
</view>
<view v-for="item in recordList" :key="item.id" class="table-row">
<view class="td cell-id">
<text class="td-txt-small">{{ item.id }}</text>
</view>
<view class="td cell-user">
<view class="u-info">
<image class="u-avatar" :src="item.avatar || '/static/logo.png'" mode="aspectFill" />
<text class="u-nick">{{ item.nickname || '未知用户' }}</text>
</view>
</view>
<view class="td cell-name">
<text class="td-txt">{{ item.coupon_name || '已失效优惠券' }}</text>
</view>
<view class="td cell-code">
<text class="td-txt code-mono">{{ item.coupon_code }}</text>
</view>
<view class="td cell-status">
<text class="status-tag" :class="getStatusClass(item.status)">
{{ getStatusLabel(item.status) }}
</text>
</view>
<view class="td cell-time">
<text class="td-txt-small">{{ formatDateTime(item.received_at) }}</text>
</view>
<view class="td cell-time">
<text class="td-txt-small">{{ formatDateTime(item.used_at) }}</text>
</view>
</view>
</view>
</view>
<!-- 3. 分页控制 -->
<view class="pagination-footer">
<view class="page-total">
<text class="total-txt">共 {{ total }} 条</text>
</view>
<view class="page-btns">
<text class="p-btn" :class="{ disabled: page <= 1 }" @click="onPrevPage"></text>
<text class="p-btn active">{{ page }}</text>
<text class="p-btn" :class="{ disabled: recordList.length < pageSize }" @click="onNextPage"></text>
</view>
</view>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
import { fetchCouponReceiveRecords, UserCouponRecord } from '@/services/admin/marketingService.uts'
const recordList = ref<UserCouponRecord[]>([])
const isLoading = ref(false)
const searchQuery = ref('')
const page = ref(1)
const pageSize = 15
const total = ref(0)
onMounted(() => {
loadData()
})
async function loadData() {
isLoading.value = true
try {
const res = await fetchCouponReceiveRecords({
search: searchQuery.value,
page: page.value,
pageSize: pageSize
})
recordList.value = res.items
total.value = res.total
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
isLoading.value = false
}
}
function handleSearch() {
page.value = 1
loadData()
}
function handleReset() {
searchQuery.value = ''
page.value = 1
loadData()
}
function onPrevPage() {
if (page.value > 1) {
page.value--
loadData()
}
}
function onNextPage() {
if (recordList.value.length >= pageSize) {
page.value++
loadData()
}
}
function getStatusLabel(status : number) : string {
switch (status) {
case 1: return '未使用'
case 2: return '已使用'
case 3: return '已过期'
default: return '未知'
}
}
function getStatusClass(status : number) : string {
switch (status) {
case 1: return 'status-unused'
case 2: return 'status-used'
case 3: return 'status-expired'
default: return ''
}
}
function formatDateTime(iso : string | null) : string {
if (!iso) return '-'
return iso.substring(0, 16).replace('T', ' ')
}
</script>
<style scoped lang="scss">
.marketing-coupon-receive {
min-height: 100vh;
background: #f0f2f5;
padding: 16px;
}
.border-shadow {
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.filter-card { padding: 24px; margin-bottom: 16px; }
.filter-row { display: flex; flex-direction: row; align-items: center; gap: 24px; }
.filter-item { display: flex; flex-direction: row; align-items: center; }
.label { font-size: 14px; color: #606266; }
.input-mock {
width: 240px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px;
padding: 0 12px; font-size: 13px;
}
.filter-btns { display: flex; flex-direction: row; gap: 12px; }
.btn-query { background: #1890ff; color: #fff; height: 32px; padding: 0 16px; border-radius: 4px; font-size: 14px; border: none; cursor: pointer; }
.btn-reset { background: #fff; color: #666; height: 32px; padding: 0 16px; border-radius: 4px; font-size: 14px; border: 1px solid #dcdfe6; cursor: pointer; }
.table-card { padding: 24px; position: relative; }
.table-container { min-height: 400px; position: relative; }
.loading-mask {
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(255,255,255,0.7); display: flex; align-items: center; justify-content: center; z-index: 10;
}
.loading-text { color: #1890ff; font-size: 14px; }
.table-header {
display: flex; flex-direction: row; background-color: #f8faff;
border-bottom: 1px solid #e8eaec;
}
.th { padding: 12px 8px; font-size: 13px; color: #515a6e; font-weight: bold; text-align: center; }
.table-row {
display: flex; flex-direction: row; border-bottom: 1px solid #e8eaec; align-items: center;
&:hover { background-color: #fafafa; }
}
.td { padding: 12px 8px; text-align: center; display: flex; align-items: center; justify-content: center; }
.td-txt { font-size: 13px; color: #515a6e; }
.td-txt-small { font-size: 12px; color: #999; }
.code-mono { font-family: monospace; color: #1890ff; }
.cell-id { width: 60px; }
.cell-user { width: 180px; justify-content: flex-start; }
.cell-name { flex: 1; min-width: 150px; }
.cell-code { width: 120px; }
.cell-status { width: 100px; }
.cell-time { width: 150px; }
.u-info { display: flex; flex-direction: row; align-items: center; gap: 8px; }
.u-avatar { width: 32px; height: 32px; border-radius: 16px; background: #f5f5f5; }
.u-nick { font-size: 13px; color: #333; }
.status-tag {
padding: 2px 8px; border-radius: 4px; font-size: 12px;
&.status-unused { background: #e6f7ff; color: #1890ff; border: 1px solid #91d5ff; }
&.status-used { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
&.status-expired { background: #f5f5f5; color: #999; border: 1px solid #d9d9d9; }
}
.pagination-footer {
margin-top: 24px; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 12px;
}
.total-txt { font-size: 13px; color: #999; }
.page-btns { display: flex; flex-direction: row; gap: 8px; }
.p-btn {
width: 28px; height: 28px; border: 1px solid #dcdfe6; border-radius: 4px;
display: flex; align-items: center; justify-content: center; font-size: 14px;
&.active { background: #1890ff; color: #fff; border-color: #1890ff; }
&.disabled { opacity: 0.5; cursor: not-allowed; }
}
.empty-row { padding: 60px 0; text-align: center; color: #999; }
</style>