BadgeManage.vue 14 KB

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