浏览代码

阈值和发货

xw 3 天之前
父节点
当前提交
6d7ab0c8db

+ 26 - 0
src/api/live/orderTip.js

@@ -0,0 +1,26 @@
+import request from '@/utils/request'
+
+
+export function getOrderTipConfig() {
+  return request({
+    url: '/live/orderTip/config',
+    method: 'get'
+  })
+}
+
+
+export function updateOrderTipConfig(data) {
+  return request({
+    url: '/live/orderTip/config',
+    method: 'put',
+    data
+  })
+}
+
+
+export function previewOrderTip(liveId) {
+  return request({
+    url: '/live/orderTip/preview/' + liveId,
+    method: 'post'
+  })
+}

+ 1 - 1
src/views/hisStore/storeOrder/healthStoreList.vue

@@ -246,7 +246,7 @@
         </el-date-picker>
       </el-form-item>
       <el-form-item label="小程序" prop="appId">
-        <el-select v-model="queryParams.appId" placeholder="请选择所属小程序" clearable size="small">
+        <el-select v-model="queryParams.appId" placeholder="请选择所属小程序" clearable filterable size="small">
           <el-option
             v-for="dict in miniAppList"
             :key="dict.appId"

+ 1 - 1
src/views/hisStore/storeOrder/index.vue

@@ -193,7 +193,7 @@
       </el-form-item>
 
       <el-form-item label="小程序" prop="appId">
-        <el-select v-model="queryParams.appId" placeholder="请选择所属小程序" clearable size="small">
+        <el-select v-model="queryParams.appId" placeholder="请选择所属小程序" clearable filterable size="small">
           <el-option
             v-for="dict in appMallOptions"
             :key="dict.appid"

+ 629 - 19
src/views/live/comment/globalConfig.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="app-container">
+  <div class="app-container premium-page">
     <el-alert
       title="以下配置对全站所有直播间生效。"
       type="warning"
@@ -8,7 +8,7 @@
       class="mb16"
     />
     <!-- App 端需监听 WebSocket cmd=liveCommentConfig,data 为 JSON(仅全局 config),与总后台无直接耦合 -->
-    <el-card shadow="never">
+    <el-card shadow="never" class="config-card">
       <el-form
         ref="form"
         v-loading="loading"
@@ -134,7 +134,7 @@
             <span>{{ form.updateTime ? parseTime(form.updateTime) : '—' }}</span>
           </el-form-item>
         </div>
-        <el-form-item>
+        <el-form-item class="action-bar">
           <el-button
             type="primary"
             :loading="saving"
@@ -144,6 +144,223 @@
           <el-button @click="loadConfig">重 置</el-button>
         </el-form-item>
       </el-form>
