EnergyBehaviorConfig.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. <template>
  2. <div class="energy-behavior-config admin-page">
  3. <div class="header">
  4. <h2>能量行为配置</h2>
  5. <div class="filters">
  6. <el-input v-model="keyword" placeholder="搜索行为名称/编码..." prefix-icon="el-icon-search" style="width:260px" clearable @keyup.enter.native="loadList" @clear="loadList" />
  7. <el-select v-model="enabledFilter" placeholder="状态筛选" @change="loadList" style="width: 140px;" clearable>
  8. <el-option label="全部" value="" />
  9. <el-option label="已启用" value="1" />
  10. <el-option label="已禁用" value="0" />
  11. </el-select>
  12. </div>
  13. </div>
  14. <div class="adm-table-wrap">
  15. <el-table :data="list" v-loading="loading" border stripe highlight-current-row height="100%">
  16. <el-table-column prop="behaviorCode" label="行为编码" width="160">
  17. <template slot="header">
  18. <span class="sort-header" @click.stop="onSortClick('behaviorCode', $event)">行为编码<span v-if="sortIndex('behaviorCode') !== null" class="sort-index" :class="sortClass('behaviorCode') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('behaviorCode') }}</span>{{ sortIcon('behaviorCode') }}</span>
  19. </template>
  20. </el-table-column>
  21. <el-table-column prop="behaviorName" label="行为名称" width="130">
  22. <template slot="header">
  23. <span class="sort-header" @click.stop="onSortClick('behaviorName', $event)">行为名称<span v-if="sortIndex('behaviorName') !== null" class="sort-index" :class="sortClass('behaviorName') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('behaviorName') }}</span>{{ sortIcon('behaviorName') }}</span>
  24. </template>
  25. </el-table-column>
  26. <el-table-column prop="description" label="描述" min-width="160" show-overflow-tooltip>
  27. <template slot="header">
  28. <span class="sort-header" @click.stop="onSortClick('description', $event)">描述<span v-if="sortIndex('description') !== null" class="sort-index" :class="sortClass('description') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('description') }}</span>{{ sortIcon('description') }}</span>
  29. </template>
  30. </el-table-column>
  31. <el-table-column label="默认值" width="80">
  32. <template slot-scope="{ row }">
  33. <el-tag size="mini" type="info">{{ row.defaultAmount }}</el-tag>
  34. </template>
  35. </el-table-column>
  36. <el-table-column prop="amountSource" label="值来源" width="120">
  37. <template slot-scope="{ row }">
  38. <el-tag size="mini" :type="sourceTypeTag(row.amountSource)">{{ sourceTypeLabel(row.amountSource) }}</el-tag>
  39. </template>
  40. </el-table-column>
  41. <el-table-column label="维度分配" min-width="200">
  42. <template slot-scope="{ row }">
  43. <span v-if="!row.dimensionAssign" style="color:#999">未配置</span>
  44. <el-tag v-for="d in parseDimAssign(row.dimensionAssign)" :key="d.dim" size="mini" :style="{ background: dimColor(d.dim), color: '#fff', marginRight: '4px' }">
  45. {{ dimLabel(d.dim) }} {{ d.ratio }}%
  46. </el-tag>
  47. </template>
  48. </el-table-column>
  49. <el-table-column label="每日上限" width="80">
  50. <template slot-scope="{ row }">
  51. <span>{{ row.dailyLimit || '不限' }}</span>
  52. </template>
  53. </el-table-column>
  54. <el-table-column label="过期天数" width="80">
  55. <template slot-scope="{ row }">
  56. <span>{{ row.expireDays || '永久' }}</span>
  57. </template>
  58. </el-table-column>
  59. <el-table-column label="状态" width="80">
  60. <template slot-scope="{ row }">
  61. <el-switch :value="row.enabled === 1" @change="toggleStatus(row)" />
  62. </template>
  63. </el-table-column>
  64. <el-table-column label="操作" width="120" fixed="right">
  65. <template slot-scope="{ row }">
  66. <el-button size="mini" type="primary" @click="showEdit(row)">编辑</el-button>
  67. </template>
  68. </el-table-column>
  69. </el-table>
  70. </div>
  71. <el-dialog title="编辑能量行为配置" :visible.sync="dialogVisible" width="700px" :close-on-click-modal="false">
  72. <el-form :model="form" label-width="110px" size="small">
  73. <el-row :gutter="20">
  74. <el-col :span="12">
  75. <el-form-item label="行为编码">
  76. <el-input v-model="form.behaviorCode" disabled />
  77. </el-form-item>
  78. </el-col>
  79. <el-col :span="12">
  80. <el-form-item label="行为名称">
  81. <el-input v-model="form.behaviorName" disabled />
  82. </el-form-item>
  83. </el-col>
  84. </el-row>
  85. <el-form-item label="描述">
  86. <el-input v-model="form.description" disabled />
  87. </el-form-item>
  88. <el-row :gutter="20">
  89. <el-col :span="8">
  90. <el-form-item label="默认能量值">
  91. <el-input-number v-model="form.defaultAmount" :min="0" :max="9999" />
  92. </el-form-item>
  93. </el-col>
  94. <el-col :span="8">
  95. <el-form-item label="值来源">
  96. <el-select v-model="form.amountSource" style="width:100%">
  97. <el-option label="固定值" value="fixed" />
  98. <el-option label="跟随积分" value="points" />
  99. <el-option label="跟随金额" value="order_amount" />
  100. <el-option label="从配置表读取" value="config" />
  101. </el-select>
  102. </el-form-item>
  103. </el-col>
  104. <el-col :span="8">
  105. <el-form-item label="过期天数">
  106. <el-input-number v-model="form.expireDays" :min="0" :max="3650" />
  107. </el-form-item>
  108. </el-col>
  109. </el-row>
  110. <el-row :gutter="20">
  111. <el-col :span="12">
  112. <el-form-item label="每日上限">
  113. <el-input-number v-model="form.dailyLimit" :min="0" :max="9999" />
  114. </el-form-item>
  115. </el-col>
  116. <el-col :span="12">
  117. <el-form-item label="冷却时间(秒)">
  118. <el-input-number v-model="form.cooldownSeconds" :min="0" :max="86400" />
  119. </el-form-item>
  120. </el-col>
  121. </el-row>
  122. <el-form-item label="维度分配">
  123. <div v-for="(d, i) in dimAssignList" :key="i" style="display:flex;align-items:center;margin-bottom:8px;gap:8px">
  124. <el-select v-model="d.dim" style="width:120px" @change="normalizeDimRatio">
  125. <el-option v-for="dim in dimensions" :key="dim.code" :label="dim.name" :value="dim.code" />
  126. </el-select>
  127. <el-slider v-model="d.ratio" :min="0" :max="100" style="flex:1" />
  128. <span style="width:40px;text-align:right">{{ d.ratio }}%</span>
  129. <el-button type="danger" size="mini" icon="el-icon-delete" circle @click="removeDim(i)" v-if="dimAssignList.length > 1" />
  130. </div>
  131. <el-button size="mini" @click="addDim" v-if="dimAssignList.length < 5">+ 添加维度</el-button>
  132. </el-form-item>
  133. </el-form>
  134. <span slot="footer">
  135. <el-button @click="dialogVisible = false">取消</el-button>
  136. <el-button type="primary" @click="saveConfig">保存</el-button>
  137. </span>
  138. </el-dialog>
  139. </div>
  140. </template>
  141. <script>
  142. import SortMixin from '@/mixins/SortMixin'
  143. import { getEnergyBehaviorList, updateEnergyBehavior, toggleEnergyBehavior } from '@/api/energy'
  144. const DIMENSION_MAP = {
  145. body: { name: '身', color: '#FF8C42' },
  146. mind: { name: '心', color: '#FF6B9D' },
  147. wisdom: { name: '智', color: '#6366F1' },
  148. action: { name: '行', color: '#10B981' },
  149. wealth: { name: '富', color: '#F59E0B' }
  150. }
  151. export default {
  152. mixins: [SortMixin],
  153. name: 'EnergyBehaviorConfig',
  154. data() {
  155. return {
  156. keyword: '',
  157. enabledFilter: '',
  158. loading: false,
  159. list: [],
  160. dialogVisible: false,
  161. form: {},
  162. dimAssignList: [],
  163. dimensions: Object.entries(DIMENSION_MAP).map(([code, v]) => ({ code, name: v.name })),
  164. sortableColumns: { behaviorCode: '行为编码', behaviorName: '行为名称', description: '描述' }
  165. }
  166. },
  167. mounted() {
  168. this.loadList()
  169. },
  170. methods: {
  171. async loadList() {
  172. this.loading = true
  173. try {
  174. const params = {}
  175. if (this.sortState.length > 0) params.sort = this.sortState
  176. const res = await getEnergyBehaviorList(params)
  177. if (res.code === 200) {
  178. this.list = (res.data || []).filter(r => {
  179. if (this.keyword) {
  180. const kw = this.keyword.toLowerCase()
  181. if (!r.behaviorCode.toLowerCase().includes(kw) && !r.behaviorName.toLowerCase().includes(kw)) return false
  182. }
  183. if (this.enabledFilter !== '') {
  184. if (r.enabled !== parseInt(this.enabledFilter)) return false
  185. }
  186. return true
  187. })
  188. }
  189. } finally {
  190. this.loading = false
  191. }
  192. },
  193. showEdit(row) {
  194. this.form = { ...row }
  195. this.dimAssignList = this.parseDimAssign(row.dimensionAssign)
  196. if (this.dimAssignList.length === 0) {
  197. this.dimAssignList = [{ dim: 'action', ratio: 100 }]
  198. }
  199. this.dialogVisible = true
  200. },
  201. parseDimAssign(json) {
  202. if (!json) return []
  203. try { return JSON.parse(json) } catch { return [] }
  204. },
  205. async saveConfig() {
  206. const dimensionAssign = JSON.stringify(this.dimAssignList.filter(d => d.ratio > 0))
  207. const payload = { ...this.form, dimensionAssign }
  208. const res = await updateEnergyBehavior(payload)
  209. if (res.code === 200) {
  210. this.$message.success('保存成功')
  211. this.dialogVisible = false
  212. this.loadList()
  213. } else {
  214. this.$message.error(res.message || '保存失败')
  215. }
  216. },
  217. async toggleStatus(row) {
  218. const res = await toggleEnergyBehavior(row.id)
  219. if (res.code === 200) {
  220. this.$message.success(res.message)
  221. this.loadList()
  222. }
  223. },
  224. addDim() {
  225. const used = this.dimAssignList.map(d => d.dim)
  226. const next = this.dimensions.find(d => !used.includes(d.code))
  227. if (next) this.dimAssignList.push({ dim: next.code, ratio: 0 })
  228. },
  229. removeDim(i) {
  230. this.dimAssignList.splice(i, 1)
  231. this.normalizeDimRatio()
  232. },
  233. normalizeDimRatio() {
  234. // 自动调整,确保总和为100
  235. },
  236. sourceTypeLabel(v) {
  237. return { fixed: '固定值', points: '跟随积分', order_amount: '跟随金额', config: '配置表读取' }[v] || v
  238. },
  239. sourceTypeTag(v) {
  240. return { fixed: '', points: 'warning', order_amount: 'danger', config: 'info' }[v] || ''
  241. },
  242. dimLabel(code) { return (DIMENSION_MAP[code] || {}).name || code },
  243. dimColor(code) { return (DIMENSION_MAP[code] || {}).color || '#999' }
  244. }
  245. }
  246. </script>
  247. <style scoped>
  248. .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
  249. .filters { display: flex; gap: 10px; align-items: center; }
  250. .sort-header {
  251. cursor: pointer;
  252. user-select: none;
  253. font-weight: 500;
  254. font-size: 12px;
  255. }
  256. .sort-header:hover {
  257. color: #409eff;
  258. }
  259. </style>