TagEdit.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. <template>
  2. <div class="tag-edit">
  3. <el-card>
  4. <template #header>
  5. <div class="header">
  6. <span class="title">{{ isEdit ? '编辑标签' : '新建标签' }}</span>
  7. <span v-if="isEdit && currentTag" class="tag-id">ID: {{ currentTag.id }}</span>
  8. </div>
  9. </template>
  10. <!-- 基础信息 -->
  11. <div class="section">
  12. <div class="section-title">基础信息</div>
  13. <el-form
  14. ref="form"
  15. :model="form"
  16. :rules="rules"
  17. label-width="100px"
  18. style="max-width: 760px"
  19. >
  20. <el-form-item label="标签名称" prop="name">
  21. <el-input v-model="form.name" placeholder="最长 40 字符" maxlength="40" show-word-limit />
  22. </el-form-item>
  23. <el-form-item label="标签描述" prop="description">
  24. <el-input
  25. v-model="form.description"
  26. type="textarea"
  27. :rows="3"
  28. placeholder="便于识别标签用途,最长 200 字符"
  29. maxlength="200"
  30. show-word-limit
  31. />
  32. </el-form-item>
  33. <el-form-item label="状态">
  34. <el-switch v-model="form.status" active-text="启用" inactive-text="停用" />
  35. </el-form-item>
  36. </el-form>
  37. </div>
  38. <!-- 规则配置 -->
  39. <div class="section">
  40. <div class="section-title">规则配置</div>
  41. <rule-builder v-model="form.groups" />
  42. </div>
  43. <!-- 操作按钮 -->
  44. <div class="footer">
  45. <el-button @click="handleCancel">取消</el-button>
  46. <el-button type="primary" @click="handleSave">保存</el-button>
  47. </div>
  48. </el-card>
  49. </div>
  50. </template>
  51. <script>
  52. import { ElMessage, ElMessageBox } from 'element-plus'
  53. import tagStore from '@/store/tagStore'
  54. import RuleBuilder from '@/components/tag/RuleBuilder.vue'
  55. import { PARAM_MAP, needValueInput } from '@/config/tagParams'
  56. export default {
  57. name: 'TagEdit',
  58. components: { RuleBuilder },
  59. data() {
  60. return {
  61. form: {
  62. name: '',
  63. description: '',
  64. status: true,
  65. groups: []
  66. },
  67. rules: {
  68. name: [
  69. { required: true, message: '请输入标签名称', trigger: 'blur' },
  70. { max: 40, message: '最长 40 字符', trigger: 'blur' }
  71. ],
  72. description: [
  73. { max: 200, message: '最长 200 字符', trigger: 'blur' }
  74. ]
  75. },
  76. currentTag: null,
  77. initialSnapshot: ''
  78. }
  79. },
  80. computed: {
  81. isEdit() {
  82. return !!this.$route.params.id
  83. }
  84. },
  85. created() {
  86. if (this.isEdit) {
  87. const target = tagStore.getById(this.$route.params.id)
  88. if (!target) {
  89. ElMessage.error('标签不存在')
  90. this.$router.replace('/customization/hsbc/tags')
  91. return
  92. }
  93. this.currentTag = target
  94. this.form = {
  95. name: target.name,
  96. description: target.description,
  97. status: target.status === 'enabled',
  98. groups: JSON.parse(JSON.stringify(target.groups || []))
  99. }
  100. }
  101. this.initialSnapshot = JSON.stringify(this.form)
  102. },
  103. methods: {
  104. validateRules() {
  105. if (!this.form.groups.length) {
  106. ElMessage.error('至少配置 1 个规则组')
  107. return false
  108. }
  109. for (let i = 0; i < this.form.groups.length; i++) {
  110. const group = this.form.groups[i]
  111. if (!group.rules || !group.rules.length) {
  112. ElMessage.error(`规则组 ${i + 1} 至少配置 1 条规则`)
  113. return false
  114. }
  115. for (let j = 0; j < group.rules.length; j++) {
  116. const rule = group.rules[j]
  117. const label = `规则组 ${i + 1} 第 ${j + 1} 条规则`
  118. if (!rule.paramKey) {
  119. ElMessage.error(`${label}:请选择参数`)
  120. return false
  121. }
  122. if (!rule.operator) {
  123. ElMessage.error(`${label}:请选择运算符`)
  124. return false
  125. }
  126. if (needValueInput(rule.operator)) {
  127. const param = PARAM_MAP[rule.paramKey]
  128. // 相对日期:需要 N 值和单位
  129. if (['withinLast', 'beforeLast'].includes(rule.operator)) {
  130. if (rule.value === '' || rule.value === null || rule.value === undefined) {
  131. ElMessage.error(`${label}:请输入相对日期数值`)
  132. return false
  133. }
  134. } else if (Array.isArray(rule.value)) {
  135. if (!rule.value.length) {
  136. ElMessage.error(`${label}:请选择比较值`)
  137. return false
  138. }
  139. } else if (rule.value === '' || rule.value === null || rule.value === undefined) {
  140. ElMessage.error(`${label}:请输入比较值`)
  141. return false
  142. }
  143. if (rule.operator === 'between') {
  144. if (rule.value2 === '' || rule.value2 === null || rule.value2 === undefined) {
  145. ElMessage.error(`${label}:请输入区间上限`)
  146. return false
  147. }
  148. if (param && (param.type === 'BigDecimal' || param.type === 'List')
  149. && Number(rule.value) > Number(rule.value2)) {
  150. ElMessage.error(`${label}:区间下限不能大于上限`)
  151. return false
  152. }
  153. }
  154. }
  155. // 时间窗口校验
  156. if (param_hasHistory(rule.paramKey)) {
  157. const tw = rule.timeWindow || {}
  158. if (tw.mode === 'specificMonth' && !tw.month) {
  159. ElMessage.error(`${label}:请选择指定月份`)
  160. return false
  161. }
  162. if (tw.mode === 'recentMonths') {
  163. if (!tw.n || tw.n < 1 || tw.n > 12) {
  164. ElMessage.error(`${label}:近 N 个月 的 N 需在 1-12 之间`)
  165. return false
  166. }
  167. if (tw.aggregator === 'atLeast') {
  168. if (!tw.m || tw.m < 1 || tw.m > tw.n) {
  169. ElMessage.error(`${label}:至少 M 个月 的 M 不能大于 N`)
  170. return false
  171. }
  172. }
  173. }
  174. }
  175. }
  176. }
  177. return true
  178. },
  179. handleSave() {
  180. this.$refs.form.validate((valid) => {
  181. if (!valid) return
  182. if (!this.validateRules()) return
  183. const doSave = () => {
  184. if (this.isEdit) {
  185. tagStore.update(this.currentTag.id, this.form)
  186. ElMessage.success('保存成功')
  187. } else {
  188. tagStore.create(this.form)
  189. ElMessage.success('新建成功')
  190. }
  191. this.$router.push('/customization/hsbc/tags')
  192. }
  193. if (this.isEdit && this.currentTag && this.currentTag.referenceCount > 0) {
  194. ElMessageBox.confirm(
  195. `该标签已被 ${this.currentTag.referenceCount} 个权益/活动引用,修改后将影响其客群匹配,确认保存?`,
  196. '提示',
  197. { type: 'warning' }
  198. ).then(doSave).catch(() => {})
  199. } else {
  200. doSave()
  201. }
  202. })
  203. },
  204. handleCancel() {
  205. const dirty = JSON.stringify(this.form) !== this.initialSnapshot
  206. if (dirty) {
  207. ElMessageBox.confirm('未保存内容将丢失,确认退出?', '提示', { type: 'warning' })
  208. .then(() => this.$router.push('/customization/hsbc/tags'))
  209. .catch(() => {})
  210. } else {
  211. this.$router.push('/customization/hsbc/tags')
  212. }
  213. }
  214. }
  215. }
  216. function param_hasHistory(key) {
  217. const p = PARAM_MAP[key]
  218. return p && p.hasHistory
  219. }
  220. </script>
  221. <style scoped>
  222. .tag-edit { padding: 0; }
  223. .header {
  224. display: flex;
  225. align-items: center;
  226. gap: 16px;
  227. }
  228. .title { font-size: 16px; font-weight: 600; }
  229. .tag-id { color: #909399; font-size: 13px; }
  230. .section {
  231. padding: 16px 0 20px;
  232. border-bottom: 1px dashed #ebeef5;
  233. }
  234. .section:last-of-type { border-bottom: none; }
  235. .section-title {
  236. font-size: 15px;
  237. font-weight: 600;
  238. color: #303133;
  239. margin-bottom: 16px;
  240. padding-left: 8px;
  241. border-left: 3px solid #409EFF;
  242. }
  243. .footer {
  244. display: flex;
  245. justify-content: center;
  246. gap: 16px;
  247. padding-top: 20px;
  248. }
  249. </style>