Files
GEAR-OA/gear-ui3/src/views/oms/dispute/index.vue
2026-06-17 19:31:10 +08:00

474 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container dispute-page">
<div class="dispute-topbar">
<el-tabs v-model="activeTab" class="dispute-topbar__tabs" @tab-click="handleTabChange">
<el-tab-pane v-for="item in stepOptions" :key="item.tab" :name="item.tab">
<template #label>
<span>{{ item.label }}</span>
</template>
</el-tab-pane>
</el-tabs>
<div class="dispute-topbar__right">
<el-radio-group v-model="activeStatus" size="small" @change="handleStatusChange">
<el-radio-button label="todo">待办</el-radio-button>
<el-radio-button label="done">已处理</el-radio-button>
<el-radio-button label="all">全部</el-radio-button>
</el-radio-group>
<el-input
v-model="keyword"
clearable
placeholder="订单号 / 客户"
@keyup.enter="handleQuery"
@clear="handleQuery"
>
<template #append>
<el-button icon="Search" @click="handleQuery" />
</template>
</el-input>
<el-button plain icon="Refresh" :loading="loading" @click="getList">刷新</el-button>
</div>
</div>
<el-row :gutter="16">
<el-col :xs="24" :sm="8" :md="7" :lg="6" :xl="5" class="dispute-left">
<div class="left-list" v-loading="loading">
<div class="left-header">
<div class="left-header__title">{{ leftTitle }}</div>
<el-button size="small" plain icon="Refresh" :loading="loading" @click="getList">刷新</el-button>
</div>
<el-empty v-if="!displayList.length && !loading" :description="emptyDescription" />
<el-scrollbar v-else class="left-scroll">
<div
v-for="item in displayList"
:key="item.taskId || item.orderId"
class="dispute-card"
:class="{ active: item.orderId === selectedOrderId }"
@click="handleSelectRow(item)"
>
<div class="dispute-card__row">
<div class="dispute-card__title">{{ item.orderCode || '-' }}</div>
<div class="dispute-card__tags">
<el-tag v-if="item.status" size="small" :type="item.status === 'DONE' ? 'success' : 'warning'" effect="plain">
{{ item.status === 'DONE' ? '已处理' : '待办' }}
</el-tag>
<el-tag size="small" type="info" effect="plain">{{ item.taskName || '-' }}</el-tag>
</div>
</div>
<div class="dispute-card__meta">
<div class="dispute-card__meta-item">客户{{ item.customerName || '-' }}</div>
<div class="dispute-card__meta-item">
<span>创建{{ formatTime(item.createTime) }}</span>
<span v-if="item.endTime" style="margin-left: 10px;">完成{{ formatTime(item.endTime) }}</span>
</div>
</div>
</div>
</el-scrollbar>
</div>
</el-col>
<el-col :xs="24" :sm="16" :md="17" :lg="18" :xl="19" class="dispute-right">
<el-card shadow="never" class="right-card">
<template #header>
<div class="right-header">
<div>
<div class="right-title">{{ rightTitle }}</div>
<div class="right-subtitle">
<span v-if="currentRow && currentRow.orderCode">当前{{ currentRow.orderCode }}</span>
<span v-else>请选择左侧异议单据</span>
</div>
</div>
<div class="right-actions">
<el-button plain icon="Refresh" size="small" :loading="loading" @click="getList">刷新</el-button>
</div>
</div>
</template>
<div class="right-body">
<el-empty v-if="!currentRow || !currentRow.orderId" description="← 请选择左侧异议列表" />
<DisputeFlow
v-else
:order-id="currentRow.orderId"
:order-code="currentRow.orderCode"
:customer-id="currentRow.customerId"
:customer-name="currentRow.customerName"
/>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { listOrderDisputeTaskList } from '@/api/oa/orderDisputeFlow'
import DisputeFlow from '@/views/oms/order/panels/disputeFlow.vue'
const stepOptions = [
{ tab: 'all', stepKey: undefined, label: '全部' },
{ tab: 'acceptTask', stepKey: 'acceptTask', label: '异议接收' },
{ tab: 'physicalTask', stepKey: 'physicalTask', label: '实物确认' },
{ tab: 'analysisTask', stepKey: 'analysisTask', label: '原因分析' },
{ tab: 'planTask', stepKey: 'planTask', label: '处置方案' },
{ tab: 'visitTask', stepKey: 'visitTask', label: '闭环回访' }
]
const activeTab = ref('all')
const activeStatus = ref('todo')
const loading = ref(false)
const list = ref([])
const currentRow = ref(null)
const keyword = ref('')
const selectedOrderIdByTab = ref({})
const silentRefreshing = ref(false)
let fastRefreshTimers = []
let autoRefreshTimer = null
const inFlight = ref(false)
let pendingRefresh = false
let lastFetchKey = ''
const stepMeta = computed(() => stepOptions.find(item => item.tab === activeTab.value) || stepOptions[0])
const activeLabel = computed(() => stepMeta.value.label || '全部')
const selectedOrderId = computed(() => currentRow.value?.orderId)
const statusLabel = computed(() => {
if (activeStatus.value === 'done') return '已处理'
if (activeStatus.value === 'all') return '全部'
return '待办'
})
const displayList = computed(() => {
const kw = (keyword.value || '').trim().toLowerCase()
if (!kw) return list.value
return list.value.filter(item => {
const orderCode = (item.orderCode || '').toLowerCase()
const customerName = (item.customerName || '').toLowerCase()
return orderCode.includes(kw) || customerName.includes(kw)
})
})
const emptyDescription = computed(() => {
const prefix = activeLabel.value === '全部' ? '' : activeLabel.value + ' '
return `暂无${prefix}订单异议${statusLabel.value}`.trim()
})
const leftTitle = computed(() => {
const prefix = activeLabel.value === '全部' ? '订单异议' : activeLabel.value
return `${prefix}${statusLabel.value}`
})
const rightTitle = computed(() => {
const prefix = activeLabel.value === '全部' ? '订单异议' : `订单异议 - ${activeLabel.value}`
return `${prefix}${statusLabel.value}`
})
function formatTime(val) {
if (!val) return '-'
try {
return new Date(val).toLocaleString()
} catch (e) {
return String(val)
}
}
function handleQuery() {
const nextList = displayList.value
if (!nextList.length) {
currentRow.value = null
return
}
const selectedId = currentRow.value?.orderId
if (selectedId && nextList.some(item => item.orderId === selectedId)) return
currentRow.value = nextList[0]
selectedOrderIdByTab.value = { ...selectedOrderIdByTab.value, [selectionKey()]: currentRow.value.orderId }
}
function fetchKey() {
return `${activeTab.value}|${activeStatus.value}`
}
async function getList(options) {
lastFetchKey = fetchKey()
if (inFlight.value) {
pendingRefresh = true
return
}
const silent = options && options.silent
if (silent) {
if (silentRefreshing.value) return
silentRefreshing.value = true
} else {
loading.value = true
}
inFlight.value = true
const localFetchKey = lastFetchKey
try {
const res = await listOrderDisputeTaskList(stepMeta.value.stepKey, activeStatus.value, 200)
if (localFetchKey !== fetchKey()) {
return
}
list.value = res?.data || []
restoreSelection()
} finally {
inFlight.value = false
if (silent) {
silentRefreshing.value = false
} else {
loading.value = false
}
if (pendingRefresh) {
pendingRefresh = false
getList({ silent: true })
}
}
}
function handleSelectRow(row) {
currentRow.value = row
selectedOrderIdByTab.value = { ...selectedOrderIdByTab.value, [selectionKey()]: row.orderId }
}
function selectionKey() {
return `${activeTab.value}:${activeStatus.value}`
}
function restoreSelection() {
const savedOrderId = selectedOrderIdByTab.value?.[selectionKey()]
if (!list.value.length) {
currentRow.value = null
return
}
if (savedOrderId) {
const exist = list.value.find(item => item.orderId === savedOrderId)
if (exist) {
currentRow.value = exist
return
}
}
const nextList = displayList.value
currentRow.value = nextList[0] || list.value[0]
if (currentRow.value?.orderId) {
selectedOrderIdByTab.value = { ...selectedOrderIdByTab.value, [selectionKey()]: currentRow.value.orderId }
}
}
function handleTabChange() {
keyword.value = ''
getList()
}
function handleStatusChange() {
keyword.value = ''
getList()
startAutoRefresh()
}
function handleTaskChanged() {
scheduleFastRefresh()
}
onMounted(() => {
if (typeof window !== 'undefined') {
window.addEventListener('order-dispute-task-changed', handleTaskChanged)
}
if (typeof document !== 'undefined') {
document.addEventListener('visibilitychange', handleVisibilityChange)
}
getList()
startAutoRefresh()
})
onActivated(() => {
getList({ silent: true })
startAutoRefresh()
})
onBeforeUnmount(() => {
if (typeof window !== 'undefined') {
window.removeEventListener('order-dispute-task-changed', handleTaskChanged)
}
if (typeof document !== 'undefined') {
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
stopAutoRefresh()
})
watch(keyword, () => {
handleQuery()
})
function clearFastRefresh() {
if (fastRefreshTimers && fastRefreshTimers.length) {
fastRefreshTimers.forEach(id => clearTimeout(id))
}
fastRefreshTimers = []
}
function scheduleFastRefresh() {
clearFastRefresh()
getList({ silent: true })
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return
fastRefreshTimers.push(setTimeout(() => getList({ silent: true }), 400))
fastRefreshTimers.push(setTimeout(() => getList({ silent: true }), 1200))
fastRefreshTimers.push(setTimeout(() => getList({ silent: true }), 2500))
}
function stopAutoRefresh() {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer)
autoRefreshTimer = null
}
}
function startAutoRefresh() {
stopAutoRefresh()
if (activeStatus.value !== 'todo') return
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return
autoRefreshTimer = setInterval(() => {
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return
getList({ silent: true })
}, 3000)
}
function handleVisibilityChange() {
startAutoRefresh()
}
</script>
<style scoped>
.dispute-page {
height: calc(100vh - 110px);
}
.dispute-topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.dispute-topbar__tabs {
flex: 1;
min-width: 360px;
}
.dispute-topbar__tabs :deep(.el-tabs__header) {
margin: 0;
}
.dispute-topbar__right {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.dispute-left,
.dispute-right {
height: calc(100vh - 170px);
}
.left-list {
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
padding: 10px;
background: #fff;
height: 100%;
}
.left-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
.left-header__title {
font-weight: 600;
color: #303133;
}
.left-scroll {
height: 100%;
}
.dispute-card {
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
padding: 10px;
margin-bottom: 10px;
cursor: pointer;
background: #fff;
}
.dispute-card.active {
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
.dispute-card__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.dispute-card__tags {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.dispute-card__title {
font-weight: 600;
color: #303133;
line-height: 20px;
word-break: break-all;
}
.dispute-card__meta {
margin-top: 6px;
display: flex;
flex-direction: column;
gap: 4px;
color: #606266;
font-size: 12px;
}
.right-card {
height: 100%;
}
.right-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.right-title {
font-size: 18px;
font-weight: 700;
color: #303133;
}
.right-subtitle {
margin-top: 4px;
font-size: 12px;
color: #909399;
}
.right-body {
height: calc(100% - 8px);
overflow: auto;
}
</style>