+
+      <el-divider content-position="left">直播下单动效提示</el-divider>
+      <el-alert type="info" :closable="false" show-icon class="mb16 order-tip-alert">
+        <div class="alert-body">
+          <p class="alert-title">配置说明:</p>
+          <ul class="alert-list">
+            <li>每分钟最少条数 = 下限;当真实订单不够时系统自动用假订单补齐</li>
+            <li>每分钟最多条数 = 硬上限;真实+假订单合计不会超过此值(超出自动丢弃)</li>
+            <li>模板占位符:[用户昵称]、[商品名称]、[时间] —— 请原样保留,系统会自动替换</li>
+            <li>修改后立即生效(Redis 缓存 5 分钟 TTL 自动失效)</li>
+          </ul>
+        </div>
+      </el-alert>
+
+      <fieldset class="order-tip-fieldset" :disabled="orderTipLoading">
+        <el-form
+          ref="orderTipForm"
+          v-loading="orderTipLoading"
+          :model="orderTip"
+          :rules="orderTipRules"
+          label-width="180px"
+          class="order-tip-form-inner"
+        >
+          <div class="feature-section section-order-tip-main">
+            <el-divider content-position="left">功能总开关</el-divider>
+            <el-form-item label="动效总开关" prop="enabled">
+              <div class="switch-row">
+                <el-switch
+                  v-model="orderTip.enabled"
+                  :active-value="1"
+                  :inactive-value="0"
+                />
+                <span class="form-tip">关闭后直播间不会显示任何下单提示</span>
+                <el-tag v-if="orderTip.enabled !== 1" type="warning" size="small" class="ml8">已关闭,配置不会生效</el-tag>
+              </div>
+            </el-form-item>
+            <el-form-item label="每分钟最少条数" prop="rateMinPerMinute">
+              <el-input-number
+                v-model="orderTip.rateMinPerMinute"
+                :min="0"
+                :max="30"
+                :precision="0"
+                controls-position="right"
+              />
+              <span class="form-tip">条/分钟</span>
+            </el-form-item>
+            <el-form-item label="每分钟最多条数" prop="rateMaxPerMinute">
+              <el-input-number
+                v-model="orderTip.rateMaxPerMinute"
+                :min="1"
+                :max="30"
+                :precision="0"
+                controls-position="right"
+              />
+              <span class="form-tip">条/分钟</span>
+            </el-form-item>
+            <el-form-item label="频率窗口" prop="windowSec">
+              <el-input-number
+                v-model="orderTip.windowSec"
+                :min="10"
+                :max="600"
+                :step="10"
+                :precision="0"
+                controls-position="right"
+              />
+              <span class="form-tip">秒</span>
+              <p class="form-tip-block">滑动窗口大小,决定以多少秒为周期统计频率,默认 60 秒即「每分钟」。</p>
+            </el-form-item>
+          </div>
+
+          <div class="feature-section section-order-tip-fake">
+            <el-divider content-position="left">假数据填充</el-divider>
+            <el-form-item label="假数据填充" prop="fakeEnabled">
+              <div class="switch-row">
+                <el-switch
+                  v-model="orderTip.fakeEnabled"
+                  :active-value="1"
+                  :inactive-value="0"
+                />
+                <span class="form-tip">真实订单稀疏时,系统自动补充假订单;关闭后只会推送真实订单</span>
+              </div>
+            </el-form-item>
+            <el-form-item label="假订单最小间隔" prop="fakeMinIntervalSec">
+              <el-input-number
+                v-model="orderTip.fakeMinIntervalSec"
+                :min="5"
+                :max="600"
+                :precision="0"
+                controls-position="right"
+                :disabled="orderTipFakeFieldsDisabled"
+              />
+              <span class="form-tip">秒</span>
+              <p class="form-tip-block">同一直播间两条假订单之间的最小间隔</p>
+            </el-form-item>
+            <el-form-item label="商品范围" prop="goodsScope">
+              <el-radio-group v-model="orderTip.goodsScope" :disabled="orderTipFakeFieldsDisabled">
+                <el-radio
+                  v-for="opt in orderTipGoodsScopeOptions"
+                  :key="opt.value"
+                  :label="opt.value"
+                >{{ opt.label }}</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item
+              v-show="orderTip.goodsScope === 'custom'"
+              label="指定商品ID"
+              prop="customGoodsIds"
+            >
+              <el-input
+                v-model="orderTip.customGoodsIds"
+                type="textarea"
+                :rows="2"
+                :disabled="orderTipFakeFieldsDisabled"
+                placeholder="输入商品ID,多个用英文逗号分隔,例如:1001,1002,1003"
+              />
+            </el-form-item>
+            <el-form-item label="昵称库" prop="nicknameLibrary">
+              <el-input
+                v-model="orderTip.nicknameLibrary"
+                type="textarea"
+                :rows="5"
+                :disabled="orderTipFakeFieldsDisabled"
+                placeholder="每行一个昵称,或用英文逗号分隔;为空时使用系统内置昵称"
+              />
+              <div class="nickname-meta">
+                <span>字符数:{{ orderTipNicknameCharCount }}</span>
+                <span class="ml16">当前昵称数:{{ orderTipNicknameLineCount }}</span>
+              </div>
+            </el-form-item>
+          </div>
+
+          <div class="feature-section section-order-tip-copy">
+            <el-divider content-position="left">文案模板</el-divider>
+            <el-form-item label="昵称脱敏" prop="nicknameMask">
+              <div class="switch-row">
+                <el-switch
+                  v-model="orderTip.nicknameMask"
+                  :active-value="1"
+                  :inactive-value="0"
+                />
+                <span class="form-tip">真实用户昵称是否脱敏(如「张三」显示为「张*」),建议开启保护隐私</span>
+              </div>
+            </el-form-item>
+            <el-form-item label="「刚刚」阈值" prop="justNowSeconds">
+              <el-input-number
+                v-model="orderTip.justNowSeconds"
+                :min="10"
+                :max="600"
+                :precision="0"
+                controls-position="right"
+              />
+              <span class="form-tip">秒</span>
+              <p class="form-tip-block">订单发生时间在此秒数内显示为「刚刚下单」,否则显示「X分钟前」</p>
+            </el-form-item>
+            <el-form-item label="刚刚下单模板" prop="templateJustNow">
+              <el-input
+                v-model="orderTip.templateJustNow"
+                placeholder="须保留占位符:[用户昵称]、[商品名称]"
+              />
+              <div class="tpl-preview" v-text="orderTipPreviewJustNow" />
+            </el-form-item>
+            <el-form-item label="时间 ago 模板" prop="templateAgo">
+              <el-input
+                v-model="orderTip.templateAgo"
+                placeholder="须保留占位符:[用户昵称]、[商品名称]、[时间]"
+              />
+              <div class="tpl-preview" v-text="orderTipPreviewAgo" />
+            </el-form-item>
+            <el-form-item label="备注" prop="remark">
+              <el-input v-model="orderTip.remark" type="textarea" :rows="2" placeholder="备注" />
+            </el-form-item>
+            <el-form-item label="最近更新人">
+              <span>{{ orderTip.updateBy || '—' }}</span>
+            </el-form-item>
+            <el-form-item label="最近更新时间">
+              <span>{{ orderTip.updateTime ? parseTime(orderTip.updateTime) : '—' }}</span>
+            </el-form-item>
+          </div>
+
+          <el-form-item class="action-bar">
+            <el-button
+              type="primary"
+              :loading="orderTipSaving"
+              :disabled="orderTipLoading"
+              v-hasPermi="['live:orderTip:edit']"
+              @click="submitOrderTipForm"
+            >保 存</el-button>
+            <el-button :disabled="orderTipLoading" @click="loadOrderTipConfig">重 置</el-button>
+          </el-form-item>
+
+          <el-divider />
+
+          <div class="preview-block">
+            <p class="preview-hint">提交后可使用下方按钮测试效果</p>
+            <div class="preview-row">
+              <span class="preview-label">直播间ID</span>
+              <el-input-number
+                v-model="orderTipPreviewLiveId"
+                :min="1"
+                :precision="0"
+                :step="1"
+                controls-position="right"
+                placeholder="请输入直播间ID"
+                class="preview-live-id"
+              />
+              <el-button
+                type="primary"
+                plain
+                :loading="orderTipPreviewLoading"
+                :disabled="orderTipLoading || orderTipPreviewDisabled"
+                v-hasPermi="['live:orderTip:preview']"
+                @click="handleOrderTipPreview"
+              >测试效果</el-button>
+            </div>
+          </div>
+        </el-form>
+      </fieldset>
     </el-card>
   </div>
 </template>
