调整目录结构

This commit is contained in:
2026-03-09 17:57:44 +08:00
parent 80acd0fd2e
commit 912087fa75
17 changed files with 2085 additions and 1133 deletions

View File

@@ -48,11 +48,11 @@ import OrderConfig from '@/pages/mall/admin/order/order-configuration/index.uvue
// --- 营销模块 --- // --- 营销模块 ---
import MarketingCouponList from '@/pages/mall/admin/marketing/coupon/list.uvue' import MarketingCouponList from '@/pages/mall/admin/marketing/coupon/list.uvue'
import MarketingCouponUser from '@/pages/mall/admin/marketing/coupon/user.uvue' import MarketingCouponUser from '@/pages/mall/admin/marketing/coupon/user.uvue'
import MarketingIntegralStatistic from '@/pages/mall/admin/marketing/integral/statistic.uvue' import MarketingIntegralStatistic from '@/pages/mall/admin/marketing/points/statistic.uvue'
import MarketingIntegralProduct from '@/pages/mall/admin/marketing/integral/list.uvue' import MarketingIntegralProduct from '@/pages/mall/admin/marketing/points/list.uvue'
import MarketingIntegralOrder from '@/pages/mall/admin/marketing/integral/order.uvue' import MarketingIntegralOrder from '@/pages/mall/admin/marketing/points/order.uvue'
import MarketingIntegralRecord from '@/pages/mall/admin/marketing/integral/record.uvue' import MarketingIntegralRecord from '@/pages/mall/admin/marketing/points/record.uvue'
import MarketingIntegralConfig from '@/pages/mall/admin/marketing/integral/config.uvue' import MarketingIntegralConfig from '@/pages/mall/admin/marketing/points/config.uvue'
import MarketingLotteryList from '@/pages/mall/admin/marketing/lottery/list.uvue' import MarketingLotteryList from '@/pages/mall/admin/marketing/lottery/list.uvue'
import MarketingLotteryConfig from '@/pages/mall/admin/marketing/lottery/config.uvue' import MarketingLotteryConfig from '@/pages/mall/admin/marketing/lottery/config.uvue'
import MarketingCombinationProduct from '@/pages/mall/admin/marketing/combination/product.uvue' import MarketingCombinationProduct from '@/pages/mall/admin/marketing/combination/product.uvue'

View File

