Browse Source

花掌柜-邀请开店功能开发

ouyang 1 week ago
parent
commit
3f6de6f04b

+ 73 - 0
hdApp/src/admin/cg/me.vue

@@ -31,6 +31,18 @@
 
       <view class="content-wrap">
 
+      <!-- 已登录:邀请开店 / 年度会员开通入口 -->
+      <view class="open-shop-card" @click="goApplyInvite">
+        <view class="open-shop-main">
+          <text class="open-shop-label">开通状态</text>
+          <text class="open-shop-desc">会员未开通</text>
+        </view>
+        <view class="open-shop-link">
+          <text class="open-shop-link-text">去开通</text>
+          <text class="open-shop-arrow">›</text>
+        </view>
+      </view>
+
       <!-- 采购账号常用功能:由工作台头部迁入 -->
       <view class="function-grid">
         <view class="function-item" @click="pageTo({ url: '/admin/shop/myShop' })">
@@ -312,6 +324,10 @@ export default {
     goRegister () {
       this.$util.pageTo({ url: "/pagesClient/official/apply" });
     },
+    /** 邀请开店 / 年度会员开通 */
+    goApplyInvite () {
+      this.$util.pageTo({ url: "/pagesClient/official/applyInvite", type: 2 });
+    },
     resetGuestState () {
       this.myInfo = {};
       this.lookMoney = 0;
@@ -777,6 +793,63 @@ export default {
   padding: 4upx 24upx 28upx;
 }
 
+.open-shop-card {
+  position: relative;
+  overflow: hidden;
+  min-height: 92upx;
+  margin-top: 0;
+  margin-bottom: 20upx;
+  padding: 20upx 24upx;
+  border-radius: 20upx;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background: #fff;
+  box-shadow: 0 8upx 24upx rgba(9, 197, 103, 0.08);
+}
+
+.open-shop-main {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  min-width: 0;
+}
+
+.open-shop-label {
+  font-size: 30upx;
+  line-height: 38upx;
+  font-weight: 700;
+  color: #111416;
+}
+
+.open-shop-desc {
+  margin-top: 8upx;
+  font-size: 24upx;
+  line-height: 32upx;
+  color: #8a9199;
+}
+
+.open-shop-link {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+  margin-left: 16upx;
+}
+
+.open-shop-link-text {
+  font-size: 28upx;
+  font-weight: 700;
+  line-height: 40upx;
+  color: #09c567;
+}
+
+.open-shop-arrow {
+  margin-left: 4upx;
+  color: #09c567;
+  font-size: 36upx;
+  line-height: 1;
+}
+
 .function-grid {
   margin-top: 0;
   min-height: 166upx;

+ 20 - 0
hdApp/src/admin/home/me.vue

@@ -64,6 +64,15 @@
             <view class="pill-btn" @click="inAmountFn">存入</view>
           </view>
         </view>
+
+        <view class="finance-card">
+          <view class="finance-label">可用余额(元)</view>
+          <view class="finance-amount">¥{{ moneyFormat(walletBalanceDisplay) }}</view>
+          <view class="finance-actions">
+            <view class="pill-btn" @click="pageTo({ url: '/admin/home/walletChangeList' })">变动明细</view>
+            <view class="pill-btn" @click="pageTo({ url: '/admin/home/walletRecharge' })">充值</view>
+          </view>
+        </view>
       </view>
 
       <view class="menu-card">
@@ -264,6 +273,14 @@ export default {
       } catch (e) {}
       // #endif
       return true;
+    },
+    /** 中央钱包可用余额,接口未返回时展示 0 */
+    walletBalanceDisplay () {
+      const val = this.myInfo && this.myInfo.walletBalance;
+      if (val === undefined || val === null || val === "") {
+        return 0;
+      }
+      return val;
     }
   },
   data () {
@@ -281,6 +298,7 @@ export default {
       inAmountShow: false,
       menuList: [
         { key: "address", name: "修改门店", icon: "modify_shop" },
+        { key: "shopInvite", name: "邀请花店", icon: "member-level" },
         { key: "book", name: "使用教程", icon: "my-book" },
         { key: "lakala", name: "收款账户管理", icon: "pay-code" },
         { key: "password", name: "密码修改", icon: "my-password" },
@@ -377,6 +395,8 @@ export default {
           this.getLoginInfo.shopId ||
           0;
         this.$util.pageTo({ url: "/admin/shop/add?id=" + shopId })
+      } else if (item.key == "shopInvite") {
+        this.$util.pageTo({ url: "/admin/home/shopInviteHub" })
       } else if (item.key == "book") {
         this.$util.pageTo({ url: "/admin/book/index" })
       } else if (item.key == "lakala") {

+ 267 - 0
hdApp/src/admin/home/shopInviteCommissionList.vue

@@ -0,0 +1,267 @@
+<!--
+  佣金变动明细
+  用途:邀请花店 hub 进入;对接 main-invite/commission-list,默认近30天
+-->
+<template>
+  <view class="commission-page app-content">
+    <view class="filter-bar">
+      <view class="date-chip">
+        <text class="date-chip-icon">📅</text>
+        <DateSelect
+          ref="dateSelectRef"
+          class="date-select"
+          top="88upx"
+          default-show-name="近30天"
+          @selectDateFn="selectDateFn"
+        />
+      </view>
+    </view>
+
+    <element-loading :loading="listLoading" text="加载中" color="#09c567">
+      <view v-if="list.data && list.data.length">
+        <view
+          v-for="(item, index) in list.data"
+          :key="item.id || index"
+          class="commission-card"
+        >
+          <view class="card-main">
+            <image
+              v-if="item.shopImg"
+              class="shop-thumb"
+              :src="item.shopImg"
+              mode="aspectFill"
+            />
+            <view v-else class="shop-thumb shop-thumb--placeholder">
+              <text>花</text>
+            </view>
+            <view class="card-body">
+              <view class="title-row">
+                <text class="shop-name">{{ item.shopName || '-' }}</text>
+                <text
+                  class="commission-money"
+                  :class="item.commissionSign < 0 ? 'commission-money--minus' : 'commission-money--plus'"
+                >
+                  {{ commissionPrefix(item) }} ¥ {{ moneyText(item.commissionAmount) }}
+                </text>
+              </view>
+              <text class="meta-line">手机号 {{ item.mobileMask || item.mobile || '-' }}</text>
+              <text class="meta-line">
+                {{ item.orderSnLabel || '会员订单号' }} {{ item.orderSn || '-' }}
+              </text>
+              <text class="meta-line">
+                {{ item.baseAmountLabel || '会员实付金额' }} ¥ {{ moneyText(item.baseAmount) }}
+              </text>
+              <text class="meta-line">创建时间 {{ formatTime(item.createTime) }}</text>
+            </view>
+          </view>
+        </view>
+        <view v-if="list.finished" class="list-end">没有更多了</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 DateSelect from '@/components/module/dateSelect'
+import ElementLoading from '@/components/element-loading/index.vue'
+import { getMainInviteCommissionList } from '@/api/main-invite'
+import list from '@/mixins/list'
+
+export default {
+  name: 'shopInviteCommissionList',
+  components: { AppWrapperEmpty, DateSelect, ElementLoading },
+  mixins: [list],
+  data () {
+    return {
+      searchTime: 'last30Days',
+      startTime: '',
+      endTime: ''
+    }
+  },
+  computed: {
+    /** 首屏/切换日期后列表加载中 */
+    listLoading () {
+      return this.list.loading && this.list.page === 1
+    }
+  },
+  onLoad () {
+    this.init()
+  },
+  methods: {
+    moneyText (val) {
+      const n = parseFloat(val)
+      if (isNaN(n)) {
+        return '0.00'
+      }
+      return n.toFixed(2)
+    },
+    formatTime (t) {
+      if (!t || t === '-') {
+        return '-'
+      }
+      const s = String(t)
+      if (s.length >= 19) {
+        return s.substr(0, 19)
+      }
+      return s
+    },
+    commissionPrefix (item) {
+      if (item && item.commissionSign < 0) {
+        return '-'
+      }
+      return '+'
+    },
+    selectDateFn (val) {
+      this.searchTime = val.searchTime || 'last30Days'
+      this.startTime = val.startTime || ''
+      this.endTime = val.endTime || ''
+      this.resetList()
+      this.init()
+    },
+    async init () {
+      const params = {
+        page: this.list.page,
+        searchTime: this.searchTime
+      }
+      if (this.startTime) {
+        params.startTime = this.startTime
+      }
+      if (this.endTime) {
+        params.endTime = this.endTime
+      }
+      const res = await getMainInviteCommissionList(params)
+      this.completes(res)
+    }
+  },
+  async onPullDownRefresh () {
+    this.resetList()
+    await this.init()
+    uni.stopPullDownRefresh()
+  },
+  async onReachBottom () {
+    if (!this.list.finished) {
+      await this.init()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.commission-page {
+  min-height: 100vh;
+  padding-bottom: 40upx;
+  background: #f5f6f8;
+}
+
+.filter-bar {
+  padding: 20upx 24upx;
+  background: #fff;
+}
+
+.date-chip {
+  display: inline-flex;
+  align-items: center;
+  max-width: 100%;
+}
+
+.date-chip-icon {
+  margin-right: 8upx;
+  font-size: 32upx;
+  color: #666;
+}
+
+.date-select {
+  flex: 1;
+}
+
+.commission-card {
+  margin: 20upx 24upx 0;
+  padding: 28upx 24upx;
+  border-radius: 16upx;
+  background: #fff;
+  box-shadow: 0 2upx 12upx rgba(0, 0, 0, 0.04);
+}
+
+.card-main {
+  display: flex;
+  align-items: flex-start;
+}
+
+.shop-thumb {
+  width: 96upx;
+  height: 96upx;
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+
+.shop-thumb--placeholder {
+  background: #f0f0f0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #999;
+  font-size: 28upx;
+}
+
+.card-body {
+  flex: 1;
+  min-width: 0;
+  margin-left: 20upx;
+}
+
+.title-row {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+}
+
+.shop-name {
+  flex: 1;
+  min-width: 0;
+  padding-right: 16upx;
+  font-size: 32upx;
+  font-weight: 700;
+  color: #222;
+  line-height: 44upx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.commission-money {
+  flex-shrink: 0;
+  font-size: 32upx;
+  font-weight: 700;
+  line-height: 44upx;
+}
+
+.commission-money--plus {
+  color: #e54d42;
+}
+
+.commission-money--minus {
+  color: #e54d42;
+}
+
+.meta-line {
+  display: block;
+  margin-top: 10upx;
+  font-size: 26upx;
+  color: #888;
+  line-height: 38upx;
+}
+
+.list-end {
+  padding: 32upx;
+  text-align: center;
+  font-size: 26upx;
+  color: #999;
+}
+
+.list-loading-placeholder {
+  min-height: 400upx;
+}
+</style>

+ 524 - 0
hdApp/src/admin/home/shopInviteHub.vue

@@ -0,0 +1,524 @@
+<!--
+  邀请花店 / 我的分佣 hub
+  用途:我的-邀请花店;最近邀请手机号来自接口 mobile(表 inviteeMobile 快照)
+-->
+<template>
+  <view class="hub-page app-content">
+    <view class="summary-card">
+      <view class="summary-top">
+        <view class="summary-left">
+          <view class="summary-label-row">
+            <text class="summary-label">可用佣金</text>
+            <text class="summary-info" @click.stop="onCommissionTip">ⓘ</text>
+          </view>
+          <view class="summary-amount">¥ {{ moneyText(invite.ableCommission) }}</view>
+        </view>
+        <view class="deposit-btn" @click="onDeposit">存入余额</view>
+      </view>
+      <view class="summary-bottom">
+        <text class="stat-label">累计佣金</text>
+        <text class="stat-value">¥ {{ moneyText(invite.totalCommission) }}</text>
+      </view>
+    </view>
+
+    <view class="invite-code-card">
+      <view class="code-head">
+        <view class="code-icon-wrap">
+          <text class="code-icon-text">邀</text>
+        </view>
+        <view class="code-main">
+          <text class="code-title">我的邀请码</text>
+          <text class="code-value">{{ inviteCodeDisplay }}</text>
+        </view>
+      </view>
+      <view class="code-actions">
+        <view class="code-btn code-btn--outline" @click="copyInviteCode">复制邀请码</view>
+        <!-- #ifdef MP-WEIXIN -->
+        <button
+          open-type="share"
+          class="code-btn code-btn--solid code-btn--share"
+          @click="onShareInviteClick"
+        >
+          邀请花店
+        </button>
+        <!-- #endif -->
+        <!-- #ifndef MP-WEIXIN -->
+        <view class="code-btn code-btn--solid" @click="onShareInvite">邀请花店</view>
+        <!-- #endif -->
+      </view>
+    </view>
+
+    <view class="menu-card">
+      <view class="menu-row" @click="goInviteShopList">
+        <text class="menu-name">邀约花店</text>
+        <view class="menu-right">
+          <text class="menu-extra">{{ invite.inviteCount || 0 }}家</text>
+          <text class="menu-arrow">›</text>
+        </view>
+      </view>
+      <view class="menu-row" @click="goCommissionList">
+        <text class="menu-name">佣金变动明细</text>
+        <text class="menu-arrow">›</text>
+      </view>
+      <view class="menu-row menu-row--rule">
+        <text class="menu-name menu-name--fixed">分佣规则</text>
+        <text class="rule-desc">{{ ruleDesc }}</text>
+      </view>
+    </view>
+
+    <view class="recent-head">
+      <text class="recent-title">最近邀请花店</text>
+    </view>
+
+    <view v-if="recentList.length" class="recent-list">
+      <view
+        v-for="(item, index) in recentList"
+        :key="item.id || index"
+        class="recent-item"
+      >
+        <view class="recent-icon-wrap">
+          <text class="recent-icon-text">店</text>
+        </view>
+        <view class="recent-info">
+          <text class="recent-shop">{{ item.shopName || '-' }}</text>
+          <text class="recent-line">手机号码:{{ item.mobile || item.mobileMask || '-' }}</text>
+          <text class="recent-line">开通时间:{{ item.openTime || '-' }}</text>
+        </view>
+        <view class="recent-right">
+          <text class="recent-money">+{{ moneyText(item.commissionAmount) }}</text>
+        </view>
+      </view>
+    </view>
+    <view v-else class="recent-empty">
+      <text>暂无邀请记录</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getMainInviteIndex } from '@/api/main-invite'
+
+export default {
+  name: 'shopInviteHub',
+  data () {
+    return {
+      invite: {
+        inviteCode: '',
+        inviteCodeMask: '',
+        totalCommission: 0,
+        ableCommission: 0,
+        commissionAmount: 0,
+        hdDiscountAmount: 0,
+        inviteCount: 0
+      },
+      recentList: []
+    }
+  },
+  computed: {
+    /** 设计稿展示邀请码本体(无则展示脱敏) */
+    inviteCodeDisplay () {
+      if (this.invite.inviteCode) {
+        return this.invite.inviteCode
+      }
+      return this.invite.inviteCodeMask || '--'
+    },
+    /** 分佣规则文案,金额取自 xhMainInvite 配置 */
+    ruleDesc () {
+      const comm = this.moneyText(this.invite.commissionAmount)
+      const disc = this.moneyText(this.invite.hdDiscountAmount)
+      return `花店开通账号并使用邀请码支付,花店享受${disc}元优惠,邀请人获得${comm}元佣金。`
+    }
+  },
+  onShow () {
+    this.loadData()
+  },
+  onPullDownRefresh () {
+    this.loadData().finally(() => {
+      uni.stopPullDownRefresh()
+    })
+  },
+  /** 微信小程序:分享当前页并携带邀请码 */
+  onShareAppMessage () {
+    return this.buildShareMessage()
+  },
+  methods: {
+    /** 组装分享卡片:路径为当前页 + inviteCode,封面图为 CDN 固定图 */
+    buildShareMessage () {
+      const inviteCode = this.invite.inviteCode || ''
+      let path = '/pagesClient/official/applyInvite'
+      if (inviteCode) {
+        path += '?inviteCode=' + encodeURIComponent(inviteCode)
+      }
+      const disc = this.moneyText(this.invite.hdDiscountAmount)
+      const imgHost = (this.$constant && this.$constant.imgUrl) || ''
+      const imageUrl = imgHost ? imgHost + '/hzg/invite/shop_invite_image.png' : ''
+      const msg = {
+        title: disc !== '0.00' ? `邀请您开通花店,享${disc}元优惠` : '邀请您开通花店',
+        path
+      }
+      if (imageUrl) {
+        msg.imageUrl = imageUrl
+      }
+      return msg
+    },
+    moneyText (val) {
+      const n = parseFloat(val)
+      if (isNaN(n)) {
+        return '0.00'
+      }
+      return n.toFixed(2)
+    },
+    async loadData () {
+      const res = await getMainInviteIndex()
+      if (res.code !== 1 || !res.data) {
+        return
+      }
+      if (res.data.invite) {
+        this.invite = { ...this.invite, ...res.data.invite }
+      }
+      this.recentList = res.data.recentList || []
+    },
+    onCommissionTip () {
+      uni.showToast({ title: '可提现佣金为当前可申请存入余额的金额', icon: 'none' })
+    },
+    onDeposit () {
+      uni.showToast({ title: '功能开发中', icon: 'none' })
+    },
+    copyInviteCode () {
+      const code = this.invite.inviteCode
+      if (!code) {
+        uni.showToast({ title: '暂无邀请码', icon: 'none' })
+        return
+      }
+      uni.setClipboardData({
+        data: String(code),
+        success: () => {
+          uni.showToast({ title: '已复制', icon: 'none' })
+        }
+      })
+    },
+    onShareInviteClick () {
+      if (!this.invite.inviteCode) {
+        uni.showToast({ title: '暂无邀请码', icon: 'none' })
+      }
+    },
+    onShareInvite () {
+      uni.showToast({ title: '请在微信小程序中使用分享', icon: 'none' })
+    },
+    goInviteShopList () {
+      this.$util.pageTo({ url: '/admin/home/shopInviteList' })
+    },
+    goCommissionList () {
+      this.$util.pageTo({ url: '/admin/home/shopInviteCommissionList' })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.hub-page {
+  min-height: 100vh;
+  padding: 24upx;
+  box-sizing: border-box;
+  background: #eaf8ef;
+}
+
+.summary-card {
+  padding: 32upx 28upx 28upx;
+  border-radius: 20upx;
+  background: linear-gradient(145deg, #09c567 0%, #3dd088 55%, #7ce0a8 100%);
+  color: #fff;
+  box-shadow: 0 8upx 24upx rgba(9, 197, 103, 0.22);
+}
+
+.summary-top {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+}
+
+.summary-label-row {
+  display: flex;
+  align-items: center;
+}
+
+.summary-label {
+  font-size: 26upx;
+  opacity: 0.95;
+}
+
+.summary-info {
+  margin-left: 10upx;
+  font-size: 24upx;
+  opacity: 0.85;
+}
+
+.summary-amount {
+  margin-top: 16upx;
+  font-size: 56upx;
+  font-weight: 800;
+  line-height: 1.1;
+}
+
+.deposit-btn {
+  margin-top: 4upx;
+  padding: 14upx 28upx;
+  border-radius: 999upx;
+  background: #fff;
+  color: #09c567;
+  font-size: 26upx;
+  font-weight: 600;
+}
+
+.summary-bottom {
+  display: flex;
+  align-items: baseline;
+  margin-top: 28upx;
+  padding-top: 24upx;
+  border-top: 1upx solid rgba(255, 255, 255, 0.28);
+}
+
+.stat-label {
+  font-size: 26upx;
+  opacity: 0.9;
+}
+
+.stat-value {
+  margin-left: 16upx;
+  font-size: 30upx;
+  font-weight: 700;
+}
+
+.invite-code-card {
+  margin-top: 24upx;
+  padding: 28upx;
+  border-radius: 20upx;
+  background: #fff;
+  box-shadow: 0 4upx 16upx rgba(9, 197, 103, 0.06);
+}
+
+.code-head {
+  display: flex;
+  align-items: center;
+}
+
+.code-icon-wrap {
+  width: 88upx;
+  height: 88upx;
+  border-radius: 50%;
+  background: #eaf8ef;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.code-icon-text {
+  color: #09c567;
+  font-size: 36upx;
+  font-weight: 700;
+}
+
+.code-main {
+  flex: 1;
+  min-width: 0;
+  margin-left: 24upx;
+}
+
+.code-title {
+  font-size: 28upx;
+  color: #666;
+}
+
+.code-value {
+  display: block;
+  margin-top: 8upx;
+  font-size: 48upx;
+  font-weight: 800;
+  color: #111;
+  letter-spacing: 2upx;
+}
+
+.code-actions {
+  display: flex;
+  margin-top: 28upx;
+}
+
+.code-btn {
+  flex: 1;
+  height: 76upx;
+  border-radius: 12upx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28upx;
+}
+
+.code-btn + .code-btn {
+  margin-left: 20upx;
+}
+
+.code-btn--outline {
+  border: 1upx solid #09c567;
+  color: #09c567;
+  background: #fff;
+}
+
+.code-btn--solid {
+  background: #09c567;
+  color: #fff;
+}
+
+.code-btn--share {
+  flex: 1;
+  padding: 0;
+  line-height: 76upx;
+  font-weight: normal;
+  border: none;
+}
+
+.code-btn--share::after {
+  border: none;
+}
+
+.menu-card {
+  margin-top: 24upx;
+  border-radius: 20upx;
+  background: #fff;
+  overflow: hidden;
+  box-shadow: 0 4upx 16upx rgba(9, 197, 103, 0.06);
+}
+
+.menu-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 32upx 28upx;
+  border-bottom: 1upx solid #f0f0f0;
+}
+
+.menu-row--rule {
+  align-items: flex-start;
+}
+
+.menu-name {
+  font-size: 30upx;
+  color: #333;
+  flex-shrink: 0;
+}
+
+.menu-name--fixed {
+  width: 160upx;
+}
+
+.rule-desc {
+  flex: 1;
+  margin-left: 16upx;
+  font-size: 24upx;
+  line-height: 36upx;
+  color: #666;
+  text-align: right;
+}
+
+.menu-right {
+  display: flex;
+  align-items: center;
+}
+
+.menu-extra {
+  margin-right: 8upx;
+  font-size: 28upx;
+  color: #999;
+}
+
+.menu-arrow {
+  color: #ccc;
+  font-size: 36upx;
+}
+
+.recent-head {
+  margin-top: 32upx;
+  margin-bottom: 16upx;
+  padding: 0 8upx;
+}
+
+.recent-title {
+  font-size: 32upx;
+  font-weight: 700;
+  color: #333;
+}
+
+.recent-list {
+  border-radius: 20upx;
+  background: #fff;
+  overflow: hidden;
+  box-shadow: 0 4upx 16upx rgba(9, 197, 103, 0.06);
+}
+
+.recent-item {
+  display: flex;
+  align-items: center;
+  padding: 28upx 24upx;
+  border-bottom: 1upx solid #f5f5f5;
+}
+
+.recent-item:last-child {
+  border-bottom: none;
+}
+
+.recent-icon-wrap {
+  width: 64upx;
+  height: 64upx;
+  border-radius: 12upx;
+  background: #eaf8ef;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.recent-icon-text {
+  color: #09c567;
+  font-size: 26upx;
+  font-weight: 600;
+}
+
+.recent-info {
+  flex: 1;
+  min-width: 0;
+  margin-left: 20upx;
+}
+
+.recent-shop {
+  font-size: 30upx;
+  font-weight: 600;
+  color: #333;
+}
+
+.recent-line {
+  display: block;
+  margin-top: 8upx;
+  font-size: 24upx;
+  color: #999;
+}
+
+.recent-right {
+  flex-shrink: 0;
+  margin-left: 16upx;
+}
+
+.recent-money {
+  font-size: 32upx;
+  font-weight: 700;
+  color: #09c567;
+}
+
+.recent-empty {
+  padding: 48upx;
+  text-align: center;
+  font-size: 28upx;
+  color: #999;
+  background: #fff;
+  border-radius: 20upx;
+}
+</style>

+ 313 - 0
hdApp/src/admin/home/shopInviteList.vue

@@ -0,0 +1,313 @@
+<!--
+  邀约花店列表
+  用途:邀请花店 hub 进入;列表接口按 inviterMainId 分页,搜索匹配 inviteeShopName / inviteeMobile
+-->
+<template>
+  <view class="list-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 class="search-btn" @click="onSearch">搜索</view>
+    </view>
+
+    <element-loading :loading="listLoading" text="加载中" color="#09c567">
+      <view v-if="list.data && list.data.length">
+        <view
+          v-for="(item, index) in list.data"
+          :key="item.id || index"
+          class="shop-card"
+        >
+          <view class="card-top">
+            <image
+              v-if="item.shopImg"
+              class="shop-thumb"
+              :src="item.shopImg"
+              mode="aspectFill"
+            />
+            <view v-else class="shop-thumb shop-thumb--placeholder">
+              <text>花</text>
+            </view>
+            <view class="shop-body">
+              <view class="name-row">
+                <text class="shop-name">{{ item.shopName || '-' }}</text>
+                <view class="status-tag" :class="'status-tag--' + item.memberStatus">
+                  {{ item.memberStatusText }}
+                </view>
+              </view>
+              <text class="shop-line">手机号:{{ item.mobile || '-' }}</text>
+              <text class="shop-line">开通时间:{{ formatTime(item.openTime || item.inviteTime) }}</text>
+            </view>
+          </view>
+          <view class="card-foot">
+            <view class="foot-item">
+              <text class="foot-label">会员费:</text>
+              <text class="foot-value">¥ {{ moneyText(item.memberFee) }}</text>
+            </view>
+            <view class="foot-item foot-item--right">
+              <text class="foot-label">已获佣金:</text>
+              <text class="foot-value foot-value--green">¥ {{ moneyText(item.earnedCommission) }}</text>
+            </view>
+          </view>
+        </view>
+        <view v-if="list.finished" class="list-end">没有更多了</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 { getMainInviteShopList } from '@/api/main-invite'
+import list from '@/mixins/list'
+
+export default {
+  name: 'shopInviteList',
+  components: { AppWrapperEmpty, ElementLoading },
+  mixins: [list],
+  data () {
+    return {
+      keyword: '',
+      searchKeyword: ''
+    }
+  },
+  computed: {
+    /** 首屏/筛选后列表加载中 */
+    listLoading () {
+      return this.list.loading && this.list.page === 1
+    }
+  },
+  onLoad () {
+    this.init()
+  },
+  methods: {
+    moneyText (val) {
+      const n = parseFloat(val)
+      if (isNaN(n)) {
+        return '0.00'
+      }
+      return n.toFixed(2)
+    },
+    formatTime (t) {
+      if (!t) {
+        return '-'
+      }
+      const s = String(t)
+      if (s.length >= 19) {
+        return s.substr(0, 19)
+      }
+      return s
+    },
+    onSearch () {
+      this.searchKeyword = (this.keyword || '').trim()
+      this.resetList()
+      this.init()
+    },
+    async init () {
+      const res = await getMainInviteShopList({
+        page: this.list.page,
+        keyword: this.searchKeyword
+      })
+      this.completes(res)
+    }
+  },
+  async onPullDownRefresh () {
+    this.resetList()
+    await this.init()
+    uni.stopPullDownRefresh()
+  },
+  async onReachBottom () {
+    if (!this.list.finished) {
+      await this.init()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.list-page {
+  min-height: 100vh;
+  padding-bottom: 40upx;
+  background: #f5f6f8;
+}
+
+.search-bar {
+  display: flex;
+  align-items: center;
+  padding: 20upx 24upx;
+  background: #fff;
+}
+
+.search-input-wrap {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  height: 72upx;
+  padding: 0 24upx;
+  border-radius: 12upx;
+  background: #fff;
+  border: 1upx solid #e8e8e8;
+}
+
+.search-icon {
+  margin-right: 12upx;
+  color: #bbb;
+  font-size: 28upx;
+}
+
+.search-input {
+  flex: 1;
+  font-size: 28upx;
+  color: #333;
+}
+
+.search-btn {
+  margin-left: 16upx;
+  padding: 0 32upx;
+  height: 72upx;
+  line-height: 72upx;
+  border-radius: 12upx;
+  background: #09c567;
+  color: #fff;
+  font-size: 28upx;
+  font-weight: 500;
+}
+
+.shop-card {
+  margin: 20upx 24upx 0;
+  padding: 24upx;
+  border-radius: 16upx;
+  background: #fff;
+  box-shadow: 0 2upx 12upx rgba(0, 0, 0, 0.04);
+}
+
+.card-top {
+  display: flex;
+  align-items: flex-start;
+}
+
+.shop-thumb {
+  width: 120upx;
+  height: 120upx;
+  border-radius: 12upx;
+  flex-shrink: 0;
+}
+
+.shop-thumb--placeholder {
+  background: #f0f0f0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #999;
+  font-size: 32upx;
+}
+
+.shop-body {
+  flex: 1;
+  min-width: 0;
+  margin-left: 20upx;
+}
+
+.name-row {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.shop-name {
+  font-size: 32upx;
+  font-weight: 700;
+  color: #222;
+  max-width: 360upx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.status-tag {
+  flex-shrink: 0;
+  padding: 4upx 16upx;
+  margin-left: 12upx;
+  border-radius: 999upx;
+  font-size: 22upx;
+}
+
+.status-tag--opened {
+  color: #09c567;
+  background: #eaf8ef;
+}
+
+.status-tag--pending {
+  color: #ff9800;
+  background: #fff8e6;
+}
+
+.status-tag--refund {
+  color: #999;
+  background: #f5f5f5;
+}
+
+.shop-line {
+  display: block;
+  margin-top: 12upx;
+  font-size: 26upx;
+  color: #888;
+  line-height: 36upx;
+}
+
+.card-foot {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 24upx;
+  padding-top: 24upx;
+  border-top: 1upx solid #f0f0f0;
+}
+
+.foot-item {
+  display: flex;
+  align-items: baseline;
+  flex: 1;
+}
+
+.foot-item--right {
+  justify-content: flex-end;
+}
+
+.foot-label {
+  font-size: 26upx;
+  color: #888;
+}
+
+.foot-value {
+  font-size: 28upx;
+  font-weight: 600;
+  color: #333;
+}
+
+.foot-value--green {
+  color: #09c567;
+}
+
+.list-end {
+  padding: 32upx;
+  text-align: center;
+  font-size: 26upx;
+  color: #999;
+}
+
+.list-loading-placeholder {
+  min-height: 400upx;
+}
+</style>

+ 369 - 0
hdApp/src/admin/home/walletChangeList.vue

@@ -0,0 +1,369 @@
+<!--
+  中央钱包(可用余额)变动明细
+  用途:我的-可用余额-变动明细;数据 xhMainWalletChange
+-->
+<template>
+  <view class="app-content wallet-change-page">
+    <view class="toolbar-wrap">
+      <view class="toolbar-side"></view>
+      <view class="date-wrap">
+        <DateSelect class="bar" top="0" @selectDateFn="selectDateFn" />
+      </view>
+      <view class="toolbar-side toolbar-side--right">
+        <picker
+          mode="selector"
+          :range="capitalTypeLabels"
+          :value="capitalTypeIndex"
+          @change="onCapitalTypeChange"
+        >
+          <view class="type-picker">
+            <text class="type-picker-text">{{ capitalTypeLabels[capitalTypeIndex] }}</text>
+            <text class="iconfont iconsanjiao_xia type-picker-arrow"></text>
+          </view>
+        </picker>
+      </view>
+    </view>
+
+    <view class="summary-bar">
+      <view class="summary-item">
+        <text class="summary-label">累计充值金额:</text>
+        <text class="summary-value">¥{{ formatMoney(summary.totalRecharge) }}</text>
+      </view>
+      <view class="summary-item">
+        <text class="summary-label">累计消费金额:</text>
+        <text class="summary-value">¥{{ formatMoney(summary.totalConsume) }}</text>
+      </view>
+      <view class="summary-item">
+        <text class="summary-label">可用余额:</text>
+        <text class="summary-value">¥{{ formatMoney(summary.walletBalance) }}</text>
+      </view>
+    </view>
+
+    <view class="main-view">
+      <element-loading :loading="listLoading" text="加载中" color="#09c567">
+        <block v-if="!$util.isEmpty(list.data)">
+          <view class="table-header">
+            <view class="header-item time-col">时间</view>
+            <view class="header-item event-col">事项</view>
+            <view class="header-item amount-col">金额</view>
+            <view class="header-item balance-col">余额</view>
+          </view>
+          <view class="table-body">
+            <view
+              v-for="(item, index) in list.data"
+              :key="item.id || index"
+              class="table-row"
+            >
+              <view class="row-item time-col">
+                <view class="time-text">{{ formatRowTime(item.addTime) }}</view>
+              </view>
+              <view class="row-item event-col">
+                <view class="event-text">{{ item.event }}</view>
+                <view v-if="!$util.isEmpty(item.remark)" class="event-remark">{{ item.remark }}</view>
+              </view>
+              <view class="row-item amount-col">
+                <view class="amount-text">{{ formatRowAmount(item) }}</view>
+              </view>
+              <view class="row-item balance-col">
+                <view class="balance-text">{{ parseFloat(item.balance) }}</view>
+              </view>
+            </view>
+          </view>
+        </block>
+        <block v-else-if="!listLoading">
+          <AppWrapperEmpty title="暂无数据" :is-empty="true" />
+        </block>
+        <view v-else class="list-loading-placeholder" />
+      </element-loading>
+    </view>
+  </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 { getMainWalletChangeList } from '@/api/main-wallet'
+import list from '@/mixins/list'
+
+export default {
+  name: 'walletChangeList',
+  components: {
+    AppWrapperEmpty,
+    DateSelect,
+    ElementLoading
+  },
+  mixins: [list],
+  data () {
+    return {
+      params: {
+        searchTime: 'today',
+        startTime: '',
+        endTime: ''
+      },
+      summary: {
+        totalRecharge: 0,
+        totalConsume: 0,
+        walletBalance: 0
+      },
+      capitalTypeIndex: 0,
+      // value 为 -1 表示不传 capitalType;其余与 dict capitalType 对齐后可扩展
+      capitalTypeOptions: [
+        { label: '--请选择--', value: -1 },
+        { label: '充值', value: 4 },
+        { label: '提现', value: 6 }
+      ]
+    }
+  },
+  computed: {
+    capitalTypeLabels () {
+      return this.capitalTypeOptions.map(o => o.label)
+    },
+    /** 首屏/筛选后列表加载中 */
+    listLoading () {
+      return this.list.loading && this.list.page === 1
+    }
+  },
+  onLoad () {
+    this.init()
+  },
+  methods: {
+    formatMoney (val) {
+      const n = parseFloat(val)
+      if (isNaN(n)) {
+        return '0.00'
+      }
+      return n.toFixed(2)
+    },
+    formatRowTime (addTime) {
+      if (!addTime || addTime.length < 16) {
+        return addTime || ''
+      }
+      return addTime.substr(5, 11)
+    },
+    formatRowAmount (item) {
+      const amt = parseFloat(item.amount)
+      if (isNaN(amt)) {
+        return item.amount
+      }
+      if (Number(item.io) === 0) {
+        return amt > 0 ? '-' + amt : amt
+      }
+      return amt > 0 ? '+' + amt : '+' + amt
+    },
+    selectDateFn (val) {
+      this.params = { ...this.params, ...val }
+      this.resetList()
+      this.init()
+    },
+    onCapitalTypeChange (e) {
+      const idx = Number(e.detail.value)
+      this.capitalTypeIndex = idx
+      this.resetList()
+      this.init()
+    },
+    buildListParams () {
+      const params = {
+        page: this.list.page,
+        ...this.params
+      }
+      const opt = this.capitalTypeOptions[this.capitalTypeIndex]
+      if (opt && opt.value !== -1) {
+        params.capitalType = opt.value
+      }
+      return params
+    },
+    async init () {
+      const res = await getMainWalletChangeList(this.buildListParams())
+      if (res.code === 1 && res.data && res.data.summary && this.list.page === 1) {
+        this.summary = {
+          totalRecharge: res.data.summary.totalRecharge,
+          totalConsume: res.data.summary.totalConsume,
+          walletBalance: res.data.summary.walletBalance
+        }
+      }
+      if (this.$util.isEmpty(res.data.list)) {
+        this.completes(res)
+        return
+      }
+      const currentList = res.data.list.slice()
+      this.completes({ ...res, data: { ...res.data, list: currentList } })
+    }
+  },
+  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>
+.wallet-change-page {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+}
+
+.toolbar-wrap {
+  display: flex;
+  align-items: center;
+  padding: 20upx 24upx;
+  background-color: #fff;
+  border-bottom: 1upx solid #eef1f6;
+}
+
+.toolbar-side {
+  flex: 0 0 160upx;
+  display: flex;
+  align-items: center;
+}
+
+.toolbar-side--right {
+  justify-content: flex-end;
+}
+
+.date-wrap {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.type-picker {
+  display: flex;
+  align-items: center;
+  max-width: 200upx;
+}
+
+.type-picker-text {
+  color: #666;
+  font-size: 26upx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.type-picker-arrow {
+  margin-left: 8upx;
+  font-size: 20upx;
+  color: #999;
+}
+
+.summary-bar {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  padding: 20upx 24upx;
+  background-color: #f0f2f6;
+  font-size: 24upx;
+  color: #333;
+}
+
+.summary-item {
+  margin-right: 24upx;
+  margin-bottom: 8upx;
+}
+
+.summary-item:last-child {
+  margin-right: 0;
+}
+
+.summary-label {
+  color: #666;
+}
+
+.summary-value {
+  color: #111;
+  font-weight: 600;
+}
+
+.main-view {
+  padding: 0;
+}
+
+.table-header {
+  display: flex;
+  background-color: #f0f2f6;
+  padding: 24upx 16upx;
+  margin-bottom: 2upx;
+  font-size: 28upx;
+  font-weight: 600;
+  color: #000;
+}
+
+.table-body {
+  background-color: #fff;
+}
+
+.table-row {
+  display: flex;
+  padding: 24upx 16upx;
+  border-bottom: 1upx solid #f0f0f0;
+}
+
+.time-col {
+  width: 150upx;
+  flex-shrink: 0;
+}
+
+.event-col {
+  flex: 1;
+  min-width: 0;
+  padding-right: 12upx;
+}
+
+.amount-col {
+  width: 120upx;
+  flex-shrink: 0;
+  text-align: center;
+}
+
+.balance-col {
+  width: 120upx;
+  flex-shrink: 0;
+  text-align: center;
+}
+
+.header-item {
+  box-sizing: border-box;
+}
+
+.row-item {
+  box-sizing: border-box;
+}
+
+.time-text {
+  font-size: 26upx;
+  color: #666;
+}
+
+.event-text {
+  font-size: 26upx;
+  color: #333;
+  word-break: break-all;
+}
+
+.event-remark {
+  margin-top: 8upx;
+  font-size: 24upx;
+  color: #999;
+  word-break: break-all;
+}
+
+.amount-text,
+.balance-text {
+  font-size: 26upx;
+  color: #333;
+}
+
+.list-loading-placeholder {
+  min-height: 400upx;
+}
+</style>

+ 256 - 0
hdApp/src/admin/home/walletRecharge.vue

@@ -0,0 +1,256 @@
+<!--
+  中央钱包充值页(仅 UI)
+  用途:我的-可用余额-充值;支付与下单后续迭代
+-->
+<template>
+  <view class="wallet-recharge-page">
+    <view class="balance-card">
+      <text class="balance-label">可用余额(元)</text>
+      <text class="balance-value">¥{{ formatMoney(walletBalance) }}</text>
+    </view>
+
+    <view class="amount-card">
+      <view class="section-title">充值金额(元)</view>
+      <view class="amount-display">¥{{ displayAmount }}</view>
+
+      <view class="amount-grid">
+        <view
+          v-for="(item, index) in presetAmounts"
+          :key="item"
+          class="amount-cell"
+          :class="{ 'amount-cell--active': !isCustom && selectedPreset === item }"
+          @click="selectPresetByIndex(index)"
+        >
+          <text>{{ item }}</text>
+        </view>
+        <view
+          class="amount-cell"
+          :class="{ 'amount-cell--active': isCustom }"
+          @click="selectCustomMode"
+        >
+          <text class="amount-cell-custom-text">自定义金额</text>
+        </view>
+      </view>
+
+      <view v-if="isCustom" class="custom-input-wrap">
+        <input
+          class="custom-input"
+          type="digit"
+          v-model="customAmount"
+          placeholder="请输入充值金额"
+        />
+      </view>
+    </view>
+
+    <view class="footer-wrap">
+      <button class="pay-btn" @click="onPayPlaceholder">微信支付</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { mainMy } from '@/api/home'
+
+/** 预设充值档位(元) */
+const PRESET_AMOUNTS = [300, 500, 1000, 2000, 3000]
+
+export default {
+  name: 'walletRecharge',
+  data () {
+    return {
+      walletBalance: 0,
+      presetAmounts: PRESET_AMOUNTS,
+      selectedPreset: PRESET_AMOUNTS[0],
+      isCustom: false,
+      customAmount: ''
+    }
+  },
+  computed: {
+    displayAmount () {
+      if (this.isCustom) {
+        const n = parseFloat(this.customAmount)
+        if (isNaN(n) || n <= 0) {
+          return '0'
+        }
+        return this.formatPlainAmount(n)
+      }
+      return String(this.selectedPreset)
+    }
+  },
+  onShow () {
+    this.loadWalletBalance()
+  },
+  methods: {
+    formatMoney (val) {
+      const n = parseFloat(val)
+      if (isNaN(n)) {
+        return '0.00'
+      }
+      return n.toFixed(2)
+    },
+    formatPlainAmount (n) {
+      if (Math.floor(n) === n) {
+        return String(n)
+      }
+      return String(n)
+    },
+    async loadWalletBalance () {
+      const res = await mainMy()
+      if (res.code === 1 && res.data) {
+        this.walletBalance = res.data.walletBalance != null ? res.data.walletBalance : 0
+      }
+    },
+    /** 小程序端避免 v-for 内传 item 导致 data-event-opts 编译失败 */
+    selectPresetByIndex (index) {
+      const amount = this.presetAmounts[index]
+      if (amount == null) {
+        return
+      }
+      this.isCustom = false
+      this.selectedPreset = amount
+      this.customAmount = ''
+    },
+    selectCustomMode () {
+      this.isCustom = true
+    },
+    onPayPlaceholder () {
+      uni.showToast({
+        title: '功能开发中',
+        icon: 'none'
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.wallet-recharge-page {
+  min-height: 100vh;
+  padding: 24upx 24upx 160upx;
+  box-sizing: border-box;
+  background: #f5f5f5;
+}
+
+.balance-card {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 28upx 32upx;
+  border-radius: 16upx;
+  background: #eef9f2;
+}
+
+.balance-label {
+  font-size: 28upx;
+  color: #666;
+}
+
+.balance-value {
+  font-size: 36upx;
+  font-weight: 700;
+  color: #09c567;
+}
+
+.amount-card {
+  margin-top: 24upx;
+  padding: 32upx 28upx 40upx;
+  border-radius: 16upx;
+  background: #fff;
+}
+
+.section-title {
+  font-size: 28upx;
+  color: #666;
+}
+
+.amount-display {
+  margin-top: 24upx;
+  padding-bottom: 28upx;
+  margin-bottom: 28upx;
+  font-size: 72upx;
+  font-weight: 800;
+  color: #111;
+  line-height: 1.1;
+  border-bottom: 1upx solid #eee;
+}
+
+.amount-grid {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.amount-cell {
+  width: calc((100% - 40upx) / 3);
+  height: 88upx;
+  margin-right: 20upx;
+  margin-bottom: 20upx;
+  border: 1upx solid #e8e8e8;
+  border-radius: 12upx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 30upx;
+  color: #333;
+  box-sizing: border-box;
+}
+
+.amount-cell:nth-child(3n) {
+  margin-right: 0;
+}
+
+.amount-cell-custom-text {
+  font-size: 24upx;
+  text-align: center;
+  line-height: 32upx;
+  padding: 0 8upx;
+}
+
+.amount-cell--active {
+  border-color: #09c567;
+  color: #09c567;
+  background: #f6fbf8;
+}
+
+.amount-cell--active .amount-cell-custom-text {
+  color: #09c567;
+}
+
+.custom-input-wrap {
+  margin-top: 4upx;
+}
+
+.custom-input {
+  width: 100%;
+  height: 88upx;
+  padding: 0 24upx;
+  border: 1upx solid #ddd;
+  border-radius: 12upx;
+  font-size: 32upx;
+  box-sizing: border-box;
+}
+
+.footer-wrap {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  padding: 24upx 32upx;
+  padding-bottom: calc(24upx + env(safe-area-inset-bottom));
+  background: #f5f5f5;
+}
+
+.pay-btn {
+  width: 100%;
+  height: 88upx;
+  line-height: 88upx;
+  border-radius: 999upx;
+  background: #09c567;
+  color: #fff;
+  font-size: 32upx;
+  border: none;
+}
+
+.pay-btn::after {
+  border: none;
+}
+</style>

+ 10 - 0
hdApp/src/api/main-invite/index.js

@@ -0,0 +1,10 @@
+/**
+ * 门店邀请 / 我的分佣 — hdApp
+ */
+import https from '@/plugins/luch-request_0.0.7/request'
+
+export const getMainInviteIndex = data => https.get('/main-invite/index', data)
+
+export const getMainInviteShopList = data => https.get('/main-invite/invite-shop-list', data)
+
+export const getMainInviteCommissionList = data => https.get('/main-invite/commission-list', data)

+ 9 - 0
hdApp/src/api/main-wallet/index.js

@@ -0,0 +1,9 @@
+/**
+ * 中央钱包(可用余额)接口 — hdApp 变动明细等
+ */
+import https from '@/plugins/luch-request_0.0.7/request'
+
+/** 钱包变动明细分页 + 汇总 */
+export const getMainWalletChangeList = data => {
+  return https.get('/main-wallet/change-list', data)
+}

+ 12 - 0
hdApp/src/api/official/index.js

@@ -28,6 +28,18 @@ export const applyRegister = data => {
 	return https.post('/apply/register', data)
 }
 
+/** 邀请注册年度会员:创建 xhRenew 并唤起微信支付 */
+export const renewCreate = data => https.post('/renew/create', data)
+
+/** 注册页会员费 / 邀请减免预览 */
+export const getRegisterFeePreview = data => https.get('/apply/register-fee-preview', data)
+
+/** 邀请开通页:是否已有店、xhApply 邀请信息 */
+export const getRegisterInviteContext = data => https.get('/apply/register-invite-context', data)
+
+/** 已开店用户:支付前保存邀请码到 xhApply */
+export const saveRegisterInvite = data => https.post('/apply/save-register-invite', data)
+
 // 获取验证码 w
 export const sendSmsW = param => https.get('/inform-user/send-sms', param)
 

+ 6 - 0
hdApp/src/pages.json

@@ -26,6 +26,11 @@
 		{"path": "admin/home/privacy","style": {"navigationBarTitleText": "隐私政策"}},
 		{ "path": "admin/home/me", "style": { "navigationBarTitleText": "我的", "enablePullDownRefresh": true, "navigationStyle": "custom" } },
 		{ "path": "admin/home/moneyChangeList", "style": { "navigationBarTitleText": "变动明细", "enablePullDownRefresh": true } },
+		{ "path": "admin/home/walletChangeList", "style": { "navigationBarTitleText": "变动明细", "enablePullDownRefresh": true } },
+		{ "path": "admin/home/walletRecharge", "style": { "navigationBarTitleText": "充值" } },
+		{ "path": "admin/home/shopInviteHub", "style": { "navigationBarTitleText": "邀请花店", "enablePullDownRefresh": true } },
+		{ "path": "admin/home/shopInviteList", "style": { "navigationBarTitleText": "邀约花店", "enablePullDownRefresh": true } },
+		{ "path": "admin/home/shopInviteCommissionList", "style": { "navigationBarTitleText": "佣金变动明细", "enablePullDownRefresh": true } },
 		{ "path": "admin/home/edit", "style": { "navigationBarTitleText": "花店名称修改" } },
 		{"path": "common/success","style": {"navigationBarBackgroundColor": "#ffffff","navigationBarTextStyle": "black"}},
 		{"path": "common/webview","style": {"navigationBarTitleText": "网页","navigationBarBackgroundColor": "#ffffff","navigationBarTextStyle": "black"}}
@@ -195,6 +200,7 @@
 				{ "path": "official/pay", "style": { "navigationBarTitleText": "支付" } },
 				{ "path": "official/renewal", "style": { "navigationBarTitleText": "续期" } },
 				{ "path": "official/apply", "style": { "navigationBarTitleText": "填写信息" } },
+				{ "path": "official/applyInvite", "style": { "navigationBarTitleText": "邀请开通" } },
 				{ "path": "official/clientApply", "style": { "navigationBarTitleText": "结果" } },
 				{ "path": "official/clientResult", "style": { "navigationBarTitleText": "提交审核" } },
 				{ "path": "official/callback", "style": { "navigationBarTitleText": "回调" } },

+ 1397 - 0
hdApp/src/pagesClient/official/applyInvite.vue

@@ -0,0 +1,1397 @@
+<!--
+  邀请开店 / 会员开通页
+  用途:分享带邀请码新用户注册+支付;已开店用户仅填邀请码并本页支付
+-->
+<template>
+  <view class="apply-page app-content">
+    <form @submit="formSubmit">
+      <view class="apply-body">
+        <!-- 年度会员价横幅 -->
+        <view class="member-banner">
+          <image
+            v-if="memberBannerBg"
+            class="member-banner-bg"
+            :src="memberBannerBg"
+            mode="aspectFill"
+          />
+          <view class="member-banner-text">
+            <text class="member-banner-title">{{ feePreview.memberTypeName || '年度会员' }}</text>
+            <view class="member-banner-price-row">
+              <text class="member-banner-yen">¥</text>
+              <text class="member-banner-price">{{ priceInteger }}</text>
+              <text class="member-banner-unit">/年</text>
+            </view>
+          </view>
+        </view>
+
+        <view v-if="hasShop" class="apply-section">
+          <view class="apply-section-title">当前门店</view>
+          <view class="module-com apply-card">
+            <tui-list-cell class="line-cell" :hover="false">
+              <view class="tui-title">门店名称</view>
+              <view class="tui-input">{{ shopBrief.name || '—' }}</view>
+            </tui-list-cell>
+            <tui-list-cell class="line-cell" :hover="false">
+              <view class="tui-title">联系电话</view>
+              <view class="tui-input">{{ shopBrief.mobile || '—' }}</view>
+            </tui-list-cell>
+          </view>
+        </view>
+
+        <view v-if="!hasShop" class="apply-section">
+          <view class="apply-section-title">门店信息</view>
+          <view class="module-com apply-card">
+            <tui-list-cell class="line-cell" :hover="false">
+              <view class="tui-title">类型</view>
+              <view class="apply-mode-btns">
+                <view class="apply-mode-btn" :class="{ 'apply-mode-btn--active': isHd == 1 }" @click="isHd = 1">有开店</view>
+                <view class="apply-mode-btn apply-mode-btn--last" :class="{ 'apply-mode-btn--active': isHd == 0 }" @click="isHd = 0">没开店</view>
+              </view>
+            </tui-list-cell>
+            <tui-list-cell class="line-cell" :hover="false">
+              <view class="tui-title">{{ isHd == 1 ? '你的店名' : '收货姓名' }}</view>
+              <input v-model="form.name" type="text" placeholder-class="phcolor" placeholder-style="color:#CCCCCC" class="tui-input" name="name" placeholder="请填写名称" />
+            </tui-list-cell>
+
+            <tui-list-cell class="line-cell line-cell--city" :hover="false">
+              <view class="tui-title">所在城市</view>
+              <view class="city-row-main" @click="openAddres">
+                <view class="tui-input" v-if="form.province">{{ form.province + '-' + form.city }}</view>
+                <view class="tui-placeholder" v-else>请选择城市</view>
+              </view>
+              <view class="city-locate-btn" @click.stop="locateCity">
+                <image class="city-locate-icon" :src="iconSrc('general-location')" mode="aspectFit"></image>
+              </view>
+            </tui-list-cell>
+            <tui-list-cell v-if="hasMap == 0" class="line-cell" :hover="false">
+              <view class="tui-title">所在地址</view>
+              <input v-model="form.address" placeholder-class="phcolor" placeholder-style="color:#CCCCCC" class="tui-input" name="address" @input="modifyShow" placeholder="请填写地址" />
+            </tui-list-cell>
+            <tui-list-cell v-else class="line-cell" :hover="false" :arrow="true" @click="selectRegion">
+              <view class="tui-title">所在地址</view>
+              <view v-if="form.address" class="tui-input">{{ form.address }}</view>
+              <view v-else class="tui-placeholder">请搜索选择地址</view>
+              <input v-model="form.address" name="address" hidden />
+            </tui-list-cell>
+
+            <tui-list-cell class="line-cell" :hover="false">
+              <view class="tui-title">楼号门牌</view>
+              <input v-model="form.floor" type="text" placeholder-class="phcolor" placeholder-style="color:#CCCCCC" class="tui-input" name="floor" placeholder="选填" @input="modifyShow" />
+            </tui-list-cell>
+          </view>
+        </view>
+
+        <view v-if="!hasShop" class="apply-section">
+          <view class="apply-section-title">注册人信息</view>
+          <view class="module-com apply-card">
+            <tui-list-cell class="line-cell code-wrap name-wrap" :hover="false">
+              <view class="tui-title required">姓名</view>
+              <!-- #ifdef MP-WEIXIN -->
+              <input
+                v-model="form.adminName"
+                type="nickname"
+                placeholder-class="phcolor"
+                placeholder-style="color:#CCCCCC"
+                class="tui-input"
+                name="adminName"
+                placeholder="请填写姓名"
+                @blur="onNameBlur"
+                @input="onNameInput"
+              />
+              <!-- #endif -->
+              <!-- #ifndef MP-WEIXIN -->
+              <input
+                v-model="form.adminName"
+                type="text"
+                placeholder-class="phcolor"
+                placeholder-style="color:#CCCCCC"
+                class="tui-input"
+                name="adminName"
+                placeholder="请填写姓名"
+              />
+              <!-- #endif -->
+            </tui-list-cell>
+            <tui-list-cell class="line-cell code-wrap mobile-wrap" :hover="false">
+              <view class="tui-title required">手机号</view>
+              <input
+                v-model="form.mobile"
+                type="number"
+                placeholder-class="phcolor"
+                @blur="checkShopFn"
+                placeholder-style="color:#CCCCCC"
+                class="tui-input tui-input--code"
+                :class="{ 'tui-input--filled': fillMobile == 0 }"
+                name="mobile"
+                :disabled="fillMobile == 0"
+                :placeholder="fillMobile == 0 ? '' : '请填写手机号'"
+              />
+              <!-- #ifdef MP-WEIXIN -->
+              <button
+                v-if="fillMobile == 1"
+                class="apply-code-btn apply-fill-btn"
+                open-type="getPhoneNumber"
+                @getphonenumber="getSj"
+              >帮我填</button>
+              <button
+                v-else
+                class="apply-code-btn apply-cancel-btn"
+                @click="cancelAutoMobile"
+              >取消</button>
+              <!-- #endif -->
+            </tui-list-cell>
+            <tui-list-cell v-if="fillMobile == 1" class="line-cell code-wrap" :hover="false">
+              <view class="tui-title required">验证码</view>
+              <input v-model="form.authCode" type="number" placeholder-class="phcolor" placeholder-style="color:#CCCCCC" class="tui-input tui-input--code" name="authCode" placeholder="请填写验证码" />
+              <button v-if="hasGet == false" class="apply-code-btn" @click="requestAuthCode()">获取</button>
+              <button v-else class="apply-code-btn apply-code-btn--disabled">{{ count }}</button>
+            </tui-list-cell>
+          </view>
+        </view>
+
+        <!-- 会员费、邀请码、减免、应付 -->
+        <view class="fee-card module-com apply-card">
+          <view class="fee-row">
+            <text class="fee-label">邀请码</text>
+            <input
+              v-model="form.inviteCode"
+              class="fee-input"
+              type="text"
+              placeholder="选填"
+              placeholder-class="phcolor"
+              placeholder-style="color:#CCCCCC"
+              @input="onInviteCodeInput"
+              @blur="onInviteCodeBlur"
+              @confirm="onInviteCodeBlur"
+            />
+          </view>
+          <view class="fee-row fee-row--border">
+            <text class="fee-label">会员费</text>
+            <text class="fee-value">¥{{ formatMoney(feePreview.price) }}</text>
+          </view>
+          <view v-if="feePreview.showInviteDiscount" class="fee-row fee-row--border">
+            <text class="fee-label fee-label--discount">邀请减免</text>
+            <text class="fee-value fee-value--discount">-¥{{ formatMoney(feePreview.hdDiscountAmount) }}</text>
+          </view>
+          <view class="fee-row fee-row--border">
+            <text class="fee-label">应付金额</text>
+            <text class="fee-value fee-value--pay">¥{{ formatPayAmount(feePreview.payAmount) }}</text>
+          </view>
+        </view>
+
+        <view class="module-com apply-card recommend-wrap" v-if="!$util.isEmpty(merchantData)">
+          <view class="recommend-tag">推荐人</view>
+          <view class="recommend-det">
+            <image class="recommend-avatar" mode="scaleToFill" :src="merchantData.logoUrl" @error="imageError" />
+            <view class="recommend-info">
+              <view class="app-size-32">{{ merchantData.name }}</view>
+            </view>
+          </view>
+        </view>
+
+        <view class="apply-agreement">
+          <checkbox-group @change="agreeItem">
+            <label class="apply-agreement-label">
+              <checkbox value="1" class="apply-checkbox" :checked="agree == 1" />
+              <text class="apply-agreement-text">
+                我已阅读并同意
+                <text class="apply-agreement-link" @click.stop="goToService()">《服务协议》</text>
+                和
+                <text class="apply-agreement-link" @click.stop="goToPrivacy()">《隐私政策》</text>
+              </text>
+            </label>
+          </checkbox-group>
+        </view>
+
+        <view class="apply-submit-wrap">
+          <button class="apply-submit-btn" formType="submit">{{ submitBtnText }}</button>
+        </view>
+      </view>
+
+      <simple-address ref="simpleAddress" :pickerValueDefault="cityPickerValueDefault" @onConfirm="onCityConfirm"></simple-address>
+      <app-area-sel :show.sync="showRegion" :city="form.city" :initial-list="addressInitialList" @change="changeAreaFn" />
+    </form>
+  </view>
+</template>
+
+<script>
+import TuiListCell from "@/components/plugin/list-cell";
+import AppAreaSel from "@/components/app-area-sel";
+import SimpleAddress from "@/components/plugin/simple-address";
+import AppAvatarModule from "@/components/module/app-avatar";
+import AppUploader from "@/components/app-uploader";
+const form = require("@/utils/formValidation.js");
+import { getIntroduce, applyRegister, renewCreate, getRegisterFeePreview, getRegisterInviteContext, saveRegisterInvite } from '@/api/official'
+// #ifdef MP-WEIXIN || H5
+import wexinPay from '@/utils/pay/wxPay'
+// #endif
+import {getAuthCode} from "@/api/apply";
+import { mapGetters,mapActions } from "vuex"
+import htzImageUpload from '@/components/htz-image-upload/htz-image-upload.vue'
+import { APIHOST } from '@/config'
+import { getShopInfo} from "@/utils/auth";
+import { getDictionaries } from '@/api/mini'
+import { checkShop } from '@/api/shop'
+import { ghsInfo } from "@/api/ghs/index";
+import { getMobile } from "@/api/auth";
+import { getHasMap } from "@/api/express"
+import { mapNearby } from '@/api/map/index'
+import { iconSrc } from '@/utils/iconSrc'
+import { persistInviteContext, tryBindPendingInvite, optionHasInviteContext, clearPendingInvite, restorePendingInviteFromLegacy, getRegisterInviteParams, expireInviteIfNeeded, getGhsProductQueryFromPending } from "@/utils/pendingGhsInvite";
+// #ifdef APP-PLUS
+import permision from "@/utils/wa-permission_1.1/permission.js";
+// #endif
+export default {
+  name: 'applyInvite',
+  components: {
+    TuiListCell,
+    AppAreaSel,
+    SimpleAddress,
+    AppAvatarModule,
+    AppUploader,
+    htzImageUpload
+  },
+  data () {
+    return {
+      merchantData: {},
+      agree:0,
+      form: {
+        name: "",
+        mobile: "",
+        province: "",
+        city: "",
+        dist: "",
+        address: "",
+        adminName: '',
+        main: 1,
+        licenseNo: "",
+        license: "",
+        floor: "",
+        longitude: 0,
+        latitude: 0,
+        showAddress:"",
+        authCode:"",
+        cover:'',
+        inviteCode: ''
+      },
+      /** 注册费用预览(/apply/register-fee-preview) */
+      feePreview: {
+        price: 0,
+        memberTypeName: '年度会员',
+        inviteValid: 0,
+        inviteNotFound: 0,
+        hdDiscountAmount: 0,
+        showInviteDiscount: 0,
+        payAmount: 0
+      },
+      inviteCodePreviewTimer: null,
+      cityPickerValueDefault: [0, 0],
+      showRegion: false,
+      addressInitialList: [],
+      locating: false,
+      hasMap: 0,
+      adminName: "",
+      logo: '',
+      hasGet:false,
+      count:120,
+      INT:false,
+      headers:{token:''},
+      currentImgData: [],
+      isHd:1,
+      uploadImgUrl:APIHOST+'/upload/save-file',
+      fillMobile: 1,
+      /** 是否已开通 xhShop(已登录且有 currentShopId) */
+      hasShop: false,
+      /** 当前账号 xhApply.id,已开店支付时使用 */
+      applyId: 0,
+      shopBrief: {
+        name: '',
+        mobile: ''
+      },
+      /** 分享链接/启动参数带入的邀请码,优先级高于 xhApply */
+      inviteCodeFromLink: ''
+    };
+  },
+	computed:{
+		...mapGetters(["getLoginInfo"]),
+    /** 提交按钮文案 */
+    submitBtnText () {
+      return this.hasShop ? '立即支付' : '确认注册'
+    },
+    /** 横幅背景图 */
+    memberBannerBg () {
+      const host = (this.$constant && this.$constant.imgUrl) || ''
+      return host ? host + '/hzg/invite/shop_invite_bg.png' : ''
+    },
+    /** 横幅展示整数价 */
+    priceInteger () {
+      const n = parseFloat(this.feePreview.price)
+      if (isNaN(n)) {
+        return '0'
+      }
+      return String(Math.round(n))
+    }
+	},
+  onLoad () {
+    expireInviteIfNeeded()
+    if (optionHasInviteContext(this.option)) {
+      persistInviteContext(this.option)
+    } else {
+      const legacyOption = uni.getStorageSync('ghsCgOption') || {}
+      const hadLegacy = !!(uni.getStorageSync('ghsCgScene') || legacyOption.shopId)
+      if (hadLegacy) {
+        restorePendingInviteFromLegacy()
+      } else {
+        clearPendingInvite()
+      }
+    }
+    const token = uni.getStorageSync('token')
+    this.headers.token = token
+    // #ifdef APP-PLUS
+    this.fillMobile = 1
+    // #endif
+    getHasMap().then(res => {
+      this.hasMap = res.data.hasMap
+    })
+    const linkCode = this.getLaunchInviteCode()
+    if (linkCode) {
+      this.inviteCodeFromLink = linkCode
+      this.form.inviteCode = linkCode
+    }
+    this.loadRegisterFeePreview(false)
+    this.loadRegisterInviteContext()
+  },
+  onUnload(){
+    clearInterval(this.INT)
+    this.clearInviteCodePreviewTimer()
+  },
+	onShow() {
+    if (uni.getStorageSync('token')) {
+      // 仅刷新门店上下文,不覆盖用户已改或链接带入的邀请码
+      this.loadRegisterInviteContext(true)
+    }
+	},
+  methods: {
+    ...mapActions(['setUserShopAll']),
+    iconSrc,
+    formatMoney (val) {
+      const n = parseFloat(val)
+      if (isNaN(n)) {
+        return '0'
+      }
+      if (Math.abs(n - Math.round(n)) < 0.001) {
+        return String(Math.round(n))
+      }
+      return n.toFixed(2)
+    },
+    formatPayAmount (val) {
+      const n = parseFloat(val)
+      if (isNaN(n)) {
+        return '0.00'
+      }
+      return n.toFixed(2)
+    },
+    /** 邀请码输入:1.5s 防抖后查费用预览 */
+    onInviteCodeInput () {
+      this.scheduleInviteCodePreview()
+    },
+    scheduleInviteCodePreview () {
+      this.clearInviteCodePreviewTimer()
+      this.inviteCodePreviewTimer = setTimeout(() => {
+        this.inviteCodePreviewTimer = null
+        this.loadRegisterFeePreview(true)
+      }, 1500)
+    },
+    clearInviteCodePreviewTimer () {
+      if (this.inviteCodePreviewTimer) {
+        clearTimeout(this.inviteCodePreviewTimer)
+        this.inviteCodePreviewTimer = null
+      }
+    },
+    onInviteCodeBlur () {
+      this.clearInviteCodePreviewTimer()
+      this.loadRegisterFeePreview(true)
+    },
+    /** 读取页面启动参数中的 inviteCode */
+    getLaunchInviteCode () {
+      const code = (this.option && this.option.inviteCode) ? String(this.option.inviteCode).trim() : ''
+      return code
+    },
+    /** 按优先级回填邀请码:链接参数 > xhApply */
+    applyInviteCodeFromContext (applyInviteCode) {
+      if (this.inviteCodeFromLink) {
+        this.form.inviteCode = this.inviteCodeFromLink
+      } else if (applyInviteCode) {
+        this.form.inviteCode = applyInviteCode
+      }
+    },
+    /** 拉取是否已开店及 xhApply 邀请快照;skipInviteFill 为 true 时不改邀请码输入框 */
+    async loadRegisterInviteContext (skipInviteFill) {
+      const token = uni.getStorageSync('token')
+      if (!token) {
+        this.hasShop = false
+        this.applyId = 0
+        return
+      }
+      try {
+        const res = await getRegisterInviteContext()
+        if (res.code !== 1 || !res.data) {
+          return
+        }
+        const d = res.data
+        this.hasShop = d.hasShop === 1
+        this.applyId = d.applyId || 0
+        this.shopBrief = {
+          name: d.shopName || '',
+          mobile: d.shopMobile || ''
+        }
+        if (!skipInviteFill) {
+          this.applyInviteCodeFromContext(d.inviteCode || '')
+          await this.loadRegisterFeePreview(false)
+        }
+      } catch (e) {
+        // 网络失败不打断填写
+      }
+    },
+    /** 拉取会员费 / 邀请减免预览 */
+    async loadRegisterFeePreview (notifyInvalid) {
+      const code = (this.form.inviteCode || '').trim()
+      try {
+        const res = await getRegisterFeePreview({ inviteCode: code })
+        if (code !== (this.form.inviteCode || '').trim()) {
+          return
+        }
+        if (res.code === 1 && res.data) {
+          this.feePreview = { ...this.feePreview, ...res.data }
+          if (notifyInvalid && code !== '' && res.data.inviteNotFound) {
+            uni.showToast({ title: '邀请人不存在', icon: 'none' })
+          }
+        }
+      } catch (e) {
+        // 网络失败不打断填写
+      }
+    },
+    ensureAppLocationPermission() {
+      return new Promise(async (resolve, reject) => {
+        // #ifdef APP-PLUS
+        if (plus.os.name === 'Android') {
+          const permissionRes = await permision.requestAndroidPermission('android.permission.ACCESS_FINE_LOCATION')
+          if (permissionRes !== 1) {
+            if (permissionRes === -1) {
+              permision.gotoAppPermissionSetting()
+            }
+            reject(new Error('请开启定位权限'))
+            return
+          }
+        }
+        if (!permision.checkSystemEnableLocation()) {
+          reject(new Error('请先开启系统定位服务'))
+          return
+        }
+        // #endif
+        resolve()
+      })
+    },
+    getDeviceLocation() {
+      return new Promise((resolve, reject) => {
+        const requestLocation = (locationType) => {
+          uni.getLocation({
+            type: locationType,
+            success: (res) => resolve({ ...res, locationType }),
+            fail: (err) => {
+              if (locationType === 'gcj02' && (
+                err.code == 18 ||
+                err.errCode == 18 ||
+                (err.errMsg && (
+                  err.errMsg.indexOf('not support gcj02') > -1 ||
+                  err.errMsg.indexOf('Not Support CoordsType') > -1
+                ))
+              )) {
+                requestLocation('wgs84')
+                return
+              }
+              reject(err)
+            }
+          })
+        }
+        // #ifdef APP-PLUS
+        if (plus.os.name === 'iOS') {
+          requestLocation('wgs84')
+          return
+        }
+        // #endif
+        requestLocation('gcj02')
+      })
+    },
+    modifyShow(){
+			this.form.showAddress = this.form.city + this.form.address + this.form.floor
+		},
+    applyNearbyAddress(item) {
+      if (!item) {
+        return false
+      }
+      if (item.district) {
+        this.form.dist = item.district
+      }
+      this.form.address = item.name || item.address || ''
+      this.form.showAddress = item.showAddress || item.address || ''
+      if (item.location) {
+        let lng = ''
+        let lat = ''
+        if (typeof item.location === 'object') {
+          lng = item.location.lng || item.location.longitude || ''
+          lat = item.location.lat || item.location.latitude || ''
+        } else {
+          const parts = String(item.location).split(',').map(v => v.trim())
+          if (parts.length >= 2) {
+            lng = parts[0]
+            lat = parts[1]
+          }
+        }
+        if (lng && lat) {
+          this.form.longitude = lng
+          this.form.latitude = lat
+        }
+      }
+      this.modifyShow()
+    },
+    fillCityFromNearby(first) {
+      if (!first) {
+        return false
+      }
+      if (first.province) {
+        this.form.province = first.province
+      }
+      if (first.city) {
+        this.form.city = first.city
+      }
+      if (first.district) {
+        this.form.dist = first.district
+      }
+      this.modifyShow()
+    },
+    async loadNearbyAddressList(fillCity) {
+      if (this.locating) {
+        return []
+      }
+      this.locating = true
+      uni.showLoading({ title: fillCity ? '定位中' : '加载中', mask: true })
+      try {
+        await this.ensureAppLocationPermission()
+        const location = await this.getDeviceLocation()
+        const res = await mapNearby({
+          lat: location.latitude,
+          long: location.longitude,
+          locationType: location.locationType || 'gcj02'
+        })
+        if (res.code != 1 || !res.data || !res.data.list || res.data.list.length === 0) {
+          if (fillCity) {
+            this.$msg('未获取到位置,请手动选择城市')
+          }
+          return []
+        }
+        const list = res.data.list
+        if (fillCity) {
+          this.fillCityFromNearby(list[0])
+        }
+        this.addressInitialList = list
+        return list
+      } catch (err) {
+        console.log('loadNearbyAddressList fail', err)
+        if (fillCity) {
+          this.$msg((err && err.message) ? err.message : '定位失败,请手动填写')
+        }
+        return []
+      } finally {
+        this.locating = false
+        uni.hideLoading()
+      }
+    },
+    async locateCity() {
+      const list = await this.loadNearbyAddressList(true)
+      if (list.length > 0) {
+        this.$msg('已定位到当前城市')
+      }
+    },
+    onHere(){
+      this.form.province = '云南省'
+      this.form.city = '昆明市'
+    },
+    cancelAutoMobile() {
+      this.$util.confirmModal({
+        content: '取消后将清空已获取的手机号,需重新填写并验证,是否确认?',
+        okText: '确认取消',
+        cancelText: '暂不取消'
+      }, () => {
+        this.resetAutoMobile()
+      })
+    },
+    resetAutoMobile() {
+      this.fillMobile = 1
+      this.form.mobile = ''
+      this.form.authCode = ''
+      if (this.INT) {
+        clearInterval(this.INT)
+        this.INT = false
+      }
+      this.hasGet = false
+      this.count = 120
+    },
+    onNameBlur(e) {
+      if (e.detail && typeof e.detail.value === 'string') {
+        this.form.adminName = e.detail.value.trim()
+      }
+    },
+    onNameInput(e) {
+      if (e.detail && typeof e.detail.value === 'string') {
+        this.form.adminName = e.detail.value.trim()
+      }
+    },
+		getSj(e){
+			let that = this
+			let currentMiniOpenId = uni.getStorageSync('currentMiniOpenId')
+			if (e.detail.errMsg === "getPhoneNumber:ok") {
+				getMobile({ iv: e.detail.iv, encryptedData: e.detail.encryptedData,miniOpenId:currentMiniOpenId }).then(subRes => {
+					uni.hideLoading()
+          if(subRes.data.mobile && subRes.data.mobile!=''){
+            this.form.mobile = subRes.data.mobile
+            this.fillMobile = 0
+            this.form.authCode = ''
+            this.checkShopFn({detail:{value:this.form.mobile}})
+          }else{
+            this.$msg("没获取到手机号")
+          }
+				});
+			} else {
+				this.$msg("获取失败")
+			}
+		},
+    checkShopFn(val){
+      // #ifdef MP-WEIXIN
+      let that = this
+      checkShop({mobile:val.detail.value}).then(res=>{
+          if(res.code == 1){
+            if(res.data.has == 1){
+                that.$util.confirmModal({content:'此手机号已注册(可能由批发店添加),您可直接登录',okText:'去登录'},() => {
+                  that.pageTo({url: '/admin/home/login'})
+                })
+            }
+          }
+      })
+      // #endif
+    },
+		goToService(){
+			this.$util.pageTo({ url: "/admin/home/register"})
+		},
+		goToPrivacy(){
+			this.$util.pageTo({ url: "/admin/home/privacy"})
+		},
+		agreeItem(e){
+			this.agree = e.detail.value && e.detail.value.length > 0 ? 1 : 0
+		},
+    requestAuthCode(){
+      let that = this
+      let mobile = this.form.mobile
+      if(this.$util.checkMobile(mobile) == false){
+        this.$msg('请输入正确手机号')
+        return
+      }
+      getAuthCode({mobile}).then(res=>{
+        if(res.code == 1){
+          that.hasGet = true
+          that.count = 120
+          that.INT = setInterval(function(){
+            if(that.count <= 0){
+              clearInterval(that.INT)
+              that.hasGet = false
+              that.count = 120
+              return
+            }
+            that.count--
+          },1000)
+        }
+      })
+    },
+    resultData (val) {
+      this.form.license = val.shortUrl
+    },
+    _resultData (val) {
+      this.form.logo = val.shortUrl
+    },
+    changeMain (val) {
+      this.form.main = val
+    },
+    bindInfo (val) {
+      this.form.adminName = val.name;
+      this.form.mobile = val.mobile
+    },
+    init () {
+      this._getMerchantDet()
+    },
+    _getMerchantDet () {
+      getIntroduce(this.option).then(res => {
+        this.merchantData = res.data.introduce
+      });
+    },
+    openAddres () {
+      this.$refs.simpleAddress.open();
+    },
+    onCityConfirm (e) {
+      this.form.province = e.provinceName;
+      this.form.city = e.cityName;
+      this.addressInitialList = []
+      this.modifyShow()
+    },
+    imgUploadSuccess(res) {
+      var _res = JSON.parse(res.data)
+      if(_res.code == 1){
+        this.currentImgData.push(_res.data.smallUrl)
+        this.form.cover = _res.data.shortUrl
+      }
+    },
+    imgDeleteFn(res){
+      this.form.cover=''
+    },
+    selectRegion() {
+      if (!this.form.city) {
+        this.$msg('请先选择城市或点击定位图标')
+        return false
+      }
+      this.openAddressSelector()
+    },
+    async openAddressSelector() {
+      if (!this.addressInitialList || this.addressInitialList.length === 0) {
+        await this.loadNearbyAddressList(false)
+      }
+      this.showRegion = true
+    },
+    changeAreaFn(e) {
+      this.applyNearbyAddress(e)
+    },
+    modifyShow(){
+			this.form.showAddress = this.form.city + this.form.address + this.form.floor
+		},
+    formSubmit (e) {
+      if (this.hasShop) {
+        this.formSubmitPayOnly()
+        return
+      }
+      let rules = [
+        { name: "name", rule: ["required"], msg: ["请填写名称"] },
+        { name: "city", rule: ["required"], msg: ["请选择城市"] },
+        { name: "address", rule: ["required"], msg: ["请填写地址"] },
+        { name: "adminName", rule: ["required"], msg: ["请填写姓名"] },
+        { name: "mobile", rule: ["required"], msg: ["请填写手机号"] }
+      ];
+      let formData = this.form;
+      let checkRes = form.validation(formData, rules)
+
+      if(this.agree == 0){
+        this.$util.confirmModal({
+          content:'请阅读并同意服务协议和隐私政策',
+          okText:'同意',
+          cancelText:'不同意'
+        },() => {
+          this.agree = 1
+          if (!checkRes) {
+            if (this.fillMobile == 1 && !this.form.authCode) {
+              this.$msg('请填写验证码')
+              return false
+            }
+            uni.showLoading({mask:true})
+            setTimeout(() => {
+              this.confirmFn()
+            })
+            uni.hideLoading()
+          } else {
+            this.$msg(checkRes);
+          }
+        })
+        return false
+      }
+
+      if (!checkRes) {
+        if (this.fillMobile == 1 && !this.form.authCode) {
+          this.$msg('请填写验证码')
+          return false
+        }
+        uni.showLoading({mask:true})
+        setTimeout(() => {
+          this.confirmFn()
+        })
+        uni.hideLoading()
+      } else {
+        this.$msg(checkRes);
+      }
+    },
+    /** 已开店:仅校验协议与邀请码后支付 */
+    formSubmitPayOnly () {
+      const inviteCode = (this.form.inviteCode || '').trim()
+      if (inviteCode !== '' && this.feePreview.inviteNotFound) {
+        this.$msg('邀请人不存在')
+        return false
+      }
+      if (this.agree == 0) {
+        this.$util.confirmModal({
+          content: '请阅读并同意服务协议和隐私政策',
+          okText: '同意',
+          cancelText: '不同意'
+        }, () => {
+          this.agree = 1
+          this.confirmPayOnly()
+        })
+        return false
+      }
+      this.confirmPayOnly()
+    },
+    /** 已开店:保存邀请码并唤起支付 */
+    confirmPayOnly () {
+      const that = this
+      const inviteCode = (this.form.inviteCode || '').trim()
+      if (inviteCode !== '' && this.feePreview.inviteNotFound) {
+        this.$msg('邀请人不存在')
+        return false
+      }
+      uni.showLoading({ mask: true })
+      saveRegisterInvite({ inviteCode }).then(res => {
+        if (res.code != 1 || that.$util.isEmpty(res.data)) {
+          uni.hideLoading()
+          that.$msg(res.msg || '出错了')
+          return false
+        }
+        const d = res.data
+        that.applyId = d.applyId || that.applyId
+        that.feePreview = { ...that.feePreview, ...d }
+        that.startPayFlow(that.applyId)
+      }).catch(() => {
+        uni.hideLoading()
+      })
+    },
+    /** 本页创建续费订单并唤起微信支付 */
+    startPayFlow (applyId) {
+      const that = this
+      const currentMiniOpenId = uni.getStorageSync('currentMiniOpenId')
+      const payAmount = parseFloat(that.feePreview.payAmount)
+      if (isNaN(payAmount) || payAmount <= 0) {
+        uni.hideLoading()
+        that.handlePayComplete()
+        return
+      }
+      uni.showLoading({ title: '正在创建订单', mask: true })
+      renewCreate({
+        applyId: applyId || 0,
+        miniOpenId: currentMiniOpenId
+      }).then(payRes => {
+        uni.hideLoading()
+        if (payRes.code != 1 || that.$util.isEmpty(payRes.data)) {
+          that.$msg(payRes.msg || '创建订单失败')
+          return
+        }
+        const payData = payRes.data
+        if (payData.needPay == 0 || payData.paid == 1) {
+          that.handlePayComplete()
+          return
+        }
+        // #ifdef MP-WEIXIN || H5
+        uni.showLoading({ title: '正在调起支付', mask: true })
+        wexinPay(payData, () => {
+          uni.hideLoading()
+          that.handlePayComplete()
+        }, () => {
+          uni.hideLoading()
+          that.$msg('支付已取消')
+        })
+        // #endif
+        // #ifdef APP-PLUS
+        that.$msg('请在微信小程序内完成会员支付')
+        // #endif
+      }).catch(() => {
+        uni.hideLoading()
+        that.$msg('创建订单失败')
+      })
+    },
+    /** 支付/0 元开通完成:本页提示后返回,不跳转结果页 */
+    handlePayComplete () {
+      const that = this
+      tryBindPendingInvite().then(() => {
+        uni.showToast({ title: '开通成功', icon: 'success', duration: 2000 })
+        setTimeout(() => {
+          const pages = getCurrentPages()
+          if (pages.length > 1) {
+            uni.navigateBack()
+          } else {
+            that.$util.pageTo({ url: '/admin/cg/me', type: 2 })
+          }
+        }, 1500)
+      })
+    },
+    /** 写入登录态(不跳转) */
+    setupLoginState (data) {
+      const that = this
+      const token = data.token || ''
+      if (that.$util.isEmpty(token)) {
+        return false
+      }
+      uni.setStorageSync('token', token)
+      uni.setStorageSync('shopId', (data.admin && data.admin.currentShopId) || data.shopId)
+      getDictionaries({ token }).then(res => {
+        if (that.$util.isEmpty(res)) {
+          return false
+        }
+        that.$store.commit('setDictionariesInfo', { ...res.data.dict, ...res.data })
+        that.$store.commit('setMyShopInfo', { ...res.data.shop, ...res.data })
+      })
+      that.$store.commit('setLoginInfo', { ...data.admin, ...data })
+      getShopInfo(true).then(res => {
+        uni.setNavigationBarTitle({ title: res.name })
+      })
+      that.setUserShopAll()
+      // #ifdef APP-PLUS
+      that.$util.afterAppLoginSuccess({ auto: 0 })
+      // #endif
+      return true
+    },
+    /** 注册完成后的页面跳转 */
+    goAfterRegister () {
+      const that = this
+      const registerInviteQuery = getGhsProductQueryFromPending()
+      const navigate = () => {
+        if (this.option.fromType && this.option.fromType == 2) {
+          this.$util.pageTo({ url: '/admin/ll/list', type: 2 })
+          return
+        }
+        if (this.option.itemId && Number(this.option.itemId) > 0 && this.option.ghsShopId && Number(this.option.ghsShopId) > 0) {
+          ghsInfo({ shopId: this.option.ghsShopId }).then(resInfo => {
+            const ghsId = resInfo.data.id
+            if (Number(ghsId) > 0) {
+              that.$util.pageTo({ url: '/admin/ghsProduct/detail?id=' + that.option.itemId + '&ghsId=' + ghsId, type: 2 })
+            }
+          })
+        } else if (registerInviteQuery) {
+          this.$util.pageTo({ url: '/pagesClient/official/result', query: registerInviteQuery, type: 2 })
+        } else {
+          this.$util.pageTo({ url: '/pagesClient/official/result', type: 2 })
+        }
+      }
+      tryBindPendingInvite().then(() => {
+        navigate()
+      })
+    },
+    /** 注册成功后的登录与跳转(免支付) */
+    handleRegisterLogin (data) {
+      if (!this.setupLoginState(data)) {
+        this.$msg('出错了')
+        return false
+      }
+      this.goAfterRegister()
+    },
+    confirmFn () {
+      restorePendingInviteFromLegacy()
+      const inviteParams = getRegisterInviteParams(this.option)
+      const inviteCode = (this.form.inviteCode || '').trim()
+      if (inviteCode !== '' && this.feePreview.inviteNotFound) {
+        this.$msg('邀请人不存在')
+        return false
+      }
+      uni.showLoading({ mask: true })
+      const currentMiniOpenId = uni.getStorageSync('currentMiniOpenId')
+      const style = this.option.style || 0
+      const from = this.option.from || 0
+      const that = this
+      this.form.showAddress = this.form.city + this.form.address + this.form.floor
+      let appRegister = 0
+      // #ifdef APP-PLUS
+      appRegister = 1
+      // #endif
+      applyRegister({
+        hdShopAdminId: inviteParams.hdShopAdminId,
+        ghsShopAdminId: inviteParams.ghsShopAdminId,
+        inviteGhsShopId: inviteParams.inviteGhsShopId,
+        inviteCode,
+        ...this.form,
+        style,
+        from,
+        miniOpenId: currentMiniOpenId,
+        appRegister,
+        fillMobile: this.fillMobile
+      }).then(subRes => {
+        uni.hideLoading()
+        if (subRes.code != 1 || that.$util.isEmpty(subRes.data)) {
+          that.$msg(subRes.msg || '出错了')
+          return false
+        }
+        const regData = subRes.data
+        if (!that.setupLoginState(regData)) {
+          that.$msg('出错了')
+          return false
+        }
+        const applyId = regData.applyId || 0
+        that.startPayFlow(applyId)
+      }).catch(() => {
+        uni.hideLoading()
+      })
+    }
+  },
+  watch: {
+  }
+};
+</script>
+<style lang="scss" scoped>
+.apply-page {
+  min-height: 100vh;
+  background: #f2f7f4;
+}
+
+.apply-body {
+  padding: 24upx 24upx 48upx;
+}
+
+.member-banner {
+  position: relative;
+  min-height: 200upx;
+  margin-bottom: 24upx;
+  padding: 32upx 28upx;
+  border-radius: 20upx;
+  overflow: hidden;
+  background: #f4faf6;
+  box-shadow: 0 8upx 24upx rgba(9, 197, 103, 0.08);
+}
+
+.member-banner-bg {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 0;
+}
+
+.member-banner-text {
+  position: relative;
+  z-index: 1;
+  max-width: 420upx;
+}
+
+.member-banner-title {
+  font-size: 36upx;
+  font-weight: 800;
+  color: #161819;
+  line-height: 50upx;
+}
+
+.member-banner-price-row {
+  display: flex;
+  align-items: baseline;
+  margin-top: 24upx;
+}
+
+.member-banner-yen {
+  font-size: 36upx;
+  font-weight: 700;
+  color: #09c567;
+}
+
+.member-banner-price {
+  margin-left: 4upx;
+  font-size: 56upx;
+  font-weight: 800;
+  color: #09c567;
+  line-height: 1;
+}
+
+.member-banner-unit {
+  margin-left: 4upx;
+  font-size: 28upx;
+  color: #6b737c;
+}
+
+.fee-card {
+  margin-bottom: 24upx;
+  padding: 8upx 32upx;
+}
+
+.fee-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  min-height: 88upx;
+  padding: 12upx 0;
+}
+
+.fee-row--border {
+  border-top: 1upx solid #eef1f5;
+}
+
+.fee-label {
+  flex-shrink: 0;
+  color: #5c6673;
+  font-size: 28upx;
+}
+
+.fee-label--discount {
+  color: #09c567;
+}
+
+.fee-input {
+  flex: 1;
+  min-width: 0;
+  margin-left: 24upx;
+  text-align: right;
+  font-size: 28upx;
+  color: #161819;
+}
+
+.fee-value {
+  color: #161819;
+  font-size: 28upx;
+  font-weight: 600;
+}
+
+.fee-value--discount {
+  color: #09c567;
+}
+
+.fee-value--pay {
+  color: #09c567;
+  font-size: 32upx;
+  font-weight: 700;
+}
+
+.apply-section {
+  margin-bottom: 24upx;
+}
+
+.apply-section-title {
+  margin-bottom: 16upx;
+  padding-left: 8upx;
+  color: #161819;
+  font-size: 30upx;
+  font-weight: 700;
+  line-height: 42upx;
+}
+
+.apply-card {
+  margin-bottom: 0;
+  border-radius: 20upx;
+  overflow: hidden;
+  box-shadow: 0 8upx 24upx rgba(9, 197, 103, 0.08);
+}
+
+.module-com {
+  background-color: #fff;
+}
+
+.apply-mode-btns {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex: 1;
+  min-width: 0;
+  height: 64upx;
+}
+
+.apply-mode-btn {
+  flex: 1;
+  min-width: 0;
+  height: 64upx;
+  box-sizing: border-box;
+  margin: 0;
+  padding: 0 24upx;
+  line-height: 64upx;
+  text-align: center;
+  color: #6b737c;
+  font-size: 26upx;
+  font-weight: 600;
+  border-radius: 999upx;
+  background: #f2f7f4;
+  border: 1upx solid #e3ebe6;
+}
+
+.apply-mode-btn--last {
+  margin-left: 16upx;
+}
+
+.apply-mode-btn--active {
+  color: #fff;
+  background: #09c567;
+  border-color: #09c567;
+  font-weight: 600;
+}
+
+.line-cell--city {
+  .city-row-main {
+    flex: 1;
+    min-width: 0;
+  }
+
+  .city-locate-btn {
+    flex-shrink: 0;
+    width: 72upx;
+    height: 72upx;
+    margin-left: 12upx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 50%;
+    background: #e8f8f0;
+  }
+
+  .city-locate-icon {
+    width: 40upx;
+    height: 40upx;
+    display: block;
+  }
+}
+
+.line-cell {
+  .tui-title {
+    width: 180upx;
+    color: #5c6673;
+    font-size: 28upx;
+  }
+  .tui-input {
+    flex: 1;
+    min-width: 0;
+    width: auto;
+    font-size: 28upx;
+    color: #161819;
+  }
+  .tui-placeholder {
+    flex: 1;
+    min-width: 0;
+    color: #ccc;
+  }
+  .phcolor {
+    color: #ccc;
+  }
+}
+
+.code-wrap {
+  .tui-input--code {
+    margin-right: 16upx;
+  }
+}
+
+.mobile-wrap {
+  .tui-input--filled {
+    color: #161819;
+  }
+}
+
+.apply-fill-btn {
+  min-width: 120upx;
+}
+
+.apply-cancel-btn {
+  min-width: 120upx;
+  color: #6b737c;
+  background: #f2f7f4;
+  border: 1upx solid #e3ebe6;
+}
+
+.apply-code-btn {
+  flex-shrink: 0;
+  min-width: 140upx;
+  height: 64upx;
+  box-sizing: border-box;
+  margin: 0;
+  padding: 0 24upx;
+  line-height: 64upx;
+  border: none;
+  border-radius: 999upx;
+  color: #fff;
+  font-size: 26upx;
+  font-weight: 600;
+  background: #09c567;
+}
+
+.apply-code-btn::after {
+  border: none;
+}
+
+.apply-code-btn--disabled {
+  color: #8a9199;
+  background: #eef1f5;
+}
+
+.apply-agreement {
+  margin-top: 8upx;
+  padding: 0 8upx;
+}
+
+.apply-agreement-label {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.apply-checkbox {
+  transform: scale(0.82);
+  flex-shrink: 0;
+}
+
+.apply-agreement-text {
+  flex: 1;
+  margin-left: 8upx;
+  color: #8a9199;
+  font-size: 26upx;
+  line-height: 36upx;
+}
+
+.apply-agreement-link {
+  color: #09c567;
+  font-weight: 600;
+}
+
+.apply-submit-wrap {
+  margin-top: 40upx;
+  padding: 0 8upx;
+}
+
+.apply-submit-btn {
+  width: 100%;
+  height: 96upx;
+  margin: 0;
+  padding: 0;
+  line-height: 96upx;
+  border: none;
+  border-radius: 999upx;
+  color: #fff;
+  font-size: 32upx;
+  font-weight: 600;
+  text-align: center;
+  background: #09c567;
+  box-shadow: 0 10upx 24upx rgba(9, 197, 103, 0.24);
+}
+
+.apply-submit-btn::after {
+  border: none;
+}
+
+.recommend-wrap {
+  padding: 28upx 32upx;
+  margin-bottom: 24upx;
+  .recommend-tag {
+    font-size: 28upx;
+    color: #5c6673;
+    font-weight: 600;
+    margin-bottom: 20upx;
+  }
+  .recommend-det {
+    @include disFlex(center, flex-start);
+    .recommend-avatar {
+      width: 90upx;
+      height: 90upx;
+      border-radius: 45upx;
+      flex-shrink: 0;
+    }
+    .recommend-info {
+      margin-left: 20upx;
+      .app-size-32 {
+        margin-bottom: 4upx;
+        color: #161819;
+      }
+    }
+  }
+}
+</style>