Browse Source

花掌柜-新增分销功能

ouyang 1 week ago
parent
commit
e0c53a23d9

+ 521 - 0
hdApp/src/admin/home/distributionReport.vue

@@ -0,0 +1,521 @@
+<!--
+  门店分销统计报表
+  用途:hdApp 应用-门店设置-分销报表,展示汇总数据并跳转各明细页
+-->
+<template>
+  <view class="report-page app-content">
+    <view class="filter-row">
+      <view class="filter-chip date-chip">
+        <image class="filter-icon" :src="iconSrc('order-calendar')" mode="aspectFit" />
+        <DateSelect class="date-select" top="0" defaultShowName="今天" :showAllOption="true" @selectDateFn="selectDateFn" />
+      </view>
+    </view>
+
+    <view class="data-panel">
+      <view class="data-panel-head">
+        <view class="data-panel-title-wrap">
+          <image class="data-panel-icon" src="/static/icons/stats-bar-filled.svg" mode="aspectFit" />
+          <text class="data-panel-title">分销数据</text>
+        </view>
+      </view>
+      <view class="data-grid">
+        <view class="data-card" @click="goOrderList">
+          <view class="data-card-main">
+            <view class="data-icon-wrap">
+              <image class="data-icon" src="/static/icons/order-volume-2.svg" mode="aspectFit" />
+            </view>
+            <view class="data-meta">
+              <text class="data-label">分佣订单</text>
+              <text class="data-value">{{ stat.orderCount || 0 }}单</text>
+            </view>
+          </view>
+          <text class="iconfont iconxiangyou data-arrow"></text>
+        </view>
+        <view class="data-card" @click="goCommissionList">
+          <view class="data-card-main">
+            <view class="data-icon-wrap">
+              <image class="data-icon" src="/static/icons/recharge.svg" mode="aspectFit" />
+            </view>
+            <view class="data-meta">
+              <text class="data-label">产生佣金</text>
+              <text class="data-value">¥{{ moneyText(stat.commissionAmount) }}</text>
+            </view>
+          </view>
+          <text class="iconfont iconxiangyou data-arrow"></text>
+        </view>
+        <view class="data-card" @click="goDistUserList">
+          <view class="data-card-main">
+            <view class="data-icon-wrap">
+              <image class="data-icon" src="/static/icons/customer-filled.svg" mode="aspectFit" />
+            </view>
+            <view class="data-meta">
+              <text class="data-label">获佣人数</text>
+              <text class="data-value">{{ stat.distUserCount || 0 }}人</text>
+            </view>
+          </view>
+          <text class="iconfont iconxiangyou data-arrow"></text>
+        </view>
+        <view class="data-card" @click="goInviteList">
+          <view class="data-card-main">
+            <view class="data-icon-wrap">
+              <image class="data-icon" src="/static/icons/member-level.svg" mode="aspectFit" />
+            </view>
+            <view class="data-meta">
+              <text class="data-label">拉新人数</text>
+              <text class="data-value">{{ stat.inviteCount || 0 }}人</text>
+            </view>
+          </view>
+          <text class="iconfont iconxiangyou data-arrow"></text>
+        </view>
+      </view>
+    </view>
+
+    <view class="status-panel">
+      <view class="status-panel-head">
+        <text class="status-panel-title">佣金状态</text>
+        <view class="status-flow-link" @click="goFlowList">
+          <text>佣金变动明细</text>
+          <text class="iconfont iconxiangyou status-flow-arrow"></text>
+        </view>
+      </view>
+      <view class="status-row">
+        <view class="status-item status-item--clickable" @click="goPendingList">
+          <image class="status-icon" src="/static/icons/order-bell.svg" mode="aspectFit" />
+          <text class="status-label">待结算</text>
+          <text class="status-amount">¥{{ moneyText(stat.pendingAmount) }}</text>
+        </view>
+        <view class="status-item status-item--clickable" @click="goSettledList">
+          <image class="status-icon" src="/static/icons/recharge.svg" mode="aspectFit" />
+          <text class="status-label">已结算</text>
+          <text class="status-amount">¥{{ moneyText(stat.settledAmount) }}</text>
+        </view>
+        <view class="status-item status-item--clickable" @click="goDepositList">
+          <image class="status-icon" src="/static/icons/stats-pie-filled.svg" mode="aspectFit" />
+          <text class="status-label">已存余额</text>
+          <text class="status-amount">¥{{ moneyText(stat.depositAmount) }}</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="report-tip">
+      <text class="report-tip-icon">💡</text>
+      <text class="report-tip-text">点击统计数据可查看对应明细</text>
+    </view>
+
+    <NotLogin />
+  </view>
+</template>
+
+<script>
+import DateSelect from '@/components/module/dateSelect'
+import NotLogin from '@/components/not-login'
+import { getDistributionReportStat } from '@/api/distribution'
+import { iconSrc } from '@/utils/iconSrc'
+
+const EMPTY_STAT = {
+  orderCount: 0,
+  commissionAmount: 0,
+  distUserCount: 0,
+  inviteCount: 0,
+  pendingAmount: 0,
+  settledAmount: 0,
+  depositAmount: 0
+}
+
+export default {
+  name: 'distributionReport',
+  components: { DateSelect, NotLogin },
+  data() {
+    return {
+      params: {
+        searchTime: 'today',
+        startTime: '',
+        endTime: ''
+      },
+      currentDateName: '今天',
+      stat: { ...EMPTY_STAT },
+      loading: false
+    }
+  },
+  onPullDownRefresh() {
+    this.getStat().then(() => {
+      uni.stopPullDownRefresh()
+    })
+  },
+  methods: {
+    iconSrc,
+    /** 全局混入入口:拉取报表汇总 */
+    init() {
+      return this.getStat()
+    },
+    /** 日期筛选变更 */
+    selectDateFn(val) {
+      this.params = {
+        searchTime: val.searchTime || '',
+        startTime: val.startTime || '',
+        endTime: val.endTime || ''
+      }
+      this.currentDateName = val.showName || this.getDateName(this.params)
+      this.getStat()
+    },
+    /** 请求门店分销报表汇总 */
+    getStat() {
+      if (this.loading) {
+        return Promise.resolve()
+      }
+      this.loading = true
+      return getDistributionReportStat(this.params)
+        .then((res) => {
+          if (res.code == 1 && res.data) {
+            this.stat = Object.assign({}, EMPTY_STAT, res.data)
+          } else {
+            this.stat = { ...EMPTY_STAT }
+          }
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    },
+    /** 组装明细页通用查询参数 */
+    getDetailQuery(extra) {
+      const query = {
+        searchTime: this.params.searchTime,
+        startTime: this.params.startTime,
+        endTime: this.params.endTime,
+        dateName: encodeURIComponent(this.currentDateName || this.getDateName(this.params))
+      }
+      if (extra) {
+        Object.assign(query, extra)
+      }
+      return query
+    },
+    /** 分佣订单明细 */
+    goOrderList() {
+      this.pageTo({
+        url: '/admin/home/distributionReportOrder',
+        query: this.getDetailQuery({ settleStatus: 'all' })
+      })
+    },
+    /** 产生佣金明细(同分佣订单列表) */
+    goCommissionList() {
+      this.goOrderList()
+    },
+    /** 获佣人数明细 */
+    goDistUserList() {
+      this.pageTo({
+        url: '/admin/home/distributionReportDist',
+        query: this.getDetailQuery()
+      })
+    },
+    /** 拉新人数明细 */
+    goInviteList() {
+      this.pageTo({
+        url: '/admin/home/distributionReportInvite',
+        query: this.getDetailQuery()
+      })
+    },
+    /** 佣金变动明细 */
+    goFlowList() {
+      this.pageTo({
+        url: '/admin/home/distributionReportFlow',
+        query: this.getDetailQuery()
+      })
+    },
+    /** 待结算佣金订单 */
+    goPendingList() {
+      this.pageTo({
+        url: '/admin/home/distributionReportOrder',
+        query: this.getDetailQuery({ settleStatus: '0' })
+      })
+    },
+    /** 已结算佣金订单 */
+    goSettledList() {
+      this.pageTo({
+        url: '/admin/home/distributionReportOrder',
+        query: this.getDetailQuery({ settleStatus: '1' })
+      })
+    },
+    /** 已存余额流水(分红存入) */
+    goDepositList() {
+      this.pageTo({
+        url: '/admin/home/distributionReportFlow',
+        query: this.getDetailQuery({ flowType: '2' })
+      })
+    },
+    moneyText(val) {
+      const num = parseFloat(val) || 0
+      return num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
+    },
+    /** 补全 DateSelect 展示名称 */
+    getDateName(params) {
+      if (!params.searchTime) {
+        return '全部'
+      }
+      if (params.searchTime === 'byMonth') {
+        return params.startTime || '选月份'
+      }
+      if (params.searchTime === 'custom') {
+        return params.startTime === params.endTime ? params.startTime : '选时段'
+      }
+      const map = {
+        today: '今天',
+        yesterday: '昨天',
+        thisWeek: '本周',
+        lastWeek: '上周',
+        thisMonth: '本月',
+        lastMonth: '上月'
+      }
+      return map[params.searchTime] || params.searchTime
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.report-page {
+  min-height: 100vh;
+  background: #f7faf8;
+  padding-bottom: 40upx;
+}
+
+.filter-row {
+  padding: 20upx 24upx 0;
+}
+
+.filter-chip {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #ffffff;
+  border-radius: 12upx;
+  padding: 16upx 20upx;
+}
+
+.filter-icon {
+  width: 32upx;
+  height: 32upx;
+  margin-right: 12upx;
+  flex-shrink: 0;
+}
+
+.date-select {
+  flex: 1;
+}
+
+.data-panel {
+  margin: 20upx 24upx 0;
+  border-radius: 16upx;
+  overflow: hidden;
+  background: #ffffff;
+  box-shadow: 0 4upx 16upx rgba(0, 0, 0, 0.04);
+}
+
+.data-panel-head {
+  background: linear-gradient(90deg, #09c567 0%, #07b35a 100%);
+  padding: 20upx 24upx;
+}
+
+.data-panel-title-wrap {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.data-panel-icon {
+  width: 36upx;
+  height: 36upx;
+  margin-right: 12upx;
+}
+
+.data-panel-title {
+  font-size: 30upx;
+  font-weight: 600;
+  color: #ffffff;
+}
+
+.data-grid {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  padding: 8upx 0;
+}
+
+.data-card {
+  width: 50%;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 28upx 16upx 28upx 24upx;
+  border-bottom: 1upx solid #f5f5f5;
+
+  &:nth-child(odd) {
+    border-right: 1upx solid #f5f5f5;
+  }
+
+  &:nth-child(3),
+  &:nth-child(4) {
+    border-bottom: none;
+  }
+
+  &:active {
+    background: #f9fdfb;
+  }
+}
+
+.data-card-main {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  min-width: 0;
+}
+
+.data-icon-wrap {
+  width: 64upx;
+  height: 64upx;
+  border-radius: 50%;
+  background: #eefbf3;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16upx;
+  flex-shrink: 0;
+}
+
+.data-icon {
+  width: 36upx;
+  height: 36upx;
+}
+
+.data-meta {
+  display: flex;
+  flex-direction: column;
+  min-width: 0;
+}
+
+.data-label {
+  font-size: 24upx;
+  color: #888888;
+  line-height: 1.4;
+}
+
+.data-value {
+  font-size: 30upx;
+  font-weight: 600;
+  color: #333333;
+  line-height: 1.4;
+  margin-top: 6upx;
+}
+
+.data-arrow {
+  flex-shrink: 0;
+  font-size: 22upx;
+  color: #cccccc;
+  margin-left: 8upx;
+}
+
+.status-panel {
+  margin: 20upx 24upx 0;
+  background: #ffffff;
+  border-radius: 16upx;
+  padding: 24upx;
+  box-shadow: 0 4upx 16upx rgba(0, 0, 0, 0.04);
+}
+
+.status-panel-head {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 24upx;
+}
+
+.status-panel-title {
+  font-size: 30upx;
+  font-weight: 600;
+  color: #333333;
+}
+
+.status-flow-link {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  font-size: 26upx;
+  color: #666666;
+
+  &:active {
+    opacity: 0.75;
+  }
+}
+
+.status-flow-arrow {
+  font-size: 22upx;
+  color: #cccccc;
+  margin-left: 4upx;
+}
+
+.status-row {
+  display: flex;
+  flex-direction: row;
+  align-items: stretch;
+}
+
+.status-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 8upx 0;
+
+  &--clickable:active {
+    opacity: 0.75;
+  }
+
+  &:not(:last-child) {
+    border-right: 1upx solid #f0f0f0;
+  }
+}
+
+.status-icon {
+  width: 44upx;
+  height: 44upx;
+  margin-bottom: 12upx;
+}
+
+.status-label {
+  font-size: 24upx;
+  color: #888888;
+  line-height: 1.4;
+}
+
+.status-amount {
+  font-size: 28upx;
+  font-weight: 600;
+  color: #09c567;
+  line-height: 1.4;
+  margin-top: 8upx;
+}
+
+.report-tip {
+  margin: 24upx 24upx 0;
+  padding: 20upx 24upx;
+  background: #eefbf3;
+  border-radius: 12upx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.report-tip-icon {
+  font-size: 28upx;
+  margin-right: 12upx;
+}
+
+.report-tip-text {
+  font-size: 24upx;
+  color: #5a9a72;
+  line-height: 1.5;
+}
+</style>

+ 374 - 0
hdApp/src/admin/home/distributionReportDist.vue

@@ -0,0 +1,374 @@
+<!--
+  门店获佣人数明细
+  用途:分销报表-获佣人数点击跳转,按时间段展示各分销员订单数与分佣金额
+-->
+<template>
+  <view class="dist-page app-content">
+    <view class="filter-panel">
+      <view class="filter-row">
+        <view class="date-chip">
+          <image class="filter-icon" :src="iconSrc('order-calendar')" mode="aspectFit" />
+          <DateSelect
+            class="date-select"
+            top="0"
+            :defaultShowName="currentDateName"
+            :showAllOption="true"
+            @selectDateFn="selectDateFn"
+          />
+        </view>
+        <view class="search-group">
+          <view class="search-input-wrap">
+            <text class="iconfont iconsearch search-icon"></text>
+            <input
+              v-model="keyword"
+              class="search-input"
+              type="text"
+              confirm-type="search"
+              placeholder="请输入客户名称"
+              @confirm="onSearch"
+            />
+          </view>
+          <view class="search-btn" @click="onSearch">查询</view>
+        </view>
+      </view>
+    </view>
+
+    <element-loading :loading="listLoading" text="加载中" color="#09c567">
+      <view v-if="list.data && list.data.length" class="dist-list">
+        <view
+          v-for="(item, index) in list.data"
+          :key="item.customId"
+          class="dist-card"
+          @click="goDistDetail(item)"
+        >
+          <app-avatar-module :src="item.smallAvatar" />
+          <view class="dist-user-meta">
+            <text class="dist-name">{{ item.name || '-' }}</text>
+            <text class="dist-orders">分佣订单 {{ item.orderCount || 0 }}单</text>
+          </view>
+          <view class="dist-stat">
+            <text class="dist-stat-label">分佣金额</text>
+            <text class="dist-stat-amount">¥{{ formatAmount(item.commissionAmount) }}</text>
+          </view>
+          <text class="iconfont iconxiangyou dist-arrow"></text>
+        </view>
+      </view>
+      <AppWrapperEmpty v-else-if="!listLoading" title="暂无获佣客户" :is-empty="true" />
+      <view v-else class="list-loading-placeholder" />
+      <view v-if="list.finished && list.data && list.data.length" class="list-end-tip">没有更多了</view>
+    </element-loading>
+  </view>
+</template>
+
+<script>
+import DateSelect from '@/components/module/dateSelect'
+import AppWrapperEmpty from '@/components/app-wrapper-empty'
+import AppAvatarModule from '@/components/module/app-avatar'
+import ElementLoading from '@/components/element-loading/index.vue'
+import { getDistributionShopDistList } from '@/api/distribution'
+import { iconSrc } from '@/utils/iconSrc'
+import list from '@/mixins/list'
+
+export default {
+  name: 'distributionReportDist',
+  components: { DateSelect, AppWrapperEmpty, AppAvatarModule, ElementLoading },
+  mixins: [list],
+  data() {
+    return {
+      keyword: '',
+      searchKeyword: '',
+      loadSeq: 0,
+      currentDateName: '今天',
+      queryParams: {
+        searchTime: 'today',
+        startTime: '',
+        endTime: ''
+      }
+    }
+  },
+  computed: {
+    /** 首屏列表加载中 */
+    listLoading() {
+      return this.list.loading && this.list.page === 1
+    }
+  },
+  onLoad(option) {
+    this.applyRouteParams(option || this.option || {})
+  },
+  onShow() {
+    this.resetList()
+    this.loadDistList()
+  },
+  methods: {
+    iconSrc,
+    /** 解析报表页传入的时间筛选 */
+    applyRouteParams(option) {
+      if (option.searchTime) {
+        this.queryParams.searchTime = option.searchTime
+        this.queryParams.startTime = option.startTime || ''
+        this.queryParams.endTime = option.endTime || ''
+      }
+      if (option.dateName) {
+        try {
+          this.currentDateName = decodeURIComponent(option.dateName)
+        } catch (e) {
+          this.currentDateName = option.dateName
+        }
+      }
+      if (option.keyword) {
+        this.keyword = option.keyword
+        this.searchKeyword = option.keyword
+      }
+    },
+    /** 拉取门店获佣人数列表 */
+    async loadDistList() {
+      const seq = ++this.loadSeq
+      const res = await getDistributionShopDistList({
+        page: this.list.page,
+        keyword: this.searchKeyword,
+        searchTime: this.queryParams.searchTime,
+        startTime: this.queryParams.startTime,
+        endTime: this.queryParams.endTime
+      })
+      if (seq !== this.loadSeq) {
+        return
+      }
+      this.completes(res)
+    },
+    /** 日期筛选变更 */
+    selectDateFn(val) {
+      this.queryParams = {
+        searchTime: val.searchTime || '',
+        startTime: val.startTime || '',
+        endTime: val.endTime || ''
+      }
+      this.currentDateName = val.showName || this.getDateName(this.queryParams)
+      this.resetList()
+      this.loadDistList()
+    },
+    /** 客户名称搜索 */
+    onSearch() {
+      this.searchKeyword = (this.keyword || '').trim()
+      this.resetList()
+      this.loadDistList()
+    },
+    /** 跳转客户详情页 */
+    goDistDetail(item) {
+      if (!item || !item.customId) {
+        return
+      }
+      this.pageTo({
+        url: '/admin/member/detail',
+        query: { id: item.customId }
+      })
+    },
+    formatAmount(val) {
+      const num = parseFloat(val) || 0
+      return num.toFixed(2)
+    },
+    /** 补全 DateSelect 展示名称 */
+    getDateName(params) {
+      if (!params.searchTime) {
+        return '全部'
+      }
+      if (params.searchTime === 'byMonth') {
+        return params.startTime || '选月份'
+      }
+      if (params.searchTime === 'custom') {
+        return params.startTime === params.endTime ? params.startTime : '选时段'
+      }
+      const map = {
+        today: '今天',
+        yesterday: '昨天',
+        thisWeek: '本周',
+        lastWeek: '上周',
+        thisMonth: '本月',
+        lastMonth: '上月'
+      }
+      return map[params.searchTime] || params.searchTime
+    }
+  },
+  async onPullDownRefresh() {
+    this.resetList()
+    await this.loadDistList()
+    uni.stopPullDownRefresh()
+  },
+  async onReachBottom() {
+    if (!this.list.finished) {
+      await this.loadDistList()
+    }
+    uni.stopPullDownRefresh()
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.dist-page {
+  min-height: 100vh;
+  background: #f5f6f7;
+  padding-bottom: 40upx;
+}
+
+.filter-panel {
+  background: #ffffff;
+  padding: 16upx 24upx;
+}
+
+.filter-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.date-chip {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-shrink: 0;
+  margin-right: 16upx;
+  max-width: 220upx;
+}
+
+.filter-icon {
+  width: 32upx;
+  height: 32upx;
+  margin-right: 8upx;
+  flex-shrink: 0;
+}
+
+.date-select {
+  flex: 1;
+  min-width: 0;
+}
+
+.search-group {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  min-width: 0;
+}
+
+.search-input-wrap {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #f5f6f7;
+  border-radius: 8upx;
+  padding: 0 16upx;
+  height: 72upx;
+  min-width: 0;
+}
+
+.search-icon {
+  font-size: 28upx;
+  color: #999999;
+  margin-right: 12upx;
+  flex-shrink: 0;
+}
+
+.search-input {
+  flex: 1;
+  font-size: 26upx;
+  height: 72upx;
+  color: #333333;
+}
+
+.search-btn {
+  margin-left: 12upx;
+  padding: 0 24upx;
+  height: 72upx;
+  line-height: 72upx;
+  background: #09c567;
+  color: #ffffff;
+  font-size: 26upx;
+  border-radius: 8upx;
+  flex-shrink: 0;
+
+  &:active {
+    opacity: 0.85;
+  }
+}
+
+.dist-list {
+  padding: 20upx 24upx 0;
+}
+
+.list-loading-placeholder {
+  min-height: 400upx;
+}
+
+.dist-card {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #ffffff;
+  border-radius: 16upx;
+  padding: 24upx;
+  margin-bottom: 20upx;
+  box-shadow: 0 2upx 8upx rgba(0, 0, 0, 0.04);
+
+  &:active {
+    opacity: 0.92;
+  }
+}
+
+.dist-user-meta {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  margin-left: 16upx;
+  min-width: 0;
+}
+
+.dist-name {
+  font-size: 30upx;
+  color: #333333;
+  font-weight: 600;
+  line-height: 1.4;
+}
+
+.dist-orders {
+  font-size: 24upx;
+  color: #999999;
+  margin-top: 8upx;
+  line-height: 1.4;
+}
+
+.dist-stat {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  margin-right: 8upx;
+  flex-shrink: 0;
+}
+
+.dist-stat-label {
+  font-size: 22upx;
+  color: #999999;
+  line-height: 1.4;
+}
+
+.dist-stat-amount {
+  font-size: 32upx;
+  color: #09c567;
+  font-weight: 700;
+  margin-top: 6upx;
+  line-height: 1.3;
+}
+
+.dist-arrow {
+  font-size: 22upx;
+  color: #cccccc;
+  flex-shrink: 0;
+}
+
+.list-end-tip {
+  text-align: center;
+  font-size: 24upx;
+  color: #cccccc;
+  padding: 24upx 0 16upx;
+}
+</style>

+ 380 - 0
hdApp/src/admin/home/distributionReportFlow.vue

@@ -0,0 +1,380 @@
+<!--
+  门店佣金变动明细
+  用途:分销报表-佣金变动明细/已存余额点击跳转,展示 xhDistributionFlow 流水
+-->
+<template>
+  <view class="flow-page app-content">
+    <view class="flow-toolbar">
+      <picker mode="date" fields="month" :value="monthValue" :end="monthEnd" @change="onMonthChange">
+        <view class="month-picker">
+          <text class="month-text">{{ monthLabel }}</text>
+          <text class="iconfont iconsanjiao_xia month-arrow"></text>
+        </view>
+      </picker>
+      <text class="flow-count">共{{ monthTotal }}笔</text>
+    </view>
+
+    <view class="flow-section-title">变动明细</view>
+
+    <element-loading :loading="listLoading" text="加载中" color="#09c567">
+      <view v-if="list.data && list.data.length" class="flow-list">
+        <view v-for="(item, index) in list.data" :key="item.id" class="flow-card">
+          <view class="flow-card-main">
+            <view class="flow-icon-wrap" :class="'flow-icon-wrap--type' + item.flowType">
+              <image
+                class="flow-icon"
+                :src="item.flowType == 2 ? '/static/icons/recharge.svg' : '/static/icons/order-volume-2.svg'"
+                mode="aspectFit"
+              />
+            </view>
+            <view class="flow-info">
+              <text class="flow-type-name">{{ item.flowTypeName }}</text>
+              <!-- 订单分红 -->
+              <block v-if="item.flowType == 1">
+                <text class="flow-line">订单号 {{ item.refSn || '-' }}</text>
+                <text class="flow-line flow-line--sub">下单客户 {{ item.subTitle || item.buyerName || '-' }}</text>
+                <text class="flow-line flow-line--sub">获佣人 {{ item.distDisplay || '-' }}</text>
+              </block>
+              <!-- 分红存入 -->
+              <block v-else-if="item.flowType == 2">
+                <text class="flow-line">存入时间 {{ formatFlowTime(item.flowTime) }}</text>
+                <text class="flow-line flow-line--sub">存入编号 {{ item.refSn || '-' }}</text>
+                <text class="flow-line flow-line--sub">获佣人 {{ item.distDisplay || '-' }}</text>
+              </block>
+              <!-- 其他 -->
+              <block v-else>
+                <text class="flow-line">{{ formatFlowTime(item.flowTime) }}</text>
+                <text v-if="item.refSn" class="flow-line flow-line--sub">{{ item.refSn }}</text>
+                <text class="flow-line flow-line--sub">获佣人 {{ item.distDisplay || '-' }}</text>
+              </block>
+            </view>
+            <view class="flow-amount-wrap">
+              <text class="flow-amount" :class="{ 'flow-amount--minus': item.flowType != 1 }">{{ item.amountText }}</text>
+              <text class="flow-status" :class="'flow-status--' + item.statusClass">{{ item.statusText }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+      <AppWrapperEmpty v-else-if="!listLoading" title="暂无变动明细" :is-empty="true" />
+      <view v-else class="list-loading-placeholder" />
+      <view v-if="list.finished && list.data && list.data.length" class="list-end-tip">没有更多了</view>
+    </element-loading>
+
+    <view class="flow-tip-box">
+      <text class="flow-tip-title">记录说明</text>
+      <text class="flow-tip-line">1. 订单完成后,分红将在设定时间结算</text>
+      <text class="flow-tip-line">2. 分红存入后,可在花店余额中查看</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import AppWrapperEmpty from '@/components/app-wrapper-empty'
+import ElementLoading from '@/components/element-loading/index.vue'
+import { getDistributionFlowList } from '@/api/distribution'
+import list from '@/mixins/list'
+
+export default {
+  name: 'distributionReportFlow',
+  components: { AppWrapperEmpty, ElementLoading },
+  mixins: [list],
+  data() {
+    const now = new Date()
+    const monthNum = now.getMonth() + 1
+    const monthValue = `${now.getFullYear()}-${monthNum < 10 ? '0' + monthNum : monthNum}`
+    return {
+      flowType: 'all',
+      loadSeq: 0,
+      monthValue,
+      monthEnd: monthValue,
+      monthTotal: 0,
+      queryParams: {
+        searchTime: 'byMonth',
+        startTime: monthValue,
+        endTime: ''
+      }
+    }
+  },
+  computed: {
+    /** 首屏列表加载中 */
+    listLoading() {
+      return this.list.loading && this.list.page === 1
+    },
+    monthLabel() {
+      if (!this.monthValue) {
+        return '选择月份'
+      }
+      const parts = this.monthValue.split('-')
+      if (parts.length < 2) {
+        return this.monthValue
+      }
+      return `${parts[0]}年${Number(parts[1])}月`
+    }
+  },
+  onLoad(option) {
+    this.applyRouteParams(option || this.option || {})
+  },
+  onShow() {
+    this.resetList()
+    this.loadFlowList()
+  },
+  methods: {
+    /** 解析报表页传入的筛选参数 */
+    applyRouteParams(option) {
+      if (option.flowType !== undefined && option.flowType !== '') {
+        this.flowType = String(option.flowType)
+      }
+      // 报表传入月份时同步月份选择器
+      if (option.startTime && /^\d{4}-\d{2}/.test(option.startTime)) {
+        this.monthValue = option.startTime.substr(0, 7)
+        this.queryParams.startTime = this.monthValue
+      } else if (option.searchTime === 'byMonth' && option.startTime) {
+        this.monthValue = option.startTime
+        this.queryParams.startTime = option.startTime
+      }
+    },
+    /** 拉取门店分红流水(勿命名 init,避免 globalMixins 提前触发) */
+    async loadFlowList() {
+      const seq = ++this.loadSeq
+      const params = {
+        page: this.list.page,
+        scope: 'shop',
+        searchTime: this.queryParams.searchTime,
+        startTime: this.queryParams.startTime,
+        endTime: this.queryParams.endTime
+      }
+      if (this.flowType !== 'all') {
+        params.flowType = this.flowType
+      }
+      const res = await getDistributionFlowList(params)
+      if (seq !== this.loadSeq) {
+        return
+      }
+      if (res.code == 1 && res.data) {
+        this.monthTotal = Number(res.data.monthTotal) || 0
+      }
+      this.completes(res)
+    },
+    onMonthChange(e) {
+      const val = e.detail.value
+      if (!val || val === this.monthValue) {
+        return
+      }
+      this.monthValue = val
+      this.queryParams.startTime = val
+      this.resetList()
+      this.loadFlowList()
+    },
+    formatFlowTime(time) {
+      if (!time) {
+        return '-'
+      }
+      const str = String(time)
+      if (str.length >= 16) {
+        return str.substr(0, 16)
+      }
+      return str
+    }
+  },
+  async onPullDownRefresh() {
+    this.resetList()
+    await this.loadFlowList()
+    uni.stopPullDownRefresh()
+  },
+  async onReachBottom() {
+    if (!this.list.finished) {
+      await this.loadFlowList()
+    }
+    uni.stopPullDownRefresh()
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.flow-page {
+  min-height: 100vh;
+  background: #f5f6f7;
+  padding-bottom: 40upx;
+}
+
+.flow-toolbar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 24upx 28upx;
+  background: #ffffff;
+}
+
+.month-picker {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.month-text {
+  font-size: 30upx;
+  font-weight: 600;
+  color: #333333;
+}
+
+.month-arrow {
+  margin-left: 8upx;
+  font-size: 22upx;
+  color: #999999;
+}
+
+.flow-count {
+  font-size: 26upx;
+  color: #999999;
+}
+
+.flow-section-title {
+  padding: 20upx 28upx 0;
+  font-size: 32upx;
+  font-weight: 600;
+  color: #333333;
+}
+
+.flow-list {
+  padding: 16upx 24upx 0;
+}
+
+.list-loading-placeholder {
+  min-height: 400upx;
+}
+
+.flow-card {
+  background: #ffffff;
+  border-radius: 16upx;
+  padding: 24upx;
+  margin-bottom: 20upx;
+  box-shadow: 0 2upx 8upx rgba(0, 0, 0, 0.04);
+}
+
+.flow-card-main {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+}
+
+.flow-icon-wrap {
+  width: 72upx;
+  height: 72upx;
+  border-radius: 50%;
+  background: #e8f8ef;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  margin-right: 20upx;
+
+  &--type2 {
+    background: #e8f8ef;
+  }
+}
+
+.flow-icon {
+  width: 40upx;
+  height: 40upx;
+}
+
+.flow-info {
+  flex: 1;
+  min-width: 0;
+  padding-right: 16upx;
+}
+
+.flow-type-name {
+  display: block;
+  font-size: 30upx;
+  font-weight: 600;
+  color: #333333;
+  line-height: 1.4;
+  margin-bottom: 8upx;
+}
+
+.flow-line {
+  display: block;
+  font-size: 24upx;
+  color: #666666;
+  line-height: 1.5;
+
+  &--sub {
+    color: #999999;
+    margin-top: 4upx;
+  }
+}
+
+.flow-amount-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  flex-shrink: 0;
+}
+
+.flow-amount {
+  font-size: 30upx;
+  font-weight: 600;
+  color: #09c567;
+  line-height: 1.3;
+
+  &--minus {
+    color: #078a52;
+  }
+}
+
+.flow-status {
+  margin-top: 10upx;
+  padding: 4upx 16upx;
+  border-radius: 20upx;
+  font-size: 22upx;
+  line-height: 1.4;
+
+  &--success {
+    color: #09c567;
+    background: #e8f8ef;
+  }
+
+  &--pending {
+    color: #ff9500;
+    background: #fff4e5;
+  }
+
+  &--default {
+    color: #999999;
+    background: #f5f5f5;
+  }
+}
+
+.list-end-tip {
+  text-align: center;
+  font-size: 24upx;
+  color: #cccccc;
+  padding: 24upx 0 16upx;
+}
+
+.flow-tip-box {
+  margin: 8upx 24upx 0;
+  padding: 24upx;
+  background: #f0faf4;
+  border-radius: 16upx;
+}
+
+.flow-tip-title {
+  display: block;
+  font-size: 28upx;
+  font-weight: 600;
+  color: #09c567;
+  margin-bottom: 12upx;
+}
+
+.flow-tip-line {
+  display: block;
+  font-size: 24upx;
+  color: #666666;
+  line-height: 1.7;
+  margin-top: 6upx;
+}
+</style>

+ 370 - 0
hdApp/src/admin/home/distributionReportInvite.vue

@@ -0,0 +1,370 @@
+<!--
+  门店拉新客户列表
+  用途:分销报表-拉新人数点击跳转,展示时间段内全店拉新记录
+-->
+<template>
+  <view class="invite-page app-content">
+    <view class="filter-panel">
+      <view class="filter-row">
+        <view class="date-chip">
+          <image class="filter-icon" :src="iconSrc('order-calendar')" mode="aspectFit" />
+          <DateSelect
+            class="date-select"
+            top="0"
+            :defaultShowName="currentDateName"
+            :showAllOption="true"
+            @selectDateFn="selectDateFn"
+          />
+        </view>
+        <view class="search-group">
+          <view class="search-input-wrap">
+            <text class="iconfont iconsearch search-icon"></text>
+            <input
+              v-model="keyword"
+              class="search-input"
+              type="text"
+              confirm-type="search"
+              placeholder="请输入客户名称"
+              @confirm="onSearch"
+            />
+          </view>
+          <view class="search-btn" @click="onSearch">查询</view>
+        </view>
+      </view>
+    </view>
+
+    <element-loading :loading="listLoading" text="加载中" color="#09c567">
+      <view v-if="list.data && list.data.length" class="invite-list">
+        <view
+          v-for="(item, index) in list.data"
+          :key="item.customId"
+          class="invite-card"
+          @click="goCustomDetail(item)"
+        >
+          <app-avatar-module :src="item.smallAvatar" />
+          <view class="invite-user-meta">
+            <text class="invite-name">{{ item.name || '-' }}</text>
+            <text class="invite-line">注册时间 {{ formatTime(item.bindTime) }}</text>
+            <text class="invite-line">拉新人 {{ formatInviter(item) }}</text>
+          </view>
+          <text class="iconfont iconxiangyou invite-arrow"></text>
+        </view>
+      </view>
+      <AppWrapperEmpty v-else-if="!listLoading" title="暂无拉新客户" :is-empty="true" />
+      <view v-else class="list-loading-placeholder" />
+      <view v-if="list.finished && list.data && list.data.length" class="list-end-tip">没有更多了</view>
+    </element-loading>
+  </view>
+</template>
+
+<script>
+import DateSelect from '@/components/module/dateSelect'
+import AppWrapperEmpty from '@/components/app-wrapper-empty'
+import AppAvatarModule from '@/components/module/app-avatar'
+import ElementLoading from '@/components/element-loading/index.vue'
+import { getDistributionShopInviteList } from '@/api/distribution'
+import { iconSrc } from '@/utils/iconSrc'
+import list from '@/mixins/list'
+
+export default {
+  name: 'distributionReportInvite',
+  components: { DateSelect, AppWrapperEmpty, AppAvatarModule, ElementLoading },
+  mixins: [list],
+  data() {
+    return {
+      keyword: '',
+      searchKeyword: '',
+      loadSeq: 0,
+      currentDateName: '今天',
+      queryParams: {
+        searchTime: 'today',
+        startTime: '',
+        endTime: ''
+      }
+    }
+  },
+  computed: {
+    /** 首屏列表加载中 */
+    listLoading() {
+      return this.list.loading && this.list.page === 1
+    }
+  },
+  onLoad(option) {
+    this.applyRouteParams(option || this.option || {})
+  },
+  onShow() {
+    this.resetList()
+    this.loadInviteList()
+  },
+  methods: {
+    iconSrc,
+    /** 解析报表页传入的时间筛选 */
+    applyRouteParams(option) {
+      if (option.searchTime) {
+        this.queryParams.searchTime = option.searchTime
+        this.queryParams.startTime = option.startTime || ''
+        this.queryParams.endTime = option.endTime || ''
+      }
+      if (option.dateName) {
+        try {
+          this.currentDateName = decodeURIComponent(option.dateName)
+        } catch (e) {
+          this.currentDateName = option.dateName
+        }
+      }
+      if (option.keyword) {
+        this.keyword = option.keyword
+        this.searchKeyword = option.keyword
+      }
+    },
+    /** 拉取门店拉新列表 */
+    async loadInviteList() {
+      const seq = ++this.loadSeq
+      const res = await getDistributionShopInviteList({
+        page: this.list.page,
+        keyword: this.searchKeyword,
+        searchTime: this.queryParams.searchTime,
+        startTime: this.queryParams.startTime,
+        endTime: this.queryParams.endTime
+      })
+      if (seq !== this.loadSeq) {
+        return
+      }
+      this.completes(res)
+    },
+    /** 日期筛选变更 */
+    selectDateFn(val) {
+      this.queryParams = {
+        searchTime: val.searchTime || '',
+        startTime: val.startTime || '',
+        endTime: val.endTime || ''
+      }
+      this.currentDateName = val.showName || this.getDateName(this.queryParams)
+      this.resetList()
+      this.loadInviteList()
+    },
+    /** 客户名称搜索 */
+    onSearch() {
+      this.searchKeyword = (this.keyword || '').trim()
+      this.resetList()
+      this.loadInviteList()
+    },
+    /** 跳转客户详情页 */
+    goCustomDetail(item) {
+      if (!item || !item.customId) {
+        return
+      }
+      this.pageTo({
+        url: '/admin/member/detail',
+        query: { id: item.customId }
+      })
+    },
+    formatTime(time) {
+      if (!time) {
+        return '-'
+      }
+      const str = String(time)
+      if (str.length >= 16) {
+        return str.substr(0, 16)
+      }
+      return str
+    },
+    /** 拉新人展示:优先姓名,无则显示手机尾号 */
+    formatInviter(item) {
+      if (!item) {
+        return '-'
+      }
+      if (item.inviterName) {
+        return item.inviterName
+      }
+      const mobile = item.inviterMobile ? String(item.inviterMobile) : ''
+      if (mobile.length >= 4) {
+        return '尾号' + mobile.substr(-4)
+      }
+      return '-'
+    },
+    /** 补全 DateSelect 展示名称 */
+    getDateName(params) {
+      if (!params.searchTime) {
+        return '全部'
+      }
+      if (params.searchTime === 'byMonth') {
+        return params.startTime || '选月份'
+      }
+      if (params.searchTime === 'custom') {
+        return params.startTime === params.endTime ? params.startTime : '选时段'
+      }
+      const map = {
+        today: '今天',
+        yesterday: '昨天',
+        thisWeek: '本周',
+        lastWeek: '上周',
+        thisMonth: '本月',
+        lastMonth: '上月'
+      }
+      return map[params.searchTime] || params.searchTime
+    }
+  },
+  async onPullDownRefresh() {
+    this.resetList()
+    await this.loadInviteList()
+    uni.stopPullDownRefresh()
+  },
+  async onReachBottom() {
+    if (!this.list.finished) {
+      await this.loadInviteList()
+    }
+    uni.stopPullDownRefresh()
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.invite-page {
+  min-height: 100vh;
+  background: #f5f6f7;
+  padding-bottom: 40upx;
+}
+
+.filter-panel {
+  background: #ffffff;
+  padding: 16upx 24upx;
+}
+
+.filter-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.date-chip {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-shrink: 0;
+  margin-right: 16upx;
+  max-width: 220upx;
+}
+
+.filter-icon {
+  width: 32upx;
+  height: 32upx;
+  margin-right: 8upx;
+  flex-shrink: 0;
+}
+
+.date-select {
+  flex: 1;
+  min-width: 0;
+}
+
+.search-group {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  min-width: 0;
+}
+
+.search-input-wrap {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #f5f6f7;
+  border-radius: 8upx;
+  padding: 0 16upx;
+  height: 72upx;
+  min-width: 0;
+}
+
+.search-icon {
+  font-size: 28upx;
+  color: #999999;
+  margin-right: 12upx;
+  flex-shrink: 0;
+}
+
+.search-input {
+  flex: 1;
+  font-size: 26upx;
+  height: 72upx;
+  color: #333333;
+}
+
+.search-btn {
+  margin-left: 12upx;
+  padding: 0 24upx;
+  height: 72upx;
+  line-height: 72upx;
+  background: #09c567;
+  color: #ffffff;
+  font-size: 26upx;
+  border-radius: 8upx;
+  flex-shrink: 0;
+
+  &:active {
+    opacity: 0.85;
+  }
+}
+
+.invite-list {
+  padding: 20upx 24upx 0;
+}
+
+.list-loading-placeholder {
+  min-height: 400upx;
+}
+
+.invite-card {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #ffffff;
+  border-radius: 16upx;
+  padding: 24upx;
+  margin-bottom: 20upx;
+  box-shadow: 0 2upx 8upx rgba(0, 0, 0, 0.04);
+
+  &:active {
+    opacity: 0.92;
+  }
+}
+
+.invite-user-meta {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  margin-left: 16upx;
+  min-width: 0;
+}
+
+.invite-name {
+  font-size: 30upx;
+  color: #333333;
+  font-weight: 600;
+  line-height: 1.4;
+}
+
+.invite-line {
+  font-size: 24upx;
+  color: #999999;
+  margin-top: 8upx;
+  line-height: 1.4;
+}
+
+.invite-arrow {
+  font-size: 22upx;
+  color: #cccccc;
+  flex-shrink: 0;
+  margin-left: 8upx;
+}
+
+.list-end-tip {
+  text-align: center;
+  font-size: 24upx;
+  color: #cccccc;
+  padding: 24upx 0 16upx;
+}
+</style>

+ 565 - 0
hdApp/src/admin/home/distributionReportOrder.vue

@@ -0,0 +1,565 @@
+<!--
+  门店分佣订单明细
+  用途:分销报表-分佣订单/产生佣金/待结算/已结算点击跳转,支持日期、关键词与结算状态筛选
+-->
+<template>
+  <view class="order-page app-content">
+    <view class="filter-panel">
+      <view class="filter-row">
+        <view class="date-chip">
+          <image class="filter-icon" :src="iconSrc('order-calendar')" mode="aspectFit" />
+          <DateSelect
+            class="date-select"
+            top="0"
+            :defaultShowName="currentDateName"
+            :showAllOption="true"
+            @selectDateFn="selectDateFn"
+          />
+        </view>
+        <view class="search-group">
+          <view class="search-input-wrap">
+            <text class="iconfont iconsearch search-icon"></text>
+            <input
+              v-model="keyword"
+              class="search-input"
+              type="text"
+              confirm-type="search"
+              placeholder="订单号/客户名称"
+              @confirm="onSearch"
+            />
+          </view>
+          <view class="search-btn" @click="onSearch">查询</view>
+        </view>
+      </view>
+    </view>
+
+    <view class="status-tabs">
+      <view
+        v-for="tab in statusTabs"
+        :key="tab.value"
+        class="status-tab"
+        :class="{ 'status-tab--active': settleStatus === tab.value }"
+        @click="changeStatusTab(tab.value)"
+      >
+        {{ tab.label }}
+      </view>
+    </view>
+
+    <element-loading :loading="listLoading" text="加载中" color="#09c567">
+      <view v-if="list.data && list.data.length" class="order-list">
+        <view
+          v-for="(item, index) in list.data"
+          :key="item.id"
+          class="order-card"
+          @click="goOrderDetail(item)"
+        >
+          <view class="order-card-head">
+            <view class="order-head-left">
+              <view class="order-icon-wrap" :class="'order-icon-wrap--' + item.statusClass">
+                <image class="order-icon" src="/static/icons/order-volume-2.svg" mode="aspectFit" />
+              </view>
+              <text class="order-type">订单分佣</text>
+            </view>
+            <text class="order-status" :class="'order-status--' + item.statusClass">{{ item.statusText }}</text>
+          </view>
+          <view class="order-card-content">
+            <view class="order-card-body">
+              <view class="order-line">
+                <text class="order-line-label">订单号</text>
+                <text class="order-line-value">{{ item.orderSn || '-' }}</text>
+              </view>
+              <view class="order-line">
+                <text class="order-line-label">下单客户</text>
+                <text class="order-line-value">{{ item.buyerName || '-' }}</text>
+              </view>
+              <view class="order-line">
+                <text class="order-line-label">下单时间</text>
+                <text class="order-line-value">{{ formatTime(item.orderTime) }}</text>
+              </view>
+              <view class="order-line">
+                <text class="order-line-label">实付金额</text>
+                <text class="order-line-value">¥{{ formatAmount(item.payAmount) }}</text>
+              </view>
+              <view class="order-line">
+                <text class="order-line-label">获佣人</text>
+                <text class="order-line-value">{{ formatDistName(item) }}</text>
+              </view>
+            </view>
+            <view class="order-amount-col">
+              <text class="order-amount" :class="'order-amount--' + item.statusClass">{{ item.amountText }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+      <AppWrapperEmpty v-else-if="!listLoading" title="暂无分佣订单" :is-empty="true" />
+      <view v-else class="list-loading-placeholder" />
+      <view v-if="list.finished && list.data && list.data.length" class="list-end-tip">没有更多了</view>
+    </element-loading>
+  </view>
+</template>
+
+<script>
+import DateSelect from '@/components/module/dateSelect'
+import AppWrapperEmpty from '@/components/app-wrapper-empty'
+import ElementLoading from '@/components/element-loading/index.vue'
+import { getDistributionContribOrderList } from '@/api/distribution'
+import { iconSrc } from '@/utils/iconSrc'
+import list from '@/mixins/list'
+
+export default {
+  name: 'distributionReportOrder',
+  components: { DateSelect, AppWrapperEmpty, ElementLoading },
+  mixins: [list],
+  data() {
+    return {
+      keyword: '',
+      searchKeyword: '',
+      distId: 0,
+      settleStatus: 'all',
+      loadSeq: 0,
+      currentDateName: '今天',
+      queryParams: {
+        searchTime: 'today',
+        startTime: '',
+        endTime: ''
+      },
+      statusTabs: [
+        { value: 'all', label: '全部' },
+        { value: '0', label: '待结算' },
+        { value: '1', label: '已结算' }
+      ]
+    }
+  },
+  computed: {
+    /** 首屏列表加载中 */
+    listLoading() {
+      return this.list.loading && this.list.page === 1
+    }
+  },
+  onLoad(option) {
+    this.applyRouteParams(option || this.option || {})
+  },
+  onShow() {
+    this.resetList()
+    this.loadOrderList()
+  },
+  methods: {
+    iconSrc,
+    /** 解析报表页传入的筛选参数 */
+    applyRouteParams(option) {
+      if (option.searchTime) {
+        this.queryParams.searchTime = option.searchTime
+        this.queryParams.startTime = option.startTime || ''
+        this.queryParams.endTime = option.endTime || ''
+      }
+      if (option.dateName) {
+        try {
+          this.currentDateName = decodeURIComponent(option.dateName)
+        } catch (e) {
+          this.currentDateName = option.dateName
+        }
+      }
+      if (option.settleStatus !== undefined && option.settleStatus !== '') {
+        this.settleStatus = String(option.settleStatus)
+      }
+      if (option.distId) {
+        this.distId = Number(option.distId) || 0
+      }
+      if (option.keyword) {
+        this.keyword = option.keyword
+        this.searchKeyword = option.keyword
+      }
+    },
+    /** 拉取门店分佣订单列表(勿命名 init,避免 globalMixins 提前触发) */
+    async loadOrderList() {
+      const seq = ++this.loadSeq
+      const params = {
+        page: this.list.page,
+        searchTime: this.queryParams.searchTime,
+        startTime: this.queryParams.startTime,
+        endTime: this.queryParams.endTime,
+        keyword: this.searchKeyword
+      }
+      // 指定分销员时不传 scope=shop,否则后端会清零 distId
+      if (this.distId > 0) {
+        params.distId = this.distId
+      } else {
+        params.scope = 'shop'
+      }
+      if (this.settleStatus !== 'all') {
+        params.settleStatus = this.settleStatus
+      }
+      const res = await getDistributionContribOrderList(params)
+      if (seq !== this.loadSeq) {
+        return
+      }
+      this.completes(res)
+    },
+    /** 日期筛选变更 */
+    selectDateFn(val) {
+      this.queryParams = {
+        searchTime: val.searchTime || '',
+        startTime: val.startTime || '',
+        endTime: val.endTime || ''
+      }
+      this.currentDateName = val.showName || this.getDateName(this.queryParams)
+      this.resetList()
+      this.loadOrderList()
+    },
+    /** 关键词查询 */
+    onSearch() {
+      this.searchKeyword = (this.keyword || '').trim()
+      this.resetList()
+      this.loadOrderList()
+    },
+    /** 切换结算状态 Tab */
+    changeStatusTab(value) {
+      if (this.settleStatus === value) {
+        return
+      }
+      this.settleStatus = value
+      this.resetList()
+      this.loadOrderList()
+    },
+    formatAmount(val) {
+      const num = parseFloat(val) || 0
+      return num.toFixed(2)
+    },
+    formatTime(time) {
+      if (!time) {
+        return '-'
+      }
+      const str = String(time)
+      if (str.length >= 16) {
+        return str.substr(0, 16)
+      }
+      return str
+    },
+    /** 获佣人展示:优先名称,无则显示占位 */
+    formatDistName(item) {
+      if (!item) {
+        return '-'
+      }
+      if (item.distName) {
+        return item.distName
+      }
+      return '-'
+    },
+    /** 跳转订单详情 */
+    goOrderDetail(item) {
+      const orderId = item && (item.orderId || item.id)
+      if (!orderId) {
+        return
+      }
+      this.pageTo({ url: '/admin/order/detail?id=' + orderId })
+    },
+    /** 补全 DateSelect 展示名称 */
+    getDateName(params) {
+      if (!params.searchTime) {
+        return '全部'
+      }
+      if (params.searchTime === 'byMonth') {
+        return params.startTime || '选月份'
+      }
+      if (params.searchTime === 'custom') {
+        return params.startTime === params.endTime ? params.startTime : '选时段'
+      }
+      const map = {
+        today: '今天',
+        yesterday: '昨天',
+        thisWeek: '本周',
+        lastWeek: '上周',
+        thisMonth: '本月',
+        lastMonth: '上月'
+      }
+      return map[params.searchTime] || params.searchTime
+    }
+  },
+  async onPullDownRefresh() {
+    this.resetList()
+    await this.loadOrderList()
+    uni.stopPullDownRefresh()
+  },
+  async onReachBottom() {
+    if (!this.list.finished) {
+      await this.loadOrderList()
+    }
+    uni.stopPullDownRefresh()
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.order-page {
+  min-height: 100vh;
+  background: #f5f6f7;
+  padding-bottom: 40upx;
+}
+
+.filter-panel {
+  background: #ffffff;
+  padding: 16upx 24upx;
+}
+
+.filter-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.date-chip {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-shrink: 0;
+  margin-right: 16upx;
+  max-width: 220upx;
+}
+
+.filter-icon {
+  width: 32upx;
+  height: 32upx;
+  margin-right: 8upx;
+  flex-shrink: 0;
+}
+
+.date-select {
+  flex: 1;
+  min-width: 0;
+}
+
+.search-group {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  min-width: 0;
+}
+
+.search-input-wrap {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #f5f6f7;
+  border-radius: 8upx;
+  padding: 0 16upx;
+  height: 72upx;
+  min-width: 0;
+}
+
+.search-icon {
+  font-size: 28upx;
+  color: #999999;
+  margin-right: 12upx;
+  flex-shrink: 0;
+}
+
+.search-input {
+  flex: 1;
+  font-size: 26upx;
+  height: 72upx;
+  color: #333333;
+}
+
+.search-btn {
+  margin-left: 12upx;
+  padding: 0 24upx;
+  height: 72upx;
+  line-height: 72upx;
+  background: #09c567;
+  color: #ffffff;
+  font-size: 26upx;
+  border-radius: 8upx;
+  flex-shrink: 0;
+
+  &:active {
+    opacity: 0.85;
+  }
+}
+
+.status-tabs {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 16upx 24upx;
+  background: #ffffff;
+  border-top: 1upx solid #f0f0f0;
+}
+
+.status-tab {
+  padding: 12upx 32upx;
+  font-size: 26upx;
+  color: #666666;
+  border-radius: 32upx;
+  background: #f5f7f6;
+  margin-right: 16upx;
+
+  &--active {
+    background: #09c567;
+    color: #ffffff;
+    font-weight: 600;
+  }
+}
+
+.order-list {
+  padding: 20upx 24upx 0;
+}
+
+.list-loading-placeholder {
+  min-height: 400upx;
+}
+
+.order-card {
+  background: #ffffff;
+  border-radius: 16upx;
+  padding: 24upx;
+  margin-bottom: 20upx;
+  box-shadow: 0 2upx 8upx rgba(0, 0, 0, 0.04);
+
+  &:active {
+    opacity: 0.92;
+  }
+}
+
+.order-card-head {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 16upx;
+}
+
+.order-head-left {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  min-width: 0;
+}
+
+.order-card-content {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.order-card-body {
+  flex: 1;
+  min-width: 0;
+  padding-right: 16upx;
+}
+
+.order-icon-wrap {
+  width: 48upx;
+  height: 48upx;
+  border-radius: 50%;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  margin-right: 12upx;
+  flex-shrink: 0;
+
+  &--success {
+    background: #e8f8ef;
+  }
+
+  &--pending {
+    background: #fff4e5;
+  }
+
+  &--invalid {
+    background: #f5f5f5;
+  }
+}
+
+.order-icon {
+  width: 28upx;
+  height: 28upx;
+}
+
+.order-type {
+  font-size: 30upx;
+  font-weight: 600;
+  color: #333333;
+}
+
+.order-status {
+  flex-shrink: 0;
+  margin-left: 16upx;
+  padding: 4upx 16upx;
+  border-radius: 20upx;
+  font-size: 22upx;
+
+  &--success {
+    color: #09c567;
+    background: #e8f8ef;
+  }
+
+  &--pending {
+    color: #ff9500;
+    background: #fff4e5;
+  }
+
+  &--invalid {
+    color: #999999;
+    background: #f5f5f5;
+  }
+}
+
+.order-line {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  margin-top: 10upx;
+}
+
+.order-line-label {
+  width: 128upx;
+  font-size: 24upx;
+  color: #999999;
+  flex-shrink: 0;
+}
+
+.order-line-value {
+  flex: 1;
+  font-size: 24upx;
+  color: #333333;
+  line-height: 1.5;
+  word-break: break-all;
+}
+
+.order-amount-col {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  justify-content: center;
+  flex-shrink: 0;
+  min-width: 140upx;
+}
+
+.order-amount {
+  font-size: 34upx;
+  font-weight: 700;
+  line-height: 1.3;
+
+  &--success {
+    color: #09c567;
+  }
+
+  &--pending {
+    color: #ff9500;
+  }
+
+  &--invalid {
+    color: #999999;
+  }
+}
+
+.list-end-tip {
+  text-align: center;
+  font-size: 24upx;
+  color: #cccccc;
+  padding: 24upx 0 16upx;
+}
+</style>

+ 754 - 0
hdApp/src/admin/home/distributionRule.vue

@@ -0,0 +1,754 @@
+<!--
+  分销规则设置页
+  用途:hdApp 应用-门店设置-分销规则,读写 xhDistributionRule / Tier / Scope
+  说明:本期分红时长固定不限、参与商品固定全部,对应界面隐藏
+-->
+<template>
+  <view class="dist-rule-page app-content">
+    <scroll-view scroll-y class="rule-scroll">
+      <!-- 分销开关 + 发放时间 -->
+      <view class="section-card">
+        <view class="switch-row">
+          <view class="switch-main">
+            <text class="switch-title">分销功能</text>
+            <text class="switch-desc">开启后,订单可按设置规则计算分销佣金</text>
+          </view>
+          <switch :checked="form.status == 1" color="#09C567" @change="onStatusChange" />
+        </view>
+        <view class="settle-row">
+          <text class="settle-label">佣金发放时间</text>
+          <view class="settle-input-wrap">
+            <text class="settle-text">订单完成后第</text>
+            <input
+              v-model="form.settleDays"
+              type="number"
+              class="settle-input"
+              placeholder="0"
+              @focus="clearInputOnFocus('settleDays')"
+            />
+            <text class="settle-text">天</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 分红方式 -->
+      <view class="section-card">
+        <view class="bonus-header">
+          <text class="bonus-header-title">分红方式</text>
+          <view class="tab-switch">
+            <view
+              class="tab-item"
+              :class="{ 'tab-item--active': form.bonusType == 1 }"
+              @click="switchBonusType(1)"
+            >
+              固定比例
+            </view>
+            <view
+              class="tab-item"
+              :class="{ 'tab-item--active': form.bonusType == 2 }"
+              @click="switchBonusType(2)"
+            >
+              按时间递减
+            </view>
+          </view>
+        </view>
+
+        <!-- 固定比例 -->
+        <block v-if="form.bonusType == 1">
+          <view class="form-line">
+            <text class="form-line-label">佣金比例</text>
+            <view class="form-line-input">
+              <input
+                v-model="form.bonusRate"
+                type="digit"
+                class="form-input"
+                placeholder="请输入佣金比例"
+                @focus="clearInputOnFocus('bonusRate')"
+              />
+              <text class="form-input-suffix">%</text>
+            </view>
+          </view>
+        </block>
+
+        <!-- 按时间递减 -->
+        <block v-else>
+          <view class="form-line form-line--readonly">
+            <text class="form-line-label">计时起点</text>
+            <view class="form-line-value">
+              <text>{{ timeStartLabel }}</text>
+            </view>
+          </view>
+          <view class="tier-section-head">
+            <text class="tier-section-title">佣金阶梯</text>
+            <text class="tier-tip">按绑定后的下单时间匹配佣金比例</text>
+          </view>
+          <view class="tier-list">
+            <view v-for="(tier, index) in form.tiers" :key="index" class="tier-item">
+              <text class="tier-name">第{{ index + 1 }}阶梯</text>
+              <view class="tier-content">
+                <view class="tier-row">
+                  <text class="tier-text">绑定后</text>
+                  <input
+                    v-model="tier.minDays"
+                    type="number"
+                    class="tier-input"
+                    :disabled="index === 0"
+                    @focus="clearTierInputOnFocus(tier, 'minDays')"
+                  />
+                  <!-- 最后一阶:X 天以上 -->
+                  <block v-if="index === form.tiers.length - 1">
+                    <text class="tier-text">天以上,佣金比例</text>
+                  </block>
+                  <!-- 中间阶梯:X 至 Y 天 -->
+                  <block v-else>
+                    <text class="tier-text">至</text>
+                    <input
+                      v-model="tier.maxDays"
+                      type="number"
+                      class="tier-input"
+                      placeholder="天数"
+                      @focus="clearTierInputOnFocus(tier, 'maxDays')"
+                      @blur="onTierMaxDaysBlur(index)"
+                    />
+                    <text class="tier-text">天,佣金比例</text>
+                  </block>
+                  <input
+                    v-model="tier.bonusRate"
+                    type="digit"
+                    class="tier-input tier-input--rate"
+                    @focus="clearTierInputOnFocus(tier, 'bonusRate')"
+                  />
+                  <text class="tier-text">%</text>
+                </view>
+                <text
+                  v-if="canRemoveTier(index)"
+                  class="tier-del iconfont iconshanchu"
+                  @click="removeTier(index)"
+                ></text>
+              </view>
+            </view>
+          </view>
+          <view class="tier-add" @click="addTier">
+            <text class="tier-add-icon">+</text>
+            <text class="tier-add-text">添加时间阶梯</text>
+          </view>
+        </block>
+      </view>
+
+      <!-- 规则说明 -->
+      <view class="rule-tip-box">
+        <view class="rule-tip-head">
+          <text class="rule-tip-icon">💡</text>
+          <text class="rule-tip-title">规则说明</text>
+        </view>
+        <text class="rule-tip-line">1. 佣金比例为顾客实际支付金额(不含运费)的百分比,如购买商品实际支付金额100元,设置佣金比例为5%,发放佣金为5元。</text>
+        <text class="rule-tip-line">2. 时间区间不可重叠或留空。</text>
+      </view>
+    </scroll-view>
+
+    <view class="footer-btn-wrap">
+      <button class="admin-button-com big blue footer-save-btn" @click="submitSave">保存提交</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getDistributionRule, saveDistributionRule } from '@/api/distribution'
+
+export default {
+  name: 'distributionRule',
+  data() {
+    return {
+      loading: false,
+      form: {
+        id: 0,
+        status: 0,
+        settleDays: 5,
+        bonusType: 1,
+        bonusRate: '',
+        durationType: 1,
+        durationDays: '',
+        timeStartType: 1,
+        tiers: []
+      }
+    }
+  },
+  computed: {
+    timeStartLabel() {
+      return this.form.timeStartType == 1 ? '客户绑定分销员时间' : '计时起点'
+    }
+  },
+  onLoad() {
+    this.loadRule()
+  },
+  methods: {
+    /** 拉取规则配置 */
+    loadRule() {
+      this.loading = true
+      getDistributionRule().then((res) => {
+        if (res.code == 1 && res.data) {
+          this.applyRuleData(res.data)
+        }
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    applyRuleData(data) {
+      this.form = {
+        id: data.id || 0,
+        status: Number(data.status) || 0,
+        settleDays: data.settleDays != null ? String(data.settleDays) : '5',
+        bonusType: Number(data.bonusType) || 1,
+        bonusRate: data.bonusRate != null && Number(data.bonusRate) > 0 ? String(data.bonusRate) : '',
+        // 本期隐藏分红时长,固定不限
+        durationType: 1,
+        durationDays: '',
+        timeStartType: Number(data.timeStartType) || 1,
+        tiers: this.normalizeTiers(data.tiers)
+      }
+    },
+    normalizeTiers(tiers) {
+      let list = []
+      if (Array.isArray(tiers) && tiers.length) {
+        list = tiers.map((item) => ({
+          minDays: item.minDays != null ? String(item.minDays) : '0',
+          maxDays: item.maxDays != null ? String(item.maxDays) : '0',
+          bonusRate: item.bonusRate != null ? String(item.bonusRate) : ''
+        }))
+      } else {
+        list = [
+          { minDays: '0', maxDays: '30', bonusRate: '10' },
+          { minDays: '31', maxDays: '60', bonusRate: '5' },
+          { minDays: '61', maxDays: '0', bonusRate: '2' }
+        ]
+      }
+      // 最后一阶固定为「及以上」
+      if (list.length) {
+        list[list.length - 1].maxDays = '0'
+      }
+      return list
+    },
+    onStatusChange(e) {
+      this.form.status = e.detail.value ? 1 : 0
+    },
+    switchBonusType(type) {
+      this.form.bonusType = type
+      if (type == 2 && (!this.form.tiers || !this.form.tiers.length)) {
+        this.form.tiers = this.normalizeTiers([])
+      }
+    },
+    /** 中间阶梯可删,首尾不可删 */
+    canRemoveTier(index) {
+      const len = this.form.tiers.length
+      return len > 2 && index > 0 && index < len - 1
+    },
+    /** 新阶梯插入最后一阶之前 */
+    addTier() {
+      const errMsg = this.validateTiersRequired()
+      if (errMsg) {
+        this.$msg(errMsg)
+        return
+      }
+      const tiers = this.form.tiers
+      if (!tiers.length) {
+        this.form.tiers = this.normalizeTiers([])
+        return
+      }
+      if (tiers.length < 2) {
+        this.form.tiers = this.normalizeTiers([])
+        return
+      }
+      const insertIndex = tiers.length - 1
+      const prevTier = tiers[insertIndex - 1]
+      let nextMin = 0
+      if (prevTier.maxDays !== '' && Number(prevTier.maxDays) >= 0) {
+        nextMin = Number(prevTier.maxDays) + 1
+      }
+      tiers.splice(insertIndex, 0, {
+        minDays: String(nextMin),
+        maxDays: '',
+        bonusRate: ''
+      })
+      this.syncLastTierMinDays()
+    },
+    removeTier(index) {
+      if (!this.canRemoveTier(index)) return
+      this.form.tiers.splice(index, 1)
+      this.syncLastTierMinDays()
+    },
+    /** 倒数第二阶结束天数变更后,同步最后一阶起始天 */
+    onTierMaxDaysBlur(index) {
+      if (index === this.form.tiers.length - 2) {
+        this.syncLastTierMinDays()
+      }
+    },
+    syncLastTierMinDays() {
+      const tiers = this.form.tiers
+      if (tiers.length < 2) return
+      const lastTier = tiers[tiers.length - 1]
+      const prevTier = tiers[tiers.length - 2]
+      lastTier.maxDays = '0'
+      if (prevTier.maxDays !== '' && Number(prevTier.maxDays) > 0) {
+        lastTier.minDays = String(Number(prevTier.maxDays) + 1)
+      }
+    },
+    clearInputOnFocus(field) {
+      if (this.form[field] === '0' || this.form[field] === 0) {
+        this.form[field] = ''
+      }
+    },
+    clearTierInputOnFocus(tier, field) {
+      if (tier[field] === '0' || tier[field] === 0) {
+        tier[field] = ''
+      }
+    },
+    buildPayload() {
+      return {
+        id: this.form.id,
+        status: Number(this.form.status) || 0,
+        settleDays: Number(this.form.settleDays) || 0,
+        bonusType: Number(this.form.bonusType) || 1,
+        bonusRate: Number(this.form.bonusRate) || 0,
+        // 本期隐藏分红时长,固定不限
+        durationType: 1,
+        durationDays: 0,
+        timeStartType: Number(this.form.timeStartType) || 1,
+        // 本期固定全部商品
+        productScope: 1,
+        goodsIds: [],
+        categoryIds: [],
+        tiers: (this.form.tiers || []).map((item, index) => ({
+          minDays: Number(item.minDays) || 0,
+          maxDays: item.maxDays === '' ? 0 : Number(item.maxDays),
+          bonusRate: Number(item.bonusRate) || 0,
+          sort: index
+        }))
+      }
+    },
+    /** 校验佣金阶梯:天数、比例必填 */
+    validateTiersRequired() {
+      const tiers = this.form.tiers || []
+      if (!tiers.length) {
+        return '请至少添加一个佣金阶梯'
+      }
+      for (let i = 0; i < tiers.length; i++) {
+        const tier = tiers[i]
+        const tierNo = i + 1
+        const isLast = i === tiers.length - 1
+        if (tier.minDays === '' || tier.minDays == null) {
+          return `请填写第${tierNo}阶梯的天数`
+        }
+        if (!isLast) {
+          if (tier.maxDays === '' || tier.maxDays == null) {
+            return `请填写第${tierNo}阶梯的结束天数`
+          }
+          if (Number(tier.maxDays) < Number(tier.minDays)) {
+            return `第${tierNo}阶梯结束天数不能小于起始天数`
+          }
+        }
+        if (tier.bonusRate === '' || tier.bonusRate == null) {
+          return `请填写第${tierNo}阶梯的佣金比例`
+        }
+        const rate = Number(tier.bonusRate)
+        if (rate <= 0 || rate > 100) {
+          return `请填写第${tierNo}阶梯正确的佣金比例(0-100)`
+        }
+      }
+      return ''
+    },
+    /** 提交前校验 */
+    validateForm() {
+      if (Number(this.form.bonusType) === 1) {
+        if (this.form.bonusRate === '' || this.form.bonusRate == null) {
+          return '请填写佣金比例'
+        }
+        const rate = Number(this.form.bonusRate)
+        if (rate <= 0 || rate > 100) {
+          return '请填写正确的佣金比例(0-100)'
+        }
+        return ''
+      }
+      return this.validateTiersRequired()
+    },
+    submitSave() {
+      if (this.loading) return
+      const errMsg = this.validateForm()
+      if (errMsg) {
+        this.$msg(errMsg)
+        return
+      }
+      this.$util.confirmModal({ content: '确认保存分销规则?' }, () => {
+        saveDistributionRule(this.buildPayload()).then((res) => {
+          if (res.code == 1) {
+            this.$msg(res.msg || '保存成功')
+            this.loadRule()
+          } else if (res.msg) {
+            this.$msg(res.msg)
+          }
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.dist-rule-page {
+  min-height: 100vh;
+  background: #f5f6f7;
+  padding-bottom: 160upx;
+}
+
+.rule-scroll {
+  height: calc(100vh - 140upx);
+  padding: 20upx 24upx 24upx;
+  box-sizing: border-box;
+}
+
+.section-card {
+  background: #ffffff;
+  border-radius: 16upx;
+  padding: 28upx 24upx;
+  margin-bottom: 20upx;
+}
+
+.switch-row {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  justify-content: space-between;
+}
+
+.switch-main {
+  flex: 1;
+  padding-right: 24upx;
+}
+
+.switch-title {
+  display: block;
+  font-size: 32upx;
+  font-weight: 600;
+  color: #1a1a1a;
+  line-height: 1.4;
+}
+
+.switch-desc {
+  display: block;
+  margin-top: 10upx;
+  font-size: 24upx;
+  color: #999999;
+  line-height: 1.5;
+}
+
+.settle-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 28upx;
+  padding-top: 24upx;
+  border-top: 1upx solid #f0f2f1;
+}
+
+.settle-label {
+  flex-shrink: 0;
+  font-size: 28upx;
+  color: #333333;
+}
+
+.settle-input-wrap {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: flex-end;
+  flex: 1;
+  margin-left: 16upx;
+}
+
+.settle-text {
+  font-size: 28upx;
+  color: #333333;
+}
+
+.settle-input {
+  width: 88upx;
+  height: 56upx;
+  margin: 0 12upx;
+  padding: 0 8upx;
+  text-align: center;
+  background: #f5f7f6;
+  border-radius: 8upx;
+  font-size: 28upx;
+  color: #333333;
+}
+
+/* 分红方式:标题与 Tab 同行 */
+.bonus-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 28upx;
+}
+
+.bonus-header-title {
+  font-size: 30upx;
+  font-weight: 600;
+  color: #1a1a1a;
+  flex-shrink: 0;
+  margin-right: 16upx;
+}
+
+.tab-switch {
+  display: flex;
+  flex-direction: row;
+  background: #f5f7f6;
+  border-radius: 10upx;
+  padding: 6upx;
+}
+
+.tab-item {
+  min-width: 168upx;
+  padding: 16upx 28upx;
+  text-align: center;
+  font-size: 28upx;
+  color: #666666;
+  border-radius: 8upx;
+  line-height: 1.2;
+
+  &--active {
+    background: #09c567;
+    color: #ffffff;
+    font-weight: 500;
+  }
+}
+
+.form-line {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-top: 24upx;
+
+  &--top {
+    align-items: flex-start;
+  }
+
+  &:first-of-type {
+    margin-top: 0;
+  }
+}
+
+.form-line-label {
+  width: 160upx;
+  flex-shrink: 0;
+  font-size: 28upx;
+  color: #333333;
+  line-height: 72upx;
+}
+
+.form-line-input {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.form-input {
+  flex: 1;
+  height: 72upx;
+  padding: 0 20upx;
+  background: #f5f7f6;
+  border-radius: 8upx;
+  font-size: 28upx;
+  color: #333333;
+}
+
+.form-input-suffix {
+  margin-left: 12upx;
+  font-size: 28upx;
+  color: #333333;
+}
+
+.form-line-value {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: flex-end;
+  font-size: 28upx;
+  color: #666666;
+}
+
+.form-line--readonly .form-line-value {
+  color: #333333;
+}
+
+.tier-section-head {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 24upx;
+  padding-top: 24upx;
+  margin-bottom: 16upx;
+  border-top: 1upx solid #f0f2f1;
+}
+
+.tier-section-title {
+  flex-shrink: 0;
+  font-size: 28upx;
+  font-weight: 600;
+  color: #333333;
+  line-height: 1.4;
+}
+
+.tier-tip {
+  flex: 1;
+  font-size: 24upx;
+  color: #999999;
+  line-height: 1.5;
+  text-align: right;
+  margin-left: 16upx;
+}
+
+.tier-list {
+  margin-top: 8upx;
+}
+
+.tier-item {
+  padding: 20upx 0;
+  border-bottom: 1upx solid #f0f2f1;
+
+  &:last-child {
+    border-bottom: none;
+  }
+}
+
+.tier-name {
+  display: block;
+  font-size: 26upx;
+  font-weight: 600;
+  color: #333333;
+  margin-bottom: 12upx;
+}
+
+.tier-content {
+  position: relative;
+  padding-right: 56upx;
+}
+
+.tier-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.tier-text {
+  font-size: 26upx;
+  color: #333333;
+  margin: 6upx 4upx;
+}
+
+.tier-input {
+  width: 88upx;
+  height: 56upx;
+  margin: 6upx 4upx;
+  padding: 0 8upx;
+  text-align: center;
+  background: #f5f7f6;
+  border-radius: 8upx;
+  font-size: 26upx;
+
+  &--rate {
+    width: 96upx;
+  }
+
+  &[disabled] {
+    color: #999999;
+    background: #fafafa;
+  }
+}
+
+.tier-del {
+  position: absolute;
+  right: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 36upx;
+  color: #ff4d6d;
+  padding: 8upx;
+}
+
+.tier-add {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  margin-top: 20upx;
+  padding: 20upx 0;
+  border: 2upx dashed #09c567;
+  border-radius: 12upx;
+}
+
+.tier-add-icon {
+  font-size: 36upx;
+  color: #09c567;
+  margin-right: 8upx;
+}
+
+.tier-add-text {
+  font-size: 28upx;
+  color: #09c567;
+}
+
+.rule-tip-box {
+  background: #fff5f5;
+  border-radius: 16upx;
+  padding: 24upx;
+  margin-bottom: 20upx;
+}
+
+.rule-tip-head {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 12upx;
+}
+
+.rule-tip-icon {
+  font-size: 28upx;
+  margin-right: 8upx;
+}
+
+.rule-tip-title {
+  font-size: 32upx;
+  font-weight: 600;
+  color: #ff4d6d;
+}
+
+.rule-tip-line {
+  display: block;
+  font-size: 28upx;
+  color: $fontPinkColor;
+  line-height: 1.7;
+  margin-top: 8upx;
+}
+
+.footer-btn-wrap {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  padding: 20upx 24upx;
+  padding-bottom: calc(20upx + constant(safe-area-inset-bottom));
+  padding-bottom: calc(20upx + env(safe-area-inset-bottom));
+  background: #ffffff;
+  box-shadow: 0 -2upx 12upx rgba(0, 0, 0, 0.06);
+}
+
+.footer-save-btn {
+  width: 100%;
+  border: none;
+}
+</style>

+ 3 - 1
hdApp/src/admin/home/homeMenus.js

@@ -58,7 +58,9 @@ export const APPLY_MENU_GROUPS = [
       { name: '收款流水', icon: homeIcon('pay-code-flow'), url: '/admin/order/scanPay', pf: 1 },
       { name: '绑定跑腿', icon: homeIcon('delivery'), url: '/admin/delivery/deliveryManage', pf: 1 },
       { name: '配送方式', icon: homeIcon('delivery-outline'), url: '/admin/home/psMethod', pf: 1 },
-      { name: '配置范围', icon: homeIcon('send_area'), url: '/admin/shop/sk', pf: 1 }
+      { name: '配置范围', icon: homeIcon('send_area'), url: '/admin/shop/sk', pf: 1 },
+      { name: '分销规则', icon: homeIcon('settings-filled'), url: '/admin/home/distributionRule', pf: 1 },
+      { name: '分销报表', icon: homeIcon('stats-pie-filled'), url: '/admin/home/distributionReport', pf: 1 }
     ]
   },
   {

+ 179 - 1
hdApp/src/admin/member/detail.vue

@@ -82,6 +82,48 @@
 
     </view>
 
+    <!-- 分销统计:信息板块上方 -->
+    <view class="module-com distribution-info">
+      <view class="distribution-row">
+        <view class="distribution-item distribution-item--clickable" @click="goDistributionFlow">
+          <view class="distribution-item-body">
+            <view class="distribution-icon-wrap">
+              <image class="distribution-icon" src="/static/icons/recharge.svg" mode="aspectFit" />
+            </view>
+            <view class="distribution-meta">
+              <text class="distribution-label">累计分红</text>
+              <text class="distribution-value">¥{{ formatDistributionAmount(distributionStat.totalCommission) }}</text>
+            </view>
+          </view>
+          <text class="iconfont iconxiangyou distribution-arrow"></text>
+        </view>
+        <view class="distribution-item distribution-item--clickable" @click="goDistributionInvite">
+          <view class="distribution-item-body">
+            <view class="distribution-icon-wrap">
+              <image class="distribution-icon" src="/static/icons/customer-filled.svg" mode="aspectFit" />
+            </view>
+            <view class="distribution-meta">
+              <text class="distribution-label">拉新人数</text>
+              <text class="distribution-value">{{ distributionStat.inviteCount }}人</text>
+            </view>
+          </view>
+          <text class="iconfont iconxiangyou distribution-arrow"></text>
+        </view>
+        <view class="distribution-item distribution-item--clickable" @click="goDistributionOrder">
+          <view class="distribution-item-body">
+            <view class="distribution-icon-wrap">
+              <image class="distribution-icon" src="/static/icons/order-volume-2.svg" mode="aspectFit" />
+            </view>
+            <view class="distribution-meta">
+              <text class="distribution-label">分红订单</text>
+              <text class="distribution-value">{{ distributionStat.orderCount }}单</text>
+            </view>
+          </view>
+          <text class="iconfont iconxiangyou distribution-arrow"></text>
+        </view>
+      </view>
+    </view>
+
     <view class="module-com user-contact-info">
         <view class="contact-header">
           <text class="contact-title">信息</text>
@@ -570,6 +612,7 @@ import { getPsMethodConfig } from '@/api/ps-method'
 import { getLevelInitData } from "@/api/member"
 import {toManRecharge} from "@/api/recharge"
 import {deductBalance} from "@/api/deduct"
+import { getDistributionUserStat } from '@/api/distribution'
 export default {
   name: "detail",
   components: {
@@ -637,7 +680,13 @@ export default {
         homeUnFee: 0
       },
       /** 门店送货上门总设置(xhPsMethod style=0) */
-      psHomeConfig: null
+      psHomeConfig: null,
+      /** 客户分销统计 */
+      distributionStat: {
+        totalCommission: 0,
+        inviteCount: 0,
+        orderCount: 0
+      }
     };
   },
   computed: {
@@ -875,8 +924,53 @@ export default {
         this.buyAmount = res.data.buyAmount?parseFloat(res.data.buyAmount):0;
         this.balance = res.data.balance?parseFloat(res.data.balance):0;
         this.syncHomeFormFromData(res.data);
+        this.loadDistributionStat(res.data.id);
       })
     },
+    /** 拉取客户分销统计 */
+    loadDistributionStat(customId) {
+      if (!customId) {
+        return
+      }
+      getDistributionUserStat({ customId }).then((res) => {
+        if (res.code == 1 && res.data) {
+          this.distributionStat = {
+            totalCommission: res.data.totalCommission || 0,
+            inviteCount: Number(res.data.inviteCount) || 0,
+            orderCount: Number(res.data.orderCount) || 0
+          }
+        }
+      }).catch(() => {})
+    },
+    /** 累计分红金额展示 */
+    formatDistributionAmount(val) {
+      const num = parseFloat(val) || 0
+      const fixed = num.toFixed(2)
+      const parts = fixed.split('.')
+      parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
+      return parts.join('.')
+    },
+    /** 跳转分红变动明细 */
+    goDistributionFlow() {
+      if (!this.data.id) {
+        return
+      }
+      this.pageTo({ url: '/admin/member/distributionFlow?customId=' + this.data.id })
+    },
+    /** 跳转拉新客户列表 */
+    goDistributionInvite() {
+      if (!this.data.id) {
+        return
+      }
+      this.pageTo({ url: '/admin/member/distributionInvite?customId=' + this.data.id })
+    },
+    /** 跳转分红订单明细 */
+    goDistributionOrder() {
+      if (!this.data.id) {
+        return
+      }
+      this.pageTo({ url: '/admin/member/distributionContrib?distId=' + this.data.id })
+    },
     /** 从客户详情同步送货上门规则字段 */
     syncHomeFormFromData(customData) {
       if (!customData || !customData.id) {
@@ -1177,6 +1271,90 @@ export default {
   border-radius: 10upx;
 }
 
+// 分销统计
+.distribution-info {
+  padding: 24upx 20upx;
+}
+
+.distribution-row {
+  display: flex;
+  flex-direction: row;
+  align-items: stretch;
+}
+
+.distribution-item {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 0 24upx 0 4upx;
+  position: relative;
+
+  &--clickable:active {
+    opacity: 0.75;
+  }
+
+  &:not(:last-child)::after {
+    content: '';
+    position: absolute;
+    right: 8upx;
+    top: 10upx;
+    bottom: 10upx;
+    width: 1upx;
+    background: #f0f0f0;
+  }
+}
+
+.distribution-item-body {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  min-width: 0;
+  padding-right: 8upx;
+}
+
+.distribution-arrow {
+  flex-shrink: 0;
+  font-size: 22upx;
+  color: #cccccc;
+}
+
+.distribution-icon-wrap {
+  width: 56upx;
+  height: 56upx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 12upx;
+}
+
+.distribution-icon {
+  width: 48upx;
+  height: 48upx;
+}
+
+.distribution-meta {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.distribution-label {
+  font-size: 24upx;
+  color: #888888;
+  line-height: 1.4;
+}
+
+.distribution-value {
+  margin-top: 8upx;
+  font-size: 28upx;
+  font-weight: 600;
+  color: #333333;
+  line-height: 1.3;
+}
+
 // 送货上门规则(客户级,覆盖 xhPsMethod 总设置)
 .home-rule-wrap {
   padding: 0;

+ 427 - 0
hdApp/src/admin/member/distributionContrib.vue

@@ -0,0 +1,427 @@
+<!--
+  分红明细页(xhDistributionOrder)
+  用途:客户详情-分红订单、拉新客户-查看贡献明细
+-->
+<template>
+  <view class="contrib-page app-content">
+    <view class="status-tabs">
+      <view
+        v-for="tab in statusTabs"
+        :key="tab.value"
+        class="status-tab"
+        :class="{ 'status-tab--active': settleStatus === tab.value }"
+        @click="changeStatusTab(tab.value)"
+      >
+        {{ tab.label }}
+      </view>
+    </view>
+
+    <view class="contrib-toolbar">
+      <picker v-if="showMonthPicker" mode="date" fields="month" :value="monthValue" :end="monthEnd" @change="onMonthChange">
+        <view class="month-picker">
+          <text class="month-text">{{ monthLabel }}</text>
+          <text class="iconfont iconsanjiao_xia month-arrow"></text>
+        </view>
+      </picker>
+      <text v-else class="month-text month-text--static">{{ dateName || '全部时间' }}</text>
+      <text class="month-total">本月分红 ¥{{ formatAmount(monthCommission) }}</text>
+    </view>
+
+    <element-loading :loading="listLoading" text="加载中" color="#09c567">
+      <view v-if="list.data && list.data.length" class="contrib-list">
+        <view v-for="(item, index) in list.data" :key="item.id" class="contrib-card">
+          <view class="contrib-card-head">
+            <text class="contrib-type">订单分红</text>
+            <text class="contrib-amount">{{ item.amountText }}</text>
+            <text class="contrib-status" :class="'contrib-status--' + item.statusClass">{{ item.statusText }}</text>
+          </view>
+          <view class="contrib-line">
+            <text class="contrib-line-icon">👤</text>
+            <text class="contrib-line-text">下单客户 {{ item.buyerName || '-' }}</text>
+          </view>
+          <view class="contrib-line contrib-line--money">
+            <text class="contrib-line-icon">💰</text>
+            <text class="contrib-line-text">订单金额 ¥{{ formatAmount(item.commissionBase ) }}(不含运费)</text>
+            <text class="contrib-line-split">|</text>
+            <text class="contrib-line-text">佣金比例 {{ item.commissionRate }}%</text>
+          </view>
+          <view class="contrib-line">
+            <text class="contrib-line-icon">📄</text>
+            <text class="contrib-line-text">订单号 {{ item.orderSn || '-' }}</text>
+          </view>
+          <view class="contrib-line">
+            <text class="contrib-line-icon">🕒</text>
+            <text class="contrib-line-text">{{ item.timeLabel }} {{ formatTime(item.timeValue) }}</text>
+          </view>
+          <view class="contrib-order-link" hover-class="contrib-order-link--hover" @click.stop="goOrderDetail(item)">
+            <text>查看订单详情</text>
+            <text class="iconfont iconxiangyou contrib-order-arrow"></text>
+          </view>
+        </view>
+      </view>
+      <AppWrapperEmpty v-else-if="!listLoading" title="暂无分红明细" :is-empty="true" />
+      <view v-else class="list-loading-placeholder" />
+    </element-loading>
+  </view>
+</template>
+
+<script>
+import AppWrapperEmpty from '@/components/app-wrapper-empty'
+import ElementLoading from '@/components/element-loading/index.vue'
+import { getDistributionContribOrderList } from '@/api/distribution'
+import list from '@/mixins/list'
+
+export default {
+  name: 'distributionContrib',
+  components: { AppWrapperEmpty, ElementLoading },
+  mixins: [list],
+  data() {
+    const now = new Date()
+    const monthNum = now.getMonth() + 1
+    const monthValue = `${now.getFullYear()}-${monthNum < 10 ? '0' + monthNum : monthNum}`
+    return {
+      distId: 0,
+      buyerId: 0,
+      buyerName: '',
+      scope: '',
+      settleStatus: 'all',
+      monthValue,
+      monthEnd: monthValue,
+      monthCommission: 0,
+      showMonthPicker: true,
+      dateName: '',
+      statusTabs: [
+        { value: 'all', label: '全部' },
+        { value: '0', label: '待结算' },
+        { value: '1', label: '已结算' },
+        { value: '2', label: '已失效' }
+      ],
+      queryParams: {
+        searchTime: 'byMonth',
+        startTime: monthValue,
+        endTime: ''
+      }
+    }
+  },
+  computed: {
+    /** 首屏列表加载中 */
+    listLoading() {
+      return this.list.loading && this.list.page === 1
+    },
+    monthLabel() {
+      if (!this.monthValue) {
+        return '选择月份'
+      }
+      const parts = this.monthValue.split('-')
+      if (parts.length < 2) {
+        return this.monthValue
+      }
+      return `${parts[0]}年${Number(parts[1])}月`
+    }
+  },
+  onLoad(option) {
+    this.applyRouteParams(option || this.option || {})
+  },
+  onShow() {
+    this.resetList()
+    this.init()
+  },
+  methods: {
+    /** 解析路由参数(兼容 globalMixins 提前触发 init) */
+    applyRouteParams(option) {
+      const distId = option.distId || option.customId
+      this.distId = distId ? Number(distId) : 0
+      this.buyerId = option.buyerId ? Number(option.buyerId) : 0
+      this.scope = option.scope || ''
+      if (option.searchTime) {
+        this.queryParams.searchTime = option.searchTime
+        this.queryParams.startTime = option.startTime || ''
+        this.queryParams.endTime = option.endTime || ''
+        if (option.searchTime !== 'byMonth') {
+          this.showMonthPicker = false
+        }
+      }
+      if (option.dateName) {
+        try {
+          this.dateName = decodeURIComponent(option.dateName)
+        } catch (e) {
+          this.dateName = option.dateName
+        }
+      }
+      if (option.settleStatus !== undefined && option.settleStatus !== '') {
+        this.settleStatus = String(option.settleStatus)
+      }
+      if (option.buyerName) {
+        try {
+          this.buyerName = decodeURIComponent(option.buyerName)
+        } catch (e) {
+          this.buyerName = option.buyerName
+        }
+      }
+    },
+    /** 拉取分红订单列表 */
+    async init() {
+      if (!this.distId && this.option) {
+        this.applyRouteParams(this.option)
+      }
+      if (!this.distId && this.scope !== 'shop') {
+        this.list.loading = false
+        return
+      }
+      const params = {
+        page: this.list.page,
+        searchTime: this.queryParams.searchTime,
+        startTime: this.queryParams.startTime,
+        endTime: this.queryParams.endTime
+      }
+      if (this.scope === 'shop') {
+        params.scope = 'shop'
+      } else {
+        params.distId = this.distId
+      }
+      if (this.buyerId > 0) {
+        params.buyerId = this.buyerId
+      }
+      if (this.settleStatus !== 'all') {
+        params.settleStatus = this.settleStatus
+      }
+      const res = await getDistributionContribOrderList(params)
+      if (res.code == 1 && res.data) {
+        this.monthCommission = res.data.monthCommission || 0
+      }
+      this.completes(res)
+    },
+    changeStatusTab(value) {
+      if (this.settleStatus === value) {
+        return
+      }
+      this.settleStatus = value
+      this.resetList()
+      this.init()
+    },
+    onMonthChange(e) {
+      const val = e.detail.value
+      if (!val || val === this.monthValue) {
+        return
+      }
+      this.monthValue = val
+      this.queryParams.startTime = val
+      this.resetList()
+      this.init()
+    },
+    formatAmount(val) {
+      const num = parseFloat(val) || 0
+      return num.toFixed(2)
+    },
+    formatTime(time) {
+      if (!time) {
+        return '-'
+      }
+      const str = String(time)
+      if (str.length >= 16) {
+        return str.substr(0, 16)
+      }
+      return str
+    },
+    goOrderDetail(item) {
+      const orderId = item && (item.orderId || item.id)
+      if (!orderId) {
+        return
+      }
+      this.pageTo({ url: '/admin/order/detail?id=' + orderId })
+    }
+  },
+  async onPullDownRefresh() {
+    this.resetList()
+    await this.init()
+    uni.stopPullDownRefresh()
+  },
+  async onReachBottom() {
+    if (!this.list.finished) {
+      await this.init()
+    }
+    uni.stopPullDownRefresh()
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.contrib-page {
+  min-height: 100vh;
+  background: #f5f6f7;
+  padding-bottom: 40upx;
+}
+
+.status-tabs {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 16upx 20upx;
+  background: #ffffff;
+}
+
+.status-tab {
+  flex: 1;
+  text-align: center;
+  padding: 14upx 0;
+  font-size: 26upx;
+  color: #666666;
+  border-radius: 8upx;
+  margin: 0 6upx;
+  background: #f5f7f6;
+
+  &--active {
+    background: #09c567;
+    color: #ffffff;
+    font-weight: 600;
+  }
+}
+
+.contrib-toolbar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20upx 24upx;
+  background: #ffffff;
+  border-top: 1upx solid #f0f0f0;
+}
+
+.month-picker {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.month-text {
+  font-size: 28upx;
+  font-weight: 600;
+  color: #333333;
+}
+
+.month-arrow {
+  margin-left: 8upx;
+  font-size: 22upx;
+  color: #999999;
+}
+
+.month-total {
+  font-size: 28upx;
+  font-weight: 600;
+  color: #09c567;
+}
+
+.contrib-list {
+  padding: 20upx 24upx 0;
+}
+
+.list-loading-placeholder {
+  min-height: 400upx;
+}
+
+.contrib-card {
+  background: #ffffff;
+  border-radius: 16upx;
+  padding: 24upx;
+  margin-bottom: 20upx;
+  box-shadow: 0 2upx 8upx rgba(0, 0, 0, 0.04);
+}
+
+.contrib-card-head {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 16upx;
+  position: relative;
+  padding-right: 120upx;
+}
+
+.contrib-type {
+  font-size: 30upx;
+  font-weight: 600;
+  color: #333333;
+  margin-right: 16upx;
+}
+
+.contrib-amount {
+  font-size: 32upx;
+  font-weight: 600;
+  color: #09c567;
+}
+
+.contrib-status {
+  position: absolute;
+  right: 0;
+  top: 0;
+  padding: 4upx 16upx;
+  border-radius: 20upx;
+  font-size: 22upx;
+
+  &--success {
+    color: #09c567;
+    background: #e8f8ef;
+  }
+
+  &--pending {
+    color: #ff9500;
+    background: #fff4e5;
+  }
+
+  &--invalid {
+    color: #999999;
+    background: #f5f5f5;
+  }
+}
+
+.contrib-line {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-top: 12upx;
+
+  &--money {
+    flex-wrap: wrap;
+  }
+}
+
+.contrib-line-icon {
+  width: 36upx;
+  font-size: 24upx;
+  flex-shrink: 0;
+}
+
+.contrib-line-text {
+  font-size: 24upx;
+  color: #666666;
+  line-height: 1.5;
+}
+
+.contrib-line-split {
+  margin: 0 12upx;
+  font-size: 24upx;
+  color: #dddddd;
+}
+
+.contrib-order-link {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: flex-end;
+  margin-top: 20upx;
+  padding: 16upx 0 4upx;
+  border-top: 1upx solid #f5f5f5;
+  font-size: 24upx;
+  color: #666666;
+  min-height: 72upx;
+}
+
+.contrib-order-link--hover {
+  opacity: 0.7;
+}
+
+.contrib-order-arrow {
+  margin-left: 6upx;
+  font-size: 22upx;
+  color: #cccccc;
+}
+</style>

+ 390 - 0
hdApp/src/admin/member/distributionFlow.vue

@@ -0,0 +1,390 @@
+<!--
+  分红变动明细页
+  用途:hdApp 客户详情-累计分红点击进入,展示 xhDistributionFlow 流水
+-->
+<template>
+  <view class="flow-page app-content">
+    <view class="flow-toolbar">
+      <picker v-if="showMonthPicker" mode="date" fields="month" :value="monthValue" :end="monthEnd" @change="onMonthChange">
+        <view class="month-picker">
+          <text class="month-text">{{ monthLabel }}</text>
+          <text class="iconfont iconsanjiao_xia month-arrow"></text>
+        </view>
+      </picker>
+      <text v-else class="month-text month-text--static">{{ dateName || '全部时间' }}</text>
+      <text class="flow-count">共{{ monthTotal }}笔</text>
+    </view>
+
+    <element-loading :loading="listLoading" text="加载中" color="#09c567">
+      <view v-if="list.data && list.data.length" class="flow-list">
+        <view v-for="(item, index) in list.data" :key="item.id" class="flow-card">
+          <view class="flow-card-main">
+            <view class="flow-icon-wrap" :class="'flow-icon-wrap--type' + item.flowType">
+              <image
+                class="flow-icon"
+                :src="item.flowType == 2 ? '/static/icons/recharge.svg' : '/static/icons/order-volume-2.svg'"
+                mode="aspectFit"
+              />
+            </view>
+            <view class="flow-info">
+              <text class="flow-type-name">{{ item.flowTypeName }}</text>
+              <!-- 订单分红 -->
+              <block v-if="item.flowType == 1">
+                <text class="flow-line">订单号:{{ item.refSn || '-' }}</text>
+                <text class="flow-line flow-line--sub">下单客户:{{ item.subTitle || item.buyerName || '-' }}</text>
+              </block>
+              <!-- 分红存入 -->
+              <block v-else-if="item.flowType == 2">
+                <text class="flow-line">存入时间 {{ formatFlowTime(item.flowTime) }}</text>
+                <text class="flow-line flow-line--sub">存入编号 {{ item.refSn || '-' }}</text>
+              </block>
+              <!-- 其他 -->
+              <block v-else>
+                <text class="flow-line">{{ formatFlowTime(item.flowTime) }}</text>
+                <text v-if="item.refSn" class="flow-line flow-line--sub">{{ item.refSn }}</text>
+              </block>
+            </view>
+            <view class="flow-amount-wrap">
+              <text class="flow-amount" :class="{ 'flow-amount--minus': item.flowType != 1 }">{{ item.amountText }}</text>
+              <text class="flow-status" :class="'flow-status--' + item.statusClass">{{ item.statusText }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+      <AppWrapperEmpty v-else-if="!listLoading" title="暂无数据" :is-empty="true" />
+      <view v-else class="list-loading-placeholder" />
+    </element-loading>
+
+    <view class="flow-tip-box">
+      <text class="flow-tip-title">记录说明</text>
+      <text class="flow-tip-line">1. 订单完成后,分红将在设定时间结算</text>
+      <text class="flow-tip-line">2. 分红存入后,可在花店余额中查看</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import AppWrapperEmpty from '@/components/app-wrapper-empty'
+import ElementLoading from '@/components/element-loading/index.vue'
+import { getDistributionFlowList } from '@/api/distribution'
+import list from '@/mixins/list'
+
+export default {
+  name: 'distributionFlow',
+  components: { AppWrapperEmpty, ElementLoading },
+  mixins: [list],
+  data() {
+    const now = new Date()
+    const monthNum = now.getMonth() + 1
+    const monthValue = `${now.getFullYear()}-${monthNum < 10 ? '0' + monthNum : monthNum}`
+    return {
+      customId: 0,
+      buyerId: 0,
+      scope: '',
+      monthValue,
+      monthEnd: monthValue,
+      monthTotal: 0,
+      showMonthPicker: true,
+      flowType: 'all',
+      dateName: '',
+      queryParams: {
+        searchTime: 'byMonth',
+        startTime: monthValue,
+        endTime: ''
+      }
+    }
+  },
+  computed: {
+    /** 首屏列表加载中 */
+    listLoading() {
+      return this.list.loading && this.list.page === 1
+    },
+    monthLabel() {
+      if (!this.monthValue) {
+        return '选择月份'
+      }
+      const parts = this.monthValue.split('-')
+      if (parts.length < 2) {
+        return this.monthValue
+      }
+      return `${parts[0]}年${Number(parts[1])}月`
+    }
+  },
+  onLoad(option) {
+    this.customId = option.customId ? Number(option.customId) : 0
+    this.buyerId = option.buyerId ? Number(option.buyerId) : 0
+    this.scope = option.scope || ''
+    if (option.searchTime) {
+      this.queryParams.searchTime = option.searchTime
+      this.queryParams.startTime = option.startTime || ''
+      this.queryParams.endTime = option.endTime || ''
+      if (option.searchTime !== 'byMonth' && option.searchTime !== 'thisMonth') {
+        this.showMonthPicker = false
+      }
+    } else if (option.searchTime === 'all') {
+      this.showMonthPicker = false
+      this.queryParams.searchTime = 'all'
+      this.queryParams.startTime = ''
+    }
+    if (option.flowType !== undefined && option.flowType !== '') {
+      this.flowType = String(option.flowType)
+    }
+    if (option.dateName) {
+      try {
+        this.dateName = decodeURIComponent(option.dateName)
+      } catch (e) {
+        this.dateName = option.dateName
+      }
+    }
+    if (option.month) {
+      this.monthValue = option.month
+      this.queryParams.startTime = option.month
+    }
+  },
+  onShow() {
+    this.resetList()
+    this.init()
+  },
+  methods: {
+    /** 拉取分红流水 */
+    async init() {
+      if (!this.customId && this.scope !== 'shop') {
+        this.list.loading = false
+        return
+      }
+      const params = {
+        page: this.list.page,
+        searchTime: this.queryParams.searchTime,
+        startTime: this.queryParams.startTime,
+        endTime: this.queryParams.endTime
+      }
+      if (this.scope === 'shop') {
+        params.scope = 'shop'
+      } else {
+        params.customId = this.customId
+      }
+      if (this.flowType !== 'all') {
+        params.flowType = this.flowType
+      }
+      if (this.buyerId > 0) {
+        params.buyerId = this.buyerId
+      }
+      const res = await getDistributionFlowList(params)
+      if (res.code == 1 && res.data) {
+        this.monthTotal = Number(res.data.monthTotal) || 0
+      }
+      this.completes(res)
+    },
+    onMonthChange(e) {
+      const val = e.detail.value
+      if (!val || val === this.monthValue) {
+        return
+      }
+      this.monthValue = val
+      this.queryParams.startTime = val
+      this.resetList()
+      this.init()
+    },
+    formatFlowTime(time) {
+      if (!time) {
+        return '-'
+      }
+      const str = String(time)
+      if (str.length >= 16) {
+        return str.substr(0, 16)
+      }
+      return str
+    }
+  },
+  async onPullDownRefresh() {
+    this.resetList()
+    await this.init()
+    uni.stopPullDownRefresh()
+  },
+  async onReachBottom() {
+    if (!this.list.finished) {
+      await this.init()
+    }
+    uni.stopPullDownRefresh()
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.flow-page {
+  min-height: 100vh;
+  background: #f5f6f7;
+  padding-bottom: 40upx;
+}
+
+.flow-toolbar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 24upx 28upx;
+  background: #ffffff;
+}
+
+.month-picker {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.month-text {
+  font-size: 30upx;
+  font-weight: 600;
+  color: #333333;
+
+  &--static {
+    padding-left: 4upx;
+  }
+}
+
+.month-arrow {
+  margin-left: 8upx;
+  font-size: 22upx;
+  color: #999999;
+}
+
+.flow-count {
+  font-size: 26upx;
+  color: #999999;
+}
+
+.flow-list {
+  padding: 20upx 24upx 0;
+}
+
+.list-loading-placeholder {
+  min-height: 400upx;
+}
+
+.flow-card {
+  background: #ffffff;
+  border-radius: 16upx;
+  padding: 24upx;
+  margin-bottom: 20upx;
+  box-shadow: 0 2upx 8upx rgba(0, 0, 0, 0.04);
+}
+
+.flow-card-main {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+}
+
+.flow-icon-wrap {
+  width: 72upx;
+  height: 72upx;
+  border-radius: 50%;
+  background: #e8f8ef;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  margin-right: 20upx;
+
+  &--type2 {
+    background: #e8f8ef;
+  }
+}
+
+.flow-icon {
+  width: 40upx;
+  height: 40upx;
+}
+
+.flow-info {
+  flex: 1;
+  min-width: 0;
+  padding-right: 16upx;
+}
+
+.flow-type-name {
+  display: block;
+  font-size: 30upx;
+  font-weight: 600;
+  color: #333333;
+  line-height: 1.4;
+  margin-bottom: 8upx;
+}
+
+.flow-line {
+  display: block;
+  font-size: 24upx;
+  color: #666666;
+  line-height: 1.5;
+
+  &--sub {
+    color: #999999;
+    margin-top: 4upx;
+  }
+}
+
+.flow-amount-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  flex-shrink: 0;
+}
+
+.flow-amount {
+  font-size: 30upx;
+  font-weight: 600;
+  color: #09c567;
+  line-height: 1.3;
+
+  &--minus {
+    color: #09c567;
+  }
+}
+
+.flow-status {
+  margin-top: 10upx;
+  padding: 4upx 16upx;
+  border-radius: 20upx;
+  font-size: 22upx;
+  line-height: 1.4;
+
+  &--success {
+    color: #09c567;
+    background: #e8f8ef;
+  }
+
+  &--pending {
+    color: #ff9500;
+    background: #fff4e5;
+  }
+
+  &--default {
+    color: #999999;
+    background: #f5f5f5;
+  }
+}
+
+.flow-tip-box {
+  margin: 8upx 24upx 0;
+  padding: 24upx;
+  background: #f0faf4;
+  border-radius: 16upx;
+}
+
+.flow-tip-title {
+  display: block;
+  font-size: 28upx;
+  font-weight: 600;
+  color: #09c567;
+  margin-bottom: 12upx;
+}
+
+.flow-tip-line {
+  display: block;
+  font-size: 24upx;
+  color: #666666;
+  line-height: 1.7;
+  margin-top: 6upx;
+}
+</style>

+ 415 - 0
hdApp/src/admin/member/distributionInvite.vue

@@ -0,0 +1,415 @@
+<!--
+  拉新客户列表页
+  用途:hdApp 客户详情-拉新人数点击进入,查询 xhDistributionUser(inviterId)
+-->
+<template>
+  <view class="invite-page app-content">
+    <view class="search-bar">
+      <view class="search-input-wrap">
+        <text class="iconfont iconsearch search-icon"></text>
+        <input
+          v-model="keyword"
+          class="search-input"
+          type="text"
+          confirm-type="search"
+          placeholder="搜索客户昵称或手机号"
+          @confirm="onSearch"
+        />
+      </view>
+    </view>
+
+    <view class="sort-bar">
+      <view
+        v-for="tab in sortTabs"
+        :key="tab.field"
+        class="sort-item"
+        :class="{ 'sort-item--active': sortField === tab.field }"
+        @click="changeSort(tab.field)"
+      >
+        <text class="sort-text">{{ tab.label }}</text>
+        <view v-if="sortField === tab.field" class="sort-arrow-wrap">
+          <text class="sort-arrow" :class="{ 'sort-arrow--up': sortOrder === 'asc' }">▼</text>
+        </view>
+        <view v-else class="sort-arrow-wrap sort-arrow-wrap--idle">
+          <text class="sort-arrow sort-arrow--up">▲</text>
+          <text class="sort-arrow">▼</text>
+        </view>
+      </view>
+    </view>
+
+    <element-loading :loading="listLoading" text="加载中" color="#09c567">
+      <view v-if="list.data && list.data.length" class="invite-list">
+        <view v-for="(item, index) in list.data" :key="item.customId" class="invite-card">
+          <view class="invite-card-main">
+            <view class="invite-user">
+              <app-avatar-module :src="item.smallAvatar" />
+              <view class="invite-user-meta">
+                <text class="invite-name">{{ item.name || '-' }}</text>
+                <text class="invite-mobile">{{ maskMobile(item.mobile) }}</text>
+                <text class="invite-bind-tag">{{ item.bindDaysText }}</text>
+              </view>
+            </view>
+            <view class="invite-stat">
+              <text class="invite-stat-label">贡献分红</text>
+              <text class="invite-stat-amount">¥{{ formatAmount(item.contribCommission) }}</text>
+              <text class="invite-stat-orders">分红订单 {{ item.contribOrderCount }}单</text>
+            </view>
+          </view>
+          <navigator
+            v-if="inviterId && getBuyerId(item)"
+            class="invite-detail-link"
+            hover-class="invite-detail-link--hover"
+            :url="buildContribUrl(item)"
+          >
+            <text>查看贡献明细</text>
+            <text class="iconfont iconxiangyou invite-detail-arrow"></text>
+          </navigator>
+        </view>
+      </view>
+      <AppWrapperEmpty v-else-if="!listLoading" title="暂无拉新客户" :is-empty="true" />
+      <view v-else class="list-loading-placeholder" />
+    </element-loading>
+  </view>
+</template>
+
+<script>
+import AppWrapperEmpty from '@/components/app-wrapper-empty'
+import AppAvatarModule from '@/components/module/app-avatar'
+import ElementLoading from '@/components/element-loading/index.vue'
+import { getDistributionInviteList } from '@/api/distribution'
+import list from '@/mixins/list'
+
+export default {
+  name: 'distributionInvite',
+  components: { AppWrapperEmpty, AppAvatarModule, ElementLoading },
+  mixins: [list],
+  data() {
+    return {
+      customId: 0,
+      keyword: '',
+      sortField: 'inviteTime',
+      sortOrder: 'desc',
+      sortTabs: [
+        { field: 'inviteTime', label: '绑定时间' },
+        { field: 'commission', label: '贡献分红' },
+        { field: 'orderCount', label: '订单数' }
+      ]
+    }
+  },
+  computed: {
+    /** 首屏列表加载中 */
+    listLoading() {
+      return this.list.loading && this.list.page === 1
+    },
+    /** 当前分销员(邀请人)客户 ID */
+    inviterId() {
+      const id = this.customId || (this.option && this.option.customId)
+      const num = Number(id)
+      return num > 0 ? num : 0
+    }
+  },
+  onLoad(option) {
+    const query = option || {}
+    this.customId = query.customId ? Number(query.customId) : 0
+  },
+  onShow() {
+    if (!this.customId && this.option && this.option.customId) {
+      this.customId = Number(this.option.customId)
+    }
+    this.resetList()
+    this.init()
+  },
+  methods: {
+    /** 拉取拉新客户列表 */
+    async init() {
+      if (!this.customId) {
+        this.list.loading = false
+        return
+      }
+      const res = await getDistributionInviteList({
+        page: this.list.page,
+        customId: this.customId,
+        keyword: this.keyword,
+        sortField: this.sortField,
+        sortOrder: this.sortOrder
+      })
+      this.completes(res)
+    },
+    onSearch() {
+      this.resetList()
+      this.init()
+    },
+    /** 切换排序字段/方向 */
+    changeSort(field) {
+      if (this.sortField === field) {
+        this.sortOrder = this.sortOrder === 'desc' ? 'asc' : 'desc'
+      } else {
+        this.sortField = field
+        this.sortOrder = field === 'inviteTime' ? 'desc' : 'desc'
+      }
+      this.resetList()
+      this.init()
+    },
+    maskMobile(mobile) {
+      if (!mobile) {
+        return '-'
+      }
+      const str = String(mobile)
+      if (str.length < 7) {
+        return str
+      }
+      return str.substr(0, 3) + '****' + str.substr(-4)
+    },
+    formatAmount(val) {
+      const num = parseFloat(val) || 0
+      return num.toFixed(2)
+    },
+    /** 下线客户 ID(兼容不同字段名) */
+    getBuyerId(item) {
+      if (!item) {
+        return 0
+      }
+      const id = item.customId || item.id || item.buyerId
+      const num = Number(id)
+      return num > 0 ? num : 0
+    },
+    /** 贡献明细分页 URL */
+    buildContribUrl(item) {
+      const buyerId = this.getBuyerId(item)
+      if (!this.inviterId || !buyerId) {
+        return ''
+      }
+      return '/admin/member/distributionContrib?distId=' + this.inviterId + '&buyerId=' + buyerId
+    }
+  },
+  async onPullDownRefresh() {
+    this.resetList()
+    await this.init()
+    uni.stopPullDownRefresh()
+  },
+  async onReachBottom() {
+    if (!this.list.finished) {
+      await this.init()
+    }
+    uni.stopPullDownRefresh()
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.invite-page {
+  min-height: 100vh;
+  background: #f5f6f7;
+  padding-bottom: 40upx;
+}
+
+.search-bar {
+  padding: 20upx 24upx;
+  background: #ffffff;
+}
+
+.search-input-wrap {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  height: 72upx;
+  padding: 0 24upx;
+  background: #f5f7f6;
+  border-radius: 36upx;
+}
+
+.search-icon {
+  font-size: 30upx;
+  color: #999999;
+  margin-right: 12upx;
+}
+
+.search-input {
+  flex: 1;
+  font-size: 28upx;
+  color: #333333;
+}
+
+.sort-bar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #ffffff;
+  padding: 0 12upx 16upx;
+  border-bottom: 1upx solid #f0f0f0;
+}
+
+.sort-item {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  padding: 16upx 0;
+  position: relative;
+
+  &--active {
+    .sort-text {
+      color: #09c567;
+      font-weight: 600;
+    }
+
+    &::after {
+      content: '';
+      position: absolute;
+      left: 50%;
+      bottom: 0;
+      transform: translateX(-50%);
+      width: 48upx;
+      height: 4upx;
+      background: #09c567;
+      border-radius: 2upx;
+    }
+  }
+}
+
+.sort-text {
+  font-size: 28upx;
+  color: #666666;
+}
+
+.sort-arrow-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-left: 6upx;
+  line-height: 1;
+
+  &--idle {
+    opacity: 0.35;
+  }
+}
+
+.sort-arrow {
+  font-size: 16upx;
+  color: #09c567;
+  line-height: 1.1;
+  transform: scale(0.9);
+
+  &--up {
+    transform: rotate(180deg) scale(0.9);
+  }
+}
+
+.sort-item:not(.sort-item--active) .sort-arrow {
+  color: #cccccc;
+}
+
+.invite-list {
+  padding: 20upx 24upx 0;
+}
+
+.list-loading-placeholder {
+  min-height: 400upx;
+}
+
+.invite-card {
+  background: #ffffff;
+  border-radius: 16upx;
+  padding: 24upx;
+  margin-bottom: 20upx;
+  box-shadow: 0 2upx 8upx rgba(0, 0, 0, 0.04);
+}
+
+.invite-card-main {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  justify-content: space-between;
+}
+
+.invite-user {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  flex: 1;
+  min-width: 0;
+  padding-right: 16upx;
+}
+
+.invite-user-meta {
+  flex: 1;
+  margin-left: 16upx;
+  min-width: 0;
+}
+
+.invite-name {
+  display: block;
+  font-size: 30upx;
+  font-weight: 600;
+  color: #333333;
+  line-height: 1.4;
+}
+
+.invite-mobile {
+  display: block;
+  margin-top: 6upx;
+  font-size: 24upx;
+  color: #999999;
+}
+
+.invite-bind-tag {
+  display: inline-block;
+  margin-top: 10upx;
+  padding: 4upx 14upx;
+  font-size: 22upx;
+  color: #09c567;
+  background: #e8f8ef;
+  border-radius: 20upx;
+}
+
+.invite-stat {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  flex-shrink: 0;
+}
+
+.invite-stat-label {
+  font-size: 24upx;
+  color: #999999;
+}
+
+.invite-stat-amount {
+  margin-top: 8upx;
+  font-size: 34upx;
+  font-weight: 600;
+  color: #09c567;
+  line-height: 1.2;
+}
+
+.invite-stat-orders {
+  margin-top: 8upx;
+  font-size: 22upx;
+  color: #999999;
+}
+
+.invite-detail-link {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: flex-end;
+  margin-top: 20upx;
+  padding: 16upx 0 4upx;
+  border-top: 1upx solid #f5f5f5;
+  font-size: 24upx;
+  color: #666666;
+  min-height: 72upx;
+  text-decoration: none;
+}
+
+.invite-detail-link--hover {
+  opacity: 0.7;
+}
+
+.invite-detail-arrow {
+  margin-left: 6upx;
+  font-size: 22upx;
+  color: #cccccc;
+}
+</style>

+ 50 - 0
hdApp/src/api/distribution/index.js

@@ -0,0 +1,50 @@
+/**
+ * 分销规则 API(xhDistributionRule / Tier / Scope)
+ * 用途:hdApp 门店设置-分销规则页
+ */
+import https from '@/plugins/luch-request_0.0.7/request'
+
+/** 获取当前门店分销规则 */
+export const getDistributionRule = () => {
+  return https.get('/distribution/get-rule')
+}
+
+/** 保存分销规则 */
+export const saveDistributionRule = data => {
+  return https.post('/distribution/save-rule', data)
+}
+
+/** 客户分销统计(客户详情页) */
+export const getDistributionUserStat = data => {
+  return https.get('/distribution/get-user-stat', data)
+}
+
+/** 客户分红变动明细 */
+export const getDistributionFlowList = data => {
+  return https.get('/distribution/get-flow-list', data)
+}
+
+/** 拉新客户列表 */
+export const getDistributionInviteList = data => {
+  return https.get('/distribution/get-invite-list', data)
+}
+
+/** 下线贡献分红明细(xhDistributionOrder) */
+export const getDistributionContribOrderList = data => {
+  return https.get('/distribution/get-contrib-order-list', data)
+}
+
+/** 门店分销报表汇总 */
+export const getDistributionReportStat = data => {
+  return https.get('/distribution/get-report-stat', data)
+}
+
+/** 门店获佣人数明细 */
+export const getDistributionShopDistList = data => {
+  return https.get('/distribution/get-shop-dist-list', data)
+}
+
+/** 门店拉新明细 */
+export const getDistributionShopInviteList = data => {
+  return https.get('/distribution/get-shop-invite-list', data)
+}

+ 15 - 22
hdApp/src/components/element-loading/index.vue

@@ -3,19 +3,23 @@
     <slot />
     <view v-if="loading" class="el-loading-mask" @touchmove.stop.prevent>
       <view class="el-loading-spinner">
-        <view class="el-loading-icon" :style="iconStyle"></view>
-        <text v-if="text" class="el-loading-text" :style="textStyle">{{ text }}</text>
+        <!-- 小程序 style 须为字符串,对象绑定不会生效 -->
+        <view class="el-loading-icon" :style="iconStyleStr"></view>
+        <text v-if="text" class="el-loading-text" :style="textStyleStr">{{ text }}</text>
       </view>
     </view>
   </view>
 </template>
 
 <script>
+/**
+ * 局部 loading 遮罩
+ * 用途:列表首屏加载等场景;color 控制转圈与文案颜色
+ */
 export default {
   name: 'ElementLoading',
   options: {
-    styleIsolation: 'shared',
-    virtualHost: true
+    styleIsolation: 'shared'
   },
   props: {
     loading: {
@@ -32,16 +36,11 @@ export default {
     }
   },
   computed: {
-    iconStyle() {
-      // 小程序内联样式仅支持单边色,其余三边在 wxss 中写死
-      return {
-        borderLeftColor: this.color
-      }
+    iconStyleStr() {
+      return 'color:' + this.color + ';'
     },
-    textStyle() {
-      return {
-        color: this.color
-      }
+    textStyleStr() {
+      return 'color:' + this.color + ';'
     }
   }
 }
@@ -70,7 +69,6 @@ export default {
   text-align: center;
 }
 
-/* 对齐 app-wrapper-empty 的转圈写法,小程序端已验证可用 */
 .el-loading-icon {
   display: block;
   width: 44rpx;
@@ -78,13 +76,9 @@ export default {
   margin: 0 auto;
   box-sizing: border-box;
   border-radius: 50%;
-  border-width: 4rpx;
-  border-style: solid;
-  border-color: #e4e7ed #e4e7ed #e4e7ed #409eff;
-  animation-name: el-loading-rotate;
-  animation-duration: 0.8s;
-  animation-timing-function: linear;
-  animation-iteration-count: infinite;
+  border: 4rpx solid transparent;
+  border-top-color: initial;
+  animation: el-loading-rotate 0.8s linear infinite;
 }
 
 .el-loading-text {
@@ -92,7 +86,6 @@ export default {
   margin-top: 16rpx;
   font-size: 28rpx;
   line-height: 1.4;
-  color: #409eff;
 }
 
 @keyframes el-loading-rotate {

+ 5 - 1
hdApp/src/mixins/list.js

@@ -30,8 +30,12 @@ export default {
       this.list.loading = false
       if (res.code !== 1) {
         this.list.finished = true
+        return
+      }
+      let listArr = res.data.list || res.data.data || res.data[listKey] || []
+      if (!Array.isArray(listArr)) {
+        listArr = []
       }
-      let listArr = res.data.list || res.data.data || res.data[listKey]
       this.list.data = this.list.page === 1 ? listArr : this.list.data.concat(listArr)
       this.list.total = res.data.totalPage * this.list.pageSize
       this.list.totalPage = res.data.totalPage || res.totalPage

+ 10 - 1
hdApp/src/pages.json

@@ -15,6 +15,12 @@
 		{ "path": "admin/home/member", "style": { "navigationBarTitleText": "客户", "enablePullDownRefresh": true, "navigationStyle": "custom" } },
 		{ "path": "admin/home/apply", "style": { "navigationBarTitleText": "应用", "enablePullDownRefresh": true } },
 		{ "path": "admin/home/psMethod", "style": { "navigationBarTitleText": "配送方式" } },
+		{ "path": "admin/home/distributionRule", "style": { "navigationBarTitleText": "分销规则" } },
+		{ "path": "admin/home/distributionReport", "style": { "navigationBarTitleText": "分销统计", "enablePullDownRefresh": true } },
+		{ "path": "admin/home/distributionReportDist", "style": { "navigationBarTitleText": "获佣客户列表", "enablePullDownRefresh": true } },
+		{ "path": "admin/home/distributionReportInvite", "style": { "navigationBarTitleText": "拉新客户列表", "enablePullDownRefresh": true } },
+		{ "path": "admin/home/distributionReportOrder", "style": { "navigationBarTitleText": "分佣订单明细", "enablePullDownRefresh": true } },
+		{ "path": "admin/home/distributionReportFlow", "style": { "navigationBarTitleText": "变动明细", "enablePullDownRefresh": true } },
 		{ "path": "admin/home/close","style": {"navigationBarTitleText": "申请注销账号"}},
 		{"path": "admin/home/register","style": {"navigationBarTitleText": "服务协议"}},
 		{"path": "admin/home/privacy","style": {"navigationBarTitleText": "隐私政策"}},
@@ -513,7 +519,10 @@
 				{"path": "level","style": {"navigationBarTitleText": "会员等级"}},
 				{"path": "buyAmountChange","style": {"navigationBarTitleText": "消费变动记录","enablePullDownRefresh": true}},
 				{"path": "growthChange","style": {"navigationBarTitleText": "成长值变动记录","enablePullDownRefresh": true}},
-				{"path": "integralChange","style": {"navigationBarTitleText": "积分变动记录","enablePullDownRefresh": true}}
+				{"path": "integralChange","style": {"navigationBarTitleText": "积分变动记录","enablePullDownRefresh": true}},
+				{"path": "distributionFlow","style": {"navigationBarTitleText": "变动明细","enablePullDownRefresh": true}},
+				{"path": "distributionInvite","style": {"navigationBarTitleText": "拉新客户","enablePullDownRefresh": true}},
+				{"path": "distributionContrib","style": {"navigationBarTitleText": "分红明细","enablePullDownRefresh": true}}
 			]
 		},
 		{

+ 1 - 0
hdApp/src/uni.scss

@@ -25,6 +25,7 @@ $fontColor2: #666666;
 $fontColor3: #999999;
 $fontColor4: #bbbbbb;
 $fontColor5: #cccccc;
+$fontPinkColor: #fc466c;
 // 边框
 $borderColor: #eee;
 $borderColor1: #46963E;