feat: 黄精粉前端功能集成 + 个人中心/资产/公排页面优化 + 去除admin copyright

主要改动:
- 个人中心: 去除HjfMemberBadge徽章, 会员等级改显示vip_name,
  "我的资产"/"公排查询"导航项改为与member-points一致风格
- 我的资产页面: 去除HjfMemberBadge, 美化卡片圆角和阴影
- 公排查询页面: 美化顶部渐变和订单卡片样式
- Admin登录页和后台布局: 彻底删除footer copyright信息
- 新增黄精粉业务页面/组件/API/Mock数据(Phase 1)
- 新增PHP环境配置文档和启动脚本

Made-with: Cursor
This commit is contained in:
apple
2026-03-13 00:49:22 +08:00
parent 21f9cc2c0a
commit f6227c0253
70 changed files with 23359 additions and 1176 deletions

View File

@@ -0,0 +1,50 @@
# 黄精粉小程序 UniApp 运行说明
## 推荐方式:使用 HBuilderX 运行到浏览器H5
本 CRMEB 项目的小程序端按 HBuilderX + uni-app 标准目录设计,**推荐使用 HBuilderX 进行开发与预览**
1. 安装 [HBuilderX](https://www.dcloud.io/hbuilderx.html)(正式版或 App 开发版)。
2. 用 HBuilderX 打开本目录:`pro_v3.5.1/view/uniapp`(即当前 package.json 所在目录)。
3. 在菜单栏选择 **运行 → 运行到浏览器 → 选择 Chrome或其他浏览器**
4. 等待编译完成后,会自动打开浏览器,即可查看 Mock 演示效果。
在 H5 预览下可看到:
- 首页 / 个人中心右下角的 **演示控制面板**(紫色悬浮按钮)
- 切换场景 A/B/C、快捷跳转、退款弹窗等 Mock 演示功能
---
## 命令行方式(可选)
已在 `package.json` 中配置脚本与依赖,满足以下条件时可尝试命令行启动 H5
- 需在 **本目录**`view/uniapp`)下执行。
- 若使用 Vue CLI 5可能与当前 uni-app 插件的 webpack 规则不兼容,出现 `No matching use for foo.js``reading 'use'` 等错误,此时请改用 HBuilderX。
### 启动 H5 开发服务
```bash
cd pro_v3.5.1/view/uniapp
# 设置环境变量并启动Mac/Linux
UNI_CLI_CONTEXT=$(pwd) UNI_PLATFORM=h5 UNI_INPUT_DIR=$(pwd) npx vue-cli-service uni-serve --platform h5
# 或使用 npm 脚本(需已安装 cross-env
npm run dev:h5
```
默认端口:**8080**(在 `vue.config.js``devServer.port` 中配置)。
启动成功后,在浏览器访问:**http://localhost:8080**。
### 微信小程序开发
使用 HBuilderX 打开本目录后,选择 **运行 → 运行到小程序模拟器 → 微信开发者工具**,并先在本地打开微信开发者工具。
---
## Mock 演示说明
- 所有 HJF 相关接口当前使用 **Mock 数据**`utils/hjfMockData.js`),无需后端即可演示。
- 右下角 **演示控制面板** 可切换场景 A新用户/ B活跃用户/ CVIP并支持快捷跳转、退款弹窗、重置引导等。
- 完整演示路线与验收点见:**docs/mock-demo-walkthrough.md**。

View File

@@ -0,0 +1,78 @@
/**
* 黄精粉健康商城 - 资产相关 API
* 资产概览、积分明细、现金明细、提现信息及申请提现
* @module api/hjfAssets
*/
import request from '@/utils/request.js';
import {
getMockAssetsOverview,
getMockPointsDetail,
getMockCashDetail,
getMockWithdrawInfo
} from '@/utils/hjfMockData.js';
/** @type {boolean} 是否使用 Mock 数据Phase 1 开发为 truePhase 4 集成改为 false */
const USE_MOCK = true;
/**
* Mock 包装:返回与 request 相同形状的 Promisestatus + data带延迟模拟网络
* @param {*} data - 要返回的响应体
* @param {number} [delay=300] - 延迟毫秒数
* @returns {Promise<{ status: number, data: * }>}
*/
function mockResponse(data, delay = 300) {
return new Promise(resolve => {
setTimeout(() => {
resolve({ status: 200, data: JSON.parse(JSON.stringify(data)) });
}, delay);
});
}
/**
* 获取资产概览(余额、冻结/可用积分、今日释放、公排总退款等)
* @returns {Promise<{ status: number, data: Object }>}
*/
export function getAssetsOverview() {
if (USE_MOCK) return mockResponse(getMockAssetsOverview());
return request.get('hjf/assets/overview');
}
/**
* 获取积分明细(分页)
* @param {Object} [params] - 查询参数,如 page、limit
* @returns {Promise<{ status: number, data: Object }>}
*/
export function getPointsDetail(params) {
if (USE_MOCK) return mockResponse(getMockPointsDetail());
return request.get('hjf/assets/points_detail', params);
}
/**
* 获取现金明细(分页)
* @param {Object} [params] - 查询参数,如 page、limit
* @returns {Promise<{ status: number, data: Object }>}
*/
export function getCashDetail(params) {
if (USE_MOCK) return mockResponse(getMockCashDetail());
return request.get('hjf/assets/cash_detail', params);
}
/**
* 获取提现信息(可提现余额、最低金额、手续费率、渠道列表等)
* @returns {Promise<{ status: number, data: Object }>}
*/
export function getWithdrawInfo() {
if (USE_MOCK) return mockResponse(getMockWithdrawInfo());
return request.get('hjf/assets/withdraw_info');
}
/**
* 申请提现POST
* @param {Object} data - 提现参数,如 amount、channel、bank_id 等
* @returns {Promise<{ status: number, data?: Object }>}
*/
export function applyWithdraw(data) {
if (USE_MOCK) return mockResponse({ success: true, msg: '提现申请已提交' });
return request.post('hjf/assets/withdraw', data);
}

View File

@@ -0,0 +1,63 @@
/**
* 黄精粉健康商城 - 会员模块 API
* 会员信息、团队数据、团队收益
* @module api/hjfMember
*/
import request from '@/utils/request.js';
import {
getMockMemberInfo,
getMockTeamData,
getMockTeamIncome
} from '@/utils/hjfMockData.js';
/** @type {boolean} Phase 1 前端开发为 truePhase 4 集成时改为 false */
const USE_MOCK = true;
/**
* Mock 包装:返回与 request.get() 相同形状的 Promise
* @param {Object} data - 要返回的响应体数据
* @param {number} [delay=300] - 模拟网络延迟(毫秒)
* @returns {Promise<{ status: number, data: Object }>}
*/
function mockResponse(data, delay = 300) {
return new Promise(resolve => {
setTimeout(() => {
resolve({ status: 200, data: JSON.parse(JSON.stringify(data)) });
}, delay);
});
}
/**
* 获取会员信息(等级、直推/伞下人数、升级进度等)
* @returns {Promise<{ status: number, data: Object }>} 会员信息
*/
export function getMemberInfo() {
if (USE_MOCK) return mockResponse(getMockMemberInfo());
return request.get('hjf/member/info');
}
/**
* 获取团队成员列表(直推/伞下成员、分页)
* @param {Object} [params] - 查询参数
* @param {number} [params.page] - 页码
* @param {number} [params.limit] - 每页条数
* @param {string} [params.type] - 筛选类型(如 direct/umbrella
* @returns {Promise<{ status: number, data: Object }>} 团队数据
*/
export function getTeamData(params) {
if (USE_MOCK) return mockResponse(getMockTeamData());
return request.get('hjf/member/team', params);
}
/**
* 获取团队收益明细(直推奖励、伞下奖励等)
* @param {Object} [params] - 查询参数
* @param {number} [params.page] - 页码
* @param {number} [params.limit] - 每页条数
* @returns {Promise<{ status: number, data: Object }>} 团队收益列表
*/
export function getTeamIncome(params) {
if (USE_MOCK) return mockResponse(getMockTeamIncome());
return request.get('hjf/member/income', params);
}

View File

@@ -0,0 +1,45 @@
/**
* 黄精粉健康商城 - 公排相关 API
* 公排状态、公排历史记录
* @see docs/frontend-new-pages-spec.md 2.2.1
*/
import request from '@/utils/request.js';
import { getMockQueueStatus, getMockQueueHistory } from '@/utils/hjfMockData.js';
/** @type {boolean} 是否使用 Mock 数据Phase 4 集成时改为 false */
const USE_MOCK = true;
/**
* Mock 包装:返回与 request.get() 相同形状的 Promise
* 300ms 延迟模拟网络JSON 深拷贝防止数据突变
* @param {*} data - 要返回的数据
* @param {number} [delay=300] - 延迟毫秒数
* @returns {Promise<{ status: number, data: * }>}
*/
function mockResponse(data, delay = 300) {
return new Promise(resolve => {
setTimeout(() => {
resolve({ status: 200, data: JSON.parse(JSON.stringify(data)) });
}, delay);
});
}
/**
* 获取公排状态(我的排队 + 全局进度)
* @returns {Promise<{ status: number, data: { totalOrders: number, myOrders: Array, progress: Object } }>}
*/
export function getQueueStatus() {
if (USE_MOCK) return mockResponse(getMockQueueStatus());
return request.get('hjf/queue/status');
}
/**
* 获取公排历史记录(分页)
* @param {Object} [params] - 查询参数,如 page、limit
* @returns {Promise<{ status: number, data: { list: Array, count: number, page: number, limit: number } }>}
*/
export function getQueueHistory(params) {
if (USE_MOCK) return mockResponse(getMockQueueHistory());
return request.get('hjf/queue/history', params);
}

View File

@@ -0,0 +1,147 @@
<template>
<view class="hjf-asset-card">
<view class="card-row">
<view class="card-item">
<view class="label">现金余额(¥)</view>
<view class="value">{{ formattedNowMoney }}</view>
</view>
<view class="card-item">
<view class="label">待释放积分</view>
<view class="value">{{ formattedFrozenPoints }}</view>
</view>
<view class="card-item">
<view class="label">已释放积分</view>
<view class="value">{{ formattedAvailablePoints }}</view>
</view>
</view>
<view v-if="todayRelease != null" class="card-footer">
<view class="footer-text">今日预计释放 {{ formattedTodayRelease }} 积分</view>
</view>
</view>
</template>
<script>
/**
* @file HjfAssetCard.vue
* @description 三栏资产展示卡片(现金余额 / 待释放积分 / 已释放积分),渐变背景。用于资产总览等页面。
* @see docs/frontend-new-pages-spec.md 第 3.2.3 节
* @see pages/users/user_money/index.vue 渐变卡片区域
*/
export default {
name: 'HjfAssetCard',
props: {
/**
* 现金余额(元),字符串便于对接接口返回
* @type {string}
* @default '0.00'
*/
nowMoney: {
type: String,
default: '0.00'
},
/**
* 待释放积分
* @type {number}
* @default 0
*/
frozenPoints: {
type: Number,
default: 0
},
/**
* 已释放积分
* @type {number}
* @default 0
*/
availablePoints: {
type: Number,
default: 0
},
/**
* 今日预计释放积分(不传则不显示底部提示)
* @type {number}
* @default null
*/
todayRelease: {
type: Number,
default: null
}
},
computed: {
/** 格式化后的现金余额,保留两位小数 */
formattedNowMoney() {
const num = parseFloat(this.nowMoney);
return isNaN(num) ? '0.00' : num.toFixed(2);
},
/** 格式化后的待释放积分 */
formattedFrozenPoints() {
return Number(this.frozenPoints).toLocaleString();
},
/** 格式化后的已释放积分 */
formattedAvailablePoints() {
return Number(this.availablePoints).toLocaleString();
},
/** 格式化后的今日预计释放 */
formattedTodayRelease() {
if (this.todayRelease == null) return '';
return Number(this.todayRelease).toLocaleString();
}
}
};
</script>
<style scoped lang="scss">
.hjf-asset-card {
width: 710rpx;
margin: 0 auto;
background: linear-gradient(90deg, var(--view-theme, #e93323) 0%, var(--view-gradient, #f76b1c) 100%);
border-radius: 32rpx;
box-sizing: border-box;
color: rgba(255, 255, 255, 0.95);
font-size: 24rpx;
position: relative;
overflow: hidden;
.card-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 36rpx 32rpx 32rpx;
}
.card-item {
flex: 1;
min-width: 0;
text-align: center;
padding: 0 8rpx;
.label {
display: block;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.95);
margin-bottom: 12rpx;
}
.value {
font-size: 40rpx;
font-weight: 600;
color: #ffffff;
font-family: 'SemiBold', sans-serif;
}
}
.card-footer {
width: 100%;
padding: 20rpx 32rpx 24rpx;
background: rgba(255, 255, 255, 0.1);
text-align: center;
.footer-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.95);
}
}
}
</style>

View File

@@ -0,0 +1,385 @@
<template>
<view v-if="showPanel" class="hjf-demo-container">
<!-- 悬浮按钮 -->
<view v-if="!expanded" class="demo-fab" @tap="togglePanel">
<text class="iconfont icon-shezhi fs-44 text-white"></text>
</view>
<!-- 展开的控制面板 -->
<view v-if="expanded" class="demo-panel">
<view class="panel-header">
<text class="panel-title">演示控制面板</text>
<text class="iconfont icon-ic_close fs-40" @tap="togglePanel"></text>
</view>
<view class="panel-body">
<!-- 场景切换 -->
<view class="section">
<view class="section-title">当前场景{{ scenarioName }}</view>
<view class="scenario-btns">
<view
v-for="s in scenarios"
:key="s.id"
:class="['scenario-btn', currentScenario === s.id ? 'active' : '']"
@tap="switchScenario(s.id)"
>
<text class="scenario-label">{{ s.name }}</text>
<text class="scenario-desc">{{ s.desc }}</text>
</view>
</view>
</view>
<!-- 特殊操作 -->
<view class="section">
<view class="section-title">特殊操作</view>
<view class="action-btns">
<view class="action-btn" @tap="triggerRefundNotice">
<text class="iconfont icon-qianbao fs-36"></text>
<text class="action-label">退款弹窗</text>
</view>
<view class="action-btn" @tap="clearGuideFlag">
<text class="iconfont icon-shuaxin fs-36"></text>
<text class="action-label">重置引导</text>
</view>
</view>
</view>
<!-- 快捷跳转 -->
<view class="section">
<view class="section-title">快捷跳转</view>
<scroll-view scroll-y class="nav-scroll">
<view
v-for="nav in quickNavs"
:key="nav.path"
class="nav-item"
@tap="navigateTo(nav.path)"
>
<text class="nav-label">{{ nav.label }}</text>
<text class="iconfont icon-ic_rightarrow fs-28"></text>
</view>
</scroll-view>
</view>
</view>
</view>
<!-- 遮罩层 -->
<view v-if="expanded" class="demo-mask" @tap="togglePanel"></view>
</view>
</template>
<script>
import { setMockScenario, getCurrentScenario } from '@/utils/hjfMockData.js';
export default {
name: 'HjfDemoPanel',
data() {
return {
showPanel: false,
expanded: false,
currentScenario: 'B',
scenarios: [
{
id: 'A',
name: '场景 A',
desc: '新用户首次体验'
},
{
id: 'B',
name: '场景 B',
desc: '活跃用户等待退款'
},
{
id: 'C',
name: '场景 C',
desc: 'VIP用户刚退款'
}
],
quickNavs: [
{ label: 'P23 新用户引导', path: '/pages/guide/hjf_intro' },
{ label: 'P01 首页', path: '/pages/index/index' },
{ label: 'P04 个人中心', path: '/pages/user/index' },
{ label: 'P12 公排状态', path: '/pages/queue/status' },
{ label: 'P13 公排历史', path: '/pages/queue/history' },
{ label: 'P14 公排规则', path: '/pages/queue/rules' },
{ label: 'P15 我的资产', path: '/pages/assets/index' },
{ label: 'P18 积分明细', path: '/pages/assets/points_detail' },
{ label: 'P16 提现页', path: '/pages/users/user_cash/index' },
{ label: 'P19 推荐收益', path: '/pages/users/user_spread_money/index' }
]
};
},
computed: {
scenarioName() {
const scenario = this.scenarios.find(s => s.id === this.currentScenario);
return scenario ? `${scenario.name} - ${scenario.desc}` : '';
}
},
mounted() {
// 仅在开发环境显示
// #ifdef H5
this.showPanel = process.env.NODE_ENV !== 'production';
// #endif
// #ifndef H5
this.showPanel = true; // 小程序默认显示,用于演示
// #endif
// 获取当前场景
this.currentScenario = getCurrentScenario();
console.log('[HjfDemoPanel] 演示控制面板已加载,当前场景:', this.currentScenario);
},
methods: {
togglePanel() {
this.expanded = !this.expanded;
},
switchScenario(scenarioId) {
if (scenarioId === this.currentScenario) return;
const success = setMockScenario(scenarioId);
if (success) {
this.currentScenario = scenarioId;
uni.showToast({
title: `已切换到场景 ${scenarioId}`,
icon: 'success'
});
// 延迟500ms后刷新当前页面
setTimeout(() => {
const pages = getCurrentPages();
if (pages.length > 0) {
const currentPage = pages[pages.length - 1];
// 触发页面的 onShow 方法刷新数据
if (currentPage.$vm && typeof currentPage.$vm.$options.onShow === 'function') {
currentPage.$vm.$options.onShow.call(currentPage.$vm);
}
}
}, 500);
}
},
triggerRefundNotice() {
uni.showToast({
title: '跳转到公排状态页查看退款弹窗',
icon: 'none',
duration: 2000
});
setTimeout(() => {
uni.navigateTo({
url: '/pages/queue/status?show_refund=1'
});
this.expanded = false;
}, 1500);
},
clearGuideFlag() {
uni.removeStorageSync('hjf_guide_read');
uni.showToast({
title: '引导标记已清除',
icon: 'success'
});
setTimeout(() => {
uni.reLaunch({
url: '/pages/guide/hjf_intro'
});
}, 1000);
},
navigateTo(path) {
this.expanded = false;
// 处理 TabBar 页面
const tabBarPages = ['/pages/index/index', '/pages/user/index'];
if (tabBarPages.includes(path)) {
uni.switchTab({ url: path });
} else {
uni.navigateTo({ url: path });
}
}
}
};
</script>
<style scoped lang="scss">
.hjf-demo-container {
position: fixed;
z-index: 9999;
}
.demo-fab {
position: fixed;
right: 30rpx;
bottom: 200rpx;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-shadow: 0 8rpx 20rpx rgba(102, 126, 234, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
transition: all 0.3s ease;
}
.demo-fab:active {
transform: scale(0.95);
}
.demo-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9998;
}
.demo-panel {
position: fixed;
right: 30rpx;
bottom: 200rpx;
width: 600rpx;
max-height: 1000rpx;
background: #fff;
border-radius: 24rpx;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
z-index: 9999;
overflow: hidden;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 32rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
}
.panel-title {
font-size: 32rpx;
font-weight: 600;
}
.panel-body {
max-height: 880rpx;
overflow-y: auto;
padding: 0 0 20rpx 0;
}
.section {
padding: 24rpx 32rpx;
border-bottom: 1px solid #f0f0f0;
}
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
}
.scenario-btns {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.scenario-btn {
padding: 24rpx 20rpx;
background: #f5f5f5;
border-radius: 16rpx;
border: 2px solid transparent;
transition: all 0.3s ease;
}
.scenario-btn.active {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
border-color: #667eea;
}
.scenario-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.scenario-desc {
display: block;
font-size: 24rpx;
color: #999;
}
.action-btns {
display: flex;
gap: 16rpx;
}
.action-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24rpx;
background: #f5f5f5;
border-radius: 16rpx;
transition: all 0.3s ease;
}
.action-btn:active {
background: #e8e8e8;
transform: scale(0.98);
}
.action-label {
font-size: 24rpx;
color: #666;
margin-top: 8rpx;
}
.nav-scroll {
max-height: 400rpx;
}
.nav-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 20rpx;
background: #f5f5f5;
border-radius: 12rpx;
margin-bottom: 12rpx;
transition: all 0.3s ease;
}
.nav-item:active {
background: #e8e8e8;
}
.nav-label {
font-size: 26rpx;
color: #333;
}
.text-white {
color: #fff;
}
</style>

View File

@@ -0,0 +1,185 @@
<template>
<view class="hjf-member-badge" :class="sizeClass">
<view class="badge-icon" :style="iconStyle">
<text class="icon-text">{{ levelText }}</text>
</view>
<text class="badge-name">{{ displayName }}</text>
</view>
</template>
<script>
/**
* 会员等级颜色映射0-4 对应文档 3.2.4
* @constant
* @type {Object<number, string>}
*/
const LEVEL_COLORS = {
0: '#999999', // 普通
1: '#CD7F32', // 创客
2: '#C0C0C0', // 云店
3: '#FFD700', // 服务商
4: '#8B5CF6' // 分公司
};
/**
* 等级默认名称level 无 levelName 时回退)
* @constant
* @type {string[]}
*/
const LEVEL_NAMES = ['普通', '创客', '云店', '服务商', '分公司'];
/**
* HjfMemberBadge — 会员等级徽章组件
*
* 展示会员等级图标(圆形数字徽)+ 等级名称,支持三种尺寸与五档等级颜色。
* 参考docs/frontend-new-pages-spec.md 第 3.2.4 节。
*
* @component HjfMemberBadge
* @example
* <HjfMemberBadge :level="2" levelName="云店" size="normal" />
*/
export default {
name: 'HjfMemberBadge',
props: {
/**
* 会员等级数字 (0-4)
* 0: 普通, 1: 创客, 2: 云店, 3: 服务商, 4: 分公司
* @type {number}
* @default 0
*/
level: {
type: Number,
default: 0,
validator(val) {
return val >= 0 && val <= 4;
}
},
/**
* 等级名称展示文案(可选,不传则按 level 回退为默认名称)
* @type {string}
* @default ''
*/
levelName: {
type: String,
default: ''
},
/**
* 尺寸:'small' | 'normal' | 'large'
* @type {'small'|'normal'|'large'}
* @default 'normal'
*/
size: {
type: String,
default: 'normal',
validator(val) {
return ['small', 'normal', 'large'].indexOf(val) !== -1;
}
}
},
computed: {
/** 尺寸类名,用于 .size-small / .size-normal / .size-large */
sizeClass() {
return `size-${this.size}`;
},
/** 当前等级对应的主题色 */
levelColor() {
const key = Math.min(4, Math.max(0, this.level));
return LEVEL_COLORS[key] || LEVEL_COLORS[0];
},
/** 徽章图标内联样式(背景色、边框色) */
iconStyle() {
return {
backgroundColor: this.levelColor,
borderColor: this.levelColor
};
},
/** 最终展示的等级名称(优先 levelName否则 LEVEL_NAMES[level] */
displayName() {
if (this.levelName && this.levelName.trim()) {
return this.levelName.trim();
}
const key = Math.min(4, Math.max(0, this.level));
return LEVEL_NAMES[key] || LEVEL_NAMES[0];
},
/** 徽章内显示的等级数字文案 */
levelText() {
const key = Math.min(4, Math.max(0, this.level));
return String(key);
}
}
};
</script>
<style scoped lang="scss">
.hjf-member-badge {
display: inline-flex;
align-items: center;
flex-wrap: nowrap;
.badge-icon {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 2rpx solid;
flex-shrink: 0;
.icon-text {
color: #fff;
font-weight: bold;
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.2);
}
}
.badge-name {
margin-left: 8rpx;
font-weight: 500;
white-space: nowrap;
}
&.size-small {
.badge-icon {
width: 32rpx;
height: 32rpx;
.icon-text {
font-size: 20rpx;
}
}
.badge-name {
font-size: 22rpx;
}
}
&.size-normal {
.badge-icon {
width: 40rpx;
height: 40rpx;
.icon-text {
font-size: 24rpx;
}
}
.badge-name {
font-size: 26rpx;
}
}
&.size-large {
.badge-icon {
width: 52rpx;
height: 52rpx;
.icon-text {
font-size: 30rpx;
}
}
.badge-name {
font-size: 30rpx;
}
}
}
</style>

