feat(uniapp_v2): 二开功能迁移与小程序主包优化

- 从 uniapp 迁移 HJF 页面、API、组件及用户/订单相关改动
- queue、assets 使用独立分包以降低主包体积
- 修复首页单根节点与支付结果页 v-if 链
- 关闭 HjfDemoPanel 全局注册;uniNoticeBar 注释 $getAppWebview 避免 __webviewId__ 报错
- 配置域名与 manifest 应用名称;cache/store 防御性处理

Made-with: Cursor
This commit is contained in:
apple
2026-03-26 12:16:01 +08:00
parent c84aeda062
commit 8e17762510
742 changed files with 184117 additions and 0 deletions

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,531 @@
<template>
<view :style="colorStyle" class="queue-rules">
<!-- 推荐佣金流程图示 -->
<view class="section">
<view class="section-title">推荐佣金流程</view>
<view class="mechanism-card">
<view class="mechanism-title">推荐3人周期佣金流程</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">您获得 20% 佣金第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">您获得 30% 佣金第2位比例</view>
</view>
<view class="flow-arrow"></view>
<!-- 第三步触发完整周期 -->
<view class="flow-step trigger">
<view class="step-circle step-in"></view>
<view class="step-desc">您邀请用户C 购买报单商品</view>
<view class="step-sub">您获得 50% 佣金第3位比例</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">进入下一推荐周期</view>
<view class="step-sub">重新从 20% 开始计算</view>
</view>
</view>
<!-- 循环说明 -->
<view class="cycle-note">
<text class="cycle-icon">🔄</text>
<text class="cycle-text">如此循环每推荐3人为一个周期按比例获得佣金</text>
</view>
</view>
<!-- 示例计算卡片 -->
<view class="example-card">
<view class="example-title">示例计算报单商品 ¥3,600</view>
<view class="example-row">
<text class="example-label">推荐第1人佣金20%</text>
<text class="example-value">¥720.00</text>
</view>
<view class="example-row">
<text class="example-label">推荐第2人佣金30%</text>
<text class="example-value">¥1,080.00</text>
</view>
<view class="example-row">
<text class="example-label">推荐第3人佣金50%</text>
<text class="example-value highlight">¥1,800.00</text>
</view>
<view class="example-divider"></view>
<view class="example-row">
<text class="example-label">一个周期累计佣金</text>
<text class="example-value highlight">¥3,600.00100%</text>
</view>
<view class="example-divider"></view>
<view class="example-desc">
佣金将在被推荐人支付订单后直接发放至您的佣金余额可随时申请提现手续费7%
</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当您推荐好友购买时您可获得对应周期位次的佣金比例20%/30%/50%)。'
},
{
title: '3人周期循环',
content: '每推荐3人为一个完整周期依次按第1人20%、第2人30%、第3人50%发放佣金。周期结束后自动进入下一轮。'
},
{
title: '佣金自动到账',
content: '佣金将在被推荐人支付订单后直接发放至您的佣金余额,无需手动申请,可在"推荐佣金"页查看。'
},
{
title: '报单商品范围',
content: '仅购买标注为"报单商品"的商品才参与佣金计算,普通商品按常规佣金比例计算。'
},
{
title: '推荐关系绑定',
content: '被推荐人须通过您的专属邀请链接/二维码注册并购买,方可绑定推荐关系并产生佣金。'
},
{
title: '积分奖励叠加',
content: '推荐佣金与积分奖励体系相互独立获得佣金的同时可正常获得待释放积分奖励每日自动释放0.4‰到可用积分。'
},
{
title: '规则变更通知',
content: '若平台对佣金规则进行调整,将提前通过公告及消息通知用户,变更后的规则不溯及既往订单。'
}
],
/**
* @type {Array<{question: string, answer: string, open: boolean}>}
* @description 常见问题列表open 字段控制手风琴展开状态
*/
faqItems: [
{
question: '推荐一个人能得多少佣金?',
answer: '取决于您在当前周期的位次。第1人20%第2人30%第3人50%。以报单商品¥3,600为例分别是¥720、¥1,080、¥1,800。',
open: false
},
{
question: '佣金会到哪里?',
answer: '佣金将发放至您的佣金余额,可在"推荐佣金"页查看并可随时申请提现提现手续费7%)。',
open: false
},
{
question: '推荐超过3人怎么计算',
answer: '每满3人为一个周期自动进入下一周期重新从第1位20%开始计算。如推荐第4人其佣金比例与第1人相同20%)。',
open: false
},
{
question: '我没有购买报单商品可以推荐别人吗?',
answer: '可以推荐,但获得佣金需要您有对应的分销等级。建议先购买报单商品成为会员,再邀请好友。',
open: false
},
{
question: '待释放积分是什么?',
answer: '每次获得佣金时同步获得等额待释放积分1元佣金=100积分。积分每天自动释放0.4‰到可用积分,可用于平台消费抵扣。',
open: false
},
{
question: '佣金和积分可以同时获得吗?',
answer: '可以。推荐好友购买报单商品后,您同时获得佣金和待释放积分,两套机制并行运作,互不影响。',
open: false
},
{
question: '如何查看我的佣金进度?',
answer: '进入"推荐佣金"页可查看当前周期推荐进度X/3、各档比例、累计佣金、佣金记录明细。',
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,249 @@
<template>
<view class="brokerage-page" :style="colorStyle">
<!-- 顶部渐变区域 -->
<view class="header-gradient">
<view class="header-gradient__circle header-gradient__circle--1"></view>
<view class="header-gradient__circle header-gradient__circle--2"></view>
<view class="header-card">
<view class="header-card__label">累计佣金收入</view>
<view class="header-card__amount">
<text class="header-card__currency">¥</text>
<text class="header-card__value">{{ progressData.total_brokerage || '0.00' }}</text>
</view>
<!-- 佣金周期进度环 -->
<HjfQueueProgress
:cycle-current="progressData.cycle_current || 0"
:cycle-total="progressData.cycle_total || 3"
:cycle-rates="progressData.cycle_rates || [20, 30, 50]"
/>
</view>
</view>
<!-- 佣金记录列表 -->
<view class="records-section">
<view class="section-header">
<view class="section-header__bar"></view>
<text class="section-header__title">佣金记录</text>
<text class="section-header__more" @tap="goToCommissionDetail">查看全部</text>
</view>
<view v-if="loading" class="loading-wrap">
<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>
<view v-else-if="records.length === 0" class="empty-wrap">
<emptyPage title="暂无佣金记录" src="/statics/images/noOrder.gif" />
</view>
<view v-else class="record-list">
<view
v-for="(item, index) in records"
:key="item.id || index"
class="record-item"
>
<view class="record-item__left">
<view class="record-item__avatar-wrap">
<image
class="record-item__avatar"
:src="item.avatar || '/statics/images/avatar.png'"
mode="aspectFill"
/>
</view>
<view class="record-item__info">
<text class="record-item__name">{{ item.nickname || '用户' }}</text>
<text class="record-item__time">{{ item.time }}</text>
</view>
</view>
<view class="record-item__right">
<text class="record-item__amount">+¥{{ item.number }}</text>
<text class="record-item__type">{{ item.title || '推荐佣金' }}</text>
</view>
</view>
</view>
</view>
<!-- 上拉加载 -->
<view v-if="records.length > 0" class="load-more">
<text v-if="loadingMore" class="load-more__text">加载中...</text>
<text v-else-if="finished" class="load-more__text"> 没有更多了 </text>
</view>
<!-- 佣金到账通知 -->
<HjfRefundNotice
:visible="showNotice"
:amount="noticeData.amount"
:order-id="noticeData.orderId"
@close="showNotice = false"
/>
</view>
</template>
<script>
import { getBrokerageProgress } 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';
import { spreadOrder } from '@/api/user.js';
export default {
name: 'BrokerageStatus',
mixins: [colors],
components: { HjfQueueProgress, HjfRefundNotice, emptyPage },
data() {
return {
progressData: {},
records: [],
loading: false,
loadingMore: false,
finished: false,
page: 1,
limit: 15,
showNotice: false,
noticeData: { amount: 0, orderId: '' }
};
},
onLoad() {
this.loadProgress();
this.loadRecords();
},
onReachBottom() {
if (!this.loadingMore && !this.finished) this.loadRecords();
},
methods: {
loadProgress() {
getBrokerageProgress()
.then(res => {
if (res && res.data) this.progressData = res.data;
})
.catch(() => {});
},
loadRecords() {
if (this.loading || this.loadingMore || this.finished) return;
const isFirst = this.page === 1;
if (isFirst) this.loading = true; else this.loadingMore = true;
spreadOrder({ page: this.page, limit: this.limit, category: 'now_money', type: 'brokerage' })
.then(res => {
const list = (res && res.data && res.data.list) ? res.data.list : [];
if (list.length < this.limit) this.finished = true;
if (isFirst) this.records = list;
else this.records = [...this.records, ...list];
this.page++;
})
.catch(() => {
if (isFirst) this.records = [];
})
.finally(() => {
this.loading = false;
this.loadingMore = false;
});
},
goToCommissionDetail() {
uni.navigateTo({ url: '/pages/users/user_spread_money/index' });
}
}
};
</script>
<style scoped lang="scss">
.brokerage-page {
min-height: 100vh;
background: #f4f5f7;
padding-bottom: 60rpx;
}
.header-gradient {
background: linear-gradient(135deg, var(--view-theme, #e93323) 0%, var(--view-gradient, #f76b1c) 100%);
padding: 40rpx 30rpx 56rpx;
position: relative;
overflow: hidden;
}
.header-gradient__circle {
position: absolute;
border-radius: 50%;
background: #fff;
opacity: 0.07;
}
.header-gradient__circle--1 { width: 400rpx; height: 400rpx; top: -150rpx; right: -80rpx; }
.header-gradient__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;
}
.header-card__label { font-size: 26rpx; color: rgba(255,255,255,0.8); margin-bottom: 8rpx; }
.header-card__amount { display: flex; align-items: baseline; margin-bottom: 28rpx; }
.header-card__currency { font-size: 36rpx; color: #fff; font-weight: 600; margin-right: 4rpx; }
.header-card__value { font-size: 72rpx; font-weight: 700; color: #fff; line-height: 1; }
.records-section {
margin: -16rpx 20rpx 0;
position: relative;
z-index: 2;
background: #fff;
border-radius: 24rpx;
padding: 0 0 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 24rpx rgba(0,0,0,0.06);
}
.section-header {
display: flex;
align-items: center;
padding: 28rpx 30rpx 20rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.section-header__bar {
width: 6rpx; height: 28rpx; border-radius: 3rpx;
background: var(--view-theme, #e93323); margin-right: 14rpx;
}
.section-header__title { font-size: 30rpx; font-weight: 600; color: #333; flex: 1; }
.section-header__more { font-size: 24rpx; color: var(--view-theme, #e93323); }
.loading-wrap {
display: flex; justify-content: center; align-items: center; gap: 12rpx;
padding: 60rpx 0;
}
.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); }
}
.empty-wrap { padding: 40rpx 0; }
.record-item {
display: flex; align-items: center; justify-content: space-between;
padding: 28rpx 30rpx;
border-bottom: 1rpx solid #f8f8f8;
}
.record-item__left { display: flex; align-items: center; gap: 20rpx; }
.record-item__avatar-wrap { width: 76rpx; height: 76rpx; border-radius: 50%; overflow: hidden; flex-shrink: 0; }
.record-item__avatar { width: 100%; height: 100%; }
.record-item__name { font-size: 28rpx; color: #333; font-weight: 500; display: block; }
.record-item__time { font-size: 22rpx; color: #999; display: block; margin-top: 6rpx; }
.record-item__right { text-align: right; }
.record-item__amount { font-size: 34rpx; font-weight: 700; color: #e93323; display: block; }
.record-item__type { font-size: 22rpx; color: #aaa; display: block; margin-top: 4rpx; }
.load-more { padding: 32rpx 0 20rpx; text-align: center; }
.load-more__text { font-size: 24rpx; color: #bbb; }
</style>