完成代码路径重构

This commit is contained in:
2026-03-18 08:36:49 +08:00
parent 4041933e42
commit c2cd6dcd95
290 changed files with 866 additions and 38459 deletions

View File

@@ -1,400 +0,0 @@
<template>
<view class="admin-marketing-integral-product">
<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="date-picker-mock">
<text class="calendar-ic">📅</text>
<text class="date-placeholder">开始日期 - 结束日期</text>
</view>
</view>
<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="请输入商品名称, ID" v-model="searchQuery" />
</view>
<view class="btn-query" @click="handleSearch">
<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 th-id">ID</view>
<view class="th th-img">商品图片</view>
<view class="th th-title">活动标题</view>
<view class="th th-integral">兑换积分</view>
<view class="th th-limit">限量</view>
<view class="th th-remain">限量剩余</view>
<view class="th th-time">创建时间</view>
<view class="th th-sort">排序</view>
<view class="th th-status">状态</view>
<view class="th th-ops">操作</view>
</view>
<view class="table-body">
<view v-for="(item, index) in productList" :key="item.id" class="table-row">
<view class="td td-id"><text class="td-txt">{{ item.id }}</text></view>
<view class="td td-img">
<image class="product-thumb" :src="item.image" mode="aspectFill"></image>
</view>
<view class="td td-title">
<text class="title-txt line-clamp-2">{{ item.title }}</text>
</view>
<view class="td td-integral"><text class="td-txt">{{ item.integral }}</text></view>
<view class="td td-limit"><text class="td-txt">{{ item.limit }}</text></view>
<view class="td td-remain"><text class="td-txt">{{ item.remain }}</text></view>
<view class="td td-time"><text class="td-txt-small">{{ item.createTime }}</text></view>
<view class="td td-sort"><text class="td-txt">{{ item.sort }}</text></view>
<view class="td td-status">
<view :class="['switch-box', item.status ? 'active' : '']" @click="toggleStatus(index)">
<view class="switch-dot"></view>
</view>
</view>
<view class="td td-ops">
<view class="op-links">
<text class="op-link">兑换记录</text>
<text class="op-split">|</text>
<text class="op-link" @click="handleEdit(item)">编辑</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>
<!-- 分页 -->
<CommonPagination
v-if="total > 0"
:total="total"
:loading="false"
:currentPage="currentPage"
:pageSize="pageSize"
:pageSizeOptionLabels="pageSizeOptionLabels"
:pageSizeIndex="pageSizeIndex"
:visiblePages="visiblePages"
:totalPage="totalPage"
:jumpPageInput="jumpPageInput"
@page-size-change="handlePageSizeChange"
@page-change="handlePageChange"
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
@jump-page="handleJumpPage"
/>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, computed } from 'vue'
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
interface ProductItem {
id: number
image: string
title: string
integral: number
limit: number
remain: number
createTime: string
sort: number
status: boolean
}
const searchQuery = ref('')
const total = ref(3)
const productList = ref<ProductItem[]>([
{
id: 48,
image: 'https://img14.360buyimg.com/n1/jfs/t1/172605/32/17036/114175/609a473eE6997455c/df82c6168e36712b.jpg',
title: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
integral: 0,
limit: 4,
remain: 0,
createTime: '2025-10-24 14:29:19',
sort: 9999,
status: true
},
{
id: 43,
image: 'https://img12.360buyimg.com/n1/jfs/t1/185449/19/11995/4379/60d96d27E6a877c8e/3c38d4e92a2a7a5a.jpg',
title: '阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FQ5270 传奇水蓝/传...',
integral: 100,
limit: 1,
remain: 0,
createTime: '2025-05-13 15:37:46',
sort: 9998,
status: true
},
{
id: 44,
image: 'https://img13.360buyimg.com/n1/jfs/t1/192173/5/11913/21447/60e57e95Ef82688f3/bc875f643e8c95a3.jpg',
title: '劳伦斯意式极简大平层设计师款直排真皮沙发简约客厅别墅大小户型',
integral: 6860,
limit: 1,
remain: 0,
createTime: '2025-05-13 15:38:02',
sort: 9996,
status: true
}
])
const handleSearch = () => { console.log('Searching...') }
const handleAdd = () => { console.log('Adding...') }
const handleEdit = (item: ProductItem) => { console.log('Editing...', item.id) }
const toggleStatus = (index: number) => {
productList.value[index].status = !productList.value[index].status
}
// 分页适配状态
const currentPage = ref(1)
const pageSize = ref(15)
let jumpPageInput = ''
const pageSizeOptions = [10, 15, 20, 30, 50]
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
const pageSizeIndex = computed(() => {
const idx = pageSizeOptions.indexOf(pageSize.value)
return idx >= 0 ? idx : 0
})
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
const visiblePages = computed(() => {
const t = totalPage.value
const cur = currentPage.value
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
return [1, -1, cur - 1, cur, cur + 1, -1, t]
})
const handlePageChange = (p: number) => { currentPage.value = p }
const handlePageSizeChange = (e: any) => {
const idx = Number(e.detail.value)
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
currentPage.value = 1
}
const handleJumpPage = () => {
const p = parseInt(jumpPageInput)
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
}
</script>
<style scoped lang="scss">
.admin-marketing-integral-product {
padding: 0;
background-color: transparent;
min-height: auto;
}
.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: 24px;
}
.filter-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.label-txt { font-size: 14px; color: #606266; white-space: nowrap; }
.date-picker-mock {
width: 280px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 12px;
gap: 8px;
}
.calendar-ic { font-size: 14px; color: #c0c4cc; }
.date-placeholder { font-size: 13px; color: #c0c4cc; }
.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;
}
.select-val { font-size: 14px; color: #c0c4cc; }
.arrow-down { font-size: 10px; color: #c0c4cc; }
.search-input {
width: 220px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
font-size: 13px;
}
.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;
}
.card-header { padding: 20px; }
.btn-primary-blue {
background-color: #2d8cf0;
padding: 8px 16px;
border-radius: 4px;
display: inline-flex;
}
.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;
display: flex;
align-items: center;
}
.table-row {
display: flex;
flex-direction: row;
border-bottom: 1px solid #e8eaec;
align-items: center;
}
.td {
padding: 12px 10px;
display: flex;
align-items: center;
}
.td-txt { font-size: 14px; color: #515a6e; }
.td-txt-small { font-size: 13px; color: #515a6e; }
/* 列表各列宽度控制 */
.th-id, .td-id { width: 60px; }
.th-img, .td-img { width: 80px; }
.th-title, .td-title { flex: 1; min-width: 200px; }
.th-integral, .td-integral { width: 100px; }
.th-limit, .td-limit { width: 80px; }
.th-remain, .td-remain { width: 100px; }
.th-time, .td-time { width: 160px; }
.th-sort, .td-sort { width: 80px; }
.th-status, .td-status { width: 80px; }
.th-ops, .td-ops { width: 220px; justify-content: flex-end; }
.product-thumb {
width: 50px;
height: 50px;
border-radius: 4px;
background-color: #f5f5f5;
}
.title-txt { font-size: 13px; color: #333; line-height: 1.5; }
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
/* Switch 开关 */
.switch-box {
width: 44px;
height: 22px;
background-color: #dcdfe6;
border-radius: 11px;
position: relative;
transition: background-color 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: transform 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: 13px; cursor: pointer; margin: 0 5px; }
.op-split { color: #e8eaec; margin: 0 5px; }
.text-danger { color: #ed4014; }
/* 分页区域已迁至 CommonPagination 组件 */
</style>

View File

@@ -1,400 +0,0 @@
<template>
<view class="admin-marketing-integral-product">
<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="date-picker-mock">
<text class="calendar-ic">📅</text>
<text class="date-placeholder">开始日期 - 结束日期</text>
</view>
</view>
<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="请输入商品名称, ID" v-model="searchQuery" />
</view>
<view class="btn-query" @click="handleSearch">
<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 th-id">ID</view>
<view class="th th-img">商品图片</view>
<view class="th th-title">活动标题</view>
<view class="th th-integral">兑换积分</view>
<view class="th th-limit">限量</view>
<view class="th th-remain">限量剩余</view>
<view class="th th-time">创建时间</view>
<view class="th th-sort">排序</view>
<view class="th th-status">状态</view>
<view class="th th-ops">操作</view>
</view>
<view class="table-body">
<view v-for="(item, index) in productList" :key="item.id" class="table-row">
<view class="td td-id"><text class="td-txt">{{ item.id }}</text></view>
<view class="td td-img">
<image class="product-thumb" :src="item.image" mode="aspectFill"></image>
</view>
<view class="td td-title">
<text class="title-txt line-clamp-2">{{ item.title }}</text>
</view>
<view class="td td-integral"><text class="td-txt">{{ item.integral }}</text></view>
<view class="td td-limit"><text class="td-txt">{{ item.limit }}</text></view>
<view class="td td-remain"><text class="td-txt">{{ item.remain }}</text></view>
<view class="td td-time"><text class="td-txt-small">{{ item.createTime }}</text></view>
<view class="td td-sort"><text class="td-txt">{{ item.sort }}</text></view>
<view class="td td-status">
<view :class="['switch-box', item.status ? 'active' : '']" @click="toggleStatus(index)">
<view class="switch-dot"></view>
</view>
</view>
<view class="td td-ops">
<view class="op-links">
<text class="op-link">兑换记录</text>
<text class="op-split">|</text>
<text class="op-link" @click="handleEdit(item)">编辑</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>
<!-- 分页 -->
<CommonPagination
v-if="total > 0"
:total="total"
:loading="false"
:currentPage="currentPage"
:pageSize="pageSize"
:pageSizeOptionLabels="pageSizeOptionLabels"
:pageSizeIndex="pageSizeIndex"
:visiblePages="visiblePages"
:totalPage="totalPage"
:jumpPageInput="jumpPageInput"
@page-size-change="handlePageSizeChange"
@page-change="handlePageChange"
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
@jump-page="handleJumpPage"
/>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, computed } from 'vue'
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
interface ProductItem {
id: number
image: string
title: string
integral: number
limit: number
remain: number
createTime: string
sort: number
status: boolean
}
const searchQuery = ref('')
const total = ref(3)
const productList = ref<ProductItem[]>([
{
id: 48,
image: 'https://img14.360buyimg.com/n1/jfs/t1/172605/32/17036/114175/609a473eE6997455c/df82c6168e36712b.jpg',
title: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
integral: 0,
limit: 4,
remain: 0,
createTime: '2025-10-24 14:29:19',
sort: 9999,
status: true
},
{
id: 43,
image: 'https://img12.360buyimg.com/n1/jfs/t1/185449/19/11995/4379/60d96d27E6a877c8e/3c38d4e92a2a7a5a.jpg',
title: '阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FQ5270 传奇水蓝/传...',
integral: 100,
limit: 1,
remain: 0,
createTime: '2025-05-13 15:37:46',
sort: 9998,
status: true
},
{
id: 44,
image: 'https://img13.360buyimg.com/n1/jfs/t1/192173/5/11913/21447/60e57e95Ef82688f3/bc875f643e8c95a3.jpg',
title: '劳伦斯意式极简大平层设计师款直排真皮沙发简约客厅别墅大小户型',
integral: 6860,
limit: 1,
remain: 0,
createTime: '2025-05-13 15:38:02',
sort: 9996,
status: true
}
])
const handleSearch = () => { console.log('Searching...') }
const handleAdd = () => { console.log('Adding...') }
const handleEdit = (item: ProductItem) => { console.log('Editing...', item.id) }
const toggleStatus = (index: number) => {
productList.value[index].status = !productList.value[index].status
}
// 分页适配状态
const currentPage = ref(1)
const pageSize = ref(15)
let jumpPageInput = ''
const pageSizeOptions = [10, 15, 20, 30, 50]
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
const pageSizeIndex = computed(() => {
const idx = pageSizeOptions.indexOf(pageSize.value)
return idx >= 0 ? idx : 0
})
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
const visiblePages = computed(() => {
const t = totalPage.value
const cur = currentPage.value
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
return [1, -1, cur - 1, cur, cur + 1, -1, t]
})
const handlePageChange = (p: number) => { currentPage.value = p }
const handlePageSizeChange = (e: any) => {
const idx = Number(e.detail.value)
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
currentPage.value = 1
}
const handleJumpPage = () => {
const p = parseInt(jumpPageInput)
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
}
</script>
<style scoped lang="scss">
.admin-marketing-integral-product {
padding: 0;
background-color: transparent;
min-height: auto;
}
.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: 24px;
}
.filter-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.label-txt { font-size: 14px; color: #606266; white-space: nowrap; }
.date-picker-mock {
width: 280px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 12px;
gap: 8px;
}
.calendar-ic { font-size: 14px; color: #c0c4cc; }
.date-placeholder { font-size: 13px; color: #c0c4cc; }
.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;
}
.select-val { font-size: 14px; color: #c0c4cc; }
.arrow-down { font-size: 10px; color: #c0c4cc; }
.search-input {
width: 220px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
font-size: 13px;
}
.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;
}
.card-header { padding: 20px; }
.btn-primary-blue {
background-color: #2d8cf0;
padding: 8px 16px;
border-radius: 4px;
display: inline-flex;
}
.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;
display: flex;
align-items: center;
}
.table-row {
display: flex;
flex-direction: row;
border-bottom: 1px solid #e8eaec;
align-items: center;
}
.td {
padding: 12px 10px;
display: flex;
align-items: center;
}
.td-txt { font-size: 14px; color: #515a6e; }
.td-txt-small { font-size: 13px; color: #515a6e; }
/* 列表各列宽度控制 */
.th-id, .td-id { width: 60px; }
.th-img, .td-img { width: 80px; }
.th-title, .td-title { flex: 1; min-width: 200px; }
.th-integral, .td-integral { width: 100px; }
.th-limit, .td-limit { width: 80px; }
.th-remain, .td-remain { width: 100px; }
.th-time, .td-time { width: 160px; }
.th-sort, .td-sort { width: 80px; }
.th-status, .td-status { width: 80px; }
.th-ops, .td-ops { width: 220px; justify-content: flex-end; }
.product-thumb {
width: 50px;
height: 50px;
border-radius: 4px;
background-color: #f5f5f5;
}
.title-txt { font-size: 13px; color: #333; line-height: 1.5; }
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
/* Switch 开关 */
.switch-box {
width: 44px;
height: 22px;
background-color: #dcdfe6;
border-radius: 11px;
position: relative;
transition: background-color 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: transform 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: 13px; cursor: pointer; margin: 0 5px; }
.op-split { color: #e8eaec; margin: 0 5px; }
.text-danger { color: #ed4014; }
/* 分页区域已迁至 CommonPagination 组件 */
</style>

View File

@@ -1,400 +0,0 @@
<template>
<view class="admin-marketing-integral-product">
<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="date-picker-mock">
<text class="calendar-ic">📅</text>
<text class="date-placeholder">开始日期 - 结束日期</text>
</view>
</view>
<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="请输入商品名称, ID" v-model="searchQuery" />
</view>
<view class="btn-query" @click="handleSearch">
<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 th-id">ID</view>
<view class="th th-img">商品图片</view>
<view class="th th-title">活动标题</view>
<view class="th th-integral">兑换积分</view>
<view class="th th-limit">限量</view>
<view class="th th-remain">限量剩余</view>
<view class="th th-time">创建时间</view>
<view class="th th-sort">排序</view>
<view class="th th-status">状态</view>
<view class="th th-ops">操作</view>
</view>
<view class="table-body">
<view v-for="(item, index) in productList" :key="item.id" class="table-row">
<view class="td td-id"><text class="td-txt">{{ item.id }}</text></view>
<view class="td td-img">
<image class="product-thumb" :src="item.image" mode="aspectFill"></image>
</view>
<view class="td td-title">
<text class="title-txt line-clamp-2">{{ item.title }}</text>
</view>
<view class="td td-integral"><text class="td-txt">{{ item.integral }}</text></view>
<view class="td td-limit"><text class="td-txt">{{ item.limit }}</text></view>
<view class="td td-remain"><text class="td-txt">{{ item.remain }}</text></view>
<view class="td td-time"><text class="td-txt-small">{{ item.createTime }}</text></view>
<view class="td td-sort"><text class="td-txt">{{ item.sort }}</text></view>
<view class="td td-status">
<view :class="['switch-box', item.status ? 'active' : '']" @click="toggleStatus(index)">
<view class="switch-dot"></view>
</view>
</view>
<view class="td td-ops">
<view class="op-links">
<text class="op-link">兑换记录</text>
<text class="op-split">|</text>
<text class="op-link" @click="handleEdit(item)">编辑</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>
<!-- 分页 -->
<CommonPagination
v-if="total > 0"
:total="total"
:loading="false"
:currentPage="currentPage"
:pageSize="pageSize"
:pageSizeOptionLabels="pageSizeOptionLabels"
:pageSizeIndex="pageSizeIndex"
:visiblePages="visiblePages"
:totalPage="totalPage"
:jumpPageInput="jumpPageInput"
@page-size-change="handlePageSizeChange"
@page-change="handlePageChange"
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
@jump-page="handleJumpPage"
/>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, computed } from 'vue'
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
interface ProductItem {
id: number
image: string
title: string
integral: number
limit: number
remain: number
createTime: string
sort: number
status: boolean
}
const searchQuery = ref('')
const total = ref(3)
const productList = ref<ProductItem[]>([
{
id: 48,
image: 'https://img14.360buyimg.com/n1/jfs/t1/172605/32/17036/114175/609a473eE6997455c/df82c6168e36712b.jpg',
title: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
integral: 0,
limit: 4,
remain: 0,
createTime: '2025-10-24 14:29:19',
sort: 9999,
status: true
},
{
id: 43,
image: 'https://img12.360buyimg.com/n1/jfs/t1/185449/19/11995/4379/60d96d27E6a877c8e/3c38d4e92a2a7a5a.jpg',
title: '阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FQ5270 传奇水蓝/传...',
integral: 100,
limit: 1,
remain: 0,
createTime: '2025-05-13 15:37:46',
sort: 9998,
status: true
},
{
id: 44,
image: 'https://img13.360buyimg.com/n1/jfs/t1/192173/5/11913/21447/60e57e95Ef82688f3/bc875f643e8c95a3.jpg',
title: '劳伦斯意式极简大平层设计师款直排真皮沙发简约客厅别墅大小户型',
integral: 6860,
limit: 1,
remain: 0,
createTime: '2025-05-13 15:38:02',
sort: 9996,
status: true
}
])
const handleSearch = () => { console.log('Searching...') }
const handleAdd = () => { console.log('Adding...') }
const handleEdit = (item: ProductItem) => { console.log('Editing...', item.id) }
const toggleStatus = (index: number) => {
productList.value[index].status = !productList.value[index].status
}
// 分页适配状态
const currentPage = ref(1)
const pageSize = ref(15)
let jumpPageInput = ''
const pageSizeOptions = [10, 15, 20, 30, 50]
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
const pageSizeIndex = computed(() => {
const idx = pageSizeOptions.indexOf(pageSize.value)
return idx >= 0 ? idx : 0
})
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
const visiblePages = computed(() => {
const t = totalPage.value
const cur = currentPage.value
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
return [1, -1, cur - 1, cur, cur + 1, -1, t]
})
const handlePageChange = (p: number) => { currentPage.value = p }
const handlePageSizeChange = (e: any) => {
const idx = Number(e.detail.value)
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
currentPage.value = 1
}
const handleJumpPage = () => {
const p = parseInt(jumpPageInput)
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
}
</script>
<style scoped lang="scss">
.admin-marketing-integral-product {
padding: 0;
background-color: transparent;
min-height: auto;
}
.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: 24px;
}
.filter-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.label-txt { font-size: 14px; color: #606266; white-space: nowrap; }
.date-picker-mock {
width: 280px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 12px;
gap: 8px;
}
.calendar-ic { font-size: 14px; color: #c0c4cc; }
.date-placeholder { font-size: 13px; color: #c0c4cc; }
.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;
}
.select-val { font-size: 14px; color: #c0c4cc; }
.arrow-down { font-size: 10px; color: #c0c4cc; }
.search-input {
width: 220px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
font-size: 13px;
}
.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;
}
.card-header { padding: 20px; }
.btn-primary-blue {
background-color: #2d8cf0;
padding: 8px 16px;
border-radius: 4px;
display: inline-flex;
}
.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;
display: flex;
align-items: center;
}
.table-row {
display: flex;
flex-direction: row;
border-bottom: 1px solid #e8eaec;
align-items: center;
}
.td {
padding: 12px 10px;
display: flex;
align-items: center;
}
.td-txt { font-size: 14px; color: #515a6e; }
.td-txt-small { font-size: 13px; color: #515a6e; }
/* 列表各列宽度控制 */
.th-id, .td-id { width: 60px; }
.th-img, .td-img { width: 80px; }
.th-title, .td-title { flex: 1; min-width: 200px; }
.th-integral, .td-integral { width: 100px; }
.th-limit, .td-limit { width: 80px; }
.th-remain, .td-remain { width: 100px; }
.th-time, .td-time { width: 160px; }
.th-sort, .td-sort { width: 80px; }
.th-status, .td-status { width: 80px; }
.th-ops, .td-ops { width: 220px; justify-content: flex-end; }
.product-thumb {
width: 50px;
height: 50px;
border-radius: 4px;
background-color: #f5f5f5;
}
.title-txt { font-size: 13px; color: #333; line-height: 1.5; }
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
/* Switch 开关 */
.switch-box {
width: 44px;
height: 22px;
background-color: #dcdfe6;
border-radius: 11px;
position: relative;
transition: background-color 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: transform 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: 13px; cursor: pointer; margin: 0 5px; }
.op-split { color: #e8eaec; margin: 0 5px; }
.text-danger { color: #ed4014; }
/* 分页区域已迁至 CommonPagination 组件 */
</style>

View File

@@ -1,348 +0,0 @@
<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">
<text class="down-ic">📥</text>
</view>
</view>
<view class="chart-body">
<AnalyticsMultiLineChart :xLabels="dates" :series="trendSeries" :height="350" />
</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-new anim-fade">
<AnalyticsPieChart :items="sourceData" :height="300" />
</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-new anim-fade">
<AnalyticsPieChart :items="consumeData" :height="300" />
</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'
import AnalyticsPieChart from '@/components/analytics/AnalyticsPieChart.uvue'
import AnalyticsMultiLineChart from '@/components/analytics/AnalyticsMultiLineChart.uvue'
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 trendSeries = [
{
name: '积分积累',
data: [120, 132, 101, 134, 90, 230, 210, 182, 191, 234, 290, 330, 310, 220, 182, 191, 234, 290, 330, 310, 220, 182, 191, 234, 290, 330, 310, 220, 182, 191],
color: '#409eff'
},
{
name: '积分消耗',
data: [220, 182, 191, 234, 290, 330, 310, 120, 132, 101, 134, 90, 230, 210, 120, 132, 101, 134, 90, 230, 210, 120, 132, 101, 134, 90, 230, 210, 120, 132],
color: '#19be6b'
}
]
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 {
padding: 0;
background-color: transparent;
min-height: auto;
}
.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; }
.down-ic { font-size: 18px; color: #999; cursor: pointer; }
.chart-body {
width: 100%;
}
/* 底部两个分析 */
.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-new {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* 列表样式布局 */
.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>