You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

586 lines
19 KiB

2 months ago
3 months ago
2 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
4 months ago
3 months ago
3 months ago
4 months ago
3 months ago
3 months ago
4 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
  1. <template>
  2. <el-card style="margin-bottom: 0.5vh;width:82.8vw">
  3. <el-col style="margin-bottom: 0.5vh">
  4. <el-text size="large">精网号</el-text>
  5. <el-input v-model="searchForm.jwcode" placeholder="请输入精网号" style="width: 12vw;margin-right:1vw" clearable/>
  6. <el-text size="large">商品名称</el-text>
  7. <el-select v-model="searchForm.goodsName" placeholder="请输入商品名称" style="width: 12vw;margin-right:1vw"
  8. clearable>
  9. <el-option v-for="item in refundGoodsOptions" :key="item" :label="item" :value="item"></el-option>
  10. </el-select>
  11. <el-text size="large">退款方式</el-text>
  12. <el-select v-model="searchForm.refundModel" placeholder="请选择" style="width: 12vw;margin-right:1vw" clearable>
  13. <el-option label="全部退款" value="0"/>
  14. <el-option label="部分退款" value="1"/>
  15. </el-select>
  16. <el-text size="large">所属地区</el-text>
  17. <el-cascader v-model="selectedMarketPath" :options="market" placeholder="请选择所属地区" clearable
  18. style="width:12vw"
  19. @change="handleMarketChange"/>
  20. </el-col>
  21. <el-col>
  22. <el-text size="large">
  23. {{ activeName === 'wait' ? '提交时间:' : '审核时间:' }}
  24. </el-text>
  25. <el-date-picker v-model="dateRange" type="datetimerange" range-separator="" start-placeholder="开始时间"
  26. end-placeholder="结束时间" style="margin-right:1vw;width:25vw"
  27. @change="handleDatePickerChange" :default-time="defaultTime" :disabled-date="disabledDate"/>
  28. <el-button @click="getToday()" :type="activeTimeRange === 'today' ? 'primary' : ''"></el-button>
  29. <el-button @click="getYesterday()" :type="activeTimeRange === 'yesterday' ? 'primary' : ''"></el-button>
  30. <el-button @click="get7Days()" :type="activeTimeRange === '7days' ? 'primary' : ''">近7天</el-button>
  31. <el-button type="success" @click="resetSearch">重置</el-button>
  32. <el-button type="primary" @click="handleSearch">查询</el-button>
  33. </el-col>
  34. </el-card>
  35. <el-card>
  36. <el-tabs v-model="activeName" type="card" @tab-click="handleClick">
  37. <el-tab-pane label="待审核" name="wait"></el-tab-pane>
  38. <el-tab-pane label="已通过" name="pass"></el-tab-pane>
  39. <el-tab-pane label="已驳回" name="reject"></el-tab-pane>
  40. </el-tabs>
  41. <div>
  42. 退款总金币数{{ format3((stats.permanentGolds + stats.freeGolds + stats.taskGolds).toFixed(2)) }}金币&nbsp;&nbsp;&nbsp;&nbsp;
  43. 永久金币{{ format3(stats.permanentGolds.toFixed(2)) }}金币&nbsp;&nbsp;&nbsp;&nbsp;
  44. 免费金币{{ format3(stats.freeGolds.toFixed(2)) }}金币&nbsp;&nbsp;&nbsp;&nbsp;
  45. 任务金币{{ format3(stats.taskGolds.toFixed(2)) }}金币
  46. </div>
  47. <el-table :data="tableData" style="height:55vh" @sort-change="handleSortChange">
  48. <el-table-column type="index" label="序号" width="60"/>
  49. <el-table-column prop="name" label="姓名" width="120" show-overflow-tooltip/>
  50. <el-table-column prop="jwcode" label="精网号" width="120"/>
  51. <el-table-column prop="market" label="所属地区" width="120"/>
  52. <el-table-column prop="orderCode" label="订单号" width="260px" show-overflow-tooltip/>
  53. <el-table-column prop="refundType" label="退款类型" width="120"/>
  54. <el-table-column prop="refundModel" label="退款方式" width="120">
  55. <template #default="{ row }">
  56. {{ row.refundModel === 0 ? '全部退款' : '部分退款' }}
  57. </template>
  58. </el-table-column>
  59. <el-table-column prop="goodsName" label="退款商品" width="120" show-overflow-tooltip/>
  60. <el-table-column prop="sumGold" label="退款金币总数" width="160" sortable="custom">
  61. <template #default="{ row }">
  62. {{ row.sumGold / 100 }}
  63. </template>
  64. </el-table-column>
  65. <el-table-column prop="permanentGold" label="永久金币" width="120" sortable="custom">
  66. <template #default="{ row }">
  67. {{ row.permanentGold / 100 }}
  68. </template>
  69. </el-table-column>
  70. <el-table-column prop="freeGold" label="免费金币" width="120" sortable="custom">
  71. <template #default="{ row }">
  72. {{ (row.freeJune + row.freeDecember) / 100 }}
  73. </template>
  74. </el-table-column>
  75. <el-table-column prop="taskGold" label="任务金币" width="120" sortable="custom">
  76. <template #default="{ row }">
  77. {{ row.taskGold / 100 }}
  78. </template>
  79. </el-table-column>
  80. <el-table-column prop="remark" label="备注" width="150" show-overflow-tooltip/>
  81. <el-table-column prop="adminName" label="提交人" width="120"/>
  82. <el-table-column v-if="checkTab === 'reject'" prop="rejectReason" label="驳回理由" width="150"
  83. show-overflow-tooltip/>
  84. <el-table-column v-if="checkTab !== 'pending'" prop="auditName" label="审核人" width="120"/>
  85. <el-table-column prop="createTime" label="提交时间" width="180" sortable="custom">
  86. <template #default="{ row }">
  87. {{
  88. checkTab === 'pending'
  89. ? moment(row.auditTime).format('YYYY-MM-DD HH:mm:ss')
  90. : moment(row.createTime).format('YYYY-MM-DD HH:mm:ss')
  91. }}
  92. </template>
  93. </el-table-column>
  94. <el-table-column v-if="checkTab !== 'pending'" prop="auditTime" label="审核时间" width="180">
  95. <template #default="{ row }">
  96. {{ row.auditTime ? moment(row.auditTime).format('YYYY-MM-DD HH:mm:ss') : '--' }}
  97. </template>
  98. </el-table-column>
  99. <el-table-column v-if="checkTab === 'pending'" fixed="right" prop="operation" label="操作" width="150px">
  100. <template #default="scope">
  101. <div class="operation">
  102. <el-popconfirm title="确定要通过此条记录吗?" @confirm="handleApprove(scope.row)">
  103. <template #reference>
  104. <el-button :disabled="clicked || cancelClicked" type="primary" text>
  105. 通过
  106. </el-button>
  107. </template>
  108. <template #actions="{ confirm, cancel }">
  109. <el-button size="small" @click="cancel">取消</el-button>
  110. <el-button type="primary" size="small" :disabled="clicked" @click="confirm">
  111. 确认
  112. </el-button>
  113. </template>
  114. </el-popconfirm>
  115. <el-button :disabled="clicked || cancelClicked" type="primary" text @click="showRejectDialog(scope.row)">
  116. 驳回
  117. 驳回
  118. </el-button>
  119. </div>
  120. </template>
  121. </el-table-column>
  122. </el-table>
  123. <el-pagination class="pagination" v-model:current-page="pagination.pageNum" v-model:page-size="pagination.pageSize"
  124. layout="total, sizes, prev, pager, next, jumper" :total="pagination.total"
  125. @size-change="handlePageSizeChange"
  126. @current-change="handleCurrentChange"></el-pagination>
  127. </el-card>
  128. <el-dialog v-model="rejectDialogVisible" title="驳回理由" width="500px">
  129. <el-form>
  130. <el-form-item label="驳回理由" required>
  131. <el-input v-model="rejectReason" type="textarea" :rows="4" placeholder="请输入驳回理由" maxlength="200"
  132. show-word-limit/>
  133. </el-form-item>
  134. </el-form>
  135. <template #footer>
  136. <span class="dialog-footer">
  137. <el-button @click="rejectDialogVisible = false">取消</el-button>
  138. <el-button type="primary" @click="handleReject">确定</el-button>
  139. </span>
  140. </template>
  141. </el-dialog>
  142. </template>
  143. <script setup>
  144. import {onMounted, reactive, ref} from 'vue'
  145. import {ElMessage} from 'element-plus'
  146. import API from '@/util/http.js'
  147. import moment from 'moment'
  148. import {useAdminStore} from "@/store/index.js";
  149. import {storeToRefs} from "pinia";
  150. import {findMenuById, permissionMapping} from "@/utils/menuTreePermission.js"
  151. import dayjs from "dayjs";
  152. const adminStore = useAdminStore();
  153. const {adminData, menuTree} = storeToRefs(adminStore);
  154. const defaultTime = [
  155. new Date(2000, 1, 1, 0, 0, 0),
  156. new Date(2000, 2, 1, 23, 59, 59),
  157. ]
  158. // 当前激活的时间按钮
  159. const activeTimeRange = ref('')
  160. const scopeValue = ref(null) // 当前行
  161. const rejectDialogVisible = ref(false)
  162. const rejectReason = ref('')
  163. // 状态常量
  164. const STATUS = {
  165. PENDING: 0, // 待审核
  166. APPROVED: 1, // 通过
  167. REJECTED: 2 // 驳回
  168. }
  169. //无法选择的时间
  170. const disabledDate = (time) => {
  171. const limitDate = new Date(2025, 0, 1);
  172. return time.getTime() < limitDate.getTime();
  173. }
  174. // 搜索表单数据
  175. const searchForm = ref({
  176. jwcode: '',
  177. refundModel: '',
  178. goodsName: '',
  179. market: "",
  180. startTime: '',
  181. endTime: '',
  182. auditStatus: '0'
  183. })
  184. const checkTab = ref('pending') // 能否不用STATUS常量,0是待审批,1是已通过,2是驳回,参数status需要Integer
  185. const dateRange = ref([])
  186. const pagination = ref({
  187. pageNum: 1,
  188. pageSize: 50,
  189. total: 0
  190. })
  191. const tableData = ref([])
  192. const marketOptions = ref([])
  193. const refundGoodsOptions = ref([])
  194. const adminInfo = ref({})
  195. // 统计合计数
  196. const stats = ref({
  197. totalNum: 0,
  198. totalCoins: 0,
  199. permanentGolds: 0,
  200. freeGolds: 0,
  201. taskGolds: 0
  202. })
  203. const rejectVisible = ref(false)
  204. const rejectObj = ref({})
  205. const passObj = ref({})
  206. // 标签页默认是待审批
  207. const activeName = ref('wait')
  208. const sortField = ref('')
  209. const sortOrder = ref('')
  210. const market = ref("")
  211. // 处理排序事件
  212. const handleSortChange = (column) => {
  213. if (column.prop === 'sumGold') {
  214. sortField.value = 'sum_gold'
  215. } else if (column.prop === 'permanentGold') {
  216. sortField.value = 'permanent_gold'
  217. } else if (column.prop === 'freeGold') {
  218. sortField.value = 'freeGold'
  219. } else if (column.prop === 'taskGold') {
  220. sortField.value = 'task_gold'
  221. } else if (column.prop === 'createTime') {
  222. sortField.value = 'create_time'
  223. } else if (column.prop === 'auditTime') {
  224. sortField.value = 'audit_time'
  225. } else {
  226. sortField.value = ''
  227. }
  228. sortOrder.value = column.order === 'ascending' ? 'asc' : 'desc'
  229. console.log('排序字段:', sortField.value)
  230. console.log('排序方式:', sortOrder.value)
  231. get()
  232. }
  233. // 显示驳回对话框
  234. const showRejectDialog = (row) => {
  235. scopeValue.value = row
  236. rejectReason.value = ''
  237. rejectDialogVisible.value = true
  238. }
  239. // 查商品名
  240. const getRefundGoods = async () => {
  241. try {
  242. const res = await API({url: '/general/goods'})
  243. refundGoodsOptions.value = res.data || []
  244. } catch (error) {
  245. console.error('获取商品列表失败', error)
  246. }
  247. }
  248. // 搜索方法
  249. const get = async function (val) {
  250. try {
  251. if (typeof val === 'number') {
  252. pagination.value.pageNum = val
  253. }
  254. if (dateRange.value && dateRange.value.length === 2) {
  255. searchForm.value.startTime = moment(dateRange.value[0]).format('YYYY-MM-DD HH:mm:ss')
  256. searchForm.value.endTime = moment(dateRange.value[1]).format('YYYY-MM-DD HH:mm:ss')
  257. } else {
  258. searchForm.value.startTime = ''
  259. searchForm.value.endTime = ''
  260. }
  261. // if (searchForm.value.market === '总部' || searchForm.value.market === '研发部') {
  262. // searchForm.value.market = '';
  263. // }
  264. const params = {
  265. pageNum: pagination.value.pageNum,
  266. pageSize: pagination.value.pageSize,
  267. refundAudit: {
  268. ...searchForm.value,
  269. sortField: sortField.value,
  270. sortOrder: sortOrder.value
  271. }
  272. }
  273. console.log('看看传给后端的参数:', params)
  274. // 校验精网号(数字格式)
  275. if (searchForm.value.jwcode) {
  276. // 纯数字
  277. const numberRegex = /^\d{1,9}$/;
  278. // 检查是否不是数字
  279. if (!numberRegex.test(searchForm.value.jwcode)) {
  280. ElMessage.error('请检查精网号格式')
  281. // 上面提示过了
  282. return
  283. }
  284. }
  285. const res = await API({url: '/audit/selectRefund', data: params})
  286. tableData.value = res.list || []
  287. pagination.value.total = res.total || 0
  288. console.log('查全部的total', pagination.value.total, res.total)
  289. } catch (error) {
  290. console.error('获取数据失败', error)
  291. }
  292. }
  293. const clicked = ref(false);
  294. // 通过
  295. const handleApprove = async (row) => {
  296. clicked.value = true
  297. if (findMenuById(menuTree.value, permissionMapping.Refund_Approval)) {
  298. try {
  299. const params = {
  300. orderCode: row.orderCode,
  301. auditId: adminData.value.id,
  302. action: 1,// action的1是通过,2是驳回
  303. rejectReason: ''
  304. }
  305. await API({url: '/audit/audit', data: params})
  306. ElMessage.success('审核通过成功')
  307. await get()
  308. clicked.value = false
  309. await getStats()
  310. } catch (error) {
  311. console.error('审核通过失败', error)
  312. ElMessage.error('操作失败')
  313. }
  314. } else {
  315. ElMessage.warning('没有权限')
  316. clicked.value = false
  317. }
  318. }
  319. //控制驳回确认按钮禁用状态
  320. const cancelClicked = ref(false)
  321. // 处理驳回操作
  322. const handleReject = async () => {
  323. cancelClicked.value = true
  324. if (findMenuById(menuTree.value, permissionMapping.Refund_Approval)) {
  325. if (!rejectReason.value.trim()) {
  326. ElMessage.warning('请输入驳回理由')
  327. return
  328. }
  329. try {
  330. const params = {
  331. orderCode: scopeValue.value.orderCode,
  332. auditId: adminData.value.id,
  333. action: 2,
  334. rejectReason: rejectReason.value
  335. }
  336. await API({url: '/audit/audit', data: params})
  337. ElMessage.success('驳回成功')
  338. rejectDialogVisible.value = false
  339. await get()
  340. cancelClicked.value = false
  341. await getStats()
  342. console.log('看看驳回参数', params)
  343. } catch (error) {
  344. console.error('驳回失败', error)
  345. ElMessage.error('操作失败')
  346. }
  347. } else {
  348. ElMessage.warning('没有权限')
  349. cancelClicked.value = false
  350. }
  351. }
  352. const getStats = async () => {
  353. try {
  354. const params = {
  355. pageNum: pagination.value.pageNum,
  356. pageSize: pagination.value.pageSize,
  357. refundAudit: {
  358. ...searchForm.value
  359. }
  360. }
  361. if (searchForm.value.jwcode) {
  362. // 纯数字
  363. const numberRegex = /^\d{1,9}$/;
  364. // 检查是否不是数字
  365. if (!numberRegex.test(searchForm.value.jwcode)) {
  366. // ElMessage.error('精网号必须为数字格式')
  367. // 上面提示过了
  368. return
  369. }
  370. }
  371. const res = await API({
  372. url: '/audit/sumRefundGold',
  373. data: params
  374. })
  375. stats.value.totalNum = res.totalNum
  376. stats.value.permanentGolds = res.permanentGolds / 100
  377. stats.value.freeGolds = res.freeGolds / 100
  378. stats.value.taskGolds = res.taskGolds / 100
  379. console.log('see see stats和搜索对象', stats.value, params)
  380. } catch (error) {
  381. console.log('请求失败', error)
  382. }
  383. }
  384. // 搜索
  385. const handleSearch = function () {
  386. trimJwCode()
  387. get()
  388. getStats()
  389. }
  390. // 重置
  391. const resetSearch = function () {
  392. const auditStatus = searchForm.value.auditStatus;
  393. searchForm.value = {
  394. jwcode: '',
  395. refundType: '',
  396. goodsName: '',
  397. market: "",
  398. startTime: '',
  399. endTime: '',
  400. sortField: '',
  401. sortOrder: '',
  402. auditStatus: auditStatus
  403. }
  404. dateRange.value = []
  405. activeTimeRange.value = '' // 清除激活状态
  406. selectedMarketPath.value = []
  407. get()
  408. getStats()
  409. }
  410. // 今天
  411. const getToday = function () {
  412. const today = dayjs()
  413. const startTime = today.startOf('day').format('YYYY-MM-DD HH:mm:ss')
  414. const endTime = today.endOf('day').format('YYYY-MM-DD HH:mm:ss')
  415. dateRange.value = [startTime, endTime]
  416. console.log('dateRange', dateRange.value)
  417. activeTimeRange.value = 'today'
  418. get()
  419. getStats()
  420. }
  421. // 昨天
  422. const getYesterday = function () {
  423. const today = dayjs()
  424. const startTime = today.subtract(1, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss')
  425. const endTime = today.subtract(1, 'day').endOf('day').format('YYYY-MM-DD HH:mm:ss')
  426. dateRange.value = [startTime, endTime]
  427. console.log('dateRange', dateRange.value)
  428. activeTimeRange.value = 'yesterday'
  429. get()
  430. getStats()
  431. }
  432. // 近7天
  433. const get7Days = function () {
  434. const today = dayjs()
  435. const startTime = today.subtract(6, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss')
  436. const endTime = today.endOf('day').format('YYYY-MM-DD HH:mm:ss')
  437. dateRange.value = [startTime, endTime]
  438. console.log('dateRange', dateRange.value)
  439. activeTimeRange.value = '7days'
  440. get()
  441. getStats()
  442. }
  443. const handleClick = function (tab, event) {
  444. activeName.value = tab.props.name
  445. if (tab.props.name === 'wait') {
  446. adminWait()
  447. } else if (tab.props.name === 'pass') {
  448. adminPass()
  449. } else if (tab.props.name === 'reject') {
  450. adminReject()
  451. }
  452. }
  453. // 当前状态
  454. const getCurrentStatus = () => {
  455. switch (activeName.value) {
  456. case 'wait':
  457. return STATUS.PENDING
  458. case 'pass':
  459. return STATUS.APPROVED
  460. case 'reject':
  461. return STATUS.REJECTED
  462. default:
  463. return ''
  464. }
  465. }
  466. // 待审核
  467. const adminWait = async function () {
  468. checkTab.value = 'pending'
  469. searchForm.value.auditStatus = STATUS.PENDING
  470. await get()
  471. await getStats()
  472. }
  473. // 已通过
  474. const adminPass = async function () {
  475. checkTab.value = 'pass'
  476. searchForm.value.auditStatus = STATUS.APPROVED
  477. await get()
  478. await getStats()
  479. }
  480. // 已驳回
  481. const adminReject = async function () {
  482. checkTab.value = 'reject'
  483. searchForm.value.auditStatus = STATUS.REJECTED
  484. await get()
  485. await getStats()
  486. }
  487. const selectedMarketPath = ref("")
  488. const handleMarketChange = (value) => {
  489. if (value && value.length > 0) {
  490. searchForm.value.market = value[value.length - 1]
  491. } else {
  492. searchForm.value.market = ''
  493. }
  494. }
  495. // 获取地区,修改为级联下拉框
  496. const getMarket = async function () {
  497. try {
  498. const result = await API({
  499. url: '/market/selectMarket',
  500. })
  501. console.log('请求成功', result)
  502. // 递归转换树形结构为级联选择器需要的格式(跳过第一级节点)
  503. const transformTree = (nodes) => {
  504. // 直接处理第一级节点的子节点
  505. const allChildren = nodes.flatMap(node => node.children || []);
  506. return allChildren.map(child => {
  507. const grandchildren = child.children && child.children.length
  508. ? transformTree([child]) // 递归处理子节点
  509. : null;
  510. return {
  511. value: child.name,
  512. label: child.name,
  513. children: grandchildren
  514. };
  515. });
  516. };
  517. market.value = transformTree(result.data)
  518. console.log('转换后的地区树==============', market.value)
  519. } catch (error) {
  520. console.log('请求失败', error)
  521. }
  522. }
  523. const trimJwCode = () => {
  524. if (searchForm.value.jwcode) {
  525. searchForm.value.jwcode = searchForm.value.jwcode.replace(/\s/g, '');
  526. }
  527. }
  528. // 日期选择器变化时清除按钮激活状态
  529. const handleDatePickerChange = () => {
  530. activeTimeRange.value = ''
  531. }
  532. const format3 = (num) => {
  533. // 每三位添加逗号
  534. return num.toLocaleString('en-US')
  535. }
  536. // 表单验证
  537. const rules = reactive({
  538. reason: [{required: true, message: '请输入驳回理由', trigger: 'blur'}]
  539. })
  540. const handlePageSizeChange = function (val) {
  541. pagination.value.pageSize = val
  542. get()
  543. }
  544. const handleCurrentChange = function (val) {
  545. pagination.value.pageNum = val
  546. get()
  547. }
  548. onMounted(async () => {
  549. getRefundGoods()
  550. await getMarket()
  551. await get()
  552. await getStats()
  553. })
  554. </script>
  555. <style scoped>
  556. .pagination {
  557. display: flex;
  558. margin-top: 0.5vh;
  559. }
  560. .operation {
  561. display: flex;
  562. }
  563. </style>