|
|
@@ -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>
|