@@ -155,6 +372,7 @@ import {
   getCommentFeatureRoles
 } from '@/api/live/commentFeature'
 import { getStockHintThreshold, updateStockHintThreshold } from '@/api/live/liveGoods'
+import { getOrderTipConfig, updateOrderTipConfig, previewOrderTip } from '@/api/live/orderTip'
 
 const splitCodes = (s) => {
   if (s == null || String(s).trim() === '') return []
@@ -166,6 +384,35 @@ const splitCodes = (s) => {
 
 const joinCodes = (arr) => (Array.isArray(arr) ? arr.map((x) => String(x).trim()).filter(Boolean) : []).join(',')
 
+const orderTipDefault = () => ({
+  configId: 1,
+  enabled: 1,
+  rateMinPerMinute: 1,
+  rateMaxPerMinute: 3,
+  windowSec: 60,
+  fakeEnabled: 1,
+  fakeMinIntervalSec: 20,
+  goodsScope: 'live',
+  customGoodsIds: '',
+  nicknameLibrary: '',
+  nicknameMask: 1,
+  templateJustNow: '[用户昵称] 刚刚下单了 [商品名称]',
+  templateAgo: '[时间]前,[用户昵称] 购买了 [商品名称]',
+  justNowSeconds: 30,
+  remark: '',
+  updateBy: '',
+  updateTime: null
+})
+
+function countNicknameTokens(text) {
+  if (text == null || String(text).trim() === '') return 0
+  return String(text)
+    .split(/[\n,]+/)
+    .map((s) => s.trim())
+    .filter(Boolean)
+    .length
+}
+
 export default {
   name: 'LiveCommentGlobalConfig',
   data() {
@@ -196,14 +443,209 @@ export default {
       rules: {
         floatCooldownSec: [{ required: true, message: '请输入冷却秒数', trigger: 'blur' }],
         pinMaxPerRoom: [{ required: true, message: '请输入单房间最大置顶数', trigger: 'blur' }]
+      },
+      orderTipLoading: false,
+      orderTipSaving: false,
+      orderTipPreviewLoading: false,
+      orderTipPreviewLiveId: undefined,
+      goodsScopeDictList: [],
+      orderTip: orderTipDefault(),
+      orderTipRules: {
+        rateMinPerMinute: [{ required: true, message: '请输入每分钟最少条数', trigger: 'blur' }],
+        rateMaxPerMinute: [{ required: true, message: '请输入每分钟最多条数', trigger: 'blur' }],
+        windowSec: [{ required: true, message: '请输入频率窗口秒数', trigger: 'blur' }],
+        fakeMinIntervalSec: [{ required: true, message: '请输入假订单最小间隔', trigger: 'blur' }],
+        goodsScope: [{ required: true, message: '请选择商品范围', trigger: 'change' }],
+        justNowSeconds: [{ required: true, message: '请输入刚刚阈值', trigger: 'blur' }],
+        templateJustNow: [{ required: true, message: '请输入刚刚下单模板', trigger: 'blur' }],
+        templateAgo: [{ required: true, message: '请输入时间 ago 模板', trigger: 'blur' }]
       }
     }
   },
+  computed: {
+    orderTipFakeFieldsDisabled() {
+      return Number(this.orderTip.fakeEnabled) !== 1
+    },
+    orderTipNicknameCharCount() {
+      return (this.orderTip.nicknameLibrary || '').length
+    },
+    orderTipNicknameLineCount() {
+      return countNicknameTokens(this.orderTip.nicknameLibrary)
+    },
+    orderTipPreviewJustNow() {
+      const t = this.orderTip.templateJustNow || ''
+      return t
+        .replace(/\[用户昵称\]/g, '养生小达人')
+        .replace(/\[商品名称\]/g, 'XX胶囊')
+    },
+    orderTipPreviewAgo() {
+      const t = this.orderTip.templateAgo || ''
+      return t
+        .replace(/\[时间\]/g, '2分钟')
+        .replace(/\[用户昵称\]/g, '养生小达人')
+        .replace(/\[商品名称\]/g, 'XX胶囊')
+    },
+    orderTipGoodsScopeOptions() {
+      const rows = this.goodsScopeDictList
+      if (rows && rows.length) {
+        return rows.map((r) => ({
+          label: r.dictLabel,
+          value: r.dictValue
+        }))
+      }
+      return [
+        { label: '全部在售商品', value: 'all' },
+        { label: '仅当前直播间', value: 'live' },
+        { label: '指定商品', value: 'custom' }
+      ]
+    },
+    orderTipPreviewDisabled() {
+      const n = this.orderTipPreviewLiveId
+      if (n === null || n === undefined) return true
+      const num = Number(n)
+      if (!Number.isFinite(num) || num < 1) return true
+      if (!Number.isInteger(num)) return true
+      return false
+    }
+  },
   created() {
     this.loadConfig()
     this.loadStockThreshold()
+    this.loadGoodsScopeDict()
+    this.loadOrderTipConfig()
   },
   methods: {
+    loadGoodsScopeDict() {
+      this.getDicts('live_order_tip_scope')
+        .then((res) => {
+          this.goodsScopeDictList = (res && res.data) || []
+        })
+        .catch(() => {
+          this.goodsScopeDictList = []
+        })
+    },
+    validateOrderTipBusiness() {
+      const min = Number(this.orderTip.rateMinPerMinute)
+      const max = Number(this.orderTip.rateMaxPerMinute)
+      if (min > max) {
+        this.$message.error('最少条数不能大于最多条数')
+        return false
+      }
+      if (max > 30) {
+        this.$message.error('最多条数不能超过 30')
+        return false
+      }
+      if (this.orderTip.goodsScope === 'custom' && !String(this.orderTip.customGoodsIds || '').trim()) {
+        this.$message.error('指定商品范围时必须填写商品ID')
+        return false
+      }
+      const t1 = (this.orderTip.templateJustNow || '').trim()
+      const t2 = (this.orderTip.templateAgo || '').trim()
+      if (!t1 || !t2) {
+        this.$message.error('文案模板不能为空')
+        return false
+      }
+      if (Number(this.orderTip.justNowSeconds) < 10) {
+        this.$message.error('「刚刚」阈值不能小于 10 秒')
+        return false
+      }
+      if (Number(this.orderTip.windowSec) < 10) {
+        this.$message.error('频率窗口不能小于 10 秒')
+        return false
+      }
+      return true
+    },
+    loadOrderTipConfig() {
+      this.orderTipLoading = true
+      getOrderTipConfig()
+        .then((res) => {
+          const d = (res && res.data) || {}
+          const base = orderTipDefault()
+          this.orderTip = {
+            ...base,
+            configId: d.configId != null ? d.configId : base.configId,
+            enabled: Number(d.enabled) === 1 ? 1 : 0,
+            rateMinPerMinute: d.rateMinPerMinute != null ? Number(d.rateMinPerMinute) : base.rateMinPerMinute,
+            rateMaxPerMinute: d.rateMaxPerMinute != null ? Number(d.rateMaxPerMinute) : base.rateMaxPerMinute,
+            windowSec: d.windowSec != null ? Number(d.windowSec) : base.windowSec,
+            fakeEnabled: Number(d.fakeEnabled) === 1 ? 1 : 0,
+            fakeMinIntervalSec: d.fakeMinIntervalSec != null ? Number(d.fakeMinIntervalSec) : base.fakeMinIntervalSec,
+            goodsScope: d.goodsScope || base.goodsScope,
+            customGoodsIds: d.customGoodsIds != null ? String(d.customGoodsIds) : '',
+            nicknameLibrary: d.nicknameLibrary != null ? String(d.nicknameLibrary) : '',
+            nicknameMask: Number(d.nicknameMask) === 1 ? 1 : 0,
+            templateJustNow: d.templateJustNow != null ? String(d.templateJustNow) : base.templateJustNow,
+            templateAgo: d.templateAgo != null ? String(d.templateAgo) : base.templateAgo,
+            justNowSeconds: d.justNowSeconds != null ? Number(d.justNowSeconds) : base.justNowSeconds,
+            remark: d.remark != null ? String(d.remark) : '',
+            updateBy: d.updateBy || '',
+            updateTime: d.updateTime
+          }
+        })
+        .finally(() => {
+          this.orderTipLoading = false
+        })
+    },
+    buildOrderTipPayload() {
+      return {
+        configId: this.orderTip.configId,
+        enabled: this.orderTip.enabled,
+        rateMinPerMinute: this.orderTip.rateMinPerMinute,
+        rateMaxPerMinute: this.orderTip.rateMaxPerMinute,
+        windowSec: this.orderTip.windowSec,
+        fakeEnabled: this.orderTip.fakeEnabled,
+        fakeMinIntervalSec: this.orderTip.fakeMinIntervalSec,
+        goodsScope: this.orderTip.goodsScope,
+        customGoodsIds: String(this.orderTip.customGoodsIds || '').trim() || null,
+        nicknameLibrary: String(this.orderTip.nicknameLibrary || '').trim() || null,
+        nicknameMask: this.orderTip.nicknameMask,
+        templateJustNow: String(this.orderTip.templateJustNow || '').trim(),
+        templateAgo: String(this.orderTip.templateAgo || '').trim(),
+        justNowSeconds: this.orderTip.justNowSeconds,
+        remark: String(this.orderTip.remark || '').trim() || null
+      }
+    },
+    submitOrderTipForm() {
+      this.$refs.orderTipForm.validate((valid) => {
+        if (!valid) return
+        if (!this.validateOrderTipBusiness()) return
+        this.orderTipSaving = true
+        updateOrderTipConfig(this.buildOrderTipPayload())
+          .then(() => {
+            this.msgSuccess('保存成功')
+            this.loadOrderTipConfig()
+          })
+          .finally(() => {
+            this.orderTipSaving = false
+          })
+      })
+    },
+    handleOrderTipPreview() {
+      if (this.orderTipPreviewDisabled) return
+      const liveId = Number(this.orderTipPreviewLiveId)
+      this.orderTipPreviewLoading = true
+      previewOrderTip(liveId)
+        .then((res) => {
+          const vo = (res && res.data) || {}
+          const content = vo.content != null ? String(vo.content) : ''
+          this.$notify({
+            title: '预览成功',
+            message: content || '已推送预览动效',
+            type: 'success',
+            duration: 4500
+          })
+        })
+        .catch((err) => {
+          const msg =
+            (err && err.message) ||
+            (typeof err === 'string' ? err : '') ||
+            '预览失败'
+          this.$message.warning(msg)
+        })
+        .finally(() => {
+          this.orderTipPreviewLoading = false
+        })
+    },
     isValidThreshold(value) {
       return /^[1-9]\d{0,3}$/.test(String(value))
     },
@@ -332,34 +774,202 @@ export default {
 
 <style scoped>
 .mb16 {
-  margin-bottom: 16px;
+  margin-bottom: 14px;
+}
+.premium-page {
+  background: linear-gradient(180deg, #f4f7fc 0%, #f8fafd 28%, #f6f8fb 100%);
+}
+.premium-page /deep/ .el-alert {
+  border-radius: 10px;
+  border: 1px solid #e3e9f3;
+}
+.premium-page /deep/ .el-alert__title {
+  font-weight: 600;
+}
+.config-card {
+  max-width: 1280px;
+  margin: 0 auto;
+  border-radius: 12px;
+  border: 1px solid #e4eaf3;
+  box-shadow: 0 10px 26px rgba(31, 50, 81, 0.06);
+}
+.config-card /deep/ .el-card__body {
+  padding: 18px 18px 20px;
 }
 .form-tip {
-  margin-left: 12px;
-  color: #909399;
+  margin-left: 10px;
+  color: #8d98ab;
+  font-size: 12px;
+}
+.form-tip-block {
+  margin: 6px 0 0;
+  color: #8d98ab;
   font-size: 12px;
+  line-height: 1.5;
+}
+.switch-row {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+}
+.ml8 {
+  margin-left: 8px;
+}
+.ml16 {
+  margin-left: 16px;
+}
+.alert-body {
+  line-height: 1.6;
+}
+.alert-title {
+  margin: 0 0 6px;
+  font-weight: 600;
+  color: #243247;
+}
+.alert-list {
+  margin: 0;
+  padding-left: 1.2em;
+  color: #4f607b;
+}
+.order-tip-alert {
+  margin-top: 2px;
+}
+.order-tip-fieldset {
+  margin: 0;
+  padding: 0;
+  border: none;
+  min-width: 0;
+}
+.order-tip-form-inner {
+  padding-top: 0;
 }
 .feature-section {
-  padding: 10px 12px 4px;
-  margin-bottom: 14px;
-  border-radius: 6px;
-  border: 1px solid #ebeef5;
-  background: #fafafa;
+  position: relative;
+  padding: 12px 14px 4px;
+  margin-bottom: 12px;
+  border-radius: 10px;
+  border: 1px solid #e6ecf4;
+  background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
+  box-shadow: 0 4px 12px rgba(38, 63, 98, 0.04);
+}
+.feature-section /deep/ .el-divider {
+  margin: 0 0 12px;
+}
+.feature-section /deep/ .el-divider__text {
+  padding: 0 10px;
+  font-weight: 600;
+  color: #1f2d42;
+  background: #fff;
+  letter-spacing: 0.3px;
+  border-radius: 12px;
+}
+.feature-section /deep/ .el-divider--horizontal {
+  margin-top: 2px;
+}
+.feature-section /deep/ .el-form-item {
+  margin-bottom: 12px;
+}
+.feature-section /deep/ .el-form-item:last-child {
+  margin-bottom: 6px;
+}
+.feature-section /deep/ .el-form-item__label {
+  color: #2a3950;
+  font-weight: 500;
+}
+.feature-section /deep/ .el-input,
+.feature-section /deep/ .el-input-number,
+.feature-section /deep/ .el-select {
+  max-width: 360px;
+}
+.feature-section /deep/ .el-input__inner,
+.feature-section /deep/ .el-textarea__inner {
+  border-color: #dfe6f0;
+  border-radius: 8px;
+}
+.feature-section /deep/ .el-input__inner:focus,
+.feature-section /deep/ .el-textarea__inner:focus {
+  border-color: #7c9ed6;
+}
+.feature-section /deep/ .el-input-number__decrease,
+.feature-section /deep/ .el-input-number__increase {
+  color: #6b7a90;
 }
 .section-threshold {
-  border-left: 4px solid #e6a23c;
-  background: #fffaf2;
+  border-left: 3px solid #f0b567;
 }
 .section-float {
-  border-left: 4px solid #409eff;
-  background: #f4f8ff;
+  border-left: 3px solid #7ca6ea;
 }
 .section-pin {
-  border-left: 4px solid #67c23a;
-  background: #f6fcf2;
+  border-left: 3px solid #88c59a;
 }
 .section-other {
-  border-left: 4px solid #909399;
-  background: #f8f8f9;
+  border-left: 3px solid #b5becc;
+}
+.section-order-tip-main {
+  border-left: 3px solid #7ca6ea;
+}
+.section-order-tip-fake {
+  border-left: 3px solid #7ca6ea;
+}
+.section-order-tip-copy {
+  border-left: 3px solid #7ca6ea;
+}
+.nickname-meta {
+  margin-top: 6px;
+  font-size: 12px;
+  color: #8996aa;
+}
+.tpl-preview {
+  margin-top: 6px;
+  padding: 8px 10px;
+  background: linear-gradient(90deg, #f7faff 0%, #fbfdff 100%);
+  color: #607089;
+  font-size: 12px;
+  border-radius: 8px;
+  border: 1px solid #e5ebf4;
+  border-left: 3px solid #9ab5e1;
+  min-height: 20px;
+  line-height: 1.5;
+}
+.preview-block {
+  margin-top: 2px;
+  padding: 12px 14px;
+  background: linear-gradient(90deg, #f7f9fd 0%, #fbfcfe 100%);
+  border-radius: 10px;
+  border: 1px dashed #d9e2ef;
+}
+.preview-hint {
+  margin: 0 0 12px;
+  font-size: 13px;
+  color: #74839a;
+  font-weight: 500;
+}
+.preview-row {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+.preview-label {
+  font-size: 14px;
+  color: #334155;
+  font-weight: 500;
+}
+.preview-live-id {
+  width: 220px;
+}
+.action-bar {
+  margin: 4px 0 14px;
+  padding: 10px 12px;
+  border-radius: 10px;
+  border: 1px solid #e3eaf4;
+  background: linear-gradient(180deg, #fdfefe 0%, #f8fbff 100%);
+}
+.action-bar /deep/ .el-button + .el-button {
+  margin-left: 10px;
+}
+.action-bar /deep/ .el-button--primary {
+  box-shadow: 0 6px 14px rgba(64, 158, 255, 0.24);
 }
 </style>

+ 13 - 0
src/views/qw/qwCompany/index.vue

@@ -272,6 +272,16 @@
             >{{dict.dictLabel}}</el-radio>
           </el-radio-group>
         </el-form-item>
+        <el-form-item label="可选iPad" prop="allowOfficial">
+          <el-switch
+            v-model="form.allowOfficial"
+            :active-value="1"
+            :inactive-value="0"
+          />
+          <div style="color: #909399; font-size: 12px; line-height: 1.5; margin-top: 6px;">
+            开启后,走官方群发(不依赖 iPad)。
+          </div>
+        </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
         <el-button type="primary" @click="submitForm">确 定</el-button>
@@ -461,6 +471,7 @@ export default {
         shareAppId: null,
         shareAgentId: null,
         shareSchema: null,
+        allowOfficial: 0,
       };
       this.resetForm("form");
     },
@@ -517,6 +528,8 @@ export default {
       const id = row.id || this.ids
       getQwCompany(id).then(response => {
         this.form = response.data;
+        const raw = this.form.allowOfficial;
+        this.form.allowOfficial = raw === 1 || raw === '1' || raw === true ? 1 : 0;
         this.form.companyIds=((this.form.companyIds).split(",")).map(Number)
         this.open = true;
         this.title = "修改企微主体";

+ 88 - 83
src/views/system/config/config.vue

@@ -687,89 +687,94 @@
           </div>
         </el-form>
       </el-tab-pane>
-      <!--      <el-tab-pane label="APP支付配置" name="store.pay">-->
-      <!--        <el-form ref="form23" :model="form23"  label-width="160px">-->
-      <!--          <el-form-item label="支付类型" prop="type">-->
-      <!--            <el-radio-group v-model="form23.type">-->
-      <!--              <el-radio label="yb">易宝</el-radio>-->
-      <!--              <el-radio label="wx">微信</el-radio>-->
-      <!--              <el-radio label="hf">汇付</el-radio>-->
-      <!--            </el-radio-group>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item   label="appId" prop="appId">-->
-      <!--            <el-input   v-model="form23.appId"  label="请输入appId"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item  v-if="form23.type=='yb'" label="易宝商户号" prop="ybAccount">-->
-      <!--            <el-input   v-model="form23.ybAccount"  label="请输入易宝商户号"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='yb'" label="易宝Key" prop="ybKey">-->
-      <!--            <el-input  v-model="form23.ybKey" label="请输入易宝Key"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='yb'" label="易宝回调地址" prop="ybNotifyUrl">-->
-      <!--            <el-input  v-model="form23.ybNotifyUrl" label="易宝回调地址"></el-input>-->
-      <!--          </el-form-item>-->
-
-      <!--          <el-form-item  v-if="form23.type=='wx'" label="微信商户号" prop="wxMchId">-->
-      <!--            <el-input   v-model="form23.wxMchId"  label="请输入微信商户号"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='wx'" label="微信Key" prop="wxMchKey">-->
-      <!--            <el-input  v-model="form23.wxMchKey" label="请输入微信Key"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='wx'" label="微信商户V3密钥" prop="wxMchKey">-->
-      <!--            <el-input  v-model="form23.wxApiV3Key" label="请输入商户V3密钥"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='wx'" label="keyPath" prop="wxMchKey">-->
-      <!--            <el-input  v-model="form23.keyPath" label="请输入商户V3密钥"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='wx'" label="privateKeyPath" prop="wxMchKey">-->
-      <!--            <el-input  v-model="form23.privateKeyPath" label="请输入商户V3密钥"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='wx'" label="privateCertPath" prop="wxMchKey">-->
-      <!--            <el-input  v-model="form23.privateCertPath" label="请输入商户V3密钥"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='wx'" label="notifyUrlScrm" prop="wxMchKey">-->
-      <!--            <el-input  v-model="form23.notifyUrlScrm" label="请输入商户V3密钥"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='wx'" label="publicKeyId" prop="wxMchKey">-->
-      <!--            <el-input  v-model="form23.publicKeyId" label="请输入商户V3密钥"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='wx'" label="publicKeyPath" prop="wxMchKey">-->
-      <!--            <el-input  v-model="form23.publicKeyPath" label="请输入商户V3密钥"></el-input>-->
-      <!--          </el-form-item>-->
-
-
-      <!--          <el-form-item  v-if="form23.type=='hf'" label="汇付产品号" prop="hfProductId">-->
-      <!--            <el-input   v-model="form23.hfProductId"  label="汇付产品号"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='hf'" label="系统号" prop="hfSysId">-->
-      <!--            <el-input  v-model="form23.hfSysId" label="系统号Key"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='hf'" label="商户号" prop="huifuId">-->
-      <!--            <el-input  v-model="form23.huifuId" label="商户号"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='hf'" label="商户私钥" prop="hfRsaPrivateKey">-->
-      <!--            <el-input  v-model="form23.hfRsaPrivateKey" label="商户私钥"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='hf'" label="汇付公钥" prop="hfRsaPublicKey">-->
-      <!--            <el-input  v-model="form23.hfRsaPublicKey" label="汇付公钥"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='hf'" label="汇付支付回调地址" prop="hfPayNotifyUrl">-->
-      <!--            <el-input  v-model="form23.hfPayNotifyUrl" label="汇付支付回调地址"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='hf'" label="大额支付回调地址" prop="hfPayOnlineNotifyUrl">-->
-      <!--            <el-input  v-model="form23.hfPayOnlineNotifyUrl" label="汇付支付回调地址"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='hf'" label="汇付退款回调地址" prop="hfRefundNotifyUrl">-->
-      <!--            <el-input  v-model="form23.hfRefundNotifyUrl" label="汇付退款回调地址"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <el-form-item v-if="form23.type=='hf'" label="汇付大额退款回调地址" prop="hfOnlineRefundNotifyUrl">-->
-      <!--            <el-input  v-model="form23.hfOnlineRefundNotifyUrl" label="汇付分账回调地址"></el-input>-->
-      <!--          </el-form-item>-->
-      <!--          <div   class="footer">-->
-      <!--            <el-button type="primary" @click="submitForm23">提  交</el-button>-->
-      <!--          </div>-->
-      <!--        </el-form>-->
-      <!--      </el-tab-pane>-->
+      <el-tab-pane label="购买配置" name="store.pay">
+  <el-form ref="form23" :model="form23" label-width="160px">
+    <el-form-item label="支付类型" prop="type">
+      <el-radio-group v-model="form23.type">
+        <el-radio label="yb">易宝</el-radio>
+        <el-radio label="wx">微信</el-radio>
+        <el-radio label="hf">汇付</el-radio>
+      </el-radio-group>
+    </el-form-item>
+
+    <el-form-item label="appId" prop="appId">
+      <el-input v-model="form23.appId" label="请输入appId"></el-input>
+    </el-form-item>
+
+    <!-- 易宝 -->
+    <el-form-item v-if="form23.type=='yb'" label="易宝商户号" prop="ybAccount">
+      <el-input v-model="form23.ybAccount" label="请输入易宝商户号"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='yb'" label="易宝Key" prop="ybKey">
+      <el-input v-model="form23.ybKey" label="请输入易宝Key"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='yb'" label="易宝回调地址" prop="ybNotifyUrl">
+      <el-input v-model="form23.ybNotifyUrl" label="易宝回调地址"></el-input>
+    </el-form-item>
+
+    <!-- 微信 -->
+    <el-form-item v-if="form23.type=='wx'" label="微信商户号" prop="wxMchId">
+      <el-input v-model="form23.wxMchId" label="请输入微信商户号"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='wx'" label="微信Key" prop="wxMchKey">
+      <el-input v-model="form23.wxMchKey" label="请输入微信Key"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='wx'" label="微信商户V3密钥" prop="wxApiV3Key">
+      <el-input v-model="form23.wxApiV3Key" label="请输入商户V3密钥"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='wx'" label="keyPath" prop="keyPath">
+      <el-input v-model="form23.keyPath" label="请输入keyPath"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='wx'" label="privateKeyPath" prop="privateKeyPath">
+      <el-input v-model="form23.privateKeyPath" label="请输入privateKeyPath"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='wx'" label="privateCertPath" prop="privateCertPath">
+      <el-input v-model="form23.privateCertPath" label="请输入privateCertPath"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='wx'" label="notifyUrlScrm" prop="notifyUrlScrm">
+      <el-input v-model="form23.notifyUrlScrm" label="请输入notifyUrlScrm"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='wx'" label="publicKeyId" prop="publicKeyId">
+      <el-input v-model="form23.publicKeyId" label="请输入publicKeyId"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='wx'" label="publicKeyPath" prop="publicKeyPath">
+      <el-input v-model="form23.publicKeyPath" label="请输入publicKeyPath"></el-input>
+    </el-form-item>
+
+    <!-- 汇付 -->
+    <el-form-item v-if="form23.type=='hf'" label="汇付产品号" prop="hfProductId">
+      <el-input v-model="form23.hfProductId" label="汇付产品号"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='hf'" label="系统号" prop="hfSysId">
+      <el-input v-model="form23.hfSysId" label="系统号"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='hf'" label="商户号" prop="huifuId">
+      <el-input v-model="form23.huifuId" label="商户号"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='hf'" label="商户私钥" prop="hfRsaPrivateKey">
+      <el-input v-model="form23.hfRsaPrivateKey" label="商户私钥"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='hf'" label="汇付公钥" prop="hfRsaPublicKey">
+      <el-input v-model="form23.hfRsaPublicKey" label="汇付公钥"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='hf'" label="汇付支付回调地址" prop="hfPayNotifyUrl">
+      <el-input v-model="form23.hfPayNotifyUrl" label="汇付支付回调地址"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='hf'" label="大额支付回调地址" prop="hfPayOnlineNotifyUrl">
+      <el-input v-model="form23.hfPayOnlineNotifyUrl" label="大额支付回调地址"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='hf'" label="汇付退款回调地址" prop="hfRefundNotifyUrl">
+      <el-input v-model="form23.hfRefundNotifyUrl" label="汇付退款回调地址"></el-input>
+    </el-form-item>
+    <el-form-item v-if="form23.type=='hf'" label="汇付大额退款回调地址" prop="hfOnlineRefundNotifyUrl">
+      <el-input v-model="form23.hfOnlineRefundNotifyUrl" label="汇付大额退款回调地址"></el-input>
+    </el-form-item>
+
+    <div class="footer">
+      <el-button type="primary" @click="submitForm23">提  交</el-button>
+    </div>
+  </el-form>
+</el-tab-pane>
       <el-tab-pane label="布局配置" name="his.appShow">
         <div>
           <el-table border :data="form10">