CfValueManage.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. <template>
  2. <div class="cf-value admin-page">
  3. <div class="header">
  4. <h2>CF值(平台积分)管理</h2>
  5. <div class="filters">
  6. <el-input v-model="searchUserId" placeholder="用户ID/用户名搜索" style="width: 200px;" clearable @keyup.enter.native="searchBalance" />
  7. <el-button type="primary" @click="searchBalance">查询余额</el-button>
  8. </div>
  9. </div>
  10. <el-tabs v-model="activeTab">
  11. <el-tab-pane label="余额列表" name="balances">
  12. <div class="adm-table-wrap">
  13. <el-table :data="balanceList" v-loading="loadingBalance" border stripe height="100%">
  14. <el-table-column prop="id" label="ID" width="70">
  15. <template slot="header">
  16. <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>
  17. </template>
  18. </el-table-column>
  19. <el-table-column label="用户" width="140">
  20. <template slot-scope="{ row }">{{ row.userName || '用户' + row.userId }}</template>
  21. </el-table-column>
  22. <el-table-column prop="familyId" label="家庭ID" width="100">
  23. <template slot="header">
  24. <span class="sort-header" @click.stop="onSortClick('familyId', $event)">家庭ID<span v-if="sortIndex('familyId') !== null" class="sort-index" :class="sortClass('familyId') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('familyId') }}</span>{{ sortIcon('familyId') }}</span>
  25. </template>
  26. </el-table-column>
  27. <el-table-column prop="totalEarned" label="累计获得" width="100">
  28. <template slot="header">
  29. <span class="sort-header" @click.stop="onSortClick('totalEarned', $event)">累计获得<span v-if="sortIndex('totalEarned') !== null" class="sort-index" :class="sortClass('totalEarned') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('totalEarned') }}</span>{{ sortIcon('totalEarned') }}</span>
  30. </template>
  31. </el-table-column>
  32. <el-table-column prop="available" label="可用" width="80">
  33. <template slot="header">
  34. <span class="sort-header" @click.stop="onSortClick('available', $event)">可用<span v-if="sortIndex('available') !== null" class="sort-index" :class="sortClass('available') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('available') }}</span>{{ sortIcon('available') }}</span>
  35. </template>
  36. </el-table-column>
  37. <el-table-column prop="frozen" label="冻结" width="80">
  38. <template slot="header">
  39. <span class="sort-header" @click.stop="onSortClick('frozen', $event)">冻结<span v-if="sortIndex('frozen') !== null" class="sort-index" :class="sortClass('frozen') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('frozen') }}</span>{{ sortIcon('frozen') }}</span>
  40. </template>
  41. </el-table-column>
  42. <el-table-column prop="withdrawn" label="已提现" width="80">
  43. <template slot="header">
  44. <span class="sort-header" @click.stop="onSortClick('withdrawn', $event)">已提现<span v-if="sortIndex('withdrawn') !== null" class="sort-index" :class="sortClass('withdrawn') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('withdrawn') }}</span>{{ sortIcon('withdrawn') }}</span>
  45. </template>
  46. </el-table-column>
  47. <el-table-column prop="exchanged" label="已兑换" width="80">
  48. <template slot="header">
  49. <span class="sort-header" @click.stop="onSortClick('exchanged', $event)">已兑换<span v-if="sortIndex('exchanged') !== null" class="sort-index" :class="sortClass('exchanged') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('exchanged') }}</span>{{ sortIcon('exchanged') }}</span>
  50. </template>
  51. </el-table-column>
  52. <el-table-column label="操作" width="100">
  53. <template slot-scope="{ row }">
  54. <el-button type="text" size="small" @click="viewLogs(row.userId)">流水</el-button>
  55. </template>
  56. </el-table-column>
  57. </el-table>
  58. </div>
  59. <el-pagination v-if="balanceTotal > 0" :current-page="balancePage" :page-size="balanceSize" :total="balanceTotal" layout="prev, pager, next" @current-change="pageBalances" />
  60. </el-tab-pane>
  61. <el-tab-pane label="流水查询" name="logs">
  62. <div class="filters">
  63. <el-input v-model="logUserId" placeholder="用户ID" style="width: 200px;" clearable @keyup.enter.native="searchLogs" />
  64. <el-button type="primary" @click="searchLogs">查询流水</el-button>
  65. </div>
  66. <div class="adm-table-wrap">
  67. <el-table :data="logList" v-loading="loadingLog" border stripe height="100%">
  68. <el-table-column prop="id" label="ID" width="70">
  69. <template slot="header">
  70. <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>
  71. </template>
  72. </el-table-column>
  73. <el-table-column label="用户" width="120">
  74. <template slot-scope="{ row }">{{ row.userName || '用户' + row.userId }}</template>
  75. </el-table-column>
  76. <el-table-column prop="type" label="类型" width="100">
  77. <template slot-scope="{ row }">
  78. <el-tag :type="typeTag(row.type)" size="small">{{ row.type }}</el-tag>
  79. </template>
  80. </el-table-column>
  81. <el-table-column prop="amount" label="数量" width="80">
  82. <template slot="header">
  83. <span class="sort-header" @click.stop="onSortClick('amount', $event)">数量<span v-if="sortIndex('amount') !== null" class="sort-index" :class="sortClass('amount') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('amount') }}</span>{{ sortIcon('amount') }}</span>
  84. </template>
  85. </el-table-column>
  86. <el-table-column prop="balanceAfter" label="变动后余额" width="100">
  87. <template slot="header">
  88. <span class="sort-header" @click.stop="onSortClick('balanceAfter', $event)">变动后余额<span v-if="sortIndex('balanceAfter') !== null" class="sort-index" :class="sortClass('balanceAfter') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('balanceAfter') }}</span>{{ sortIcon('balanceAfter') }}</span>
  89. </template>
  90. </el-table-column>
  91. <el-table-column prop="refType" label="来源" width="130">
  92. <template slot="header">
  93. <span class="sort-header" @click.stop="onSortClick('refType', $event)">来源<span v-if="sortIndex('refType') !== null" class="sort-index" :class="sortClass('refType') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('refType') }}</span>{{ sortIcon('refType') }}</span>
  94. </template>
  95. </el-table-column>
  96. <el-table-column prop="refId" label="关联ID" width="80">
  97. <template slot="header">
  98. <span class="sort-header" @click.stop="onSortClick('refId', $event)">关联ID<span v-if="sortIndex('refId') !== null" class="sort-index" :class="sortClass('refId') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('refId') }}</span>{{ sortIcon('refId') }}</span>
  99. </template>
  100. </el-table-column>
  101. <el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip>
  102. <template slot="header">
  103. <span class="sort-header" @click.stop="onSortClick('remark', $event)">备注<span v-if="sortIndex('remark') !== null" class="sort-index" :class="sortClass('remark') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('remark') }}</span>{{ sortIcon('remark') }}</span>
  104. </template>
  105. </el-table-column>
  106. <el-table-column prop="createdAt" label="时间" width="160">
  107. <template slot="header">
  108. <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>
  109. </template>
  110. </el-table-column>
  111. </el-table>
  112. </div>
  113. <el-pagination v-if="logTotal > 0" :current-page="logPage" :page-size="logSize" :total="logTotal" layout="prev, pager, next" @current-change="pageLogs" />
  114. </el-tab-pane>
  115. </el-tabs>
  116. </div>
  117. </template>
  118. <script>
  119. import SortMixin from '@/mixins/SortMixin'
  120. import ListStateMixin from '@/mixins/ListStateMixin'
  121. import { getCfValueBalance, getCfValueLogs, getCfValueAllBalances, searchUsers } from '@/api/admin'
  122. export default {
  123. name: 'CfValueManage',
  124. mixins: [SortMixin, ListStateMixin],
  125. data() {
  126. return {
  127. activeTab: 'balances',
  128. searchUserId: '',
  129. logUserId: '',
  130. sortableColumns: { id: 'ID', createdAt: '时间', familyId: '家庭ID', totalEarned: '累计获得', available: '可用', frozen: '冻结', withdrawn: '已提现', exchanged: '已兑换', amount: '数量', balanceAfter: '变动后余额', refType: '来源', refId: '关联ID', remark: '备注' },
  131. // Balances
  132. balanceList: [],
  133. loadingBalance: false,
  134. balancePage: 1,
  135. balanceSize: 20,
  136. balanceTotal: 0,
  137. // Logs
  138. logList: [],
  139. loadingLog: false,
  140. logPage: 1,
  141. logSize: 20,
  142. logTotal: 0
  143. }
  144. },
  145. mounted() {
  146. this.loadBalances()
  147. },
  148. methods: {
  149. resolveUserNames(items, idField) {
  150. if (!items || items.length === 0) return Promise.resolve()
  151. var ids = []
  152. items.forEach(function(item) {
  153. if (item[idField] && ids.indexOf(item[idField]) === -1) ids.push(item[idField])
  154. })
  155. var nameMap = {}
  156. var self = this
  157. return Promise.all(ids.map(function(id) {
  158. return searchUsers({ keyword: String(id) })
  159. .then(function(res) {
  160. if (res.data && res.data.length > 0) {
  161. var u = res.data[0]
  162. nameMap[id] = u.nickname || u.realName || '用户' + id
  163. } else {
  164. nameMap[id] = '用户' + id
  165. }
  166. })
  167. .catch(function() {
  168. nameMap[id] = '用户' + id
  169. })
  170. })).then(function() {
  171. items.forEach(function(item) {
  172. item.userName = nameMap[item[idField]] || '用户' + item[idField]
  173. })
  174. })
  175. },
  176. typeTag(type) {
  177. const map = { earn: 'success', spend: 'warning', freeze: 'info', unfreeze: 'info', withdraw: 'primary' }
  178. return map[type] || 'info'
  179. },
  180. loadList() { this.loadBalances() },
  181. loadData() { this.searchLogs() },
  182. loadBalances() {
  183. this.loadingBalance = true
  184. const params = { page: this.balancePage, size: this.balanceSize }
  185. if (this.sortState.length > 0) params.sort = this.sortState
  186. getCfValueAllBalances(params).then(async res => {
  187. if (res.code === 200) {
  188. this.balanceList = res.data.records || []
  189. this.balanceTotal = res.data.total || 0
  190. await this.resolveUserNames(this.balanceList, 'userId')
  191. }
  192. }).finally(() => { this.loadingBalance = false })
  193. },
  194. pageBalances(page) {
  195. this.balancePage = page
  196. this.loadBalances()
  197. },
  198. searchBalance() {
  199. if (!this.searchUserId) { this.loadBalances(); return }
  200. this.loadingBalance = true
  201. getCfValueBalance({ userId: this.searchUserId }).then(async res => {
  202. if (res.code === 200) {
  203. this.balanceList = [res.data]
  204. this.balanceTotal = 1
  205. await this.resolveUserNames(this.balanceList, 'userId')
  206. }
  207. }).finally(() => { this.loadingBalance = false })
  208. },
  209. viewLogs(userId) {
  210. this.logUserId = String(userId)
  211. this.activeTab = 'logs'
  212. this.searchLogs()
  213. },
  214. searchLogs() {
  215. if (!this.logUserId) return
  216. this.loadingLog = true
  217. this.logPage = 1
  218. const params = { userId: this.logUserId, page: 1, size: this.logSize }
  219. if (this.sortState.length > 0) params.sort = this.sortState
  220. getCfValueLogs(params).then(async res => {
  221. if (res.code === 200) {
  222. this.logList = res.data.records || []
  223. this.logTotal = res.data.total || 0
  224. await this.resolveUserNames(this.logList, 'userId')
  225. }
  226. }).finally(() => { this.loadingLog = false })
  227. },
  228. pageLogs(page) {
  229. this.logPage = page
  230. this.searchLogs()
  231. }
  232. }
  233. }
  234. </script>
  235. <style scoped>
  236. .cf-value { padding: 20px; }
  237. .sort-header {
  238. cursor: pointer;
  239. user-select: none;
  240. font-weight: 500;
  241. font-size: 12px;
  242. }
  243. .sort-header:hover {
  244. color: #409eff;
  245. }
  246. </style>