完成代码路径重构
This commit is contained in:
@@ -1,787 +0,0 @@
|
||||
<template>
|
||||
<view class="admin-marketing-coupon">
|
||||
<view class="content-body">
|
||||
<!-- 搜索过滤栏 -->
|
||||
<view class="filter-card border-shadow">
|
||||
<view class="filter-row">
|
||||
<view class="filter-item item-w">
|
||||
<text class="label-txt">优惠券名称:</text>
|
||||
<view class="input-wrap">
|
||||
<input class="search-input" v-model="filter.name" placeholder="请输入优惠券名称" />
|
||||
<text class="count-txt">{{ filter.name.length }}/18</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-item item-w">
|
||||
<text class="label-txt">优惠券类型:</text>
|
||||
<view class="select-mock">
|
||||
<text class="select-val">请选择</text>
|
||||
<text class="arrow-down">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-item item-w">
|
||||
<text class="label-txt">是否有效:</text>
|
||||
<view class="select-mock">
|
||||
<text class="select-val">请选择</text>
|
||||
<text class="arrow-down">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-row mt-20">
|
||||
<view class="filter-item item-w">
|
||||
<text class="label-txt">发放方式:</text>
|
||||
<view class="select-mock">
|
||||
<text class="select-val">请选择</text>
|
||||
<text class="arrow-down">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="btn-query" @click="handleQuery">
|
||||
<text class="query-txt">查询</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据展示区域 -->
|
||||
<view class="table-card border-shadow">
|
||||
<view class="card-header">
|
||||
<view class="btn-primary-blue" @click="handleAdd">
|
||||
<text class="btn-txt">添加优惠券</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表格主体 - 使用 CSS Grid 确保严格对齐 -->
|
||||
<view class="table-container">
|
||||
<!-- 表头 -->
|
||||
<view class="table-header-row table-grid">
|
||||
<view class="th p-1">ID</view>
|
||||
<view class="th p-1 name-col">优惠券名称</view>
|
||||
<view class="th p-2">优惠券类型</view>
|
||||
<view class="th p-1">面值/门槛</view>
|
||||
<view class="th p-3">领取/使用限制</view>
|
||||
<view class="th p-3">领取日期</view>
|
||||
<view class="th p-2">发布数量</view>
|
||||
<view class="th p-2">状态</view>
|
||||
<view class="th p-1 op-cell shadow-left">操作</view>
|
||||
</view>
|
||||
|
||||
<!-- 表身 -->
|
||||
<scroll-view class="table-body-scroll" scroll-x="true">
|
||||
<view class="table-body">
|
||||
<view v-for="(item, index) in dataList" :key="item.id" class="table-row table-grid">
|
||||
<view class="td p-1"><text class="td-txt">{{ item.id }}</text></view>
|
||||
<view class="td p-1 name-col">
|
||||
<view class="name-box">
|
||||
<text class="td-txt name-bold" :title="item.description">{{ item.name }}</text>
|
||||
<text class="date-small">{{ item.createdAt }} 创建</text>
|
||||
<text v-if="item.description" class="desc-tip">{{ item.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td p-2"><text class="td-txt">{{ item.type }}</text></view>
|
||||
<view class="td p-1">
|
||||
<view class="value-req">
|
||||
<text class="td-txt price-txt">{{ item.value.toFixed(2) }}{{ item.type.includes('折扣') ? '折' : '元' }}</text>
|
||||
<text class="date-small">{{ item.minOrderAmount > 0 ? '满' + item.minOrderAmount + '元可用' : '无门槛' }}</text>
|
||||
<text v-if="item.maxDiscountAmount != null" class="date-small danger">最多减{{ item.maxDiscountAmount }}元</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td p-3">
|
||||
<view class="limit-info">
|
||||
<text class="td-txt">{{ item.receiveType }}</text>
|
||||
<text class="date-small">每人限领: {{ item.perUserLimit }}张</text>
|
||||
<text class="date-small">领取限制: {{ item.usageLimit === 0 ? '不限' : item.usageLimit + '次/天' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td p-3"><text class="td-txt date-small">{{ item.receiveDate }}</text></view>
|
||||
<view class="td p-2">
|
||||
<view class="pub-info">
|
||||
<text class="td-txt">{{ item.publishTotal === 0 ? '不限量' : '总: ' + item.publishTotal }}</text>
|
||||
<text v-if="item.publishTotal > 0" class="pub-txt danger">剩: {{ item.remainingQuantity }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td p-2">
|
||||
<StatusSwitch
|
||||
:modelValue="item.isOpen"
|
||||
@update:modelValue="(val : boolean) => onToggleStatus(index, val)"
|
||||
/>
|
||||
</view>
|
||||
<!-- 操作列:Sticky 固定 -->
|
||||
<view class="td p-1 op-cell shadow-left">
|
||||
<view class="op-links">
|
||||
<text class="op-link" @click="handleShowRecords(item)">领取记录</text>
|
||||
<text class="op-split">|</text>
|
||||
<text class="op-link" @click="handleEdit(item)">编辑</text>
|
||||
<text class="op-split">|</text>
|
||||
<text class="op-link text-danger" @click="handleDelete(item)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<CommonPagination
|
||||
v-if="dataList.length > 0 || pageState.loading"
|
||||
:total="pageState.total"
|
||||
:loading="pageState.loading"
|
||||
:currentPage="pageState.currentPage"
|
||||
:pageSize="pageState.pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="pageState.jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { pageState.jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
|
||||
<!-- 空数据状态 -->
|
||||
<view class="table-empty" v-if="dataList.length === 0 && !pageState.loading">
|
||||
<text class="empty-txt">暂无数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 领取记录模态框 -->
|
||||
<view v-if="showRecordsModal" class="modal-mask" @click="showRecordsModal = false">
|
||||
<view class="modal-container" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">领取记录 - {{ selectedCoupon?.name }}</text>
|
||||
<text class="modal-close" @click="showRecordsModal = false">×</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="record-table">
|
||||
<view class="record-header record-grid">
|
||||
<text class="r-th">ID</text>
|
||||
<text class="r-th">用户名</text>
|
||||
<text class="r-th">用户头像</text>
|
||||
<text class="r-th">领取时间</text>
|
||||
</view>
|
||||
<scroll-view scroll-y="true" class="record-list">
|
||||
<view v-for="rec in currentRecords" :key="rec.id" class="record-row record-grid">
|
||||
<text class="r-td">{{ rec.id }}</text>
|
||||
<text class="r-td">{{ rec.username }}</text>
|
||||
<view class="r-td">
|
||||
<image class="avatar-img" :src="rec.avatar" mode="aspectFill"></image>
|
||||
</view>
|
||||
<text class="r-td">{{ rec.time }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import StatusSwitch from '@/components/StatusSwitch.uvue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
interface CouponItem {
|
||||
id: number
|
||||
id_uuid: string // 保持对 UUID 的引用以进行后续操作
|
||||
name: string
|
||||
description: string
|
||||
type: string
|
||||
value: number
|
||||
minOrderAmount: number
|
||||
maxDiscountAmount: number | null
|
||||
receiveType: string
|
||||
receiveDate: string
|
||||
useTime: string
|
||||
publishTotal: number
|
||||
remainingQuantity: number
|
||||
perUserLimit: number
|
||||
usageLimit: number
|
||||
isOpen: boolean
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
interface RecordItem {
|
||||
id: string
|
||||
username: string
|
||||
avatar: string
|
||||
time: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔐 权限和弹窗逻辑说明:
|
||||
* - isAdmin: 模拟当前用户的 admin 权限状态。
|
||||
* - handleDelete: 点击删除时首先弹出确认对话框。
|
||||
* - 权限校验: 在确认删除的回调中检查 isAdmin 状态。
|
||||
* - 拦截行为: 如果非 admin,弹出错误警告并阻止执行删除操作。
|
||||
*/
|
||||
const isAdmin = ref(true) // 为了方便测试,设为 true
|
||||
|
||||
const filter = reactive({
|
||||
name: ''
|
||||
})
|
||||
|
||||
const dataList = ref<CouponItem[]>([])
|
||||
|
||||
// --- 分页与列表状态管理 ---
|
||||
const pageState = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
loading: false,
|
||||
jumpPageInput: ''
|
||||
})
|
||||
|
||||
const pageSizeOptions = [10, 20, 30, 50, 100]
|
||||
const pageSizeOptionLabels = computed((): string[] => pageSizeOptions.map((size: number): string => `${size} 条/页`))
|
||||
const pageSizeIndex = computed((): number => {
|
||||
const index = pageSizeOptions.indexOf(pageState.pageSize)
|
||||
return index === -1 ? 0 : index
|
||||
})
|
||||
|
||||
const totalPage = computed((): number => {
|
||||
return Math.ceil(pageState.total / pageState.pageSize)
|
||||
})
|
||||
|
||||
const visiblePages = computed((): number[] => {
|
||||
const current = pageState.currentPage
|
||||
const total = totalPage.value
|
||||
if (total <= 7) {
|
||||
const pages: number[] = []
|
||||
for (let i = 1; i <= total; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
if (current <= 4) {
|
||||
return [1, 2, 3, 4, 5, -1, total]
|
||||
}
|
||||
|
||||
if (current >= total - 3) {
|
||||
return [1, -1, total - 4, total - 3, total - 2, total - 1, total]
|
||||
}
|
||||
|
||||
return [1, -1, current - 1, current, current + 1, -1, total]
|
||||
})
|
||||
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
if (pageState.loading) return
|
||||
let val = 0
|
||||
if (typeof e.detail.value === 'string') {
|
||||
val = parseInt(e.detail.value)
|
||||
} else {
|
||||
val = e.detail.value as number
|
||||
}
|
||||
pageState.pageSize = pageSizeOptions[val]
|
||||
pageState.currentPage = 1
|
||||
fetchCouponTemplates()
|
||||
}
|
||||
|
||||
const handlePageChange = (p: number) => {
|
||||
if (pageState.loading || p < 1 || p > totalPage.value || p === pageState.currentPage) return
|
||||
pageState.currentPage = p
|
||||
pageState.jumpPageInput = ''
|
||||
fetchCouponTemplates()
|
||||
}
|
||||
|
||||
const handleJumpPage = () => {
|
||||
if (pageState.loading) return
|
||||
let jumpTo = parseInt(pageState.jumpPageInput)
|
||||
if (isNaN(jumpTo)) return
|
||||
if (jumpTo < 1) jumpTo = 1
|
||||
if (jumpTo > totalPage.value) jumpTo = totalPage.value
|
||||
|
||||
pageState.jumpPageInput = String(jumpTo)
|
||||
if (jumpTo !== pageState.currentPage) {
|
||||
pageState.currentPage = jumpTo
|
||||
fetchCouponTemplates()
|
||||
}
|
||||
}
|
||||
|
||||
const fetchCouponTemplates = async () => {
|
||||
pageState.loading = true
|
||||
uni.showLoading({ title: '加载中...', mask: true })
|
||||
|
||||
try {
|
||||
// 兼容使用 { count: 'exact' } 获取精确总数
|
||||
let query = supa.from('ml_coupon_templates').select('*', { count: 'exact' })
|
||||
|
||||
// 如果有名称筛选
|
||||
if (filter.name.trim() != '') {
|
||||
query = query.like('name', `%${filter.name}%`)
|
||||
}
|
||||
|
||||
// 计算分页 range
|
||||
const start = (pageState.currentPage - 1) * pageState.pageSize
|
||||
const end = pageState.currentPage * pageState.pageSize - 1
|
||||
|
||||
const res = await query.order('created_at', { ascending: false }).range(start, end).execute()
|
||||
|
||||
if (res.error != null || res.status >= 400) {
|
||||
const msg = res.error?.message ?? `获取数据失败 (${res.status})`
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试安全获取总数。如果有的封装库剥离了 count 属性,则以当前返回的数据长度作为保底
|
||||
let fCount = 0
|
||||
if (res.count != null) {
|
||||
fCount = res.count as number
|
||||
} else if (res['total'] != null) {
|
||||
fCount = res['total'] as number
|
||||
}
|
||||
|
||||
if (!Array.isArray(res.data)) {
|
||||
console.warn('Expected array but got:', res.data)
|
||||
dataList.value = []
|
||||
pageState.total = fCount
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = res.data as Array<UTSJSONObject>
|
||||
|
||||
// 如果获取到的总数为 0 但实际有数据,说明接口 count 丢了,用当前拉取的数据量兜底防止分页区坍塌
|
||||
pageState.total = Math.max(fCount, rawData.length)
|
||||
|
||||
dataList.value = rawData.map((item : UTSJSONObject) : CouponItem => {
|
||||
// 优先获取 cid (自增 ID),如果没有则取 id (UUID) 的后几位或 0
|
||||
const displayId = (item.get('cid') as number | null) ?? 0
|
||||
const uuid = item.get('id') as string ?? ''
|
||||
|
||||
const couponType = item.get('coupon_type') as number ?? 1
|
||||
let typeStr = '未知类型'
|
||||
if (couponType === 1) typeStr = '满减券'
|
||||
else if (couponType === 2) typeStr = '折扣券'
|
||||
else if (couponType === 3) typeStr = '免运费券'
|
||||
|
||||
// 获取面值逻辑:如果是折扣券 (discount_type=2),展示方式可能不同
|
||||
const discountType = item.get('discount_type') as number ?? 1
|
||||
const discountValue = item.get('discount_value') as number ?? 0
|
||||
let displayValue = discountValue
|
||||
|
||||
const startTime = item.get('start_time') as string ?? ''
|
||||
const endTime = item.get('end_time') as string ?? ''
|
||||
const createdAt = item.get('created_at') as string ?? ''
|
||||
const dateStr = (startTime != '' && endTime != '')
|
||||
? `${startTime.slice(0, 10)} 至 ${endTime.slice(0, 10)}`
|
||||
: '不限时'
|
||||
|
||||
return {
|
||||
id: displayId,
|
||||
id_uuid: uuid,
|
||||
name: item.get('name') as string ?? '未命名优惠券',
|
||||
description: item.get('description') as string ?? '',
|
||||
type: typeStr,
|
||||
value: displayValue,
|
||||
minOrderAmount: item.get('min_order_amount') as number ?? 0,
|
||||
maxDiscountAmount: item.get('max_discount_amount') as number | null,
|
||||
receiveType: (item.get('merchant_id') == null ? '平台发放' : '商家发放'),
|
||||
receiveDate: dateStr,
|
||||
useTime: dateStr,
|
||||
publishTotal: (item.get('total_quantity') as number ?? 0),
|
||||
remainingQuantity: (item.get('remaining_quantity') as number ?? 0),
|
||||
perUserLimit: item.get('per_user_limit') as number ?? 1,
|
||||
usageLimit: item.get('usage_limit') as number ?? 1,
|
||||
isOpen: (item.get('status') as number) === 1,
|
||||
createdAt: createdAt.slice(0, 16).replace('T', ' ')
|
||||
} as CouponItem
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Fetch Coupons Error:', e)
|
||||
uni.showToast({ title: '访问数据库异常', icon: 'none' })
|
||||
} finally {
|
||||
pageState.loading = false
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
pageState.currentPage = 1 // 重置到第一页
|
||||
fetchCouponTemplates()
|
||||
}
|
||||
const handleAdd = () => {
|
||||
uni.showToast({ title: '导航至添加页面', icon: 'none' })
|
||||
}
|
||||
|
||||
const toggleStatus = async (index: number) => {
|
||||
const item = dataList.value[index]
|
||||
const newStatus = item.isOpen ? 2 : 1 // 1: 正常, 2: 暂停
|
||||
|
||||
try {
|
||||
const res = await supa.from('ml_coupon_templates')
|
||||
.update({ status: newStatus } as UTSJSONObject)
|
||||
.eq('id', item.id_uuid)
|
||||
.execute()
|
||||
|
||||
if (res.error != null || res.status >= 400) {
|
||||
uni.showToast({ title: '状态更新失败: ' + (res.error?.message ?? `HTTP ${res.status}`), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
item.isOpen = !item.isOpen
|
||||
uni.showToast({ title: '状态更新成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '更新状态异常', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 领取记录逻辑
|
||||
const showRecordsModal = ref(false)
|
||||
const selectedCoupon = ref<CouponItem | null>(null)
|
||||
const currentRecords = ref<RecordItem[]>([])
|
||||
|
||||
const handleShowRecords = (item: CouponItem) => {
|
||||
selectedCoupon.value = item
|
||||
// 模拟接口数据
|
||||
currentRecords.value = [
|
||||
{ id: '70074', username: '131****7722', avatar: 'https://placeholder.com/40', time: '2025-02-25 21:33:35' },
|
||||
{ id: '36794', username: '147****4489', avatar: 'https://placeholder.com/40', time: '2025-02-25 21:58:27' },
|
||||
{ id: '60902', username: '花开花落', avatar: 'https://placeholder.com/40', time: '2025-02-25 21:59:14' },
|
||||
{ id: '70312', username: '55454', avatar: 'https://placeholder.com/40', time: '2025-02-25 23:20:00' }
|
||||
]
|
||||
showRecordsModal.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (item: CouponItem) => {
|
||||
uni.showToast({ title: `编辑: ${item.name}`, icon: 'none' })
|
||||
}
|
||||
|
||||
const handleCopy = (item: CouponItem) => {
|
||||
uni.showToast({ title: `复制: ${item.name}`, icon: 'success' })
|
||||
}
|
||||
|
||||
// 删除逻辑与权限验证
|
||||
const handleDelete = (item: CouponItem) => {
|
||||
uni.showModal({
|
||||
title: '确认提示',
|
||||
content: `确定要删除“${item.name}”这一条优惠券数据吗?此操作不可逆。`,
|
||||
cancelText: '取消',
|
||||
confirmText: '确定',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
// 🔒 权限校验:检查用户是否具有 admin 权限
|
||||
if (!isAdmin.value) {
|
||||
uni.showModal({
|
||||
title: '操作被拦截',
|
||||
content: '您无权限删除,请联系系统管理员。',
|
||||
showCancel: false,
|
||||
confirmText: '阅读并关闭'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 实际从数据库删除
|
||||
const delRes = await supa.from('ml_coupon_templates')
|
||||
.eq('id', item.id_uuid)
|
||||
.delete()
|
||||
.execute()
|
||||
|
||||
if (delRes.error != null || delRes.status >= 400) {
|
||||
uni.showToast({ title: '删除失败: ' + (delRes.error?.message ?? `HTTP ${delRes.status}`), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
|
||||
// 如果当前页只有一条数据,且不是第一页,则退回上一页
|
||||
if (dataList.value.length === 1 && pageState.currentPage > 1) {
|
||||
pageState.currentPage--
|
||||
}
|
||||
|
||||
// 重新拉取当前页数据
|
||||
fetchCouponTemplates()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作数据库异常', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('Coupon list initializing and fetching data...')
|
||||
fetchCouponTemplates()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-marketing-coupon {
|
||||
padding: 0px;
|
||||
background-color: #f5f7f9;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.content-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 过滤栏 */
|
||||
.filter-card { padding: 24px; }
|
||||
.filter-row { display: flex; flex-direction: row; align-items: center; flex-wrap: wrap; gap: 24px; }
|
||||
.mt-20 { margin-top: 20px; }
|
||||
|
||||
.filter-item { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.item-w { width: 320px; }
|
||||
|
||||
.label-txt { font-size: 14px; color: #606266; min-width: 80px; text-align: right; }
|
||||
|
||||
.input-wrap {
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.search-input { flex: 1; height: 100%; font-size: 14px; background: none; border: none; outline: none; }
|
||||
.count-txt { font-size: 12px; color: #c0c4cc; }
|
||||
|
||||
.select-mock {
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.select-val { font-size: 14px; color: #c0c4cc; }
|
||||
.arrow-down { font-size: 10px; color: #c0c4cc; }
|
||||
|
||||
.btn-query {
|
||||
background-color: #2d8cf0;
|
||||
padding: 0 20px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.query-txt { color: #fff; font-size: 14px; }
|
||||
|
||||
/* 表格区域 */
|
||||
.table-card { background-color: #fff; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.card-header { padding: 24px; }
|
||||
|
||||
.btn-primary-blue {
|
||||
background-color: #2d8cf0;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-txt { color: #fff; font-size: 14px; }
|
||||
|
||||
.table-container {
|
||||
padding: 0 24px 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 🧱 Grid 核心布局系统 */
|
||||
.table-grid {
|
||||
display: grid;
|
||||
/* 列宽定义:ID, 名称, 类型, 面值/门槛, 领取/限制, 日期, 数量, 状态, 操作 */
|
||||
grid-template-columns: 60px 2.5fr 100px 150px 180px 160px 100px 80px 240px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.table-header-row {
|
||||
background-color: #f8f8f9;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
}
|
||||
|
||||
.th {
|
||||
padding: 12px 10px;
|
||||
font-size: 14px;
|
||||
color: #515a6e;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
background-color: #fff;
|
||||
}
|
||||
.table-row:hover { background-color: #f0faff; }
|
||||
|
||||
.td {
|
||||
padding: 12px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
/* 文本溢出策略:支持正常换行 */
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.name-box, .value-req, .limit-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.desc-tip {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
background-color: #f9f9f9;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
display: none; /* 默认隐藏,在 hover 状态显示(H5仿真) */
|
||||
}
|
||||
.table-row:hover .desc-tip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.td-txt { font-size: 14px; color: #515a6e; line-height: 1.5; }
|
||||
.name-bold { font-weight: 500; color: #333; }
|
||||
.price-txt { color: #f56c6c; font-weight: bold; }
|
||||
.date-small { font-size: 12px; color: #999; }
|
||||
.danger { color: #ed4014; }
|
||||
|
||||
.pub-info { display: flex; flex-direction: column; }
|
||||
.pub-txt.danger { color: #ed4014; font-size: 11px; margin-top: 2px; }
|
||||
|
||||
/* 🚀 Sticky 操作列实现 */
|
||||
.op-cell {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
background-color: #fff; /* 背景必须不透明 */
|
||||
z-index: 10;
|
||||
justify-content: center;
|
||||
}
|
||||
/* 鼠标悬停时保持背景色同步 */
|
||||
.table-row:hover .op-cell {
|
||||
background-color: #f0faff;
|
||||
}
|
||||
|
||||
.shadow-left {
|
||||
box-shadow: -6px 0 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.op-links { display: flex; flex-direction: row; align-items: center; flex-wrap: wrap; justify-content: center; }
|
||||
.op-link { color: #2d8cf0; font-size: 13px; cursor: pointer; margin: 2px 4px; white-space: nowrap; }
|
||||
.op-split { color: #e8eaec; margin: 0; font-size: 12px; }
|
||||
.text-danger { color: #ed4014; }
|
||||
|
||||
/* 📱 响应式媒体查询 (Priority 策略) */
|
||||
/* 平板:隐藏次要列 */
|
||||
@media screen and (max-width: 1450px) {
|
||||
.table-grid {
|
||||
grid-template-columns: 70px 2fr 90px 80px 0 150px 0 100px 90px 240px;
|
||||
}
|
||||
.p-3 { display: none; } /* 隐藏优先级 3:领取方式、使用时间 */
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1100px) {
|
||||
.table-grid {
|
||||
grid-template-columns: 0 1.5fr 0 80px 0 140px 0 100px 0 220px;
|
||||
}
|
||||
.p-1:first-child, .p-2 { display: none; } /* 隐藏 ID, 类型, 状态 */
|
||||
}
|
||||
|
||||
/* 手机:极致压缩,浮动操作 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.table-grid {
|
||||
grid-template-columns: 1fr 100px 140px;
|
||||
}
|
||||
.p-1:not(.name-col):not(.op-cell), .p-2, .p-3 { display: none; }
|
||||
.op-cell { width: 140px; border-left: 1px solid #f0f0f0; }
|
||||
.op-split { display: none; }
|
||||
.op-links { flex-direction: column; align-items: center; }
|
||||
}
|
||||
|
||||
/* 空数据状态 */
|
||||
.table-empty {
|
||||
padding: 60px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #e8eaec;
|
||||
}
|
||||
.empty-txt {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 🎭 模态框样式 */
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: rgba(0,0,0,0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal-container {
|
||||
width: 90%;
|
||||
max-width: 760px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 85vh;
|
||||
box-shadow: 0 10px 50px rgba(0,0,0,0.2);
|
||||
}
|
||||
.modal-header {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.modal-title { font-size: 16px; font-weight: bold; color: #1f2d3d; }
|
||||
.modal-close { font-size: 28px; color: #bfcbd9; cursor: pointer; line-height: 1; }
|
||||
.modal-close:hover { color: #ff4949; }
|
||||
|
||||
.modal-body { padding: 24px; flex: 1; overflow: hidden; display: flex; flex-direction: column; }
|
||||
|
||||
.record-table { border: 1px solid #ebeef5; border-radius: 4px; flex: 1; overflow: hidden; display: flex; flex-direction: column; }
|
||||
.record-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 1.5fr 100px 180px;
|
||||
}
|
||||
.record-header { background-color: #f5f7fa; border-bottom: 1px solid #ebeef5; }
|
||||
.r-th, .r-td { padding: 12px 15px; font-size: 14px; color: #606266; display: flex; align-items: center; }
|
||||
.r-th { font-weight: bold; color: #909399; }
|
||||
.record-list { flex: 1; height: 300px; }
|
||||
.record-row { border-bottom: 1px solid #f0f2f5; transition: background 0.2s; }
|
||||
.record-row:hover { background-color: #f9fafc; }
|
||||
.avatar-img { width: 32px; height: 32px; border-radius: 4px; background-color: #eee; }
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.record-grid {
|
||||
grid-template-columns: 50px 1fr 0 140px;
|
||||
}
|
||||
.record-grid .r-th:nth-child(1), .record-grid .r-td:nth-child(1),
|
||||
.record-grid .r-th:nth-child(3), .record-grid .r-td:nth-child(3) { display: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="coupon-receive">
|
||||
<view class="page">
|
||||
<view class="Header">
|
||||
<text class="Title">领取情况</text>
|
||||
<text class="SubTitle">marketing/coupon/receive</text>
|
||||
</view>
|
||||
|
||||
<view class="Card">
|
||||
<text class="Label">页面参数(query)</text>
|
||||
<text class="Mono">{{ params }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
|
||||
|
||||
const params = ref('')
|
||||
|
||||
onLoad((options) => {
|
||||
// options: Record<string, any>
|
||||
params.value = JSON.stringify(options ?? {})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.Page {
|
||||
padding: 0;
|
||||
}
|
||||
.Header {
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.Title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.SubTitle {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.Card {
|
||||
margin-top: 24rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.Label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.Mono {
|
||||
font-size: 24rpx;
|
||||
font-family: monospace;
|
||||
line-height: 36rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user