Forráskód Böngészése

Merge remote-tracking branch 'origin/cfclub' into cfclub

liaoxg 4 napja
szülő
commit
71d69332ac

+ 5 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityAdminService.java

@@ -43,6 +43,7 @@ public class ActivityAdminService {
         String status = (String) params.get("status");
         String dimensionCode = (String) params.get("dimensionCode");
         String auditStatus = (String) params.get("auditStatus");
+        String keyword = (String) params.get("keyword");
 
         LambdaQueryWrapper<Activity> wrapper = new LambdaQueryWrapper<>();
         if (vendorUserId != null) {
@@ -58,6 +59,10 @@ public class ActivityAdminService {
         if (auditStatus != null && !auditStatus.isEmpty()) {
             wrapper.eq(Activity::getAuditStatus, auditStatus);
         }
+        if (keyword != null && !keyword.isEmpty()) {
+            wrapper.and(w -> w.like(Activity::getTitle, keyword)
+                    .or().like(Activity::getLocation, keyword));
+        }
         wrapper.orderByDesc(Activity::getUpdatedAt);
 
         SortUtil.applySort(wrapper);

+ 150 - 0
cfc-frontend/pages/family/self-check-result.vue

@@ -21,6 +21,53 @@
         <text class="scr-hero-date" v-if="result.createdAt">自检时间:{{ formatDate(result.createdAt) }}</text>
       </view>
 
+      <!-- 结果解读引导卡(告诉用户结果代表什么、接下来怎么做) -->
+      <view class="scr-guide">
+        <view class="scr-guide-head">
+          <text class="scr-guide-title">📖 怎么看这个结果</text>
+        </view>
+        <text class="scr-guide-desc">这份自检测的是「身·智·富·行·心」五个维度的家庭幸福状态,每个维度 9 分,反映你近期在该方面的实际情况。</text>
+
+        <!-- 总分解读 -->
+        <view class="scr-guide-total">
+          <text class="scr-guide-total-label">你的总分 {{ result.totalScore }}/45:</text>
+          <text class="scr-guide-total-text">{{ overallExplain }}</text>
+        </view>
+
+        <!-- 维度等级图例 -->
+        <view class="scr-guide-legend">
+          <view class="scr-guide-legend-item">
+            <view class="scr-guide-legend-dot lg-healthy"></view>
+            <text class="scr-guide-legend-text">健康(7-9 分):保持就好,不用刻意加码</text>
+          </view>
+          <view class="scr-guide-legend-item">
+            <view class="scr-guide-legend-dot lg-attention"></view>
+            <text class="scr-guide-legend-text">留意(4-6 分):看看下方解读,找到能下手的一步</text>
+          </view>
+          <view class="scr-guide-legend-item">
+            <view class="scr-guide-legend-dot lg-tense"></view>
+            <text class="scr-guide-legend-text">紧绷(0-3 分):先别想着补短板,从最小行动开始</text>
+          </view>
+        </view>
+
+        <!-- 行动指引 -->
+        <view class="scr-guide-steps">
+          <text class="scr-guide-steps-title">接下来怎么做:</text>
+          <view class="scr-guide-step">
+            <text class="scr-guide-step-num">①</text>
+            <text class="scr-guide-step-text">往下看「AI 解读」,了解每个低分维度代表什么</text>
+          </view>
+          <view class="scr-guide-step">
+            <text class="scr-guide-step-num">②</text>
+            <text class="scr-guide-step-text">点底部「生成健康计划」,把建议变成可执行的任务</text>
+          </view>
+          <view class="scr-guide-step">
+            <text class="scr-guide-step-num">③</text>
+            <text class="scr-guide-step-text">15 天后再测一次,看看哪里进步了</text>
+          </view>
+        </view>
+      </view>
+
       <!-- 维度得分 -->
       <view class="scr-card">
         <view class="scr-card-title">
@@ -139,6 +186,16 @@ export default {
       return this.result && this.result.advices && this.result.advices.some(function(a) {
         return a.score != null && a.score <= 6
       })
+    },
+    overallExplain: function() {
+      var total = this.result ? (this.result.totalScore || 0) : 0
+      if (total >= 35) {
+        return '五维整体状态健康,继续保持平衡就好'
+      }
+      if (total >= 20) {
+        return '部分维度需要留意,看看下方 AI 解读找到第一步'
+      }
+      return '有几个维度比较紧绷,建议从下周微行动开始调整'
     }
   },
   onLoad: function(options) {
@@ -434,6 +491,99 @@ export default {
 .scr-dim-level.lvl-healthy { background: #E8F5E9; color: #2E7D32; }
 .scr-dim-level.lvl-attention { background: #FFF8E1; color: #F57F17; }
 .scr-dim-level.lvl-tense { background: #FFEBEE; color: #C62828; }
+.scr-guide {
+  background: #FFF7ED;
+  border-radius: 24rpx;
+  padding: 30rpx 26rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(249, 114, 22, 0.08);
+}
+.scr-guide-head {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+.scr-guide-title {
+  font-size: 30rpx;
+  font-weight: 700;
+  color: #F97316;
+}
+.scr-guide-desc {
+  font-size: 24rpx;
+  color: #888;
+  line-height: 1.6;
+  margin-bottom: 24rpx;
+}
+.scr-guide-total {
+  display: flex;
+  align-items: baseline;
+  margin-bottom: 24rpx;
+}
+.scr-guide-total-label {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  margin-right: 12rpx;
+}
+.scr-guide-total-text {
+  font-size: 48rpx;
+  font-weight: 700;
+  color: #F97316;
+}
+.scr-guide-legend {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx 24rpx;
+  margin-bottom: 24rpx;
+}
+.scr-guide-legend-item {
+  display: flex;
+  align-items: center;
+  flex: 1 1 120rpx;
+}
+.scr-guide-legend-dot {
+  width: 24rpx;
+  height: 24rpx;
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+.lg-healthy { background: #2E7D32; }
+.lg-attention { background: #F57F17; }
+.lg-tense { background: #C62828; }
+.scr-guide-legend-text {
+  font-size: 22rpx;
+  color: #555;
+  margin-left: 10rpx;
+}
+.scr-guide-steps {
+  margin-top: 16rpx;
+}
+.scr-guide-steps-title {
+  font-size: 24rpx;
+  color: #F97316;
+  margin-bottom: 12rpx;
+  display: block;
+}
+.scr-guide-step {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8rpx;
+}
+.scr-guide-step-num {
+  font-size: 28rpx;
+  color: #F97316;
+  margin-right: 12rpx;
+  min-width: 20rpx;
+}
+.scr-guide-step-text {
+  font-size: 24rpx;
+  color: #333;
+  flex: 1;
+}
+.scr-guide-step:last-child {
+  margin-bottom: 0;
+}
+
 .scr-dim-summary {
   flex: 1;
   font-size: 24rpx;

+ 31 - 10
cfc-frontend/pages/membership/upgrade.vue

@@ -52,7 +52,7 @@
             <text class="period-original" v-if="showOriginalPrice(p.key)">¥{{ getOriginalPriceForPeriod(p.key) / 100 }}</text>
           </view>
         </view>
-        <button class="btn-upgrade" @click="goNext">下一步</button>
+        <button class="btn-upgrade" @click="goNext">{{ isRenew ? '续费' : '下一步' }}</button>
       </view>
 
       <!-- 权益对比表 -->
@@ -62,12 +62,14 @@
           <view class="compare-row header-row">
             <text class="compare-cell cell-feature">权益项</text>
             <text class="compare-cell cell-free">免费</text>
+            <text class="compare-cell cell-basic" :class="selectedLevel === 'BASIC' ? 'active' : ''">体验会员</text>
             <text class="compare-cell cell-family" :class="selectedLevel === 'FAMILY' ? 'active' : ''">家庭会员</text>
             <text class="compare-cell cell-premium" :class="selectedLevel === 'PREMIUM' ? 'active' : ''">高级会员</text>
           </view>
           <view class="compare-row" v-for="(item, idx) in compareData" :key="idx">
             <text class="compare-cell cell-feature">{{ item.name }}</text>
             <text class="compare-cell cell-free">{{ item.free }}</text>
+            <text class="compare-cell cell-basic" :class="selectedLevel === 'BASIC' ? 'active' : ''">{{ item.basic }}</text>
             <text class="compare-cell cell-family" :class="selectedLevel === 'FAMILY' ? 'active' : ''">{{ item.family }}</text>
             <text class="compare-cell cell-premium" :class="selectedLevel === 'PREMIUM' ? 'active' : ''">{{ item.premium }}</text>
           </view>
@@ -111,13 +113,17 @@ export default {
       isFree: true,
       currentLevelName: '免费用户',
       compareData: [
-        { name: '商品折扣', free: '普通价格', family: '会员折扣', premium: '会员折扣', lifetime: '会员折扣' },
-        { name: '家庭管理', free: '基础功能', family: '全部功能', premium: '全部功能+优先响应', lifetime: '全部功能+专属管家' },
-        { name: '管家服务', free: '—', family: '—', premium: '每周一次', lifetime: '专属管家7×24h' },
-        { name: '推荐佣金', free: '不可参与', family: '可参与', premium: '可参与', lifetime: '可参与' },
-        { name: '专属活动', free: '部分参与', family: '全部参与', premium: '优先参与', lifetime: 'VIP专属+定制活动' },
-        { name: '成长报告', free: '月度', family: '每周+深度', premium: '每日+深度+专家解读', lifetime: '实时+专家深度解读' },
-        { name: '任务配额', free: '每日5个', family: '不限量', premium: '不限量+优先审核', lifetime: '不限量+专属通道' }
+        { name: '商品折扣', free: '普通价格', basic: '会员折扣', family: '会员折扣', premium: '会员折扣', lifetime: '会员折扣' },
+        { name: '家庭管理', free: '基础功能', basic: '3个孩子', family: '10个孩子', premium: '20个孩子', lifetime: '全部功能+专属管家' },
+        { name: 'AI健康顾问', free: '—', basic: '3次/月', family: '不限', premium: '不限', lifetime: '不限' },
+        { name: '报告解读', free: '—', basic: '1次/月', family: '每周+深度', premium: '每日+深度+专家解读', lifetime: '实时+专家深度解读' },
+        { name: '健康报告上传', free: '—', basic: '2份/月', family: '不限', premium: '不限', lifetime: '不限' },
+        { name: '测评预约', free: '仅查看', basic: '1次/月', family: '不限', premium: '优先', lifetime: '优先' },
+        { name: '管家服务', free: '—', basic: '—', family: '月1次', premium: '每周一次', lifetime: '专属管家7×24h' },
+        { name: '推荐佣金', free: '不可参与', basic: '可参与', family: '可参与', premium: '可参与', lifetime: '可参与' },
+        { name: '专属活动', free: '部分参与', basic: '部分参与', family: '全部参与', premium: '优先参与', lifetime: 'VIP专属+定制活动' },
+        { name: '成长报告', free: '月度', basic: '基础模板', family: '每周+深度', premium: '每日+深度+专家解读', lifetime: '实时+专家深度解读' },
+        { name: '任务配额', free: '每日5个', basic: '每日3个', family: '不限量', premium: '不限量+优先审核', lifetime: '不限量+专属通道' }
       ],
       periods: [
         { key: 'monthly', label: '月度', unit: '/月' },
@@ -136,6 +142,11 @@ export default {
         return lv.levelCode !== 'FREE' && lv.levelCode !== 'LIFETIME' && lv.levelCode !== 'PROVIDER'
       })
     },
+    isRenew() {
+      var current = this.membership && this.membership.memberLevel
+      if (current && current !== 'FREE' && this.selectedLevel === current) return true
+      return false
+    },
     visiblePeriods() {
       var self = this
       return this.periods.filter(function(p) {
@@ -206,11 +217,9 @@ export default {
         if (this.membership && this.membership.memberLevel && this.membership.memberLevel !== 'FREE') {
           this.isFree = false
           this.currentLevelName = this.membership.levelName || '家庭会员'
-          this.selectedLevel = this.membership.memberLevel
         } else {
           this.isFree = true
           this.currentLevelName = '免费用户'
-          this.selectedLevel = 'FAMILY'
         }
       } catch (e) {
         console.error('加载会员信息失败', e)
@@ -218,6 +227,7 @@ export default {
       try {
         const levelsRes = await getMembershipPlans()
         this.levels = levelsRes.data || []
+        this.applyDefaultLevel()
       } catch (e) {
         console.error('加载会员等级失败', e)
       }
@@ -248,6 +258,17 @@ export default {
     selectPeriod: function(key) {
       this.selectedPeriod = key
     },
+    applyDefaultLevel: function() {
+      var current = this.membership && this.membership.memberLevel ? this.membership.memberLevel : 'FREE'
+      var rank = { 'FREE': 0, 'BASIC': 1, 'FAMILY': 2, 'PREMIUM': 3 }
+      var currentRank = rank[current] !== undefined ? rank[current] : 0
+      var target = null
+      for (var i = 0; i < this.buyableLevels.length; i++) {
+        var r = rank[this.buyableLevels[i].levelCode]
+        if (r !== undefined && r > currentRank) { target = this.buyableLevels[i].levelCode; break }
+      }
+      this.selectedLevel = target || current
+    },
     getPriceForPeriod: function(periodKey) {
       var plan = null
       for (var i = 0; i < this.levels.length; i++) {

+ 234 - 30
cfc-web/src/views/admin/HomeBannerManage.vue

@@ -25,26 +25,33 @@
       <div class="table-scroll-wrap-sm">
         <el-table :max-height="tableHeight" :data="list" v-loading="loading" border stripe>
           <el-table-column prop="id" label="ID" width="60" />
-          <el-table-column label="封面" width="80">
+          <el-table-column label="类型" width="80">
             <template slot-scope="{ row }">
-              <img v-if="row.coverImage" :src="row.coverImage" style="width:50px;height:35px;object-fit:cover;border-radius:4px;" />
-              <span v-else style="color:#999">无</span>
+              <el-tag size="mini" :type="typeTag(row.bannerType)">{{ typeLabel(row.bannerType) }}</el-tag>
             </template>
           </el-table-column>
-          <el-table-column prop="title" label="标题" min-width="160" show-overflow-tooltip>
+          <el-table-column label="标题" min-width="120" show-overflow-tooltip>
             <template slot="header"><span class="sort-header" @click.stop="onSortClick('title')">标题<span v-if="sortIndex('title') !== null" class="sort-index" :class="sortClass('title') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('title') }}</span>{{ sortIcon('title') }}</span></template>
+            <template slot-scope="{ row }">{{ row.title }}</template>
           </el-table-column>
-          <el-table-column label="类型" width="80">
+          <el-table-column label="关联内容" width="160">
             <template slot-scope="{ row }">
-              <el-tag size="mini" :type="typeTag(row.bannerType)">{{ typeLabel(row.bannerType) }}</el-tag>
+              <div v-if="row.bannerType === 'activity' && row.refId">
+                <el-tag size="mini" type="primary">活动 #{{ row.refId }}</el-tag>
+              </div>
+              <div v-else-if="row.bannerType === 'product' && row.refId">
+                <el-tag size="mini" type="info">商品 #{{ row.refId }}</el-tag>
+              </div>
+              <div v-else-if="row.bannerType === 'link' && row.linkUrl">
+                <el-tag size="mini" type="info">{{ row.linkUrl }}</el-tag>
+              </div>
+              <div v-else style="color:#bbb">-</div>
             </template>
           </el-table-column>
-          <el-table-column label="关联" width="120">
+          <el-table-column label="封面" width="80">
             <template slot-scope="{ row }">
-              <span v-if="row.bannerType === 'activity' && row.refId">活动 #{{ row.refId }}</span>
-              <span v-else-if="row.bannerType === 'product' && row.refId">商品 #{{ row.refId }}</span>
-              <span v-else-if="row.bannerType === 'link' && row.linkUrl" style="font-size:12px;color:#999;overflow:hidden;text-overflow:ellipsis;display:block;max-width:110px;">{{ row.linkUrl }}</span>
-              <span v-else style="color:#bbb">-</span>
+              <img v-if="row.coverImage" :src="row.coverImage" style="width:50px;height:35px;object-fit:cover;border-radius:4px;" />
+              <span v-else style="color:#999">无</span>
             </template>
           </el-table-column>
           <el-table-column label="排序" width="80">
@@ -52,7 +59,7 @@
               <el-input-number v-model="row.sortOrder" :min="0" :max="999" size="mini" @change="onSortChange(row)" controls-position="right" />
             </template>
           </el-table-column>
-          <el-table-column label="投放时间" width="200">
+          <el-table-column label="投放时间" width="180">
             <template slot-scope="{ row }">
               <span v-if="row.startTime || row.endTime">{{ formatDate(row.startTime) }} ~ {{ formatDate(row.endTime) }}</span>
               <span v-else style="color:#bbb">不限</span>
@@ -88,11 +95,12 @@
     </el-card>
 
     <!-- 编辑弹窗 -->
-    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px" :close-on-click-modal="false">
-      <el-form :model="form" :rules="rules" ref="bannerForm" label-width="100px">
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="800px" :close-on-click-modal="false">
+      <el-form :model="form" :rules="rules" ref="bannerForm" label-width="150px">
         <el-form-item label="标题" prop="title">
           <el-input v-model="form.title" placeholder="展示标题,最多50字" maxlength="50" show-word-limit />
         </el-form-item>
+
         <el-form-item label="类型" prop="bannerType">
           <el-select v-model="form.bannerType" placeholder="选择类型" style="width:100%;">
             <el-option label="活动" value="activity" />
@@ -100,21 +108,70 @@
             <el-option label="链接" value="link" />
           </el-select>
         </el-form-item>
-        <el-form-item v-if="form.bannerType !== 'link'" label="关联内容" prop="refId">
-          <el-input-number v-model="form.refId" :min="1" placeholder="活动/商品 ID" style="width:100%;" />
+
+        <!-- 活动选择器 -->
+        <el-form-item v-if="form.bannerType === 'activity'" label="关联活动">
+          <el-select
+            v-model="form.refId"
+            placeholder="搜索活动"
+            filterable
+            :remote="true"
+            :remote-method="searchActivities"
+            style="width:100%"
+            clearable
+          >
+            <el-option key="" label="-- 请选择 --" value="">
+            </el-option>
+          </el-select>
+        </el-form-item>
+
+        <!-- 商品选择器 -->
+        <el-form-item v-if="form.bannerType === 'product'" label="关联商品">
+          <el-select
+            v-model="form.refId"
+            placeholder="搜索商品"
+            filterable
+            :remote="true"
+            :remote-method="searchProducts"
+            style="width:100%"
+            clearable
+          >
+            <el-option key="" label="-- 请选择 --" value="">
+            </el-option>
+          </el-select>
         </el-form-item>
-        <el-form-item v-if="form.bannerType === 'link'" label="跳转链接" prop="linkUrl">
-          <el-input v-model="form.linkUrl" placeholder="完整 URL 或小程序页面路径" />
+
+        <!-- 链接类型表单 -->
+        <el-form-item v-if="form.bannerType === 'link'" label="跳转链接">
+          <el-input v-model="form.linkUrl" placeholder="小程序页面路径(如 /pages/activity/activity-detail/activity-detail?id=123)" />
+          <el-button type="success" size="mini" @click="selectMiniPage">选择小程序页面</el-button>
         </el-form-item>
+
+        <!-- 封面图 -->
         <el-form-item label="封面图">
-          <el-input v-model="form.coverImage" placeholder="图片 URL" />
+          <el-upload
+            :action="uploadActionUrl"
+            :headers="uploadHeaders"
+            :on-success="handleUploadSuccess"
+            :on-error="handleUploadError"
+            :before-upload="beforeUpload"
+            :show-file-list="false"
+            accept="image/jpeg,image/png,image/gif,image/webp"
+          >
+            <img v-if="form.coverImage" :src="form.coverImage" class="cover-preview" />
+            <el-button v-else type="primary" plain>+ 上传封面</el-button>
+          </el-upload>
           <div v-if="form.coverImage" style="margin-top:8px;">
-            <el-image :src="form.coverImage" style="width:100px;height:60px;" fit="cover" />
+            <el-image :src="form.coverImage" style="width:200px;height:120px;" fit="cover" />
           </div>
+          <el-button v-if="form.coverImage" size="mini" type="text" style="margin-left:10px;" @click="form.coverImage = ''">清除</el-button>
+          <div style="margin-top:4px;font-size:12px;color:#999;">建议尺寸 750×320rpx(约 2.35:1),单张不超过 5MB</div>
         </el-form-item>
+
         <el-form-item label="排序">
           <el-input-number v-model="form.sortOrder" :min="0" :max="999" style="width:100px;" />
         </el-form-item>
+
         <el-form-item label="投放时间">
           <el-date-picker
             v-model="form.startTime"
@@ -134,6 +191,7 @@
             style="margin-left:8px;"
           />
         </el-form-item>
+
         <el-form-item label="启用">
           <el-switch v-model="form.enabled" :active-value="1" :inactive-value="0" />
         </el-form-item>
@@ -148,6 +206,24 @@
 
 <script>
 import { getHomeBannerList, saveHomeBanner, deleteHomeBanner, batchDeleteHomeBanner, toggleHomeBanner, updateBannerSort } from '@/api/homeBanner'
+import { getActivityList } from '@/api/activity'
+import { getProductList } from '@/api/admin'
+
+// 小程序已注册页面(供链接类型选择)
+const MINI_PAGES = [
+  { path: '/pages/activity/index', label: '活动中心' },
+  { path: '/pages/discover/index', label: '发现' },
+  { path: '/pages/shop/index/index', label: '商城' },
+  { path: '/pages/article-center/index', label: '知识分享' },
+  { path: '/pages/diet/index', label: '饮食' },
+  { path: '/pages/health/report-list', label: '健康报告' },
+  { path: '/pages/energy/index', label: '五维能量' },
+  { path: '/pages/membership/index', label: '会员中心' },
+  { path: '/pages/promotion/index', label: '财富中心' },
+  { path: '/pages/tianpan/index', label: '家庭天盘' },
+  { path: '/pages/assessment/apply', label: '申请测评' },
+  { path: '/pages/tasks/tasks', label: '任务列表' }
+]
 
 export default {
   data() {
@@ -159,8 +235,6 @@ export default {
       total: 0,
       filterStatus: '',
       filterType: '',
-      sortCol: 'id',
-      sortDir: 'asc',
       dialogVisible: false,
       submitLoading: false,
       editId: null,
@@ -182,20 +256,39 @@ export default {
         refId: [
           {
             required: true,
-            message: '请输入关联 ID',
+            message: '请选择关联内容',
             trigger: 'blur',
             validator: (rule, value, callback) => {
               if (this.form.bannerType === 'link') {
                 callback()
               } else if (!value || value <= 0) {
-                callback(new Error('请输入有效ID'))
+                callback(new Error('请选择关联内容'))
               } else {
                 callback()
               }
             }
           }
+        ],
+        // 封面图必填(产品类型)
+        coverImage: [
+          {
+            required: true,
+            message: '请上传封面图',
+            trigger: 'change'
+          }
         ]
-      }
+      },
+      // 活动搜索状态
+      activitySearchKeyword: '',
+      activityLoading: false,
+      activityOptions: [],
+      activityPage: 1,
+      // 商品搜索状态
+      productSearchKeyword: '',
+      productLoading: false,
+      productOptions: [],
+      productPage: 1,
+      showPagePicker: false
     }
   },
   computed: {
@@ -204,6 +297,14 @@ export default {
     },
     dialogTitle() {
       return this.editId ? '编辑广告位' : '新增广告位'
+    },
+    uploadActionUrl() {
+      const baseURL = process.env.VUE_APP_BASE_API || ''
+      return baseURL + '/api/admin/articles/upload/image'
+    },
+    uploadHeaders() {
+      const token = localStorage.getItem('token')
+      return token ? { Authorization: 'Bearer ' + token } : {}
     }
   },
   mounted() {
@@ -259,6 +360,16 @@ export default {
           startTime: row.startTime || null,
           endTime: row.endTime || null
         }
+        // 根据类型预填搜索结果
+        if (row.bannerType === 'activity') {
+          this.activitySearchKeyword = ''
+          this.activityOptions = []
+          this.loadActivityOptions()
+        } else if (row.bannerType === 'product') {
+          this.productSearchKeyword = ''
+          this.productOptions = []
+          this.loadProductOptions()
+        }
       } else {
         this.editId = null
         this.form = {
@@ -273,6 +384,8 @@ export default {
           startTime: null,
           endTime: null
         }
+        this.activityOptions = []
+        this.productOptions = []
       }
       this.dialogVisible = true
       this.$nextTick(function() {
@@ -331,6 +444,86 @@ export default {
     onSortChange(row) {
       updateBannerSort({ id: row.id, sortOrder: row.sortOrder }).catch(function() {})
     },
+
+    // --- 活动搜索 ---
+    searchActivities: async function(query) {
+      this.activityLoading = true
+      this.activityOptions = []
+      try {
+        const res = await getActivityList({
+          keyword: query || undefined,
+          page: 1,
+          size: 20
+        })
+        const data = (res && res.data) || {}
+        this.activityOptions = data.records || []
+        this.activityPage = 1
+        this.activityLoading = false
+      } catch (e) {
+        console.error('[HomeBanner] 搜索活动失败', e)
+        this.activityLoading = false
+      }
+    },
+    loadActivityOptions() {
+      this.searchActivities(this.activitySearchKeyword)
+    },
+
+    // 商品搜索
+    searchProducts: async function(query) {
+      this.productLoading = true
+      this.productOptions = []
+      try {
+        const res = await getProductList({
+          keyword: query || undefined,
+          page: 1,
+          size: 20
+        })
+        const data = (res && res.data) || {}
+        this.productOptions = data.records || []
+        this.productPage = 1
+        this.productLoading = false
+      } catch (e) {
+        console.error('[HomeBanner] 搜索商品失败', e)
+        this.productLoading = false
+      }
+    },
+    loadProductOptions() {
+      this.searchProducts(this.productSearchKeyword)
+    },
+
+    handleUploadSuccess(res) {
+      if (res && res.code === 200 && res.data && res.data.url) {
+        this.form.coverImage = res.data.url
+        this..success('上传成功')
+      } else {
+        this..error('上传失败')
+      }
+    },
+    handleUploadError(err) {
+      console.error('[HomeBanner] 上传失败', err)
+      this..error('图片上传失败,请重试')
+    },
+    beforeUpload(file) {
+      const isImage = file.type.startsWith('image/jpeg') || file.type.startsWith('image/png') || file.type.startsWith('image/gif') || file.type.startsWith('image/webp')
+      if (!isImage) {
+        this..error('只能上传图片格式(jpg/png/gif/webp)')
+        return false
+      }
+      const isLt5M = file.size / 1024 / 1024 < 5
+      if (!isLt5M) {
+        this..error('图片大小不能超过 5MB')
+        return false
+      }
+      return true
+    },
+    selectMiniPage() {
+      this.showPagePicker = true
+    },
+    selectPage(path) {
+      this.form.linkUrl = path
+      this.showPagePicker = false
+    },
+    // --- 工具方法 ---
     typeLabel(type) {
       var map = { activity: '活动', product: '商品', link: '链接' }
       return map[type] || type || '-'
@@ -372,12 +565,27 @@ export default {
     },
     formatTime(t) {
       if (!t) return '-'
-      // 兼容字符串格式和 timestamp
       var ts = t
       if (typeof t === 'string' && t.indexOf('T') > -1) {
         ts = new Date(t).getTime()
       }
       return this.formatDate(ts)
+    },
+    // 选择小程序页面
+    selectMiniPage() {
+      this.$message.info('小程序页面选择功能待后端开发')
+    },
+    beforeUpload(file) {
+      const isJPG = file.type === 'image/jpeg'
+      const isPNG = file.type === 'image/png'
+      const isGIF = file.type === 'image/gif'
+      const isQQ = file.type === 'image/webp'
+      if (!isJPG && !isPNG && !isGIF && !isQQ) {
+        this.$message.error('只能上传 jpg/png/gif/webp 格式的图片!')
+        return false
+      }
+      this.$message.warning('上传图片受限,请使用后台上传接口上传封面图')
+      return false
     }
   }
 }
@@ -400,8 +608,4 @@ export default {
   gap: 12px;
   flex-wrap: wrap;
 }
-.pagination-wrap {
-  display: flex;
-  justify-content: flex-end;
-}
 </style>