PointsLog.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879
  1. <template>
  2. <div class="points-management">
  3. <el-card>
  4. <template #header>
  5. <div class="card-header">
  6. <span>积分明细管理</span>
  7. <div class="header-actions">
  8. <el-button type="primary" @click="showAdjustDialog">手动调整积分</el-button>
  9. </div>
  10. </div>
  11. </template>
  12. <!-- 搜索栏 -->
  13. <div class="search-bar">
  14. <el-form :model="searchForm" inline>
  15. <el-form-item label="用户">
  16. <el-input
  17. v-model="selectedUserDisplay"
  18. placeholder="点击选择用户"
  19. clearable
  20. style="width: 150px;"
  21. @click="showUserDialog"
  22. @clear="handleUserClear"
  23. />
  24. </el-form-item>
  25. <el-form-item label="积分类型">
  26. <el-select
  27. v-model="searchForm.type"
  28. placeholder="请选择类型"
  29. clearable
  30. style="width: 150px;"
  31. >
  32. <el-option
  33. v-for="item in pointsTypes"
  34. :key="item.value"
  35. :label="item.label"
  36. :value="item.value"
  37. />
  38. </el-select>
  39. </el-form-item>
  40. <el-form-item label="时间范围">
  41. <el-date-picker
  42. v-model="searchForm.dateRange"
  43. type="datetimerange"
  44. range-separator="至"
  45. start-placeholder="开始时间"
  46. end-placeholder="结束时间"
  47. format="YYYY-MM-DD HH:mm:ss"
  48. value-format="YYYY-MM-DD HH:mm:ss"
  49. style="width: 300px;"
  50. />
  51. </el-form-item>
  52. <el-form-item>
  53. <el-button type="primary" @click="handleSearch" :loading="loading">搜索</el-button>
  54. <el-button @click="handleReset">重置</el-button>
  55. </el-form-item>
  56. </el-form>
  57. </div>
  58. <!-- 统计卡片 -->
  59. <div class="stats-cards">
  60. <el-row :gutter="20" style="margin-bottom: 20px;">
  61. <el-col :span="6">
  62. <el-card class="stats-card">
  63. <div class="stats-item">
  64. <div class="stats-label">积分发放</div>
  65. <div class="stats-value increase">+{{ statsData.positive_total || 0 }}</div>
  66. </div>
  67. </el-card>
  68. </el-col>
  69. <el-col :span="6">
  70. <el-card class="stats-card">
  71. <div class="stats-item">
  72. <div class="stats-label">积分消耗</div>
  73. <div class="stats-value decrease">{{ statsData.negative_total || 0 }}</div>
  74. </div>
  75. </el-card>
  76. </el-col>
  77. </el-row>
  78. </div>
  79. <!-- 积分记录表格 -->
  80. <el-table
  81. :data="pointsList"
  82. border
  83. style="width: 100%"
  84. v-loading="loading"
  85. row-key="id"
  86. >
  87. <el-table-column prop="id" label="记录ID" width="80" />
  88. <el-table-column prop="user_id" label="用户ID" width="100" />
  89. <el-table-column prop="user.nickname" label="用户名" width="100" />
  90. <el-table-column label="积分变动" width="150">
  91. <template #default="{ row }">
  92. <span
  93. :class="{
  94. 'point-increase': row.point > 0,
  95. 'point-decrease': row.point < 0,
  96. 'point-freeze': row.freeze_point !== 0
  97. }"
  98. >
  99. {{ row.point > 0 ? '+' : '' }}{{ row.point }}
  100. <span v-if="row.freeze_point !== 0" class="freeze-point">
  101. (冻结: {{ row.freeze_point > 0 ? '+' : '' }}{{ row.freeze_point }})
  102. </span>
  103. </span>
  104. </template>
  105. </el-table-column>
  106. <el-table-column label="变动后余额" width="150">
  107. <template #default="{ row }">
  108. <div>
  109. <div>积分: {{ row.after_point }}</div>
  110. <div v-if="row.after_freeze_point !== 0" class="freeze-balance">
  111. 冻结: {{ row.after_freeze_point }}
  112. </div>
  113. </div>
  114. </template>
  115. </el-table-column>
  116. <el-table-column prop="type" label="变动类型" width="120">
  117. <template #default="{ row }">
  118. <el-tag
  119. :type="getTypeTagType(row.type)"
  120. size="small"
  121. >
  122. {{ getTypeName(row.type) }}
  123. </el-tag>
  124. </template>
  125. </el-table-column>
  126. <el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
  127. <el-table-column prop="create_time" label="创建时间" width="180" />
  128. </el-table>
  129. <!-- 分页 -->
  130. <el-pagination
  131. :current-page="pagination.page"
  132. :page-size="pagination.limit"
  133. :total="pagination.total"
  134. :page-sizes="[10, 20, 50, 100]"
  135. layout="total, sizes, prev, pager, next, jumper"
  136. style="margin-top: 20px; text-align: right;"
  137. @size-change="handleSizeChange"
  138. @current-change="handleCurrentChange"
  139. />
  140. </el-card>
  141. <!-- 手动调整积分对话框 -->
  142. <el-dialog
  143. v-model="adjustDialogVisible"
  144. title="手动调整积分"
  145. width="500px"
  146. >
  147. <el-form
  148. ref="adjustFormRef"
  149. :model="adjustForm"
  150. :rules="adjustRules"
  151. label-width="100px"
  152. >
  153. <el-form-item label="选择用户" prop="user_id">
  154. <el-input
  155. v-model="selectedAdjustUserDisplay"
  156. placeholder="点击选择用户"
  157. readonly
  158. style="cursor: pointer;"
  159. @click="showAdjustUserDialog"
  160. />
  161. </el-form-item>
  162. <el-form-item label="操作类型" prop="operation_type">
  163. <el-radio-group v-model="adjustForm.operation_type">
  164. <el-radio label="increase">增加积分</el-radio>
  165. <el-radio label="decrease">减少积分</el-radio>
  166. </el-radio-group>
  167. </el-form-item>
  168. <el-form-item label="积分数量" prop="point">
  169. <el-input-number
  170. v-model="adjustForm.point"
  171. :min="1"
  172. :max="999999"
  173. :step="1"
  174. placeholder="请输入积分数量"
  175. style="width: 100%;"
  176. />
  177. </el-form-item>
  178. <el-form-item label="备注" prop="remark">
  179. <el-input
  180. v-model="adjustForm.remark"
  181. type="textarea"
  182. :rows="3"
  183. placeholder="请输入调整原因"
  184. />
  185. </el-form-item>
  186. </el-form>
  187. <template #footer>
  188. <span class="dialog-footer">
  189. <el-button @click="adjustDialogVisible = false">取消</el-button>
  190. <el-button
  191. type="primary"
  192. @click="handleAdjustPoints"
  193. :loading="adjustLoading"
  194. >
  195. 确定
  196. </el-button>
  197. </span>
  198. </template>
  199. </el-dialog>
  200. <!-- 调整积分用户选择对话框 -->
  201. <el-dialog
  202. v-model="adjustUserDialogVisible"
  203. title="选择用户"
  204. width="80%"
  205. >
  206. <div style="margin-bottom: 20px;">
  207. <el-input
  208. v-model="adjustUserSearchKeyword"
  209. placeholder="请输入用户ID、昵称或手机号进行搜索"
  210. clearable
  211. style="width: 300px;"
  212. @keyup.enter="handleAdjustUserSearch"
  213. >
  214. <template #append>
  215. <el-button @click="handleAdjustUserSearch">搜索</el-button>
  216. </template>
  217. </el-input>
  218. <el-button @click="resetAdjustUserSearch" style="margin-left: 10px;">重置</el-button>
  219. </div>
  220. <el-table :data="adjustUserList" @row-click="selectAdjustUser" v-loading="adjustUserLoading" max-height="400px">
  221. <el-table-column prop="id" label="ID" min-width="80"></el-table-column>
  222. <el-table-column label="头像" min-width="80">
  223. <template #default="{ row }">
  224. <el-avatar :size="40" :src="row.avatar" :alt="row.nickname">
  225. <template #default>
  226. <el-icon><User /></el-icon>
  227. </template>
  228. </el-avatar>
  229. </template>
  230. </el-table-column>
  231. <el-table-column prop="nickname" label="昵称" min-width="80"></el-table-column>
  232. <el-table-column prop="mobile" label="手机号" min-width="120">
  233. <template #default="{ row }">
  234. {{ row.mobile || '-' }}
  235. </template>
  236. </el-table-column>
  237. <el-table-column prop="point" label="当前积分" min-width="100">
  238. <template #default="{ row }">
  239. <span class="point-value">{{ row.point || 0 }}</span>
  240. </template>
  241. </el-table-column>
  242. <el-table-column prop="update_time" label="更新时间" min-width="200" />
  243. <el-table-column label="操作" width="80" fixed="right">
  244. <template #default="{ row }">
  245. <el-button type="primary" size="small" @click="selectAdjustUser(row)">选择</el-button>
  246. </template>
  247. </el-table-column>
  248. </el-table>
  249. <el-pagination
  250. :current-page="adjustUserPagination.page"
  251. :page-size="adjustUserPagination.limit"
  252. :total="adjustUserPagination.total"
  253. :page-sizes="[10, 20, 50]"
  254. layout="total, sizes, prev, pager, next"
  255. style="margin-top: 20px; text-align: right;"
  256. @size-change="handleAdjustUserSizeChange"
  257. @current-change="handleAdjustUserCurrentChange"
  258. />
  259. </el-dialog>
  260. <!-- 详情对话框 -->
  261. <el-dialog
  262. v-model="detailDialogVisible"
  263. title="积分记录详情"
  264. width="600px"
  265. >
  266. <el-descriptions :column="2" border v-if="currentDetail">
  267. <el-descriptions-item label="记录ID">{{ currentDetail.id }}</el-descriptions-item>
  268. <el-descriptions-item label="用户ID">{{ currentDetail.user_id }}</el-descriptions-item>
  269. <el-descriptions-item label="积分变动">
  270. <span
  271. :class="{
  272. 'point-increase': currentDetail.point > 0,
  273. 'point-decrease': currentDetail.point < 0
  274. }"
  275. >
  276. {{ currentDetail.point > 0 ? '+' : '' }}{{ currentDetail.point }}
  277. </span>
  278. </el-descriptions-item>
  279. <el-descriptions-item label="冻结积分变动">
  280. <span v-if="currentDetail.freeze_point !== 0">
  281. {{ currentDetail.freeze_point > 0 ? '+' : '' }}{{ currentDetail.freeze_point }}
  282. </span>
  283. <span v-else>无</span>
  284. </el-descriptions-item>
  285. <el-descriptions-item label="变动后积分余额">{{ currentDetail.after_point }}</el-descriptions-item>
  286. <el-descriptions-item label="变动后冻结余额">{{ currentDetail.after_freeze_point }}</el-descriptions-item>
  287. <el-descriptions-item label="变动类型">
  288. <el-tag :type="getTypeTagType(currentDetail.type)" size="small">
  289. {{ getTypeName(currentDetail.type) }}
  290. </el-tag>
  291. </el-descriptions-item>
  292. <el-descriptions-item label="业务类型">
  293. <el-tag type="info" size="small">
  294. {{ getBizTypeName(currentDetail.biz_type) }}
  295. </el-tag>
  296. </el-descriptions-item>
  297. <el-descriptions-item label="业务ID">{{ currentDetail.biz_id || '无' }}</el-descriptions-item>
  298. <el-descriptions-item label="创建时间">{{ currentDetail.create_time }}</el-descriptions-item>
  299. <el-descriptions-item label="备注" :span="2">{{ currentDetail.remark || '无' }}</el-descriptions-item>
  300. </el-descriptions>
  301. </el-dialog>
  302. <!-- 用户选择对话框 -->
  303. <el-dialog
  304. v-model="userDialogVisible"
  305. title="选择用户"
  306. width="80%"
  307. >
  308. <div style="margin-bottom: 20px;">
  309. <el-input
  310. v-model="userSearchKeyword"
  311. placeholder="请输入用户ID、昵称或手机号进行搜索"
  312. clearable
  313. style="width: 300px;"
  314. @keyup.enter="handleUserSearch"
  315. >
  316. <template #append>
  317. <el-button @click="handleUserSearch">搜索</el-button>
  318. </template>
  319. </el-input>
  320. <el-button @click="resetUserSearch" style="margin-left: 10px;">重置</el-button>
  321. </div>
  322. <el-table :data="userList" @row-click="selectUser" v-loading="userLoading" max-height="400px">
  323. <el-table-column prop="id" label="ID" min-width="80"></el-table-column>
  324. <el-table-column label="头像" min-width="80">
  325. <template #default="{ row }">
  326. <el-avatar :size="40" :src="row.avatar" :alt="row.nickname">
  327. <template #default>
  328. <el-icon><User /></el-icon>
  329. </template>
  330. </el-avatar>
  331. </template>
  332. </el-table-column>
  333. <el-table-column prop="nickname" label="昵称" min-width="80"></el-table-column>
  334. <el-table-column prop="mobile" label="手机号" min-width="120">
  335. <template #default="{ row }">
  336. {{ row.mobile || '-' }}
  337. </template>
  338. </el-table-column>
  339. <el-table-column prop="point" label="当前积分" min-width="100">
  340. <template #default="{ row }">
  341. <span class="point-value">{{ row.point || 0 }}</span>
  342. </template>
  343. </el-table-column>
  344. <el-table-column prop="update_time" label="更新时间" min-width="200" />
  345. <el-table-column label="操作" width="80" fixed="right">
  346. <template #default="{ row }">
  347. <el-button type="primary" size="small" @click="selectUser(row)">选择</el-button>
  348. </template>
  349. </el-table-column>
  350. </el-table>
  351. <el-pagination
  352. :current-page="userPagination.page"
  353. :page-size="userPagination.limit"
  354. :total="userPagination.total"
  355. :page-sizes="[10, 20, 50]"
  356. layout="total, sizes, prev, pager, next"
  357. style="margin-top: 20px; text-align: right;"
  358. @size-change="handleUserSizeChange"
  359. @current-change="handleUserCurrentChange"
  360. />
  361. </el-dialog>
  362. </div>
  363. </template>
  364. <script>
  365. import request from '@/utils/request'
  366. export default {
  367. name: 'PointsLog',
  368. data() {
  369. return {
  370. // 搜索表单
  371. searchForm: {
  372. user_id: '',
  373. type: '',
  374. biz_type: '',
  375. dateRange: []
  376. },
  377. // 分页
  378. pagination: {
  379. page: 1,
  380. limit: 20,
  381. total: 0
  382. },
  383. // 数据
  384. pointsList: [],
  385. loading: false,
  386. statsData: {},
  387. // 枚举数据
  388. pointsTypes: [
  389. { value: 1, label: '新增' },
  390. { value: 2, label: '减少' },
  391. { value: 3, label: '对冲' }
  392. ],
  393. bizTypes: [
  394. { value: 1, label: '订单' },
  395. { value: 2, label: '签到' },
  396. { value: 3, label: '推荐奖励' },
  397. { value: 4, label: '手动调整' },
  398. { value: 5, label: '商城兑换' }
  399. ],
  400. // 手动调整对话框
  401. adjustDialogVisible: false,
  402. adjustLoading: false,
  403. adjustFormRef: null,
  404. adjustForm: {
  405. user_id: '',
  406. operation_type: 'increase',
  407. point: 0,
  408. remark: ''
  409. },
  410. adjustRules: {
  411. user_id: [
  412. { required: true, message: '请选择用户', trigger: 'change' }
  413. ],
  414. operation_type: [
  415. { required: true, message: '请选择操作类型', trigger: 'change' }
  416. ],
  417. point: [
  418. { required: true, message: '请输入积分数量', trigger: 'blur' },
  419. { type: 'number', min: 1, message: '积分数量必须大于0', trigger: 'blur' },
  420. { validator: this.validateInteger, message: '积分数量必须是整数', trigger: 'blur' }
  421. ],
  422. remark: [
  423. { required: true, message: '请输入积分变动备注', trigger: 'blur' }
  424. ]
  425. },
  426. // 详情对话框
  427. detailDialogVisible: false,
  428. currentDetail: null,
  429. // 用户选择对话框
  430. userDialogVisible: false,
  431. userSearchKeyword: '',
  432. userList: [],
  433. userLoading: false,
  434. userPagination: {
  435. page: 1,
  436. limit: 20,
  437. total: 0
  438. },
  439. // 调整积分用户选择对话框
  440. adjustUserDialogVisible: false,
  441. adjustUserSearchKeyword: '',
  442. adjustUserList: [],
  443. adjustUserLoading: false,
  444. adjustUserPagination: {
  445. page: 1,
  446. limit: 20,
  447. total: 0
  448. }
  449. }
  450. },
  451. computed: {
  452. selectedUserDisplay: {
  453. get() {
  454. if (!this.searchForm.user_id) return ''
  455. const selectedUser = this.userList.find(user => user.id.toString() === this.searchForm.user_id)
  456. return selectedUser ? selectedUser.nickname : this.searchForm.user_id
  457. },
  458. set(value) {
  459. // 不需要设置逻辑
  460. }
  461. },
  462. selectedAdjustUserDisplay: {
  463. get() {
  464. if (!this.adjustForm.user_id) return ''
  465. const selectedUser = this.adjustUserList.find(user => user.id.toString() === this.adjustForm.user_id)
  466. return selectedUser ? selectedUser.nickname : this.adjustForm.user_id
  467. },
  468. set(value) {
  469. // 不需要设置逻辑
  470. }
  471. }
  472. },
  473. mounted() {
  474. this.getUserPointLogs()
  475. },
  476. methods: {
  477. getTypeName(type) {
  478. const item = this.pointsTypes.find(item => item.value === type)
  479. return item ? item.label : '未知'
  480. },
  481. getTypeTagType(type) {
  482. const typeMap = {
  483. 1: 'success',
  484. 2: 'danger',
  485. 3: 'warning'
  486. }
  487. return typeMap[type] || 'info'
  488. },
  489. getBizTypeName(bizType) {
  490. const item = this.bizTypes.find(item => item.value === bizType)
  491. return item ? item.label : '未知'
  492. },
  493. validateInteger(rule, value, callback) {
  494. if (!Number.isInteger(Number(value))) {
  495. callback(new Error('积分数量必须是整数'))
  496. } else {
  497. callback()
  498. }
  499. },
  500. // 获取积分记录列表
  501. async getUserPointLogs() {
  502. try {
  503. this.loading = true
  504. const params = {
  505. page: this.pagination.page,
  506. limit: this.pagination.limit,
  507. ...this.searchForm
  508. }
  509. if (this.searchForm.dateRange && this.searchForm.dateRange.length === 2) {
  510. params.create_time = this.searchForm.dateRange
  511. }
  512. delete params.dateRange
  513. const response = await request.get('/points/user_point_log', { params })
  514. this.pointsList = response.page.data || []
  515. this.pagination.total = response.page.total || 0
  516. this.statsData = response.data || {}
  517. } catch (error) {
  518. console.error('获取积分记录失败:', error)
  519. this.$message.error('获取积分记录失败')
  520. } finally {
  521. this.loading = false
  522. }
  523. },
  524. // 搜索
  525. handleSearch() {
  526. this.pagination.page = 1
  527. this.getUserPointLogs()
  528. },
  529. // 重置
  530. handleReset() {
  531. Object.assign(this.searchForm, {
  532. user_id: '',
  533. type: '',
  534. biz_type: '',
  535. dateRange: []
  536. })
  537. this.pagination.page = 1
  538. this.getUserPointLogs()
  539. },
  540. // 分页变化
  541. handleSizeChange(newSize) {
  542. this.pagination.limit = newSize
  543. this.pagination.page = 1
  544. this.getUserPointLogs()
  545. },
  546. handleCurrentChange(newPage) {
  547. this.pagination.page = newPage
  548. this.getUserPointLogs()
  549. },
  550. // 用户清除处理方法
  551. handleUserClear() {
  552. this.searchForm.user_id = ''
  553. },
  554. // 重置调整表单
  555. resetAdjustForm() {
  556. Object.assign(this.adjustForm, {
  557. user_id: '',
  558. operation_type: 'increase',
  559. point: 0,
  560. remark: ''
  561. })
  562. },
  563. // 显示调整积分用户选择对话框
  564. showAdjustUserDialog() {
  565. this.adjustUserDialogVisible = true
  566. this.getAdjustUserList()
  567. },
  568. // 获取调整积分用户列表
  569. async getAdjustUserList() {
  570. try {
  571. this.adjustUserLoading = true
  572. const params = {
  573. page: this.adjustUserPagination.page,
  574. limit: this.adjustUserPagination.limit,
  575. keyword: this.adjustUserSearchKeyword
  576. }
  577. const response = await request.get('/points/user_point', { params })
  578. this.adjustUserList = response.page.data || []
  579. this.adjustUserPagination.total = response.page.total || 0
  580. } catch (error) {
  581. console.error('获取用户列表失败:', error)
  582. this.$message.error('获取用户列表失败')
  583. } finally {
  584. this.adjustUserLoading = false
  585. }
  586. },
  587. // 选择调整积分用户
  588. selectAdjustUser(user) {
  589. this.adjustForm.user_id = user.id.toString()
  590. this.adjustUserDialogVisible = false
  591. this.$message.success(`已选择用户: ${user.nickname} (ID: ${user.id})`)
  592. },
  593. // 搜索调整积分用户
  594. handleAdjustUserSearch() {
  595. this.adjustUserPagination.page = 1
  596. this.getAdjustUserList()
  597. },
  598. // 重置调整积分用户搜索
  599. resetAdjustUserSearch() {
  600. this.adjustUserSearchKeyword = ''
  601. this.adjustUserPagination.page = 1
  602. this.getAdjustUserList()
  603. },
  604. // 调整积分用户列表分页处理
  605. handleAdjustUserSizeChange(newSize) {
  606. this.adjustUserPagination.limit = newSize
  607. this.adjustUserPagination.page = 1
  608. this.getAdjustUserList()
  609. },
  610. handleAdjustUserCurrentChange(newPage) {
  611. this.adjustUserPagination.page = newPage
  612. this.getAdjustUserList()
  613. },
  614. // 显示调整对话框
  615. showAdjustDialog() {
  616. this.resetAdjustForm()
  617. this.adjustDialogVisible = true
  618. },
  619. // 手动调整积分
  620. async handleAdjustPoints() {
  621. try {
  622. await this.$refs.adjustFormRef.validate()
  623. this.adjustLoading = true
  624. let pointValue = parseInt(this.adjustForm.point, 10)
  625. let typeValue = 1
  626. if (this.adjustForm.operation_type === 'decrease') {
  627. pointValue = -Math.abs(pointValue)
  628. typeValue = 2
  629. } else {
  630. pointValue = Math.abs(pointValue)
  631. typeValue = 1
  632. }
  633. const data = {
  634. user_id: this.adjustForm.user_id,
  635. point: pointValue,
  636. type: typeValue,
  637. remark: this.adjustForm.remark
  638. }
  639. await request.put(`/points/user_point/${this.adjustForm.user_id}/adjust`, data)
  640. this.$message.success('积分调整成功')
  641. this.adjustDialogVisible = false
  642. this.resetAdjustForm()
  643. this.getUserPointLogs()
  644. } catch (error) {
  645. console.error('积分调整失败:', error)
  646. if (error.message) {
  647. this.$message.error(error.message)
  648. } else {
  649. this.$message.error('积分调整失败,请检查输入信息')
  650. }
  651. } finally {
  652. this.adjustLoading = false
  653. }
  654. },
  655. // 显示详情
  656. showDetailDialog(row) {
  657. this.currentDetail = row
  658. this.detailDialogVisible = true
  659. },
  660. // 显示用户选择对话框
  661. showUserDialog() {
  662. this.userDialogVisible = true
  663. this.getUserList()
  664. },
  665. // 获取用户列表
  666. async getUserList() {
  667. try {
  668. this.userLoading = true
  669. const params = {
  670. page: this.userPagination.page,
  671. limit: this.userPagination.limit,
  672. keyword: this.userSearchKeyword
  673. }
  674. const response = await request.get('/points/user_point', { params })
  675. this.userList = response.page.data || []
  676. this.userPagination.total = response.page.total || 0
  677. } catch (error) {
  678. console.error('获取用户列表失败:', error)
  679. this.$message.error('获取用户列表失败')
  680. } finally {
  681. this.userLoading = false
  682. }
  683. },
  684. // 搜索用户
  685. handleUserSearch() {
  686. this.userPagination.page = 1
  687. this.getUserList()
  688. },
  689. // 重置用户搜索
  690. resetUserSearch() {
  691. this.userSearchKeyword = ''
  692. this.userPagination.page = 1
  693. this.getUserList()
  694. },
  695. // 选择用户
  696. selectUser(user) {
  697. this.searchForm.user_id = user.id.toString()
  698. this.userDialogVisible = false
  699. this.$message.success(`已选择用户: ${user.nickname} (ID: ${user.id})`)
  700. },
  701. // 用户列表分页处理
  702. handleUserSizeChange(newSize) {
  703. this.userPagination.limit = newSize
  704. this.userPagination.page = 1
  705. this.getUserList()
  706. },
  707. handleUserCurrentChange(newPage) {
  708. this.userPagination.page = newPage
  709. this.getUserList()
  710. },
  711. }
  712. }
  713. </script>
  714. <style scoped>
  715. .points-management {
  716. padding: 20px;
  717. }
  718. .card-header {
  719. display: flex;
  720. justify-content: space-between;
  721. align-items: center;
  722. font-weight: 600;
  723. font-size: 16px;
  724. }
  725. .header-actions {
  726. display: flex;
  727. gap: 10px;
  728. }
  729. .search-bar {
  730. background: #f5f7fa;
  731. padding: 15px;
  732. border-radius: 4px;
  733. margin-bottom: 20px;
  734. }
  735. .stats-cards .stats-card {
  736. text-align: center;
  737. }
  738. .stats-cards .stats-card .stats-item .stats-label {
  739. color: #909399;
  740. font-size: 14px;
  741. margin-bottom: 8px;
  742. }
  743. .stats-cards .stats-card .stats-item .stats-value {
  744. font-size: 24px;
  745. font-weight: bold;
  746. }
  747. .stats-cards .stats-card .stats-item .stats-value.increase {
  748. color: #67c23a;
  749. }
  750. .stats-cards .stats-card .stats-item .stats-value.decrease {
  751. color: #f56c6c;
  752. }
  753. .point-increase {
  754. color: #67c23a;
  755. font-weight: bold;
  756. }
  757. .point-decrease {
  758. color: #f56c6c;
  759. font-weight: bold;
  760. }
  761. .point-freeze {
  762. color: #e6a23c;
  763. }
  764. .freeze-point {
  765. color: #e6a23c;
  766. font-size: 12px;
  767. margin-left: 5px;
  768. }
  769. .freeze-balance {
  770. color: #e6a23c;
  771. font-size: 12px;
  772. }
  773. :deep(.el-form--inline .el-form-item) {
  774. margin-right: 15px;
  775. margin-bottom: 10px;
  776. }
  777. :deep(.el-table .el-table__cell) {
  778. padding: 8px 0;
  779. }
  780. :deep(.el-dialog .el-form-item) {
  781. margin-bottom: 20px;
  782. }
  783. :deep(.el-dialog .el-form-item__label) {
  784. font-weight: 500;
  785. }
  786. .dialog-footer {
  787. display: flex;
  788. justify-content: flex-end;
  789. gap: 10px;
  790. }
  791. .point-value {
  792. color: #67c23a;
  793. font-weight: 500;
  794. }
  795. :deep(.el-table .el-table__row) {
  796. cursor: pointer;
  797. }
  798. :deep(.el-table .el-table__row:hover) {
  799. background-color: #f5f7fa;
  800. }
  801. :deep(.el-input.is-readonly .el-input__inner) {
  802. cursor: pointer;
  803. }
  804. </style>