xsh_1997 пре 1 недеља
родитељ
комит
04c745d4d3

+ 8 - 0
ruoyi-ui/src/api/agri/merchantEntryApply.js

@@ -42,3 +42,11 @@ export function rejectMerchantEntryApply(data) {
42 42
     data: data
43 43
   })
44 44
 }
45
+
46
+// 完成公示(公示期满后建档商户/店铺)
47
+export function completeMerchantEntryPublicity(applyId) {
48
+  return request({
49
+    url: '/agri/merchantEntryApply/completePublicity/' + applyId,
50
+    method: 'put'
51
+  })
52
+}

+ 2 - 1
ruoyi-ui/src/views/agri/org/merchant/detail.vue

@@ -14,6 +14,7 @@
14 14
         <el-descriptions-item label="商户类型">{{ merchantTypeLabel(detail.merchantType) }}</el-descriptions-item>
15 15
         <el-descriptions-item label="商户所属单位">{{ detail.unitName || '—' }}</el-descriptions-item>
16 16
         <el-descriptions-item label="商户名称">{{ detail.merchantName || '待完善' }}</el-descriptions-item>
17
+        <el-descriptions-item label="商户管理员">{{ detail.adminName || '—' }}</el-descriptions-item>
17 18
         <el-descriptions-item label="认证状态">
18 19
           <el-tag size="small" :type="certStatusTag(detail.certStatus)">{{ certStatusLabel(detail.certStatus) }}</el-tag>
19 20
         </el-descriptions-item>
