PointsLog.vue 32 KB

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