Ver código fonte

基础管理

xsh_1997 1 semana atrás
pai
commit
8652b9554e

+ 194 - 0
doc/平台后台/商城设置/商城设置前端技术方案.md

@@ -0,0 +1,194 @@
1
+# 商城设置 — 前端技术方案
2
+
3
+> **依据:** 《商城设置功能需求.md》v1.1、《商城设置技术方案.md》v1.1  
4
+> **前端规范:** `doc/前端设计/前端设计.md`  
5
+> **范围:** 仅 **ruoyi-ui 平台端** 全平台商城设置 **单页分区表单**(备案、商品、订单、会员默认头像);**不含** 店铺设置、C 端/商家端页面。  
6
+> **实现状态:** `index.vue`、`api/agri/mall/setting.js` **已落地**;待菜单配置及 `/agri/mallSetting` 联调。
7
+
8
+---
9
+
10
+## 1. 技术栈与写法约定
11
+
12
+| 项 | 说明 |
13
+|----|------|
14
+| 框架 | Vue 2 + Element UI |
15
+| 请求 | `@/utils/request`(**无** `X-Shop-Id`;平台全平台配置) |
16
+| 参考页面 | `agri/org/shopSetting/index.vue`(单例配置 GET/PUT)、`agri/content/mallServiceAgreement/index.vue`(分区卡片 + 只读项) |
17
+| 布局 | 多个 `el-card` 分区 + `<br/>` 分隔;**非** 检索+表格 CRUD |
18
+| 图片 | 全局 `image-upload` + `image-preview` |
19
+| URL 校验 | `@/utils/validate` · `validURL` |
20
+
21
+---
22
+
23
+## 2. 业务要点(前端需体现)
24
+
25
+| 项 | 说明 |
26
+|----|------|
27
+| 单例配置 | 全平台 **一套**;与 **店铺设置** 菜单、接口 **独立** |
28
+| 只读项 | 货币符 ¥、货币中文「元」、编号前缀 SN、已支付/已发货可取消(固定 **否**)— **展示不可改** |
29
+| 可编辑项 | 备案四字段、是否显示销量、自动确认收货天数、售后申请限制天数、会员默认头像 |
30
+| 备案 | 号与链接 **可空**;链接非空须 http/https |
31
+| 销量开关 | `showSales`:`1` 是 / `0` 否;关闭后 C 端 **隐藏销量数字**,排序逻辑不变 |
32
+| 订单天数 | 自动确认 **1~90** 自然日;售后限制 **1~365** 自然日(自确认收货起) |
33
+| 默认头像 | **必填**;png/jpg/jpeg,≤5MB;更换不覆盖已有自定义头像会员 |
34
+| 保存体 | **仅** 可编辑字段;只读项 **不参与** PUT |
35
+
36
+---
37
+
38
+## 3. 文件清单
39
+
40
+| 类型 | 路径 | 说明 |
41
+|------|------|------|
42
+| 配置页 | `ruoyi-ui/src/views/agri/mall/setting/index.vue` | 分区表单、校验、保存/重置 |
43
+| API | `ruoyi-ui/src/api/agri/mall/setting.js` | GET/PUT `/agri/mallSetting` |
44
+
45
+**组件 name(keep-alive):** `AgriMallSetting`
46
+
47
+**不提供:** 商家发品 `entryHints` 调用(商家端另册);C 端 config 拉取;配置导出/审计。
48
+
49
+---
50
+
51
+## 4. 菜单与路由
52
+
53
+| 菜单名称 | 组件路径 | 路由 path(建议) | 权限标识 |
54
+|----------|----------|-------------------|----------|
55
+| 商城设置 | `agri/mall/setting/index` | `mall/setting` | 见下表 |
56
+
57
+**上级菜单:** 平台管理端 → **内容管理**(与 Banner、商城服务协议同级,以实现 SQL 为准)
58
+
59
+| 能力 | 权限 | 页面落点 |
60
+|------|------|----------|
61
+| 查看配置 | `agri:mall:setting:query` | 进入页面、GET 加载 |
62
+| 保存配置 | `agri:mall:setting:edit` | 「保存」按钮 `v-hasPermi` |
63
+
64
+---
65
+
66
+## 5. 页面结构(与代码一致)
67
+
68
+```text
69
+商城设置 index.vue
70
+├── 页头说明 card
71
+│   └── 全平台生效提示 + 与店铺设置/订单超时分工说明
72
+├── <br/>
73
+├── 备案信息 card
74
+│   ├── ICP 备案号 icpNo
75
+│   ├── ICP 备案链接 icpLink
76
+│   ├── 公安备案号 psbNo
77
+│   └── 公安备案链接 psbLink
78
+├── <br/>
79
+├── 商品设置 card
80
+│   ├── 商品货币符 currencySymbol(只读 ¥)
81
+│   ├── 商品货币中文 currencyNameCn(只读 元)
82
+│   ├── 商品编号前缀 goodsSnPrefix(只读 SN)
83
+│   └── 是否显示销量 showSales(单选 1/0)
84
+├── <br/>
85
+├── 订单设置 card
86
+│   ├── 自动确认收货 autoConfirmDays(1~90)
87
+│   ├── 买家申请售后限制 aftersaleLimitDays(1~365)
88
+│   ├── 已支付订单可取消 paidOrderCancelable(只读 否)
89
+│   └── 已发货订单可取消发货 shippedOrderCancelable(只读 否)
90
+├── <br/>
91
+├── 会员默认头像 card
92
+│   ├── image-upload defaultAvatar
93
+│   └── image-preview 预览
94
+└── <br/>
95
+    └── 操作 card:保存 / 重置(重新 GET)
96
+```
97
+
98
+---
99
+
100
+## 6. API 封装
101
+
102
+**模块:** `ruoyi-ui/src/api/agri/mall/setting.js`
103
+
104
+| 方法 | HTTP | 路径 | 说明 |
105
+|------|------|------|------|
106
+| `getMallSetting()` | GET | `/agri/mallSetting` | 加载 `MallSettingVO` |
107
+| `saveMallSetting(data)` | PUT | `/agri/mallSetting` | 提交 `MallSettingSaveDTO` |
108
+
109
+### 6.1 GET 响应字段(前端使用)
110
+
111
+| 字段 | 类型 | 页面用途 |
112
+|------|------|----------|
113
+| icpNo / icpLink / psbNo / psbLink | string | 备案输入 |
114
+| currencySymbol / currencyNameCn | string | 只读展示 |
115
+| goodsSnPrefix | string | 只读展示 |
116
+| showSales | `"0"` \| `"1"` | 单选绑定 |
117
+| autoConfirmDays | number | `el-input-number` |
118
+| aftersaleLimitDays | number | `el-input-number` |
119
+| paidOrderCancelable / shippedOrderCancelable | boolean | 只读「是/否」 |
120
+| defaultAvatar | string | 图片 URL |
121
+
122
+### 6.2 PUT 请求体(仅可编辑项)
123
+
124
+```json
125
+{
126
+  "icpNo": "",
127
+  "icpLink": "",
128
+  "psbNo": "",
129
+  "psbLink": "",
130
+  "showSales": "1",
131
+  "autoConfirmDays": 7,
132
+  "aftersaleLimitDays": 7,
133
+  "defaultAvatar": "https://..."
134
+}
135
+```
136
+
137
+**不传:** `currencySymbol`、`currencyNameCn`、`goodsSnPrefix`、取消开关只读字段。
138
+
139
+---
140
+
141
+## 7. 交互与校验
142
+
143
+| 校验项 | 前端规则 | 失败提示 |
144
+|--------|----------|----------|
145
+| icpLink / psbLink | 非空时 `validURL` | 备案链接格式不正确 |
146
+| autoConfirmDays | 必填;组件 min=1 max=90 | 请填写自动确认收货天数 |
147
+| aftersaleLimitDays | 必填;组件 min=1 max=365 | 请填写售后申请限制天数 |
148
+| showSales | 必选 | 请选择是否显示销量 |
149
+| defaultAvatar | 必填 | 请上传会员默认头像 |
150
+
151
+| 操作 | 行为 |
152
+|------|------|
153
+| 保存 | `validate` → PUT → 成功提示 → 重新 GET |
154
+| 重置 | 放弃未保存修改,重新 `loadConfig` |
155
+
156
+---
157
+
158
+## 8. 与兄弟模块区分
159
+
160
+| 模块 | 路径 | 职责 |
161
+|------|------|------|
162
+| **商城设置(本页)** | `agri/mall/setting` | 备案、销量展示、订单时限、默认头像 |
163
+| 店铺设置 | `agri/org/shopSetting` | 商品默认审核、子管理员上限 |
164
+| 商城服务协议 | `agri/content/mallServiceAgreement` | 买家注册协议 |
165
+| 首页 Banner | `agri/content/banner` | 首页运营位 |
166
+
167
+---
168
+
169
+## 9. 联调与验收对照
170
+
171
+| 编号 | 场景 | 前端预期 |
172
+|------|------|----------|
173
+| MC-T1 | 保存 ICP 号+链接 | 保存成功;C 端页脚联调可见(后端/C 端) |
174
+| MC-T2 | 关闭显示销量 | `showSales=0` 提交成功 |
175
+| MC-T6 | 只读项 | 货币/前缀/取消开关 **不可编辑** |
176
+| MC-A1 | 未传头像保存 | 前端校验阻断 |
177
+| — | 首次 GET 无数据 | 后端 lazy init 默认值后返回;前端兜底默认 7 天、销量开 |
178
+
179
+---
180
+
181
+## 10. 菜单配置示例(运维)
182
+
183
+```text
184
+菜单名称:商城设置
185
+组件路径:agri/mall/setting/index
186
+路由地址:mall/setting
187
+权限字符:agri:mall:setting:query
188
+按钮:agri:mall:setting:edit(保存)
189
+父级:内容管理
190
+```
191
+
192
+---
193
+
194
+*文档版本:v1.0 · 关联《商城设置功能需求.md》v1.1、《商城设置技术方案.md》v1.1*

