|
@@ -11,15 +11,23 @@
|
|
|
<button class="search-btn" @click="handleSearch">搜索</button>
|
|
<button class="search-btn" @click="handleSearch">搜索</button>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- 状态筛选 -->
|
|
|
|
|
|
|
+ <!-- 状态筛选 + 全选 -->
|
|
|
<view class="filter-bar">
|
|
<view class="filter-bar">
|
|
|
- <view
|
|
|
|
|
- v-for="(option, index) in statusOptions"
|
|
|
|
|
- :key="index"
|
|
|
|
|
- class="filter-item"
|
|
|
|
|
- :class="{ active: activeStatus === option.value }"
|
|
|
|
|
- @click="handleStatusChange(option.value)">
|
|
|
|
|
- <text>{{ option.label }}</text>
|
|
|
|
|
|
|
+ <scroll-view class="filter-scroll" scroll-x="true" :show-scrollbar="false">
|
|
|
|
|
+ <view class="filter-scroll-inner">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(option, index) in statusOptions"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ class="filter-item"
|
|
|
|
|
+ :class="{ active: activeStatus === option.value }"
|
|
|
|
|
+ @click="handleStatusChange(option.value)">
|
|
|
|
|
+ <text>{{ option.label }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </scroll-view>
|
|
|
|
|
+ <view class="filter-divider"></view>
|
|
|
|
|
+ <view class="filter-item select-all-item" :class="{ active: isSelectAll }" @click="toggleSelectAll">
|
|
|
|
|
+ <text>{{ isSelectAll ? '取消全选' : '全选待退款' }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
@@ -28,9 +36,16 @@
|
|
|
<view
|
|
<view
|
|
|
class="refund-item"
|
|
class="refund-item"
|
|
|
v-for="(item, index) in list"
|
|
v-for="(item, index) in list"
|
|
|
- :key="index">
|
|
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ :class="{ selected: selectedIds.has(item.refundLogId) }"
|
|
|
|
|
+ @click="handleItemClick(item)">
|
|
|
<view class="item-header">
|
|
<view class="item-header">
|
|
|
<view class="item-left">
|
|
<view class="item-left">
|
|
|
|
|
+ <view class="checkbox-wrapper" v-if="isRefundableStatus(item.status)" @click.stop="toggleSelect(item)">
|
|
|
|
|
+ <view class="checkbox" :class="{ checked: selectedIds.has(item.refundLogId) }">
|
|
|
|
|
+ <AppIcon v-if="selectedIds.has(item.refundLogId)" name="check" :size="20" color="#FFFFFF" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
<text class="item-phone">{{ item.mobilePhone || '-' }}</text>
|
|
<text class="item-phone">{{ item.mobilePhone || '-' }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
<text class="status-tag" :style="getStatusStyle(item.status)">{{ fmtDictName('RefundLog.status', item.status) }}</text>
|
|
<text class="status-tag" :style="getStatusStyle(item.status)">{{ fmtDictName('RefundLog.status', item.status) }}</text>
|
|
@@ -70,7 +85,7 @@
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="item-footer" v-if="isRefundableStatus(item.status)">
|
|
<view class="item-footer" v-if="isRefundableStatus(item.status)">
|
|
|
- <button class="refund-btn" @click="handleProcessRefund(item)">退款处理</button>
|
|
|
|
|
|
|
+ <button class="refund-btn" @click.stop="handleProcessRefund(item)">退款处理</button>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
@@ -95,13 +110,21 @@
|
|
|
<view class="loading-spinner"></view>
|
|
<view class="loading-spinner"></view>
|
|
|
<text class="loading-text">加载中...</text>
|
|
<text class="loading-text">加载中...</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 批量操作栏 -->
|
|
|
|
|
+ <view class="batch-bar" v-if="selectedIds.size > 0">
|
|
|
|
|
+ <view class="batch-info">
|
|
|
|
|
+ <text class="batch-count">已选 {{ selectedIds.size }} 项</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <button class="batch-btn" @click="handleBatchRefund">批量退款处理</button>
|
|
|
|
|
+ </view>
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import { ref, onMounted, computed } from 'vue'
|
|
|
import { onReachBottom } from '@dcloudio/uni-app'
|
|
import { onReachBottom } from '@dcloudio/uni-app'
|
|
|
-import { getRefundLogs, processRefund } from '../../api/finance.js'
|
|
|
|
|
|
|
+import { getRefundLogs, processRefund, batchProcessRefund } from '../../api/finance.js'
|
|
|
import { formatTime, showToast, formatAmount, fmtDictName, getDictColor } from '../../utils/index.js'
|
|
import { formatTime, showToast, formatAmount, fmtDictName, getDictColor } from '../../utils/index.js'
|
|
|
import dictUtil, { loadDicts } from '../../utils/dict.js'
|
|
import dictUtil, { loadDicts } from '../../utils/dict.js'
|
|
|
|
|
|
|
@@ -113,6 +136,7 @@ const hasMore = ref(true)
|
|
|
const loadMoreStatus = ref('more')
|
|
const loadMoreStatus = ref('more')
|
|
|
const searchKeyword = ref('')
|
|
const searchKeyword = ref('')
|
|
|
const activeStatus = ref('')
|
|
const activeStatus = ref('')
|
|
|
|
|
+const selectedIds = ref(new Set())
|
|
|
|
|
|
|
|
const statusOptions = computed(() => dictUtil.getDictFilterOptions('RefundLog.status'))
|
|
const statusOptions = computed(() => dictUtil.getDictFilterOptions('RefundLog.status'))
|
|
|
|
|
|
|
@@ -126,12 +150,52 @@ const isRefundableStatus = (status) => {
|
|
|
return status == dictUtil.getDictValue('RefundLog.status', '待退款')
|
|
return status == dictUtil.getDictValue('RefundLog.status', '待退款')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 当前页所有可退款的ID列表
|
|
|
|
|
+const refundableIds = computed(() => {
|
|
|
|
|
+ return list.value
|
|
|
|
|
+ .filter(item => isRefundableStatus(item.status))
|
|
|
|
|
+ .map(item => item.refundLogId)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const isSelectAll = computed(() => {
|
|
|
|
|
+ const ids = refundableIds.value
|
|
|
|
|
+ return ids.length > 0 && ids.every(id => selectedIds.value.has(id))
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const toggleSelect = (item) => {
|
|
|
|
|
+ const id = item.refundLogId
|
|
|
|
|
+ const next = new Set(selectedIds.value)
|
|
|
|
|
+ if (next.has(id)) {
|
|
|
|
|
+ next.delete(id)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ next.add(id)
|
|
|
|
|
+ }
|
|
|
|
|
+ selectedIds.value = next
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const toggleSelectAll = () => {
|
|
|
|
|
+ if (isSelectAll.value) {
|
|
|
|
|
+ selectedIds.value = new Set()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ selectedIds.value = new Set(refundableIds.value)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleItemClick = (item) => {
|
|
|
|
|
+ if (item.outRefundNo) {
|
|
|
|
|
+ uni.navigateTo({
|
|
|
|
|
+ url: `/pages/finance/refund-detail?outRefundNo=${item.outRefundNo}`
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const loadData = async (isLoadMore = false) => {
|
|
const loadData = async (isLoadMore = false) => {
|
|
|
if (!isLoadMore) {
|
|
if (!isLoadMore) {
|
|
|
page.value = 1
|
|
page.value = 1
|
|
|
list.value = []
|
|
list.value = []
|
|
|
hasMore.value = true
|
|
hasMore.value = true
|
|
|
loadMoreStatus.value = 'more'
|
|
loadMoreStatus.value = 'more'
|
|
|
|
|
+ selectedIds.value = new Set()
|
|
|
} else {
|
|
} else {
|
|
|
loadMoreStatus.value = 'loading'
|
|
loadMoreStatus.value = 'loading'
|
|
|
}
|
|
}
|
|
@@ -211,6 +275,37 @@ const handleProcessRefund = (item) => {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const handleBatchRefund = () => {
|
|
|
|
|
+ const ids = [...selectedIds.value]
|
|
|
|
|
+ if (ids.length === 0) {
|
|
|
|
|
+ showToast('请选择退款记录')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '批量退款确认',
|
|
|
|
|
+ content: `确定对已选的 ${ids.length} 笔退款进行批量处理吗?`,
|
|
|
|
|
+ success: async (res) => {
|
|
|
|
|
+ if (res.confirm) {
|
|
|
|
|
+ uni.showLoading({ title: '批量退款处理中...', mask: true })
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await batchProcessRefund(ids)
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ if (result && result.code === 200) {
|
|
|
|
|
+ showToast(`批量退款已提交,共${ids.length}笔`, 'success')
|
|
|
|
|
+ selectedIds.value = new Set()
|
|
|
|
|
+ loadData()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ showToast(result?.msg || '批量退款处理失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ showToast('批量退款处理失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
|
await loadDicts()
|
|
await loadDicts()
|
|
|
loadData()
|
|
loadData()
|
|
@@ -221,7 +316,7 @@ onMounted(async () => {
|
|
|
.refund-container {
|
|
.refund-container {
|
|
|
min-height: 100vh;
|
|
min-height: 100vh;
|
|
|
background-color: #F5F7FA;
|
|
background-color: #F5F7FA;
|
|
|
- padding-bottom: 60rpx;
|
|
|
|
|
|
|
+ padding-bottom: 140rpx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.search-bar {
|
|
.search-bar {
|
|
@@ -263,23 +358,51 @@ onMounted(async () => {
|
|
|
|
|
|
|
|
.filter-bar {
|
|
.filter-bar {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- padding: 16rpx 20rpx;
|
|
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 12rpx 20rpx;
|
|
|
background-color: #FFFFFF;
|
|
background-color: #FFFFFF;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-scroll {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-scroll-inner {
|
|
|
|
|
+ display: flex;
|
|
|
gap: 16rpx;
|
|
gap: 16rpx;
|
|
|
- flex-wrap: wrap;
|
|
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-divider {
|
|
|
|
|
+ width: 1rpx;
|
|
|
|
|
+ height: 32rpx;
|
|
|
|
|
+ background-color: #E5E5E5;
|
|
|
|
|
+ margin: 0 16rpx;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
.filter-item {
|
|
.filter-item {
|
|
|
padding: 8rpx 24rpx;
|
|
padding: 8rpx 24rpx;
|
|
|
background-color: #F5F5F5;
|
|
background-color: #F5F5F5;
|
|
|
border-radius: 100rpx;
|
|
border-radius: 100rpx;
|
|
|
font-size: 24rpx;
|
|
font-size: 24rpx;
|
|
|
color: #666666;
|
|
color: #666666;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
.filter-item.active {
|
|
.filter-item.active {
|
|
|
background: #C6171E;
|
|
background: #C6171E;
|
|
|
color: #FFFFFF;
|
|
color: #FFFFFF;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.select-all-item {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ color: #C6171E;
|
|
|
|
|
+ background-color: #FFF0F0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.refund-list {
|
|
.refund-list {
|
|
|
padding: 20rpx;
|
|
padding: 20rpx;
|
|
|
}
|
|
}
|
|
@@ -290,6 +413,10 @@ onMounted(async () => {
|
|
|
margin-bottom: 16rpx;
|
|
margin-bottom: 16rpx;
|
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
|
|
|
}
|
|
}
|
|
|
|
|
+.refund-item.selected {
|
|
|
|
|
+ background-color: #FFF5F5;
|
|
|
|
|
+ border: 2rpx solid #C6171E;
|
|
|
|
|
+}
|
|
|
.item-header {
|
|
.item-header {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
justify-content: space-between;
|
|
@@ -298,6 +425,31 @@ onMounted(async () => {
|
|
|
border-bottom: 1rpx solid #F0F0F0;
|
|
border-bottom: 1rpx solid #F0F0F0;
|
|
|
margin-bottom: 16rpx;
|
|
margin-bottom: 16rpx;
|
|
|
}
|
|
}
|
|
|
|
|
+.item-left {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.checkbox-wrapper {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ width: 44rpx;
|
|
|
|
|
+ height: 44rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.checkbox {
|
|
|
|
|
+ width: 40rpx;
|
|
|
|
|
+ height: 40rpx;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ border: 2rpx solid #D0D0D0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+.checkbox.checked {
|
|
|
|
|
+ background-color: #C6171E;
|
|
|
|
|
+ border-color: #C6171E;
|
|
|
|
|
+}
|
|
|
.item-phone {
|
|
.item-phone {
|
|
|
font-size: 30rpx;
|
|
font-size: 30rpx;
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
@@ -385,4 +537,42 @@ onMounted(async () => {
|
|
|
color: #999999;
|
|
color: #999999;
|
|
|
margin-top: 16rpx;
|
|
margin-top: 16rpx;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* 批量操作栏 */
|
|
|
|
|
+.batch-bar {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 20rpx 30rpx;
|
|
|
|
|
+ padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
|
|
|
|
+ background-color: #FFFFFF;
|
|
|
|
|
+ box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.08);
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+}
|
|
|
|
|
+.batch-info {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+.batch-count {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: #1A1A1A;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+.batch-btn {
|
|
|
|
|
+ padding: 16rpx 40rpx;
|
|
|
|
|
+ background: #C6171E;
|
|
|
|
|
+ color: #FFFFFF;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+.batch-btn:active {
|
|
|
|
|
+ background: #A81212;
|
|
|
|
|
+ transform: scale(0.97);
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|