View File

@@ -0,0 +1,177 @@
<template>
<view class="hjf-queue-progress">
<!-- 环形进度 -->
<view class="progress-ring-wrap">
<view class="progress-ring" :style="ringStyle">
<view class="progress-ring-inner">
<text class="progress-text">{{ currentCount }}/{{ triggerMultiple }}</text>
</view>
</view>
</view>
<!-- 条形进度 -->
<view class="progress-bar-wrap">
<view class="progress-bar-track">
<view class="progress-bar-fill" :style="barStyle" />
</view>
<view class="progress-label">
<text>当前批次进度</text>
<text class="progress-value">{{ currentCount }}/{{ triggerMultiple }}</text>
</view>
<view v-if="nextRefundNo != null" class="next-refund">
下一退款序号: {{ nextRefundNo }}
</view>
</view>
</view>
</template>
<script>
/**
* @file HjfQueueProgress.vue
* @description 公排批次进度组件:环形/条形进度条展示当前批次进度(如 2/4并显示下一个退款序号。
* @see docs/frontend-new-pages-spec.md 第 2.2.2 节
*/
export default {
name: 'HjfQueueProgress',
props: {
/**
* 当前批次已入队数
* @type {number}
*/
currentCount: {
type: Number,
default: 0
},
/**
* 触发倍数(每多少人触发一批退款,默认 4
* @type {number}
*/
triggerMultiple: {
type: Number,
default: 4
},
/**
* 下一个退款的 queue_no可选有值时显示「下一退款序号」
* @type {number|null}
*/
nextRefundNo: {
type: Number,
default: null
}
},
computed: {
/**
* 进度百分比0100用于条形/环形展示
* @returns {number}
*/
progressPercent() {
const total = this.triggerMultiple;
if (!total || total <= 0) return 0;
const p = Math.min(100, (this.currentCount / total) * 100);
return Math.round(p * 10) / 10;
},
/**
* 环形进度样式conic-gradient 用 progressPercent
* @returns {Object}
*/
ringStyle() {
return {
'--progress-percent': this.progressPercent,
'--progress-color': 'var(--view-theme)'
};
},
/**
* 条形进度填充宽度与主题色
* @returns {Object}
*/
barStyle() {
return {
width: this.progressPercent + '%',
backgroundColor: 'var(--view-theme)'
};
}
}
};
</script>
<style scoped lang="scss">
.hjf-queue-progress {
padding: 24rpx;
}
.progress-ring-wrap {
display: flex;
justify-content: center;
margin-bottom: 32rpx;
}
.progress-ring {
--progress-percent: 0;
--progress-color: var(--view-theme);
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background: conic-gradient(
var(--progress-color) 0deg,
var(--progress-color) calc(var(--progress-percent) * 3.6deg),
#eee calc(var(--progress-percent) * 3.6deg),
#eee 360deg
);
display: flex;
align-items: center;
justify-content: center;
}
.progress-ring-inner {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.progress-text {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.progress-bar-wrap {
padding: 0 8rpx;
}
.progress-bar-track {
height: 16rpx;
border-radius: 8rpx;
background: #eee;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: 8rpx;
transition: width 0.25s ease;
}
.progress-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16rpx;
font-size: 24rpx;
color: #666;
}
.progress-value {
font-weight: 600;
color: var(--view-theme);
}
.next-refund {
margin-top: 12rpx;
font-size: 22rpx;
color: #999;
}
</style>

View File

@@ -0,0 +1,221 @@
<template>
<view v-if="visible" class="hjf-refund-notice" @tap.self="handleMaskTap">
<view class="hjf-refund-notice__mask" />
<view class="hjf-refund-notice__box">
<view class="hjf-refund-notice__icon-wrap">
<text class="hjf-refund-notice__icon"></text>
</view>
<view class="hjf-refund-notice__title">恭喜您的公排订单已退款</view>
<view class="hjf-refund-notice__desc">已入账到现金余额</view>
<view class="hjf-refund-notice__amount">
<text class="hjf-refund-notice__amount-label">退款金额</text>
<text class="hjf-refund-notice__amount-value">{{ formattedAmount }}</text>
</view>
<view v-if="orderId" class="hjf-refund-notice__order">
<text class="hjf-refund-notice__order-label">订单号</text>
<text class="hjf-refund-notice__order-value">{{ orderId }}</text>
</view>
<view class="hjf-refund-notice__btn" @tap="handleConfirm">确认</view>
</view>
</view>
</template>
<script>
/**
* @file HjfRefundNotice.vue
* @description 公排退款成功后的弹窗通知,展示退款金额、已入账到现金余额、订单号,确认按钮关闭弹窗。
* @see docs/frontend-new-pages-spec.md 第 2.2.3 节
*/
export default {
name: 'HjfRefundNotice',
props: {
/**
* 是否显示弹窗
* @type {boolean}
* @default false
*/
visible: {
type: Boolean,
default: false
},
/**
* 退款金额(元)
* @type {number}
* @default 0
*/
amount: {
type: Number,
default: 0
},
/**
* 订单号
* @type {string}
* @default ''
*/
orderId: {
type: String,
default: ''
}
},
computed: {
/**
* 格式化后的退款金额格式¥3,600.00(千分位 + 两位小数)
* @returns {string}
*/
formattedAmount() {
const num = Number(this.amount);
if (isNaN(num)) return '¥0.00';
const fixed = num.toFixed(2);
const [intPart, decPart] = fixed.split('.');
const formatted = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return `¥${formatted}.${decPart}`;
}
},
methods: {
/**
* 点击确认按钮,关闭弹窗并触发 close 事件
*/
handleConfirm() {
this.$emit('close');
},
/**
* 点击遮罩层,关闭弹窗并触发 close 事件
*/
handleMaskTap() {
this.$emit('close');
}
}
};
</script>
<style scoped lang="scss">
.hjf-refund-notice {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.hjf-refund-notice__mask {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
.hjf-refund-notice__box {
position: relative;
width: 100%;
max-width: 560rpx;
background: #fff;
border-radius: 24rpx;
padding: 48rpx 40rpx 40rpx;
box-sizing: border-box;
}
.hjf-refund-notice__icon-wrap {
width: 88rpx;
height: 88rpx;
margin: 0 auto 24rpx;
background: var(--view-gradient, linear-gradient(135deg, #52c41a 0%, #73d13d 100%));
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.hjf-refund-notice__icon {
font-size: 48rpx;
color: #fff;
font-weight: bold;
line-height: 1;
}
.hjf-refund-notice__title {
font-size: 36rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 12rpx;
}
.hjf-refund-notice__desc {
font-size: 28rpx;
color: #666;
text-align: center;
margin-bottom: 32rpx;
}
.hjf-refund-notice__amount {
background: #f5f5f5;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.hjf-refund-notice__amount-label {
font-size: 28rpx;
color: #666;
}
.hjf-refund-notice__amount-value {
font-size: 36rpx;
font-weight: 600;
color: #ff4d4f;
}
.hjf-refund-notice__order {
background: #fafafa;
border-radius: 12rpx;
padding: 20rpx 24rpx;
margin-bottom: 40rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.hjf-refund-notice__order-label {
font-size: 26rpx;
color: #999;
}
.hjf-refund-notice__order-value {
font-size: 26rpx;
color: #666;
max-width: 360rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hjf-refund-notice__btn {
height: 88rpx;
line-height: 88rpx;
text-align: center;
font-size: 32rpx;
font-weight: 500;
color: #fff;
background: var(--view-gradient, linear-gradient(135deg, #1890ff 0%, #40a9ff 100%));
border-radius: 44rpx;
}
.hjf-refund-notice__btn:active {
opacity: 0.9;
}
</style>

View File

@@ -1,12 +1,13 @@
<template>
<view class="wf-item-page wf-page0">
<view class='pictrue'>
<view class='pictrue' style="position: relative;">
<easy-loadimage
mode="widthFix"
:image-src="item.image"
:borderSrc="item.activity_frame.image"
width="100%"
borderRadius="16rpx 16rpx 0 0"></easy-loadimage>
<view class="queue-badge" v-if="item.is_queue_goods == 1">参与公排</view>
</view>
<view class="info_box">
<view class="w-full line2 fs-28 text--w111-333 lh-40rpx">
@@ -126,4 +127,17 @@
.text-mer{
color: $primary-merchant;
}
.queue-badge {
position: absolute;
top: 12rpx;
right: 12rpx;
padding: 4rpx 14rpx;
border-radius: 20rpx;
font-size: 20rpx;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #52c41a, #389e0d);
z-index: 10;
letter-spacing: 2rpx;
}
</style>

View File

@@ -108,9 +108,9 @@
}
},
tabBarData(newVal){
if(newVal){
if (newVal && newVal.effectConfig && newVal.menuList && newVal.menuList.length) {
let configData = newVal;
if(this.isTabBar){
if (this.isTabBar) {
this.newData = configData;
}
this.showTabBar = configData.effectConfig.tabVal;
@@ -120,6 +120,10 @@
uni.showTabBar()
}
this.$emit('newDataStatus', configData.effectConfig.tabVal)
} else {
// 数据无效或为空时显示原生 tabBar
this.showTabBar = false;
uni.showTabBar();
}
}
// 'newData.menuList'(newValue, oldValue) {
@@ -155,7 +159,7 @@
methods: {
navigationInfo() {
//判断渲染来源是一级菜单还是微页面
if(this.isTabBar && this.tabBarData.effectConfig){
if (this.isTabBar && this.tabBarData && this.tabBarData.effectConfig && this.tabBarData.menuList && this.tabBarData.menuList.length) {
let configData = this.tabBarData;
this.newData = configData;
this.showTabBar = configData.effectConfig.tabVal;
@@ -165,8 +169,11 @@
uni.showTabBar()
}
this.$emit('newDataStatus', configData.effectConfig.tabVal)
} else {
// 无自定义底部菜单数据或接口未返回时,显示原生 tabBar
this.showTabBar = false;
uni.showTabBar();
}
},
goRouter(item) {
var pages = getCurrentPages();

View File

@@ -27,12 +27,15 @@ import BaseMoney from './components/BaseMoney.vue';
import BaseTag from './components/BaseTag.vue';
import easyLoadimage from './components/easy-loadimage/easy-loadimage.vue'
import baseDrawer from '@/components/tui-drawer/tui-drawer.vue';
import HjfDemoPanel from './components/HjfDemoPanel.vue';
Vue.component('home', home)
Vue.component('skeleton', skeleton)
Vue.component('BaseMoney', BaseMoney)
Vue.component('BaseTag', BaseTag)
Vue.component('baseDrawer', baseDrawer)
Vue.component('easyLoadimage', easyLoadimage)
Vue.component('HjfDemoPanel', HjfDemoPanel)
// #ifdef H5

File diff suppressed because it is too large Load Diff

View File

@@ -76,6 +76,63 @@
"navigationStyle": "custom",
"enablePullDownRefresh": true
}
},
{
"path": "pages/queue/status",
"style": {
"navigationBarTitleText": "公排状态",
"navigationStyle": "custom"
}
},
{
"path": "pages/queue/history",
"style": {
"navigationBarTitleText": "公排历史",
"app-plus": {
"titleNView": {
"type": "default"
}
}
}
},
{
"path": "pages/queue/rules",
"style": {
"navigationBarTitleText": "公排规则",
"app-plus": {
"titleNView": {
"type": "default"
}
}
}
},
{
"path": "pages/assets/index",
"style": {
"navigationBarTitleText": "我的资产",
"navigationStyle": "custom"
}
},
{
"path": "pages/assets/points_detail",
"style": {
"navigationBarTitleText": "积分明细",
"app-plus": {
"titleNView": {
"type": "default"
}
}
}
},
{
"path": "pages/guide/hjf_intro",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom",
"app-plus": {
"titleNView": false
}
}
}
],
// "plugins": {

View File

@@ -0,0 +1,38 @@
# Conversation - 2026-03-10 18:17:19
## Prompt
创建页面 points_detail.vueP17 积分明细页):
文件路径: /Users/apple/scott2026/huangjingfen/pro_v3.5.1/view/uniapp/pages/assets/points_detail.vue
内容要求:
1. 使用 Vue 2 Options API
2. 导入 getPointsDetail from '@/api/hjfAssets.js'
3. data: list, page, loading, finished, activeTab
4. onLoad + onReachBottom: 加载和分页
5. 模板: Tab切换全部/待释放/已释放)+ 积分流水列表
6. 样式: <style scoped lang="scss">
7. 参考: docs/frontend-new-pages-spec.md 第 3.3.2 节
完整 JSDoc。
## Response
The file was created successfully (377 lines total). Here's a summary of what was built:
**`pages/assets/points_detail.vue`** — P17 积分明细页,共 377 行:
**模板结构:**
- **Tab 导航栏**3 个 Tab全部 / 待释放 / 已释放),`sticky` 吸顶,激活项下划线用 `var(--view-theme)` 着色
- **按日期分组列表**:每个分组显示日期标题 + 白色卡片,卡片内每条流水展示标题、时间、状态标签(橙色=待释放 / 绿色=已释放)、右侧积分增减(`+N` 主题色 / `-N` 黑色)
- **加载更多**:旋转 loading 图标 + 文案;**空状态**:引入 `emptyPage` 组件
**脚本Vue 2 Options API**
- `data``tabs``activeTab``list``dateKeys``page``limit``loading``finished``loadTitle`
- `onLoad`:支持 `?type=frozen|released` 参数直接定位 Tab
- `onReachBottom`:触底自动翻页
- `changeTab()`:切 Tab 时 `resetAndLoad()` 重置所有状态
- `loadList()`:防重入(`loading || finished` 守卫),按 `add_time` 日期前缀分组追加数据
**样式:** `<style scoped lang="scss">`,全 `rpx` 单位,使用 `var(--view-theme)` 主题色。

View File

@@ -0,0 +1,565 @@
<template>
<view class="hjf-assets-page" :style="colorStyle">
<view class="assets-wrapper">
<view class="assets-header">
<view class="assets-header__top">
<view class="assets-header__title">我的资产</view>
</view>
<view v-if="loading" class="skeleton-card"></view>
<view v-else class="hero-card">
<view class="hero-card__bg-circle hero-card__bg-circle--1"></view>
<view class="hero-card__bg-circle hero-card__bg-circle--2"></view>
<view class="hero-card__main">
<view class="hero-card__label">现金余额()</view>
<view class="hero-card__money">
<text class="hero-card__yen">¥</text>{{ assetsInfo ? Number(assetsInfo.now_money).toFixed(2) : '0.00' }}
</view>
</view>
<view class="hero-card__row">
<view class="hero-card__col">
<view class="hero-card__col-val">{{ formattedFrozenPoints }}</view>
<view class="hero-card__col-key">待释放积分</view>
</view>
<view class="hero-card__sep"></view>
<view class="hero-card__col">
<view class="hero-card__col-val">{{ formattedAvailablePoints }}</view>
<view class="hero-card__col-key">已释放积分</view>
</view>
<view class="hero-card__sep"></view>
<view class="hero-card__col" v-if="assetsInfo && assetsInfo.today_release != null">
<view class="hero-card__col-val hero-card__col-val--accent">{{ assetsInfo.today_release }}</view>
<view class="hero-card__col-key">今日释放</view>
</view>
</view>
</view>
</view>
<view class="quick-nav">
<view class="quick-nav__item" hover-class="quick-nav__item--hover" @tap="goPointsDetail">
<view class="quick-nav__icon quick-nav__icon--points">
<text class="iconfont icon-jifen"></text>
</view>
<view class="quick-nav__label">积分明细</view>
<view class="quick-nav__desc">待释放 / 已释放</view>
</view>
<view class="quick-nav__item" hover-class="quick-nav__item--hover" @tap="goCashDetail">
<view class="quick-nav__icon quick-nav__icon--cash">
<text class="iconfont icon-qianbao"></text>
</view>
<view class="quick-nav__label">现金明细</view>
<view class="quick-nav__desc">收支流水记录</view>
</view>
<view class="quick-nav__item" hover-class="quick-nav__item--hover" @tap="goWithdraw">
<view class="quick-nav__icon quick-nav__icon--withdraw">
<text class="iconfont icon-tixian"></text>
</view>
<view class="quick-nav__label">申请提现</view>
<view class="quick-nav__desc">可用余额提现</view>
</view>
</view>
</view>
<view class="release-card" v-if="!loading && assetsInfo">
<view class="release-card__header">
<view class="release-card__dot"></view>
<view class="release-card__title">今日释放预告</view>
<view class="release-card__date">{{ todayDateStr }}</view>
</view>
<view class="release-card__body">
<view class="release-card__item">
<view class="release-card__value release-card__value--highlight">
{{ assetsInfo.today_release != null ? assetsInfo.today_release : 0 }}
</view>
<view class="release-card__key">今日预计释放(积分)</view>
</view>
<view class="release-card__divider"></view>
<view class="release-card__item">
<view class="release-card__value">{{ formattedFrozenPoints }}</view>
<view class="release-card__key">待释放总积分</view>
</view>
<view class="release-card__divider"></view>
<view class="release-card__item">
<view class="release-card__value">{{ formattedAvailablePoints }}</view>
<view class="release-card__key">已释放积分</view>
</view>
</view>
<view class="release-card__tips">
<text class="iconfont icon-tishi"></text>
积分每日自动释放释放后可用于抵扣消费
</view>
</view>
<view class="stats-row" v-if="!loading && assetsInfo">
<view class="stats-item">
<view class="stats-icon stats-icon--refund">
<text class="iconfont icon-qianbao"></text>
</view>
<view class="stats-info">
<view class="stats-value">¥{{ assetsInfo.total_queue_refund }}</view>
<view class="stats-label">公排累计退款</view>
</view>
</view>
<view class="stats-divider"></view>
<view class="stats-item">
<view class="stats-icon stats-icon--points">
<text class="iconfont icon-jifen"></text>
</view>
<view class="stats-info">
<view class="stats-value">{{ formattedTotalPoints }}</view>
<view class="stats-label">累计获得积分</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { getAssetsOverview } from '@/api/hjfAssets.js';
import colors from '@/mixins/color.js';
export default {
name: 'AssetsIndex',
mixins: [colors],
data() {
return {
assetsInfo: null,
loading: false
};
},
computed: {
todayDateStr() {
const d = new Date();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${d.getFullYear()}-${mm}-${dd}`;
},
formattedFrozenPoints() {
if (!this.assetsInfo) return '0';
return Number(this.assetsInfo.frozen_points).toLocaleString();
},
formattedAvailablePoints() {
if (!this.assetsInfo) return '0';
return Number(this.assetsInfo.available_points).toLocaleString();
},
formattedTotalPoints() {
if (!this.assetsInfo) return '0';
return Number(this.assetsInfo.total_points_earned).toLocaleString();
}
},
onLoad() {
this.fetchAssetsOverview();
},
onShow() {
if (this.assetsInfo) {
this.fetchAssetsOverview();
}
},
methods: {
fetchAssetsOverview() {
this.loading = true;
getAssetsOverview()
.then(res => {
if (res && res.status === 200) {
this.assetsInfo = res.data;
}
})
.catch(() => {
uni.showToast({ title: '加载失败,请稍后重试', icon: 'none' });
})
.finally(() => {
this.loading = false;
});
},
goPointsDetail() {
uni.navigateTo({ url: '/pages/assets/points_detail' });
},
goCashDetail() {
uni.navigateTo({ url: '/pages/users/user_bill/index?type=2' });
},
goWithdraw() {
uni.navigateTo({ url: '/pages/users/user_cash/index' });
}
}
};
</script>
<style scoped lang="scss">
.hjf-assets-page {
min-height: 100vh;
background-color: #f4f5f7;
padding-bottom: 60rpx;
}
.assets-wrapper {
background: linear-gradient(180deg, var(--view-theme, #e93323) 0%, var(--view-gradient, #f76b1c) 50%, #f4f5f7 100%);
padding-bottom: 4rpx;
}
.assets-header {
padding-top: 32rpx;
}
.assets-header__top {
padding: 0 30rpx 24rpx;
}
.assets-header__title {
font-size: 36rpx;
font-weight: 700;
color: #fff;
letter-spacing: 2rpx;
}
.skeleton-card {
width: 710rpx;
height: 280rpx;
background: linear-gradient(90deg, rgba(255,255,255,0.15) 25%, rgba(255,255,255,0.25) 50%, rgba(255,255,255,0.15) 75%);
background-size: 400% 100%;
border-radius: 32rpx;
margin: 0 auto 20rpx;
animation: skeleton-shimmer 1.5s infinite;
}
@keyframes skeleton-shimmer {
0% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.hero-card {
width: 710rpx;
margin: 0 auto;
border-radius: 32rpx;
background: linear-gradient(135deg, var(--view-theme, #e93323) 0%, var(--view-gradient, #f76b1c) 100%);
box-sizing: border-box;
position: relative;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
}
.hero-card__bg-circle {
position: absolute;
border-radius: 50%;
opacity: 0.08;
background: #fff;
}
.hero-card__bg-circle--1 {
width: 320rpx;
height: 320rpx;
top: -80rpx;
right: -60rpx;
}
.hero-card__bg-circle--2 {
width: 200rpx;
height: 200rpx;
bottom: -40rpx;
left: -30rpx;
}
.hero-card__main {
padding: 40rpx 36rpx 28rpx;
position: relative;
z-index: 1;
}
.hero-card__label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 12rpx;
}
.hero-card__money {
font-size: 64rpx;
font-weight: 700;
color: #fff;
font-family: 'SemiBold', sans-serif;
line-height: 1.1;
}
.hero-card__yen {
font-size: 36rpx;
font-weight: 500;
margin-right: 4rpx;
}
.hero-card__row {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.12);
padding: 24rpx 0;
position: relative;
z-index: 1;
}
.hero-card__col {
flex: 1;
text-align: center;
}
.hero-card__col-val {
font-size: 36rpx;
font-weight: 600;
color: #fff;
font-family: 'SemiBold', sans-serif;
margin-bottom: 6rpx;
}
.hero-card__col-val--accent {
color: #ffe58f;
}
.hero-card__col-key {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.75);
}
.hero-card__sep {
width: 1rpx;
height: 48rpx;
background: rgba(255, 255, 255, 0.2);
flex-shrink: 0;
}
.quick-nav {
display: flex;
margin: 20rpx 20rpx 0;
}
.quick-nav__item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
background: #fff;
border-radius: 20rpx;
padding: 30rpx 12rpx 26rpx;
box-sizing: border-box;
margin: 0 8rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
transition: all 0.2s;
}
.quick-nav__item:first-child { margin-left: 0; }
.quick-nav__item:last-child { margin-right: 0; }
.quick-nav__item--hover {
opacity: 0.75;
transform: scale(0.97);
}
.quick-nav__icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.quick-nav__icon .iconfont {
font-size: 38rpx;
color: #fff;
}
.quick-nav__icon--points {
background: linear-gradient(135deg, var(--view-theme, #e93323) 0%, var(--view-gradient, #f76b1c) 100%);
box-shadow: 0 6rpx 16rpx rgba(233, 51, 35, 0.25);
}
.quick-nav__icon--cash {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
box-shadow: 0 6rpx 16rpx rgba(82, 196, 26, 0.25);
}
.quick-nav__icon--withdraw {
background: linear-gradient(135deg, #fa8c16 0%, #ffc53d 100%);
box-shadow: 0 6rpx 16rpx rgba(250, 140, 22, 0.25);
}
.quick-nav__label {
font-size: 26rpx;
font-weight: 500;
color: #333;
margin-bottom: 6rpx;
}
.quick-nav__desc {
font-size: 20rpx;
color: #999;
text-align: center;
}
.release-card {
width: 710rpx;
margin: 24rpx auto;
background: #fff;
border-radius: 24rpx;
box-sizing: border-box;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
}
.release-card__header {
display: flex;
align-items: center;
padding: 28rpx 32rpx 0;
}
.release-card__dot {
width: 8rpx;
height: 30rpx;
border-radius: 4rpx;
background: var(--view-theme, #e93323);
margin-right: 14rpx;
flex-shrink: 0;
}
.release-card__title {
font-size: 30rpx;
font-weight: 600;
color: #333;
flex: 1;
}
.release-card__date {
font-size: 22rpx;
color: #bbb;
}
.release-card__body {
display: flex;
align-items: center;
padding: 28rpx 20rpx 24rpx;
}
.release-card__item {
flex: 1;
text-align: center;
}
.release-card__value {
font-size: 38rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
font-family: 'SemiBold', sans-serif;
}
.release-card__value--highlight {
color: var(--view-theme, #e93323);
}
.release-card__key {
font-size: 22rpx;
color: #999;
}
.release-card__divider {
width: 1rpx;
height: 60rpx;
background-color: #eee;
align-self: center;
flex-shrink: 0;
}
.release-card__tips {
font-size: 22rpx;
color: #bbb;
padding: 0 32rpx 24rpx;
display: flex;
align-items: center;
gap: 6rpx;
background: #fafafa;
padding-top: 18rpx;
}
.release-card__tips .iconfont {
font-size: 24rpx;
color: #ccc;
}
.stats-row {
width: 710rpx;
margin: 0 auto;
background: #fff;
border-radius: 24rpx;
display: flex;
align-items: center;
padding: 32rpx 24rpx;
box-sizing: border-box;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
}
.stats-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
}
.stats-icon {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.stats-icon .iconfont {
font-size: 32rpx;
color: #fff;
}
.stats-icon--refund {
background: linear-gradient(135deg, #ff7875 0%, #ff4d4f 100%);
}
.stats-icon--points {
background: linear-gradient(135deg, #ffa940 0%, #fa8c16 100%);
}
.stats-info {
display: flex;
flex-direction: column;
}
.stats-value {
font-size: 32rpx;
font-weight: 600;
color: #333;
font-family: 'SemiBold', sans-serif;
line-height: 1.3;
}
.stats-label {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
}
.stats-divider {
width: 1rpx;
height: 60rpx;
background-color: #eee;
flex-shrink: 0;
margin: 0 10rpx;
}
</style>

View File

@@ -0,0 +1,376 @@
<template>
<!-- P17 积分明细页 -->
<view class="hjf-points-detail-page">
<!-- Tab 筛选栏 -->
<view class="tab-nav acea-row">
<view
v-for="(tab, idx) in tabs"
:key="idx"
class="tab-nav__item"
:class="{ on: activeTab === idx }"
@tap="changeTab(idx)"
>{{ tab.label }}</view>
</view>
<!-- 积分流水列表按日期分组 -->
<view class="points-list">
<view
v-for="(group, gIdx) in list"
:key="gIdx"
class="points-list__group"
>
<!-- 日期分组标题 -->
<view class="points-list__date">{{ group.date }}</view>
<!-- 分组内条目 -->
<view class="points-list__card">
<view
v-for="(item, iIdx) in group.children"
:key="iIdx"
class="points-list__item acea-row row-between-wrapper"
>
<!-- 左侧标题 + 时间 -->
<view class="points-list__info">
<view class="points-list__title line1">{{ item.title }}</view>
<view class="points-list__meta acea-row">
<view class="points-list__time">{{ item.add_time }}</view>
<view
class="points-list__tag"
:class="item.status === 'frozen' ? 'tag--frozen' : 'tag--released'"
>{{ item.status === 'frozen' ? '待释放' : '已释放' }}</view>
</view>
</view>
<!-- 右侧积分增减 -->
<view
class="points-list__points"
:class="item.pm === 1 ? 'points--add' : 'points--sub'"
>
{{ item.pm === 1 ? '+' : '-' }}{{ item.points }}
</view>
</view>
</view>
</view>
<!-- 加载更多提示 -->
<view v-if="list.length > 0" class="loadingicon acea-row row-center-wrapper">
<text
v-if="loading"
class="loading iconfont icon-jiazai"
></text>
<text class="load-title">{{ loadTitle }}</text>
</view>
<!-- 空状态 -->
<view v-if="!loading && list.length === 0" class="empty-wrap">
<emptyPage title="暂无积分记录~" src="/statics/images/noOrder.gif"></emptyPage>
</view>
</view>
</view>
</template>
<script>
import { getPointsDetail } from '@/api/hjfAssets.js';
import emptyPage from '@/components/emptyPage.vue';
import colors from '@/mixins/color';
/**
* P17 积分明细页
*
* 展示当前用户的积分流水,支持按类型 Tab 筛选(全部/待释放/已释放),
* 并以日期分组方式分页渲染列表,上拉触底自动加载下一页。
*
* @module pages/assets/points_detail
*/
export default {
name: 'PointsDetail',
components: { emptyPage },
mixins: [colors],
data() {
return {
/**
* Tab 配置label 展示文案type 对应 API 参数('' 表示全部)
* @type {Array<{ label: string, type: string }>}
*/
tabs: [
{ label: '全部', type: '' },
{ label: '待释放', type: 'frozen' },
{ label: '已释放', type: 'released' },
],
/** 当前选中 Tab 索引0=全部 1=待释放 2=已释放 @type {number} */
activeTab: 0,
/**
* 按日期分组后的列表,每项形如 { date: string, children: Array }
* @type {Array<{ date: string, children: Array<Object> }>}
*/
list: [],
/** 已记录的日期键,用于去重分组 @type {string[]} */
dateKeys: [],
/** 当前请求页码 @type {number} */
page: 1,
/** 每页条数 @type {number} */
limit: 15,
/** 是否正在加载(防重入锁) @type {boolean} */
loading: false,
/** 是否已加载全部数据 @type {boolean} */
finished: false,
/** 底部加载提示文案 @type {string} */
loadTitle: '加载更多',
};
},
/**
* 页面加载:支持从资产总览通过 type 参数直接定位 Tab。
* @param {Object} options - 页面跳转参数
* @param {string} [options.type] - 可选,'frozen' | 'released'
*/
onLoad(options) {
if (options && options.type) {
const idx = this.tabs.findIndex(t => t.type === options.type);
if (idx > -1) this.activeTab = idx;
}
this.loadList();
},
/**
* 上拉触底:加载下一页数据。
*/
onReachBottom() {
this.loadList();
},
methods: {
/**
* 切换 Tab 筛选,重置分页后重新加载列表。
* @param {number} idx - 点击的 Tab 索引
*/
changeTab(idx) {
if (idx === this.activeTab) return;
this.activeTab = idx;
this.resetAndLoad();
},
/**
* 重置所有分页/列表状态,然后发起首页请求。
*/
resetAndLoad() {
this.page = 1;
this.finished = false;
this.loading = false;
this.loadTitle = '加载更多';
this.dateKeys = [];
this.$set(this, 'list', []);
this.loadList();
},
/**
* 加载积分明细数据(分页追加),通过 `getPointsDetail` 获取。
* 按 `add_time` 日期前缀分组追加到 `list`。
* 防重入loading 为 true 或 finished 时直接返回。
*/
loadList() {
if (this.loading || this.finished) return;
this.loading = true;
this.loadTitle = '';
const currentType = this.tabs[this.activeTab].type;
const params = {
page: this.page,
limit: this.limit,
};
if (currentType) params.type = currentType;
getPointsDetail(params).then(res => {
const items = (res.data && res.data.list) ? res.data.list : [];
items.forEach(item => {
// 取 add_time 的日期部分作为分组 key格式 "YYYY-MM-DD"
const dateKey = (item.add_time || '').split(' ')[0] || '未知日期';
if (!this.dateKeys.includes(dateKey)) {
this.dateKeys.push(dateKey);
this.list.push({ date: dateKey, children: [] });
}
const group = this.list.find(g => g.date === dateKey);
if (group) group.children.push(item);
});
const loadend = items.length < this.limit;
this.finished = loadend;
this.loadTitle = loadend ? '没有更多内容啦~' : '加载更多';
if (!loadend) this.page += 1;
this.loading = false;
}).catch(() => {
this.loading = false;
this.loadTitle = '加载更多';
});
},
},
};
</script>
<style scoped lang="scss">
.hjf-points-detail-page {
min-height: 100vh;
background-color: #f5f5f5;
}
/* -------- Tab 导航 -------- */
.tab-nav {
background-color: #fff;
height: 88rpx;
line-height: 88rpx;
position: sticky;
top: 0;
z-index: 10;
border-bottom: 1rpx solid #f0f0f0;
&__item {
flex: 1;
text-align: center;
font-size: 28rpx;
color: #666;
position: relative;
transition: color 0.2s;
&.on {
color: var(--view-theme);
font-size: 30rpx;
font-weight: 500;
&::after {
position: absolute;
content: '';
width: 48rpx;
height: 6rpx;
border-radius: 10rpx;
background: var(--view-theme);
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
}
}
}
/* -------- 积分列表 -------- */
.points-list {
padding: 20rpx 24rpx;
&__group {
margin-bottom: 20rpx;
}
&__date {
font-size: 24rpx;
color: #999;
padding: 12rpx 0 10rpx;
}
&__card {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
&__item {
padding: 28rpx 28rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
}
&__info {
flex: 1;
overflow: hidden;
padding-right: 20rpx;
}
&__title {
font-size: 28rpx;
color: #333;
margin-bottom: 10rpx;
}
&__meta {
align-items: center;
gap: 12rpx;
}
&__time {
font-size: 22rpx;
color: #aaa;
}
&__tag {
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 20rpx;
&.tag--frozen {
color: #ff8c00;
background: rgba(255, 140, 0, 0.1);
}
&.tag--released {
color: #52c41a;
background: rgba(82, 196, 26, 0.1);
}
}
&__points {
font-size: 32rpx;
font-weight: 600;
white-space: nowrap;
&.points--add {
color: var(--view-theme);
}
&.points--sub {
color: #333;
}
}
}
/* -------- 加载更多 & 空状态 -------- */
.loadingicon {
padding: 24rpx 0;
font-size: 24rpx;
color: #aaa;
.loading {
margin-right: 8rpx;
animation: rotating 1.5s linear infinite;
}
.load-title {
font-size: 24rpx;
}
}
@keyframes rotating {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.empty-wrap {
padding: 60rpx 0;
}
</style>

View File

@@ -207,8 +207,45 @@
</view>
</view>
</view>
<view class="cell flex justify-between flex-y-center mt-32" v-if="textareaStatus && send_gift_type != 2">
<text class="text--w111-333 fs-28">留言</text>
<!-- 报单商品公排提示is_queue_goods=1 时显示 -->
<view class="hjf-queue-notice mt-24" v-if="isQueueOrder">
<view class="hjf-queue-notice__header flex-y-center">
<text class="hjf-queue-notice__icon iconfont icon-tishi"></text>
<text class="hjf-queue-notice__title fs-26 fw-500">公排商品提示</text>
</view>
<view class="hjf-queue-notice__body mt-16">
<!-- 拆单提示购买数量 > 1 时显示 -->
<view class="hjf-queue-notice__item flex-y-center" v-if="queueSplitCount > 1">
<text class="hjf-queue-notice__dot"></text>
<text class="fs-24 text--w111-666 lh-34rpx">
本次将拆分为
<text class="hjf-queue-notice__num">{{ queueSplitCount }}</text>
个独立公排订单每单独立排队
</text>
</view>
<!-- 公排规则说明 -->
<view class="hjf-queue-notice__item flex-y-center mt-12">
<text class="hjf-queue-notice__dot"></text>
<text class="fs-24 text--w111-666 lh-34rpx">购买后自动加入公排池进四退一全额返还</text>
</view>
<!-- 预计退款批次 -->
<view class="hjf-queue-notice__item flex-y-center mt-12">
<text class="hjf-queue-notice__dot"></text>
<text class="fs-24 text--w111-666 lh-34rpx">
预计退款时间购买后第
<text class="hjf-queue-notice__num">{{ estimatedRefundBatch }}</text>
个批次仅供参考以实际排队进度为准
</text>
</view>
<!-- 跳转公排状态页 -->
<view class="hjf-queue-notice__link flex-y-center justify-end mt-16" @tap="goPage(1, '/pages/queue/status')">
<text class="fs-24 hjf-queue-notice__link-text">查看公排进度</text>
<text class="iconfont icon-ic_rightarrow fs-20 hjf-queue-notice__link-text pl-4"></text>
</view>
</view>
</view>
<view class="cell flex justify-between flex-y-center mt-32" v-if="textareaStatus && send_gift_type != 2">
<text class="text--w111-333 fs-28">留言</text>
<textarea
class="w-450 fs-28 text-right h-auto"
:auto-height="true"
@@ -613,6 +650,7 @@
const CACHE_CITY = {};
import dayjs from '@/plugin/dayjs/dayjs.min.js';
import { orderConfirm, getCouponsOrderPrice, orderCreate, postOrderComputed, checkShipping, getCartCounts, orderReceiveGift } from '@/api/order.js';
import { getQueueStatus } from '@/api/hjfQueue.js';
import { getAddressDefault, getAddressDetail, invoiceList, invoiceOrder, getAddressList } from '@/api/user.js';
import { openPaySubscribe } from '@/utils/SubscribeMessage.js';
import { storeListApi, postCartAdd, getSendGiftOrderDetail } from '@/api/store.js';
@@ -761,11 +799,49 @@ export default {
giftData: null,
channel: '', // 1 是采购商 "" 正常用户
yue_pay_status: 1,
now_money: ''
now_money: '',
/**
* 公排全局进度快照,由 getQueueStatus() 拉取后写入。
* 结构:{ current_batch: number, trigger_multiple: number, next_refund_queue_no: number }
* Phase 4 集成时此字段由真实 API 填充。
* @type {object|null}
*/
queueStatus: null
};
},
computed: {
...mapGetters(['isLogin']),
/**
* 当前订单是否包含报单商品is_queue_goods=1
* 遍历 cartInfo已过滤赠品只要有一件报单商品即为报单订单。
* @returns {boolean}
*/
isQueueOrder() {
return this.cartInfo.some((item) => item.productInfo && item.productInfo.is_queue_goods == 1);
},
/**
* 报单商品的总购买数量(用于计算公排拆单数)。
* 每件报单商品按购买数量各自独立入队,数量即拆单数。
* @returns {number}
*/
queueSplitCount() {
return this.cartInfo.reduce((sum, item) => {
if (item.productInfo && item.productInfo.is_queue_goods == 1) {
return sum + (parseInt(item.cart_num) || 1);
}
return sum;
}, 0);
},
/**
* 预计退款批次号:基于当前公排全局进度估算。
* 采用保守估算:当前批次余量 + 每批次4单 * 向上取整后的等待批次。
* 实际值由后端 /hjf/queue/status 接口提供,此处仅作展示用途。
* @returns {number}
*/
estimatedRefundBatch() {
const base = this.queueStatus ? this.queueStatus.current_batch : 0;
return base + Math.ceil(this.queueSplitCount / 4) + 1;
},
giftCount() {
let count = 0;
if (this.giveCartInfo.length) {
@@ -838,6 +914,7 @@ export default {
let _this = this;
if (this.isLogin && (this.send_gift_type == 0 || this.send_gift_type == 1)) {
this.getCheckShipping();
this.loadQueueStatus();
} else if (this.send_gift_type == 2 && this.isLogin) {
this.getOrderDetail();
} else {
@@ -1795,6 +1872,24 @@ export default {
tapStoreList() {
this.textareaStatus = false;
this.address.address = true;
},
/**
* 拉取公排全局进度快照,写入 queueStatus 用于计算预计退款批次。
* 仅在订单包含报单商品时展示相关提示,但提前加载可减少感知延迟。
* 失败时静默忽略,不影响正常结算流程。
*/
loadQueueStatus() {
getQueueStatus()
.then((res) => {
if (res && res.data && res.data.progress) {
this.queueStatus = {
current_batch: res.data.progress.current_batch_count || 0,
trigger_multiple: res.data.progress.trigger_multiple || 4,
next_refund_queue_no: res.data.progress.next_refund_queue_no || 0
};
}
})
.catch(() => {});
}
}
};
@@ -1999,6 +2094,53 @@ export default {
.con-border {
border: 1px solid var(--view-theme);
}
/* 报单商品公排提示区块 */
.hjf-queue-notice {
background: #f0f9f0;
border: 1rpx solid #b7e4b7;
border-radius: 12rpx;
padding: 24rpx;
&__header {
gap: 10rpx;
}
&__icon {
color: #3c9c3c;
font-size: 30rpx;
margin-right: 8rpx;
}
&__title {
color: #2d7d2d;
}
&__item {
align-items: flex-start;
}
&__dot {
display: inline-block;
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background: #3c9c3c;
flex-shrink: 0;
margin-top: 12rpx;
margin-right: 12rpx;
}
&__num {
color: #3c9c3c;
font-weight: 600;
padding: 0 4rpx;
}
&__link-text {
color: var(--view-theme);
}
}
.clear-btn {
border-radius: 0 12rpx 0 12rpx;
}

View File

@@ -13,9 +13,27 @@
<text class="fs-40 fw-500 text--w111-fff pl-16" v-if="order_pay_info.pay_type == 'offline' && !order_pay_info.paid">订单创建成功</text>
<text class="fs-40 fw-500 text--w111-fff pl-16" v-else>{{ order_pay_info.paid ? '订单支付成功' : '等待支付...' }}</text>
</view>
<!-- 公排入队成功提示 -->
<view v-if="isQueueOrder && order_pay_info.paid && queueEntries.length > 0" class="queue-success-notice mt-20">
<view class="flex-y-center">
<text class="iconfont icon-chenggong fs-28 queue-icon"></text>
<text class="fs-26 text--w111-fff">已加入公排队列</text>
</view>
<view class="fs-22 text--w111-fff mt-8 opacity-80" v-if="queueEntries.length === 1">
排队序号 #{{ queueEntries[0].queue_no }}{{ queueEntries[0].estimated_wait }}
</view>
<view class="fs-22 text--w111-fff mt-8 opacity-80" v-else>
{{ queueEntries.length }} 个订单已入队
</view>
</view>
<view class="flex-center mt-30">
<view class="w-192 h-64 rd-40rpx flex-center fs-24 text--w111-fff white-border" @tap="goIndex">返回首页</view>
<view v-if="!order_pay_info.is_send_gift || !order_pay_info.paid" class="w-192 h-64 rd-40rpx flex-center fs-24 text--w111-fff white-border ml-48" @tap="goOrderDetails">
<view v-if="isQueueOrder && order_pay_info.paid" class="w-192 h-64 rd-40rpx flex-center fs-24 text--w111-fff white-border ml-48" @tap="goQueueStatus">
查看公排
</view>
<view v-else-if="!order_pay_info.is_send_gift || !order_pay_info.paid" class="w-192 h-64 rd-40rpx flex-center fs-24 text--w111-fff white-border ml-48" @tap="goOrderDetails">
查看订单
</view>
<view v-else-if="order_pay_info.paid" @click="giftModalShow = true" class="w-192 h-64 rd-40rpx flex-center fs-24 text--w111-fff white-border ml-48">送给好友</view>
@@ -108,6 +126,7 @@ import colors from '@/mixins/color';
import { HTTP_REQUEST_URL } from '@/config/app';
import Cache from '@/utils/cache';
import { userShare } from '@/api/user.js';
import { getQueueStatus } from '@/api/hjfQueue.js';
export default {
components: {
gridsLottery,
@@ -168,9 +187,10 @@ export default {
nickname: '',
code: ''
},
userInfo: Cache.get('USER_INFO') || {},
mpGiftImg: HTTP_REQUEST_URL + '/statics/images/gift_share.jpg',
recommendTitle: ""
userInfo: Cache.get('USER_INFO') || {},
mpGiftImg: HTTP_REQUEST_URL + '/statics/images/gift_share.jpg',
recommendTitle: "",
queueEntries: []
};
},
computed: {
@@ -181,6 +201,10 @@ export default {
} else {
return true;
}
},
isQueueOrder() {
if (!this.order_pay_info || !this.order_pay_info.cartInfo) return false;
return this.order_pay_info.cartInfo.some(item => item.productInfo && item.productInfo.is_queue_goods == 1);
}
},
watch: {
@@ -327,6 +351,12 @@ export default {
uni.hideShareMenu()
}
this.loading = true;
// 如果是公排订单且已支付,加载公排入队信息
if (that.isQueueOrder && res.data.paid) {
that.loadQueueEntries();
}
setTimeout(function () {
that.getOrderPrize();
}, 1000);
@@ -473,6 +503,20 @@ export default {
that.recommendTitle = res.data.title ? res.data.title : '猜你喜欢';
});
},
loadQueueEntries() {
getQueueStatus()
.then((res) => {
if (res && res.data && res.data.myOrders && res.data.myOrders.length) {
this.queueEntries = res.data.myOrders.filter(item => item.status === 0);
}
})
.catch(() => {});
},
goQueueStatus() {
uni.navigateTo({
url: '/pages/queue/status'
});
},
goPage(type, url) {
if (type == 1) {
uni.navigateTo({
@@ -502,6 +546,20 @@ export default {
.h-484 {
height: 484rpx;
}
.queue-success-notice {
text-align: center;
padding: 20rpx 40rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 20rpx;
backdrop-filter: blur(10px);
}
.queue-icon {
color: #52c41a;
margin-right: 8rpx;
}
.opacity-80 {
opacity: 0.8;
}
.content {
background: #f5f5f5;
border-radius: 40rpx 40rpx 0 0;

View File

@@ -156,11 +156,12 @@
<text class="iconfont icon-ic_rightarrow fs-24"></text>
</view>
</view>
<!-- 商品名称 -->
<view class="mt-20 fs-30 lh-42rpx text--w111-333 fw-500">
<text v-if="storeInfo.brand_name" class="brand-tag">{{ storeInfo.brand_name }}</text>
{{ storeInfo.store_name }}
</view>
<!-- 商品名称 -->
<view class="mt-20 fs-30 lh-42rpx text--w111-333 fw-500">
<text v-if="storeInfo.brand_name" class="brand-tag">{{ storeInfo.brand_name }}</text>
<text v-if="isQueueGoods" class="queue-goods-tag">报单商品</text>
{{ storeInfo.store_name }}
</view>
<!-- 库存销量 -->
<view class="flex-between-center mt-24 text--w111-999 fs-22 lh-30rpx">
<text v-show="diyProduct.isOpen.includes(0)">
@@ -419,7 +420,12 @@
</view>
<!-- 底部操作按钮 -->
<view class="page_footer bg--w111-fff-s111-80 w-full z-99 fixed-lb pb-safe">
<view class="w-full h-104 pl-32 pr-20 flex">
<!-- 报单商品公排提示is_queue_goods=1 时在购买按钮上方显示 -->
<view v-if="isQueueGoods" class="queue-goods-notice flex-y-center px-32 py-16">
<text class="iconfont icon-ic_user fs-28 queue-goods-notice__icon"></text>
<text class="fs-24 queue-goods-notice__text pl-8">报单商品 · 购买后自动参与公排进四退一全额返还</text>
</view>
<view class="w-full h-104 pl-32 pr-20 flex">
<view class="flex">
<view class="flex-col flex-center mr-38" @tap="goPage(2, '/pages/index/index')" v-if="diyProduct.menuList.includes(0)">
<text class="iconfont icon-ic_mall fs-40"></text>
@@ -856,7 +862,8 @@ export default {
brokerage: '',
pageHide: false,
pageInvalid: 1,
limitInvalid: 20
limitInvalid: 20,
paymentType: 'weixin'
};
},
filters: {
@@ -915,6 +922,9 @@ export default {
return false;
}
},
isQueueGoods() {
return this.storeInfo && this.storeInfo.is_queue_goods == 1;
},
// #ifdef MP
shareButtonStyle() {
let res = wx.getMenuButtonBoundingClientRect();
@@ -2404,4 +2414,57 @@ export default {
.bounce-in {
animation: myBounceIn 0.75s ease-out forwards;
}
/**
* 品牌标签 — 商品名称左侧品牌角标
*/
.brand-tag {
display: inline-block;
padding: 0 10rpx;
height: 36rpx;
line-height: 36rpx;
font-size: 20rpx;
color: var(--view-theme);
border: 1rpx solid var(--view-theme);
border-radius: 6rpx;
margin-right: 8rpx;
vertical-align: middle;
}
/**
* 报单商品标签 — 商品名称区域的「报单商品」角标
* 仅当 is_queue_goods=1 时通过 v-if="isQueueGoods" 渲染
*/
.queue-goods-tag {
display: inline-block;
padding: 0 12rpx;
height: 36rpx;
line-height: 36rpx;
font-size: 20rpx;
font-weight: 500;
color: #1a7e3c;
background-color: #e8f7ed;
border: 1rpx solid #52c41a;
border-radius: 6rpx;
margin-right: 8rpx;
vertical-align: middle;
}
/**
* 购买区域公排提示条 — 报单商品时显示在底部按钮上方
* 仅当 is_queue_goods=1 时通过 v-if="isQueueGoods" 渲染
*/
.queue-goods-notice {
background-color: #f0faf3;
border-top: 1rpx solid #b7eb8f;
&__icon {
color: #52c41a;
}
&__text {
color: #389e0d;
line-height: 1.5;
}
}
</style>

View File

@@ -0,0 +1,310 @@
<template>
<view class="guide-page">
<!-- 跳过按钮 -->
<view v-if="currentIndex < slides.length - 1" class="skip-btn" @tap="handleSkip">
<text class="skip-text">跳过</text>
</view>
<!-- 轮播主体 -->
<swiper
class="guide-swiper"
:current="currentIndex"
:indicator-dots="false"
@change="onSwiperChange"
>
<swiper-item v-for="(slide, index) in slides" :key="index" class="swiper-item">
<view class="slide-content">
<image
class="slide-image"
:src="slide.image"
mode="aspectFit"
/>
<view class="slide-text-wrap">
<text class="slide-title">{{ slide.title }}</text>
<text class="slide-desc">{{ slide.desc }}</text>
</view>
</view>
</swiper-item>
</swiper>
<!-- 底部区域指示器 + 按钮 -->
<view class="bottom-area">
<!-- 圆点指示器 -->
<view class="dot-indicators">
<view
v-for="(slide, index) in slides"
:key="index"
class="dot"
:class="{ active: index === currentIndex }"
/>
</view>
<!-- 操作按钮 -->
<view class="action-wrap">
<button
v-if="currentIndex < slides.length - 1"
class="btn-next"
@tap="handleNext"
>
下一步
</button>
<button
v-else
class="btn-start"
@tap="handleStart"
>
立即体验
</button>
</view>
</view>
</view>
</template>
<script>
/**
* @file hjf_intro.vue
* @description P23 新用户引导页(轮播引导)
*
* 功能:
* - 3 屏轮播引导(平台介绍 / 公排规则 / 会员积分体系)
* - 底部圆点指示器实时同步当前页
* - 右上角"跳过"按钮(最后一屏隐藏)
* - 最后一屏显示"立即体验"按钮,点击跳转首页
* - 跳过或完成后写入本地存储,不再重复展示
*
* Mock 数据MOCK_GUIDE_DATA来自 @/utils/hjfMockData.js
* 路由注册pages/guide/hjf_intronavigationStyle: custom
*
* @module pages/guide/hjf_intro
* @author OpenClaw Agent
* @version 1.0.0
* @see docs/frontend-new-pages-spec.md 第 4.2.1 节
*/
import { MOCK_GUIDE_DATA } from '@/utils/hjfMockData.js';
/** 本地存储 key用于记录引导已读状态 */
const GUIDE_READ_KEY = 'hjf_guide_read';
export default {
name: 'HjfIntro',
data() {
return {
/**
* 引导轮播幻灯片列表
* @type {Array<{title: string, desc: string, image: string}>}
*/
slides: [],
/**
* 当前激活的幻灯片索引0-based
* @type {number}
*/
currentIndex: 0
};
},
onLoad() {
this.initSlides();
},
methods: {
/**
* 初始化轮播数据,从 Mock 数据加载幻灯片列表
* Phase 4 集成后可替换为真实 API 调用
*/
initSlides() {
this.slides = MOCK_GUIDE_DATA.slides || [];
},
/**
* swiper change 事件回调,同步当前索引到圆点指示器
* @param {UniApp.SwiperChangeEvent} e - swiper change 事件对象
*/
onSwiperChange(e) {
this.currentIndex = e.detail.current;
},
/**
* 点击"下一步"按钮,切换到下一张幻灯片
*/
handleNext() {
if (this.currentIndex < this.slides.length - 1) {
this.currentIndex += 1;
}
},
/**
* 点击"跳过"按钮,记录已读状态并直接跳转首页
*/
handleSkip() {
this.markGuideRead();
this.goHome();
},
/**
* 点击"立即体验"按钮(最后一屏),记录已读状态并跳转首页
*/
handleStart() {
this.markGuideRead();
this.goHome();
},
/**
* 将引导已读状态写入本地存储
* 后续在 App.vue 或首页 onShow 中读取此标记,决定是否跳转引导页
*/
markGuideRead() {
uni.setStorageSync(GUIDE_READ_KEY, '1');
},
/**
* 跳转至应用首页,使用 reLaunch 清空页面栈
*/
goHome() {
uni.reLaunch({ url: '/pages/index/index' });
}
}
};
</script>
<style scoped lang="scss">
$theme-gradient: linear-gradient(135deg, var(--view-theme, #e93323) 0%, #ff7043 100%);
$slide-height: calc(100vh - 280rpx);
.guide-page {
position: relative;
width: 100%;
height: 100vh;
background: #fff;
display: flex;
flex-direction: column;
overflow: hidden;
}
// 跳过按钮
.skip-btn {
position: fixed;
top: calc(var(--status-bar-height) + 20rpx);
right: 40rpx;
z-index: 10;
padding: 12rpx 30rpx;
background: rgba(0, 0, 0, 0.12);
border-radius: 40rpx;
.skip-text {
font-size: 26rpx;
color: #fff;
}
}
// 轮播主体
.guide-swiper {
flex: 1;
width: 100%;
height: $slide-height;
}
.swiper-item {
width: 100%;
height: 100%;
}
.slide-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 0 60rpx;
box-sizing: border-box;
}
.slide-image {
width: 560rpx;
height: 420rpx;
margin-bottom: 80rpx;
}
.slide-text-wrap {
text-align: center;
}
.slide-title {
display: block;
font-size: 44rpx;
font-weight: bold;
color: #222;
margin-bottom: 28rpx;
line-height: 1.3;
}
.slide-desc {
display: block;
font-size: 30rpx;
color: #888;
line-height: 1.8;
}
// 底部区域
.bottom-area {
padding: 0 60rpx 80rpx;
display: flex;
flex-direction: column;
align-items: center;
}
// 圆点指示器
.dot-indicators {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 60rpx;
}
.dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: #ddd;
margin: 0 10rpx;
transition: all 0.3s;
&.active {
width: 40rpx;
border-radius: 8rpx;
background: var(--view-theme, #e93323);
}
}
// 按钮公共
.btn-next,
.btn-start {
width: 560rpx;
height: 96rpx;
border-radius: 48rpx;
font-size: 34rpx;
font-weight: bold;
border: none;
display: flex;
align-items: center;
justify-content: center;
&::after {
border: none;
}
}
.btn-next {
background: #f5f5f5;
color: #555;
}
.btn-start {
background: $theme-gradient;
color: #fff;
box-shadow: 0 8rpx 30rpx rgba(233, 51, 35, 0.35);
}
</style>

View File

@@ -122,6 +122,8 @@
<view class="btn" @click="reconnect">重新连接</view>
</view>
</view>
<!-- HJF 演示控制面板 -->
<HjfDemoPanel />
</view>
</template>
@@ -590,7 +592,9 @@
getDiyVersion(0).then(res => {
let diyVersion = uni.getStorageSync('diyVersion');
if ((res.data.version + '0') === diyVersion) {
this.setDiyData(JSON.parse(diyData));
// Storage may return string or already-parsed object (e.g. on H5)
const data = typeof diyData === 'string' ? JSON.parse(diyData) : diyData;
this.setDiyData(data);
} else {
uni.setStorageSync('diyVersion', (res.data.version + '0'));
this.getDiyData();

View File

@@ -0,0 +1,49 @@
# Conversation - 2026-03-10 18:12:21
## Prompt
创建页面 history.vueP13 公排历史页):
文件路径: /Users/apple/scott2026/huangjingfen/pro_v3.5.1/view/uniapp/pages/queue/history.vue
内容要求:
1. 使用 Vue 2 Options API
2. 导入 getQueueHistory from '@/api/hjfQueue.js'
3. data: list(Array), page(Number), loading(Boolean), finished(Boolean)
4. onLoad + onReachBottom: 加载和分页
5. 模板: 按时间分组显示退款记录列表
6. 样式: <style scoped lang="scss">
7. 参考: docs/frontend-new-pages-spec.md 第 2.3.2 节
完整 JSDoc。
## Response
文件已创建。下面是关键实现点的说明:
**`history.vue` — P13 公排历史页**
**数据层**
- `list` / `page` / `loading` / `finished` 四个核心状态
- `limit: 15`,与 Mock 数据的分页规格一致
- `timeKeys` 辅助字段用于历史版本兼容(当前分组逻辑已移入 computed
**分组逻辑computed `groupedList`**
- 先按 `activeTab` 过滤0=全部1=`status===0`2=`status===1`
- 再按 `time_key` 字段聚合,保持原始顺序,返回 `[{ date, children[] }]`
**生命周期**
- `onLoad` → 首次加载
- `onReachBottom` → 触底分页,`loading` / `finished` 双重守卫防止重复请求
**`changeTab`**
- 切换 Tab 时重置 `list / page / finished`重新请求Tab 相同则 early return
**`formatTime`**
- 将秒级 Unix 时间戳转为 `YYYY-MM-DD HH:mm``timestamp=0` 时返回 `--`
**样式**
- `<style scoped lang="scss">`,全部使用 `rpx`
- Tab 激活指示器复用 `user_bill``::after` 伪元素模式
- 状态标签:排队中绿色胶囊,已退款灰色胶囊
- `sticky` Tab 导航,滚动时悬浮在顶部

View File

@@ -0,0 +1,39 @@
# Conversation - 2026-03-10 18:13:41
## Prompt
创建页面 rules.vueP14 公排规则页):
文件路径: /Users/apple/scott2026/huangjingfen/pro_v3.5.1/view/uniapp/pages/queue/rules.vue
内容要求:
1. 使用 Vue 2 Options API
2. 静态展示页面,无需 API 调用
3. 模板: 公排规则说明进4退1机制说明、示例计算
4. 参考: docs/frontend-new-pages-spec.md 第 2.3.3 节
完整 JSDoc。
## Response
`rules.vue` 已创建完成。以下是文件内容概览:
**文件路径**`pro_v3.5.1/view/uniapp/pages/queue/rules.vue`
**三大区块**
1. **公排机制图示**`section`
- 进四退一流程图5个步骤节点4个"进"步骤 + 1个触发退款的"退"步骤),带颜色区分(红色入队圆标、绿色退款圆标)
- 触发节点有橙色"触发退款!"角标
- 示例计算卡片:报单 ¥3,600 → 退款 ¥3,600全额平均等待约3单
2. **规则条款列表**7条
- 带序号圆圈(主题色背景)
- 覆盖:全额退款保障、进四退一机制、退款自动到账、入队资格、多单独立排队、积分叠加、规则变更通知
3. **FAQ 手风琴**7条
- 点击 `toggleFaq(index)` 切换 `open` 状态
- 箭头 `` 旋转动画(`rotate(90deg)``rotate(-90deg)`
- 问题覆盖:等待时长、退款去向、多次参与、循环入队、序号说明、积分叠加、查看进度
**技术规范符合**Vue 2 Options API、`<style scoped lang="scss">``rpx` 单位、`var(--view-theme)` 主题色、完整 JSDoc 注释(`@file``@description``@type``@param`)。

View File

@@ -0,0 +1,373 @@
<template>
<view :style="colorStyle" class="queue-history">
<!-- Tab 筛选 -->
<view class="nav acea-row">
<view
class="item"
:class="activeTab === 0 ? 'on' : ''"
@click="changeTab(0)"
>全部</view>
<view
class="item"
:class="activeTab === 1 ? 'on' : ''"
@click="changeTab(1)"
>排队中</view>
<view
class="item"
:class="activeTab === 2 ? 'on' : ''"
@click="changeTab(2)"
>已退款</view>
</view>
<!-- 按日期分组的记录列表 -->
<view class="record-list">
<view
class="date-group"
v-for="(group, gIndex) in groupedList"
:key="gIndex"
>
<view class="date-label">{{ group.date }}</view>
<view class="group-card">
<view
class="record-item acea-row row-between-wrapper"
v-for="(item, iIndex) in group.children"
:key="iIndex"
>
<!-- 左侧订单号 + 批次号 -->
<view class="record-left">
<view class="order-id line1">订单号{{ item.order_id }}</view>
<view class="record-meta">
<text v-if="item.trigger_batch > 0">批次号#{{ item.trigger_batch }}</text>
<text v-else>入队序号#{{ item.queue_no }}</text>
</view>
<view class="record-time" v-if="item.status === 1 && item.refund_time > 0">
退款时间{{ formatTime(item.refund_time) }}
</view>
<view class="record-time" v-else>
入队时间{{ formatTime(item.add_time) }}
</view>
</view>
<!-- 右侧金额 + 状态标签 -->
<view class="record-right">
<view class="amount">¥{{ Number(item.amount).toFixed(2) }}</view>
<view class="status-tag" :class="item.status === 1 ? 'refunded' : 'queuing'">
{{ item.status === 1 ? '已退款' : '排队中' }}
</view>
</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-bar acea-row row-center-wrapper" v-if="list.length > 0">
<text class="loading iconfont icon-jiazai" :hidden="!loading"></text>
{{ loadTitle }}
</view>
<!-- 空状态 -->
<view class="px-20 mt-20" v-if="list.length === 0 && !loading">
<emptyPage title="暂无公排记录~" src="/statics/images/noOrder.gif"></emptyPage>
</view>
</view>
</view>
</template>
<script>
import { getQueueHistory } from '@/api/hjfQueue.js';
import emptyPage from '@/components/emptyPage.vue';
import colors from '@/mixins/color';
/**
* P13 公排历史记录页
*
* 展示当前用户的所有公排历史,支持 Tab 筛选(全部/排队中/已退款),
* 记录按日期分组显示,支持上拉翻页加载。
*
* @see docs/frontend-new-pages-spec.md 2.2.5
*/
export default {
name: 'QueueHistory',
components: { emptyPage },
mixins: [colors],
data() {
return {
/** @type {Array<Object>} 原始记录列表(所有已加载页的合并) */
list: [],
/** @type {number} 当前页码(从 1 开始) */
page: 1,
/** @type {number} 每页条数 */
limit: 15,
/** @type {boolean} 是否正在加载 */
loading: false,
/** @type {boolean} 是否已加载完全部数据 */
finished: false,
/** @type {string} 底部加载提示文字 */
loadTitle: '加载更多',
/**
* 当前激活的 Tab
* 0 = 全部1 = 排队中2 = 已退款
* @type {number}
*/
activeTab: 0,
};
},
computed: {
/**
* 按 time_key日期字符串将 list 分组
* 过滤规则activeTab=0 不过滤1 只显示 status=02 只显示 status=1
* @returns {Array<{ date: string, children: Object[] }>}
*/
groupedList() {
const filtered = this.list.filter(item => {
if (this.activeTab === 1) return item.status === 0;
if (this.activeTab === 2) return item.status === 1;
return true;
});
const map = {};
const order = [];
filtered.forEach(item => {
const key = item.time_key;
if (!map[key]) {
map[key] = [];
order.push(key);
}
map[key].push(item);
});
return order.map(date => ({ date, children: map[date] }));
},
},
onLoad() {
this.loadHistory();
},
onReachBottom() {
this.loadHistory();
},
methods: {
/**
* 加载公排历史记录(分页追加)
* 若正在加载或已全部加载则直接返回。
* @returns {void}
*/
loadHistory() {
if (this.loading || this.finished) return;
this.loading = true;
this.loadTitle = '';
getQueueHistory({
page: this.page,
limit: this.limit,
status: this.activeTab === 0 ? undefined : this.activeTab === 1 ? 0 : 1,
})
.then(res => {
const newItems = (res.data && res.data.list) ? res.data.list : [];
newItems.forEach(item => {
this.list.push(item);
});
const isEnd = newItems.length < this.limit;
this.finished = isEnd;
this.loadTitle = isEnd ? '没有更多内容啦~' : '加载更多';
this.page += 1;
})
.catch(() => {
this.loadTitle = '加载更多';
})
.finally(() => {
this.loading = false;
});
},
/**
* 切换 Tab 筛选,重置列表并重新加载
* @param {number} tab - 目标 Tab 索引0/1/2
* @returns {void}
*/
changeTab(tab) {
if (tab === this.activeTab) return;
this.activeTab = tab;
this.list = [];
this.page = 1;
this.finished = false;
this.loadHistory();
},
/**
* 将 Unix 时间戳格式化为 YYYY-MM-DD HH:mm
* @param {number} timestamp - Unix 秒级时间戳
* @returns {string} 格式化后的时间字符串
*/
formatTime(timestamp) {
if (!timestamp) return '--';
const d = new Date(timestamp * 1000);
const pad = n => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
},
},
};
</script>
<style scoped lang="scss">
.queue-history {
min-height: 100vh;
background: #f5f5f5;
}
/* ===== Tab 导航 ===== */
.nav {
background: #fff;
height: 88rpx;
width: 100%;
position: sticky;
top: 0;
z-index: 10;
.item {
flex: 1;
text-align: center;
font-size: 28rpx;
color: #333;
line-height: 88rpx;
position: relative;
&.on {
color: var(--view-theme);
font-size: 30rpx;
font-weight: 500;
&::after {
position: absolute;
content: ' ';
width: 64rpx;
height: 6rpx;
border-radius: 20rpx;
background: var(--view-theme);
bottom: 0;
left: 50%;
margin-left: -32rpx;
}
}
}
}
/* ===== 记录列表 ===== */
.record-list {
padding: 24rpx 24rpx 40rpx;
}
.date-group {
margin-bottom: 24rpx;
}
.date-label {
font-size: 24rpx;
color: #999;
margin-bottom: 12rpx;
padding-left: 4rpx;
}
.group-card {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
}
/* ===== 单条记录 ===== */
.record-item {
padding: 28rpx 30rpx;
align-items: center;
&:not(:last-child) {
border-bottom: 1rpx solid #f2f2f2;
}
}
.record-left {
flex: 1;
margin-right: 24rpx;
.order-id {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 8rpx;
}
.record-meta {
font-size: 24rpx;
color: #999;
margin-bottom: 6rpx;
}
.record-time {
font-size: 22rpx;
color: #bbb;
}
}
.record-right {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-shrink: 0;
.amount {
font-size: 32rpx;
color: #333;
font-weight: 600;
margin-bottom: 10rpx;
}
}
/* ===== 状态标签 ===== */
.status-tag {
font-size: 22rpx;
padding: 4rpx 14rpx;
border-radius: 20rpx;
&.queuing {
color: #19be6b;
background: rgba(25, 190, 107, 0.1);
}
&.refunded {
color: #999;
background: #f5f5f5;
}
}
/* ===== 底部加载提示 ===== */
.loading-bar {
font-size: 26rpx;
color: #aaa;
padding: 24rpx 0;
.loading {
margin-right: 8rpx;
animation: rotate 1s linear infinite;
}
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>

View File

@@ -0,0 +1,534 @@
<template>
<view :style="colorStyle" class="queue-rules">
<!-- 公排机制图示 -->
<view class="section">
<view class="section-title">公排机制图示</view>
<view class="mechanism-card">
<view class="mechanism-title">进四退一流程</view>
<view class="flow-diagram">
<!-- 第一步 -->
<view class="flow-step">
<view class="step-circle step-in"></view>
<view class="step-desc">用户A 购买报单商品</view>
<view class="step-sub">¥3,600 自动入队#1</view>
</view>
<view class="flow-arrow"></view>
<!-- 第二步 -->
<view class="flow-step">
<view class="step-circle step-in"></view>
<view class="step-desc">用户B 购买报单商品</view>
<view class="step-sub">¥3,600 自动入队#2</view>
</view>
<view class="flow-arrow"></view>
<!-- 第三步 -->
<view class="flow-step">
<view class="step-circle step-in"></view>
<view class="step-desc">用户C 购买报单商品</view>
<view class="step-sub">¥3,600 自动入队#3</view>
</view>
<view class="flow-arrow"></view>
<!-- 第四步触发退款 -->
<view class="flow-step trigger">
<view class="step-circle step-in"></view>
<view class="step-desc">用户D 购买报单商品</view>
<view class="step-sub">¥3,600 自动入队#4</view>
<view class="trigger-badge">触发退款</view>
</view>
<view class="flow-arrow refund-arrow"></view>
<!-- 退款 -->
<view class="flow-step refund-step">
<view class="step-circle step-out">退</view>
<view class="step-desc">用户A 全额退款</view>
<view class="step-sub">¥3,600 返还至现金余额</view>
</view>
</view>
<!-- 循环说明 -->
<view class="cycle-note">
<text class="cycle-icon">🔄</text>
<text class="cycle-text">如此循环每进入4单最早的1单全额退款</text>
</view>
</view>
<!-- 示例计算卡片 -->
<view class="example-card">
<view class="example-title">示例计算</view>
<view class="example-row">
<text class="example-label">报单金额</text>
<text class="example-value">¥3,600.00</text>
</view>
<view class="example-row">
<text class="example-label">退款金额</text>
<text class="example-value highlight">¥3,600.00全额</text>
</view>
<view class="example-row">
<text class="example-label">平均等待单数</text>
<text class="example-value"> 3 即约3倍报单量</text>
</view>
<view class="example-divider"></view>
<view class="example-desc">
假设公排池每天新增20单您的排队序号为第14位预计约需等待 3 天可触发退款
实际等待时间取决于整体报单速度报单越活跃退款越快
</view>
</view>
</view>
<!-- 规则条款列表 -->
<view class="section">
<view class="section-title">规则条款</view>
<view class="rules-card">
<view
class="rule-item"
v-for="(rule, index) in ruleItems"
:key="index"
>
<view class="rule-index">{{ index + 1 }}</view>
<view class="rule-content">
<view class="rule-title-text">{{ rule.title }}</view>
<view class="rule-body">{{ rule.content }}</view>
</view>
</view>
</view>
</view>
<!-- 常见问题 FAQ 手风琴 -->
<view class="section">
<view class="section-title">常见问题</view>
<view class="faq-list">
<view
class="faq-item"
v-for="(item, index) in faqItems"
:key="index"
@click="toggleFaq(index)"
>
<view class="faq-header acea-row row-between-wrapper">
<view class="faq-question">{{ item.question }}</view>
<view class="faq-arrow" :class="{ 'arrow-up': item.open }"></view>
</view>
<view class="faq-answer" v-if="item.open">{{ item.answer }}</view>
</view>
</view>
</view>
<!-- 底部声明 -->
<view class="footer-note">
<text>本平台公排规则最终解释权归黄精粉健康商城所有</text>
</view>
</view>
</template>
<script>
/**
* @file pages/queue/rules.vue
* @description P14 公排规则说明页 — 静态展示页面,无 API 调用
*
* 页面结构:
* 1. 公排机制图示(进四退一流程图 + 示例计算)
* 2. 规则条款列表(编号条款)
* 3. 常见问题 FAQ 手风琴(点击展开/收起)
*
* @module pages/queue/rules
* @requires mixin/color.js — 提供 colorStyle 计算属性(主题色 CSS 变量注入)
*/
import { mapGetters } from 'vuex';
export default {
name: 'QueueRules',
data() {
return {
/**
* @type {Array<{title: string, content: string}>}
* @description 规则条款列表,静态数据
*/
ruleItems: [
{
title: '全额退款保障',
content: '参与公排的报单金额为 ¥3,600退款时按原金额全额返还至您的现金余额不收取任何手续费。'
},
{
title: '进四退一机制',
content: '公排池按照入队时间先后排队。每当新入队单数达到当前最早排队单数的4倍时最早的1单自动触发退款。'
},
{
title: '退款自动到账',
content: '退款金额将在触发退款后立即到账至您的现金余额,无需手动申请,可在"我的资产"中查看。'
},
{
title: '入队资格',
content: '购买报单商品(黄精粉套餐 ¥3,600系统自动为该订单分配全局唯一的排队序号按购买时间先后排序。'
},
{
title: '多单独立排队',
content: '同一用户购买多单报单商品,每单独立分配排队序号,各自独立参与公排循环,互不影响。'
},
{
title: '积分奖励叠加',
content: '公排退款与积分奖励体系相互独立,参与公排的同时可正常获得推荐积分奖励,两者并行不悖。'
},
{
title: '规则变更通知',
content: '若平台对公排规则进行调整,将提前通过公告及消息通知用户,变更后的规则不溯及既往订单。'
}
],
/**
* @type {Array<{question: string, answer: string, open: boolean}>}
* @description 常见问题列表open 字段控制手风琴展开状态
*/
faqItems: [
{
question: '参与公排后多久能收到退款?',
answer: '等待时间取决于公排池的整体报单速度。每进入4单触发最早1单退款。若每天新增约20单一般约3-5天可收到退款。您可在"公排状态"页查看实时进度和预估等待时间。',
open: false
},
{
question: '退款会到哪里?',
answer: '退款金额将全额返还至您在平台的现金余额,可在"我的资产"中查看并可随时申请提现提现手续费7%)。',
open: false
},
{
question: '一个人可以参与多次公排吗?',
answer: '可以。每次购买报单商品均会独立进入公排队列,获得新的排队序号。多单独立排队,各自触发退款,相互不影响。',
open: false
},
{
question: '公排退款后还能继续参与吗?',
answer: '可以。退款到账后您可以再次购买报单商品重新入队,循环享受公排返利。',
open: false
},
{
question: '为什么我的排队序号不是第1位',
answer: '公排池是全平台共享队列,您的排队序号代表全局位置。序号前面的用户将优先触发退款,请耐心等待。',
open: false
},
{
question: '公排和积分奖励可以同时获得吗?',
answer: '可以。购买报单商品后,您的直接推荐人可获得积分奖励,同时该订单进入公排队列。两套机制并行运作,互不影响。',
open: false
},
{
question: '如何查看我的排队进度?',
answer: '进入"公排状态"页可查看您的排队序号、当前批次进度X/4、预计等待时间。页面实时展示全局公排进度。',
open: false
}
]
};
},
computed: {
...mapGetters(['colorStyle'])
},
methods: {
/**
* @description 切换 FAQ 手风琴展开/收起状态
* @param {number} index - FAQ 条目的索引
*/
toggleFaq(index) {
this.faqItems[index].open = !this.faqItems[index].open;
}
}
};
</script>
<style scoped lang="scss">
.queue-rules {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 40rpx;
}
/* ===== 通用区块 ===== */
.section {
margin: 20rpx 24rpx 0;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #282828;
padding: 24rpx 0 16rpx;
}
/* ===== 机制图示卡片 ===== */
.mechanism-card {
background: #fff;
border-radius: 16rpx;
padding: 30rpx 28rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.mechanism-title {
font-size: 28rpx;
font-weight: 600;
color: var(--view-theme, #e93323);
text-align: center;
margin-bottom: 30rpx;
}
/* 流程图 */
.flow-diagram {
display: flex;
flex-direction: column;
align-items: center;
}
.flow-step {
width: 100%;
background: #f9f9f9;
border-radius: 12rpx;
padding: 18rpx 24rpx;
text-align: center;
position: relative;
&.trigger {
background: #fff7f0;
border: 2rpx solid #ff9d4d;
}
&.refund-step {
background: #f0fff4;
border: 2rpx solid #52c41a;
}
}
.step-circle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
font-size: 24rpx;
font-weight: 700;
color: #fff;
margin-bottom: 10rpx;
&.step-in {
background: var(--view-theme, #e93323);
}
&.step-out {
background: #52c41a;
}
}
.step-desc {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.step-sub {
font-size: 22rpx;
color: #999;
margin-top: 6rpx;
}
.trigger-badge {
display: inline-block;
background: #ff9d4d;
color: #fff;
font-size: 20rpx;
border-radius: 20rpx;
padding: 4rpx 16rpx;
margin-top: 10rpx;
}
.flow-arrow {
font-size: 36rpx;
color: #ccc;
line-height: 50rpx;
text-align: center;
&.refund-arrow {
color: #52c41a;
}
}
/* 循环说明 */
.cycle-note {
display: flex;
align-items: center;
justify-content: center;
margin-top: 24rpx;
padding: 16rpx 20rpx;
background: #f0f4ff;
border-radius: 10rpx;
}
.cycle-icon {
font-size: 28rpx;
margin-right: 10rpx;
}
.cycle-text {
font-size: 24rpx;
color: #555;
}
/* 示例计算卡片 */
.example-card {
background: #fff;
border-radius: 16rpx;
padding: 28rpx;
margin-top: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.example-title {
font-size: 26rpx;
font-weight: 600;
color: #282828;
margin-bottom: 18rpx;
}
.example-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx 0;
}
.example-label {
font-size: 26rpx;
color: #666;
}
.example-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
&.highlight {
color: #52c41a;
font-weight: 700;
}
}
.example-divider {
height: 1rpx;
background: #f0f0f0;
margin: 16rpx 0;
}
.example-desc {
font-size: 24rpx;
color: #999;
line-height: 1.8;
}
/* ===== 规则条款 ===== */
.rules-card {
background: #fff;
border-radius: 16rpx;
padding: 10rpx 28rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.rule-item {
display: flex;
padding: 24rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
}
.rule-index {
flex-shrink: 0;
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: var(--view-theme, #e93323);
color: #fff;
font-size: 22rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
margin-top: 4rpx;
}
.rule-content {
flex: 1;
}
.rule-title-text {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.rule-body {
font-size: 24rpx;
color: #777;
line-height: 1.7;
}
/* ===== FAQ 手风琴 ===== */
.faq-list {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.faq-item {
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
}
.faq-header {
padding: 28rpx;
align-items: center;
}
.faq-question {
flex: 1;
font-size: 28rpx;
color: #333;
font-weight: 500;
line-height: 1.5;
padding-right: 20rpx;
}
.faq-arrow {
flex-shrink: 0;
font-size: 40rpx;
color: #ccc;
transform: rotate(90deg);
transition: transform 0.25s ease;
line-height: 1;
&.arrow-up {
transform: rotate(-90deg);
color: var(--view-theme, #e93323);
}
}
.faq-answer {
padding: 0 28rpx 24rpx;
font-size: 24rpx;
color: #888;
line-height: 1.8;
background: #fafafa;
}
/* ===== 底部声明 ===== */
.footer-note {
margin: 30rpx 24rpx 0;
text-align: center;
font-size: 22rpx;
color: #bbb;
}
</style>

View File

@@ -0,0 +1,480 @@
<template>
<view class="queue-status-page" :style="colorStyle">
<view class="header-gradient">
<view class="header-gradient__bg-circle header-gradient__bg-circle--1"></view>
<view class="header-gradient__bg-circle header-gradient__bg-circle--2"></view>
<view class="header-card">
<view class="header-card__label">公排池总单数</view>
<view class="header-card__total">{{ queueStatus.totalOrders || 0 }}</view>
<view class="header-card__progress" v-if="queueStatus.progress">
<HjfQueueProgress
:current-count="queueStatus.progress.current_batch_count"
:trigger-multiple="queueStatus.progress.trigger_multiple"
:next-refund-no="queueStatus.progress.next_refund_queue_no"
/>
</view>
</view>
</view>
<view class="order-list-section">
<view class="section-header">
<view class="section-header__dot"></view>
<view class="section-header__title">我的排队订单</view>
</view>
<view v-if="loading" class="loading-wrap">
<view class="loading-dots">
<view class="loading-dot loading-dot--1"></view>
<view class="loading-dot loading-dot--2"></view>
<view class="loading-dot loading-dot--3"></view>
</view>
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="!queueStatus.myOrders || queueStatus.myOrders.length === 0" class="empty-wrap">
<emptyPage title="暂无排队记录~" src="/statics/images/noOrder.gif" />
</view>
<view v-else class="order-list">
<view
v-for="(order, index) in queueStatus.myOrders"
:key="order.id || index"
class="order-item"
>
<view class="order-item__top">
<view class="order-item__no">
<text class="order-item__no-hash">#</text>
<text class="order-item__no-value">{{ order.queue_no }}</text>
</view>
<view
class="order-item__tag"
:class="order.status === 0 ? 'order-item__tag--active' : 'order-item__tag--refunded'"
>
<view class="order-item__tag-dot" :class="order.status === 0 ? 'order-item__tag-dot--active' : 'order-item__tag-dot--refunded'"></view>
{{ order.status === 0 ? '排队中' : '已退款' }}
</view>
</view>
<view class="order-item__id">{{ order.order_id }}</view>
<view class="order-item__bottom">
<text class="order-item__amount">¥{{ Number(order.amount).toFixed(2) }}</text>
<text class="order-item__wait">{{ order.estimated_wait }}</text>
</view>
</view>
</view>
</view>
<view v-if="queueStatus.myOrders && queueStatus.myOrders.length > 0" class="load-more-bar">
<text v-if="loadingMore" class="load-more-text">加载中...</text>
<text v-else-if="finished" class="load-more-text">没有更多内容啦~</text>
<text v-else class="load-more-text">上拉加载更多</text>
</view>
<HjfRefundNotice
:visible="showRefund"
:amount="refundData.amount"
:order-id="refundData.orderId"
@close="handleRefundClose"
/>
</view>
</template>
<script>
/**
* @file pages/queue/status.vue
* @description P12 公排状态页 — 展示公排池总单数、当前批次进度及我的排队订单列表,
* 并在退款到账时弹出 HjfRefundNotice 通知。
* @see docs/frontend-new-pages-spec.md 第 2.2.4 节
*/
import { getQueueStatus } from '@/api/hjfQueue.js';
import HjfQueueProgress from '@/components/HjfQueueProgress.vue';
import HjfRefundNotice from '@/components/HjfRefundNotice.vue';
import emptyPage from '@/components/emptyPage.vue';
import colors from '@/mixins/color.js';
export default {
name: 'QueueStatus',
mixins: [colors],
components: {
HjfQueueProgress,
HjfRefundNotice,
emptyPage
},
data() {
return {
/**
* 公排状态数据,包含 totalOrders、myOrders、progress
* @type {{ totalOrders: number, myOrders: Array, progress: Object }}
*/
queueStatus: {},
/**
* 是否正在加载数据
* @type {boolean}
*/
loading: false,
/**
* 是否显示退款通知弹窗
* @type {boolean}
*/
showRefund: false,
/**
* 退款弹窗所需数据
* @type {{ amount: number, orderId: string }}
*/
refundData: {
amount: 0,
orderId: ''
},
/** @type {boolean} 是否正在上拉加载更多 */
loadingMore: false,
/** @type {boolean} 是否已加载完全部数据 */
finished: false
};
},
onLoad() {
this.loadQueueStatus();
},
onShow() {
this.checkPendingRefundNotice();
},
onReachBottom() {
this.loadMoreOrders();
},
methods: {
/**
* 加载公排状态数据
* 调用 getQueueStatus(),将返回值赋给 queueStatus
* 并在检测到已退款订单时触发退款弹窗。
* @returns {Promise<void>}
*/
loadQueueStatus() {
this.loading = true;
getQueueStatus()
.then(res => {
if (res && res.data) {
this.$set(this, 'queueStatus', res.data);
this.detectNewRefund(res.data.myOrders || []);
}
})
.catch(err => {
console.error('[QueueStatus] loadQueueStatus error:', err);
uni.showToast({ title: '加载失败,请稍后重试', icon: 'none' });
})
.finally(() => {
this.loading = false;
});
},
/**
* 检测是否存在刚完成退款的订单,有则弹出退款通知。
* 策略:取 status=1 且 refund_time 最大的一条(最近退款),
* 结合页面跳转参数 show_refund=1 触发弹窗。
* @param {Array} orders - 我的排队订单列表
*/
detectNewRefund(orders) {
const refunded = orders.filter(o => o.status === 1 && o.refund_time > 0);
if (!refunded.length) return;
refunded.sort((a, b) => b.refund_time - a.refund_time);
const latest = refunded[0];
const showParam = this._pageParams && this._pageParams.show_refund;
if (showParam === '1') {
this.refundData = {
amount: latest.amount,
orderId: latest.order_id
};
this.showRefund = true;
}
},
/**
* 页面显示时检查是否需要弹出退款通知(从外部跳转携带参数时使用)
*/
checkPendingRefundNotice() {
const pages = getCurrentPages();
const current = pages[pages.length - 1];
const options = (current && current.options) || {};
if (options.show_refund === '1' && this.queueStatus.myOrders) {
this.detectNewRefund(this.queueStatus.myOrders);
}
},
/**
* 上拉加载更多(当前数据由单次接口全量返回,模拟已到底状态)
* Phase 4 接入分页接口后替换为真实分页逻辑。
*/
loadMoreOrders() {
if (this.loadingMore || this.finished) return;
this.loadingMore = true;
setTimeout(() => {
this.finished = true;
this.loadingMore = false;
}, 500);
},
/**
* 关闭退款通知弹窗
*/
handleRefundClose() {
this.showRefund = false;
}
}
};
</script>
<style scoped lang="scss">
.queue-status-page {
min-height: 100vh;
background: #f4f5f7;
padding-bottom: 40rpx;
}
.header-gradient {
background: linear-gradient(135deg, var(--view-theme, #e93323) 0%, var(--view-gradient, #f76b1c) 100%);
padding: 36rpx 30rpx 48rpx;
position: relative;
overflow: hidden;
}
.header-gradient__bg-circle {
position: absolute;
border-radius: 50%;
background: #fff;
opacity: 0.06;
}
.header-gradient__bg-circle--1 {
width: 400rpx;
height: 400rpx;
top: -160rpx;
right: -100rpx;
}
.header-gradient__bg-circle--2 {
width: 240rpx;
height: 240rpx;
bottom: -60rpx;
left: -50rpx;
}
.header-card {
position: relative;
z-index: 1;
background: rgba(255, 255, 255, 0.14);
border-radius: 28rpx;
padding: 36rpx 32rpx;
backdrop-filter: blur(10px);
}
.header-card__label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 10rpx;
}
.header-card__total {
font-size: 64rpx;
font-weight: 700;
color: #fff;
font-family: 'SemiBold', sans-serif;
line-height: 1.1;
margin-bottom: 28rpx;
}
.header-card__progress {
background: rgba(255, 255, 255, 0.95);
border-radius: 18rpx;
overflow: hidden;
}
.order-list-section {
margin: -16rpx 20rpx 0;
position: relative;
z-index: 2;
}
.section-header {
display: flex;
align-items: center;
padding: 24rpx 4rpx 20rpx;
}
.section-header__dot {
width: 8rpx;
height: 30rpx;
border-radius: 4rpx;
background: var(--view-theme, #e93323);
margin-right: 14rpx;
flex-shrink: 0;
}
.section-header__title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.loading-wrap {
padding: 60rpx 0;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
}
.loading-dots {
display: flex;
gap: 12rpx;
}
.loading-dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
background: var(--view-theme, #e93323);
animation: dot-bounce 1.2s infinite ease-in-out;
}
.loading-dot--2 { animation-delay: 0.2s; }
.loading-dot--3 { animation-delay: 0.4s; }
@keyframes dot-bounce {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1.2); }
}
.loading-text {
font-size: 26rpx;
color: #999;
}
.empty-wrap {
padding: 40rpx 0;
text-align: center;
}
.order-item {
background: #fff;
border-radius: 24rpx;
padding: 30rpx 32rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
position: relative;
overflow: hidden;
}
.order-item__top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18rpx;
}
.order-item__no {
display: flex;
align-items: baseline;
}
.order-item__no-hash {
font-size: 26rpx;
font-weight: 500;
color: var(--view-theme, #e93323);
margin-right: 4rpx;
}
.order-item__no-value {
font-size: 36rpx;
font-weight: 700;
color: #333;
font-family: 'SemiBold', sans-serif;
}
.order-item__tag {
font-size: 22rpx;
font-weight: 500;
padding: 8rpx 20rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
gap: 8rpx;
}
.order-item__tag-dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
}
.order-item__tag--active {
background: #e6f7ee;
color: #389e0d;
}
.order-item__tag-dot--active {
background: #52c41a;
}
.order-item__tag--refunded {
background: #f5f5f5;
color: #999;
}
.order-item__tag-dot--refunded {
background: #bbb;
}
.order-item__id {
font-size: 24rpx;
color: #999;
padding-bottom: 18rpx;
word-break: break-all;
}
.order-item__bottom {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 18rpx;
border-top: 1rpx solid #f0f0f0;
}
.order-item__amount {
font-size: 34rpx;
font-weight: 600;
color: #333;
font-family: 'SemiBold', sans-serif;
}
.order-item__wait {
font-size: 24rpx;
color: #999;
background: #fafafa;
padding: 6rpx 16rpx;
border-radius: 8rpx;
}
.load-more-bar {
padding: 32rpx 0 48rpx;
text-align: center;
}
.load-more-text {
font-size: 26rpx;
color: #aaa;
}
</style>

View File

@@ -43,10 +43,6 @@ export default {
<view class="name display-add" v-if="!userInfo.uid" @tap="openAuto">请点击授权</view>
<view class="acea-row row-middle" v-if="userInfo.uid">
<view class="name">{{ userInfo.nickname }}</view><strong></strong>
<view class="vip flex-center" v-if="userInfo.level">
<text class="iconfont icon-huiyuandengji"></text>
V{{userInfo.level}}
</view>
</view>
<view class="flex-y-center mt-10">
<view class="h-42 flex-center fs-22 text--w111-fff pl-16 pr-14 rd-30rpx b-f mr-12"
@@ -60,7 +56,13 @@ export default {
</view>
<template v-if="!userInfo.is_channel && !userInfo.is_service">
<view class="bind-phone" v-if="!userInfo.phone && userInfo.uid" @tap="bindPhone">绑定手机号</view>
<view class="phone" v-else>{{ perShowType ? 'ID' + userInfo.uid : userInfo.phone }}</view>
<view class="acea-row row-middle" v-else>
<view class="phone">{{ perShowType ? 'ID' + userInfo.uid : userInfo.phone }}</view>
<view class="vip flex-center" v-if="userInfo.level">
<text class="iconfont icon-huiyuandengji"></text>
{{ userInfo.vip_name || ('V' + userInfo.level) }}
</view>
</view>
</template>
</view>
@@ -220,18 +222,17 @@ export default {
}
}
.vip{
// width: 64rpx;
height: 26rpx;
height: 40rpx;
background: #FEF0D9;
border: 1px solid #FACC7D;
border-radius: 50rpx;
font-size: 18rpx;
font-weight: 500;
font-size: 26rpx;
font-weight: 600;
color: #DFA541;
margin-left: 10rpx;
padding: 0 6rpx;
padding: 0 12rpx;
.iconfont {
font-size: 20rpx !important;
font-size: 28rpx !important;
margin-right: 4rpx;
color: #DFA541 !important;
}

View File

@@ -4,6 +4,29 @@
<template v-if="isObjectData(diyData)">
<user-member :userInfo="userInfo" :memberData="diyData.member" :orderAdminData="orderAdminData" :balanceStatus="balanceStatus" :isScrolling="isScrolling"></user-member>
<user-order :orderMenu="orderMenu" :orderAdminData="orderAdminData" :userInfo="userInfo" :memberData="diyData.member" :orderData="diyData.order"></user-order>
<!-- 黄精粉快捷入口我的资产 & 公排记录 member-points 保持一致风格 -->
<view class="acea-row member-points hjf-nav-row">
<view class="acea-row row-middle row-center item" @tap="intoPage('/pages/assets/index')">
<view>
<view>我的资产</view>
<view class="arrow">
查看余额积分
<text class="iconfont icon-ic_rightarrow"></text>
</view>
</view>
<image src="@/static/images/user-member.png" class="image"></image>
</view>
<view class="acea-row row-middle row-center item" @tap="intoPage('/pages/queue/status')">
<view>
<view>公排查询</view>
<view class="arrow">
查看排队进度
<text class="iconfont icon-ic_rightarrow"></text>
</view>
</view>
<image src="@/static/images/user-points.png" class="image"></image>
</view>
</view>
<user-order-static
v-if="isObjectData(orderAdminData) && orderAdminData.order.user_order"
:orderAdminData="orderAdminData.order"
@@ -13,7 +36,7 @@
<user-menu :menuData="diyData.menu" :routineContact="routineContact"></user-menu>
<user-menu v-if="storeMenuShow" :menuData="diyData.merMenu"></user-menu>
<view class="copy_right pb-20">
<template v-if="configData.copyrightContext">
<template v-if="configData && configData.copyrightContext">
<image :src="configData.copyrightImage" mode="aspectFill" class="copyRightImg"></image>
<view class="of0b21">
{{ configData.copyrightContext }}
@@ -29,6 +52,8 @@
<!-- #ifdef MP -->
<editUserModal :isShow="editModal" @closeEdit="closeEdit" @editSuccess="editSuccess"></editUserModal>
<!-- #endif -->
<!-- HJF 演示控制面板 -->
<HjfDemoPanel />
</view>
</template>
<script>
@@ -187,7 +212,7 @@ export default {
color: ['#333', '#333'] //边框颜色支持渐变色
},
imgHost: HTTP_REQUEST_URL,
configData: Cache.get('BASIC_CONFIG'),
configData: Cache.get('BASIC_CONFIG') || {},
copyrightImage: HTTP_REQUEST_URL + '/statics/images/product/support.png',
giftPic: '',
vip_type: 1,
@@ -632,13 +657,15 @@ export default {
};
</script>
<style lang="scss" scoped>
<style scoped lang="scss">
.footer-placeholder {
height: calc(98rpx + constant(safe-area-inset-bottom));
height: calc(98rpx + env(safe-area-inset-bottom));
height: 98rpx;
}
.user-page {
position: relative;
padding-bottom: calc(100rpx+ constant(safe-area-inset-bottom));
padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
padding-bottom: 100rpx;
@@ -667,4 +694,62 @@ export default {
margin-bottom: 20rpx;
}
}
// 黄精粉导航行:复用 member-points 样式(与会员中心/积分商城完全一致)
.hjf-nav-row {
margin-top: 0 !important;
}
.member-points {
border-radius: 20rpx;
margin: 20rpx;
background-color: #ffffff;
.item {
position: relative;
flex: 1;
height: 134rpx;
padding-left: 40rpx;
font-weight: 500;
font-size: 28rpx;
line-height: 34rpx;
color: #333333;
&::before {
content: '';
position: absolute;
top: 50%;
left: 0;
height: 48rpx;
border-left: 1rpx solid #eeeeee;
transform: translateY(-50%);
}
&:first-child::before {
display: none;
}
.iconfont {
position: relative;
font-size: 20rpx;
}
}
.arrow {
margin-top: 12rpx;
font-weight: 400;
font-size: 22rpx;
line-height: 24rpx;
color: #ff7d00;
}
.image {
width: 88rpx;
height: 88rpx;
margin-left: 40rpx;
}
.iconfont {
margin-left: 2rpx;
font-size: 24rpx;
}
}
</style>

View File

@@ -1,30 +1,68 @@
<template>
<view :style="colorStyle">
<view class='bill-details'>
<!-- 类型筛选导航 -->
<view class='nav acea-row'>
<view class='item' :class='type==0 ? "on":""' @click='changeType(0)'>全部</view>
<view class='item' :class='type==1 ? "on":""' @click='changeType(1)'>消费</view>
<view class='item' :class='type==2 ? "on":""' @click='changeType(2)'>储值</view>
<view
class='item'
:class='type == 0 ? "on" : ""'
@click='changeType(0)'
>全部</view>
<view
class='item'
:class='type == 1 ? "on" : ""'
@click='changeType(1)'
>消费</view>
<view
class='item'
:class='type == 2 ? "on" : ""'
@click='changeType(2)'
>储值</view>
<view
class='item'
:class='type == "queue_refund" ? "on" : ""'
@click='changeType("queue_refund")'
>公排退款</view>
</view>
<!-- 账单列表 -->
<view class='sign-record'>
<view class='list' v-for="(item,index) in userBillList" :key="index">
<view class='list' v-for="(item, index) in userBillList" :key="index">
<view class='item'>
<view class='data'>{{item.time}}</view>
<view class='data'>{{ item.time }}</view>
<view class='listn'>
<view class='itemn acea-row row-between-wrapper' v-for="(vo,indexn) in item.child" :key="indexn">
<view
class='itemn acea-row row-between-wrapper'
v-for="(vo, indexn) in item.child"
:key="indexn"
>
<view>
<view class='name line1'>{{vo.title}}</view>
<view>{{vo.add_time}}</view>
<view class='name line1'>
{{ vo.title }}
<!-- 公排退款标记 -->
<text
v-if="vo.type === 'queue_refund'"
class='queue-refund-tag'
>公排退款</text>
</view>
<view class='time-text'>{{ vo.add_time }}</view>
</view>
<view class='num' :class="vo.pm ? 'num-add' : 'num-sub'">
<text v-if="vo.pm">+{{ vo.number }}</text>
<text v-else>-{{ vo.number }}</text>
</view>
<view class='num' v-if="vo.pm">+{{vo.number}}</view>
<view class='num' v-else>-{{vo.number}}</view>
</view>
</view>
</view>
</view>
<view class='loadingicon acea-row row-center-wrapper' v-if="userBillList.length>0">
<text class='loading iconfont icon-jiazai' :hidden='loading==false'></text>{{loadTitle}}
<!-- 加载更多 -->
<view class='loadingicon acea-row row-center-wrapper' v-if="userBillList.length > 0">
<text class='loading iconfont icon-jiazai' :hidden='loading == false'></text>
{{ loadTitle }}
</view>
<!-- 空状态 -->
<view class="px-20 mt-20" v-if="userBillList.length == 0">
<emptyPage title="暂无记录~" src="/statics/images/noOrder.gif"></emptyPage>
</view>
@@ -35,18 +73,25 @@
</template>
<script>
import {
getCommissionInfo,
moneyList
} from '@/api/user.js';
import {
toLogin
} from '@/libs/login.js';
import {
mapGetters
} from "vuex";
import { moneyList } from '@/api/user.js';
import { toLogin } from '@/libs/login.js';
import { mapGetters } from 'vuex';
import emptyPage from '@/components/emptyPage.vue';
import colors from "@/mixins/color";
import colors from '@/mixins/color';
/**
* 账单明细页
*
* 展示用户的账单流水,支持按类型筛选:
* - 0: 全部
* - 1: 消费
* - 2: 储值
* - "queue_refund": 公排退款type=queue_refund 时显示专属标记)
*
* 列表按日期分组,支持上拉分页加载。
*
* @module pages/users/user_bill/index
*/
export default {
components: {
emptyPage,
@@ -54,125 +99,116 @@
mixins: [colors],
data() {
return {
/** @type {string} 底部加载提示文字 */
loadTitle: '加载更多',
/** @type {boolean} 是否正在加载中(防止重复请求) */
loading: false,
/** @type {boolean} 是否已加载全部数据 */
loadend: false,
/** @type {number} 当前页码 */
page: 1,
/** @type {number} 每页条数 */
limit: 15,
/**
* 当前筛选类型
* 0=全部 1=消费 2=储值 "queue_refund"=公排退款
* @type {number|string}
*/
type: 0,
/** @type {Array<Object>} 按日期分组的账单列表 */
userBillList: [],
times:[],
isAuto: false, //没有授权的不会自动授权
isShowAuth: false //是否隐藏授权
/** @type {Array<string>} 已加载的日期键列表(用于去重分组) */
times: [],
};
},
computed: mapGetters(['isLogin']),
computed: {
...mapGetters(['isLogin']),
},
onShow() {
uni.removeStorageSync('form_type_cart');
if (this.isLogin) {
this.getUserBillList();
} else {
toLogin()
toLogin();
}
},
/**
* 生命周期函数--监听页面加载
* 生命周期函数监听页面加载
* @param {Object} options - 页面跳转参数
* @param {number|string} [options.type=0] - 初始筛选类型
*/
onLoad: function(options) {
onLoad(options) {
this.type = options.type || 0;
},
/**
* 页面上拉触底事件的处理函数
* 页面上拉触底 — 加载下一页
*/
onReachBottom: function() {
onReachBottom() {
this.getUserBillList();
},
onPageScroll(object) {
/**
* 页面滚动事件 — 广播 scroll 事件供子组件使用
*/
onPageScroll() {
uni.$emit('scroll');
},
methods: {
/**
* 授权回调
* 获取账单明细列表(分页追加)
*
* 请求 moneyList 接口,将返回数据按日期分组追加到 userBillList。
* 若 loading 或 loadend 为 true 则直接返回,防止重复/越界请求。
*
* @returns {void}
*/
onLoadFun: function() {
this.getUserBillList();
this.isShowAuth = false;
},
// 授权关闭
authColse: function(e) {
this.isShowAuth = e
},
/**
* 获取账户明细
*/
getUserBillList: function() {
let that = this;
let page = that.page;
let limit = that.limit;
if (that.loading) return;
if (that.loadend) return;
that.loading = true;
that.loadTitle = '';
moneyList({
page: page,
limit: limit
},that.type).then(res => {
for (let i = 0; i < res.data.time.length; i++) {
if (!this.times.includes(res.data.time[i])) {
this.times.push(res.data.time[i])
this.userBillList.push({
time: res.data.time[i],
child: []
})
}
}
for (let x = 0; x < this.times.length; x++) {
for (let j = 0; j < res.data.list.length; j++) {
if (this.times[x] === res.data.list[j].time_key) {
this.userBillList[x].child.push(res.data.list[j])
getUserBillList() {
if (this.loading) return;
if (this.loadend) return;
this.loading = true;
this.loadTitle = '';
moneyList({ page: this.page, limit: this.limit }, this.type)
.then(res => {
const { time: timeKeys, list } = res.data;
// 按日期键建立分组(跨页去重)
timeKeys.forEach(key => {
if (!this.times.includes(key)) {
this.times.push(key);
this.userBillList.push({ time: key, child: [] });
}
}
}
let loadend = res.data.list.length < that.limit;
that.loadend = loadend;
that.loadTitle = loadend ? '没有更多内容啦~' : '加载更多';
that.page += 1;
that.loading = false;
}).catch(err=>{
that.loading = false;
that.loadTitle = '加载更多';
})
});
// 将明细条目归入对应日期分组
this.times.forEach((key, idx) => {
list.forEach(item => {
if (item.time_key === key) {
this.userBillList[idx].child.push(item);
}
});
});
const loadend = list.length < this.limit;
this.loadend = loadend;
this.loadTitle = loadend ? '没有更多内容啦~' : '加载更多';
this.page += 1;
this.loading = false;
})
.catch(() => {
this.loading = false;
this.loadTitle = '加载更多';
});
},
// getUserBillList: function() {
// let that = this;
// if (that.loadend) return;
// if (that.loading) return;
// that.loading = true;
// that.loadTitle = "";
// let data = {
// page: that.page,
// limit: that.limit
// }
// getCommissionInfo(data, that.type).then(function(res) {
// let list = res.data,
// loadend = list.length < that.limit;
// that.userBillList = that.$util.SplitArray(list, that.userBillList);
// that.$set(that, 'userBillList', that.userBillList);
// that.loadend = loadend;
// that.loading = false;
// that.loadTitle = loadend ? "没有更多内容啦~" : "加载更多";
// that.page = that.page + 1;
// }, function(res) {
// that.loading = false;
// that.loadTitle = '加载更多';
// });
// },
/**
* 切换导航
* 切换账单筛选类型并重置列表
*
* @param {number|string} type - 目标类型0全部 1消费 2储值 "queue_refund"公排退款)
* @returns {void}
*/
changeType: function(type) {
changeType(type) {
if (this.type === type) return;
this.type = type;
this.loadend = false;
this.page = 1;
@@ -180,35 +216,71 @@
this.$set(this, 'userBillList', []);
this.getUserBillList();
},
}
}
},
};
</script>
<style scoped lang='scss'>
.sign-record .list .item .data{
.sign-record .list .item .data {
color: #999;
padding: 20rpx 30rpx 10rpx;
font-size: 26rpx;
}
.sign-record .list .item .listn{
.sign-record .list .item .listn {
width: 710rpx;
margin: 0 auto;
border-radius: 24rpx;
}
.sign-record .list .item .listn .itemn{
.sign-record .list .item .listn .itemn {
padding: 0;
margin: 0 30rpx;
height: 150rpx;
}
.sign-record .list .item .listn .itemn:nth-last-child(1){
.sign-record .list .item .listn .itemn:nth-last-child(1) {
border-bottom: 0;
}
.sign-record .list .item .listn .itemn .name{
.sign-record .list .item .listn .itemn .name {
color: #333;
font-size: 28rpx;
}
.sign-record .list .item .listn .itemn .num{
color: #333333;
.sign-record .list .item .listn .itemn .time-text {
color: #999;
font-size: 24rpx;
margin-top: 8rpx;
}
.sign-record .list .item .listn .itemn .num {
font-family: 'Regular';
font-size: 30rpx;
}
.num-add {
color: var(--view-theme);
}
.num-sub {
color: #333333;
}
/* 公排退款标记 */
.queue-refund-tag {
display: inline-block;
margin-left: 10rpx;
padding: 2rpx 12rpx;
border-radius: 20rpx;
font-size: 20rpx;
color: #fff;
background: var(--view-theme);
vertical-align: middle;
line-height: 1.6;
}
/* 顶部类型筛选导航 */
.bill-details .nav {
background-color: #fff;
height: 80rpx;
@@ -225,11 +297,11 @@
.bill-details .nav .item.on {
color: var(--view-theme);
/* border-bottom: 3rpx solid var(--view-theme); */
font-size: 30rpx;
position: relative;
}
.bill-details .nav .item.on::after{
.bill-details .nav .item.on::after {
position: absolute;
width: 64rpx;
height: 6rpx;
@@ -237,7 +309,7 @@
content: ' ';
background: var(--view-theme);
bottom: 0;
left:50%;
left: 50%;
margin-left: -32rpx;
}
</style>

View File

@@ -45,78 +45,89 @@
</view>
</view>
<view :hidden="currentTab != 0">
<form @submit="subCash">
<view class='list'>
<view class="itemCon">
<view class='item acea-row row-between-wrapper'>
<view class='name'>持卡人</view>
<view class='input'><input placeholder='请输入持卡人姓名' placeholder-class='placeholder'
name="name" onKeypress="javascript:if(event.keyCode == 32)event.returnValue = false;"></input></view>
</view>
<view class='item acea-row row-between-wrapper'>
<view class='name'>卡号</view>
<view class='input'><input type='number' placeholder='请输入卡号' placeholder-class='placeholder'
name="cardnum"></input></view>
</view>
<view class='item acea-row row-between-wrapper'>
<view class='name'>银行</view>
<view class='input'>
<picker @change="bindPickerChange" :value="index" :range="array">
<view class="acea-row row-between-wrapper">
<text class='Bank'>{{array[index]}}</text>
<text class='iconfont icon-ic_rightarrow'></text>
</view>
</picker>
</view>
</view>
<view class='item acea-row row-between-wrapper'>
<view class='name'>提现</view>
<view class='input acea-row row-between-wrapper'>
<input @input='inputNum' :value='cashVal' :maxlength="moneyMaxLeng" :placeholder='"最低提现金额:¥"+minPrice' placeholder-class='placeholder'
name="money" type='digit'></input>
<view class="all" @click="allCash">全部提现</view>
</view>
<form @submit="subCash">
<view class='list'>
<view class="itemCon">
<view class='item acea-row row-between-wrapper'>
<view class='name'>持卡人</view>
<view class='input'><input placeholder='请输入持卡人姓名' placeholder-class='placeholder'
name="name" onKeypress="javascript:if(event.keyCode == 32)event.returnValue = false;"></input></view>
</view>
<view class='item acea-row row-between-wrapper'>
<view class='name'>卡号</view>
<view class='input'><input type='number' placeholder='请输入卡号' placeholder-class='placeholder'
name="cardnum"></input></view>
</view>
<view class='item acea-row row-between-wrapper'>
<view class='name'>银行</view>
<view class='input'>
<picker @change="bindPickerChange" :value="index" :range="array">
<view class="acea-row row-between-wrapper">
<text class='Bank'>{{array[index]}}</text>
<text class='iconfont icon-ic_rightarrow'></text>
</view>
</picker>
</view>
</view>
<view class='tip'>
当前可提现金额: <text
class="price">{{userInfo.commissionCount}}</text>,冻结佣金{{userInfo.broken_commission}}
</view>
<view class='tip'>
提现手续费: <text class="price">{{withdraw_fee}}%</text>,实际到账:<text class="price">{{true_money}}</text>
</view>
<view class='tip'>
说明: <text class="num">每笔佣金的冻结期为{{userInfo.broken_day}}到期后可提现</text>
<view class='item acea-row row-between-wrapper'>
<view class='name'>提现</view>
<view class='input acea-row row-between-wrapper'>
<input @input='inputNum' :value='cashVal' :maxlength="moneyMaxLeng" :placeholder='"最低提现金额:¥"+minPrice' placeholder-class='placeholder'
name="money" type='digit'></input>
<view class="all" @click="allCash">全部提现</view>
</view>
</view>
</view>
<button formType="submit" class='bnt bg-color'>立即提现</button>
</form>
<view class='tip'>
当前可提现金额: <text
class="price">{{userInfo.commissionCount}}</text>,冻结佣金{{userInfo.broken_commission}}
</view>
<view class='tip fee-breakdown'>
手续费<text class="fee-rate">{{withdraw_fee}}%</text><text class="price">¥{{feeAmount}}</text>
<text class="fee-sep"> | </text>实际到账<text class="price">¥{{actualAmount}}</text>
</view>
<view class='tip'>
说明: <text class="num">每笔佣金的冻结期为{{userInfo.broken_day}}到期后可提现</text>
</view>
<view class='tip tip-warning'>
温馨提示: <text class="num">公排退款提现需收取 <text class="fee">{{withdraw_fee}}%</text> 手续费实际到账金额以扣除手续费后为准</text>
</view>
</view>
<view :hidden="currentTab != 1">
<form @submit="subCash">
<view class='list'>
<view class="itemCon">
<view class='item acea-row row-between-wrapper'>
<view class='name'>提现</view>
<view class='input acea-row row-between-wrapper'>
<input @input='inputNum' :value='cashVal' :maxlength="moneyMaxLeng" :placeholder='"最低提现金额:¥"+minPrice' placeholder-class='placeholder'
name="money" type='digit'></input>
<view class="all" @click="allCash">全部提现</view>
</view>
<button formType="submit" class='bnt bg-color'>立即提现</button>
</form>
</view>
<view :hidden="currentTab != 1">
<form @submit="subCash">
<view class='list'>
<view class="itemCon">
<view class='item acea-row row-between-wrapper'>
<view class='name'>提现</view>
<view class='input acea-row row-between-wrapper'>
<input @input='inputNum' :value='cashVal' :maxlength="moneyMaxLeng" :placeholder='"最低提现金额:¥"+minPrice' placeholder-class='placeholder'
name="money" type='digit'></input>
<view class="all" @click="allCash">全部提现</view>
</view>
</view>
<view class='tip'>
当前可提现金额: <text
class="price">{{userInfo.commissionCount}}</text>,冻结佣金{{userInfo.broken_commission}}
</view>
<view class='tip'>
说明: <text class="num">每笔佣金的冻结期为{{userInfo.broken_day}}到期后可提现</text>
</view>
</view>
<button formType="submit" class='bnt bg-color'>立即提现</button>
</form>
<view class='tip'>
当前可提现金额: <text
class="price">{{userInfo.commissionCount}}</text>,冻结佣金{{userInfo.broken_commission}}
</view>
<view class='tip fee-breakdown'>
手续费<text class="fee-rate">{{withdraw_fee}}%</text><text class="price">¥{{feeAmount}}</text>
<text class="fee-sep"> | </text>实际到账<text class="price">¥{{actualAmount}}</text>
</view>
<view class='tip'>
说明: <text class="num">每笔佣金的冻结期为{{userInfo.broken_day}}到期后可提现</text>
</view>
<view class='tip tip-warning'>
温馨提示: <text class="num">公排退款提现需收取 <text class="fee">{{withdraw_fee}}%</text> 手续费实际到账金额以扣除手续费后为准</text>
</view>
</view>
<view :hidden="currentTab != 2">
<button formType="submit" class='bnt bg-color'>立即提现</button>
</form>
</view>
<view :hidden="currentTab != 2">
<form @submit="subCash">
<view class='list'>
<view class="itemCon">
@@ -152,17 +163,21 @@
当前可提现金额: <text
class="price">{{userInfo.commissionCount}}</text>,冻结佣金{{userInfo.broken_commission}}
</view>
<view class='tip'>
提现手续费: <text class="price">{{withdraw_fee}}%</text>,实际到账:<text class="price">{{true_money}}</text>
</view>
<view class='tip'>
说明: <text class="num">每笔佣金的冻结期为{{userInfo.broken_day}}到期后可提现</text>
</view>
<view class='tip fee-breakdown'>
手续费<text class="fee-rate">{{withdraw_fee}}%</text><text class="price">¥{{feeAmount}}</text>
<text class="fee-sep"> | </text>实际到账<text class="price">¥{{actualAmount}}</text>
</view>
<button formType="submit" class='bnt bg-color'>立即提现</button>
</form>
</view>
<view :hidden='currentTab != 3'>
<view class='tip'>
说明: <text class="num">每笔佣金的冻结期为{{userInfo.broken_day}}到期后可提现</text>
</view>
<view class='tip tip-warning'>
温馨提示: <text class="num">公排退款提现需收取 <text class="fee">{{withdraw_fee}}%</text> 手续费实际到账金额以扣除手续费后为准</text>
</view>
</view>
<button formType="submit" class='bnt bg-color'>立即提现</button>
</form>
</view>
<view :hidden='currentTab != 3'>
<form @submit="subCash">
<view class='list'>
<view class="itemCon">
@@ -204,18 +219,22 @@
当前可提现金额: <text
class="price">{{userInfo.commissionCount}}</text>,冻结佣金{{userInfo.broken_commission}}
</view>
<view class='tip'>
提现手续费: <text class="price">{{withdraw_fee}}%</text>,实际到账:<text class="price">{{true_money}}</text>
</view>
<view class='tip'>
说明: <text class="num">每笔佣金的冻结期为{{userInfo.broken_day}}到期后可提现</text>
</view>
<view class='tip fee-breakdown'>
手续费<text class="fee-rate">{{withdraw_fee}}%</text><text class="price">¥{{feeAmount}}</text>
<text class="fee-sep"> | </text>实际到账<text class="price">¥{{actualAmount}}</text>
</view>
<button formType="submit" class='bnt bg-color'>立即提现</button>
</form>
</view>
<view class='tip'>
说明: <text class="num">每笔佣金的冻结期为{{userInfo.broken_day}}到期后可提现</text>
</view>
<view class='tip tip-warning'>
温馨提示: <text class="num">公排退款提现需收取 <text class="fee">{{withdraw_fee}}%</text> 手续费实际到账金额以扣除手续费后为准</text>
</view>
</view>
<button formType="submit" class='bnt bg-color'>立即提现</button>
</form>
</view>
</view>
</view>
<home></home>
</view>
</template>
@@ -226,6 +245,7 @@
extractBank,
getUserInfo
} from '@/api/user.js';
import { getWithdrawInfo } from '@/api/hjfAssets.js';
import { toLogin } from '@/libs/login.js';
import { mapGetters } from "vuex";
import colors from '@/mixins/color.js';
@@ -257,7 +277,12 @@
extract_wechat_type:0,
cashVal: '',
copyIndex:null,
platform: ''
platform: '',
/**
* 提现配置信息(来自 hjfAssets.getWithdrawInfo
* @type {{ now_money: string, min_extract: number, fee_rate: number, extract_bank: string[] }}
*/
withdrawInfo: {}
};
},
computed: {
@@ -269,6 +294,24 @@
// }else{
// return false
// }
},
/**
* 当前输入金额对应的手续费金额7%,保留两位小数)
* @returns {string}
*/
feeAmount() {
const val = parseFloat(this.cashVal) || 0;
const rate = parseFloat(this.withdraw_fee) || 7;
return (Math.floor(val * rate) / 100).toFixed(2);
},
/**
* 扣除手续费后实际到账金额(保留两位小数)
* @returns {string}
*/
actualAmount() {
const val = parseFloat(this.cashVal) || 0;
const fee = parseFloat(this.feeAmount) || 0;
return Math.max(0, val - fee).toFixed(2);
}
},
watch: {
@@ -298,6 +341,7 @@
})
this.getUserInfo();
this.getUserExtractBank();
this.loadWithdrawInfo();
} else {
toLogin()
}
@@ -365,8 +409,22 @@
onLoadFun: function() {
this.getUserInfo();
this.getUserExtractBank();
this.loadWithdrawInfo();
this.isShowAuth = false;
},
/**
* 加载提现配置信息(手续费率、最低提现额、可提现余额)
* 结果存入 withdrawInfo并将 withdraw_fee 同步为接口返回的 fee_rate默认 7%
* @returns {void}
*/
loadWithdrawInfo: function() {
getWithdrawInfo().then(res => {
this.withdrawInfo = res.data || {};
if (this.withdrawInfo.fee_rate !== undefined) {
this.withdraw_fee = String(this.withdrawInfo.fee_rate);
}
});
},
// 授权关闭
authColse: function(e) {
this.isShowAuth = e
@@ -856,6 +914,47 @@
}
}
.cash-withdrawal .wrapper .list .tip-warning {
color: #E6A23C;
background-color: #FDF6EC;
border-radius: 8rpx;
padding: 12rpx 16rpx;
margin-top: 12rpx;
line-height: 1.6;
.num{
margin-left: 8rpx;
}
.fee{
color: #E64340;
font-weight: 600;
}
}
.cash-withdrawal .wrapper .list .fee-breakdown {
background-color: #F0F9EB;
border-radius: 8rpx;
padding: 10rpx 16rpx;
margin-top: 12rpx;
color: #606266;
.fee-rate {
color: #E64340;
font-weight: 600;
}
.fee-sep {
color: #CCCCCC;
margin: 0 4rpx;
}
.price {
color: var(--view-theme);
margin: 0 4rpx;
}
}
.cash-withdrawal .wrapper .list .tip2 {
font-size: 26rpx;
color: #999;

View File

@@ -32,24 +32,28 @@
class='recharge'>储值</view>
<!-- #endif -->
</view>
<view class='cumulative acea-row row-middle'>
<!-- #ifdef APP-PLUS || H5 -->
<view class='item'>
<view>累计储值()</view>
<view class='money'>{{userInfo.recharge || 0}}</view>
</view>
<!-- #endif -->
<!-- #ifdef MP -->
<view class='item' v-if="recharge_switch">
<view>累计储值()</view>
<view class='money'>{{userInfo.recharge || 0}}</view>
</view>
<!-- #endif -->
<view class='item'>
<view>累计消费()</view>
<view class='money'>{{userInfo.orderStatusSum || 0}}</view>
</view>
<view class='cumulative acea-row row-middle'>
<!-- #ifdef APP-PLUS || H5 -->
<view class='item'>
<view>累计储值()</view>
<view class='money'>{{userInfo.recharge || 0}}</view>
</view>
<!-- #endif -->
<!-- #ifdef MP -->
<view class='item' v-if="recharge_switch">
<view>累计储值()</view>
<view class='money'>{{userInfo.recharge || 0}}</view>
</view>
<!-- #endif -->
<view class='item'>
<view>累计消费()</view>
<view class='money'>{{userInfo.orderStatusSum || 0}}</view>
</view>
<view class='item'>
<view>公排退款()</view>
<view class='money'>{{queueRefundedTotal}}</view>
</view>
</view>
<view class="pictrue">
<image :src="imgHost+'/statics/images/users/pig.png'"></image>
</view>
@@ -129,6 +133,7 @@
import {
mapGetters
} from "vuex";
import { getQueueStatus } from '@/api/hjfQueue.js';
import recommend from '@/components/recommend/index';
import colors from "@/mixins/color";
import {
@@ -148,6 +153,12 @@
userInfo: {
now_money: 0,
},
/**
* 公排累计退款金额(元)
* 由 getQueueStatus() 返回的 myOrders 中 status===1 的订单金额累加而来
* @type {number}
*/
queueRefundedTotal: 0,
hostProduct: [],
isClose: false,
recharge_switch: 0,
@@ -167,6 +178,7 @@
if (newV) {
this.getUserInfo();
this.get_activity();
this.getQueueRefundedTotal();
}
},
deep: true
@@ -179,6 +191,7 @@
if (this.isLogin) {
this.getUserInfo();
this.get_activity();
this.getQueueRefundedTotal();
} else {
toLogin()
}
@@ -222,6 +235,23 @@
that.$set(that, "activity", res.data);
})
},
/**
* 获取公排累计退款金额
* 调用 getQueueStatus(),将 myOrders 中 status===1已退款的订单金额累加
* 结果保存到 queueRefundedTotal保留两位小数单位
* @see docs/frontend-new-pages-spec.md 6.1.4
* @returns {void}
*/
getQueueRefundedTotal: function() {
let that = this;
getQueueStatus().then(res => {
const orders = (res.data && res.data.myOrders) || [];
const total = orders
.filter(order => order.status === 1)
.reduce((sum, order) => sum + Number(order.amount || 0), 0);
that.queueRefundedTotal = total.toFixed(2);
});
},
/**
* 获取我的推荐
*/

View File

@@ -1,62 +1,91 @@
<template>
<view :style="colorStyle">
<view class='commission-details'>
<!-- 团队收益统计卡片5.1.7 -->
<view class="team-stats-card">
<view class="team-stats-title">团队业绩统计</view>
<view class="team-stats-row">
<view class="team-stats-item">
<view class="team-stats-num">{{ teamData.direct_count }}</view>
<view class="team-stats-label">直推人数</view>
</view>
<view class="team-stats-divider"></view>
<view class="team-stats-item">
<view class="team-stats-num">{{ teamData.umbrella_count }}</view>
<view class="team-stats-label">伞下人数</view>
</view>
<view class="team-stats-divider"></view>
<view class="team-stats-item">
<view class="team-stats-num text-primary">{{ teamData.umbrella_orders }}</view>
<view class="team-stats-label">团队订单数</view>
</view>
</view>
</view>
<view class='search acea-row row-between-wrapper' v-if="recordType != 1 && recordType != 4">
<view class='input'>
<text class="iconfont icon-ic_search"></text>
<input placeholder='搜索用户名称' placeholder-class='placeholder' v-model="keyword" @confirm="submitForm"
confirm-type='search' name="search"></input>
<input
placeholder='搜索用户名称'
placeholder-class='placeholder'
v-model="keyword"
@confirm="submitForm"
confirm-type='search'
name="search"
></input>
</view>
</view>
<timeSlot @changeTime="changeTime"></timeSlot>
<view class='sign-record'>
<view class="top_num" v-if="recordType != 4 && recordList.length">
支出¥{{expend || 0}} &nbsp;&nbsp;&nbsp; 收入¥{{income || 0}}
</view>
<view class="box">
<block v-for="(item,index) in recordList" :key="index" v-if="recordList.length>0">
<block v-for="(item, index) in recordList" :key="index" v-if="recordList.length > 0">
<view class='list'>
<view class='item'>
<!-- <view class='data'>{{item.time}}</view> -->
<view class='listn'>
<!-- <block v-for="(child,indexn) in item.child" :key="indexn"> -->
<view class='itemn1 flex justify-between'>
<view>
<view class='name line1'>
{{item.title}}
<!-- <text class="status_badge success" v-if="recordType == 4 && item.status == 1">审核通过</text> -->
<text class="status_badge default" v-if="recordType == 4 && item.status == 0">待审核</text>
<text class="status_badge error" v-if="recordType == 4 && item.status == 2">审核未通过</text>
<!-- 提现记录 0 待审核 1 通过 2 未通过 -->
</view>
</view>
<!-- 积分来源标签直推 / 伞下 -->
<view class="income-type-tag" v-if="item.type">
<text class="tag-direct" v-if="item.type === 'direct'">直推奖励</text>
<text class="tag-umbrella" v-else-if="item.type === 'umbrella'">伞下奖励</text>
</view>
<view class="mark" v-if="item.extract_status == -1">原因{{item.extract_msg}}</view>
<view>{{item.add_time}}</view>
<view v-if="item.is_frozen && item.is_frozen == 1">佣金冻结中解冻时间{{item.frozen_time}}</view>
</view>
<view>
<view class='num' :class="recordType == 4 && item.status == 0?'on':''"
v-if="item.pm == 1">+{{item.number}}</view>
<view class='num' v-else>-{{item.number}}</view>
<!-- 积分数量展示不带 ¥ 6.1.5 §2 -->
<view class='num' :class="recordType == 4 && item.status == 0 ? 'on' : ''"
v-if="item.pm == 1">+{{item.points !== undefined ? item.points : item.number}}</view>
<view class='num' v-else>-{{item.points !== undefined ? item.points : item.number}}</view>
<view class="fail" v-if="item.extract_status == -1 && item.type == 'extract'">审核未通过</view>
<view class="wait" v-if="item.extract_status == 0 && item.type == 'extract'">待审核</view>
<view class="wait" v-if="item.is_frozen == 1">冻结中</view>
<view class="w-154 h-56 rd-30rpx flex-center mt-16 bg-color fs-24 text--w111-fff"
v-if="item.extract_status == 0 && item.type == 'extract'"
@tap="extractCancel(item.link_id)"
>取消提现</view>
v-if="item.extract_status == 0 && item.type == 'extract'"
@tap="extractCancel(item.link_id)">取消提现</view>
<!-- #ifdef MP-WEIXIN -->
<view class="w-154 h-56 rd-30rpx flex-center mt-16 ml-12 bg-color fs-24 text--w111-fff"
v-if="item.wechat_state == 1 && item.type == 'extract'"
v-if="item.wechat_state == 1 && item.type == 'extract'"
@tap="jumpPath('/pages/users/user_spread_money/receiving?type=1&id=' + item.extract_order_id)">立即收款</view>
<!-- #endif -->
<!-- #ifdef H5 -->
<view class="w-154 h-56 rd-30rpx flex-center mt-16 ml-12 bg-color fs-24 text--w111-fff"
v-if="item.wechat_state == 1 && item.type == 'extract' && isWeixin"
v-if="item.wechat_state == 1 && item.type == 'extract' && isWeixin"
@tap="jumpPath('/pages/users/user_spread_money/receiving?type=1&id=' + item.extract_order_id)">立即收款</view>
<!-- #endif -->
</view>
</view>
<!-- </block> -->
</view>
</view>
</view>
@@ -64,7 +93,7 @@
</view>
<view class='loadingicon acea-row row-center-wrapper' v-if="recordList.length">
<text class='loading iconfont icon-jiazai' :hidden='loading==false'></text>{{loadTitle}}
<text class='loading iconfont icon-jiazai' :hidden='loading == false'></text>{{loadTitle}}
</view>
<view class="empty" v-if="!recordList.length">
<emptyPage title='暂无数据~' src="/statics/images/noOrder.gif"></emptyPage>
@@ -76,21 +105,21 @@
</template>
<script>
import { moneyList, getSpreadInfo, extractCancelApi } from '@/api/user.js';
import { moneyList, getSpreadInfo, spreadCount, extractCancelApi } from '@/api/user.js';
import { getTeamData, getTeamIncome } from '@/api/hjfMember.js';
import { toLogin } from '@/libs/login.js';
import { mapGetters } from "vuex";
import emptyPage from '@/components/emptyPage.vue'
import { mapGetters } from 'vuex';
import emptyPage from '@/components/emptyPage.vue';
import colors from '@/mixins/color.js';
import timeSlot from '../components/timeSlot/index.vue';
// #ifdef H5
import Auth from '@/libs/wechat';
// #endif
export default {
components: {
emptyPage,
timeSlot
},
components: { emptyPage, timeSlot },
mixins: [colors],
data() {
return {
name: '',
@@ -111,12 +140,33 @@
income: '',
expend: '',
disabled: false,
/**
* 团队概况数据
* @type {{ direct_count: number, umbrella_count: number, umbrella_orders: number }}
*/
teamData: {
direct_count: 0,
umbrella_count: 0,
umbrella_orders: 0,
},
/**
* 团队收益明细列表(来自 getTeamIncome
* @type {Array<{ id: number, title: string, type: string, points: number, from_nickname: string, add_time: string }>}
*/
teamIncome: [],
// #ifdef H5
isWeixin: Auth.isWeixin(),
//#endif
// #endif
};
},
computed: mapGetters(['isLogin']),
computed: {
...mapGetters(['isLogin']),
},
onLoad(options) {
if (this.isLogin) {
this.type = options.type;
@@ -124,7 +174,8 @@
toLogin();
}
},
onShow: function() {
onShow() {
uni.removeStorageSync('form_type_cart');
this.page = 1;
this.limit = 20;
@@ -133,25 +184,22 @@
this.status = false;
this.$set(this, 'recordList', []);
this.$set(this, 'times', []);
let type = this.type;
this.loadTeamData();
const type = this.type;
if (type == 1) {
uni.setNavigationBarTitle({
title: "佣金记录"
});
uni.setNavigationBarTitle({ title: '推荐收益' });
this.name = '提现总额';
this.recordType = 3;
this.getRecordList();
} else if (type == 2) {
uni.setNavigationBarTitle({
title: "佣金记录"
});
this.name = '佣金明细';
uni.setNavigationBarTitle({ title: '推荐收益' });
this.name = '推荐收益明细';
this.recordType = 3;
this.getRecordList();
} else if (type == 4) {
uni.setNavigationBarTitle({
title: "提现记录"
});
uni.setNavigationBarTitle({ title: '提现记录' });
this.name = '提现明细';
this.recordType = 4;
this.getRecordList();
@@ -161,23 +209,44 @@
icon: 'none',
duration: 1000,
mask: true,
success: function(res) {
setTimeout(function() {
success() {
setTimeout(() => {
// #ifndef H5
uni.navigateBack({
delta: 1,
});
uni.navigateBack({ delta: 1 });
// #endif
// #ifdef H5
history.back();
// #endif
}, 1200)
}, 1200);
},
});
}
},
methods: {
/**
* 加载团队概况和收益明细,统一在 onShow 调用
* @returns {void}
*/
loadTeamData() {
getTeamData().then(res => {
const d = (res && res.data) || {};
this.teamData = {
direct_count: d.direct_count || 0,
umbrella_count: d.umbrella_count || 0,
umbrella_orders: d.umbrella_orders || 0,
};
}).catch(() => {});
getTeamIncome({ page: 1, limit: this.limit }).then(res => {
this.teamIncome = (res && res.data && res.data.list) || [];
}).catch(() => {});
},
/**
* 搜索框确认时重置并重新加载列表
* @returns {void}
*/
submitForm() {
this.page = 1;
this.limit = 20;
@@ -188,98 +257,166 @@
this.$set(this, 'times', []);
this.getRecordList();
},
/**
* 时间筛选回调
* @param {{ start: number, stop: number }} time - 筛选时间段
* @returns {void}
*/
changeTime(time) {
this.start = time.start
this.stop = time.stop
this.start = time.start;
this.stop = time.stop;
this.page = 1;
this.loadend = false;
this.$set(this, 'recordList', []);
this.getRecordList();
},
getRecordList: function() {
let that = this;
let page = that.page;
let limit = that.limit;
let recordType = that.recordType;
if (that.loading) return;
if (that.loadend) return;
/**
* 加载收益流水列表(分页追加)
* @returns {void}
*/
getRecordList() {
const that = this;
const { page, limit, recordType } = that;
if (that.loading || that.loadend) return;
that.loading = true;
that.loadTitle = '';
moneyList({
keyword: this.keyword,
start: this.start,
stop: this.stop,
page: page,
limit: limit
page,
limit,
}, recordType).then(res => {
this.expend = res.data.expend;
this.income = res.data.income;
// for (let i = 0; i < res.data.time.length; i++) {
// // if (!this.times.includes(res.data.time[i])) {
// this.times.push(res.data.time[i])
// this.recordList.push({
// time: res.data.time[i],
// child: []
// })
// // }
// }
// // for (let x = 0; x < this.times.length; x++) {
// for (let j = 0; j < res.data.list.length; j++) {
// // if (this.times[x] === res.data.list[j].time_key) {
// // }
// this.recordList[j].child.push(res.data.list[j])
// }
// // }
this.recordList = this.recordList.concat(res.data.list)
let loadend = res.data.list.length < that.limit;
this.recordList = this.recordList.concat(res.data.list);
const loadend = res.data.list.length < that.limit;
that.loadend = loadend;
that.loadTitle = loadend ? '没有更多内容啦~' : '加载更多';
that.page += 1;
that.loading = false;
}).catch(err => {
}).catch(() => {
that.loading = false;
that.loadTitle = '加载更多';
})
},
getRecordListCount: function() {
let that = this;
getSpreadInfo().then(res => {
that.recordCount = res.data.commissionCount;
that.extractCount = res.data.extractCount;
});
},
jumpPath(url){
uni.navigateTo({
url
})
/**
* 页面内跳转
* @param {string} url - 目标路由
* @returns {void}
*/
jumpPath(url) {
uni.navigateTo({ url });
},
extractCancel(id){
if(this.disabled) return
/**
* 取消提现申请
* @param {number|string} id - 提现订单 ID
* @returns {void}
*/
extractCancel(id) {
if (this.disabled) return;
this.disabled = true;
extractCancelApi(id).then(res=>{
extractCancelApi(id).then(res => {
this.disabled = false;
this.changeTime({start:0,stop: 0});
return this.$util.Tips({
title: res.msg
});
}).catch(err=>{
return this.$util.Tips({
title: err
});
})
}
this.changeTime({ start: 0, stop: 0 });
return this.$util.Tips({ title: res.msg });
}).catch(err => {
this.disabled = false;
return this.$util.Tips({ title: err });
});
},
},
onReachBottom: function() {
onReachBottom() {
this.getRecordList();
}
}
},
};
</script>
<style scoped lang="scss">
.empty{
.empty {
margin: 0 20rpx 20rpx 20rpx;
}
/* ===== 团队统计卡片 ===== */
.team-stats-card {
background: linear-gradient(135deg, #4e9f3d 0%, #1e5128 100%);
border-radius: 20rpx;
margin: 20rpx 20rpx 0;
padding: 30rpx 20rpx 24rpx;
color: #fff;
}
.team-stats-title {
font-size: 28rpx;
font-weight: 600;
margin-bottom: 24rpx;
opacity: 0.92;
}
.team-stats-row {
display: flex;
align-items: center;
justify-content: space-around;
}
.team-stats-item {
flex: 1;
text-align: center;
}
.team-stats-num {
font-size: 40rpx;
font-weight: bold;
line-height: 1.2;
}
.team-stats-num.text-primary {
color: #ffe082;
}
.team-stats-label {
font-size: 24rpx;
opacity: 0.8;
margin-top: 8rpx;
}
.team-stats-divider {
width: 1rpx;
height: 56rpx;
background: rgba(255, 255, 255, 0.3);
}
/* ===== 积分来源标签 ===== */
.income-type-tag {
margin-bottom: 8rpx;
}
.tag-direct,
.tag-umbrella {
display: inline-block;
height: 36rpx;
border-radius: 6rpx;
font-size: 22rpx;
line-height: 36rpx;
padding: 0 10rpx;
}
.tag-direct {
background: rgba(78, 159, 61, 0.12);
color: #4e9f3d;
}
.tag-umbrella {
background: rgba(24, 144, 255, 0.1);
color: #1890ff;
}
/* ===== 搜索框 ===== */
.commission-details .search {
width: 100%;
background-color: #fff;
@@ -301,12 +438,6 @@
padding-left: 70rpx;
}
.box {
border-radius: 24rpx;
margin: 0 20rpx;
overflow: hidden;
}
.commission-details .search .input .placeholder {
color: #bbb;
}
@@ -320,12 +451,15 @@
transform: translateY(-50%);
}
/* ===== 列表区 ===== */
.sign-record {
margin-top: 20rpx;
}
.commission-details .promoterHeader .headerCon .money {
font-size: 36rpx;
.box {
border-radius: 24rpx;
margin: 0 20rpx;
overflow: hidden;
}
.top_num {
@@ -337,24 +471,47 @@
.radius15 {
border-radius: 14rpx 14rpx 0 0;
}
.sign-record .list .item .listn .itemn1{border-bottom:1rpx solid #eee;padding:22rpx 24rpx;}
.sign-record .list .item .listn .itemn1 .name{width:390rpx;font-size:28rpx;color:#333;margin-bottom:12rpx;}
.sign-record .list .item .listn .itemn1 .num{font-size:36rpx;color:#333333;font-family:'Regular';text-align: right;}
.sign-record .list .item .listn .itemn1 .num.font-color{color:#e93323!important;}
.sign-record .list .item .listn .itemn1 .fail{
.sign-record .list .item .listn .itemn1 {
border-bottom: 1rpx solid #eee;
padding: 22rpx 24rpx;
}
.sign-record .list .item .listn .itemn1 .name {
width: 390rpx;
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
}
.sign-record .list .item .listn .itemn1 .num {
font-size: 36rpx;
color: #333333;
font-family: 'Regular';
text-align: right;
}
.sign-record .list .item .listn .itemn1 .num.font-color {
color: #e93323 !important;
}
.sign-record .list .item .listn .itemn1 .fail {
color: #E93323;
margin-top: 14rpx;
text-align: right;
}
.sign-record .list .item .listn .itemn1 .wait{
.sign-record .list .item .listn .itemn1 .wait {
color: #FFB200;
margin-top: 14rpx;
text-align: right;
}
.mark{
.mark {
margin-bottom: 10rpx;
}
.status_badge{
.status_badge {
display: inline-block;
height: 40rpx;
border-radius: 8rpx;
@@ -362,18 +519,21 @@
line-height: 40rpx;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
margin-left:16rpx;
padding:0 12rpx 0;
margin-left: 16rpx;
padding: 0 12rpx 0;
}
.success{
.success {
background: rgba(24, 144, 255, .1);
color: #1890FF;
}
.default{
.default {
background: #FFF1E5;
color: #FF7D00;
}
.error{
.error {
background: #FDEBEB;
color: #F53F3F;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -183,7 +183,9 @@ const actions = {
let diyVersion = uni.getStorageSync('diyVersionNav');
if (res.data.version === diyVersion) {
let pageFooter = uni.getStorageSync('pageFooterData');
commit("SET_PAGE_FOOTER", pageFooter);
if (pageFooter && pageFooter.menuList && pageFooter.menuList.length) {
commit("SET_PAGE_FOOTER", pageFooter);
}
} else {
uni.setStorageSync('diyVersionNav', res.data.version);
let result = await getNavigation();

View File

@@ -0,0 +1,809 @@
/**
* 黄精粉健康商城 - UniApp Mock 数据集中管理
* Phase 1 前端开发使用Phase 4 集成后可移除
*/
// ========== 场景切换系统 ==========
/**
* 当前演示场景
* 'A' - 新用户(首次体验)
* 'B' - 活跃用户(等待退款中)- 默认
* 'C' - VIP用户退款刚触发
*/
let MOCK_SCENARIO = 'B';
/**
* 切换场景并触发页面刷新
* @param {string} scenario - 'A' | 'B' | 'C'
*/
export function setMockScenario(scenario) {
if (['A', 'B', 'C'].includes(scenario)) {
MOCK_SCENARIO = scenario;
console.log(`[HJF Mock] 已切换到场景 ${scenario}`);
// 触发全局事件,通知页面刷新
uni.$emit('hjf-scenario-changed', scenario);
return true;
}
return false;
}
/**
* 获取当前场景
*/
export function getCurrentScenario() {
return MOCK_SCENARIO;
}
// ========== 公排模块 ==========
export const MOCK_QUEUE_STATUS = {
totalOrders: 156,
myOrders: [
{
id: 1,
order_id: 'HJF202603100001',
amount: 3600.00,
queue_no: 142,
status: 0,
refund_time: 0,
trigger_batch: 0,
add_time: 1741593600,
position: 14,
estimated_wait: '约3天'
},
{
id: 2,
order_id: 'HJF202603080002',
amount: 3600.00,
queue_no: 98,
status: 1,
refund_time: 1741507200,
trigger_batch: 24,
add_time: 1741420800,
position: 0,
estimated_wait: '已退款'
},
{
id: 3,
order_id: 'HJF202603070003',
amount: 3600.00,
queue_no: 85,
status: 0,
refund_time: 0,
trigger_batch: 0,
add_time: 1741334400,
position: 46,
estimated_wait: '约12天'
}
],
progress: {
current_batch_count: 2,
trigger_multiple: 4,
next_refund_queue_no: 39
}
};
export const MOCK_QUEUE_HISTORY = {
list: [
{
id: 1,
order_id: 'HJF202603050001',
amount: 3600.00,
queue_no: 45,
status: 1,
refund_time: 1741334400,
trigger_batch: 11,
add_time: 1741161600,
time_key: '2026-03-07'
},
{
id: 2,
order_id: 'HJF202603060002',
amount: 3600.00,
queue_no: 67,
status: 0,
refund_time: 0,
trigger_batch: 0,
add_time: 1741248000,
time_key: '2026-03-06'
},
{
id: 3,
order_id: 'HJF202603040003',
amount: 3600.00,
queue_no: 33,
status: 1,
refund_time: 1741248000,
trigger_batch: 8,
add_time: 1741075200,
time_key: '2026-03-04'
}
],
count: 25,
page: 1,
limit: 15
};
// ========== 资产模块 ==========
export const MOCK_ASSETS_OVERVIEW = {
now_money: '7200.00',
frozen_points: 15000,
available_points: 3200,
today_release: 6,
total_queue_refund: '14400.00',
total_points_earned: 18200,
member_level: 2,
member_level_name: '云店'
};
export const MOCK_POINTS_DETAIL = {
list: [
{
id: 1,
title: '直推奖励 - 用户张三购买报单商品',
type: 'reward_direct',
points: 800,
pm: 1,
status: 'frozen',
add_time: '2026-03-10 14:30',
order_id: 'HJF202603100005'
},
{
id: 2,
title: '每日释放 - 待释放积分自动解冻',
type: 'release',
points: 6,
pm: 1,
status: 'released',
add_time: '2026-03-10 00:00',
release_date: '2026-03-10'
},
{
id: 3,
title: '积分消费 - 购买普通商品',
type: 'consume',
points: 200,
pm: 0,
status: 'released',
add_time: '2026-03-09 16:22',
order_id: 'HJF202603090012'
},
{
id: 4,
title: '伞下奖励 - 用户李四购买报单商品',
type: 'reward_umbrella',
points: 300,
pm: 1,
status: 'frozen',
add_time: '2026-03-09 10:15',
order_id: 'HJF202603090003'
},
{
id: 5,
title: '每日释放 - 待释放积分自动解冻',
type: 'release',
points: 6,
pm: 1,
status: 'released',
add_time: '2026-03-09 00:00',
release_date: '2026-03-09'
}
],
count: 45,
page: 1,
limit: 15
};
export const MOCK_CASH_DETAIL = {
list: [
{
id: 1,
title: '公排退款 - 订单HJF202603050001',
amount: '3600.00',
pm: 1,
add_time: '2026-03-07 12:00',
order_id: 'HJF202603050001'
},
{
id: 2,
title: '提现 - 微信零钱',
amount: '930.00',
pm: 0,
add_time: '2026-03-06 15:30',
remark: '手续费¥70.00'
},
{
id: 3,
title: '购物消费',
amount: '299.00',
pm: 0,
add_time: '2026-03-05 09:20',
order_id: 'HJF202603050010'
}
],
count: 12,
page: 1,
limit: 15
};
export const MOCK_WITHDRAW_INFO = {
now_money: '7200.00',
min_extract: 100,
fee_rate: 7,
extract_bank: ['微信零钱', '支付宝', '银行卡'],
bank_list: [
{ bank_name: '中国工商银行', bank_code: '1234****5678' }
]
};
// ========== 会员模块 ==========
export const MOCK_MEMBER_INFO = {
member_level: 2,
member_level_name: '云店',
direct_count: 8,
umbrella_count: 35,
umbrella_orders: 42,
next_level_name: '服务商',
next_level_require: 100,
progress_percent: 42
};
export const MOCK_TEAM_DATA = {
direct_count: 8,
umbrella_count: 35,
umbrella_orders: 42,
members: [
{
uid: 10087,
nickname: '张三',
avatar: '/static/images/default_avatar.png',
member_level: 1,
member_level_name: '创客',
join_time: '2026-02-15',
direct_orders: 5,
is_direct: true
},
{
uid: 10088,
nickname: '李四',
avatar: '/static/images/default_avatar.png',
member_level: 0,
member_level_name: '普通会员',
join_time: '2026-03-01',
direct_orders: 1,
is_direct: false,
parent_nickname: '张三'
},
{
uid: 10089,
nickname: '王五',
avatar: '/static/images/default_avatar.png',
member_level: 2,
member_level_name: '云店',
join_time: '2026-01-20',
direct_orders: 12,
is_direct: true
}
],
page: 1,
count: 35
};
export const MOCK_TEAM_INCOME = {
list: [
{
id: 1,
title: '直推奖励',
from_uid: 10087,
from_nickname: '张三',
order_id: 'HJF202603100005',
points: 800,
type: 'direct',
add_time: '2026-03-10 14:30'
},
{
id: 2,
title: '伞下奖励(级差)',
from_uid: 10088,
from_nickname: '李四',
order_id: 'HJF202603090003',
points: 300,
type: 'umbrella',
add_time: '2026-03-09 10:15'
},
{
id: 3,
title: '直推奖励',
from_uid: 10089,
from_nickname: '王五',
order_id: 'HJF202603080010',
points: 800,
type: 'direct',
add_time: '2026-03-08 16:45'
}
],
count: 22,
page: 1,
limit: 15
};
// ========== 引导模块 ==========
export const MOCK_GUIDE_DATA = {
slides: [
{
title: '欢迎来到黄精粉健康商城',
desc: '健康好物,品质生活',
image: '/static/images/guide/slide1.png'
},
{
title: '公排返利机制',
desc: '购买报单商品自动进入公排每进4单退1单全额返还',
image: '/static/images/guide/slide2.png'
},
{
title: '会员积分体系',
desc: '推荐好友即获积分奖励,积分每日自动释放',
image: '/static/images/guide/slide3.png'
}
]
};
// ========== 场景数据集合 ==========
/**
* 场景 A - 新用户(首次体验)
*/
const SCENARIO_A_DATA = {
queueStatus: {
totalOrders: 12,
myOrders: [],
progress: {
current_batch_count: 0,
trigger_multiple: 4,
next_refund_queue_no: 4
}
},
queueHistory: {
list: [],
count: 0,
page: 1,
limit: 15
},
assetsOverview: {
now_money: '0.00',
frozen_points: 0,
available_points: 0,
today_release: 0,
total_queue_refund: '0.00',
total_points_earned: 0,
member_level: 0,
member_level_name: '普通会员'
},
pointsDetail: {
list: [],
count: 0,
page: 1,
limit: 15
},
cashDetail: {
list: [],
count: 0,
page: 1,
limit: 15
},
withdrawInfo: {
now_money: '0.00',
min_extract: 100,
fee_rate: 7,
extract_bank: ['微信零钱', '支付宝', '银行卡'],
bank_list: []
},
memberInfo: {
member_level: 0,
member_level_name: '普通会员',
direct_count: 0,
umbrella_count: 0,
umbrella_orders: 0,
next_level_name: '创客',
next_level_require: 3,
progress_percent: 0
},
teamData: {
direct_count: 0,
umbrella_count: 0,
umbrella_orders: 0,
members: [],
page: 1,
count: 0
},
teamIncome: {
list: [],
count: 0,
page: 1,
limit: 15
}
};
/**
* 场景 B - 活跃用户(等待退款中)- 使用原有数据
*/
const SCENARIO_B_DATA = {
queueStatus: MOCK_QUEUE_STATUS,
queueHistory: MOCK_QUEUE_HISTORY,
assetsOverview: MOCK_ASSETS_OVERVIEW,
pointsDetail: MOCK_POINTS_DETAIL,
cashDetail: MOCK_CASH_DETAIL,
withdrawInfo: MOCK_WITHDRAW_INFO,
memberInfo: MOCK_MEMBER_INFO,
teamData: MOCK_TEAM_DATA,
teamIncome: MOCK_TEAM_INCOME
};
/**
* 场景 C - VIP 用户(退款刚触发)
*/
const SCENARIO_C_DATA = {
queueStatus: {
totalOrders: 289,
myOrders: [
{
id: 10,
order_id: 'HJF202603110001',
amount: 3600.00,
queue_no: 285,
status: 1,
refund_time: Date.now() / 1000 - 120,
trigger_batch: 71,
add_time: Date.now() / 1000 - 86400 * 5,
position: 0,
estimated_wait: '已退款'
},
{
id: 9,
order_id: 'HJF202603090010',
amount: 3600.00,
queue_no: 268,
status: 0,
refund_time: 0,
trigger_batch: 0,
add_time: Date.now() / 1000 - 86400 * 2,
position: 8,
estimated_wait: '约2天'
},
{
id: 8,
order_id: 'HJF202603080008',
amount: 3600.00,
queue_no: 244,
status: 1,
refund_time: Date.now() / 1000 - 86400 * 3,
trigger_batch: 61,
add_time: Date.now() / 1000 - 86400 * 8,
position: 0,
estimated_wait: '已退款'
},
{
id: 7,
order_id: 'HJF202603050007',
amount: 3600.00,
queue_no: 196,
status: 1,
refund_time: Date.now() / 1000 - 86400 * 10,
trigger_batch: 49,
add_time: Date.now() / 1000 - 86400 * 15,
position: 0,
estimated_wait: '已退款'
}
],
progress: {
current_batch_count: 1,
trigger_multiple: 4,
next_refund_queue_no: 72
}
},
queueHistory: {
list: [
{
id: 10,
order_id: 'HJF202603110001',
amount: 3600.00,
queue_no: 285,
status: 1,
refund_time: Date.now() / 1000 - 120,
trigger_batch: 71,
add_time: Date.now() / 1000 - 86400 * 5,
time_key: '2026-03-11'
},
{
id: 9,
order_id: 'HJF202603090010',
amount: 3600.00,
queue_no: 268,
status: 0,
refund_time: 0,
trigger_batch: 0,
add_time: Date.now() / 1000 - 86400 * 2,
time_key: '2026-03-09'
},
{
id: 8,
order_id: 'HJF202603080008',
amount: 3600.00,
queue_no: 244,
status: 1,
refund_time: Date.now() / 1000 - 86400 * 3,
trigger_batch: 61,
add_time: Date.now() / 1000 - 86400 * 8,
time_key: '2026-03-08'
}
],
count: 4,
page: 1,
limit: 15
},
assetsOverview: {
now_money: '25200.00',
frozen_points: 38500,
available_points: 12600,
today_release: 15,
total_queue_refund: '50400.00',
total_points_earned: 51100,
member_level: 3,
member_level_name: '服务商'
},
pointsDetail: {
list: [
{
id: 50,
title: '直推奖励 - 用户刘五购买报单商品',
type: 'reward_direct',
points: 1000,
pm: 1,
status: 'frozen',
add_time: '2026-03-11 10:20',
order_id: 'HJF202603110025'
},
{
id: 49,
title: '伞下奖励 - 用户赵六购买报单商品',
type: 'reward_umbrella',
points: 200,
pm: 1,
status: 'frozen',
add_time: '2026-03-11 08:15',
order_id: 'HJF202603110018'
},
{
id: 48,
title: '每日释放 - 待释放积分自动解冻',
type: 'release',
points: 15,
pm: 1,
status: 'released',
add_time: '2026-03-11 00:00',
release_date: '2026-03-11'
},
{
id: 47,
title: '直推奖励 - 用户孙七购买报单商品',
type: 'reward_direct',
points: 1000,
pm: 1,
status: 'frozen',
add_time: '2026-03-10 16:30',
order_id: 'HJF202603100045'
}
],
count: 156,
page: 1,
limit: 15
},
cashDetail: {
list: [
{
id: 15,
title: '公排退款 - 订单HJF202603110001',
amount: '3600.00',
pm: 1,
add_time: '2026-03-11 10:00',
order_id: 'HJF202603110001'
},
{
id: 14,
title: '公排退款 - 订单HJF202603080008',
amount: '3600.00',
pm: 1,
add_time: '2026-03-08 14:00',
order_id: 'HJF202603080008'
},
{
id: 13,
title: '提现 - 微信零钱',
amount: '9300.00',
pm: 0,
add_time: '2026-03-07 15:30',
remark: '手续费¥700.00'
},
{
id: 12,
title: '公排退款 - 订单HJF202603050007',
amount: '3600.00',
pm: 1,
add_time: '2026-03-05 12:00',
order_id: 'HJF202603050007'
}
],
count: 28,
page: 1,
limit: 15
},
withdrawInfo: {
now_money: '25200.00',
min_extract: 100,
fee_rate: 7,
extract_bank: ['微信零钱', '支付宝', '银行卡'],
bank_list: [
{ bank_name: '中国工商银行', bank_code: '1234****5678' }
]
},
memberInfo: {
member_level: 3,
member_level_name: '服务商',
direct_count: 15,
umbrella_count: 80,
umbrella_orders: 125,
next_level_name: '分公司',
next_level_require: 1000,
progress_percent: 12.5
},
teamData: {
direct_count: 15,
umbrella_count: 80,
umbrella_orders: 125,
members: [
{
uid: 10091,
nickname: '刘五',
avatar: '/static/images/default_avatar.png',
member_level: 2,
member_level_name: '云店',
join_time: '2026-02-01',
direct_orders: 35,
is_direct: true
},
{
uid: 10092,
nickname: '赵六',
avatar: '/static/images/default_avatar.png',
member_level: 2,
member_level_name: '云店',
join_time: '2026-02-10',
direct_orders: 28,
is_direct: true
},
{
uid: 10093,
nickname: '孙七',
avatar: '/static/images/default_avatar.png',
member_level: 1,
member_level_name: '创客',
join_time: '2026-02-20',
direct_orders: 12,
is_direct: true
},
{
uid: 10094,
nickname: '周八',
avatar: '/static/images/default_avatar.png',
member_level: 0,
member_level_name: '普通会员',
join_time: '2026-03-05',
direct_orders: 2,
is_direct: false,
parent_nickname: '刘五'
}
],
page: 1,
count: 80
},
teamIncome: {
list: [
{
id: 50,
title: '直推奖励',
from_uid: 10091,
from_nickname: '刘五',
order_id: 'HJF202603110025',
points: 1000,
type: 'direct',
add_time: '2026-03-11 10:20'
},
{
id: 49,
title: '伞下奖励(级差)',
from_uid: 10094,
from_nickname: '周八',
order_id: 'HJF202603110018',
points: 200,
type: 'umbrella',
add_time: '2026-03-11 08:15'
},
{
id: 48,
title: '直推奖励',
from_uid: 10092,
from_nickname: '赵六',
order_id: 'HJF202603100045',
points: 1000,
type: 'direct',
add_time: '2026-03-10 16:30'
},
{
id: 47,
title: '伞下奖励(级差)',
from_uid: 10095,
from_nickname: '吴九',
order_id: 'HJF202603100032',
points: 200,
type: 'umbrella',
add_time: '2026-03-10 11:45'
}
],
count: 85,
page: 1,
limit: 15
}
};
/**
* 场景数据映射
*/
const MOCK_SCENARIO_DATA = {
A: SCENARIO_A_DATA,
B: SCENARIO_B_DATA,
C: SCENARIO_C_DATA
};
/**
* 场景感知的 Mock 数据获取函数
*/
export function getMockQueueStatus() {
return JSON.parse(JSON.stringify(MOCK_SCENARIO_DATA[MOCK_SCENARIO].queueStatus));
}
export function getMockQueueHistory() {
return JSON.parse(JSON.stringify(MOCK_SCENARIO_DATA[MOCK_SCENARIO].queueHistory));
}
export function getMockAssetsOverview() {
return JSON.parse(JSON.stringify(MOCK_SCENARIO_DATA[MOCK_SCENARIO].assetsOverview));
}
export function getMockPointsDetail() {
return JSON.parse(JSON.stringify(MOCK_SCENARIO_DATA[MOCK_SCENARIO].pointsDetail));
}
export function getMockCashDetail() {
return JSON.parse(JSON.stringify(MOCK_SCENARIO_DATA[MOCK_SCENARIO].cashDetail));
}
export function getMockWithdrawInfo() {
return JSON.parse(JSON.stringify(MOCK_SCENARIO_DATA[MOCK_SCENARIO].withdrawInfo));
}
export function getMockMemberInfo() {
return JSON.parse(JSON.stringify(MOCK_SCENARIO_DATA[MOCK_SCENARIO].memberInfo));
}
export function getMockTeamData() {
return JSON.parse(JSON.stringify(MOCK_SCENARIO_DATA[MOCK_SCENARIO].teamData));
}
export function getMockTeamIncome() {
return JSON.parse(JSON.stringify(MOCK_SCENARIO_DATA[MOCK_SCENARIO].teamIncome));
}

View File

@@ -18,21 +18,32 @@ module.exports = {
}
},
chainWebpack: config => {
// 优先使用 HBuilderX 插件内的 babel 插件,若不存在则使用项目 node_modules便于命令行运行
const path = require('path')
const HX_BABEL = '/Applications/HBuilderX.app/Contents/HBuilderX/plugins/uniapp-cli/node_modules'
const optionalChaining = require.resolve('@babel/plugin-proposal-optional-chaining', { paths: [HX_BABEL] })
const nullishCoalescing = require.resolve('@babel/plugin-proposal-nullish-coalescing-operator', { paths: [HX_BABEL] })
config.module.rule('js').use('babel-loader').tap(options => {
options = options || {}
options.plugins = options.plugins || []
const pluginPaths = options.plugins.map(p => (Array.isArray(p) ? p[0] : p))
if (!pluginPaths.includes(optionalChaining)) {
options.plugins.push(optionalChaining)
}
if (!pluginPaths.includes(nullishCoalescing)) {
options.plugins.push(nullishCoalescing)
}
return options
})
const projectRoot = path.resolve(__dirname)
let optionalChaining, nullishCoalescing
try {
optionalChaining = require.resolve('@babel/plugin-proposal-optional-chaining', { paths: [HX_BABEL] })
nullishCoalescing = require.resolve('@babel/plugin-proposal-nullish-coalescing-operator', { paths: [HX_BABEL] })
} catch (e) {
optionalChaining = require.resolve('@babel/plugin-proposal-optional-chaining', { paths: [projectRoot] })
nullishCoalescing = require.resolve('@babel/plugin-proposal-nullish-coalescing-operator', { paths: [projectRoot] })
}
if (config.module.rules.get('js')) {
config.module.rule('js').use('babel-loader').tap(options => {
options = options || {}
options.plugins = options.plugins || []
const pluginPaths = options.plugins.map(p => (Array.isArray(p) ? p[0] : p))
if (!pluginPaths.includes(optionalChaining)) {
options.plugins.push(optionalChaining)
}
if (!pluginPaths.includes(nullishCoalescing)) {
options.plugins.push(nullishCoalescing)
}
return options
})
}
},
configureWebpack: config => {
if (process.env.NODE_ENV === 'production') {