This commit is contained in:
2026-02-03 21:35:57 +08:00
parent 93b42a277a
commit 3922409a25
105 changed files with 14758 additions and 5861 deletions

View File

@@ -1,65 +1,324 @@
<template>
<AdminLayout currentPage="coupon-list">
<view class="page">
<view class="Header">
<text class="Title">优惠券列表</text>
<text class="SubTitle">marketing/coupon/list</text>
</view>
<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" placeholder="请输入优惠券名称" v-model="filter.name" />
<text class="count-txt">0/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="Card">
<text class="Label">页面参数query</text>
<text class="Mono">{{ params }}</text>
<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>
<!-- 表格主体 -->
<view class="table-container">
<view class="table-header-row">
<view class="th" style="width: 80px;">ID</view>
<view class="th" style="width: 180px;">优惠券名称</view>
<view class="th" style="width: 120px;">优惠券类型</view>
<view class="th" style="width: 100px;">面值</view>
<view class="th" style="width: 120px;">领取方式</view>
<view class="th" style="width: 150px;">领取日期</view>
<view class="th" style="width: 120px;">使用时间</view>
<view class="th" style="width: 120px;">发布数量</view>
<view class="th" style="width: 100px;">是否开启</view>
<view class="th" style="flex: 1; min-width: 220px;">操作</view>
</view>
<view class="table-body">
<view v-for="(item, index) in dataList" :key="item.id" class="table-row">
<view class="td" style="width: 80px;"><text class="td-txt">{{ item.id }}</text></view>
<view class="td" style="width: 180px;"><text class="td-txt name-bold">{{ item.name }}</text></view>
<view class="td" style="width: 120px;"><text class="td-txt">{{ item.type }}</text></view>
<view class="td" style="width: 100px;"><text class="td-txt price-txt">{{ item.value.toFixed(2) }}</text></view>
<view class="td" style="width: 120px;"><text class="td-txt">{{ item.receiveType }}</text></view>
<view class="td" style="width: 150px;">
<text v-if="item.id === 1628" class="td-txt date-small">2023-10-18 00:00 - 2025-11-05 00:00</text>
<text v-else class="td-txt">{{ item.receiveDate }}</text>
</view>
<view class="td" style="width: 120px;"><text class="td-txt">{{ item.useTime }}</text></view>
<view class="td" style="width: 120px;">
<view v-if="item.publishTotal > 0" class="pub-info">
<text class="pub-txt">发布: {{ item.publishTotal }}</text>
<text class="pub-txt danger">剩余: {{ item.publishRemain }}</text>
</view>
<text v-else class="td-txt">不限量</text>
</view>
<view class="td" style="width: 100px;">
<view :class="['switch-box', item.isOpen ? 'active' : '']" @click="toggleStatus(index)">
<view class="switch-dot"></view>
</view>
</view>
<view class="td" style="flex: 1; min-width: 220px;">
<view class="op-links">
<text class="op-link">领取记录</text>
<text class="op-split">|</text>
<text class="op-link">编辑</text>
<text class="op-split">|</text>
<text class="op-link">复制</text>
<text class="op-split">|</text>
<text class="op-link text-danger">删除</text>
</view>
</view>
</view>
</view>
</view>
<!-- 分页 -->
<view class="pagination-footer">
<text class="total-txt">共 16 条</text>
<view class="page-select">
<text class="page-val">15条/页 ▼</text>
</view>
<view class="page-btns">
<text class="p-btn disabled"><</text>
<text class="p-btn active">1</text>
<text class="p-btn">2</text>
<text class="p-btn">></text>
</view>
<view class="page-jump">
<text class="jump-txt">前往</text>
<input class="jump-input" placeholder="1" />
<text class="jump-txt">页</text>
</view>
</view>
</view>
</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'
import { ref, reactive } from 'vue'
interface CouponItem {
id: number
name: string
type: string
value: number
receiveType: string
receiveDate: string
useTime: string
publishTotal: number
publishRemain: number
isOpen: boolean
}
const params = ref('')
onLoad((options) => {
// options: Record<string, any>
params.value = JSON.stringify(options ?? {})
const filter = reactive({
name: ''
})
const dataList = ref<CouponItem[]>([
{ id: 1643, name: '满100减30', type: '通用券', value: 30.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '10天', publishTotal: 0, publishRemain: 0, isOpen: false },
{ id: 1642, name: '满10减7', type: '通用券', value: 7.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '10天', publishTotal: 0, publishRemain: 0, isOpen: true },
{ id: 1641, name: '会员优惠券', type: '通用券', value: 200.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '200天', publishTotal: 0, publishRemain: 0, isOpen: true },
{ id: 1640, name: '会员优惠券', type: '通用券', value: 29.90, receiveType: '用户领取', receiveDate: '不限时', useTime: '200天', publishTotal: 0, publishRemain: 0, isOpen: true },
{ id: 1639, name: '会员优惠券', type: '通用券', value: 1.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '200天', publishTotal: 0, publishRemain: 0, isOpen: true },
{ id: 1638, name: '商品券', type: '商品券', value: 1.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '200天', publishTotal: 0, publishRemain: 0, isOpen: true },
{ id: 1636, name: '测试多个商品消耗一个券', type: '商品券', value: 500.00, receiveType: '系统赠送', receiveDate: '不限时', useTime: '3天', publishTotal: 0, publishRemain: 0, isOpen: true },
{ id: 1635, name: '优惠券', type: '通用券', value: 10.00, receiveType: '系统赠送', receiveDate: '不限时', useTime: '10天', publishTotal: 0, publishRemain: 0, isOpen: true },
{ id: 1634, name: '限时优惠', type: '通用券', value: 20.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '5天', publishTotal: 0, publishRemain: 0, isOpen: true },
{ id: 1633, name: '店庆券', type: '品类券', value: 100.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '10天', publishTotal: 0, publishRemain: 0, isOpen: true },
{ id: 1632, name: '优惠券', type: '品类券', value: 99.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '10天', publishTotal: 8999, publishRemain: 8604, isOpen: true },
{ id: 1628, name: '全场通用券', type: '通用券', value: 9.90, receiveType: '用户领取', receiveDate: 'RANGE', useTime: '不限时', publishTotal: 59999, publishRemain: 59331, isOpen: true }
])
const handleQuery = () => { console.log('Querying...') }
const handleAdd = () => { console.log('Adding coupon...') }
const toggleStatus = (index: number) => {
dataList.value[index].isOpen = !dataList.value[index].isOpen
}
</script>
<style>
.Page {
padding: 24rpx;
<style scoped lang="scss">
.admin-marketing-coupon {
background-color: #f0f2f5;
min-height: 100vh;
padding: 24px;
}
.Header {
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
.border-shadow {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.Title {
font-size: 36rpx;
font-weight: 700;
.content-body {
display: flex;
flex-direction: column;
gap: 20px;
}
.SubTitle {
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.7;
/* 过滤栏 */
.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;
}
.Card {
margin-top: 24rpx;
padding: 24rpx;
border-radius: 16rpx;
background: #ffffff;
.search-input { flex: 1; height: 100%; font-size: 14px; }
.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;
}
.Label {
font-size: 26rpx;
font-weight: 600;
margin-bottom: 12rpx;
.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;
}
.Mono {
font-size: 24rpx;
font-family: monospace;
line-height: 36rpx;
word-break: break-all;
.query-txt { color: #fff; font-size: 14px; }
/* 表格区域 */
.table-card { background-color: #fff; display: flex; flex-direction: column; }
.card-header { padding: 20px; }
.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 20px 20px; }
.table-header-row { display: flex; flex-direction: row; background-color: #f8f8f9; border-bottom: 1px solid #e8eaec; }
.th { padding: 12px 10px; font-size: 14px; color: #515a6e; font-weight: bold; }
.table-row { display: flex; flex-direction: row; border-bottom: 1px solid #e8eaec; border-left: 1px solid transparent; }
.table-row:hover { background-color: #ebf7ff; }
.td { padding: 12px 10px; display: flex; align-items: center; }
.td-txt { font-size: 14px; color: #515a6e; }
.name-bold { font-weight: 500; color: #333; }
.price-txt { color: #515a6e; }
.date-small { font-size: 12px; line-height: 1.4; color: #999; }
.pub-info { display: flex; flex-direction: column; }
.pub-txt { font-size: 12px; color: #2d8cf0; }
.pub-txt.danger { color: #ed4014; }
/* Switch 开关 */
.switch-box {
width: 44px;
height: 22px;
background-color: #dcdfe6;
border-radius: 11px;
position: relative;
transition: all 0.3s;
cursor: pointer;
}
.switch-box.active { background-color: #2d8cf0; }
.switch-dot {
width: 18px;
height: 18px;
background-color: #fff;
border-radius: 9px;
position: absolute;
top: 2px;
left: 2px;
transition: all 0.3s;
}
.switch-box.active .switch-dot { transform: translateX(22px); }
.op-links { display: flex; flex-direction: row; align-items: center; }
.op-link { color: #2d8cf0; font-size: 14px; cursor: pointer; margin: 0 5px; }
.op-split { color: #e8eaec; margin: 0 5px; }
.text-danger { color: #ed4014; }
/* 分页 */
.pagination-footer {
padding: 20px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 12px;
border-top: 1px solid #f0f0f0;
}
.total-txt { font-size: 14px; color: #606266; }
.page-val { font-size: 14px; color: #606266; border: 1px solid #dcdfe6; padding: 4px 10px; border-radius: 4px; }
.page-btns { display: flex; flex-direction: row; gap: 8px; }
.p-btn {
width: 32px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px;
display: flex; align-items: center; justify-content: center; font-size: 14px; color: #666;
}
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
.p-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; }
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
.jump-txt { font-size: 14px; color: #606266; }
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 14px; }
</style>

View File

@@ -0,0 +1,230 @@
<template>
<view class="admin-marketing-coupon-user">
<view class="content-body">
<!-- 搜索过滤栏 -->
<view class="filter-card border-shadow">
<view class="filter-row">
<view class="filter-item">
<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">
<text class="label-txt">领取人:</text>
<input class="search-input" placeholder="请输入领取人" v-model="filter.username" />
</view>
<view class="filter-item filter-long">
<text class="label-txt">优惠券搜索:</text>
<input class="search-input input-wide" placeholder="请输入优惠券名称" v-model="filter.couponName" />
</view>
<view class="btn-query" @click="handleQuery">
<text class="query-txt">查询</text>
</view>
</view>
</view>
<!-- 主要表格区域 -->
<view class="table-card border-shadow">
<view class="table-container">
<view class="table-header-row">
<view class="th" style="width: 100px;">ID</view>
<view class="th" style="width: 150px;">优惠券名称</view>
<view class="th" style="width: 180px;">领取人</view>
<view class="th" style="width: 100px;">面值</view>
<view class="th" style="width: 120px;">最低消费额</view>
<view class="th" style="width: 180px;">开始使用时间</view>
<view class="th" style="width: 180px;">结束使用时间</view>
<view class="th" style="width: 120px;">获取方式</view>
<view class="th" style="width: 100px;">是否可用</view>
<view class="th" style="flex: 1; min-width: 100px;">状态</view>
</view>
<view class="table-body">
<view v-for="item in recordList" :key="item.id" class="table-row">
<view class="td" style="width: 100px;"><text class="td-txt">{{ item.id }}</text></view>
<view class="td" style="width: 150px;"><text class="td-txt">{{ item.couponName }}</text></view>
<view class="td" style="width: 180px;"><text class="td-txt">{{ item.username }}</text></view>
<view class="td" style="width: 100px;"><text class="td-txt price-txt">{{ item.value.toFixed(2) }}</text></view>
<view class="td" style="width: 120px;"><text class="td-txt price-txt">{{ item.minSpend.toFixed(2) }}</text></view>
<view class="td" style="width: 180px;"><text class="td-txt time-txt">{{ item.startTime }}</text></view>
<view class="td" style="width: 180px;"><text class="td-txt time-txt">{{ item.endTime }}</text></view>
<view class="td" style="width: 120px;"><text class="td-txt">{{ item.getType }}</text></view>
<view class="td" style="width: 100px;">
<text v-if="item.isValid" class="status-ic-success">✓</text>
<text v-else class="status-ic-fail">×</text>
</view>
<view class="td" style="flex: 1; min-width: 100px;"><text :class="['td-txt', item.status === '已使用' ? 'status-used' : '']">{{ item.status }}</text></view>
</view>
</view>
</view>
<!-- 分页 -->
<view class="pagination-footer">
<text class="total-txt">共 16 条</text>
<view class="page-select">
<text class="page-val">15条/页 ▼</text>
</view>
<view class="page-btns">
<text class="p-btn disabled"><</text>
<text class="p-btn active">1</text>
<text class="p-btn">2</text>
<text class="p-btn">></text>
</view>
<view class="page-jump">
<text class="jump-txt">前往</text>
<input class="jump-input" placeholder="1" />
<text class="jump-txt">页</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive } from 'vue'
interface CouponRecord {
id: number
couponName: string
username: string
value: number
minSpend: number
startTime: string
endTime: string
getType: string
isValid: boolean
status: string
}
const filter = reactive({
username: '',
couponName: ''
})
const recordList = ref<CouponRecord[]>([
{ id: 217732, couponName: '满10减7', username: '嘻嘻', value: 7.00, minSpend: 0.00, startTime: '2026-02-03 17:23', endTime: '2026-02-13 17:23', getType: '手动领取', isValid: true, status: '未使用' },
{ id: 217731, couponName: '满10减7', username: '1岁上班22岁退休', value: 7.00, minSpend: 0.00, startTime: '2026-02-03 17:20', endTime: '2026-02-13 17:20', getType: '手动领取', isValid: true, status: '未使用' },
{ id: 217730, couponName: '店庆券', username: '136****0434', value: 100.00, minSpend: 1000.00, startTime: '2026-02-03 17:13', endTime: '2026-02-13 17:13', getType: '手动领取', isValid: true, status: '未使用' },
{ id: 217729, couponName: '优惠券', username: '187****2801', value: 100.00, minSpend: 599.00, startTime: '2026-02-03 16:36', endTime: '2026-03-05 16:36', getType: '手动领取', isValid: true, status: '未使用' },
{ id: 217728, couponName: '会员优惠券', username: '彭祖Dean', value: 29.90, minSpend: 0.00, startTime: '2026-02-03 16:06', endTime: '2026-08-22 16:06', getType: '手动领取', isValid: true, status: '未使用' },
{ id: 217727, couponName: '会员优惠券', username: '彭祖Dean', value: 200.00, minSpend: 0.00, startTime: '2026-02-03 16:06', endTime: '2026-08-22 16:06', getType: '手动领取', isValid: true, status: '未使用' },
{ id: 217726, couponName: '满10减7', username: '181****6929', value: 7.00, minSpend: 0.00, startTime: '2026-02-03 15:56', endTime: '2026-02-13 15:56', getType: '手动领取', isValid: true, status: '未使用' },
{ id: 217725, couponName: '优惠券', username: '181****3601', value: 100.00, minSpend: 599.00, startTime: '2026-02-03 15:51', endTime: '2026-03-05 15:51', getType: '手动领取', isValid: true, status: '未使用' },
{ id: 217724, couponName: '商品券', username: '陌年微凉TL', value: 1.00, minSpend: 0.00, startTime: '2026-02-03 15:17', endTime: '2026-08-22 15:17', getType: '手动领取', isValid: true, status: '未使用' },
{ id: 217723, couponName: '限时优惠', username: '陌年微凉TL', value: 20.00, minSpend: 199.00, startTime: '2026-02-03 15:17', endTime: '2026-02-08 15:17', getType: '手动领取', isValid: false, status: '已使用' }
])
const handleQuery = () => { console.log('Querying redemption records...') }
</script>
<style scoped lang="scss">
.admin-marketing-coupon-user {
background-color: #f0f2f5;
min-height: 100vh;
padding: 24px;
}
.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;
}
/* 过滤栏 */
.filter-card { padding: 24px; }
.filter-row { display: flex; flex-direction: row; align-items: center; flex-wrap: wrap; gap: 32px; }
.filter-item { display: flex; flex-direction: row; align-items: center; gap: 12px; }
.label-txt { font-size: 14px; color: #606266; }
.select-mock {
width: 180px;
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; }
.search-input {
width: 180px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
font-size: 14px;
}
.input-wide { width: 240px; }
.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 { padding-top: 20px; }
.table-container { padding: 0 20px 20px; }
.table-header-row { display: flex; flex-direction: row; background-color: #f8f8f9; border-bottom: 1px solid #e8eaec; }
.th { padding: 12px 10px; font-size: 14px; color: #515a6e; font-weight: bold; }
.table-row { display: flex; flex-direction: row; border-bottom: 1px solid #e8eaec; }
.table-row:hover { background-color: #ebf7ff; }
.td { padding: 12px 10px; display: flex; align-items: center; }
.td-txt { font-size: 14px; color: #515a6e; }
.price-txt { color: #515a6e; }
.time-txt { color: #515a6e; }
.status-ic-success { color: #2d8cf0; font-weight: bold; font-size: 16px; margin-left: 10px; }
.status-ic-fail { color: #ed4014; font-weight: bold; font-size: 16px; margin-left: 10px; }
.status-used { color: #999; }
/* 分页 */
.pagination-footer {
padding: 20px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 12px;
border-top: 1px solid #f0f0f0;
}
.total-txt { font-size: 14px; color: #606266; }
.page-val { font-size: 14px; color: #606266; border: 1px solid #dcdfe6; padding: 4px 10px; border-radius: 4px; }
.page-btns { display: flex; flex-direction: row; gap: 8px; }
.p-btn {
width: 32px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px;
display: flex; align-items: center; justify-content: center; font-size: 14px; color: #666;
}
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
.p-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; }
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
.jump-txt { font-size: 14px; color: #606266; }
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 14px; }
</style>

View File

@@ -0,0 +1,480 @@
<template>
<view class="admin-marketing-integral-statistic">
<view class="content-body">
<!-- 顶部时间选择 -->
<view class="filter-card border-shadow">
<view class="filter-item">
<text class="label-txt">时间选择:</text>
<view class="date-picker-mock">
<text class="calendar-ic">📅</text>
<text class="date-range">2026/01/05 - 2026/02/03</text>
</view>
</view>
</view>
<!-- 核心指标卡片 -->
<view class="stats-row">
<view class="stat-card border-shadow">
<view class="sc-left bg-blue">
<text class="sc-icon">💠</text>
</view>
<view class="sc-right">
<text class="sc-val">744904340.25</text>
<text class="sc-label">当前积分</text>
</view>
</view>
<view class="stat-card border-shadow">
<view class="sc-left bg-orange">
<text class="sc-icon">🪙</text>
</view>
<view class="sc-right">
<text class="sc-val">59026484</text>
<text class="sc-label">累计总积分</text>
</view>
</view>
<view class="stat-card border-shadow">
<view class="sc-left bg-green">
<text class="sc-icon">💎</text>
</view>
<view class="sc-right">
<text class="sc-val">3189</text>
<text class="sc-label">累计消耗积分</text>
</view>
</view>
</view>
<!-- 积分使用趋势 -->
<view class="chart-card border-shadow">
<view class="chart-header">
<text class="chart-title">积分使用趋势</text>
<view class="chart-legend">
<view class="legend-item">
<view class="l-dot bg-blue-line"></view>
<text class="l-txt">积分积累</text>
</view>
<view class="legend-item">
<view class="l-dot bg-green-line"></view>
<text class="l-txt">积分消耗</text>
</view>
<text class="down-ic">📥</text>
</view>
</view>
<!-- Mock 线图 -->
<view class="line-chart-box">
<view class="y-labels">
<text class="y-txt">3,500,000</text>
<text class="y-txt">3,000,000</text>
<text class="y-txt">2,500,000</text>
<text class="y-txt">2,000,000</text>
<text class="y-txt">1,500,000</text>
<text class="y-txt">1,000,000</text>
<text class="y-txt">500,000</text>
<text class="y-txt">0</text>
</view>
<view class="chart-area">
<view class="chart-grid">
<view v-for="i in 7" :key="i" class="grid-line"></view>
</view>
<!-- 线条绘制 (简单 Mock) -->
<view class="line-svg-mock">
<view class="trend-path"></view>
</view>
<view class="x-labels">
<text class="x-txt" v-for="date in dates" :key="date">{{ date }}</text>
</view>
</view>
</view>
</view>
<!-- 底部两个分析卡片 -->
<view class="bottom-analysis">
<!-- 积分来源分析 -->
<view class="analysis-card border-shadow">
<view class="analysis-header">
<text class="ah-title">积分来源分析</text>
<view class="btn-toggle" @click="toggleSourceStyle">
<text class="toggle-txt">切换样式</text>
</view>
</view>
<view class="analysis-content">
<!-- 饼图样式 -->
<view v-if="sourceStyle === 'pie'" class="pie-layout anim-fade">
<view class="pie-chart-wrap">
<view class="pie-circle"></view>
<view class="pie-label-line">
<text class="pl-txt">后台赠送</text>
</view>
</view>
<view class="pie-legend-list">
<view v-for="item in sourceData" :key="item.label" class="p-leg-item">
<view class="p-dot" :style="{backgroundColor: item.color}"></view>
<text class="p-txt">{{ item.label }}</text>
</view>
</view>
</view>
<!-- 列表样式 -->
<view v-else class="list-layout anim-fade">
<view class="list-head">
<text class="lh-col" style="width: 50px;">来源</text>
<text class="lh-col" style="flex: 1; text-align: center;">金额</text>
<text class="lh-col" style="width: 200px; text-align: right;">占比率</text>
</view>
<view class="list-body">
<view v-for="(item, index) in sourceData" :key="item.label" class="list-row">
<view class="lr-rank"><text class="rank-txt">{{ index + 1 }}</text></view>
<text class="lr-label">{{ item.label }}</text>
<text class="lr-val">{{ item.value }}</text>
<view class="lr-progress-box">
<view class="prog-bg">
<view class="prog-inner" :style="{width: item.percent + '%'}"></view>
</view>
<text class="prog-txt">{{ item.percent }}%</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 积分消耗分析 -->
<view class="analysis-card border-shadow">
<view class="analysis-header">
<text class="ah-title">积分消耗</text>
<view class="btn-toggle" @click="toggleConsumeStyle">
<text class="toggle-txt">切换样式</text>
</view>
</view>
<view class="analysis-content">
<!-- 饼图样式 -->
<view v-if="consumeStyle === 'pie'" class="pie-layout anim-fade">
<view class="pie-chart-wrap consume-pie">
<view class="pie-circle-c"></view>
<view class="pie-label-line-c">
<text class="pl-txt">订单抵扣</text>
</view>
</view>
<view class="pie-legend-list">
<view v-for="item in consumeData" :key="item.label" class="p-leg-item">
<view class="p-dot" :style="{backgroundColor: item.color}"></view>
<text class="p-txt">{{ item.label }}</text>
</view>
</view>
</view>
<!-- 列表样式 -->
<view v-else class="list-layout anim-fade">
<view class="list-head">
<text class="lh-col" style="width: 50px;">来源</text>
<text class="lh-col" style="flex: 1; text-align: center;">金额</text>
<text class="lh-col" style="width: 200px; text-align: right;">占比率</text>
</view>
<view class="list-body">
<view v-for="(item, index) in consumeData" :key="item.label" class="list-row">
<view class="lr-rank"><text class="rank-txt">{{ index + 1 }}</text></view>
<text class="lr-label">{{ item.label }}</text>
<text class="lr-val">{{ item.value }}</text>
<view class="lr-progress-box">
<view class="prog-bg">
<view class="prog-inner" :style="{width: item.percent + '%'}"></view>
</view>
<text class="prog-txt">{{ item.percent }}%</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
const dates = ['01-05', '01-06', '01-07', '01-08', '01-09', '01-10', '01-11', '01-12', '01-13', '01-14', '01-15', '01-16', '01-17', '01-18', '01-19', '01-20', '01-21', '01-22', '01-23', '01-24', '01-25', '01-26', '01-27', '01-28', '01-29', '01-30', '01-31', '02-01', '02-02', '02-03']
const sourceStyle = ref('pie')
const consumeStyle = ref('pie')
const sourceData = [
{ label: '后台赠送', value: 59021632, percent: 100, color: '#778899' },
{ label: '签到获得', value: 3620, percent: 0, color: '#FFB980' },
{ label: '九宫格抽奖', value: 0, percent: 0, color: '#FF7F50' },
{ label: '商品赠送', value: 0, percent: 0, color: '#5AB1EF' },
{ label: '订单赠送', value: 0, percent: 0, color: '#2EC7C9' }
]
const consumeData = [
{ label: '订单抵扣', value: 3051, percent: 95.7, color: '#5AB1EF' },
{ label: '九宫格抽奖', value: 138, percent: 4.3, color: '#2EC7C9' },
{ label: '兑换商品', value: 0, percent: 0, color: '#FF7F50' },
{ label: '后台减少', value: 0, percent: 0, color: '#FFB980' },
{ label: '退款退回', value: 0, percent: 0, color: '#D87A80' }
]
const toggleSourceStyle = () => {
sourceStyle.value = sourceStyle.value === 'pie' ? 'list' : 'pie'
}
const toggleConsumeStyle = () => {
consumeStyle.value = consumeStyle.value === 'pie' ? 'list' : 'pie'
}
</script>
<style scoped lang="scss">
.admin-marketing-integral-statistic {
background-color: #f0f2f5;
min-height: 100vh;
padding: 24px;
}
.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;
}
/* 时间选择 */
.filter-card {
padding: 24px;
display: flex;
}
.filter-item { display: flex; flex-direction: row; align-items: center; gap: 12px; }
.label-txt { font-size: 14px; color: #606266; }
.date-picker-mock {
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 5px 15px;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.calendar-ic { font-size: 16px; color: #999; }
.date-range { font-size: 14px; color: #333; }
/* 核心卡片 */
.stats-row {
display: flex;
flex-direction: row;
gap: 20px;
}
.stat-card {
flex: 1;
display: flex;
flex-direction: row;
padding: 24px;
align-items: center;
}
.sc-left {
width: 64px;
height: 64px;
border-radius: 32px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;
}
.sc-icon { font-size: 28px; color: #fff; }
.bg-blue { background-color: #409eff; }
.bg-orange { background-color: #ff9900; }
.bg-green { background-color: #19be6b; }
.sc-right { display: flex; flex-direction: column; }
.sc-val { font-size: 28px; font-weight: bold; color: #333; margin-bottom: 5px; }
.sc-label { font-size: 14px; color: #999; }
/* 趋势图 */
.chart-card {
padding: 24px;
display: flex;
flex-direction: column;
}
.chart-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.chart-title { font-size: 16px; font-weight: bold; color: #333; }
.chart-legend { display: flex; flex-direction: row; align-items: center; gap: 20px; }
.legend-item { display: flex; flex-direction: row; align-items: center; gap: 8px; }
.l-dot { width: 12px; height: 12px; border-radius: 6px; }
.bg-blue-line { background-color: #409eff; }
.bg-green-line { background-color: #19be6b; }
.l-txt { font-size: 12px; color: #666; }
.down-ic { font-size: 18px; color: #999; cursor: pointer; }
.line-chart-box {
display: flex;
flex-direction: row;
height: 400px;
}
.y-labels {
width: 80px;
display: flex;
flex-direction: column;
justify-content: space-between;
padding-bottom: 30px;
}
.y-txt { font-size: 12px; color: #999; text-align: right; padding-right: 10px; }
.chart-area {
flex: 1;
border-left: 1px solid #eee;
border-bottom: 1px solid #eee;
position: relative;
}
.chart-grid {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.grid-line { height: 1px; background-color: #f5f5f5; width: 100%; }
.line-svg-mock {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
overflow: hidden;
}
/* 趋势线条 Mock用一个带有波浪背景的视图模拟实际项目中应使用 Chart 库 */
.trend-path {
position: absolute;
width: 100%;
height: 100%;
background: radial-gradient(circle at 20% 40%, transparent 10%, #409eff 10.5%, #409eff 11%, transparent 11.5%);
background-size: 40px 100%;
opacity: 0.1; /* 仅作为占位示意 */
}
.x-labels {
position: absolute;
bottom: -30px;
left: 0; right: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.x-txt { font-size: 10px; color: #999; transform: rotate(-45deg); white-space: nowrap; }
/* 底部两个分析 */
.bottom-analysis {
display: flex;
flex-direction: row;
gap: 20px;
}
.analysis-card {
flex: 1;
padding: 24px;
}
.analysis-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.ah-title { font-size: 16px; font-weight: bold; color: #333; }
.btn-toggle {
border: 1px solid #dcdfe6;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
}
.toggle-txt { font-size: 12px; color: #666; }
.analysis-content {
min-height: 350px;
}
/* 饼图样式布局 */
.pie-layout {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
padding-top: 20px;
}
.pie-chart-wrap {
width: 240px;
height: 240px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.pie-circle {
width: 180px;
height: 180px;
border-radius: 90px;
background-color: #778899; /* 主体颜色 */
box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
}
.pie-circle-c {
width: 180px;
height: 180px;
border-radius: 90px;
background: conic-gradient(#5AB1EF 0 95%, #2EC7C9 95% 100%);
}
.pie-label-line {
position: absolute;
right: 0;
top: 50%;
border-top: 1px solid #999;
width: 40px;
padding-top: 5px;
}
.pl-txt { font-size: 12px; color: #666; white-space: nowrap; }
.pie-legend-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.p-leg-item { display: flex; flex-direction: row; align-items: center; gap: 10px; }
.p-dot { width: 10px; height: 10px; border-radius: 2px; }
.p-txt { font-size: 13px; color: #666; }
/* 列表样式布局 */
.list-layout { display: flex; flex-direction: column; }
.list-head {
display: flex;
flex-direction: row;
background-color: #f8f8f9;
padding: 12px;
border-radius: 4px;
}
.lh-col { font-size: 14px; font-weight: bold; color: #515a6e; }
.list-row {
display: flex;
flex-direction: row;
align-items: center;
padding: 15px 12px;
border-bottom: 1px solid #f0f0f0;
}
.lr-rank { width: 30px; height: 30px; display: flex; align-items: center; }
.rank-txt { font-size: 14px; color: #999; }
.lr-label { width: 100px; font-size: 14px; color: #333; }
.lr-val { flex: 1; font-size: 14px; color: #333; text-align: center; }
.lr-progress-box { width: 200px; display: flex; flex-direction: row; align-items: center; gap: 10px; justify-content: flex-end; }
.prog-bg { flex: 1; height: 10px; background-color: #f5f5f5; border-radius: 5px; overflow: hidden; }
.prog-inner { height: 100%; background-color: #2d8cf0; border-radius: 5px; }
.prog-txt { font-size: 13px; color: #666; width: 40px; text-align: right; }
.anim-fade { animation: fadeIn 0.3s ease-in-out; }
@keyframes fadeIn { from { opacity: 0; transform: scale(0.98); } to { opacity: 1; transform: scale(1); } }
</style>