BadgeManage.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. <template>
  2. <div class="badge-manage-page">
  3. <el-card>
  4. <div slot="header" class="clearfix">
  5. <span>勋章管理</span>
  6. <div style="float:right; display:flex; gap:10px;">
  7. <el-button type="primary" size="small" @click="showAddDialog">新增勋章</el-button>
  8. </div>
  9. </div>
  10. <!-- 搜索 -->
  11. <el-form :model="filters" inline size="small" style="margin-bottom:16px;">
  12. <el-form-item label="名称">
  13. <el-input v-model="filters.name" placeholder="搜索勋章名称" clearable @input="onSearchInput" />
  14. </el-form-item>
  15. <el-form-item label="分类">
  16. <el-select v-model="filters.category" placeholder="全部分类" clearable @change="fetchList" style="width:140px;">
  17. <el-option v-for="c in categoryOptions" :key="c.value" :label="c.label" :value="c.value" />
  18. </el-select>
  19. </el-form-item>
  20. <el-form-item label="等级">
  21. <el-select v-model="filters.level" placeholder="全部等级" clearable @change="fetchList" style="width:140px;">
  22. <el-option v-for="c in levelOptions" :key="c.value" :label="c.label" :value="c.value" />
  23. </el-select>
  24. </el-form-item>
  25. </el-form>
  26. <el-table :data="list" border stripe v-loading="loading" empty-text="暂无数据" style="width:100%" :max-height="tableHeight">
  27. <el-table-column prop="id" label="ID" width="60" />
  28. <el-table-column prop="name" label="名称" min-width="120" />
  29. <el-table-column prop="category" label="分类" width="90" />
  30. <el-table-column label="等级" width="80">
  31. <template slot-scope="{ row }">
  32. {{ levelMap[row.level] || row.level || '-' }}
  33. </template>
  34. </el-table-column>
  35. <el-table-column label="稀有度" width="80">
  36. <template slot-scope="{ row }">
  37. {{ rarityMap[row.rarity] || row.rarity || '-' }}
  38. </template>
  39. </el-table-column>
  40. <el-table-column label="启用" width="80" align="center">
  41. <template slot-scope="{ row }">
  42. <el-switch
  43. :value="row.isActive === 1"
  44. active-color="#67C23A"
  45. @change="onToggleActive(row)"
  46. />
  47. </template>
  48. </el-table-column>
  49. <el-table-column prop="sortOrder" label="排序" width="60" align="center" />
  50. <el-table-column prop="createdAt" label="创建时间" width="160" />
  51. <el-table-column label="操作" width="160" fixed="right">
  52. <template slot-scope="{ row }">
  53. <el-button type="text" @click="editRow(row)">编辑</el-button>
  54. <el-popconfirm title="确定删除此勋章吗?" @confirm="deleteRow(row)">
  55. <el-button type="text" style="color:#f56c6c" slot="reference">删除</el-button>
  56. </el-popconfirm>
  57. </template>
  58. </el-table-column>
  59. </el-table>
  60. </el-card>
  61. <!-- 新增/编辑弹窗 -->
  62. <el-dialog :visible.sync="dialog.visible" :title="dialog.isEdit ? '编辑勋章' : '新增勋章'" width="600px" :close-on-click-modal="false">
  63. <el-form :model="dialog.form" label-width="110px" size="small">
  64. <el-row :gutter="16">
  65. <el-col :span="12">
  66. <el-form-item label="名称" required>
  67. <el-input v-model="dialog.form.name" placeholder="勋章名称" />
  68. </el-form-item>
  69. </el-col>
  70. <el-col :span="12">
  71. <el-form-item label="分类" required>
  72. <el-select v-model="dialog.form.category" placeholder="请选择" style="width:100%">
  73. <el-option v-for="c in categoryOptions" :key="c.value" :label="c.label" :value="c.value" />
  74. </el-select>
  75. </el-form-item>
  76. </el-col>
  77. </el-row>
  78. <el-row :gutter="16">
  79. <el-col :span="12">
  80. <el-form-item label="等级">
  81. <el-select v-model="dialog.form.level" placeholder="请选择" style="width:100%">
  82. <el-option v-for="c in levelOptions" :key="c.value" :label="c.label" :value="c.value" />
  83. </el-select>
  84. </el-form-item>
  85. </el-col>
  86. <el-col :span="12">
  87. <el-form-item label="稀有度">
  88. <el-select v-model="dialog.form.rarity" placeholder="请选择" style="width:100%">
  89. <el-option v-for="c in rarityOptions" :key="c.value" :label="c.label" :value="c.value" />
  90. </el-select>
  91. </el-form-item>
  92. </el-col>
  93. </el-row>
  94. <el-form-item label="图标URL">
  95. <el-input v-model="dialog.form.icon" placeholder="勋章图标地址" />
  96. </el-form-item>
  97. <el-form-item label="描述">
  98. <el-input type="textarea" :rows="3" v-model="dialog.form.description" placeholder="勋章描述..." />
  99. </el-form-item>
  100. <el-row :gutter="16">
  101. <el-col :span="12">
  102. <el-form-item label="触发类型">
  103. <el-input v-model="dialog.form.triggerType" placeholder="如: micro_action_count" />
  104. </el-form-item>
  105. </el-col>
  106. <el-col :span="12">
  107. <el-form-item label="触发阈值">
  108. <el-input-number v-model="dialog.form.threshold" :min="0" style="width:100%" />
  109. </el-form-item>
  110. </el-col>
  111. </el-row>
  112. <el-row :gutter="16">
  113. <el-col :span="12">
  114. <el-form-item label="排序">
  115. <el-input-number v-model="dialog.form.sortOrder" :min="0" style="width:100%" />
  116. </el-form-item>
  117. </el-col>
  118. <el-col :span="12">
  119. <el-form-item label="过期天数">
  120. <el-input-number v-model="dialog.form.expireDays" :min="0" style="width:100%" />
  121. </el-form-item>
  122. </el-col>
  123. </el-row>
  124. <el-row :gutter="16">
  125. <el-col :span="12">
  126. <el-form-item label="是否启用">
  127. <el-switch v-model="dialog.form._active" active-color="#67C23A" />
  128. </el-form-item>
  129. </el-col>
  130. <el-col :span="12">
  131. <el-form-item label="需要审批">
  132. <el-switch v-model="dialog.form._needApproval" active-color="#67C23A" />
  133. </el-form-item>
  134. </el-col>
  135. </el-row>
  136. </el-form>
  137. <div slot="footer">
  138. <el-button @click="dialog.visible = false" size="small">取消</el-button>
  139. <el-button type="primary" @click="saveDialog" size="small" :loading="dialog.saving">保存</el-button>
  140. </div>
  141. </el-dialog>
  142. </div>
  143. </template>
  144. <script>
  145. import { getBadgeList, createBadge, updateBadge, deleteBadge } from '@/api/badge'
  146. export default {
  147. computed: {
  148. tableHeight() {
  149. return window.innerHeight - 300
  150. }
  151. },
  152. data() {
  153. return {
  154. loading: false,
  155. list: [],
  156. searchTimer: null,
  157. filters: { name: '', category: '', level: '' },
  158. categoryOptions: [
  159. { value: '学习', label: '学习' },
  160. { value: '运动', label: '运动' },
  161. { value: '家务', label: '家务' },
  162. { value: '阅读', label: '阅读' },
  163. { value: '打卡', label: '打卡' },
  164. { value: '综合', label: '综合' }
  165. ],
  166. levelOptions: [
  167. { value: 'bronze', label: '铜牌' },
  168. { value: 'silver', label: '银牌' },
  169. { value: 'gold', label: '金牌' },
  170. { value: 'diamond', label: '钻石' }
  171. ],
  172. rarityOptions: [
  173. { value: 'common', label: '普通' },
  174. { value: 'rare', label: '稀有' },
  175. { value: 'epic', label: '史诗' },
  176. { value: 'legendary', label: '传说' }
  177. ],
  178. levelMap: { bronze: '铜牌', silver: '银牌', gold: '金牌', diamond: '钻石' },
  179. rarityMap: { common: '普通', rare: '稀有', epic: '史诗', legendary: '传说' },
  180. dialog: {
  181. visible: false,
  182. isEdit: false,
  183. saving: false,
  184. form: {
  185. id: null,
  186. name: '',
  187. description: '',
  188. icon: '',
  189. category: '',
  190. level: '',
  191. rarity: '',
  192. triggerType: '',
  193. threshold: 0,
  194. sortOrder: 0,
  195. expireDays: 0,
  196. _active: true,
  197. _needApproval: false
  198. }
  199. }
  200. }
  201. },
  202. mounted() { this.fetchList() },
  203. methods: {
  204. onSearchInput() {
  205. if (this.searchTimer) clearTimeout(this.searchTimer)
  206. this.searchTimer = setTimeout(() => { this.fetchList() }, 300)
  207. },
  208. async fetchList() {
  209. this.loading = true
  210. try {
  211. const res = await getBadgeList()
  212. var data = res.data || []
  213. // Apply client-side filters
  214. if (this.filters.name) {
  215. var q = this.filters.name.toLowerCase()
  216. data = data.filter(function(d) { return d.name && d.name.toLowerCase().indexOf(q) !== -1 })
  217. }
  218. if (this.filters.category) {
  219. data = data.filter(function(d) { return d.category === this.filters.category }.bind(this))
  220. }
  221. if (this.filters.level) {
  222. data = data.filter(function(d) { return d.level === this.filters.level }.bind(this))
  223. }
  224. this.list = data
  225. } catch (e) {
  226. console.error('加载勋章列表失败', e)
  227. this.$message.error('加载失败')
  228. } finally {
  229. this.loading = false
  230. }
  231. },
  232. showAddDialog() {
  233. this.dialog.isEdit = false
  234. this.dialog.form = {
  235. id: null,
  236. name: '',
  237. description: '',
  238. icon: '',
  239. category: '',
  240. level: '',
  241. rarity: '',
  242. triggerType: '',
  243. threshold: 0,
  244. sortOrder: 0,
  245. expireDays: 0,
  246. _active: true,
  247. _needApproval: false
  248. }
  249. this.dialog.visible = true
  250. },
  251. editRow(row) {
  252. this.dialog.isEdit = true
  253. this.dialog.form = {
  254. id: row.id,
  255. name: row.name || '',
  256. description: row.description || '',
  257. icon: row.icon || '',
  258. category: row.category || '',
  259. level: row.level || '',
  260. rarity: row.rarity || '',
  261. triggerType: row.triggerType || '',
  262. threshold: row.threshold != null ? row.threshold : 0,
  263. sortOrder: row.sortOrder != null ? row.sortOrder : 0,
  264. expireDays: row.expireDays != null ? row.expireDays : 0,
  265. _active: row.isActive === 1,
  266. _needApproval: !!row.needApproval
  267. }
  268. this.dialog.visible = true
  269. },
  270. async saveDialog() {
  271. var form = this.dialog.form
  272. if (!form.name) {
  273. this.$message.warning('请填写勋章名称')
  274. return
  275. }
  276. if (!form.category) {
  277. this.$message.warning('请选择分类')
  278. return
  279. }
  280. this.dialog.saving = true
  281. try {
  282. var payload = {
  283. id: form.id,
  284. name: form.name,
  285. description: form.description,
  286. icon: form.icon,
  287. category: form.category,
  288. level: form.level,
  289. rarity: form.rarity,
  290. triggerType: form.triggerType,
  291. threshold: form.threshold,
  292. sortOrder: form.sortOrder,
  293. expireDays: form.expireDays,
  294. isActive: form._active ? 1 : 0,
  295. needApproval: form._needApproval
  296. }
  297. if (form.id) {
  298. await updateBadge(payload)
  299. this.$message.success('更新成功')
  300. } else {
  301. await createBadge(payload)
  302. this.$message.success('创建成功')
  303. }
  304. this.dialog.visible = false
  305. this.fetchList()
  306. } catch (e) {
  307. console.error('保存失败', e)
  308. this.$message.error('保存失败')
  309. } finally {
  310. this.dialog.saving = false
  311. }
  312. },
  313. async deleteRow(row) {
  314. try {
  315. await deleteBadge(row.id)
  316. this.$message.success('删除成功')
  317. this.fetchList()
  318. } catch (e) {
  319. console.error('删除失败', e)
  320. this.$message.error('删除失败')
  321. }
  322. },
  323. async onToggleActive(row) {
  324. try {
  325. var newVal = row.isActive === 1 ? 0 : 1
  326. await updateBadge({ id: row.id, isActive: newVal })
  327. this.$message.success(newVal === 1 ? '已启用' : '已禁用')
  328. row.isActive = newVal
  329. } catch (e) {
  330. console.error('操作失败', e)
  331. this.$message.error('操作失败')
  332. }
  333. }
  334. }
  335. }
  336. </script>
  337. <style scoped>
  338. .badge-manage-page {
  339. padding: 16px;
  340. }
  341. </style>