@@ -475,7 +475,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'marketing_integral_statistic', id: 'marketing_integral_statistic',
title: '积分统计', title: '积分统计',
path: '/pages/mall/admin/marketing/integral/statistic', path: '/pages/mall/admin/marketing/points/statistic',
componentKey: 'MarketingIntegralStatistic', componentKey: 'MarketingIntegralStatistic',
parentId: 'marketing', parentId: 'marketing',
groupId: 'marketing-integral', groupId: 'marketing-integral',
@@ -485,7 +485,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'marketing_integral_product', id: 'marketing_integral_product',
title: '积分商品', title: '积分商品',
path: '/pages/mall/admin/marketing/integral/product', path: '/pages/mall/admin/marketing/points/product',
componentKey: 'MarketingIntegralProduct', componentKey: 'MarketingIntegralProduct',
parentId: 'marketing', parentId: 'marketing',
groupId: 'marketing-integral', groupId: 'marketing-integral',
@@ -495,7 +495,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'marketing_integral_order', id: 'marketing_integral_order',
title: '积分订单', title: '积分订单',
path: '/pages/mall/admin/marketing/integral/order', path: '/pages/mall/admin/marketing/points/order',
componentKey: 'MarketingIntegralOrder', componentKey: 'MarketingIntegralOrder',
parentId: 'marketing', parentId: 'marketing',
groupId: 'marketing-integral', groupId: 'marketing-integral',
@@ -505,7 +505,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'marketing_integral_record', id: 'marketing_integral_record',
title: '积分记录', title: '积分记录',
path: '/pages/mall/admin/marketing/integral/record', path: '/pages/mall/admin/marketing/points/record',
componentKey: 'MarketingIntegralRecord', componentKey: 'MarketingIntegralRecord',
parentId: 'marketing', parentId: 'marketing',
groupId: 'marketing-integral', groupId: 'marketing-integral',
@@ -515,7 +515,7 @@ export const routes: RouteRecord[] = [
{ {
id: 'marketing_integral_config', id: 'marketing_integral_config',
title: '积分配置', title: '积分配置',
path: '/pages/mall/admin/marketing/integral/config', path: '/pages/mall/admin/marketing/points/config',
componentKey: 'MarketingIntegralConfig', componentKey: 'MarketingIntegralConfig',
parentId: 'marketing', parentId: 'marketing',
groupId: 'marketing-integral', groupId: 'marketing-integral',

View File

@@ -1,4 +1,4 @@
--- ---
🚧 注意: 🚧 注意:
⚠ 注意:当前使用 mock 数据,后续真实接口完成后替换 ⚠ 注意:当前使用 mock 数据,后续真实接口完成后替换
@@ -1947,6 +1947,23 @@ const iconMap: Record<string, string> = {
- **CRMEB 路由映射**: 1:1 复刻 CRMEB 的路由和菜单结构 - **CRMEB 路由映射**: 1:1 复刻 CRMEB 的路由和菜单结构
- **双侧边栏布局**: 主侧边栏(一级) + 二级侧边栏(分组) - **双侧边栏布局**: 主侧边栏(一级) + 二级侧边栏(分组)
## 🎯 阶段十八: Vue/Vite 编译失败导致的连锁依赖雪崩 (500 错误与动态导入阻断)
### **原因三十二SCSS 括号闭合错误引发的 ?import 连锁报错**
- **现象**:
1. 浏览器控制台出现核心组件的 SCSS 编译失败GET /pages/mall/admin/product/product-management/index.uvue?...&lang.scss 500
2. 随后出现警告:[Vue warn]: Unhandled error during execution of async component loader
3. 最终报错阻断页面级加载TypeError: Failed to fetch dynamically imported module: /pages/mall/admin/homePage/index.uvue?import
- **原因**:
在修改或合并页面(如整合目录结构)时,不慎破坏了 <style lang="scss"> 其中的结构(例如留下了一个多余的闭合大括号 } 或丢失了 </style>)。由于 Vite 处理 uni-app-x 时是按块编译的CSS 预处理报错会导致服务端直接向该组件抛出 **500 错误**。
在 "内部路由/状态驱动" 模式下,我们的系统依赖 dminComponentMap.uts 全量静态扫描所有的管理页面。一旦链路树中的某个子节点(例如 product-management/index.uvue发生了 500 编译失败,会导致整个模块依赖树发生雪崩。父级页面(如引了全局 Layout 的 homePage/index.uvue会因为底层的依赖断裂无法组装出正确的 JS 模块,最终导致 **动态导入失败** 的假象。
- **解决方案**:
1. **禁止盲目改路由****绝对不要**因为看到 homePage 报错就去重写 homePage 或者怀疑路由表配错了。
2. **顺藤摸瓜找源头**:沿着浏览器 Network 或者 Console 错误的最顶部往上翻,找到第一个且唯一一个抛出 500 的资源(在本例中是 lang.scss
3. **修复语法树**:回到那个触发 500 的文件,检查并修复 emplate、script、style 标签的闭锁以及其内部(特别是 SCSS 嵌套)的语法错误(如括号配对)。语法自洽后,整个异步组件树便会瞬间全量恢复正常。
- **防止复发规范**: 当执行文件全局批量替换或目录大迁移后,切勿遗留未闭合的代码块。修复问题必须采用“由底向外”的收敛原则。
--- ---
这个指南现在涵盖了 uni-app-x 项目开发中最常见的 17 类问题(新增动态导入与语法遮蔽解析),为后续开发提供了完整的故障排除和最佳实践指导。 🚀 这个指南现在涵盖了 uni-app-x 项目开发中最常见的 17 类问题(新增动态导入与语法遮蔽解析),为后续开发提供了完整的故障排除和最佳实践指导。 🚀
@@ -2188,4 +2205,5 @@ curl -i -X OPTIONS "http://192.168.1.61:9122/rest/v1/ml_coupon_templates?select=
--- ---
这个指南现在涵盖了 uni-app-x 项目开发中最常见的 32 类问题(包含全局组件化最佳实践),为后续开发提供了完整的故障排除和标准化指导。 🚀 这个指南现在涵盖了 uni-app-x 项目开发中最常见的 33 类问题(包含全局组件化最佳实践),为后续开发提供了完整的故障排除和标准化指导。 🚀

View File

@@ -1,264 +0,0 @@
<template>
<view class="admin-marketing-integral-config">
<view class="config-card box-shadow">
<view class="tabs-container">
<view class="tab-item" :class="{ active: activeTab == 0 }" @click="activeTab = 0">基础配置</view>
<view class="tab-item" :class="{ active: activeTab == 1 }" @click="activeTab = 1">抵扣配置</view>
<view class="tab-item" :class="{ active: activeTab == 2 }" @click="activeTab = 2">过期配置</view>
</view>
<view class="config-body">
<!-- 基础配置 -->
<view v-if="activeTab == 0" class="config-section">
<view class="form-item">
<view class="item-label">积分名称:</view>
<view class="item-content">
<input class="admin-input" v-model="form.integral_name" placeholder="积分" />
<text class="desc">用户看到的积分名称,如:金豆、积分</text>
</view>
</view>
<view class="form-item">
<view class="item-label">积分赠送单位:</view>
<view class="item-content">
<input class="admin-input-long" type="number" v-model="form.integral_unit" />
<text class="desc">下单消费1元赠送多少积分</text>
</view>
</view>
<view class="form-item">
<view class="item-label">积分冻结时间:</view>
<view class="item-content">
<view class="input-with-unit">
<input class="admin-input-unit" type="number" v-model="form.freeze_time" />
<text class="unit">天</text>
</view>
<text class="desc">订单确认收货后,赠送积分需要冻结多少天转为可用积分</text>
</view>
</view>
</view>
<!-- 抵扣配置 -->
<view v-if="activeTab == 1" class="config-section">
<view class="form-item">
<view class="item-label">积分抵扣比例:</view>
<view class="item-content">
<view class="input-with-unit">
<input class="admin-input-unit" v-model="form.integral_ratio" />
<text class="unit">元</text>
</view>
<text class="desc">1积分可抵扣多少元</text>
</view>
</view>
<view class="form-item">
<view class="item-label">积分抵扣上限:</view>
<view class="item-content">
<view class="input-with-unit">
<input class="admin-input-unit" type="number" v-model="form.integral_max" />
<text class="unit">%</text>
</view>
<text class="desc">单笔订单最高可抵扣金额比例0-100</text>
</view>
</view>
</view>
<!-- 过期配置 -->
<view v-if="activeTab == 2" class="config-section">
<view class="form-item">
<view class="item-label">积分有效期:</view>
<view class="item-content">
<radio-group class="radio-group" @change="validTypeChange">
<label class="radio-label">
<radio value="0" :checked="form.valid_type == 0" /> 永久有效
</label>
<label class="radio-label">
<radio value="1" :checked="form.valid_type == 1" /> 固定时长
</label>
</radio-group>
</view>
</view>
<view class="form-item" v-if="form.valid_type == 1">
<view class="item-label">固定时长:</view>
<view class="item-content">
<view class="input-with-unit">
<input class="admin-input-unit" type="number" v-model="form.valid_year" />
<text class="unit">年</text>
</view>
<text class="desc">从积分获得日期起,多少年后过期</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="form-ops">
<button class="submit-btn" @click="handleSubmit">保存设置</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive } from 'vue'
const activeTab = ref(0)
const form = reactive({
integral_name: '积分',
integral_unit: 10,
integral_ratio: 0.1,
integral_max: 50,
freeze_time: 7,
valid_type: 0,
valid_year: 1
})
const validTypeChange = (e: any) => {
form.valid_type = parseInt(e.detail.value as string)
}
const handleSubmit = () => {
uni.showLoading({ title: '保存中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: '保存成功', icon: 'success' })
}, 1000)
}
</script>
<style scoped>
.box-shadow {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
}
.config-card {
max-width: 1000px;
margin: 0 auto;
overflow: hidden;
min-height: 650px;
}
/* 顶部标签 */
.tabs-container {
display: flex;
flex-direction: row;
border-bottom: 1px solid #e8eaec;
padding: 0 20px;
}
.tab-item {
padding: 15px 25px;
font-size: 14px;
color: #515a6e;
cursor: pointer;
position: relative;
}
.tab-item.active {
color: #2d8cf0;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #2d8cf0;
}
.config-body {
padding: 40px 60px;
min-height: 550px;
}
.form-item {
display: flex;
flex-direction: row;
margin-bottom: 25px;
align-items: flex-start;
}
.item-label {
width: 140px;
font-size: 14px;
color: #515a6e;
text-align: right;
padding-right: 20px;
line-height: 32px;
}
.item-content {
flex: 1;
}
.admin-input, .admin-input-long {
width: 320px;
height: 32px;
border: 1px solid #dcdee2;
border-radius: 4px;
padding: 0 10px;
font-size: 14px;
}
.input-with-unit {
width: 320px;
display: flex;
flex-direction: row;
align-items: center;
border: 1px solid #dcdee2;
border-radius: 4px;
overflow: hidden;
}
.admin-input-unit {
flex: 1;
height: 32px;
padding: 0 10px;
font-size: 14px;
border: none;
}
.unit {
padding: 0 12px;
background-color: #f8f8f9;
border-left: 1px solid #dcdee2;
font-size: 12px;
color: #808695;
height: 32px;
line-height: 32px;
}
.desc {
display: block;
font-size: 12px;
color: #c5c8ce;
margin-top: 8px;
}
.radio-group {
display: flex;
flex-direction: row;
height: 32px;
align-items: center;
}
.radio-label {
margin-right: 20px;
font-size: 14px;
color: #515a6e;
display: flex;
flex-direction: row;
align-items: center;
}
.form-ops {
margin-top: 50px;
padding-left: 140px;
}
.submit-btn {
width: 120px;
height: 36px;
line-height: 36px;
background-color: #2d8cf0;
color: #fff;
font-size: 14px;
border: none;
border-radius: 4px;
}
</style>

View File

@@ -1,412 +0,0 @@
<template>
<view class="admin-marketing-integral-order">
<!-- 顶部状态选项卡 -->
<view class="status-tabs box-shadow">
<view v-for="(item, index) in statusOptions" :key="index"
:class="['tab-item', currentStatus == item.value ? 'active' : '']"
@click="currentStatus = item.value">
<text class="tab-label">{{ item.label }}</text>
<text class="tab-count">({{ item.count }})</text>
</view>
</view>
<!-- 筛选区域 -->
<view class="filter-card box-shadow">
<view class="filter-row">
<view class="filter-item">
<text class="label">订单状态:</text>
<picker :range="statusOptions" range-key="label" @change="statusChange">
<view class="picker-value">{{ getStatusLabel(currentStatus) }} <text class="iconfont icon-arrow-down"></text></view>
</picker>
</view>
<view class="filter-item">
<text class="label">创建时间:</text>
<view class="date-range-mock">
<text class="date-text">全部</text>
<text class="iconfont icon-calendar"></text>
</view>
</view>
<view class="filter-item search-box">
<input class="admin-input" placeholder="订单号/收货人/电话" v-model="searchQuery" />
<button class="admin-btn admin-btn-primary search-btn" @click="handleSearch">搜索</button>
<button class="admin-btn admin-btn-default reset-btn" @click="handleReset">重置</button>
</view>
</view>
</view>
<!-- 工具栏 -->
<view class="table-toolbar">
<button class="admin-btn admin-btn-default">订单导出</button>
</view>
<!-- 表格区域 -->
<view class="table-card box-shadow">
<view class="table-header">
<text class="col-200">订单号</text>
<text class="col-150">用户信息</text>
<text class="col-250">商品信息</text>
<text class="col-100">兑换积分</text>
<text class="col-100">订单状态</text>
<text class="col-180">下单时间</text>
<text class="col-120 center">操作</text>
</view>
<view class="table-body">
<view v-for="(item, index) in tableData" :key="index" class="table-row">
<view class="col-200">
<text class="order-id-text">{{ item.orderId }}</text>
</view>
<view class="col-150">
<text class="user-nickname-uid">{{ item.nickname }}/{{ item.uid }}</text>
</view>
<view class="col-250 product-cell">
<view class="tabBox">
<image class="tabBox_img" :src="item.image" mode="aspectFill"></image>
<view class="tabBox_right">
<text class="tabBox_tit">{{ item.storeName }} | {{ item.sku }}</text>
<text class="tabBox_pice">积分{{ item.integral }} x {{ item.num }}</text>
</view>
</view>
</view>
<view class="col-100">
<text class="integral-val">{{ item.integral }}</text>
</view>
<view class="col-100">
<text class="status-text">{{ getStatusText(item.status) }}</text>
</view>
<view class="col-180">
<text class="time-text">{{ item.addTime }}</text>
</view>
<view class="col-120 center ops-cell">
<text class="op-link" v-if="item.status === 0" @click="handleShip(item)">发送货</text>
<text class="op-link" @click="handleMore(item)">更多 <text class="iconfont icon-arrow-down" style="font-size: 10px;"></text></text>
</view>
</view>
</view>
<!-- 分页 -->
<view class="table-pagination">
<text class="total-text">共 {{ total }} 条</text>
<view class="page-ops">
<button class="page-btn" disabled>上一页</button>
<text class="current-page">1</text>
<button class="page-btn">下一页</button>
</view>
</view>
</view>
</view>
</template>
<script uts>
export default {
data() {
return {
currentStatus: -1,
searchQuery: '',
total: 8,
statusOptions: [
{ label: '全部', value: -1, count: 8 },
{ label: '待发货', value: 0, count: 6 },
{ label: '待收货', value: 1, count: 1 },
{ label: '待评价', value: 2, count: 0 },
{ label: '已完成', value: 3, count: 1 }
] as any[],
tableData: [
{
orderId: 'wx719261472118538240',
nickname: '故事的小黄花',
uid: 80674,
storeName: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
sku: 'L,卡其',
image: '/static/logo.png',
integral: 200,
num: 1,
status: 0,
addTime: '2025-11-10 18:48:18'
},
{
orderId: 'wx717772249846775808',
nickname: '蓝桥春雪',
uid: 80582,
storeName: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
sku: 'M,卡其',
image: '/static/logo.png',
integral: 200,
num: 1,
status: 0,
addTime: '2025-11-06 16:10:39'
},
{
orderId: 'wx717763196697444352',
nickname: './',
uid: 78504,
storeName: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
sku: 'S,卡其',
image: '/static/logo.png',
integral: 200,
num: 1,
status: 0,
addTime: '2025-11-06 15:34:41'
},
{
orderId: 'wx714911802768490496',
nickname: '177****1167',
uid: 36391,
storeName: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
sku: 'XL,卡其',
image: '/static/logo.png',
integral: 0,
num: 1,
status: 0,
addTime: '2025-10-29 18:44:16'
},
{
orderId: 'wx665016819706232832',
nickname: '陈修然',
uid: 75658,
storeName: 'MLB官方 男女情侣软顶棒球帽明星同款运动帽遮阳鸭舌帽休闲CP19',
sku: 'XS,纽约洋基队/绿色',
image: '/static/logo.png',
integral: 299,
num: 1,
status: 0,
addTime: '2025-06-14 02:19:25'
}
] as any[]
}
},
methods: {
getStatusLabel(val : number) : string {
const item = this.statusOptions.find((opt: any) => opt.value === val)
return item ? item.label : '全部'
},
getStatusText(status : number) : string {
switch(status) {
case 0: return '未发货';
case 1: return '待收货';
case 2: return '待评价';
case 3: return '已完成';
default: return '未知';
}
},
handleShip(item: any) {
uni.showToast({ title: '去发货: ' + item.orderId, icon: 'none' })
},
handleMore(item: any) {
uni.showActionSheet({
itemList: ['订单详情', '订单记录', '订单备注'],
success: (res) => {
uni.showToast({ title: '点击了' + res.tapIndex, icon: 'none' })
}
})
},
statusChange(e : any) {
this.currentStatus = this.statusOptions[e.detail.value].value as number
},
handleSearch() {
uni.showToast({ title: '查询', icon: 'none' })
},
handleReset() {
this.searchQuery = ''
this.currentStatus = -1
}
}
}
</script>
<style scoped>
.admin-marketing-integral-order {
padding: 0;
background-color: transparent;
min-height: auto;
}
.box-shadow {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
margin-bottom: 16px;
}
/* 顶部状态卡片 */
.status-tabs {
display: flex;
flex-direction: row;
padding: 0 10px;
}
.tab-item {
padding: 15px 20px;
display: flex;
flex-direction: row;
align-items: center;
position: relative;
cursor: pointer;
}
.tab-item.active {
color: #2d8cf0;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #2d8cf0;
}
.tab-label {
font-size: 14px;
}
.tab-count {
font-size: 12px;
margin-left: 4px;
opacity: 0.8;
}
/* 筛选卡片 */
.filter-card {
padding: 20px;
}
.filter-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.filter-item {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 24px;
margin-bottom: 5px;
}
.label {
font-size: 14px;
color: #606266;
white-space: nowrap;
}
.date-range-mock {
width: 280px;
height: 32px;
padding: 0 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: #fff;
}
.date-text { font-size: 13px; color: #c0c4cc; }
.admin-input {
width: 200px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 10px;
font-size: 14px;
}
.search-btn {
margin-left: 10px;
}
/* 表格区域 */
.table-card {
overflow: hidden;
}
.table-header {
display: flex;
flex-direction: row;
background-color: #f8f8f9;
border-bottom: 1px solid #e8eaec;
padding: 12px 10px;
}
.table-header text {
font-size: 14px;
font-weight: bold;
color: #515a6e;
}
.table-row {
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid #e8eaec;
padding: 15px 10px;
}
/* 列宽定义 */
.col-100 { width: 100px; }
.col-120 { width: 120px; }
.col-150 { width: 150px; }
.col-180 { width: 180px; }
.col-200 { width: 200px; }
.col-250 { width: 250px; flex: 1; }
.center { text-align: center; justify-content: center; }
.order-id-text { font-size: 13px; color: #515a6e; }
.user-nickname-uid { font-size: 13px; color: #515a6e; }
/* 商品信息列 */
.product-cell {
display: flex;
flex-direction: row;
}
.tabBox {
display: flex;
flex-direction: row;
align-items: center;
}
.tabBox_img {
width: 36px;
height: 36px;
border-radius: 2px;
margin-right: 8px;
}
.tabBox_right {
display: flex;
flex-direction: column;
}
.tabBox_tit {
font-size: 12px;
color: #515a6e;
line-height: 1.4;
}
.tabBox_pice {
font-size: 12px;
color: #808695;
margin-top: 2px;
}
.integral-val { font-size: 13px; color: #515a6e; }
.status-text { font-size: 13px; color: #515a6e; }
.time-text { font-size: 13px; color: #515a6e; }
.ops-cell {
display: flex;
flex-direction: row;
}
.op-link {
font-size: 13px;
color: #2d8cf0;
margin: 0 8px;
cursor: pointer;
}
/* 分页 */
.table-pagination {
padding: 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
.total-text { font-size: 14px; color: #515a6e; margin-right: 15px; }
</style>

View File

@@ -1,302 +0,0 @@
<template>
<view class="admin-marketing-integral-record">
<!-- 筛选 -->
<view class="box-shadow filter-card">
<view class="filter-row">
<view class="filter-item">
<text class="label">时间选择:</text>
<view class="date-range-mock">
<text class="date-text">全部</text>
<text class="iconfont icon-calendar" style="font-size: 16px; color: #c0c4cc;"></text>
</view>
</view>
<view class="filter-item">
<text class="label">交易类型:</text>
<picker mode="selector" :range="typeOptions" range-key="label" @change="typeChange">
<view class="admin-input-picker">
<text>{{ typeLabel }}</text>
<text class="iconfont icon-arrow-down" style="font-size: 14px; color: #c0c4cc;"></text>
</view>
</picker>
</view>
<view class="filter-item">
<text class="label">搜索:</text>
<input class="admin-input" v-model="searchQuery" placeholder="请输入昵称" />
<button class="search-btn btn-primary" @click="handleSearch">搜索</button>
<button class="reset-btn btn-default" @click="handleReset">重置</button>
</view>
</view>
</view>
<!-- 表格 -->
<view class="box-shadow table-card">
<view class="table-header">
<view class="col-80"><text>ID</text></view>
<view class="col-150"><text>关联订单</text></view>
<view class="col-150"><text>交易时间</text></view>
<view class="col-100"><text>交易积分</text></view>
<view class="col-120"><text>用户</text></view>
<view class="col-120"><text>交易类型</text></view>
<view class="col-150"><text>备注</text></view>
<view class="col-100"><text>备注</text></view>
</view>
<view class="table-content">
<view v-for="(item, index) in tableData" :key="index" class="table-row">
<view class="col-80"><text class="cell-text">{{ item.id }}</text></view>
<view class="col-150"><text class="cell-link">{{ item.relation || '-' }}</text></view>
<view class="col-150"><text class="cell-text">{{ item.add_time }}</text></view>
<view class="col-100">
<text :class="item.pm == 1 ? 'z-price' : 'f-price'">{{ item.pm == 1 ? '+' : '-' }} {{ item.number }}</text>
</view>
<view class="col-120"><text class="cell-text">{{ item.nickname }}</text></view>
<view class="col-120"><text class="cell-text">{{ item.type_name }}</text></view>
<view class="col-150"><text class="cell-text mark-text">{{ item.mark }}</text></view>
<view class="col-100">
<text class="op-link" @click="handleMark(item)">修改备注</text>
</view>
</view>
</view>
<!-- 分页 -->
<view class="table-pagination">
<text class="total-text">共 {{ total }} 条</text>
<view class="page-ops">
<button class="page-btn" disabled>上一页</button>
<text class="current-page">1</text>
<button class="page-btn">下一页</button>
</view>
</view>
</view>
</view>
</template>
<script uts>
export default {
data() {
return {
searchQuery: '',
currentType: '',
typeOptions: [
{ label: '全部', value: '' },
{ label: '积分订单', value: 'integral_order' },
{ label: '签到奖励', value: 'sign' },
{ label: '后台充值', value: 'system' }
] as any[],
total: 5,
tableData: [
{
id: 1256,
relation: '',
add_time: '2025-11-12 09:30',
number: 10,
pm: 1,
nickname: '故事的小黄花',
type_name: '签到奖励',
mark: '连续签到3天奖励'
},
{
id: 1255,
relation: 'wx719261472118538240',
add_time: '2025-11-10 18:48',
number: 200,
pm: 0,
nickname: '故事的小黄花',
type_name: '积分兑换',
mark: '兑换商品扣除'
},
{
id: 1254,
relation: '',
add_time: '2025-11-05 14:20',
number: 1000,
pm: 1,
nickname: '蓝桥春雪',
type_name: '后台充值',
mark: '管理员手动充值'
}
] as any[]
}
},
computed: {
typeLabel(): string {
const found = this.typeOptions.find((o: any): boolean => o.value == this.currentType)
return found != null ? (found['label'] as string) : '全部'
}
},
methods: {
typeChange(e: any) {
this.currentType = this.typeOptions[e.detail.value].value as string
},
handleSearch() {
uni.showToast({ title: '搜索: ' + this.searchQuery, icon: 'none' })
},
handleReset() {
this.searchQuery = ''
this.currentType = ''
},
handleMark(item: any) {
uni.showToast({ title: '修改备注: ' + item.id, icon: 'none' })
}
}
}
</script>
<style scoped>
.admin-marketing-integral-record {
padding: 0;
background-color: transparent;
min-height: auto;
}
.box-shadow {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
margin-bottom: 16px;
}
.filter-card {
padding: 20px;
}
.filter-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.filter-item {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 24px;
margin-bottom: 5px;
}
.label {
font-size: 14px;
color: #606266;
white-space: nowrap;
}
.date-range-mock {
width: 200px;
height: 32px;
padding: 0 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: #fff;
}
.date-text { font-size: 13px; color: #c0c4cc; }
.admin-input-picker {
width: 150px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 10px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.admin-input-picker text { font-size: 14px; color: #606266; }
.admin-input {
width: 200px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 10px;
font-size: 14px;
}
.search-btn, .reset-btn {
margin-left: 10px;
height: 32px;
line-height: 32px;
padding: 0 15px;
font-size: 14px;
border-radius: 4px;
}
.btn-primary { background-color: #2d8cf0; color: #fff; border: none; }
.btn-default { background-color: #fff; color: #606266; border: 1px solid #dcdfe6; }
/* 表格 */
.table-card {
overflow-x: auto;
}
.table-header {
display: flex;
flex-direction: row;
background-color: #f8f8f9;
border-bottom: 1px solid #e8eaec;
padding: 12px 10px;
}
.table-header text {
font-size: 14px;
font-weight: bold;
color: #515a6e;
}
.table-row {
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid #e8eaec;
padding: 15px 10px;
}
.col-80 { width: 80px; }
.col-100 { width: 100px; }
.col-120 { width: 120px; }
.col-150 { width: 150px; }
.col-180 { width: 180px; }
.cell-text { font-size: 13px; color: #515a6e; }
.cell-link { font-size: 13px; color: #2d8cf0; }
.mark-text { color: #808695; }
.z-price { font-size: 14px; color: #19be6b; font-weight: bold; }
.f-price { font-size: 14px; color: #ed4014; font-weight: bold; }
.op-link {
font-size: 13px;
color: #2d8cf0;
cursor: pointer;
}
/* 分页 */
.table-pagination {
padding: 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
.total-text { font-size: 14px; color: #515a6e; margin-right: 15px; }
.page-ops {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.page-btn {
height: 32px;
padding: 0 12px;
font-size: 14px;
border-radius: 2px;
border: 1px solid #d9d9d9;
background: #fff;
margin: 0;
}
.current-page {
padding: 0 12px;
font-size: 14px;
color: #2d8cf0;
}
</style>

View File

@@ -1,24 +1,264 @@
<template> <template>
<view class="page"> <view class="admin-marketing-integral-config">
<view class="header"> <view class="config-card box-shadow">
<text class="title">{{ title }}</text> <view class="tabs-container">
<text class="sub-title">页面占位 (自动生成)</text> <view class="tab-item" :class="{ active: activeTab == 0 }" @click="activeTab = 0">基础配置</view>
<view class="tab-item" :class="{ active: activeTab == 1 }" @click="activeTab = 1">抵扣配置</view>
<view class="tab-item" :class="{ active: activeTab == 2 }" @click="activeTab = 2">过期配置</view>
</view>
<view class="config-body">
<!-- 基础配置 -->
<view v-if="activeTab == 0" class="config-section">
<view class="form-item">
<view class="item-label">积分名称:</view>
<view class="item-content">
<input class="admin-input" v-model="form.integral_name" placeholder="积分" />
<text class="desc">用户看到的积分名称,如:金豆、积分</text>
</view>
</view>
<view class="form-item">
<view class="item-label">积分赠送单位:</view>
<view class="item-content">
<input class="admin-input-long" type="number" v-model="form.integral_unit" />
<text class="desc">下单消费1元赠送多少积分</text>
</view>
</view>
<view class="form-item">
<view class="item-label">积分冻结时间:</view>
<view class="item-content">
<view class="input-with-unit">
<input class="admin-input-unit" type="number" v-model="form.freeze_time" />
<text class="unit">天</text>
</view>
<text class="desc">订单确认收货后,赠送积分需要冻结多少天转为可用积分</text>
</view>
</view>
</view>
<!-- 抵扣配置 -->
<view v-if="activeTab == 1" class="config-section">
<view class="form-item">
<view class="item-label">积分抵扣比例:</view>
<view class="item-content">
<view class="input-with-unit">
<input class="admin-input-unit" v-model="form.integral_ratio" />
<text class="unit">元</text>
</view>
<text class="desc">1积分可抵扣多少元</text>
</view>
</view>
<view class="form-item">
<view class="item-label">积分抵扣上限:</view>
<view class="item-content">
<view class="input-with-unit">
<input class="admin-input-unit" type="number" v-model="form.integral_max" />
<text class="unit">%</text>
</view>
<text class="desc">单笔订单最高可抵扣金额比例0-100</text>
</view>
</view>
</view>
<!-- 过期配置 -->
<view v-if="activeTab == 2" class="config-section">
<view class="form-item">
<view class="item-label">积分有效期:</view>
<view class="item-content">
<radio-group class="radio-group" @change="validTypeChange">
<label class="radio-label">
<radio value="0" :checked="form.valid_type == 0" /> 永久有效
</label>
<label class="radio-label">
<radio value="1" :checked="form.valid_type == 1" /> 固定时长
</label>
</radio-group>
</view>
</view>
<view class="form-item" v-if="form.valid_type == 1">
<view class="item-label">固定时长:</view>
<view class="item-content">
<view class="input-with-unit">
<input class="admin-input-unit" type="number" v-model="form.valid_year" />
<text class="unit">年</text>
</view>
<text class="desc">从积分获得日期起,多少年后过期</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="form-ops">
<button class="submit-btn" @click="handleSubmit">保存设置</button>
</view>
</view> </view>
</view> </view>
</view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import { ref } from 'vue' import { ref, reactive } from 'vue'
const currentPage = ref<string>('points-config')
const title = ref<string>('config') const activeTab = ref(0)
const form = reactive({
integral_name: '积分',
integral_unit: 10,
integral_ratio: 0.1,
integral_max: 50,
freeze_time: 7,
valid_type: 0,
valid_year: 1
})
const validTypeChange = (e: any) => {
form.valid_type = parseInt(e.detail.value as string)
}
const handleSubmit = () => {
uni.showLoading({ title: '保存中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: '保存成功', icon: 'success' })
}, 1000)
}
</script> </script>
<style scoped lang="scss"> <style scoped>
@import '@/uni.scss'; .box-shadow {
.page { padding: 0; } background-color: #fff;
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; } border-radius: 4px;
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; } box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; } }
.config-card {
max-width: 1000px;
margin: 0 auto;
overflow: hidden;
min-height: 650px;
}
/* 顶部标签 */
.tabs-container {
display: flex;
flex-direction: row;
border-bottom: 1px solid #e8eaec;
padding: 0 20px;
}
.tab-item {
padding: 15px 25px;
font-size: 14px;
color: #515a6e;
cursor: pointer;
position: relative;
}
.tab-item.active {
color: #2d8cf0;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #2d8cf0;
}
.config-body {
padding: 40px 60px;
min-height: 550px;
}
.form-item {
display: flex;
flex-direction: row;
margin-bottom: 25px;
align-items: flex-start;
}
.item-label {
width: 140px;
font-size: 14px;
color: #515a6e;
text-align: right;
padding-right: 20px;
line-height: 32px;
}
.item-content {
flex: 1;
}
.admin-input, .admin-input-long {
width: 320px;
height: 32px;
border: 1px solid #dcdee2;
border-radius: 4px;
padding: 0 10px;
font-size: 14px;
}
.input-with-unit {
width: 320px;
display: flex;
flex-direction: row;
align-items: center;
border: 1px solid #dcdee2;
border-radius: 4px;
overflow: hidden;
}
.admin-input-unit {
flex: 1;
height: 32px;
padding: 0 10px;
font-size: 14px;
border: none;
}
.unit {
padding: 0 12px;
background-color: #f8f8f9;
border-left: 1px solid #dcdee2;
font-size: 12px;
color: #808695;
height: 32px;
line-height: 32px;
}
.desc {
display: block;
font-size: 12px;
color: #c5c8ce;
margin-top: 8px;
}
.radio-group {
display: flex;
flex-direction: row;
height: 32px;
align-items: center;
}
.radio-label {
margin-right: 20px;
font-size: 14px;
color: #515a6e;
display: flex;
flex-direction: row;
align-items: center;
}
.form-ops {
margin-top: 50px;
padding-left: 140px;
}
.submit-btn {
width: 120px;
height: 36px;
line-height: 36px;
background-color: #2d8cf0;
color: #fff;
font-size: 14px;
border: none;
border-radius: 4px;
}
</style> </style>

View File

@@ -1,22 +1,398 @@
<template> <template>
<view class="page"> <view class="admin-marketing-integral-product">
<view class="header"> <view class="content-body">
<text class="title">{{ title }}</text> <!-- 搜索过滤栏 -->
<text class="sub-title">页面已修复 (UTF-8)</text> <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>
<!-- 分页 -->
<view class="pagination-footer">
<view class="page-total">
<text class="total-txt">共 {{ total }} 条</text>
</view>
<view class="page-select">
<text class="page-val">15条/页 ▼</text>
</view>
<view class="page-btns">
<text class="p-btn"><</text>
<text class="p-btn active">1</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> </view>
</view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import { ref } from 'vue' import { ref, reactive } from 'vue'
const currentPage = ref<string>('goods')
const title = ref<string>('goods') 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
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/uni.scss'; .admin-marketing-integral-product {
.page { padding: 0; } padding: 0;
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; } background-color: transparent;
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; } min-height: auto;
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; } }
.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; }
/* 分页 */
.pagination-footer {
padding: 20px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
.total-txt { font-size: 13px; color: #606266; }
.page-val { font-size: 13px; 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 #e8eaec;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
color: #666;
}
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
.jump-txt { font-size: 13px; color: #606266; }
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 13px; }
</style> </style>

View File

@@ -1,92 +1,398 @@
<template> <template>
<view class="Page"> <view class="admin-marketing-integral-product">
<view class="Header"> <view class="content-body">
<text class="Title">积分统计</text> <!-- 搜索过滤栏 -->
<text class="SubTitle">marketing/points/index</text> <view class="filter-card border-shadow">
</view> <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="Card"> <!-- 主要内容区域 -->
<text class="Label">页面参数query</text> <view class="table-card border-shadow">
<text class="Mono">{{ params }}</text> <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>
<!-- 分页 -->
<view class="pagination-footer">
<view class="page-total">
<text class="total-txt">共 {{ total }} 条</text>
</view>
<view class="page-select">
<text class="page-val">15条/页 ▼</text>
</view>
<view class="page-btns">
<text class="p-btn"><</text>
<text class="p-btn active">1</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>
</view> </view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import { ref, computed } from 'vue' import { ref, reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
const params = ref('') interface ProductItem {
const tab = ref('stats') id: number
image: string
title: string
integral: number
limit: number
remain: number
createTime: string
sort: number
status: boolean
}
onLoad((options) => { const searchQuery = ref('')
params.value = JSON.stringify(options ?? {}) const total = ref(3)
tab.value = options?.tab || 'stats'
})
const currentPage = computed(() => { const productList = ref<ProductItem[]>([
switch (tab.value) { {
case 'goods': return 'points-goods' id: 48,
case 'order': return 'points-order' image: 'https://img14.360buyimg.com/n1/jfs/t1/172605/32/17036/114175/609a473eE6997455c/df82c6168e36712b.jpg',
case 'record': return 'points-record' title: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
case 'config': return 'points-config' integral: 0,
case 'lottery-list': return 'lottery-list' limit: 4,
case 'lottery-config': return 'lottery-config' remain: 0,
case 'groupbuy-goods': return 'groupbuy-goods' createTime: '2025-10-24 14:29:19',
case 'groupbuy-list': return 'groupbuy-list' sort: 9999,
case 'seckill-goods': return 'seckill-goods' status: true
case 'seckill-list': return 'seckill-list' },
case 'seckill-config': return 'seckill-config' {
case 'member-type': return 'member-type' id: 43,
case 'member-rights': return 'member-rights' image: 'https://img12.360buyimg.com/n1/jfs/t1/185449/19/11995/4379/60d96d27E6a877c8e/3c38d4e92a2a7a5a.jpg',
case 'member-card': return 'member-card' title: '阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FQ5270 传奇水蓝/传...',
case 'member-record': return 'member-record' integral: 100,
case 'member-config': return 'member-config' limit: 1,
case 'live-room': return 'live-room' remain: 0,
case 'live-goods': return 'live-goods' createTime: '2025-05-13 15:37:46',
case 'live-anchor': return 'live-anchor' sort: 9998,
case 'recharge-amount': return 'recharge-amount' status: true
case 'recharge-config': return 'recharge-config' },
case 'recharge-record': return 'recharge-record' {
case 'newcomer': return 'newcomer' id: 44,
default: return 'points-stats' 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
}
</script> </script>
<style> <style scoped lang="scss">
.Page { .admin-marketing-integral-product {
padding: 0; padding: 0;
background-color: transparent;
min-height: auto;
} }
.Header {
padding: 24rpx; .border-shadow {
border-radius: 16rpx; background-color: #fff;
background: #ffffff; border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
} }
.Title {
font-size: 36rpx; .content-body {
font-weight: 700; display: flex;
flex-direction: column;
gap: 20px;
} }
.SubTitle {
margin-top: 8rpx; /* 过滤栏 */
font-size: 24rpx; .filter-card {
opacity: 0.7; padding: 24px;
} }
.Card {
margin-top: 24rpx; .filter-row {
padding: 24rpx; display: flex;
border-radius: 16rpx; flex-direction: row;
background: #ffffff; align-items: center;
flex-wrap: wrap;
gap: 24px;
} }
.Label {
font-size: 26rpx; .filter-item {
font-weight: 600; display: flex;
margin-bottom: 12rpx; flex-direction: row;
align-items: center;
gap: 8px;
} }
.Mono {
font-size: 24rpx; .label-txt { font-size: 14px; color: #606266; white-space: nowrap; }
font-family: monospace;
line-height: 36rpx; .date-picker-mock {
word-break: break-all; 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; }
/* 分页 */
.pagination-footer {
padding: 20px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
.total-txt { font-size: 13px; color: #606266; }
.page-val { font-size: 13px; 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 #e8eaec;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
color: #666;
}
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
.jump-txt { font-size: 13px; color: #606266; }
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 13px; }
</style> </style>

View File

@@ -1,25 +1,412 @@
<template> <template>
<AdminLayout :currentPage="currentPage"> <view class="admin-marketing-integral-order">
<view class="page"> <!-- 顶部状态选项卡 -->
<view class="header"> <view class="status-tabs box-shadow">
<text class="title">{{ title }}</text> <view v-for="(item, index) in statusOptions" :key="index"
<text class="sub-title">页面已修复 (UTF-8)</text> :class="['tab-item', currentStatus == item.value ? 'active' : '']"
@click="currentStatus = item.value">
<text class="tab-label">{{ item.label }}</text>
<text class="tab-count">({{ item.count }})</text>
</view> </view>
</view> </view>
</AdminLayout>
<!-- 筛选区域 -->
<view class="filter-card box-shadow">
<view class="filter-row">
<view class="filter-item">
<text class="label">订单状态:</text>
<picker :range="statusOptions" range-key="label" @change="statusChange">
<view class="picker-value">{{ getStatusLabel(currentStatus) }} <text class="iconfont icon-arrow-down"></text></view>
</picker>
</view>
<view class="filter-item">
<text class="label">创建时间:</text>
<view class="date-range-mock">
<text class="date-text">全部</text>
<text class="iconfont icon-calendar"></text>
</view>
</view>
<view class="filter-item search-box">
<input class="admin-input" placeholder="订单号/收货人/电话" v-model="searchQuery" />
<button class="admin-btn admin-btn-primary search-btn" @click="handleSearch">搜索</button>
<button class="admin-btn admin-btn-default reset-btn" @click="handleReset">重置</button>
</view>
</view>
</view>
<!-- 工具栏 -->
<view class="table-toolbar">
<button class="admin-btn admin-btn-default">订单导出</button>
</view>
<!-- 表格区域 -->
<view class="table-card box-shadow">
<view class="table-header">
<text class="col-200">订单号</text>
<text class="col-150">用户信息</text>
<text class="col-250">商品信息</text>
<text class="col-100">兑换积分</text>
<text class="col-100">订单状态</text>
<text class="col-180">下单时间</text>
<text class="col-120 center">操作</text>
</view>
<view class="table-body">
<view v-for="(item, index) in tableData" :key="index" class="table-row">
<view class="col-200">
<text class="order-id-text">{{ item.orderId }}</text>
</view>
<view class="col-150">
<text class="user-nickname-uid">{{ item.nickname }}/{{ item.uid }}</text>
</view>
<view class="col-250 product-cell">
<view class="tabBox">
<image class="tabBox_img" :src="item.image" mode="aspectFill"></image>
<view class="tabBox_right">
<text class="tabBox_tit">{{ item.storeName }} | {{ item.sku }}</text>
<text class="tabBox_pice">积分{{ item.integral }} x {{ item.num }}</text>
</view>
</view>
</view>
<view class="col-100">
<text class="integral-val">{{ item.integral }}</text>
</view>
<view class="col-100">
<text class="status-text">{{ getStatusText(item.status) }}</text>
</view>
<view class="col-180">
<text class="time-text">{{ item.addTime }}</text>
</view>
<view class="col-120 center ops-cell">
<text class="op-link" v-if="item.status === 0" @click="handleShip(item)">发送货</text>
<text class="op-link" @click="handleMore(item)">更多 <text class="iconfont icon-arrow-down" style="font-size: 10px;"></text></text>
</view>
</view>
</view>
<!-- 分页 -->
<view class="table-pagination">
<text class="total-text">共 {{ total }} 条</text>
<view class="page-ops">
<button class="page-btn" disabled>上一页</button>
<text class="current-page">1</text>
<button class="page-btn">下一页</button>
</view>
</view>
</view>
</view>
</template> </template>
<script setup lang="uts"> <script uts>
import { ref } from 'vue' export default {
import AdminLayout from '@/layouts/admin/AdminLayout.uvue' data() {
const currentPage = ref<string>('order') return {
const title = ref<string>('order') currentStatus: -1,
searchQuery: '',
total: 8,
statusOptions: [
{ label: '全部', value: -1, count: 8 },
{ label: '待发货', value: 0, count: 6 },
{ label: '待收货', value: 1, count: 1 },
{ label: '待评价', value: 2, count: 0 },
{ label: '已完成', value: 3, count: 1 }
] as any[],
tableData: [
{
orderId: 'wx719261472118538240',
nickname: '故事的小黄花',
uid: 80674,
storeName: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
sku: 'L,卡其',
image: '/static/logo.png',
integral: 200,
num: 1,
status: 0,
addTime: '2025-11-10 18:48:18'
},
{
orderId: 'wx717772249846775808',
nickname: '蓝桥春雪',
uid: 80582,
storeName: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
sku: 'M,卡其',
image: '/static/logo.png',
integral: 200,
num: 1,
status: 0,
addTime: '2025-11-06 16:10:39'
},
{
orderId: 'wx717763196697444352',
nickname: './',
uid: 78504,
storeName: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
sku: 'S,卡其',
image: '/static/logo.png',
integral: 200,
num: 1,
status: 0,
addTime: '2025-11-06 15:34:41'
},
{
orderId: 'wx714911802768490496',
nickname: '177****1167',
uid: 36391,
storeName: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
sku: 'XL,卡其',
image: '/static/logo.png',
integral: 0,
num: 1,
status: 0,
addTime: '2025-10-29 18:44:16'
},
{
orderId: 'wx665016819706232832',
nickname: '陈修然',
uid: 75658,
storeName: 'MLB官方 男女情侣软顶棒球帽明星同款运动帽遮阳鸭舌帽休闲CP19',
sku: 'XS,纽约洋基队/绿色',
image: '/static/logo.png',
integral: 299,
num: 1,
status: 0,
addTime: '2025-06-14 02:19:25'
}
] as any[]
}
},
methods: {
getStatusLabel(val : number) : string {
const item = this.statusOptions.find((opt: any) => opt.value === val)
return item ? item.label : '全部'
},
getStatusText(status : number) : string {
switch(status) {
case 0: return '未发货';
case 1: return '待收货';
case 2: return '待评价';
case 3: return '已完成';
default: return '未知';
}
},
handleShip(item: any) {
uni.showToast({ title: '去发货: ' + item.orderId, icon: 'none' })
},
handleMore(item: any) {
uni.showActionSheet({
itemList: ['订单详情', '订单记录', '订单备注'],
success: (res) => {
uni.showToast({ title: '点击了' + res.tapIndex, icon: 'none' })
}
})
},
statusChange(e : any) {
this.currentStatus = this.statusOptions[e.detail.value].value as number
},
handleSearch() {
uni.showToast({ title: '查询', icon: 'none' })
},
handleReset() {
this.searchQuery = ''
this.currentStatus = -1
}
}
}
</script> </script>
<style scoped lang="scss"> <style scoped>
@import '@/uni.scss'; .admin-marketing-integral-order {
.page { padding: $space-lg; } padding: 0;
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; } background-color: transparent;
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; } min-height: auto;
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; } }
.box-shadow {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
margin-bottom: 16px;
}
/* 顶部状态卡片 */
.status-tabs {
display: flex;
flex-direction: row;
padding: 0 10px;
}
.tab-item {
padding: 15px 20px;
display: flex;
flex-direction: row;
align-items: center;
position: relative;
cursor: pointer;
}
.tab-item.active {
color: #2d8cf0;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #2d8cf0;
}
.tab-label {
font-size: 14px;
}
.tab-count {
font-size: 12px;
margin-left: 4px;
opacity: 0.8;
}
/* 筛选卡片 */
.filter-card {
padding: 20px;
}
.filter-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.filter-item {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 24px;
margin-bottom: 5px;
}
.label {
font-size: 14px;
color: #606266;
white-space: nowrap;
}
.date-range-mock {
width: 280px;
height: 32px;
padding: 0 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: #fff;
}
.date-text { font-size: 13px; color: #c0c4cc; }
.admin-input {
width: 200px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 10px;
font-size: 14px;
}
.search-btn {
margin-left: 10px;
}
/* 表格区域 */
.table-card {
overflow: hidden;
}
.table-header {
display: flex;
flex-direction: row;
background-color: #f8f8f9;
border-bottom: 1px solid #e8eaec;
padding: 12px 10px;
}
.table-header text {
font-size: 14px;
font-weight: bold;
color: #515a6e;
}
.table-row {
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid #e8eaec;
padding: 15px 10px;
}
/* 列宽定义 */
.col-100 { width: 100px; }
.col-120 { width: 120px; }
.col-150 { width: 150px; }
.col-180 { width: 180px; }
.col-200 { width: 200px; }
.col-250 { width: 250px; flex: 1; }
.center { text-align: center; justify-content: center; }
.order-id-text { font-size: 13px; color: #515a6e; }
.user-nickname-uid { font-size: 13px; color: #515a6e; }
/* 商品信息列 */
.product-cell {
display: flex;
flex-direction: row;
}
.tabBox {
display: flex;
flex-direction: row;
align-items: center;
}
.tabBox_img {
width: 36px;
height: 36px;
border-radius: 2px;
margin-right: 8px;
}
.tabBox_right {
display: flex;
flex-direction: column;
}
.tabBox_tit {
font-size: 12px;
color: #515a6e;
line-height: 1.4;
}
.tabBox_pice {
font-size: 12px;
color: #808695;
margin-top: 2px;
}
.integral-val { font-size: 13px; color: #515a6e; }
.status-text { font-size: 13px; color: #515a6e; }
.time-text { font-size: 13px; color: #515a6e; }
.ops-cell {
display: flex;
flex-direction: row;
}
.op-link {
font-size: 13px;
color: #2d8cf0;
margin: 0 8px;
cursor: pointer;
}
/* 分页 */
.table-pagination {
padding: 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
.total-text { font-size: 14px; color: #515a6e; margin-right: 15px; }
</style> </style>

View File

@@ -1,24 +1,302 @@
<template> <template>
<view class="page"> <view class="admin-marketing-integral-record">
<view class="header"> <!-- 筛选 -->
<text class="title">{{ title }}</text> <view class="box-shadow filter-card">
<text class="sub-title">页面占位 (自动生成)</text> <view class="filter-row">
<view class="filter-item">
<text class="label">时间选择:</text>
<view class="date-range-mock">
<text class="date-text">全部</text>
<text class="iconfont icon-calendar" style="font-size: 16px; color: #c0c4cc;"></text>
</view>
</view>
<view class="filter-item">
<text class="label">交易类型:</text>
<picker mode="selector" :range="typeOptions" range-key="label" @change="typeChange">
<view class="admin-input-picker">
<text>{{ typeLabel }}</text>
<text class="iconfont icon-arrow-down" style="font-size: 14px; color: #c0c4cc;"></text>
</view>
</picker>
</view>
<view class="filter-item">
<text class="label">搜索:</text>
<input class="admin-input" v-model="searchQuery" placeholder="请输入昵称" />
<button class="search-btn btn-primary" @click="handleSearch">搜索</button>
<button class="reset-btn btn-default" @click="handleReset">重置</button>
</view>
</view> </view>
</view> </view>
<!-- 表格 -->
<view class="box-shadow table-card">
<view class="table-header">
<view class="col-80"><text>ID</text></view>
<view class="col-150"><text>关联订单</text></view>
<view class="col-150"><text>交易时间</text></view>
<view class="col-100"><text>交易积分</text></view>
<view class="col-120"><text>用户</text></view>
<view class="col-120"><text>交易类型</text></view>
<view class="col-150"><text>备注</text></view>
<view class="col-100"><text>备注</text></view>
</view>
<view class="table-content">
<view v-for="(item, index) in tableData" :key="index" class="table-row">
<view class="col-80"><text class="cell-text">{{ item.id }}</text></view>
<view class="col-150"><text class="cell-link">{{ item.relation || '-' }}</text></view>
<view class="col-150"><text class="cell-text">{{ item.add_time }}</text></view>
<view class="col-100">
<text :class="item.pm == 1 ? 'z-price' : 'f-price'">{{ item.pm == 1 ? '+' : '-' }} {{ item.number }}</text>
</view>
<view class="col-120"><text class="cell-text">{{ item.nickname }}</text></view>
<view class="col-120"><text class="cell-text">{{ item.type_name }}</text></view>
<view class="col-150"><text class="cell-text mark-text">{{ item.mark }}</text></view>
<view class="col-100">
<text class="op-link" @click="handleMark(item)">修改备注</text>
</view>
</view>
</view>
<!-- 分页 -->
<view class="table-pagination">
<text class="total-text">共 {{ total }} 条</text>
<view class="page-ops">
<button class="page-btn" disabled>上一页</button>
<text class="current-page">1</text>
<button class="page-btn">下一页</button>
</view>
</view>
</view>
</view>
</template> </template>
<script setup lang="uts"> <script uts>
import { ref } from 'vue' export default {
const currentPage = ref<string>('points-record') data() {
const title = ref<string>('record') return {
searchQuery: '',
currentType: '',
typeOptions: [
{ label: '全部', value: '' },
{ label: '积分订单', value: 'integral_order' },
{ label: '签到奖励', value: 'sign' },
{ label: '后台充值', value: 'system' }
] as any[],
total: 5,
tableData: [
{
id: 1256,
relation: '',
add_time: '2025-11-12 09:30',
number: 10,
pm: 1,
nickname: '故事的小黄花',
type_name: '签到奖励',
mark: '连续签到3天奖励'
},
{
id: 1255,
relation: 'wx719261472118538240',
add_time: '2025-11-10 18:48',
number: 200,
pm: 0,
nickname: '故事的小黄花',
type_name: '积分兑换',
mark: '兑换商品扣除'
},
{
id: 1254,
relation: '',
add_time: '2025-11-05 14:20',
number: 1000,
pm: 1,
nickname: '蓝桥春雪',
type_name: '后台充值',
mark: '管理员手动充值'
}
] as any[]
}
},
computed: {
typeLabel(): string {
const found = this.typeOptions.find((o: any): boolean => o.value == this.currentType)
return found != null ? (found['label'] as string) : '全部'
}
},
methods: {
typeChange(e: any) {
this.currentType = this.typeOptions[e.detail.value].value as string
},
handleSearch() {
uni.showToast({ title: '搜索: ' + this.searchQuery, icon: 'none' })
},
handleReset() {
this.searchQuery = ''
this.currentType = ''
},
handleMark(item: any) {
uni.showToast({ title: '修改备注: ' + item.id, icon: 'none' })
}
}
}
</script> </script>
<style scoped lang="scss"> <style scoped>
@import '@/uni.scss'; .admin-marketing-integral-record {
.page { padding: 0; } padding: 0;
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; } background-color: transparent;
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; } min-height: auto;
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; } }
.box-shadow {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
margin-bottom: 16px;
}
.filter-card {
padding: 20px;
}
.filter-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.filter-item {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 24px;
margin-bottom: 5px;
}
.label {
font-size: 14px;
color: #606266;
white-space: nowrap;
}
.date-range-mock {
width: 200px;
height: 32px;
padding: 0 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: #fff;
}
.date-text { font-size: 13px; color: #c0c4cc; }
.admin-input-picker {
width: 150px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 10px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.admin-input-picker text { font-size: 14px; color: #606266; }
.admin-input {
width: 200px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 10px;
font-size: 14px;
}
.search-btn, .reset-btn {
margin-left: 10px;
height: 32px;
line-height: 32px;
padding: 0 15px;
font-size: 14px;
border-radius: 4px;
}
.btn-primary { background-color: #2d8cf0; color: #fff; border: none; }
.btn-default { background-color: #fff; color: #606266; border: 1px solid #dcdfe6; }
/* 表格 */
.table-card {
overflow-x: auto;
}
.table-header {
display: flex;
flex-direction: row;
background-color: #f8f8f9;
border-bottom: 1px solid #e8eaec;
padding: 12px 10px;
}
.table-header text {
font-size: 14px;
font-weight: bold;
color: #515a6e;
}
.table-row {
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid #e8eaec;
padding: 15px 10px;
}
.col-80 { width: 80px; }
.col-100 { width: 100px; }
.col-120 { width: 120px; }
.col-150 { width: 150px; }
.col-180 { width: 180px; }
.cell-text { font-size: 13px; color: #515a6e; }
.cell-link { font-size: 13px; color: #2d8cf0; }
.mark-text { color: #808695; }
.z-price { font-size: 14px; color: #19be6b; font-weight: bold; }
.f-price { font-size: 14px; color: #ed4014; font-weight: bold; }
.op-link {
font-size: 13px;
color: #2d8cf0;
cursor: pointer;
}
/* 分页 */
.table-pagination {
padding: 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
.total-text { font-size: 14px; color: #515a6e; margin-right: 15px; }
.page-ops {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.page-btn {
height: 32px;
padding: 0 12px;
font-size: 14px;
border-radius: 2px;
border: 1px solid #d9d9d9;
background: #fff;
margin: 0;
}
.current-page {
padding: 0 12px;
font-size: 14px;
color: #2d8cf0;
}
</style> </style>

View File

@@ -1,24 +1,348 @@
<template> <template>
<view class="page"> <view class="admin-marketing-integral-statistic">
<view class="header"> <view class="content-body">
<text class="title">{{ title }}</text> <!-- 顶部时间选择 -->
<text class="sub-title">积分统计数据,包含积分发放和消费情况?</text> <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> </view>
</view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import { ref } from 'vue' import { ref } from 'vue'
const currentPage = ref<string>('points-stats') import AnalyticsPieChart from '@/components/analytics/AnalyticsPieChart.uvue'
const title = ref<string>('积分统计') 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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/uni.scss'; .admin-marketing-integral-statistic {
.page { padding: 0; } padding: 0;
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; } background-color: transparent;
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; } min-height: auto;
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; } }
.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> </style>

View File

@@ -524,8 +524,8 @@ function moveToRecycle(id: number) {
.p-name-txt { font-size: 13px; line-height: 1.4; color: #333; } .p-name-txt { font-size: 13px; line-height: 1.4; color: #333; }
.activity-tags { .activity-tags {
.op-divider { color: #e8e8e8; font-size: 12px; margin: 0 4px; } .op-divider { color: #e8e8e8; font-size: 12px; margin: 0 4px; }
}
.more-dropdown { .more-dropdown {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -561,4 +561,5 @@ function moveToRecycle(id: number) {
&.disabled { color: #c0c4cc; background: #f5f7fa; } &.disabled { color: #c0c4cc; background: #f5f7fa; }
} }
} }
</style> </style>

View File

@@ -1,4 +1,4 @@
GET http://localhost:5173/pages/mall/admin/marketing/coupon/list.uvue?import net::ERR_ABORTED 500 (Internal Server Error) index.uvue:991 GET http://localhost:5173/pages/mall/admin/product/product-management/index.uvue?vue&type=style&index=0&scoped=6161a702&lang.scss net::ERR_ABORTED 500 (Internal Server Error)
main.uts:16 [Vue warn]: Unhandled error during execution of async component loader main.uts:16 [Vue warn]: Unhandled error during execution of async component loader
at <AsyncComponentWrapper> at <AsyncComponentWrapper>
at <PageBody> at <PageBody>