+ 18 - 0
ruoyi-ui/src/api/agri/mall/setting.js

@@ -0,0 +1,18 @@
1
+import request from '@/utils/request'
2
+
3
+// 获取全平台商城设置
4
+export function getMallSetting() {
5
+  return request({
6
+    url: '/agri/mallSetting',
7
+    method: 'get'
8
+  })
9
+}
10
+
11
+// 保存全平台商城设置
12
+export function saveMallSetting(data) {
13
+  return request({
14
+    url: '/agri/mallSetting',
15
+    method: 'put',
16
+    data: data
17
+  })
18
+}

+ 313 - 0
ruoyi-ui/src/views/agri/mall/setting/index.vue

@@ -0,0 +1,313 @@
1
+<template>
2
+  <div class="app-container mall-setting-page">
3
+    <el-form ref="form" :model="form" :rules="rules" label-width="180px">
4
+      <!-- 页头说明 -->
5
+      <el-card v-loading="loading" shadow="never" class="section-card">
6
+        <div slot="header" class="card-header">
7
+          <span>商城设置</span>
8
+          <span class="header-tip">全平台商城设置,修改后对全站 C 端及商家发品生效</span>
9
+        </div>
10
+        <p class="page-desc">与「店铺设置」独立:本页维护备案、商品展示、订单时限与会员默认头像;支付超时关单请在订单参数中配置。</p>
11
+      </el-card>
12
+
13
+      <br/>
14
+
15
+      <!-- 备案信息 -->
16
+      <el-card shadow="never" class="section-card">
17
+        <div slot="header">备案信息</div>
18
+        <el-row :gutter="20">
19
+          <el-col :span="12">
20
+            <el-form-item label="ICP 备案号" prop="icpNo">
21
+              <el-input v-model="form.icpNo" placeholder="如:京 ICP 备 xxxxx 号" maxlength="128" clearable />
22
+            </el-form-item>
23
+          </el-col>
24
+          <el-col :span="12">
25
+            <el-form-item label="ICP 备案链接" prop="icpLink">
26
+              <el-input v-model="form.icpLink" placeholder="https://..." maxlength="512" clearable />
27
+            </el-form-item>
28
+          </el-col>
29
+          <el-col :span="12">
30
+            <el-form-item label="公安备案号" prop="psbNo">
31
+              <el-input v-model="form.psbNo" placeholder="如:京公网安备 xxxxx 号" maxlength="128" clearable />
32
+            </el-form-item>
33
+          </el-col>
34
+          <el-col :span="12">
35
+            <el-form-item label="公安备案链接" prop="psbLink">
36
+              <el-input v-model="form.psbLink" placeholder="https://..." maxlength="512" clearable />
37
+            </el-form-item>
38
+          </el-col>
39
+        </el-row>
40
+        <div class="form-tip">备案号与链接均填写时 C 端才会展示对应项;链接须为 http/https 格式。</div>
41
+      </el-card>
42
+
43
+      <br/>
44
+
45
+      <!-- 商品设置 -->
46
+      <el-card shadow="never" class="section-card">
47
+        <div slot="header">商品设置</div>
48
+        <el-row :gutter="20">
49
+          <el-col :span="12">
50
+            <el-form-item label="商品货币符">
51
+              <span class="readonly-text">{{ form.currencySymbol || '¥' }}</span>
52
+            </el-form-item>
53
+          </el-col>
54
+          <el-col :span="12">
55
+            <el-form-item label="商品货币中文">
56
+              <span class="readonly-text">{{ form.currencyNameCn || '元' }}</span>
57
+            </el-form-item>
58
+          </el-col>
59
+          <el-col :span="12">
60
+            <el-form-item label="商品编号前缀">
61
+              <span class="readonly-text">{{ form.goodsSnPrefix || 'SN' }}</span>
62
+            </el-form-item>
63
+          </el-col>
64
+          <el-col :span="12">
65
+            <el-form-item label="是否显示销量" prop="showSales">
66
+              <el-radio-group v-model="form.showSales">
67
+                <el-radio label="1">是</el-radio>
68
+                <el-radio label="0">否</el-radio>
69
+              </el-radio-group>
70
+            </el-form-item>
71
+          </el-col>
72
+        </el-row>
73
+        <div class="form-tip">关闭「显示销量」后 C 端商品详情与列表不展示销量数字,热销排序仍按销量字段计算。</div>
74
+      </el-card>
75
+
76
+      <br/>
77
+
78
+      <!-- 订单设置 -->
79
+      <el-card shadow="never" class="section-card">
80
+        <div slot="header">订单设置</div>
81
+        <el-row :gutter="20">
82
+          <el-col :span="12">
83
+            <el-form-item label="自动确认收货" prop="autoConfirmDays">
84
+              <el-input-number
85
+                v-model="form.autoConfirmDays"
86
+                :min="1"
87
+                :max="90"
88
+                :precision="0"
89
+                controls-position="right"
90
+              />
91
+              <span class="form-tip-inline">天(已发货后,自然日)</span>
92
+            </el-form-item>
93
+          </el-col>
94
+          <el-col :span="12">
95
+            <el-form-item label="买家申请售后限制" prop="aftersaleLimitDays">
96
+              <el-input-number
97
+                v-model="form.aftersaleLimitDays"
98
+                :min="1"
99
+                :max="365"
100
+                :precision="0"
101
+                controls-position="right"
102
+              />
103
+              <span class="form-tip-inline">天(自确认收货起)</span>
104
+            </el-form-item>
105
+          </el-col>
106
+          <el-col :span="12">
107
+            <el-form-item label="已支付订单可取消">
108
+              <span class="readonly-text">{{ formatReadonlyBool(form.paidOrderCancelable) }}</span>
109
+            </el-form-item>
110
+          </el-col>
111
+          <el-col :span="12">
112
+            <el-form-item label="已发货订单可取消发货">
113
+              <span class="readonly-text">{{ formatReadonlyBool(form.shippedOrderCancelable) }}</span>
114
+            </el-form-item>
115
+          </el-col>
116
+        </el-row>
117
+        <div class="form-tip">自动确认收货与《订单管理》完成规则一致;在途已发货订单保存后按新天数重新判定。</div>
118
+      </el-card>
119
+
120
+      <br/>
121
+
122
+      <!-- 会员默认头像 -->
123
+      <el-card shadow="never" class="section-card">
124
+        <div slot="header">会员默认头像</div>
125
+        <el-form-item label="移动端默认头像" prop="defaultAvatar">
126
+          <image-upload
127
+            v-model="form.defaultAvatar"
128
+            :limit="1"
129
+            :file-size="5"
130
+            :file-type="['png', 'jpg', 'jpeg']"
131
+          />
132
+        </el-form-item>
133
+        <div v-if="form.defaultAvatar" class="avatar-preview">
134
+          <span class="preview-label">预览:</span>
135
+          <image-preview :src="form.defaultAvatar" :width="80" :height="80" />
136
+        </div>
137
+        <div class="form-tip">新注册或未上传头像的会员将展示此图片;已有自定义头像的会员不受影响。</div>
138
+      </el-card>
139
+
140
+      <br/>
141
+
142
+      <!-- 操作按钮 -->
143
+      <el-card shadow="never" class="section-card">
144
+        <el-form-item label-width="0">
145
+          <el-button type="primary" @click="submitForm" v-hasPermi="['agri:mall:setting:edit']">保 存</el-button>
146
+          <el-button @click="loadConfig">重 置</el-button>
147
+        </el-form-item>
148
+      </el-card>
149
+    </el-form>
150
+  </div>
151
+</template>
152
+
153
+<script>
154
+import { getMallSetting, saveMallSetting } from "@/api/agri/mall/setting"
155
+import { validURL } from "@/utils/validate"
156
+
157
+export default {
158
+  name: "AgriMallSetting",
159
+  data() {
160
+    // 备案链接:非空时须为合法 http/https URL
161
+    const validateFilingLink = (rule, value, callback) => {
162
+      if (!value || !String(value).trim()) {
163
+        callback()
164
+        return
165
+      }
166
+      if (!validURL(value)) {
167
+        callback(new Error("备案链接格式不正确,须为 http 或 https 地址"))
168
+        return
169
+      }
170
+      callback()
171
+    }
172
+    return {
173
+      // 页面加载遮罩
174
+      loading: true,
175
+      // 表单数据
176
+      form: {
177
+        icpNo: "",
178
+        icpLink: "",
179
+        psbNo: "",
180
+        psbLink: "",
181
+        currencySymbol: "¥",
182
+        currencyNameCn: "元",
183
+        goodsSnPrefix: "SN",
184
+        showSales: "1",
185
+        autoConfirmDays: 7,
186
+        aftersaleLimitDays: 7,
187
+        paidOrderCancelable: false,
188
+        shippedOrderCancelable: false,
189
+        defaultAvatar: ""
190
+      },
191
+      // 表单校验规则
192
+      rules: {
193
+        icpLink: [{ validator: validateFilingLink, trigger: "blur" }],
194
+        psbLink: [{ validator: validateFilingLink, trigger: "blur" }],
195
+        showSales: [{ required: true, message: "请选择是否显示销量", trigger: "change" }],
196
+        autoConfirmDays: [{ required: true, message: "请填写自动确认收货天数", trigger: "blur" }],
197
+        aftersaleLimitDays: [{ required: true, message: "请填写售后申请限制天数", trigger: "blur" }],
198
+        defaultAvatar: [{ required: true, message: "请上传会员默认头像", trigger: "change" }]
199
+      }
200
+    }
201
+  },
202
+  created() {
203
+    this.loadConfig()
204
+  },
205
+  methods: {
206
+    /** 加载全平台商城设置 */
207
+    loadConfig() {
208
+      this.loading = true
209
+      getMallSetting().then(response => {
210
+        const data = response.data || {}
211
+        this.form = {
212
+          icpNo: data.icpNo || "",
213
+          icpLink: data.icpLink || "",
214
+          psbNo: data.psbNo || "",
215
+          psbLink: data.psbLink || "",
216
+          currencySymbol: data.currencySymbol || "¥",
217
+          currencyNameCn: data.currencyNameCn || "元",
218
+          goodsSnPrefix: data.goodsSnPrefix || "SN",
219
+          showSales: data.showSales != null ? String(data.showSales) : "1",
220
+          autoConfirmDays: data.autoConfirmDays != null ? data.autoConfirmDays : 7,
221
+          aftersaleLimitDays: data.aftersaleLimitDays != null ? data.aftersaleLimitDays : 7,
222
+          paidOrderCancelable: data.paidOrderCancelable === true,
223
+          shippedOrderCancelable: data.shippedOrderCancelable === true,
224
+          defaultAvatar: data.defaultAvatar || ""
225
+        }
226
+        this.loading = false
227
+        this.$nextTick(() => {
228
+          if (this.$refs.form) {
229
+            this.$refs.form.clearValidate()
230
+          }
231
+        })
232
+      }).catch(() => {
233
+        this.loading = false
234
+      })
235
+    },
236
+    /** 只读布尔展示为「是/否」 */
237
+    formatReadonlyBool(val) {
238
+      return val ? "是" : "否"
239
+    },
240
+    /** 保存商城设置(仅提交可编辑字段) */
241
+    submitForm() {
242
+      this.$refs["form"].validate(valid => {
243
+        if (!valid) {
244
+          return
245
+        }
246
+        const payload = {
247
+          icpNo: this.form.icpNo,
248
+          icpLink: this.form.icpLink,
249
+          psbNo: this.form.psbNo,
250
+          psbLink: this.form.psbLink,
251
+          showSales: this.form.showSales,
252
+          autoConfirmDays: this.form.autoConfirmDays,
253
+          aftersaleLimitDays: this.form.aftersaleLimitDays,
254
+          defaultAvatar: this.form.defaultAvatar
255
+        }
256
+        saveMallSetting(payload).then(response => {
257
+          this.$modal.msgSuccess(response.msg || "保存成功")
258
+          this.loadConfig()
259
+        })
260
+      })
261
+    }
262
+  }
263
+}
264
+</script>
265
+
266
+<style scoped>
267
+.card-header {
268
+  display: flex;
269
+  align-items: center;
270
+  flex-wrap: wrap;
271
+  gap: 12px;
272
+}
273
+.header-tip {
274
+  color: #909399;
275
+  font-size: 13px;
276
+  font-weight: normal;
277
+}
278
+.page-desc {
279
+  margin: 0;
280
+  color: #606266;
281
+  font-size: 13px;
282
+  line-height: 1.6;
283
+}
284
+.section-card {
285
+  margin-bottom: 0;
286
+}
287
+.readonly-text {
288
+  color: #606266;
289
+}
290
+.form-tip {
291
+  color: #909399;
292
+  font-size: 12px;
293
+  line-height: 1.5;
294
+  margin-top: 4px;
295
+}
296
+.form-tip-inline {
297
+  color: #909399;
298
+  font-size: 12px;
299
+  margin-left: 8px;
300
+}
301
+.avatar-preview {
302
+  display: flex;
303
+  align-items: center;
304
+  margin-left: 180px;
305
+  margin-top: -8px;
306
+  margin-bottom: 8px;
307
+}
308
+.preview-label {
309
+  color: #606266;
310
+  font-size: 13px;
311
+  margin-right: 8px;
312
+}
313
+</style>