完善页面

This commit is contained in:
2026-02-04 09:14:37 +08:00
parent 3476c006e6
commit df642813c3
16 changed files with 3376 additions and 322 deletions

View File

@@ -0,0 +1,272 @@
<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 uts>
export default {
data() {
return {
activeTab: 0,
form: {
integral_name: '积分',
integral_unit: 10,
integral_ratio: 0.1,
integral_max: 50,
freeze_time: 7,
valid_type: 0,
valid_year: 1
}
}
},
methods: {
validTypeChange(e: any) {
this.form.valid_type = parseInt(e.detail.value as string)
},
handleSubmit() {
uni.showLoading({ title: '保存中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: '保存成功', icon: 'success' })
}, 1000)
}
}
}
</script>
<style scoped>
.admin-marketing-integral-config {
padding: 16px;
background-color: #f5f7f9;
min-height: 100vh;
}
.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;
}
/* 顶部标签 */
.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;
}
.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,81 +1,398 @@
<template>
<view class="page-container">
<view class="page-header">
<text class="page-title">积分管理</text>
<text class="page-subtitle">Component: MarketingIntegral</text>
</view>
<view class="page-content">
<view class="placeholder-card">
<text class="placeholder-title">页面占位</text>
<text class="placeholder-desc">该功能模块正在开发中</text>
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text>
<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>
<!-- 分页 -->
<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>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { ref, reactive } from 'vue'
// TODO: 实现 积分管理 的具体功能
const loading = ref<boolean>(false)
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>
<style scoped lang="scss">
.page-container {
padding: 20px;
.admin-marketing-integral-product {
background-color: #f0f2f5;
min-height: 100vh;
background: #f5f5f5;
}
.page-header {
margin-bottom: 20px;
}
.page-title {
display: block;
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.page-subtitle {
display: block;
font-size: 14px;
color: #999;
}
.page-content {
background: #fff;
border-radius: 4px;
padding: 24px;
}
.placeholder-card {
text-align: center;
padding: 60px 20px;
.border-shadow {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.placeholder-title {
display: block;
font-size: 18px;
font-weight: 600;
color: #666;
margin-bottom: 12px;
.content-body {
display: flex;
flex-direction: column;
gap: 20px;
}
.placeholder-desc {
display: block;
/* 过滤栏 */
.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: #999;
margin-bottom: 8px;
color: #515a6e;
font-weight: bold;
display: flex;
align-items: center;
}
.placeholder-info {
display: block;
font-size: 12px;
color: #1890ff;
.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>

View File

@@ -0,0 +1,408 @@
<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: {
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: 16px;
background-color: #f5f7f9;
min-height: 100vh;
}
.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

@@ -0,0 +1,277 @@
<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>
<uni-icons type="calendar" size="16" color="#c0c4cc"></uni-icons>
</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>
<uni-icons type="arrowdown" size="14" color="#c0c4cc"></uni-icons>
</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>
<uni-pagination :total="total" :pageSize="10" :current="1"></uni-pagination>
</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: 16px;
background-color: #f5f7f9;
min-height: 100vh;
}
.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; }
</style>

View File

@@ -48,41 +48,11 @@
<view class="chart-header">
<text class="chart-title">积分使用趋势</text>
<view class="chart-legend">
<view class="legend-item">
<view class="l-dot bg-blue-line"></view>
<text class="l-txt">积分积累</text>
</view>
<view class="legend-item">
<view class="l-dot bg-green-line"></view>
<text class="l-txt">积分消耗</text>
</view>
<text class="down-ic">📥</text>
</view>
</view>
<!-- Mock 线图 -->
<view class="line-chart-box">
<view class="y-labels">
<text class="y-txt">3,500,000</text>
<text class="y-txt">3,000,000</text>
<text class="y-txt">2,500,000</text>
<text class="y-txt">2,000,000</text>
<text class="y-txt">1,500,000</text>
<text class="y-txt">1,000,000</text>
<text class="y-txt">500,000</text>
<text class="y-txt">0</text>
</view>
<view class="chart-area">
<view class="chart-grid">
<view v-for="i in 7" :key="i" class="grid-line"></view>
</view>
<!-- 线条绘制 (简单 Mock) -->
<view class="line-svg-mock">
<view class="trend-path"></view>
</view>
<view class="x-labels">
<text class="x-txt" v-for="date in dates" :key="date">{{ date }}</text>
</view>
</view>
<view class="chart-body">
<AnalyticsMultiLineChart :xLabels="dates" :series="trendSeries" :height="350" />
</view>
</view>
@@ -99,19 +69,8 @@
<view class="analysis-content">
<!-- 饼图样式 -->
<view v-if="sourceStyle === 'pie'" class="pie-layout anim-fade">
<view class="pie-chart-wrap">
<view class="pie-circle"></view>
<view class="pie-label-line">
<text class="pl-txt">后台赠送</text>
</view>
</view>
<view class="pie-legend-list">
<view v-for="item in sourceData" :key="item.label" class="p-leg-item">
<view class="p-dot" :style="{backgroundColor: item.color}"></view>
<text class="p-txt">{{ item.label }}</text>
</view>
</view>
<view v-if="sourceStyle === 'pie'" class="pie-layout-new anim-fade">
<AnalyticsPieChart :items="sourceData" :height="300" />
</view>
<!-- 列表样式 -->
@@ -149,19 +108,8 @@
<view class="analysis-content">
<!-- 饼图样式 -->
<view v-if="consumeStyle === 'pie'" class="pie-layout anim-fade">
<view class="pie-chart-wrap consume-pie">
<view class="pie-circle-c"></view>
<view class="pie-label-line-c">
<text class="pl-txt">订单抵扣</text>
</view>
</view>
<view class="pie-legend-list">
<view v-for="item in consumeData" :key="item.label" class="p-leg-item">
<view class="p-dot" :style="{backgroundColor: item.color}"></view>
<text class="p-txt">{{ item.label }}</text>
</view>
</view>
<view v-if="consumeStyle === 'pie'" class="pie-layout-new anim-fade">
<AnalyticsPieChart :items="consumeData" :height="300" />
</view>
<!-- 列表样式 -->
@@ -194,9 +142,24 @@
<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')
@@ -309,67 +272,12 @@ const toggleConsumeStyle = () => {
}
.chart-title { font-size: 16px; font-weight: bold; color: #333; }
.chart-legend { display: flex; flex-direction: row; align-items: center; gap: 20px; }
.legend-item { display: flex; flex-direction: row; align-items: center; gap: 8px; }
.l-dot { width: 12px; height: 12px; border-radius: 6px; }
.bg-blue-line { background-color: #409eff; }
.bg-green-line { background-color: #19be6b; }
.l-txt { font-size: 12px; color: #666; }
.down-ic { font-size: 18px; color: #999; cursor: pointer; }
.line-chart-box {
display: flex;
flex-direction: row;
height: 400px;
}
.y-labels {
width: 80px;
display: flex;
flex-direction: column;
justify-content: space-between;
padding-bottom: 30px;
}
.y-txt { font-size: 12px; color: #999; text-align: right; padding-right: 10px; }
.chart-area {
flex: 1;
border-left: 1px solid #eee;
border-bottom: 1px solid #eee;
position: relative;
}
.chart-grid {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.grid-line { height: 1px; background-color: #f5f5f5; width: 100%; }
.line-svg-mock {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
overflow: hidden;
}
/* 趋势线条 Mock用一个带有波浪背景的视图模拟实际项目中应使用 Chart 库 */
.trend-path {
position: absolute;
.chart-body {
width: 100%;
height: 100%;
background: radial-gradient(circle at 20% 40%, transparent 10%, #409eff 10.5%, #409eff 11%, transparent 11.5%);
background-size: 40px 100%;
opacity: 0.1; /* 仅作为占位示意 */
}
.x-labels {
position: absolute;
bottom: -30px;
left: 0; right: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.x-txt { font-size: 10px; color: #999; transform: rotate(-45deg); white-space: nowrap; }
/* 底部两个分析 */
.bottom-analysis {
display: flex;
@@ -401,52 +309,12 @@ const toggleConsumeStyle = () => {
}
/* 饼图样式布局 */
.pie-layout {
.pie-layout-new {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
padding-top: 20px;
}
.pie-chart-wrap {
width: 240px;
height: 240px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
align-items: center;
}
.pie-circle {
width: 180px;
height: 180px;
border-radius: 90px;
background-color: #778899; /* 主体颜色 */
box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
}
.pie-circle-c {
width: 180px;
height: 180px;
border-radius: 90px;
background: conic-gradient(#5AB1EF 0 95%, #2EC7C9 95% 100%);
}
.pie-label-line {
position: absolute;
right: 0;
top: 50%;
border-top: 1px solid #999;
width: 40px;
padding-top: 5px;
}
.pl-txt { font-size: 12px; color: #666; white-space: nowrap; }
.pie-legend-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.p-leg-item { display: flex; flex-direction: row; align-items: center; gap: 10px; }
.p-dot { width: 10px; height: 10px; border-radius: 2px; }
.p-txt { font-size: 13px; color: #666; }
/* 列表样式布局 */
.list-layout { display: flex; flex-direction: column; }