shizhongqi 2 месяцев назад
Родитель
Сommit
6de3ab29f4

+ 364 - 0
hdApp/src/admin/birthday/board.vue

@@ -0,0 +1,364 @@
+<template>
+  <view class="birthday-board app-content">
+    <view class="top-bar">
+      <view class="action-row">
+        <view class="green-btn" @click="notifyTomorrow">一键通知</view>
+        <view class="green-btn outline" @click="goGiftStats">赠礼统计</view>
+        <view class="search-wrap">
+          <input class="search-inp" v-model="searchText" placeholder="请输入搜索关键字" @confirm="reload" />
+        </view>
+      </view>
+      <view class="filter-row">
+        <text class="filter-label">时间区间</text>
+        <view
+          v-for="p in periodTabs"
+          :key="p.value"
+          class="pill"
+          :class="{ active: period === p.value }"
+          @click="changePeriod(p.value)"
+        >{{ p.name }}</view>
+      </view>
+      <scroll-view scroll-x class="status-tabs">
+        <view
+          v-for="tab in statusTabs"
+          :key="tab.key"
+          class="status-tab"
+          :class="{ active: statusFilter === tab.value }"
+          @click="changeStatus(tab.value)"
+        >
+          <text>{{ tab.name }}</text>
+          <text class="tab-num">{{ counts[tab.key] || 0 }}</text>
+        </view>
+      </scroll-view>
+    </view>
+
+    <view class="list-wrap">
+      <view
+        v-for="(item, index) in list.data"
+        :key="item.id || index"
+        class="gift-card"
+        @click="onCardClick(item)"
+      >
+        <image v-if="item.avatar" class="avatar" :src="item.avatar" mode="aspectFill" />
+        <view v-else class="avatar placeholder" />
+        <view class="card-body">
+          <view class="name-row">
+            <text class="name">{{ item.name }}</text>
+            <text class="mobile">{{ item.mobile }}</text>
+            <image
+              v-if="item.member > 0"
+              class="level-icon"
+              :src="memberBadgeSrc(item.member)"
+              mode="aspectFit"
+            />
+            <text v-if="item.memberName" class="level-name">{{ item.memberName }}</text>
+          </view>
+          <view v-if="item.status === 0" class="line link" @click.stop="fillBirthday(item)">点击填写生日</view>
+          <view v-else class="line">生日:{{ item.birthdayDisplay }}</view>
+          <view class="line gift-line">生日赠礼:{{ item.giftName || '—' }}</view>
+          <view v-if="item.status === 2 && item.pickupTime" class="line sub">预约领取时间:{{ formatTime(item.pickupTime) }}</view>
+          <view v-if="item.status === 3 && item.collectTime" class="line sub">领取时间:{{ formatTime(item.collectTime) }}</view>
+          <view v-if="item.status === 4" class="line sub">预约领取时间:超时自动放弃</view>
+        </view>
+        <view class="status-badge" :class="'st-' + item.status">{{ item.statusName }}</view>
+      </view>
+      <app-wrapper-empty v-if="list.finished && $util.isEmpty(list.data)" title="暂无数据" :is-empty="true" />
+    </view>
+  </view>
+</template>
+
+<script>
+import list from '@/mixins/list'
+import AppWrapperEmpty from '@/components/app-wrapper-empty'
+import { getBoardList, getBoardCounts, notifyTomorrow } from '@/api/birthday'
+import { iconSrc } from '@/utils/iconSrc'
+
+export default {
+  name: 'BirthdayBoard',
+  components: { AppWrapperEmpty },
+  mixins: [list],
+  data() {
+    return {
+      searchText: '',
+      period: 'week',
+      statusFilter: -1,
+      periodTabs: [
+        { name: '今日', value: 'today' },
+        { name: '明日', value: 'tomorrow' },
+        { name: '近一周', value: 'week' }
+      ],
+      statusTabs: [
+        { name: '全部', key: 'all', value: -1 },
+        { name: '待通知', key: 'pendingNotify', value: 1 },
+        { name: '待领取', key: 'pendingClaim', value: 2 },
+        { name: '已领取', key: 'collected', value: 3 },
+        { name: '未填写', key: 'noBirthday', value: 0 },
+        { name: '超时放弃', key: 'expired', value: 4 }
+      ],
+      counts: {}
+    }
+  },
+  methods: {
+    iconSrc,
+    memberBadgeSrc(member) {
+      const m = Number(member)
+      if (m >= 1 && m <= 5) {
+        return `/static/member-icons/member-${m}.svg`
+      }
+      return ''
+    },
+    formatTime(t) {
+      if (!t) return ''
+      return String(t).replace(/-/g, '.').substring(0, 16)
+    },
+    init() {
+      this.loadCounts()
+      this.resetList()
+    },
+    loadCounts() {
+      getBoardCounts({ period: this.period }).then(res => {
+        if (res.code === 1) {
+          this.counts = res.data || {}
+        }
+      })
+    },
+    changePeriod(v) {
+      this.period = v
+      this.loadCounts()
+      this.resetList()
+    },
+    changeStatus(v) {
+      this.statusFilter = v
+      this.resetList()
+    },
+    reload() {
+      this.loadCounts()
+      this.resetList()
+    },
+    resetList() {
+      this.list.page = 1
+      this.list.data = []
+      this.list.finished = false
+      this.loadMore()
+    },
+    loadMore() {
+      if (this.list.finished) return
+      getBoardList({
+        page: this.list.page,
+        pageSize: 20,
+        period: this.period,
+        status: this.statusFilter,
+        searchText: this.searchText
+      }).then(res => {
+        if (res.code === 1) {
+          const d = res.data || {}
+          const listArr = d.list || []
+          this.list.loading = false
+          this.list.data = this.list.page === 1 ? listArr : this.list.data.concat(listArr)
+          this.list.finished = !!d.finished
+          if (!d.finished) {
+            this.list.page += 1
+          }
+        }
+      })
+    },
+    notifyTomorrow() {
+      this.$util.confirmModal({ content: '确认为明天生日的客户发送领取短信?' }, () => {
+        notifyTomorrow().then(res => {
+          if (res.code === 1) {
+            this.$msg(res.msg || '操作成功')
+            this.reload()
+          }
+        })
+      })
+    },
+    goGiftStats() {
+      this.$util.pageTo({ url: '/admin/birthday/giftStats', query: { period: this.period } })
+    },
+    fillBirthday(item) {
+      this.$util.pageTo({ url: '/admin/member/detail', query: { id: item.customId } })
+    },
+    onCardClick(item) {
+      if (item.status === 0) {
+        this.fillBirthday(item)
+        return
+      }
+      this.$util.pageTo({ url: '/admin/member/detail', query: { id: item.customId } })
+    }
+  },
+  onPullDownRefresh() {
+    this.reload()
+    uni.stopPullDownRefresh()
+  },
+  onReachBottom() {
+    this.loadMore()
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.birthday-board {
+  min-height: 100vh;
+  background: #f5f6f7;
+  padding-bottom: 40upx;
+}
+.top-bar {
+  background: #fff;
+  padding: 20upx 24upx 0;
+  position: sticky;
+  top: 0;
+  z-index: 10;
+}
+.action-row {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 12upx;
+}
+.green-btn {
+  padding: 12upx 24upx;
+  background: linear-gradient(135deg, #19d471, #08b955);
+  color: #fff;
+  font-size: 26upx;
+  border-radius: 8upx;
+}
+.green-btn.outline {
+  background: #fff;
+  color: #08b955;
+  border: 1upx solid #08b955;
+}
+.search-wrap {
+  flex: 1;
+  min-width: 200upx;
+}
+.search-inp {
+  height: 64upx;
+  background: #f1f3f2;
+  border-radius: 32upx;
+  padding: 0 24upx;
+  font-size: 26upx;
+}
+.filter-row {
+  display: flex;
+  align-items: center;
+  margin-top: 20upx;
+  flex-wrap: wrap;
+}
+.filter-label {
+  font-size: 26upx;
+  color: #666;
+  margin-right: 12upx;
+}
+.pill {
+  padding: 8upx 20upx;
+  margin-right: 12upx;
+  font-size: 24upx;
+  border-radius: 24upx;
+  background: #e8f8ef;
+  color: #333;
+}
+.pill.active {
+  background: #08b955;
+  color: #fff;
+}
+.status-tabs {
+  white-space: nowrap;
+  margin-top: 16upx;
+  padding-bottom: 12upx;
+}
+.status-tab {
+  display: inline-flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 12upx 20upx;
+  font-size: 24upx;
+  color: #666;
+}
+.status-tab.active {
+  color: #08b955;
+  font-weight: 600;
+}
+.tab-num {
+  font-size: 22upx;
+  margin-top: 4upx;
+}
+.list-wrap {
+  padding: 16upx 24upx;
+}
+.gift-card {
+  display: flex;
+  background: #fff;
+  border-radius: 16upx;
+  padding: 24upx;
+  margin-bottom: 20upx;
+  position: relative;
+}
+.avatar {
+  width: 96upx;
+  height: 96upx;
+  border-radius: 50%;
+  flex-shrink: 0;
+  background: #eee;
+}
+.avatar.placeholder {
+  background: #e0e0e0;
+}
+.card-body {
+  flex: 1;
+  margin-left: 20upx;
+  min-width: 0;
+}
+.name-row {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8upx;
+}
+.name {
+  font-size: 30upx;
+  font-weight: 600;
+}
+.mobile {
+  font-size: 24upx;
+  color: #888;
+}
+.level-icon {
+  width: 40upx;
+  height: 40upx;
+}
+.level-name {
+  font-size: 22upx;
+  color: #08b955;
+  background: #e8f8ef;
+  padding: 2upx 10upx;
+  border-radius: 8upx;
+}
+.line {
+  font-size: 24upx;
+  color: #666;
+  margin-top: 8upx;
+}
+.line.link {
+  color: #1989fa;
+}
+.line.sub {
+  color: #999;
+  font-size: 22upx;
+}
+.gift-line {
+  color: #333;
+}
+.status-badge {
+  position: absolute;
+  right: 250upx;
+  top: 110upx;
+  font-size: 22upx;
+  padding: 4upx 12upx;
+  border-radius: 8upx;
+}
+.st-1 { background: #ffe8e8; color: #e54; }
+.st-2 { background: #e8f0ff; color: #36c; }
+.st-3 { background: #eee; color: #888; }
+.st-0 { background: #fff3e0; color: #f90; }
+.st-4 { background: #ddd; color: #555; }
+</style>

+ 151 - 0
hdApp/src/admin/birthday/giftStats.vue

@@ -0,0 +1,151 @@
+<template>
+  <view class="gift-stats app-content">
+    <view class="period-row">
+      <text class="label">时间周期</text>
+      <view
+        v-for="p in periodTabs"
+        :key="p.value"
+        class="pill"
+        :class="{ active: period === p.value }"
+        @click="changePeriod(p.value)"
+      >{{ p.name }}</view>
+    </view>
+
+    <view class="table-block">
+      <view class="block-title">待领取生日赠礼</view>
+      <view class="table">
+        <view class="tr head">
+          <view class="td">生日赠礼名称</view>
+          <view class="td num">人数</view>
+        </view>
+        <view v-for="(row, i) in pending" :key="'p' + i" class="tr">
+          <view class="td">{{ row.giftName }}</view>
+          <view class="td num">{{ row.num }}</view>
+        </view>
+        <view v-if="!pending.length" class="empty">暂无数据</view>
+      </view>
+    </view>
+
+    <view class="table-block">
+      <view class="block-title">已领取生日赠礼</view>
+      <view class="table">
+        <view class="tr head">
+          <view class="td">生日赠礼名称</view>
+          <view class="td num">人数</view>
+        </view>
+        <view v-for="(row, i) in collected" :key="'c' + i" class="tr">
+          <view class="td">{{ row.giftName }}</view>
+          <view class="td num">{{ row.num }}</view>
+        </view>
+        <view v-if="!collected.length" class="empty">暂无数据</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getGiftStats } from '@/api/birthday'
+
+export default {
+  name: 'BirthdayGiftStats',
+  data() {
+    return {
+      period: 'today',
+      periodTabs: [
+        { name: '今日', value: 'today' },
+        { name: '全部', value: 'all' },
+        { name: '明日', value: 'tomorrow' },
+        { name: '近一周', value: 'week' }
+      ],
+      pending: [],
+      collected: []
+    }
+  },
+  onLoad(query) {
+    if (query.period) {
+      this.period = query.period
+    }
+    this.loadData()
+  },
+  methods: {
+    changePeriod(v) {
+      this.period = v
+      this.loadData()
+    },
+    loadData() {
+      getGiftStats({ period: this.period }).then(res => {
+        if (res.code === 1 && res.data) {
+          this.pending = res.data.pending || []
+          this.collected = res.data.collected || []
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.gift-stats {
+  min-height: 100vh;
+  background: #f5f6f7;
+  padding: 24upx;
+}
+.period-row {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  background: #fff;
+  padding: 24upx;
+  border-radius: 12upx;
+  margin-bottom: 24upx;
+}
+.label {
+  font-size: 28upx;
+  margin-right: 16upx;
+}
+.pill {
+  padding: 10upx 24upx;
+  margin-right: 12upx;
+  font-size: 26upx;
+  border-radius: 8upx;
+  background: #e8f8ef;
+}
+.pill.active {
+  background: #08b955;
+  color: #fff;
+}
+.table-block {
+  background: #fff;
+  border-radius: 12upx;
+  padding: 24upx;
+  margin-bottom: 24upx;
+}
+.block-title {
+  font-size: 30upx;
+  font-weight: 600;
+  margin-bottom: 20upx;
+}
+.tr {
+  display: flex;
+  border-bottom: 1upx solid #eee;
+  padding: 16upx 0;
+}
+.tr.head {
+  font-weight: 600;
+  color: #333;
+}
+.td {
+  flex: 1;
+  font-size: 26upx;
+}
+.td.num {
+  flex: 0 0 120upx;
+  text-align: right;
+}
+.empty {
+  text-align: center;
+  color: #999;
+  padding: 40upx;
+  font-size: 26upx;
+}
+</style>

+ 64 - 2
hdApp/src/admin/home/components/mall-home-panel.vue

@@ -93,6 +93,19 @@
         </view>
       </view>
 
+      <view class="module-com birthday-entry-wrap" @click="goBirthdayBoard">
+        <view class="birthday-entry-row">
+          <view class="birthday-entry-left">
+            <text class="birthday-entry-icon">🎂</text>
+            <text class="birthday-entry-title">最近生日</text>
+            <text class="birthday-entry-stat">今日 {{ birthdaySummary.today || 0 }} 人</text>
+            <text class="birthday-entry-stat">明日 {{ birthdaySummary.tomorrow || 0 }} 人</text>
+            <text class="birthday-entry-stat">近一周 {{ birthdaySummary.week || 0 }} 人</text>
+          </view>
+          <text class="birthday-entry-arrow">›</text>
+        </view>
+      </view>
+
       <view class="home-card">
         <view class="section-header">
           <view class="header-title"><view class="title-inline-icon"><image class="title-icon-img" :src="iconSrc('bouquet')" mode="aspectFit" /></view><text>花束</text></view>
@@ -228,6 +241,7 @@ import { consoleIndex } from "@/api/workbench";
 import autoUpdateMixins from "@/mixins/autoUpdate";
 import { currentShop } from "@/api/shop";
 import { unReadMsgCount } from "@/api/chat";
+import { getWorkbenchSummary } from '@/api/birthday';
 import {
   HOME_MENU_DEFAULT_KEYS,
   HOME_MENU_STORAGE_KEYS,
@@ -279,7 +293,8 @@ export default {
       menuIconSize: 32,
       moreIconSize: 22,
       editorIconSize: 20,
-      editorSectionIconSize: 24
+      editorSectionIconSize: 24,
+      birthdaySummary: { today: 0, tomorrow: 0, week: 0 }
     };
   },
   computed: {
@@ -444,8 +459,13 @@ export default {
           this.overview = res.data.overview
         }
       })
+      const p3 = getWorkbenchSummary().then(res => {
+        if (res.code == 1 && res.data) {
+          this.birthdaySummary = res.data
+        }
+      })
       this.getUnReadMsgCount()
-      return Promise.all([p1, p2])
+      return Promise.all([p1, p2, p3])
     },
     showShopName() {
 			currentShop().then(res => {
@@ -466,6 +486,9 @@ export default {
     noticeFn (item) {
       this.$util.pageTo({ url: item.page })
     },
+    goBirthdayBoard() {
+      this.$util.pageTo({ url: '/admin/birthday/board' })
+    },
     getUnReadMsgCount(){
       unReadMsgCount().then(res=>{
         if(res.code == 1){
@@ -720,6 +743,45 @@ export default {
   background: rgba(255, 255, 255, 0.32);
 }
 // 消息
+.birthday-entry-wrap {
+  margin: 24upx 24upx 0;
+  border-radius: 18upx;
+  overflow: hidden;
+}
+.birthday-entry-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 28upx 24upx;
+  background: #fff;
+}
+.birthday-entry-left {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  flex: 1;
+  min-width: 0;
+  font-size: 26upx;
+  color: #333;
+}
+.birthday-entry-icon {
+  font-size: 36upx;
+  margin-right: 12upx;
+}
+.birthday-entry-title {
+  font-weight: 600;
+  margin-right: 16upx;
+}
+.birthday-entry-stat {
+  color: #666;
+  margin-right: 12upx;
+}
+.birthday-entry-arrow {
+  font-size: 40upx;
+  color: #ccc;
+  line-height: 1;
+}
+
 .news-wrap {
   overflow: hidden;
   margin: 24upx 24upx 0;

+ 235 - 389
hdApp/src/admin/member/level.vue

@@ -1,65 +1,60 @@
 <template>
-	<view class="app-content">
-		<!-- 页面标题 -->
+	<view class="app-content level-page">
 		<view class="page-header">
-			<view class="page-title">会员等级</view>
-			<view class="page-subtitle">充值或消费1元获得一个成长值</view>
-			<view class="page-subtitle">欠款充值没有成长值</view>
+			<view class="page-title">会员等级设置</view>
+			<view class="page-subtitle">设置不同会员等级的消费/充值金额标准</view>
+			<view class="title-decor"></view>
 		</view>
 
-		<!-- 主要内容区域 -->
 		<view class="main-content">
-			<!-- 会员等级表格 -->
-			<view class="table-container">
-				<t-table :headerBackgroundColor="'#f0f9f0'">
-					<view>
-						<t-tr header>
-							<t-th width="20%">
-								<view class="table-header">图标</view>
-							</t-th>
-							<t-th width="30%">
-								<view class="table-header">等级</view>
-							</t-th>
-							<t-th width="50%">
-								<view class="table-header">成长值</view>
-							</t-th>
-						</t-tr>
+			<view class="table-head">
+				<view class="col-icon">图标</view>
+				<view class="col-level">等级</view>
+				<view class="col-amount">金额(元)</view>
+				<view class="col-discount">折扣</view>
+			</view>
+			<view class="level-card" v-for="(item, index) in map" :key="index">
+				<view class="card-top">
+					<view class="member-icon col-icon">
+						<image v-if="item.member == 1" class="member-badge-icon" src="@/static/member-icons/member-1.svg" mode="aspectFit" />
+						<image v-else-if="item.member == 2" class="member-badge-icon" src="@/static/member-icons/member-2.svg" mode="aspectFit" />
+						<image v-else-if="item.member == 3" class="member-badge-icon" src="@/static/member-icons/member-3.svg" mode="aspectFit" />
+						<image v-else-if="item.member == 4" class="member-badge-icon" src="@/static/member-icons/member-4.svg" mode="aspectFit" />
+						<image v-else-if="item.member == 5" class="member-badge-icon" src="@/static/member-icons/member-5.svg" mode="aspectFit" />
+					</view>
+					<view class="col-level">
+						<view class="level-name">{{ item.name }}</view>
+					</view>
+					<view class="field-inline col-amount">
+						<input v-model="item.amount" type="digit" class="field-inp" placeholder="金额" @focus="() => clearDiscount(index)" />
 					</view>
-					<view>
-						<view v-for="(item, inIndex) in map" :key="inIndex">
-							<t-tr>
-								<t-td align="center" color="info" width="20%">
-									<view class="member-icon">
-										<image v-if="item.member == 1" class="member-badge-icon" style="width:54upx;height:54upx;" src="@/static/member-icons/member-1.svg" mode="aspectFit"></image>
-										<image v-else-if="item.member == 2" class="member-badge-icon" style="width:52upx;height:52upx;" src="@/static/member-icons/member-2.svg" mode="aspectFit"></image>
-										<image v-else-if="item.member == 3" class="member-badge-icon" style="width:51upx;height:51upx;" src="@/static/member-icons/member-3.svg" mode="aspectFit"></image>
-										<image v-else-if="item.member == 4" class="member-badge-icon" style="width:57upx;height:57upx;" src="@/static/member-icons/member-4.svg" mode="aspectFit"></image>
-										<image v-else-if="item.member == 5" class="member-badge-icon" style="width:54upx;height:54upx;" src="@/static/member-icons/member-5.svg" mode="aspectFit"></image>
-									</view>
-								</t-td>
-								<t-td align="center" color="info" width="30%">
-									<view class="level-name">{{ item.name }}</view>
-								</t-td>
-								<t-td align="center" color="info" width="50%">
-									<view class="input-wrapper">
-										<input
-											v-model="item.amount"
-											type="digit"
-											class="amount-input"
-											@focus="() => clearDiscount(inIndex)"
-											placeholder="请填写消费金额"
-										/>
-									</view>
-								</t-td>
-							</t-tr>
-						</view>
+					<view class="field-inline col-discount">
+						<input v-model="item.discount" type="digit" class="field-inp short" placeholder="如9.5" />
 					</view>
-				</t-table>
+				</view>
+				<view class="card-section">
+					<view class="section-label">生日权益</view>
+					<input
+						v-model="item.birthdayBenefit"
+						class="section-inp"
+						maxlength="50"
+						placeholder="请输入生日权益内容"
+					/>
+					<view class="char-count">{{ (item.birthdayBenefit || '').length }}/50</view>
+				</view>
+				<view class="card-section">
+					<view class="section-label">会员权益说明</view>
+					<textarea
+						v-model="item.benefitDesc"
+						class="section-textarea"
+						maxlength="500"
+						placeholder="请输入会员权益说明内容"
+					/>
+					<view class="char-count">{{ (item.benefitDesc || '').length }}/500</view>
+				</view>
 			</view>
-
 		</view>
 
-		<!-- 底部按钮 -->
 		<view class="app-footer">
 			<view class="footer-btn admin-button-com big default" style="border:1upx solid #CCCCCC;" @click="goBack()">返回上页</view>
 			<view class="footer-btn admin-button-com big blue" @click="saveData">提交修改</view>
@@ -67,38 +62,35 @@
 	</view>
 </template>
 <script>
-import AppWrapperEmpty from "@/components/app-wrapper-empty";
 import list from '@/mixins/list';
-import ModalModule from "@/components/plugin/modal";
 import { getLevelInitData, modifyLevel } from '@/api/member';
 
 export default {
 	name: "MemberLevel",
-	components: {
-		AppWrapperEmpty,
-		ModalModule
-	},
 	mixins: [list],
 	data() {
 		return {
-			// 会员等级数据
 			map: [],
-			// 降级天数设置
 			reduceDay: 30
 		};
 	},
 	methods: {
-		/**
-		 * 初始化页面数据
-		 */
 		init() {
 			getLevelInitData().then(res => {
 				if (res.code === 1) {
 					this.map = res.data.member || [];
-					// 为每个等级添加member字段用于显示对应图标
 					this.map.forEach((item, index) => {
 						if (!item.member) {
-							item.member = index + 1; // 等级1-5对应member值1-5
+							item.member = index + 1;
+						}
+						if (item.discount === undefined || item.discount === null || item.discount === '') {
+							item.discount = 10;
+						}
+						if (!item.birthdayBenefit) {
+							item.birthdayBenefit = '';
+						}
+						if (!item.benefitDesc) {
+							item.benefitDesc = '';
 						}
 					});
 					this.reduceDay = res.data.reduceDay || 30;
@@ -108,66 +100,34 @@ export default {
 				this.$msg('获取数据失败,请重试');
 			});
 		},
-
-		/**
-		 * 清空指定等级的消费金额
-		 * @param {number} index - 等级索引
-		 */
 		clearDiscount(index) {
 			this.$set(this.map[index], 'amount', '');
 		},
-
-		/**
-		 * 清空降级天数设置
-		 */
-		clearReduceDay() {
-			this.reduceDay = '';
-		},
-
-		/**
-		 * 返回上一页
-		 */
 		goBack() {
 			uni.navigateBack();
 		},
-
-		/**
-		 * 保存数据
-		 */
 		saveData() {
-			// 数据验证
 			if (!this.validateData()) {
 				return;
 			}
-			const confirmContent = '确认提交?';
-			this.$util.confirmModal({ content: confirmContent }, () => {
-				const requestData = {
+			this.$util.confirmModal({ content: '确认提交?' }, () => {
+				modifyLevel({
 					data: this.map,
 					reduceDay: this.reduceDay
-				};
-				modifyLevel(requestData).then(res => {
+				}).then(res => {
 					if (res.code === 1) {
 						this.$msg(res.msg);
-						setTimeout(() => {
-							uni.navigateBack();
-						}, 900);
+						setTimeout(() => uni.navigateBack(), 900);
 					}
-				})
-			})
+				});
+			});
 		},
-		/**
-		 * 验证表单数据
-		 * @returns {boolean} 验证结果
-		 */
 		validateData() {
-			// 检查是否有空的消费金额
 			const hasEmptyAmount = this.map.some(item => !item.amount || item.amount === '');
 			if (hasEmptyAmount) {
 				this.$msg('请填写完整的消费金额');
 				return false;
 			}
-
-			// 检查消费金额是否为正数
 			const hasInvalidAmount = this.map.some(item => {
 				const amount = parseFloat(item.amount);
 				return isNaN(amount) || amount < 0;
@@ -176,343 +136,229 @@ export default {
 				this.$msg('请输入有效的消费金额');
 				return false;
 			}
-
-			// 检查降级天数
-			if (this.reduceDay !== '' && this.reduceDay !== 0) {
-				const reduceDayNum = parseInt(this.reduceDay);
-				if (isNaN(reduceDayNum) || reduceDayNum < 0) {
-					this.$msg('请输入有效的降级天数');
-					return false;
-				}
+			const hasLongBenefit = this.map.some(item => (item.birthdayBenefit || '').length > 50);
+			if (hasLongBenefit) {
+				this.$msg('生日权益不超过50字');
+				return false;
+			}
+			const hasLongDesc = this.map.some(item => (item.benefitDesc || '').length > 500);
+			if (hasLongDesc) {
+				this.$msg('会员权益说明不超过500字');
+				return false;
 			}
-
 			return true;
 		}
 	}
 };
 </script>
 <style lang="scss" scoped>
-.app-content {
-	background: linear-gradient(135deg, #f0f9f0 0%, #a8e6a8 100%);
-	min-height: 100vh;
-	padding: 0;
-	padding-bottom: 180upx; /* 为底部固定按钮预留足够空间 */
-	display: flex;
-	flex-direction: column;
+.level-page {
 	position: relative;
-	z-index: 1;
+	background: linear-gradient(180deg, #eff9ef 0%, #f8fcf8 44%, #f7fbf7 100%);
+	min-height: 100vh;
+	padding: 0 16upx 160upx;
+	box-sizing: border-box;
+	overflow: hidden;
 }
 
-/* 页面头部 */
 .page-header {
-	background: #fff;
-	padding: 40upx 30upx 30upx;
-	margin-bottom: 30upx;
-	box-shadow: 0 2upx 12upx rgba(0, 0, 0, 0.08);
-	border-radius: 0 0 20upx 20upx;
-	
+	position: relative;
+	z-index: 1;
+	padding: 44upx 30upx 28upx;
 	.page-title {
-		font-size: 44upx;
-		font-weight: 600;
-		color: #2c3e50;
+		font-size: 42upx;
+		font-weight: 700;
+		color: #1f6f40;
 		text-align: center;
-		margin-bottom: 12upx;
+		letter-spacing: 2upx;
 	}
-	
 	.page-subtitle {
-		font-size: 30upx;
-		color: #7f8c8d;
+		font-size: 22upx;
+		color: #a5aaa8;
 		text-align: center;
-		opacity: 0.8;
+		margin-top: 12upx;
+	}
+	.title-decor {
+		width: 10upx;
+		height: 10upx;
+		margin: 18upx auto 0;
+		position: relative;
+		background: #4ec56a;
+		transform: rotate(45deg);
+	}
+	.title-decor::before,
+	.title-decor::after {
+		content: '';
+		position: absolute;
+		top: 4upx;
+		width: 28upx;
+		height: 1upx;
+		background: #d8ead6;
+		transform: rotate(-45deg);
+	}
+	.title-decor::before {
+		right: 24upx;
+	}
+	.title-decor::after {
+		left: 24upx;
 	}
 }
-
-/* 主要内容区域 */
 .main-content {
-	padding: 0 30upx;
-	flex: 1;
-}
-
-/* 表格容器 */
-.table-container {
+	position: relative;
+	z-index: 1;
 	background: #fff;
-	border-radius: 16upx;
-	box-shadow: 0 4upx 20upx rgba(0, 0, 0, 0.08);
-	overflow: hidden;
-	margin-bottom: 40upx;
-	
-	/* 覆盖表格组件默认样式 */
-	:deep(.t-table) {
-		border: none;
-		
-		.t-tr {
-			border-bottom: 1upx solid #e8f5e8;
-			
-			&:last-child {
-				border-bottom: none;
-			}
-		}
-		
-		.t-th, .t-td {
-			padding: 24upx 12upx;
-			border-right: 1upx solid #e8f5e8;
-			
-			&:last-child {
-				border-right: none;
-			}
-		}
-		
-		.t-th:first-child, .t-td:first-child {
-			padding: 24upx 8upx; /* 图标列更紧凑 */
-		}
-		
-		.t-th:last-child, .t-td:last-child {
-			padding: 24upx 16upx; /* 消费金额列稍微宽松一些 */
-		}
-		
-		.t-th {
-			background: #f0f9f0 !important;
-		}
-	}
+	border-radius: 18upx;
+	padding: 0 18upx 2upx;
+	box-shadow: 0 8upx 28upx rgba(35, 121, 63, 0.08);
 }
-
-.table-header {
-	color: #2d5f3f;
-	font-size: 32upx;
+.table-head {
+	display: flex;
+	align-items: center;
+	height: 74upx;
+	font-size: 28upx;
 	font-weight: 600;
+	color: #28884d;
+	border-bottom: 1upx solid #edf3ed;
+	box-sizing: border-box;
+}
+.table-head .col-icon,
+.table-head .col-level,
+.table-head .col-amount,
+.table-head .col-discount {
+	text-align: center;
+}
+.level-card {
+	padding: 22upx 0 24upx;
+	border-bottom: 1upx solid #f1f4f0;
+}
+.level-card:last-child {
+	border-bottom: 0;
+}
+.card-top {
+	display: flex;
+	align-items: center;
+	margin-bottom: 18upx;
+}
+.col-icon {
+	width: 112upx;
+	flex-shrink: 0;
+	text-align: center;
+}
+.col-level {
+	width: 170upx;
+	flex-shrink: 0;
+}
+.col-amount {
+	flex: 1;
+	margin-left: 6upx;
+}
+.col-discount {
+	width: 114upx;
+	flex-shrink: 0;
+	margin-left: 18upx;
 }
-
 .member-icon {
 	display: flex;
 	align-items: center;
 	justify-content: center;
-	flex-shrink: 0;
 }
-
 .member-badge-icon {
-	flex-shrink: 0;
-	filter: drop-shadow(0 2upx 6upx rgba(0,0,0,0.3));
+	width: 92upx;
+	height: 92upx;
 }
-
 .level-name {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	width: 152upx;
+	height: 56upx;
+	background: linear-gradient(90deg, #67d85a, #41b94e);
 	color: #fff;
+	padding: 0 14upx;
+	border-radius: 24upx;
+	font-size: 24upx;
 	font-weight: 600;
-	font-size: 32upx;
-	padding: 8upx 16upx;
-	background: linear-gradient(45deg, #4caf50 0%, #2e7d32 100%);
-	background: -webkit-linear-gradient(45deg, #4caf50 0%, #2e7d32 100%);
-	color: #fff;
-	border-radius: 20upx;
-	display: inline-block;
-	min-width: 120upx;
-	text-align: center;
-	box-shadow: 0 2upx 8upx rgba(76, 175, 80, 0.3);
+	box-shadow: 0 4upx 10upx rgba(67, 183, 78, 0.2);
+	box-sizing: border-box;
 }
-
-.input-wrapper {
-	padding: 8upx;
+.field-inline {
+	display: flex;
+	align-items: center;
 }
-
-.amount-input {
-	width: 200upx;
-	height: 70upx;
-	border: 2upx solid #c8e6c9;
-	border-radius: 12upx;
-	padding: 0 20upx;
-	font-size: 30upx;
-	color: #2d5f3f;
+.field-inp {
+	width: 100%;
+	height: 54upx;
+	line-height: 54upx;
+	border: 1upx solid #edf0ed;
+	border-radius: 8upx;
+	padding: 0 8upx;
+	font-size: 26upx;
+	font-weight: 600;
+	color: #368753;
 	text-align: center;
+	box-sizing: border-box;
 	background: #fff;
-	transition: all 0.3s ease;
-	
-	&:focus {
-		border-color: #4caf50;
-		box-shadow: 0 0 0 6upx rgba(76, 175, 80, 0.15);
-		outline: none;
-	}
-	
-	&::placeholder {
-		color: #81c784;
-		font-size: 26upx;
-	}
-}
-
-/* 降级规则设置 */
-.rule-container {
-	background: #fff;
-	border-radius: 16upx;
-	padding: 40upx 30upx;
-	box-shadow: 0 4upx 20upx rgba(0, 0, 0, 0.08);
-	margin-bottom: 60upx; /* 增加底部间距,避免被按钮遮挡 */
 }
-
-.rule-title {
-	font-size: 36upx;
-	font-weight: 600;
-	color: #2d5f3f;
-	margin-bottom: 24upx;
-	text-align: center;
+.field-inp.short {
+	width: 100%;
 }
-
-.rule-content {
+.card-section {
+	position: relative;
 	display: flex;
-	align-items: center;
-	justify-content: center;
-	flex-wrap: wrap;
-	gap: 16upx;
-	margin-bottom: 20upx;
+	align-items: flex-start;
+	margin-top: 18upx;
 }
-
-.rule-text {
-	font-size: 32upx;
-	color: #2d5f3f;
-	font-weight: 500;
+.section-label {
+	width: 132upx;
+	flex-shrink: 0;
+	line-height: 54upx;
+	font-size: 24upx;
+	font-weight: 600;
+	color: #3b3f3c;
 }
-
-.day-input {
-	width: 120upx;
-	height: 60upx;
-	border: 2upx solid #c8e6c9;
-	border-radius: 12upx;
-	padding: 0 16upx;
-	font-size: 30upx;
-	color: #2d5f3f;
-	text-align: center;
-	background: #fff;
-	transition: all 0.3s ease;
-	
-	&:focus {
-		border-color: #4caf50;
-		box-shadow: 0 0 0 6upx rgba(76, 175, 80, 0.15);
-		outline: none;
-	}
-	
-	&::placeholder {
-		color: #81c784;
-		font-size: 26upx;
-	}
+.section-inp {
+	flex: 1;
+	height: 58upx;
+	line-height: 58upx;
+	border: 1upx solid #edf0ed;
+	border-radius: 8upx;
+	padding: 0 18upx;
+	font-size: 24upx;
+	box-sizing: border-box;
+	color: #333;
 }
-
-.rule-tip {
+.section-textarea {
+	flex: 1;
+	height: 140upx;
+	min-height: 130upx;
+	border: 1upx solid #edf0ed;
+	border-radius: 8upx;
+	padding: 14upx 18upx 28upx;
 	font-size: 24upx;
-	color: #66bb6a;
-	text-align: center;
+	line-height: 34upx;
+	box-sizing: border-box;
+	color: #333;
+}
+.char-count {
+	position: absolute;
+	right: 16upx;
+	bottom: 8upx;
+	font-size: 22upx;
+	color: #a8a8a8;
+	line-height: 1;
 }
-
-/* 底部按钮区域 */
 .app-footer {
 	position: fixed;
-	bottom: 0;
 	left: 0;
 	right: 0;
-	width: 100%;
-	background: #fff;
-	padding: 20upx 30upx;
-	padding-bottom: calc(20upx + constant(safe-area-inset-bottom));
-	padding-bottom: calc(20upx + env(safe-area-inset-bottom));
-	box-shadow: 0 -2upx 10upx rgba(0, 0, 0, 0.08);
-	z-index: 9999;
+	bottom: 0;
 	display: flex;
-	align-items: center;
-	justify-content: center;
-	gap: 20upx;
-	height: auto;
-	box-sizing: border-box;
-	border-top: 1upx solid #f0f0f0;
-}
-
-.footer-btn {
-	flex: 1;
-	height: 80upx;
-	line-height: 80upx;
-	min-width: 120upx;
-	border-radius: 8upx;
-	font-size: 32upx;
-	font-weight: 500;
-	border: none;
-	margin: 0;
-	padding: 0;
-	text-align: center;
-}
-
-
-
-/* 响应式设计 */
-@media screen and (max-width: 750upx) {
-	.page-header {
-		padding: 30upx 20upx 20upx;
-		
-		.page-title {
-			font-size: 38upx;
-		}
-		
-		.page-subtitle {
-			font-size: 26upx;
-		}
-	}
-	
-	.main-content {
-		padding: 0 20upx;
-	}
-	
-	.rule-content {
-		flex-direction: column;
-		gap: 12upx;
-	}
-	
-	.app-footer {
-		padding: 20upx 30upx;
-		padding-bottom: calc(20upx + constant(safe-area-inset-bottom));
-		padding-bottom: calc(20upx + env(safe-area-inset-bottom));
-		gap: 16upx;
-	}
-	
+	padding: 20upx 24upx;
+	background: #fff;
+	box-shadow: 0 -4upx 12upx rgba(0, 0, 0, 0.06);
 	.footer-btn {
 		flex: 1;
-		height: 76upx;
-		line-height: 76upx;
-		min-width: 100upx;
-		font-size: 30upx;
-	}
-}
-
-/* 动画效果 */
-.table-container {
-	animation: slideInUp 0.6s ease-out;
-}
-
-.rule-container {
-	animation: slideInUp 0.8s ease-out;
-}
-
-
-
-@keyframes slideInUp {
-	from {
-		opacity: 0;
-		transform: translateY(30upx);
-	}
-	to {
-		opacity: 1;
-		transform: translateY(0);
-	}
-}
-
-/* 高亮聚焦效果 */
-.amount-input:focus,
-.day-input:focus {
-	animation: focusGlow 0.3s ease-in-out;
-}
-
-@keyframes focusGlow {
-	0% {
-		box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4);
-	}
-	50% {
-		box-shadow: 0 0 0 8upx rgba(76, 175, 80, 0.2);
-	}
-	100% {
-		box-shadow: 0 0 0 6upx rgba(76, 175, 80, 0.15);
+		margin: 0 10upx;
 	}
 }
-</style>
+</style>

+ 13 - 0
hdApp/src/api/birthday/index.js

@@ -0,0 +1,13 @@
+import https from '@/plugins/luch-request_0.0.7/request'
+
+export const getWorkbenchSummary = data => https.get('/birthday-gift/workbench-summary', data)
+
+export const getBoardList = data => https.get('/birthday-gift/board-list', data)
+
+export const getBoardCounts = data => https.get('/birthday-gift/board-counts', data)
+
+export const getGiftStats = data => https.get('/birthday-gift/gift-stats', data)
+
+export const notifyTomorrow = data => https.get('/birthday-gift/notify-tomorrow', data)
+
+export const printBirthdayGift = data => https.get('/birthday-gift/print', data)

+ 7 - 0
hdApp/src/pages.json

@@ -226,6 +226,13 @@
 				{ "path": "rechargeChange", "style": {"navigationBarTitleText": "充值记录"}}
 			]
 		},
+		{
+			"root": "admin/birthday",
+			"pages": [
+				{ "path": "board", "style": { "navigationBarTitleText": "生日看板", "enablePullDownRefresh": true } },
+				{ "path": "giftStats", "style": { "navigationBarTitleText": "赠礼统计" } }
+			]
+		},
 		{
 			"root": "admin/coupon",
 			"pages": [