@@ -204,7 +205,7 @@ export default {
204 205
     /** 跳转店铺列表 */
205 206
     goShopList() {
206 207
       const name = this.detail.merchantName || this.detail.unitName
207
-      this.$router.push({ path: "/org/shop", query: { merchantName: name } })
208
+      this.$router.push({ path: "/agri/org/shop", query: { merchantName: name } })
208 209
     },
209 210
     /** 修改认证状态(两步确认) */
210 211
     handleCertChange(item) {

+ 64 - 10
ruoyi-ui/src/views/agri/org/merchant/index.vue

@@ -41,6 +41,11 @@
41 41
             <span>{{ scope.row.merchantName || '待完善' }}</span>
42 42
           </template>
43 43
         </el-table-column>
44
+        <el-table-column label="商户管理员" align="center" prop="adminName" min-width="110" :show-overflow-tooltip="true">
45
+          <template slot-scope="scope">
46
+            <span>{{ scope.row.adminName || '—' }}</span>
47
+          </template>
48
+        </el-table-column>
44 49
         <el-table-column label="联系人" align="center" prop="contactName" width="100">
45 50
           <template slot-scope="scope"><span>{{ scope.row.contactName || '—' }}</span></template>
46 51
         </el-table-column>
@@ -100,7 +105,7 @@
100 105
                 </el-col>
101 106
                 <el-col :span="12">
102 107
                   <el-form-item label="证件号码" prop="idCardNo">
103
-                    <el-input v-model="form.idCardNo" maxlength="18" :disabled="!!form.merchantId" placeholder="选填" />
108
+                    <el-input v-model="form.idCardNo" maxlength="18" :disabled="!!form.merchantId" :placeholder="form.merchantId ? '' : '必填'" />
104 109
                   </el-form-item>
105 110
                 </el-col>
106 111
                 <el-col :span="12">
@@ -206,7 +211,7 @@
206 211
                 <el-col :span="12"><el-form-item label="企业名称" prop="companyName"><el-input v-model="form.companyName" maxlength="128" /></el-form-item></el-col>
207 212
                 <el-col :span="12">
208 213
                   <el-form-item label="统一社会信用代码" prop="creditCode">
209
-                    <el-input v-model="form.creditCode" maxlength="18" :disabled="!!form.merchantId" placeholder="选填" />
214
+                    <el-input v-model="form.creditCode" maxlength="18" :disabled="!!form.merchantId" :placeholder="form.merchantId ? '' : '必填'" />
210 215
                   </el-form-item>
211 216
                 </el-col>
212 217
                 <el-col :span="24">
@@ -247,8 +252,8 @@
247 252
             </template>
248 253
           </el-tab-pane>
249 254
 
250
-          <!-- 经营信息(仅编辑) -->
251
-          <el-tab-pane v-if="form.merchantId" label="商户经营信息" name="biz">
255
+          <!-- 商户经营信息(新增/编辑均须填写完整) -->
256
+          <el-tab-pane label="商户经营信息" name="biz">
252 257
             <el-row :gutter="16">
253 258
               <el-col :span="12"><el-form-item label="商户名称" prop="merchantName"><el-input v-model="form.merchantName" maxlength="128" /></el-form-item></el-col>
254 259
               <el-col :span="12"><el-form-item label="客服电话" prop="servicePhone"><el-input v-model="form.servicePhone" maxlength="20" /></el-form-item></el-col>
@@ -372,7 +377,7 @@ export default {
372 377
     }
373 378
   },
374 379
   computed: {
375
-    /** 表单校验:新增个人仅姓名必填;新增企业仅法人姓名+企业名称必填;绑定仅新增时校验 */
380
+    /** 表单校验:v1.8 新增须主体+经营完整;编辑须经营字段校验 */
376 381
     formRules() {
377 382
       const rules = {
378 383
         merchantType: [{ required: true, message: "请选择主体类型", trigger: "change" }]
@@ -380,9 +385,46 @@ export default {
380 385
       const isAdd = !this.form.merchantId
381 386
       if (this.form.merchantType === "1") {
382 387
         rules.personName = [{ required: true, message: "请输入姓名", trigger: "blur" }]
388
+        if (isAdd) {
389
+          rules.idCardNo = [{ required: true, message: "请输入证件号码", trigger: "blur" }]
390
+        }
383 391
       } else {
384 392
         rules.legalName = [{ required: true, message: "请输入法人姓名", trigger: "blur" }]
385 393
         rules.companyName = [{ required: true, message: "请输入企业名称", trigger: "blur" }]
394
+        if (isAdd) {
395
+          rules.creditCode = [{ required: true, message: "请输入统一社会信用代码", trigger: "blur" }]
396
+        }
397
+      }
398
+      // 商户经营信息(§6.6 完整判定字段)
399
+      rules.merchantName = [{ required: true, message: "请输入商户名称", trigger: "blur" }]
400
+      rules.servicePhone = [{ required: true, message: "请输入客服电话", trigger: "blur" }]
401
+      rules.bizRegionCascader = [{
402
+        validator: (rule, value, callback) => {
403
+          if (this.form.bizRegionCode && this.form.bizRegionName) {
404
+            callback()
405
+            return
406
+          }
407
+          if (value && value.length >= 3) {
408
+            callback()
409
+            return
410
+          }
411
+          callback(new Error("请选择经营地区"))
412
+        },
413
+        trigger: "change"
414
+      }]
415
+      rules.bizDetailAddress = [{ required: true, message: "请输入经营详细地址", trigger: "blur" }]
416
+      rules.contactName = [{ required: true, message: "请输入联系人姓名", trigger: "blur" }]
417
+      rules.contactPhone = [{ required: true, message: "请输入联系人手机", trigger: "blur" }]
418
+      rules.contactEmail = [
419
+        { required: true, message: "请输入联系人邮箱", trigger: "blur" },
420
+        { type: "email", message: "邮箱格式不正确", trigger: "blur" }
421
+      ]
422
+      rules.bankName = [{ required: true, message: "请输入开户银行", trigger: "blur" }]
423
+      rules.bankBranch = [{ required: true, message: "请输入支行名称", trigger: "blur" }]
424
+      rules.bankAccount = [{ required: true, message: "请输入银行账号", trigger: "blur" }]
425
+      if (this.form.merchantType === "2") {
426
+        rules.businessLicense = [{ required: true, message: "请上传营业执照电子版", trigger: "change" }]
427
+        rules.accountPermit = [{ required: true, message: "请上传开户许可证", trigger: "change" }]
386 428
       }
387 429
       if (isAdd) {
388 430
         rules.bindType = [{ required: true, message: "请选择绑定类型", trigger: "change" }]
@@ -465,6 +507,19 @@ export default {
465 507
         bindType: "SYS_USER",
466 508
         bindUserId: undefined,
467 509
         bindMemberId: undefined,
510
+        merchantName: undefined,
511
+        servicePhone: undefined,
512
+        bizRegionCode: undefined,
513
+        bizRegionName: undefined,
514
+        bizDetailAddress: undefined,
515
+        contactName: undefined,
516
+        contactPhone: undefined,
517
+        contactEmail: undefined,
518
+        bankName: undefined,
519
+        bankBranch: undefined,
520
+        bankAccount: undefined,
521
+        businessLicense: undefined,
522
+        accountPermit: undefined,
468 523
         bizRegionCascader: []
469 524
       }
470 525
       this.activeTab = "subject"
@@ -621,12 +676,11 @@ export default {
621 676
         const payload = this.buildSubmitPayload()
622 677
         const submitFn = payload.merchantId ? updateMerchant : addMerchant
623 678
         submitFn(payload).then(response => {
624
-          let msg = payload.merchantId ? "修改成功" : "新增成功"
625
-          if (response.data && response.data.bizCompleteChanged) {
626
-            msg = "保存成功,已可开设店铺"
627
-          }
679
+          let msg = "修改成功"
628 680
           if (!payload.merchantId) {
629
-            msg = "请尽快在编辑中完善商户经营信息后再开设店铺"
681
+            msg = "保存成功,已可开设店铺"
682
+          } else if (response.data && response.data.bizCompleteChanged) {
683
+            msg = "保存成功,已可开设店铺"
630 684
           }
631 685
           if (response.data && response.data.warnExpired) {
632 686
             this.$modal.msgWarning("证件或营业期限已过期,请注意风险")

+ 84 - 16
ruoyi-ui/src/views/agri/org/merchantEntryApply/detail.vue

@@ -9,6 +9,16 @@
9 9
     custom-class="detail-drawer"
10 10
   >
11 11
     <div v-loading="loading" class="drawer-content">
12
+      <!-- 仅开店铺模式提示 -->
13
+      <el-alert
14
+        v-if="detail.shopOnlyMode"
15
+        title="本申请为「仅开店铺」模式:主体资质与商户经营信息取自已有商户快照;公示完成后将复用已有商户并新建店铺。"
16
+        type="info"
17
+        :closable="false"
18
+        show-icon
19
+        class="mb16"
20
+      />
21
+
12 22
       <!-- 基础信息 -->
13 23
       <h4 class="section-header">基础信息</h4>
14 24
       <el-descriptions :column="2" border size="small" class="mb16">
@@ -18,9 +28,15 @@
18 28
         </el-descriptions-item>
19 29
         <el-descriptions-item label="主体类型">{{ merchantTypeLabel(detail.merchantType) }}</el-descriptions-item>
20 30
         <el-descriptions-item label="申请会员名称">{{ detail.memberCode || '—' }}</el-descriptions-item>
31
+        <el-descriptions-item v-if="detail.shopOnlyMode" label="复用商户ID">{{ detail.existingMerchantId || '—' }}</el-descriptions-item>
21 32
         <el-descriptions-item label="申请时间">{{ parseTime(detail.applyTime) || '—' }}</el-descriptions-item>
22 33
         <el-descriptions-item label="审核人">{{ detail.auditBy || '—' }}</el-descriptions-item>
23 34
         <el-descriptions-item label="审核时间">{{ parseTime(detail.auditTime) || '—' }}</el-descriptions-item>
35
+        <el-descriptions-item v-if="detail.applyStatus === '3' || detail.applyStatus === '1'" label="公示时间" :span="2">
36
+          {{ formatPublicityPeriod(detail) }}
37
+        </el-descriptions-item>
38
+        <el-descriptions-item v-if="detail.applyStatus === '1' && detail.merchantId" label="商户ID">{{ detail.merchantId }}</el-descriptions-item>
39
+        <el-descriptions-item v-if="detail.applyStatus === '1' && detail.shopId" label="店铺ID">{{ detail.shopId }}</el-descriptions-item>
24 40
         <el-descriptions-item v-if="detail.applyStatus === '2'" label="拒绝原因" :span="2">
25 41
           <span class="reject-reason">{{ detail.rejectReason || '—' }}</span>
26 42
         </el-descriptions-item>
@@ -83,7 +99,7 @@
83 99
       </el-descriptions>
84 100
 
85 101
       <!-- 店铺信息 -->
86
-      <h4 class="section-header">首家店铺信息</h4>
102
+      <h4 class="section-header">{{ detail.shopOnlyMode ? '拟开设店铺信息' : '首家店铺信息' }}</h4>
87 103
       <el-descriptions :column="2" border size="small" class="mb16">
88 104
         <el-descriptions-item label="店铺名称">{{ shop.shopName || '—' }}</el-descriptions-item>
89 105
         <el-descriptions-item label="商家电话">{{ shop.shopPhone || '—' }}</el-descriptions-item>
@@ -94,10 +110,11 @@
94 110
         <el-descriptions-item label="店铺描述">{{ shop.shopDesc || '—' }}</el-descriptions-item>
95 111
       </el-descriptions>
96 112
 
97
-      <!-- 审核操作区(仅待审核) -->
98
-      <div v-if="detail.canApprove || detail.canReject" class="action-bar">
99
-        <el-button type="success" @click="handleApprove" v-hasPermi="['agri:merchantEntryApply:audit']">审核通过</el-button>
100
-        <el-button type="danger" plain @click="rejectOpen = true" v-hasPermi="['agri:merchantEntryApply:audit']">审核驳回</el-button>
113
+      <!-- 审核操作区 -->
114
+      <div v-if="detail.canApprove || detail.canReject || detail.canCompletePublicity" class="action-bar">
115
+        <el-button v-if="detail.canApprove" type="success" @click="handleApprove" v-hasPermi="['agri:merchantEntryApply:audit']">审核通过</el-button>
116
+        <el-button v-if="detail.canReject" type="danger" plain @click="rejectOpen = true" v-hasPermi="['agri:merchantEntryApply:audit']">审核驳回</el-button>
117
+        <el-button v-if="detail.canCompletePublicity" type="primary" @click="handleCompletePublicity" v-hasPermi="['agri:merchantEntryApply:audit']">完成公示</el-button>
101 118
       </div>
102 119
     </div>
103 120
 
@@ -117,7 +134,12 @@
117 134
 </template>
118 135
 
119 136
 <script>
120
-import { getMerchantEntryApply, approveMerchantEntryApply, rejectMerchantEntryApply } from "@/api/agri/merchantEntryApply"
137
+import {
138
+  getMerchantEntryApply,
139
+  approveMerchantEntryApply,
140
+  rejectMerchantEntryApply,
141
+  completeMerchantEntryPublicity
142
+} from "@/api/agri/merchantEntryApply"
121 143
 
122 144
 export default {
123 145
   name: "MerchantEntryApplyDetail",
@@ -168,25 +190,53 @@ export default {
168 190
       this.loading = true
169 191
       getMerchantEntryApply(this.applyId).then(response => {
170 192
         const data = response.data || {}
193
+        const formJson = data.formJson || {}
194
+        // shopOnlyMode 可能在 VO 顶层或 form_json 内
195
+        if (data.shopOnlyMode === undefined) {
196
+          data.shopOnlyMode = formJson.shopOnlyMode
197
+        }
198
+        if (data.existingMerchantId === undefined) {
199
+          data.existingMerchantId = formJson.existingMerchantId
200
+        }
171 201
         this.detail = data
172
-        this.subject = data.subject || data.formJson?.subject || {}
173
-        this.biz = data.biz || data.formJson?.biz || {}
174
-        this.shop = data.shop || data.formJson?.shop || {}
202
+        this.subject = data.subject || formJson.subject || {}
203
+        this.biz = data.biz || formJson.biz || {}
204
+        this.shop = data.shop || formJson.shop || {}
175 205
         this.loading = false
176 206
       }).catch(() => {
177 207
         this.loading = false
178 208
       })
179 209
     },
180
-    /** 申请状态文案 */
210
+    /** 申请状态文案(0待审 1已完成 2未通过 3公示中) */
181 211
     applyStatusLabel(status) {
182
-      const map = { "0": "待审核", "1": "审核通过", "2": "审核未通过" }
212
+      const map = {
213
+        "0": "待审核",
214
+        "1": "已完成入驻",
215
+        "2": "审核未通过",
216
+        "3": "公示中"
217
+      }
183 218
       return map[status] || "—"
184 219
     },
185 220
     /** 申请状态标签颜色 */
186 221
     applyStatusTag(status) {
187
-      const map = { "0": "warning", "1": "success", "2": "danger" }
222
+      const map = { "0": "warning", "1": "success", "2": "danger", "3": "primary" }
188 223
       return map[status] || "info"
189 224
     },
225
+    /** 公示时间展示 */
226
+    formatPublicityPeriod(row) {
227
+      if (!row || (row.applyStatus !== "3" && row.applyStatus !== "1")) {
228
+        return "—"
229
+      }
230
+      const start = this.parseTime(row.publicityStartTime)
231
+      const end = this.parseTime(row.publicityEndTime)
232
+      if (!start && !end) {
233
+        return "—"
234
+      }
235
+      if (start && end) {
236
+        return start + " 至 " + end
237
+      }
238
+      return start || end || "—"
239
+    },
190 240
     /** 商户类型文案 */
191 241
     merchantTypeLabel(type) {
192 242
       return type === "2" ? "企业" : type === "1" ? "个人" : "—"
@@ -210,14 +260,32 @@ export default {
210 260
     handleClose(done) {
211 261
       done()
212 262
     },
213
-    /** 审核通过(两步确认) */
263
+    /** 审核通过(进入公示,不立即建档) */
214 264
     handleApprove() {
215
-      this.$modal.confirm("确认审核通过?通过后将自动创建商户与首家店铺。").then(() => {
265
+      this.$modal.confirm("确认审核通过?通过后将进入公示期,公示完成后再创建商户与店铺。").then(() => {
216 266
         return approveMerchantEntryApply(this.applyId, { confirmed: true })
267
+      }).then(() => {
268
+        this.$modal.msgSuccess("已进入公示")
269
+        this.loadDetail()
270
+        this.$emit("success")
271
+      }).catch(() => {})
272
+    },
273
+    /** 完成公示(公示期满后建档) */
274
+    handleCompletePublicity() {
275
+      const tip = this.detail.shopOnlyMode
276
+        ? "确认完成公示?将复用已有商户并创建申请中的店铺。"
277
+        : "确认完成公示?将创建商户与首家店铺,并绑定申请会员经营账号。"
278
+      this.$modal.confirm(tip).then(() => {
279
+        return completeMerchantEntryPublicity(this.applyId)
217 280
       }).then(response => {
218
-        this.$modal.msgSuccess("审核通过")
281
+        const data = response.data || {}
282
+        let msg = "入驻已完成"
283
+        if (data.merchantId || data.shopId) {
284
+          msg += "(商户ID:" + (data.merchantId || "—") + ",店铺ID:" + (data.shopId || "—") + ")"
285
+        }
286
+        this.$modal.msgSuccess(msg)
219 287
         this.localVisible = false
220
-        this.$emit("success", response.data)
288
+        this.$emit("success", data)
221 289
       }).catch(() => {})
222 290
     },
223 291
     /** 提交驳回 */

+ 52 - 7
ruoyi-ui/src/views/agri/org/merchantEntryApply/index.vue

@@ -7,9 +7,10 @@
7 7
           <el-input v-model="queryParams.keyword" placeholder="申请会员名称" clearable style="width: 200px" @keyup.enter.native="handleQuery" />
8 8
         </el-form-item>
9 9
         <el-form-item label="申请状态" prop="applyStatus">
10
-          <el-select v-model="queryParams.applyStatus" placeholder="全部" clearable style="width: 140px">
10
+          <el-select v-model="queryParams.applyStatus" placeholder="全部" clearable style="width: 150px">
11 11
             <el-option label="待审核" value="0" />
12
-            <el-option label="审核通过" value="1" />
12
+            <el-option label="公示中" value="3" />
13
+            <el-option label="已完成入驻" value="1" />
13 14
             <el-option label="审核未通过" value="2" />
14 15
           </el-select>
15 16
         </el-form-item>
@@ -27,14 +28,18 @@
27 28
         <el-tab-pane name="0">
28 29
           <span slot="label">待审核<el-badge v-if="pendingCount > 0" :value="pendingCount" class="tab-badge" /></span>
29 30
         </el-tab-pane>
30
-        <el-tab-pane label="审核通过" name="1" />
31
+        <el-tab-pane label="公示中" name="3" />
32
+        <el-tab-pane label="已完成入驻" name="1" />
31 33
         <el-tab-pane label="审核未通过" name="2" />
32 34
       </el-tabs>
33 35
 
34 36
       <el-table border v-loading="loading" :data="applyList">
35 37
         <el-table-column label="申请信息" align="center" min-width="160" :show-overflow-tooltip="true">
36 38
           <template slot-scope="scope">
37
-            <span>{{ scope.row.merchantType === '2' ? '企业' : '个人' }} · {{ scope.row.subjectLabel || '—' }}</span>
39
+            <span>
40
+              {{ scope.row.merchantType === '2' ? '企业' : '个人' }} · {{ scope.row.subjectLabel || '—' }}
41
+              <el-tag v-if="scope.row.shopOnlyMode" size="mini" type="info" class="shop-only-tag">仅开店铺</el-tag>
42
+            </span>
38 43
           </template>
39 44
         </el-table-column>
40 45
         <el-table-column label="申请会员名称" align="center" prop="memberCode" min-width="120" :show-overflow-tooltip="true">
@@ -49,6 +54,11 @@
49 54
             <el-tag size="small" :type="applyStatusTag(scope.row.applyStatus)">{{ applyStatusLabel(scope.row.applyStatus) }}</el-tag>
50 55
           </template>
51 56
         </el-table-column>
57
+        <el-table-column label="公示时间" align="center" min-width="180" :show-overflow-tooltip="true">
58
+          <template slot-scope="scope">
59
+            <span>{{ formatPublicityPeriod(scope.row) }}</span>
60
+          </template>
61
+        </el-table-column>
52 62
         <el-table-column label="申请时间" align="center" prop="applyTime" width="160">
53 63
           <template slot-scope="scope">
54 64
             <span>{{ parseTime(scope.row.applyTime) }}</span>
@@ -61,6 +71,8 @@
61 71
         </el-table-column>
62 72
       </el-table>
63 73
 
74
+      <el-empty v-if="!loading && !applyList.length" :description="emptyDescription" />
75
+
64 76
       <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
65 77
     </el-card>
66 78
 
@@ -94,6 +106,15 @@ export default {
94 106
       }
95 107
     }
96 108
   },
109
+  computed: {
110
+    /** 空列表提示文案 */
111
+    emptyDescription() {
112
+      if (this.statusTab === "0" || this.queryParams.applyStatus === "0") {
113
+        return "暂无待审核入驻申请"
114
+      }
115
+      return "暂无入驻申请"
116
+    }
117
+  },
97 118
   created() {
98 119
     this.loadPendingCount()
99 120
     this.getList()
@@ -116,16 +137,36 @@ export default {
116 137
         this.loading = false
117 138
       })
118 139
     },
119
-    /** 申请状态文案 */
140
+    /** 申请状态文案(0待审 1已完成 2未通过 3公示中) */
120 141
     applyStatusLabel(status) {
121
-      const map = { "0": "待审核", "1": "审核通过", "2": "审核未通过" }
142
+      const map = {
143
+        "0": "待审核",
144
+        "1": "已完成入驻",
145
+        "2": "审核未通过",
146
+        "3": "公示中"
147
+      }
122 148
       return map[status] || "—"
123 149
     },
124 150
     /** 申请状态标签颜色 */
125 151
     applyStatusTag(status) {
126
-      const map = { "0": "warning", "1": "success", "2": "danger" }
152
+      const map = { "0": "warning", "1": "success", "2": "danger", "3": "primary" }
127 153
       return map[status] || "info"
128 154
     },
155
+    /** 公示时间展示 */
156
+    formatPublicityPeriod(row) {
157
+      if (!row || (row.applyStatus !== "3" && row.applyStatus !== "1")) {
158
+        return "—"
159
+      }
160
+      const start = this.parseTime(row.publicityStartTime)
161
+      const end = this.parseTime(row.publicityEndTime)
162
+      if (!start && !end) {
163
+        return "—"
164
+      }
165
+      if (start && end) {
166
+        return start + " 至 " + end
167
+      }
168
+      return start || end || "—"
169
+    },
129 170
     /** Tab 切换 */
130 171
     handleTabClick() {
131 172
       this.queryParams.applyStatus = this.statusTab === "all" ? undefined : this.statusTab
@@ -165,4 +206,8 @@ export default {
165 206
 .tab-badge {
166 207
   margin-left: 4px;
167 208
 }
209
+.shop-only-tag {
210
+  margin-left: 6px;
211
+  vertical-align: middle;
212
+}
168 213
 </style>

+ 196 - 81
ruoyi-ui/src/views/agri/org/shop/index.vue

@@ -4,7 +4,7 @@
4 4
     <el-card shadow="never" class="search-card">
5 5
       <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
6 6
         <el-form-item label="关键词" prop="keyword">
7
-          <el-input v-model="queryParams.keyword" placeholder="店铺/商户/经营账号" clearable style="width: 220px" @keyup.enter.native="handleQuery" />
7
+          <el-input v-model="queryParams.keyword" placeholder="店铺/商户/账号" clearable style="width: 220px" @keyup.enter.native="handleQuery" />
8 8
         </el-form-item>
9 9
         <el-form-item label="店铺状态" prop="shopStatus">
10 10
           <el-select v-model="queryParams.shopStatus" placeholder="全部" clearable style="width: 120px">
@@ -47,15 +47,20 @@
47 47
             <el-tag v-else type="info" size="small">停业</el-tag>
48 48
           </template>
49 49
         </el-table-column>
50
-        <el-table-column label="经营账号" align="center" min-width="160">
50
+        <el-table-column label="评分" align="center" prop="rating" width="80">
51
+          <template slot-scope="scope">
52
+            <span>{{ formatRating(scope.row.rating) }}</span>
53
+          </template>
54
+        </el-table-column>
55
+        <el-table-column label="主账号" align="center" min-width="160">
51 56
           <template slot-scope="scope">
52 57
             <span>{{ scope.row.accountAdminName || '—' }} / {{ scope.row.accountLoginName || '—' }}</span>
53 58
           </template>
54 59
         </el-table-column>
55
-        <el-table-column label="操作" align="center" width="220" fixed="right">
60
+        <el-table-column label="操作" align="center" width="240" fixed="right">
56 61
           <template slot-scope="scope">
57 62
             <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['agri:shop:edit']">编辑</el-button>
58
-            <el-button size="mini" type="text" icon="el-icon-user" @click="handleAccount(scope.row)" v-hasPermi="['agri:shop:account']">账号管理</el-button>
63
+            <el-button size="mini" type="text" icon="el-icon-user" @click="handleAccount(scope.row)" v-hasPermi="['agri:shop:account']">更换主账号</el-button>
59 64
             <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['agri:shop:remove']">删除</el-button>
60 65
           </template>
61 66
         </el-table-column>
@@ -84,16 +89,16 @@
84 89
             <el-option v-for="item in merchantOptions" :key="item.merchantId" :label="item.merchantName || item.unitName" :value="item.merchantId" />
85 90
           </el-select>
86 91
         </el-form-item>
87
-        <el-form-item v-if="!form.shopId && merchantAccountHint" label="经营账号">
92
+        <el-form-item v-if="!form.shopId && merchantAccountHint" label="账号">
88 93
           <span class="readonly-text">{{ merchantAccountHint }}</span>
89
-          <div class="form-tip">经营账号由商户入驻时绑定,开店沿用该账号</div>
94
+          <div class="form-tip">账号由商户入驻时绑定,开店沿用该账号</div>
90 95
         </el-form-item>
91 96
         <el-form-item v-else-if="form.shopId" label="所属商户">
92 97
           <span class="readonly-text">{{ form.merchantName || '—' }}</span>
93 98
         </el-form-item>
94
-        <el-form-item v-if="form.shopId" label="经营账号">
99
+        <el-form-item v-if="form.shopId" label="账号">
95 100
           <span class="readonly-text">{{ form.accountAdminName || '—' }} / {{ form.accountLoginName || '—' }}</span>
96
-          <div class="form-tip">如需修改请使用「账号管理」</div>
101
+          <div class="form-tip">如需修改请使用「更换主账号」</div>
97 102
         </el-form-item>
98 103
         <el-form-item v-if="!form.shopId" label="店铺状态" prop="shopStatus">
99 104
           <el-radio-group v-model="form.shopStatus">
@@ -116,34 +121,63 @@
116 121
       </div>
117 122
     </el-dialog>
118 123
 
119
-    <!-- 店铺账号管理弹窗 -->
120
-    <el-dialog title="店铺账号管理" :visible.sync="accountOpen" width="560px" append-to-body>
121
-      <el-form ref="accountForm" :model="accountForm" :rules="accountRules" label-width="100px">
122
-        <el-alert v-if="accountForm.shops && accountForm.shops.length" type="info" :closable="false" class="mb12">
123
-          修改后将同步至该商户下全部店铺(共 {{ accountForm.shops.length }} 家)
124
-        </el-alert>
125
-        <div v-if="accountForm.shops && accountForm.shops.length" class="shop-tags mb12">
126
-          <el-tag v-for="item in accountForm.shops" :key="item.shopId" size="small" class="shop-tag" :type="item.shopStatus === '0' ? 'success' : 'info'">
127
-            {{ item.shopName }}
128
-          </el-tag>
129
-        </div>
130
-        <el-form-item label="登录名" prop="loginNameRaw">
131
-          <el-input
132
-            v-model="accountForm.loginNameRaw"
133
-            :placeholder="accountForm.loginNameMasked ? ('当前 ' + accountForm.loginNameMasked + ',修改请填完整手机号') : '11位手机号'"
134
-            maxlength="11"
135
-          />
136
-          <div class="form-tip">登录名须为 11 位手机号,修改后将同步至该商户下全部店铺</div>
124
+    <!-- 更换主账号弹窗(v1.4:换绑管理员/会员,不改登录名密码) -->
125
+    <el-dialog title="更换主账号" :visible.sync="accountOpen" width="600px" append-to-body @close="resetAccountForm">
126
+      <el-form ref="accountForm" :model="accountForm" :rules="accountRules" label-width="110px">
127
+        <el-alert type="warning" :closable="false" class="mb12" title="更换后将同步至该商户下全部店铺" />
128
+        <el-form-item label="当前主账号">
129
+          <span class="readonly-text">{{ accountForm.adminName || '—' }} / {{ accountForm.loginName || '—' }}</span>
130
+        </el-form-item>
131
+        <el-form-item v-if="accountForm.shops && accountForm.shops.length" label="下属店铺">
132
+          <div class="shop-tags">
133
+            <el-tag
134
+              v-for="item in accountForm.shops"
135
+              :key="item.shopId"
136
+              size="small"
137
+              class="shop-tag"
138
+              :type="item.shopStatus === '0' ? 'success' : 'info'"
139
+            >
140
+              {{ item.shopName }}({{ item.shopStatus === '0' ? '开业' : '停业' }})
141
+            </el-tag>
142
+          </div>
143
+        </el-form-item>
144
+        <el-form-item label="绑定方式" prop="bindType">
145
+          <el-radio-group v-model="accountForm.bindType" @change="handleAccountBindTypeChange">
146
+            <el-radio label="SYS_USER">平台管理员</el-radio>
147
+            <el-radio label="MEMBER">会员</el-radio>
148
+          </el-radio-group>
137 149
         </el-form-item>
138
-        <el-form-item label="管理员姓名" prop="adminName">
139
-          <el-input v-model="accountForm.adminName" placeholder="管理员姓名" maxlength="64" />
150
+        <el-form-item v-if="accountForm.bindType === 'SYS_USER'" label="选择管理员" prop="bindUserId">
151
+          <el-select
152
+            v-model="accountForm.bindUserId"
153
+            filterable
154
+            remote
155
+            reserve-keyword
156
+            placeholder="输入用户名/手机号搜索"
157
+            :remote-method="searchAdminUser"
158
+            :loading="bindSearchLoading"
159
+            style="width: 100%"
160
+          >
161
+            <el-option v-for="item in adminUserList" :key="item.userId" :label="formatAdminOption(item)" :value="item.userId" />
162
+          </el-select>
140 163
         </el-form-item>
141
-        <el-form-item label="重置密码" prop="password">
142
-          <el-input v-model="accountForm.password" type="password" placeholder="留空则不修改" maxlength="20" show-password />
164
+        <el-form-item v-if="accountForm.bindType === 'MEMBER'" label="选择会员" prop="bindMemberId">
165
+          <el-select
166
+            v-model="accountForm.bindMemberId"
167
+            filterable
168
+            remote
169
+            reserve-keyword
170
+            placeholder="输入会员名称/手机号搜索"
171
+            :remote-method="searchMember"
172
+            :loading="bindSearchLoading"
173
+            style="width: 100%"
174
+          >
175
+            <el-option v-for="item in memberList" :key="item.memberId" :label="formatMemberOption(item)" :value="item.memberId" />
176
+          </el-select>
143 177
         </el-form-item>
144 178
       </el-form>
145 179
       <div slot="footer">
146
-        <el-button type="primary" @click="submitAccount">保 存</el-button>
180
+        <el-button type="primary" @click="submitAccount">确 定</el-button>
147 181
         <el-button @click="accountOpen = false">取 消</el-button>
148 182
       </div>
149 183
     </el-dialog>
@@ -152,7 +186,7 @@
152 186
 
153 187
 <script>
154 188
 import { listShop, getShop, addShop, updateShop, delShop, deleteShopCheck, getShopAccount, updateShopAccount } from "@/api/agri/shop"
155
-import { selectMerchantList, openShopCheck } from "@/api/agri/merchant"
189
+import { selectMerchantList, openShopCheck, adminUserOptions, memberOptions } from "@/api/agri/merchant"
156 190
 
157 191
 export default {
158 192
   name: "AgriShop",
@@ -169,18 +203,6 @@ export default {
169 203
       }
170 204
       callback()
171 205
     }
172
-    /** 经营账号登录名:11 位手机号(v1.3) */
173
-    const validateLoginName = (rule, value, callback) => {
174
-      if (!value) {
175
-        callback(new Error("请输入登录名"))
176
-        return
177
-      }
178
-      if (!/^1[3-9]\d{9}$/.test(value)) {
179
-        callback(new Error("登录名须为11位手机号"))
180
-        return
181
-      }
182
-      callback()
183
-    }
184 206
     return {
185 207
       loading: true,
186 208
       showSearch: true,
@@ -194,7 +216,9 @@ export default {
194 216
       merchantAccountOk: false,
195 217
       currentMerchantId: null,
196 218
       originalShopStatus: "0",
197
-      originalLoginName: "",
219
+      bindSearchLoading: false,
220
+      adminUserList: [],
221
+      memberList: [],
198 222
       queryParams: {
199 223
         pageNum: 1,
200 224
         pageSize: 10,
@@ -204,18 +228,34 @@ export default {
204 228
       },
205 229
       form: {},
206 230
       accountForm: {},
231
+      accountRules: {
232
+        bindType: [{ required: true, message: "请选择绑定方式", trigger: "change" }],
233
+        bindUserId: [{
234
+          validator: (rule, value, callback) => {
235
+            if (this.accountForm.bindType === "SYS_USER" && !value) {
236
+              callback(new Error("请选择平台管理员"))
237
+              return
238
+            }
239
+            callback()
240
+          },
241
+          trigger: "change"
242
+        }],
243
+        bindMemberId: [{
244
+          validator: (rule, value, callback) => {
245
+            if (this.accountForm.bindType === "MEMBER" && !value) {
246
+              callback(new Error("请选择会员"))
247
+              return
248
+            }
249
+            callback()
250
+          },
251
+          trigger: "change"
252
+        }]
253
+      },
207 254
       rules: {
208 255
         shopName: [{ required: true, message: "请输入店铺名称", trigger: "blur" }],
209 256
         shopAvatar: [{ required: true, message: "请上传店铺头像", trigger: "change" }],
210 257
         shopPhone: [{ validator: validateShopPhone, trigger: "blur" }],
211 258
         merchantId: [{ required: true, message: "请选择商户", trigger: "change" }]
212
-      },
213
-      accountRules: {
214
-        loginNameRaw: [
215
-          { required: true, message: "请输入登录名", trigger: "blur" },
216
-          { validator: validateLoginName, trigger: "blur" }
217
-        ],
218
-        adminName: [{ required: true, message: "请输入管理员姓名", trigger: "blur" }]
219 259
       }
220 260
     }
221 261
   },
@@ -228,6 +268,17 @@ export default {
228 268
     this.loadMerchantOptions()
229 269
   },
230 270
   methods: {
271
+    /** 评分展示:无评价 —,有评价保留两位小数 */
272
+    formatRating(rating) {
273
+      if (rating == null || rating === "") {
274
+        return "—"
275
+      }
276
+      const num = Number(rating)
277
+      if (Number.isNaN(num)) {
278
+        return "—"
279
+      }
280
+      return num.toFixed(2)
281
+    },
231 282
     /** 查询店铺列表 */
232 283
     getList() {
233 284
       this.loading = true
@@ -311,7 +362,7 @@ export default {
311 362
         getShopAccount(merchantId).then(res => {
312 363
           const data = res.data || {}
313 364
           if (!data.loginName && !data.adminName) {
314
-            this.$modal.msgWarning("该商户尚未配置经营账号,请先在商户管理中完成入驻绑定")
365
+            this.$modal.msgWarning("该商户尚未配置账号,请先在商户管理中完成入驻绑定")
315 366
             this.form.merchantId = undefined
316 367
             return
317 368
           }
@@ -370,7 +421,7 @@ export default {
370 421
           this.doUpdateShop()
371 422
         } else {
372 423
           if (!this.merchantAccountOk) {
373
-            this.$modal.msgWarning("请先选择已绑定经营账号的商户")
424
+            this.$modal.msgWarning("请先选择已绑定账号的商户")
374 425
             return
375 426
           }
376 427
           addShop(this.buildAddPayload()).then(() => {
@@ -395,49 +446,113 @@ export default {
395 446
         this.getList()
396 447
       })
397 448
     },
398
-    /** 打开账号管理 */
449
+    /** 重置更换主账号表单 */
450
+    resetAccountForm() {
451
+      this.accountForm = {}
452
+      this.adminUserList = []
453
+      this.memberList = []
454
+      if (this.$refs.accountForm) {
455
+        this.$refs.accountForm.clearValidate()
456
+      }
457
+    },
458
+    /** 打开更换主账号 */
399 459
     handleAccount(row) {
400 460
       this.currentMerchantId = row.merchantId
401 461
       getShopAccount(row.merchantId).then(response => {
402 462
         const data = response.data || {}
403
-        const loginRaw = data.loginNameRaw || ""
404
-        this.originalLoginName = loginRaw
405 463
         this.accountForm = {
406
-          loginNameRaw: loginRaw,
407
-          loginNameMasked: data.loginName || "",
464
+          accountId: data.accountId,
408 465
           adminName: data.adminName,
409
-          password: undefined,
410
-          shops: data.shops || []
466
+          loginName: data.loginName,
467
+          shops: data.shops || [],
468
+          bindType: "SYS_USER",
469
+          bindUserId: undefined,
470
+          bindMemberId: undefined
411 471
         }
472
+        this.adminUserList = []
473
+        this.memberList = []
412 474
         this.accountOpen = true
413 475
       })
414 476
     },
415
-    /** 保存账号(改登录名需 confirm) */
416
-    submitAccount(confirm) {
477
+    /** 切换绑定方式时清空已选账号 */
478
+    handleAccountBindTypeChange() {
479
+      this.accountForm.bindUserId = undefined
480
+      this.accountForm.bindMemberId = undefined
481
+      this.adminUserList = []
482
+      this.memberList = []
483
+      this.$nextTick(() => {
484
+        if (this.$refs.accountForm) {
485
+          this.$refs.accountForm.clearValidate(["bindUserId", "bindMemberId"])
486
+        }
487
+      })
488
+    },
489
+    /** 管理员下拉展示 */
490
+    formatAdminOption(item) {
491
+      const parts = [item.userName, item.nickName].filter(Boolean)
492
+      if (item.phonenumber) {
493
+        parts.push(item.phonenumber)
494
+      }
495
+      return parts.join(" / ")
496
+    },
497
+    /** 会员下拉展示 */
498
+    formatMemberOption(item) {
499
+      const parts = [item.memberCode, item.nickName].filter(Boolean)
500
+      if (item.mobile) {
501
+        parts.push(item.mobile)
502
+      }
503
+      return parts.join(" / ")
504
+    },
505
+    /** 远程搜索平台管理员 */
506
+    searchAdminUser(keyword) {
507
+      if (!keyword || String(keyword).trim().length < 1) {
508
+        this.adminUserList = []
509
+        return
510
+      }
511
+      this.bindSearchLoading = true
512
+      adminUserOptions(keyword).then(response => {
513
+        this.adminUserList = response.data || []
514
+        this.bindSearchLoading = false
515
+      }).catch(() => {
516
+        this.bindSearchLoading = false
517
+      })
518
+    },
519
+    /** 远程搜索会员 */
520
+    searchMember(keyword) {
521
+      if (!keyword || String(keyword).trim().length < 1) {
522
+        this.memberList = []
523
+        return
524
+      }
525
+      this.bindSearchLoading = true
526
+      memberOptions(keyword).then(response => {
527
+        this.memberList = response.data || []
528
+        this.bindSearchLoading = false
529
+      }).catch(() => {
530
+        this.bindSearchLoading = false
531
+      })
532
+    },
533
+    /** 提交更换主账号(换绑 account_id,不改 sys_user 资料) */
534
+    submitAccount() {
417 535
       this.$refs["accountForm"].validate(valid => {
418 536
         if (!valid) {
419 537
           return
420 538
         }
421
-        const payload = {
422
-          loginName: this.accountForm.loginNameRaw,
423
-          adminName: this.accountForm.adminName,
424
-          password: this.accountForm.password || null
425
-        }
426
-        // 改登录名时走两步 confirm(v1.3);未改登录名则不传 confirm
427
-        if (this.accountForm.loginNameRaw !== this.originalLoginName) {
428
-          payload.confirm = confirm === true
429
-        }
430
-        updateShopAccount(this.currentMerchantId, payload).then(response => {
431
-          if (response.data && response.data.needConfirm) {
432
-            this.$modal.confirm(response.data.confirmMessage || "将同步至该商户下全部店铺,是否继续?").then(() => {
433
-              this.submitAccount(true)
434
-            }).catch(() => {})
539
+        const payload = { bindType: this.accountForm.bindType }
540
+        if (payload.bindType === "SYS_USER") {
541
+          if (this.accountForm.bindUserId === this.accountForm.accountId) {
542
+            this.$modal.msgWarning("所选账号与当前主账号相同")
435 543
             return
436 544
           }
437
-          this.$modal.msgSuccess("保存成功")
545
+          payload.bindUserId = this.accountForm.bindUserId
546
+        } else {
547
+          payload.bindMemberId = this.accountForm.bindMemberId
548
+        }
549
+        this.$modal.confirm("更换后将同步至该商户下全部店铺,是否继续?").then(() => {
550
+          return updateShopAccount(this.currentMerchantId, payload)
551
+        }).then(() => {
552
+          this.$modal.msgSuccess("更换成功")
438 553
           this.accountOpen = false
439 554
           this.getList()
440
-        })
555
+        }).catch(() => {})
441 556
       })
442 557
     },
443 558
     /** 删除店铺(先预检再确认) */
@@ -449,7 +564,7 @@ export default {
449 564
           this.$modal.msgWarning(reasons || "当前店铺不满足删除条件")
450 565
           return
451 566
         }
452
-        this.$modal.confirm("删除后不可恢复,是否继续?").then(() => {
567
+        this.$modal.confirm("删除后店铺不可恢复经营,是否继续?").then(() => {
453 568
           return delShop(row.shopId)
454 569
         }).then(() => {
455 570
           this.$modal.msgSuccess("删除成功")
@@ -477,7 +592,7 @@ export default {
477 592
   margin-bottom: 12px;
478 593
 }
479 594
 .shop-tags {
480
-  padding-left: 100px;
595
+  line-height: 1.8;
481 596
 }
482 597
 .shop-tag {
483 598
   margin-right: 8px;

+ 14 - 1
ruoyi-ui/src/views/agri/seller/goods/detail.vue

@@ -103,7 +103,7 @@
103 103
       <div v-if="detail.detailContent" class="detail-content mb16" v-html="detail.detailContent"></div>
104 104
 
105 105
       <div class="action-bar">
106
-        <el-button v-if="detail.canEdit !== false" type="primary" plain @click="handleEdit" v-hasPermi="['agri:seller:goods:edit']">编辑</el-button>
106
+        <el-button v-if="canShowEdit" type="primary" plain @click="handleEdit" v-hasPermi="['agri:seller:goods:edit']">编辑</el-button>
107 107
         <el-button v-if="detail.canSubmit" type="success" @click="handleSubmit" v-hasPermi="['agri:seller:goods:edit']">提交上架</el-button>
108 108
         <el-button v-if="detail.canOffShelf" type="warning" plain @click="handleOffShelf" v-hasPermi="['agri:seller:goods:offshelf']">下架</el-button>
109 109
         <el-button v-if="detail.canDelete" type="danger" plain @click="handleDelete" v-hasPermi="['agri:seller:goods:remove']">删除</el-button>
@@ -133,6 +133,15 @@ export default {
133 133
       detail: {}
134 134
     }
135 135
   },
136
+  computed: {
137
+    /** 未上架 / 审核失败 / 已下架可编辑;优先用后端 canEdit */
138
+    canShowEdit() {
139
+      if (this.detail.canEdit === true) return true
140
+      if (this.detail.canEdit === false) return false
141
+      const status = this.detail.goodsStatus
142
+      return status === "0" || status === "3" || status === "4"
143
+    }
144
+  },
136 145
   watch: {
137 146
     visible(val) {
138 147
       this.localVisible = val
@@ -180,6 +189,10 @@ export default {
180 189
     },
181 190
     /** 跳转编辑 */
182 191
     handleEdit() {
192
+      if (!this.canShowEdit) {
193
+        this.$modal.msgWarning("当前状态不可编辑;出售中须先下架,待审核须等待平台审核结果")
194
+        return
195
+      }
183 196
       this.$emit("edit", this.detail)
184 197
       this.localVisible = false
185 198
     },

+ 62 - 7
ruoyi-ui/src/views/agri/seller/goods/index.vue

@@ -28,7 +28,9 @@
28 28
       <el-tabs v-model="statusTab" @tab-click="handleTabClick">
29 29
         <el-tab-pane label="全部" name="all" />
30 30
         <el-tab-pane label="未上架" name="0" />
31
-        <el-tab-pane label="待审核" name="1" />
31
+        <el-tab-pane name="1">
32
+          <span slot="label">待审核<el-badge v-if="pendingCount > 0" :value="pendingCount" class="tab-badge" /></span>
33
+        </el-tab-pane>
32 34
         <el-tab-pane label="出售中" name="2" />
33 35
         <el-tab-pane label="审核失败" name="3" />
34 36
         <el-tab-pane label="已下架" name="4" />
@@ -63,9 +65,14 @@
63 65
         <el-table-column label="库存" align="center" prop="stock" width="70" />
64 66
         <el-table-column label="销量" align="center" prop="salesCount" width="70" />
65 67
         <el-table-column label="商品分类" align="center" prop="categoryPath" min-width="120" :show-overflow-tooltip="true" />
66
-        <el-table-column label="店铺分类" align="center" prop="shopCategoryName" min-width="110" :show-overflow-tooltip="true">
68
+        <el-table-column label="店铺分类" align="center" prop="shopCategoryPath" min-width="110" :show-overflow-tooltip="true">
67 69
           <template slot-scope="scope">
68
-            <span>{{ scope.row.shopCategoryName || '—' }}</span>
70
+            <span>{{ scope.row.shopCategoryPath || scope.row.shopCategoryName || '—' }}</span>
71
+          </template>
72
+        </el-table-column>
73
+        <el-table-column label="提交上架时间" align="center" prop="submitTime" width="160">
74
+          <template slot-scope="scope">
75
+            <span>{{ parseTime(scope.row.submitTime) || '—' }}</span>
69 76
           </template>
70 77
         </el-table-column>
71 78
         <el-table-column label="状态" align="center" prop="goodsStatus" width="90">
@@ -76,7 +83,7 @@
76 83
         <el-table-column label="操作" align="center" width="240" fixed="right">
77 84
           <template slot-scope="scope">
78 85
             <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(scope.row)" v-hasPermi="['agri:seller:goods:query']">详情</el-button>
79
-            <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['agri:seller:goods:edit']">编辑</el-button>
86
+            <el-button v-if="canRowEdit(scope.row)" size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['agri:seller:goods:edit']">编辑</el-button>
80 87
             <el-button v-if="canRowSubmit(scope.row)" size="mini" type="text" icon="el-icon-upload2" @click="handleRowSubmit(scope.row)" v-hasPermi="['agri:seller:goods:edit']">提交上架</el-button>
81 88
             <el-button v-if="scope.row.goodsStatus === '2'" size="mini" type="text" icon="el-icon-bottom" @click="handleRowOffShelf(scope.row)" v-hasPermi="['agri:seller:goods:offshelf']">下架</el-button>
82 89
             <el-button v-if="canRowDelete(scope.row)" size="mini" type="text" icon="el-icon-delete" @click="handleRowDelete(scope.row)" v-hasPermi="['agri:seller:goods:remove']">删除</el-button>
@@ -84,6 +91,10 @@
84 91
         </el-table-column>
85 92
       </el-table>
86 93
 
94
+      <el-empty v-if="!loading && !goodsList.length" description="暂无商品">
95
+        <el-button type="primary" size="small" @click="handleAdd" v-hasPermi="['agri:seller:goods:add']">添加商品</el-button>
96
+      </el-empty>
97
+
87 98
       <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
88 99
     </el-card>
89 100
 
@@ -270,6 +281,7 @@ export default {
270 281
       loading: true,
271 282
       showSearch: true,
272 283
       total: 0,
284
+      pendingCount: 0,
273 285
       goodsList: [],
274 286
       selection: [],
275 287
       detailOpen: false,
@@ -358,12 +370,22 @@ export default {
358 370
     initPage() {
359 371
       this.loadShopContext().then(() => {
360 372
         this.loadFormOptions()
373
+        this.loadPendingCount()
361 374
         this.getList()
362 375
       }).catch(() => {
363 376
         this.loadFormOptions()
377
+        this.loadPendingCount()
364 378
         this.getList()
365 379
       })
366 380
     },
381
+    /** 待审核 Tab 角标数量 */
382
+    loadPendingCount() {
383
+      listSellerGoods({ pageNum: 1, pageSize: 1, goodsStatusQuery: "1" }).then(response => {
384
+        this.pendingCount = response.total || 0
385
+      }).catch(() => {
386
+        this.pendingCount = 0
387
+      })
388
+    },
367 389
     /** 写入 X-Shop-Id 缓存 */
368 390
     loadShopContext() {
369 391
       return getSellerContext().then(response => {
@@ -482,12 +504,30 @@ export default {
482 504
     },
483 505
     /** 是否可提交上架 */
484 506
     canRowSubmit(row) {
507
+      if (row.canSubmit === true) return true
508
+      if (row.canSubmit === false) return false
509
+      return row.goodsStatus === "0" || row.goodsStatus === "3" || row.goodsStatus === "4"
510
+    },
511
+    /** 是否可编辑(待审核、出售中不可编辑) */
512
+    canRowEdit(row) {
513
+      if (row.canEdit === true) return true
514
+      if (row.canEdit === false) return false
485 515
       return row.goodsStatus === "0" || row.goodsStatus === "3" || row.goodsStatus === "4"
486 516
     },
487 517
     /** 是否可删除 */
488 518
     canRowDelete(row) {
519
+      if (row.canDelete === true) return true
520
+      if (row.canDelete === false) return false
489 521
       return this.canRowSubmit(row)
490 522
     },
523
+    /** 批量操作失败时展示 reasons */
524
+    showBatchFailure(error) {
525
+      const data = (error && error.data) || (error && error.response && error.response.data)
526
+      const reasons = data && data.reasons
527
+      if (reasons && reasons.length) {
528
+        this.$modal.msgWarning(reasons.join(";"))
529
+      }
530
+    },
491 531
     handleTabClick() {
492 532
       this.queryParams.goodsStatusQuery = this.statusTab === "all" ? undefined : this.statusTab
493 533
       this.queryParams.pageNum = 1
@@ -686,6 +726,10 @@ export default {
686 726
     },
687 727
     /** 打开编辑商品 */
688 728
     handleUpdate(row) {
729
+      if (!this.canRowEdit(row)) {
730
+        this.$modal.msgWarning("当前状态不可编辑;出售中须先下架,待审核须等待平台审核结果")
731
+        return
732
+      }
689 733
       getSellerGoods(row.goodsId).then(response => {
690 734
         const data = response.data || {}
691 735
         this.form = {
@@ -774,7 +818,9 @@ export default {
774 818
       }).then(() => {
775 819
         this.$modal.msgSuccess("批量提交成功")
776 820
         this.handleSuccess()
777
-      }).catch(() => {})
821
+      }).catch(err => {
822
+        this.showBatchFailure(err)
823
+      })
778 824
     },
779 825
     /** 单条下架 */
780 826
     handleRowOffShelf(row) {
@@ -793,7 +839,9 @@ export default {
793 839
       }).then(() => {
794 840
         this.$modal.msgSuccess("批量下架成功")
795 841
         this.handleSuccess()
796
-      }).catch(() => {})
842
+      }).catch(err => {
843
+        this.showBatchFailure(err)
844
+      })
797 845
     },
798 846
     /** 单条删除 */
799 847
     handleRowDelete(row) {
@@ -812,9 +860,12 @@ export default {
812 860
       }).then(() => {
813 861
         this.$modal.msgSuccess("批量删除成功")
814 862
         this.handleSuccess()
815
-      }).catch(() => {})
863
+      }).catch(err => {
864
+        this.showBatchFailure(err)
865
+      })
816 866
     },
817 867
     handleSuccess() {
868
+      this.loadPendingCount()
818 869
       this.getList()
819 870
     }
820 871
   }
@@ -880,4 +931,8 @@ export default {
880 931
   width: 100%;
881 932
   margin-bottom: 12px;
882 933
 }
934
+.tab-badge {
935
+  margin-left: 6px;
936
+  vertical-align: middle;
937
+}
883 938
 </style>

+ 8 - 0
shop-app/api/merchantEntry.js

@@ -30,6 +30,14 @@ export function submitEntryApply(data) {
30 30
   })
31 31
 }
32 32
 
33
+/** 入驻上下文(是否仅开店铺、商户快照) */
34
+export function getEntryContext() {
35
+  return request({
36
+    url: '/api/merchant/entry/context',
37
+    method: 'GET'
38
+  })
39
+}
40
+
33 41
 /** 我的入驻申请列表 */
34 42
 export function getMyEntryApplies() {
35 43
   return request({

+ 94 - 11
shop-app/subpackage/account/entry-apply.vue

@@ -9,7 +9,17 @@
9 9
 			<text>您有待审核或公示中的申请,请先在「我的入驻申请」查看进度</text>
10 10
 			<button class="mine-btn-outline entry-closed__btn" @click="goList">查看申请</button>
11 11
 		</view>
12
+		<view v-else-if="shopOnlyBlocked" class="entry-closed">
13
+			<text>{{ entryContext.blockReason || '当前无法申请开设新店铺' }}</text>
14
+			<button class="mine-btn-outline entry-closed__btn" @click="goList">查看申请</button>
15
+		</view>
12 16
 		<template v-else>
17
+			<view v-if="shopOnlyMode" class="entry-shop-only-banner">
18
+				<text class="entry-shop-only-banner__title">仅开店铺模式</text>
19
+				<text class="entry-shop-only-banner__tip">您已是商户管理员,本次仅需填写拟开设店铺信息,主体与经营信息沿用已有商户。</text>
20
+				<text v-if="entryContext.merchantName" class="entry-shop-only-banner__line">商户名称:{{ entryContext.merchantName }}</text>
21
+				<text v-if="entryContext.subjectLabel" class="entry-shop-only-banner__line">主体:{{ entryContext.subjectLabel }}</text>
22
+			</view>
13 23
 			<view class="entry-steps">
14 24
 				<text
15 25
 					v-for="(s, i) in stepTitles"
@@ -18,8 +28,8 @@
18 28
 				>{{ s }}</text>
19 29
 			</view>
20 30
 			<scroll-view class="entry-scroll" scroll-y>
21
-				<!-- 主体类型 -->
22
-				<view v-if="currentStep === 'type'" class="form-card">
31
+				<!-- 主体类型(新主体申请) -->
32
+				<view v-if="!shopOnlyMode && currentStep === 'type'" class="form-card">
23 33
 					<view
24 34
 						:class="['type-card', { 'type-card--on': form.merchantType === '1' }]"
25 35
 						@click="selectType('1')"
@@ -91,10 +101,17 @@
91 101
 import { ref, reactive, computed } from 'vue'
92 102
 import SafeNavBar from '@/components/common/SafeNavBar.vue'
93 103
 import { onLoad } from '@dcloudio/uni-app'
94
-import { getEntryAgreement, getEntryStatus, getMyEntryApplies, submitEntryApply } from '@/api/merchantEntry'
104
+import { getEntryAgreement, getEntryStatus, getMyEntryApplies, getEntryContext, submitEntryApply } from '@/api/merchantEntry'
95 105
 import { ensureApiToken } from '@/utils/apiAuth'
96 106
 import { hasBlockingApply } from '@/utils/entryConstants'
97
-import { createEntryForm, validateEntryStep, buildEntrySubmitPayload, applyRegion, applyRegRegion } from '@/utils/entryForm'
107
+import {
108
+  createEntryForm,
109
+  validateEntryStep,
110
+  buildEntrySubmitPayload,
111
+  buildShopOnlySubmitPayload,
112
+  applyRegion,
113
+  applyRegRegion
114
+} from '@/utils/entryForm'
98 115
 import { MERCHANT_TYPE_PERSON } from '@/utils/entryConstants'
99 116
 import AgreementBlock from '@/components/account/AgreementBlock.vue'
100 117
 import EntryPersonSubject from '@/components/mine/entry/EntryPersonSubject.vue'
@@ -122,12 +139,32 @@ const agreement = reactive({
122 139
 const stepIndex = ref(0)
123 140
 const entryOpen = ref(true)
124 141
 const blocked = ref(false)
142
+const entryContext = ref({
143
+  shopOnlyMode: false,
144
+  canShopOnlyApply: false,
145
+  blockReason: '',
146
+  merchantId: null,
147
+  merchantType: '',
148
+  subjectLabel: '',
149
+  merchantName: ''
150
+})
125 151
 const regionBiz = ref({ regionCode: '', regionName: '', pathCodes: [] })
126 152
 const regionReg = ref({ regionCode: '', regionName: '', pathCodes: [] })
127 153
 const userStore = useUserStore()
128 154
 
129
-const stepKeys = computed(() => ['type', 'subject', 'biz', 'shop', 'submit'])
130
-const stepTitles = ['类型', '主体', '经营', '店铺', '提交']
155
+/** 可仅填店铺提交(路径 A2) */
156
+const shopOnlyMode = computed(() => !!entryContext.value.shopOnlyMode && !!entryContext.value.canShopOnlyApply)
157
+/** 已是商户管理员但不可仅开店铺 */
158
+const shopOnlyBlocked = computed(() => !!entryContext.value.shopOnlyMode && !entryContext.value.canShopOnlyApply)
159
+
160
+const stepKeys = computed(() => {
161
+  if (shopOnlyMode.value) return ['shop', 'submit']
162
+  return ['type', 'subject', 'biz', 'shop', 'submit']
163
+})
164
+const stepTitles = computed(() => {
165
+  if (shopOnlyMode.value) return ['店铺', '提交']
166
+  return ['类型', '主体', '经营', '店铺', '提交']
167
+})
131 168
 const currentStep = computed(() => stepKeys.value[stepIndex.value])
132 169
 const isPerson = computed(() => form.merchantType === MERCHANT_TYPE_PERSON)
133 170
 const nextBtnText = computed(() => {
@@ -141,8 +178,8 @@ onLoad(() => {
141 178
 })
142 179
 
143 180
 function initPage() {
144
-  Promise.all([getEntryAgreement(), getEntryStatus(), getMyEntryApplies()])
145
-    .then(([agRes, stRes, myRes]) => {
181
+  Promise.all([getEntryAgreement(), getEntryStatus(), getMyEntryApplies(), getEntryContext()])
182
+    .then(([agRes, stRes, myRes, ctxRes]) => {
146 183
       const ag = agRes.data || {}
147 184
       agreement.enabled = !!ag.enabled
148 185
       agreement.message = ag.message || ''
@@ -152,7 +189,22 @@ function initPage() {
152 189
       agreement.checkboxLabel = ag.checkboxLabel || agreement.checkboxLabel
153 190
       entryOpen.value = stRes.data?.entryOpen !== false && agreement.enabled
154 191
       blocked.value = hasBlockingApply(myRes.data || [])
155
-      if (entryOpen.value && !blocked.value) {
192
+      entryContext.value = {
193
+        shopOnlyMode: !!ctxRes.data?.shopOnlyMode,
194
+        canShopOnlyApply: !!ctxRes.data?.canShopOnlyApply,
195
+        blockReason: ctxRes.data?.blockReason || '',
196
+        merchantId: ctxRes.data?.merchantId,
197
+        merchantType: ctxRes.data?.merchantType || '',
198
+        subjectLabel: ctxRes.data?.subjectLabel || '',
199
+        merchantName: ctxRes.data?.merchantName || ''
200
+      }
201
+      if (shopOnlyMode.value) {
202
+        form.merchantType = entryContext.value.merchantType || MERCHANT_TYPE_PERSON
203
+        form.shop = createEntryForm(form.merchantType).shop
204
+        form.agreementAccepted = false
205
+        stepIndex.value = 0
206
+      }
207
+      if (entryOpen.value && !blocked.value && !shopOnlyBlocked.value) {
156 208
         ensureMemberNickName()
157 209
       }
158 210
     })
@@ -226,13 +278,19 @@ function doSubmit() {
226 278
     ensureMemberNickName()
227 279
     return
228 280
   }
281
+  const confirmTip = shopOnlyMode.value
282
+    ? '提交后不可修改,将复用已有商户并申请开设新店铺,是否确认?'
283
+    : '提交后不可修改,是否确认?'
229 284
   uni.showModal({
230 285
     title: '确认提交',
231
-    content: '提交后不可修改,是否确认?',
286
+    content: confirmTip,
232 287
     success: (res) => {
233 288
       if (!res.confirm) return
234 289
       submitGuard.run(async () => {
235
-        await submitEntryApply(buildEntrySubmitPayload(form))
290
+        const payload = shopOnlyMode.value
291
+          ? buildShopOnlySubmitPayload(form)
292
+          : buildEntrySubmitPayload(form)
293
+        await submitEntryApply(payload)
236 294
         uni.showToast({ title: '提交成功,请等待审核', icon: 'none', duration: 2500 })
237 295
         setTimeout(() => {
238 296
           uni.redirectTo({ url: PAGE_ENTRY_LIST })
@@ -338,4 +396,29 @@ function goList() {
338 396
   font-size: 26rpx;
339 397
   color: #666;
340 398
 }
399
+.entry-shop-only-banner {
400
+  margin: 16rpx 24rpx 0;
401
+  padding: 24rpx;
402
+  background: #e8f5e9;
403
+  border-radius: 12rpx;
404
+}
405
+.entry-shop-only-banner__title {
406
+  display: block;
407
+  font-size: 28rpx;
408
+  font-weight: 600;
409
+  color: #2e7d32;
410
+}
411
+.entry-shop-only-banner__tip {
412
+  display: block;
413
+  margin-top: 8rpx;
414
+  font-size: 24rpx;
415
+  color: #4a6b4f;
416
+  line-height: 1.5;
417
+}
418
+.entry-shop-only-banner__line {
419
+  display: block;
420
+  margin-top: 8rpx;
421
+  font-size: 24rpx;
422
+  color: #5c5652;
423
+}
341 424
 </style>

+ 1 - 0
shop-app/subpackage/account/entry-detail.vue

@@ -37,6 +37,7 @@
37 37
 			<view v-if="detail.applyStatus === APPLY_STATUS_DONE" class="detail-done">
38 38
 				<text>入驻已完成,请使用经营账号登录商家后台进行发品与店铺经营。</text>
39 39
 				<text v-if="detail.merchantId" class="detail-done__sub">商户 ID:{{ detail.merchantId }}</text>
40
+				<text v-if="detail.shopId" class="detail-done__sub">店铺 ID:{{ detail.shopId }}</text>
40 41
 			</view>
41 42
 		</view>
42 43
 		<view v-if="detail.applyStatus === APPLY_STATUS_REJECT && canReapply" class="form-footer">

+ 11 - 1
shop-app/subpackage/account/entry-list.vue

@@ -21,6 +21,10 @@
21 21
 					</text>
22 22
 				</view>
23 23
 				<text class="entry-card__time">申请时间:{{ item.applyTime || '—' }}</text>
24
+				<text
25
+					v-if="item.applyStatus === APPLY_STATUS_PUBLICITY && formatPublicityPeriod(item)"
26
+					class="entry-card__publicity"
27
+				>公示期:{{ formatPublicityPeriod(item) }}</text>
24 28
 				<text
25 29
 					v-if="item.applyStatus === '2' && item.rejectReason"
26 30
 					class="entry-card__reject"
@@ -41,7 +45,7 @@ import { onShow } from '@dcloudio/uni-app'
41 45
 import { getMyEntryApplies } from '@/api/merchantEntry'
42 46
 import { getEntryStatus } from '@/api/merchantEntry'
43 47
 import { ensureApiToken } from '@/utils/apiAuth'
44
-import { formatApplyStatus, canSubmitNewApply } from '@/utils/entryConstants'
48
+import { formatApplyStatus, canSubmitNewApply, formatPublicityPeriod, APPLY_STATUS_PUBLICITY } from '@/utils/entryConstants'
45 49
 import { PAGE_ENTRY_APPLY, PAGE_ENTRY_DETAIL } from '@/utils/pageRoute'
46 50
 
47 51
 const list = ref([])
@@ -131,6 +135,12 @@ function goApply() {
131 135
   font-size: 24rpx;
132 136
   color: #999;
133 137
 }
138
+.entry-card__publicity {
139
+  display: block;
140
+  margin-top: 12rpx;
141
+  font-size: 24rpx;
142
+  color: #1976d2;
143
+}
134 144
 .entry-card__reject {
135 145
   display: block;
136 146
   margin-top: 12rpx;

+ 2 - 2
shop-app/subpackage/account/profile.vue

@@ -1,10 +1,10 @@
1 1
 <template>
2 2
 	<view class="form-page">
3 3
 		<view class="form-card">
4
-			<!-- <view class="form-row">
4
+			<view class="form-row">
5 5
 				<text class="form-row__label">用户 ID</text>
6 6
 				<text class="form-row__readonly">{{ profile.memberId || '—' }}</text>
7
-			</view> -->
7
+			</view>
8 8
 			<view class="form-row">
9 9
 				<text class="form-row__label">会员名称</text>
10 10
 				<text class="form-row__readonly">{{ profile.memberCode || '—' }}</text>

+ 12 - 0
shop-app/utils/entryConstants.js

@@ -56,3 +56,15 @@ export function canSubmitNewApply(list, entryOpen) {
56 56
   if (!entryOpen) return false
57 57
   return !hasBlockingApply(list)
58 58
 }
59
+
60
+/** 公示时间展示(列表/详情) */
61
+export function formatPublicityPeriod(item) {
62
+  if (!item || (item.applyStatus !== APPLY_STATUS_PUBLICITY && item.applyStatus !== APPLY_STATUS_DONE)) {
63
+    return ''
64
+  }
65
+  const start = item.publicityStartTime || ''
66
+  const end = item.publicityEndTime || ''
67
+  if (!start && !end) return ''
68
+  if (start && end) return `${start} 至 ${end}`
69
+  return start || end
70
+}

+ 9 - 1
shop-app/utils/entryForm.js

@@ -226,7 +226,7 @@ export function validateEntryStep(form, stepKey) {
226 226
   return ''
227 227
 }
228 228
 
229
-/** 提交前组装 DTO */
229
+/** 提交前组装 DTO(新主体完整申请) */
230 230
 export function buildEntrySubmitPayload(form) {
231 231
   return {
232 232
     subject: { ...form.subject, merchantType: form.merchantType },
@@ -235,3 +235,11 @@ export function buildEntrySubmitPayload(form) {
235 235
     agreementAccepted: !!form.agreementAccepted
236 236
   }
237 237
 }
238
+
239
+/** 仅开店铺:仅提交 shop + 协议勾选(主体/经营由后端合并已有商户) */
240
+export function buildShopOnlySubmitPayload(form) {
241
+  return {
242
+    shop: { ...form.shop },
243
+    agreementAccepted: !!form.agreementAccepted
244
+  }
245
+}