474 lines
13 KiB
Vue
474 lines
13 KiB
Vue
|
|
<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>
|