globalConfig.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. <template>
  2. <div class="app-container">
  3. <el-alert
  4. title="以下配置对全站所有直播间生效。"
  5. type="warning"
  6. :closable="false"
  7. show-icon
  8. class="mb16"
  9. />
  10. <!-- App 端需监听 WebSocket cmd=liveCommentConfig,data 为 JSON(仅全局 config),与总后台无直接耦合 -->
  11. <el-card shadow="never">
  12. <el-form
  13. ref="form"
  14. v-loading="loading"
  15. :model="form"
  16. :rules="rules"
  17. label-width="160px"
  18. >
  19. <div class="feature-section section-threshold">
  20. <el-divider content-position="left">库存提示阈值</el-divider>
  21. <el-form-item label="阈值配置">
  22. <el-input
  23. v-model.trim="thresholdInput"
  24. placeholder="请输入1~9999"
  25. style="width: 220px"
  26. @keyup.enter.native="saveStockThreshold"
  27. />
  28. <el-button
  29. type="primary"
  30. size="mini"
  31. :loading="thresholdSaving"
  32. style="margin-left: 12px"
  33. @click="saveStockThreshold"
  34. >保存</el-button>
  35. <span class="form-tip">当库存小于等于该阈值时,App端显示“仅剩X件”。</span>
  36. </el-form-item>
  37. </div>
  38. <div class="feature-section section-float">
  39. <el-divider content-position="left">飘屏</el-divider>
  40. <el-form-item label="飘屏总开关" prop="floatEnabled">
  41. <el-switch
  42. v-model="form.floatEnabled"
  43. :active-value="1"
  44. :inactive-value="0"
  45. />
  46. </el-form-item>
  47. <el-form-item label="用户飘屏冷却" prop="floatCooldownSec">
  48. <el-input-number
  49. v-model="form.floatCooldownSec"
  50. :min="0"
  51. :precision="0"
  52. controls-position="right"
  53. placeholder="秒"
  54. />
  55. <span class="form-tip">同一用户连续飘屏的最小间隔(秒)</span>
  56. </el-form-item>
  57. <el-form-item label="可飘屏角色" prop="floatRoleCodes">
  58. <el-select
  59. v-model="floatRoleList"
  60. multiple
  61. filterable
  62. placeholder="请选择角色"
  63. style="width: 420px"
  64. :loading="rolesLoading"
  65. @visible-change="ensureRolesLoaded"
  66. >
  67. <el-option
  68. v-for="r in roleOptions"
  69. :key="r"
  70. :label="r"
  71. :value="r"
  72. />
  73. </el-select>
  74. <span class="form-tip">选项来自企业角色;保存为英文逗号分隔的 role_name</span>
  75. </el-form-item>
  76. </div>
  77. <div class="feature-section section-pin">
  78. <el-divider content-position="left">评论置顶</el-divider>
  79. <el-form-item label="单房间最大置顶数" prop="pinMaxPerRoom">
  80. <el-input-number
  81. v-model="form.pinMaxPerRoom"
  82. :min="1"
  83. :precision="0"
  84. controls-position="right"
  85. />
  86. </el-form-item>
  87. <el-form-item label="可选置顶时长" prop="pinDurationOptions">
  88. <el-select
  89. v-model="pinDurationList"
  90. multiple
  91. placeholder="分钟;-1 表示永久"
  92. style="width: 420px"
  93. >
  94. <el-option label="5 分钟" :value="5" />
  95. <el-option label="10 分钟" :value="10" />
  96. <el-option label="15 分钟" :value="15" />
  97. <el-option label="30 分钟" :value="30" />
  98. <el-option label="60 分钟" :value="60" />
  99. <el-option label="永久 (-1)" :value="-1" />
  100. </el-select>
  101. <span class="form-tip">保存为逗号分隔数字,如 5,10,30,-1</span>
  102. </el-form-item>
  103. <el-form-item label="可置顶角色" prop="pinRoleCodes">
  104. <el-select
  105. v-model="pinRoleList"
  106. multiple
  107. filterable
  108. placeholder="请选择角色"
  109. style="width: 420px"
  110. :loading="rolesLoading"
  111. @visible-change="ensureRolesLoaded"
  112. >
  113. <el-option
  114. v-for="r in roleOptions"
  115. :key="r"
  116. :label="r"
  117. :value="r"
  118. />
  119. </el-select>
  120. </el-form-item>
  121. </div>
  122. <div class="feature-section section-other">
  123. <el-divider content-position="left">其他</el-divider>
  124. <el-form-item label="备注" prop="remark">
  125. <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注" />
  126. </el-form-item>
  127. <el-form-item label="最近更新人">
  128. <span>{{ form.updateBy || '—' }}</span>
  129. </el-form-item>
  130. <el-form-item label="最近更新时间">
  131. <span>{{ form.updateTime ? parseTime(form.updateTime) : '—' }}</span>
  132. </el-form-item>
  133. </div>
  134. <el-form-item>
  135. <el-button
  136. type="primary"
  137. :loading="saving"
  138. @click="submitForm"
  139. v-hasPermi="['live:commentFeature:edit']"
  140. >保 存</el-button>
  141. <el-button @click="loadConfig">重 置</el-button>
  142. </el-form-item>
  143. </el-form>
  144. </el-card>
  145. </div>
  146. </template>
  147. <script>
  148. import {
  149. getCommentFeatureConfig,
  150. updateCommentFeatureConfig,
  151. getCommentFeatureRoles
  152. } from '@/api/live/commentFeature'
  153. import { getStockHintThreshold, updateStockHintThreshold } from '@/api/live/liveGoods'
  154. const splitCodes = (s) => {
  155. if (s == null || String(s).trim() === '') return []
  156. return String(s)
  157. .split(',')
  158. .map((x) => x.trim())
  159. .filter(Boolean)
  160. }
  161. const joinCodes = (arr) => (Array.isArray(arr) ? arr.map((x) => String(x).trim()).filter(Boolean) : []).join(',')
  162. export default {
  163. name: 'LiveCommentGlobalConfig',
  164. data() {
  165. return {
  166. loading: false,
  167. saving: false,
  168. thresholdSaving: false,
  169. thresholdInput: '',
  170. /** 下拉候选项(GET /live/commentFeature/roles),并与已选值合并以便回显 */
  171. roleOptions: [],
  172. rolesLoaded: false,
  173. rolesLoading: false,
  174. floatRoleList: [],
  175. pinRoleList: [],
  176. pinDurationList: [],
  177. form: {
  178. configId: 1,
  179. floatEnabled: 0,
  180. floatCooldownSec: 0,
  181. floatRoleCodes: '',
  182. pinMaxPerRoom: 1,
  183. pinDurationOptions: '',
  184. pinRoleCodes: '',
  185. remark: '',
  186. updateBy: '',
  187. updateTime: null
  188. },
  189. rules: {
  190. floatCooldownSec: [{ required: true, message: '请输入冷却秒数', trigger: 'blur' }],
  191. pinMaxPerRoom: [{ required: true, message: '请输入单房间最大置顶数', trigger: 'blur' }]
  192. }
  193. }
  194. },
  195. created() {
  196. this.loadConfig()
  197. this.loadStockThreshold()
  198. },
  199. methods: {
  200. isValidThreshold(value) {
  201. return /^[1-9]\d{0,3}$/.test(String(value))
  202. },
  203. loadStockThreshold() {
  204. getStockHintThreshold()
  205. .then((response) => {
  206. const threshold = response && response.threshold
  207. this.thresholdInput = threshold != null ? String(threshold) : ''
  208. })
  209. .catch((error) => {
  210. this.$message.error((error && error.msg) || (error && error.message) || '阈值加载失败')
  211. })
  212. },
  213. saveStockThreshold() {
  214. if (!this.isValidThreshold(this.thresholdInput)) {
  215. this.$message.error('阈值范围需在1~9999')
  216. return
  217. }
  218. this.thresholdSaving = true
  219. updateStockHintThreshold(Number(this.thresholdInput))
  220. .then(() => {
  221. this.msgSuccess('保存成功')
  222. this.loadStockThreshold()
  223. })
  224. .catch((error) => {
  225. this.$message.error((error && error.msg) || (error && error.message) || '保存失败')
  226. })
  227. .finally(() => {
  228. this.thresholdSaving = false
  229. })
  230. },
  231. /** 将已选 role_name 合并进 options,避免接口未返回历史已选项时标签不显示 */
  232. mergeSelectedIntoRoleOptions() {
  233. const set = new Set(Array.isArray(this.roleOptions) ? this.roleOptions : [])
  234. ;(this.floatRoleList || []).forEach((x) => {
  235. const s = x != null ? String(x).trim() : ''
  236. if (s) set.add(s)
  237. })
  238. ;(this.pinRoleList || []).forEach((x) => {
  239. const s = x != null ? String(x).trim() : ''
  240. if (s) set.add(s)
  241. })
  242. this.roleOptions = Array.from(set)
  243. },
  244. /** 首次展开下拉时拉取角色列表;空数据不报错 */
  245. ensureRolesLoaded(visible) {
  246. if (!visible) return
  247. if (this.rolesLoaded) {
  248. this.mergeSelectedIntoRoleOptions()
  249. return
  250. }
  251. this.rolesLoading = true
  252. getCommentFeatureRoles()
  253. .then((response) => {
  254. const list = response && response.data
  255. this.roleOptions = Array.isArray(list) ? list.slice() : []
  256. this.rolesLoaded = true
  257. this.mergeSelectedIntoRoleOptions()
  258. })
  259. .catch(() => {
  260. this.roleOptions = []
  261. this.rolesLoaded = true
  262. this.mergeSelectedIntoRoleOptions()
  263. })
  264. .finally(() => {
  265. this.rolesLoading = false
  266. })
  267. },
  268. loadConfig() {
  269. this.loading = true
  270. getCommentFeatureConfig()
  271. .then((response) => {
  272. const d = response.data || {}
  273. this.form = {
  274. configId: d.configId != null ? d.configId : 1,
  275. floatEnabled: Number(d.floatEnabled) === 1 ? 1 : 0,
  276. floatCooldownSec: d.floatCooldownSec != null ? Number(d.floatCooldownSec) : 0,
  277. floatRoleCodes: d.floatRoleCodes || '',
  278. pinMaxPerRoom: d.pinMaxPerRoom != null ? Number(d.pinMaxPerRoom) : 1,
  279. pinDurationOptions: d.pinDurationOptions || '',
  280. pinRoleCodes: d.pinRoleCodes || '',
  281. remark: d.remark || '',
  282. updateBy: d.updateBy || '',
  283. updateTime: d.updateTime
  284. }
  285. this.floatRoleList = splitCodes(this.form.floatRoleCodes)
  286. this.pinRoleList = splitCodes(this.form.pinRoleCodes)
  287. this.pinDurationList = splitCodes(this.form.pinDurationOptions).map((x) => {
  288. const n = Number(x)
  289. return Number.isNaN(n) ? x : n
  290. })
  291. this.mergeSelectedIntoRoleOptions()
  292. })
  293. .finally(() => {
  294. this.loading = false
  295. })
  296. },
  297. submitForm() {
  298. this.$refs.form.validate((valid) => {
  299. if (!valid) return
  300. const pinDur = joinCodes(this.pinDurationList)
  301. const payload = {
  302. configId: this.form.configId,
  303. floatEnabled: this.form.floatEnabled,
  304. floatCooldownSec: this.form.floatCooldownSec,
  305. floatRoleCodes: joinCodes(this.floatRoleList),
  306. pinMaxPerRoom: this.form.pinMaxPerRoom,
  307. pinDurationOptions: pinDur,
  308. pinRoleCodes: joinCodes(this.pinRoleList),
  309. remark: (this.form.remark || '').trim()
  310. }
  311. this.saving = true
  312. updateCommentFeatureConfig(payload)
  313. .then(() => {
  314. this.msgSuccess('保存成功')
  315. this.loadConfig()
  316. })
  317. .finally(() => {
  318. this.saving = false
  319. })
  320. })
  321. }
  322. }
  323. }
  324. </script>
  325. <style scoped>
  326. .mb16 {
  327. margin-bottom: 16px;
  328. }
  329. .form-tip {
  330. margin-left: 12px;
  331. color: #909399;
  332. font-size: 12px;
  333. }
  334. .feature-section {
  335. padding: 10px 12px 4px;
  336. margin-bottom: 14px;
  337. border-radius: 6px;
  338. border: 1px solid #ebeef5;
  339. background: #fafafa;
  340. }
  341. .section-threshold {
  342. border-left: 4px solid #e6a23c;
  343. background: #fffaf2;
  344. }
  345. .section-float {
  346. border-left: 4px solid #409eff;
  347. background: #f4f8ff;
  348. }
  349. .section-pin {
  350. border-left: 4px solid #67c23a;
  351. background: #f6fcf2;
  352. }
  353. .section-other {
  354. border-left: 4px solid #909399;
  355. background: #f8f8f9;
  356. }
  357. </style>