订单异议开发
This commit is contained in:
40
gear-ui3/src/api/oa/orderDisputeFlow.js
Normal file
40
gear-ui3/src/api/oa/orderDisputeFlow.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getOrderDisputeFlowByOrder(orderId) {
|
||||
return request({
|
||||
url: `/oa/orderDisputeFlow/byOrder/${orderId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function startOrderDisputeFlow(data) {
|
||||
return request({
|
||||
url: '/oa/orderDisputeFlow/start',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function confirmOrderDisputeFlow(data) {
|
||||
return request({
|
||||
url: '/oa/orderDisputeFlow/confirm',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function listOrderDisputeTodo(stepKey) {
|
||||
return request({
|
||||
url: '/oa/orderDisputeFlow/todoList',
|
||||
method: 'get',
|
||||
params: { stepKey }
|
||||
})
|
||||
}
|
||||
|
||||
export function listOrderDisputeTaskList(stepKey, status, limit) {
|
||||
return request({
|
||||
url: '/oa/orderDisputeFlow/taskList',
|
||||
method: 'get',
|
||||
params: { stepKey, status, limit }
|
||||
})
|
||||
}
|
||||
@@ -71,13 +71,18 @@ const loadData = async () => {
|
||||
type: 'user'
|
||||
}))
|
||||
|
||||
// 如果只选择用户,过滤掉部门节点
|
||||
let allNodes = props.userOnly
|
||||
? transformedUserList
|
||||
: [...transformedDeptList, ...transformedUserList]
|
||||
if (props.userOnly) {
|
||||
// 仅选择用户时直接平铺用户节点,避免因父部门节点被过滤导致整棵树为空
|
||||
treeData.value = transformedUserList.map(user => ({
|
||||
...user,
|
||||
parentId: null,
|
||||
children: undefined
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
// 构建树形结构
|
||||
treeData.value = buildTree(allNodes)
|
||||
// 构建部门 + 用户树形结构
|
||||
treeData.value = buildTree([...transformedDeptList, ...transformedUserList])
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
</el-tabs>
|
||||
|
||||
<div class="right-body" v-loading="detailLoading">
|
||||
<el-empty v-if="entityType === 'customer' ? !selectedCustomerId : !selectedSupplierId" :description="entityType === 'customer' ? '请选择左侧客户' : '请选择左侧供货商'" />
|
||||
<el-empty v-if="activeTab !== 'edit' && (entityType === 'customer' ? !selectedCustomerId : !selectedSupplierId)" :description="entityType === 'customer' ? '请选择左侧客户' : '请选择左侧供货商'" />
|
||||
|
||||
<template v-else-if="entityType === 'customer'">
|
||||
<div v-show="activeTab === 'detail'">
|
||||
@@ -305,7 +305,13 @@
|
||||
</el-table>
|
||||
|
||||
<el-dialog :title="disputeDialogTitle" v-model="disputeOpen" width="1100px" append-to-body>
|
||||
<ReturnExchange v-if="disputeOpen && disputeOrderId" :orderId="disputeOrderId" />
|
||||
<DisputeFlow
|
||||
v-if="disputeOpen && disputeOrderId"
|
||||
:order-id="disputeOrderId"
|
||||
:order-code="disputeOrderCode"
|
||||
:customer-id="selectedCustomerId"
|
||||
:customer-name="selectedCustomer && selectedCustomer.name ? selectedCustomer.name : ''"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="disputeOpen = false">关闭</el-button>
|
||||
</template>
|
||||
@@ -385,12 +391,12 @@ import { listShippingOrder } from "@/api/oms/shippingOrder";
|
||||
import { listOrderDetail } from "@/api/oms/orderDetail";
|
||||
import { listReceivable } from "@/api/finance/receivable";
|
||||
import request from "@/utils/request";
|
||||
import ReturnExchange from "@/views/oms/order/panels/return.vue";
|
||||
import DisputeFlow from "@/views/oms/order/panels/disputeFlow.vue";
|
||||
import * as XLSX from "xlsx";
|
||||
|
||||
export default {
|
||||
name: "Customer",
|
||||
components: { ReturnExchange },
|
||||
components: { DisputeFlow },
|
||||
setup() {
|
||||
const { proxy } = getCurrentInstance();
|
||||
const { customer_from } = proxy.useDict("customer_from");
|
||||
|
||||
473
gear-ui3/src/views/oms/dispute/index.vue
Normal file
473
gear-ui3/src/views/oms/dispute/index.vue
Normal file
@@ -0,0 +1,473 @@
|
||||
<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>
|
||||
432
gear-ui3/src/views/oms/order/panels/disputeFlow.vue
Normal file
432
gear-ui3/src/views/oms/order/panels/disputeFlow.vue
Normal file
@@ -0,0 +1,432 @@
|
||||
<template>
|
||||
<div class="dispute-flow">
|
||||
<div class="dispute-flow__hero">
|
||||
<div class="dispute-flow__header">
|
||||
<div class="dispute-flow__title">
|
||||
<div class="dispute-flow__title-main">订单异议流程</div>
|
||||
<div class="dispute-flow__title-sub">
|
||||
<span v-if="orderCode">订单:{{ orderCode }}</span>
|
||||
<span v-if="flowInfo && flowInfo.procInsId" style="margin-left: 10px;">流程:{{ flowInfo.procInsId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dispute-flow__actions">
|
||||
<el-button size="small" plain icon="Refresh" :loading="loading" @click="loadFlow">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dispute-flow__summary">
|
||||
<div class="dispute-flow__summary-item">
|
||||
<span class="dispute-flow__summary-label">客户</span>
|
||||
<span class="dispute-flow__summary-value">{{ customerName || '-' }}</span>
|
||||
</div>
|
||||
<div class="dispute-flow__summary-item">
|
||||
<span class="dispute-flow__summary-label">当前状态</span>
|
||||
<span class="dispute-flow__summary-value">
|
||||
<el-tag :type="flowLoaded && flowInfo && flowInfo.status === 'FINISHED' ? 'success' : 'warning'" effect="plain">
|
||||
{{ !flowLoaded ? '加载中' : flowInfo && flowInfo.status === 'FINISHED' ? '已闭环' : flowInfo ? '处理中' : '未发起' }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="dispute-flow__summary-item">
|
||||
<span class="dispute-flow__summary-label">当前负责人</span>
|
||||
<span class="dispute-flow__summary-value">{{ !flowLoaded ? '—' : flowInfo ? userLabel(flowInfo.currentAssignee) : '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="flowLoaded && !flowInfo"
|
||||
type="info"
|
||||
:closable="false"
|
||||
title="该订单尚未发起异议流程"
|
||||
show-icon
|
||||
style="margin-bottom: 12px;"
|
||||
/>
|
||||
|
||||
<div v-if="flowLoaded && !flowInfo && showStart" class="dispute-flow__start">
|
||||
<div class="dispute-flow__start-card">
|
||||
<div class="dispute-flow__section-title">指定各节点负责人</div>
|
||||
<el-form :model="startForm" label-width="90px" class="dispute-flow__start-form">
|
||||
<el-form-item label="异议接收">
|
||||
<UserSelect v-model="startForm.acceptUserId" user-only clearable placeholder="选择负责人" style="width: 320px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="实物确认">
|
||||
<UserSelect v-model="startForm.physicalUserId" user-only clearable placeholder="选择负责人" style="width: 320px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="原因分析">
|
||||
<UserSelect v-model="startForm.analysisUserId" user-only clearable placeholder="选择负责人" style="width: 320px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="处置方案">
|
||||
<UserSelect v-model="startForm.planUserId" user-only clearable placeholder="选择负责人" style="width: 320px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="闭环回访">
|
||||
<UserSelect v-model="startForm.visitUserId" user-only clearable placeholder="选择负责人" style="width: 320px;" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="dispute-flow__start-actions">
|
||||
<el-button type="primary" :loading="startLoading" @click="startFlow">发起流程</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else-if="flowLoaded">
|
||||
<div class="dispute-flow__content">
|
||||
<div class="dispute-flow__timeline-card">
|
||||
<div class="dispute-flow__section-title">流程进度</div>
|
||||
<el-steps direction="vertical" :active="activeStepIndex" finish-status="success">
|
||||
<el-step
|
||||
v-for="s in stepDefs"
|
||||
:key="s.key"
|
||||
:title="s.label"
|
||||
:description="stepDesc(s)"
|
||||
/>
|
||||
</el-steps>
|
||||
</div>
|
||||
|
||||
<div class="dispute-flow__action-card">
|
||||
<div class="dispute-flow__section-title">当前操作</div>
|
||||
|
||||
<div v-if="flowInfo.status === 'FINISHED'" class="dispute-flow__finished">
|
||||
<el-alert type="success" :closable="false" title="异议流程已闭环" show-icon />
|
||||
</div>
|
||||
|
||||
<div v-else class="dispute-flow__handle">
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="当前节点">
|
||||
<el-tag type="warning" v-if="flowInfo.currentTaskName">{{ flowInfo.currentTaskName }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人">
|
||||
<span>{{ userLabel(flowInfo.currentAssignee) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="处理意见">
|
||||
<el-input v-model="confirmComment" type="textarea" :rows="4" placeholder="填写处理意见(可选)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="dispute-flow__handle-actions">
|
||||
<el-button type="primary" :loading="confirmLoading" :disabled="!canConfirm" @click="confirmStep">确认</el-button>
|
||||
<el-button plain :disabled="confirmLoading" @click="confirmComment = ''">清空</el-button>
|
||||
</div>
|
||||
<div v-if="!canConfirm" class="dispute-flow__tip">仅当前节点负责人可执行确认操作。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-skeleton :rows="8" animated />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { getOrderDisputeFlowByOrder, startOrderDisputeFlow, confirmOrderDisputeFlow } from '@/api/oa/orderDisputeFlow'
|
||||
import { listUser } from '@/api/system/user'
|
||||
import UserSelect from '@/components/UserSelect/index.vue'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const props = defineProps({
|
||||
orderId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
orderCode: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
customerId: {
|
||||
type: [String, Number],
|
||||
default: undefined
|
||||
},
|
||||
customerName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showStart: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const startLoading = ref(false)
|
||||
const confirmLoading = ref(false)
|
||||
const flowInfo = ref(null)
|
||||
const flowLoaded = ref(false)
|
||||
const confirmComment = ref('')
|
||||
|
||||
const userMap = ref(new Map())
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const res = await listUser({ pageNum: 1, pageSize: 9999 })
|
||||
const rows = res?.rows || []
|
||||
const map = new Map()
|
||||
rows.forEach(u => {
|
||||
map.set(String(u.userId), u.nickName || u.userName || String(u.userId))
|
||||
})
|
||||
userMap.value = map
|
||||
} catch (e) {
|
||||
userMap.value = new Map()
|
||||
}
|
||||
}
|
||||
|
||||
function userLabel(userId) {
|
||||
if (!userId) return '-'
|
||||
const key = String(userId)
|
||||
return userMap.value.get(key) || key
|
||||
}
|
||||
|
||||
const stepDefs = [
|
||||
{ key: 'acceptTask', label: '异议接收', varKey: 'acceptUserId' },
|
||||
{ key: 'physicalTask', label: '实物确认', varKey: 'physicalUserId' },
|
||||
{ key: 'analysisTask', label: '原因分析', varKey: 'analysisUserId' },
|
||||
{ key: 'planTask', label: '处置方案', varKey: 'planUserId' },
|
||||
{ key: 'visitTask', label: '闭环回访', varKey: 'visitUserId' }
|
||||
]
|
||||
|
||||
const startForm = reactive({
|
||||
acceptUserId: undefined,
|
||||
physicalUserId: undefined,
|
||||
analysisUserId: undefined,
|
||||
planUserId: undefined,
|
||||
visitUserId: undefined
|
||||
})
|
||||
|
||||
function notifyTaskChanged(action) {
|
||||
if (typeof window === 'undefined') return
|
||||
window.dispatchEvent(new CustomEvent('order-dispute-task-changed', {
|
||||
detail: {
|
||||
action,
|
||||
orderId: props.orderId != null ? Number(props.orderId) : undefined,
|
||||
orderCode: props.orderCode || undefined
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
function resetStartForm() {
|
||||
startForm.acceptUserId = userStore.id || undefined
|
||||
startForm.physicalUserId = undefined
|
||||
startForm.analysisUserId = undefined
|
||||
startForm.planUserId = undefined
|
||||
startForm.visitUserId = undefined
|
||||
}
|
||||
|
||||
async function loadFlow() {
|
||||
if (!props.orderId) {
|
||||
flowInfo.value = null
|
||||
flowLoaded.value = true
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
flowLoaded.value = false
|
||||
try {
|
||||
const res = await getOrderDisputeFlowByOrder(props.orderId)
|
||||
flowInfo.value = res?.data || null
|
||||
} finally {
|
||||
loading.value = false
|
||||
flowLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function startFlow() {
|
||||
if (!props.orderId) return
|
||||
startLoading.value = true
|
||||
try {
|
||||
await startOrderDisputeFlow({
|
||||
orderId: String(props.orderId),
|
||||
orderCode: props.orderCode || undefined,
|
||||
customerId: props.customerId != null ? String(props.customerId) : undefined,
|
||||
customerName: props.customerName || undefined,
|
||||
acceptUserId: startForm.acceptUserId != null ? String(startForm.acceptUserId) : undefined,
|
||||
physicalUserId: startForm.physicalUserId != null ? String(startForm.physicalUserId) : undefined,
|
||||
analysisUserId: startForm.analysisUserId != null ? String(startForm.analysisUserId) : undefined,
|
||||
planUserId: startForm.planUserId != null ? String(startForm.planUserId) : undefined,
|
||||
visitUserId: startForm.visitUserId != null ? String(startForm.visitUserId) : undefined
|
||||
})
|
||||
await loadFlow()
|
||||
notifyTaskChanged('start')
|
||||
} finally {
|
||||
startLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const currentUserIdStr = computed(() => {
|
||||
const id = userStore.id
|
||||
return id != null ? String(id) : ''
|
||||
})
|
||||
|
||||
const canConfirm = computed(() => {
|
||||
if (!flowInfo.value || flowInfo.value.status !== 'RUNNING') return false
|
||||
const assignee = flowInfo.value.currentAssignee
|
||||
return assignee && String(assignee) === currentUserIdStr.value
|
||||
})
|
||||
|
||||
async function confirmStep() {
|
||||
if (!props.orderId) return
|
||||
confirmLoading.value = true
|
||||
try {
|
||||
await confirmOrderDisputeFlow({
|
||||
orderId: String(props.orderId),
|
||||
comment: confirmComment.value || ''
|
||||
})
|
||||
confirmComment.value = ''
|
||||
await loadFlow()
|
||||
notifyTaskChanged('confirm')
|
||||
} finally {
|
||||
confirmLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function stepDesc(stepDef) {
|
||||
const fi = flowInfo.value
|
||||
if (!fi) return ''
|
||||
const done = (fi.steps || []).find(s => s.stepKey === stepDef.key)
|
||||
if (done && done.endTime) {
|
||||
const who = userLabel(done.assignee)
|
||||
const when = done.endTime ? new Date(done.endTime).toLocaleString() : ''
|
||||
const msg = done.comment ? `;意见:${done.comment}` : ''
|
||||
return `已完成:${who}${when ? ';时间:' + when : ''}${msg}`
|
||||
}
|
||||
const currentKey = fi.currentTaskKey
|
||||
if (currentKey === stepDef.key) {
|
||||
const who = userLabel(fi.currentAssignee)
|
||||
return `进行中:${who}`
|
||||
}
|
||||
const who = userLabel(fi.variables ? fi.variables[stepDef.varKey] : '')
|
||||
return who && who !== '-' ? `待处理:${who}` : '待处理'
|
||||
}
|
||||
|
||||
const activeStepIndex = computed(() => {
|
||||
const fi = flowInfo.value
|
||||
if (!fi) return 0
|
||||
const idx = stepDefs.findIndex(s => s.key === fi.currentTaskKey)
|
||||
if (idx >= 0) return idx
|
||||
const doneCount = (fi.steps || []).filter(s => s.endTime).length
|
||||
return Math.min(doneCount, stepDefs.length - 1)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
resetStartForm()
|
||||
await loadUsers()
|
||||
await loadFlow()
|
||||
})
|
||||
|
||||
watch(() => props.orderId, async () => {
|
||||
resetStartForm()
|
||||
flowLoaded.value = false
|
||||
await loadFlow()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dispute-flow {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.dispute-flow__hero {
|
||||
padding: 16px 18px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fafbfd 100%);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dispute-flow__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dispute-flow__title-main {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.dispute-flow__title-sub {
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dispute-flow__summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.dispute-flow__summary-item {
|
||||
padding: 12px 14px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.dispute-flow__summary-label {
|
||||
display: block;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dispute-flow__summary-value {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dispute-flow__section-title {
|
||||
margin-bottom: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dispute-flow__start-card,
|
||||
.dispute-flow__timeline-card,
|
||||
.dispute-flow__action-card {
|
||||
padding: 18px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.dispute-flow__start-form {
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.dispute-flow__start-actions {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dispute-flow__content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 1.2fr) minmax(320px, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.dispute-flow__handle {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.dispute-flow__handle-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dispute-flow__tip {
|
||||
margin-top: 12px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.dispute-flow__summary,
|
||||
.dispute-flow__content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -108,6 +108,12 @@
|
||||
<el-table-column label="原因" prop="reason" min-width="160" />
|
||||
<el-table-column label="状态" prop="status" width="120" />
|
||||
<el-table-column label="涉及金额" prop="amount" width="120" />
|
||||
<el-table-column label="流程" width="160" align="center">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="openStart(scope.row)">发起流程</el-button>
|
||||
<el-button link type="primary" @click="openFlow(scope.row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
@@ -118,15 +124,63 @@
|
||||
@pagination="fetchDetail"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="startOpen" title="发起异议流程" width="720px" append-to-body destroy-on-close>
|
||||
<el-form :model="startForm" label-width="90px">
|
||||
<el-form-item label="订单ID">
|
||||
<el-input :model-value="startRow && startRow.orderId ? String(startRow.orderId) : '-'" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="客户">
|
||||
<el-input :model-value="startRow && startRow.customerName ? startRow.customerName : '-'" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="异议接收">
|
||||
<UserSelect v-model="startForm.acceptUserId" user-only clearable placeholder="选择负责人" style="width: 320px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="实物确认">
|
||||
<UserSelect v-model="startForm.physicalUserId" user-only clearable placeholder="选择负责人" style="width: 320px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="原因分析">
|
||||
<UserSelect v-model="startForm.analysisUserId" user-only clearable placeholder="选择负责人" style="width: 320px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="处置方案">
|
||||
<UserSelect v-model="startForm.planUserId" user-only clearable placeholder="选择负责人" style="width: 320px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="闭环回访">
|
||||
<UserSelect v-model="startForm.visitUserId" user-only clearable placeholder="选择负责人" style="width: 320px;" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="startOpen = false">取消</el-button>
|
||||
<el-button type="primary" :loading="startLoading" @click="submitStart">确定发起</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="flowOpen" title="订单异议流程" width="1200px" append-to-body destroy-on-close>
|
||||
<el-empty v-if="!flowRow || !flowRow.orderId" description="请选择订单" />
|
||||
<DisputeFlow
|
||||
v-else
|
||||
:order-id="flowRow.orderId"
|
||||
:customer-id="flowRow.customerId"
|
||||
:customer-name="flowRow.customerName"
|
||||
:show-start="false"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="flowOpen = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="ReturnExchangeSummary">
|
||||
import { computed, getCurrentInstance, onMounted, reactive, ref, toRefs } from 'vue'
|
||||
import CustomerSelect from '@/components/CustomerSelect/index.vue'
|
||||
import UserSelect from '@/components/UserSelect/index.vue'
|
||||
import DisputeFlow from '@/views/oms/order/panels/disputeFlow.vue'
|
||||
import { listSalesman } from '@/api/oms/salesman'
|
||||
import { getReturnExchangeSummary, listReturnExchange } from '@/api/oa/returnExchange'
|
||||
import { startOrderDisputeFlow } from '@/api/oa/orderDisputeFlow'
|
||||
import * as XLSX from 'xlsx'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
@@ -138,6 +192,73 @@ const salesmanOptions = ref([])
|
||||
const timeRange = ref([])
|
||||
const summaryDims = ref([])
|
||||
const exportGroupBy = ref('')
|
||||
const startOpen = ref(false)
|
||||
const startLoading = ref(false)
|
||||
const startRow = ref(null)
|
||||
const flowOpen = ref(false)
|
||||
const flowRow = ref(null)
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const startForm = reactive({
|
||||
acceptUserId: undefined,
|
||||
physicalUserId: undefined,
|
||||
analysisUserId: undefined,
|
||||
planUserId: undefined,
|
||||
visitUserId: undefined
|
||||
})
|
||||
|
||||
function resetStartForm() {
|
||||
const uid = userStore.id
|
||||
startForm.acceptUserId = uid != null ? String(uid) : undefined
|
||||
startForm.physicalUserId = uid != null ? String(uid) : undefined
|
||||
startForm.analysisUserId = uid != null ? String(uid) : undefined
|
||||
startForm.planUserId = uid != null ? String(uid) : undefined
|
||||
startForm.visitUserId = uid != null ? String(uid) : undefined
|
||||
}
|
||||
|
||||
function openStart(row) {
|
||||
startRow.value = row
|
||||
resetStartForm()
|
||||
startOpen.value = true
|
||||
}
|
||||
|
||||
function openFlow(row) {
|
||||
flowRow.value = row
|
||||
flowOpen.value = true
|
||||
}
|
||||
|
||||
function notifyTaskChanged(action, orderId) {
|
||||
if (typeof window === 'undefined') return
|
||||
window.dispatchEvent(new CustomEvent('order-dispute-task-changed', {
|
||||
detail: { action, orderId }
|
||||
}))
|
||||
}
|
||||
|
||||
async function submitStart() {
|
||||
if (!startRow.value || !startRow.value.orderId) {
|
||||
proxy.$modal.msgError('缺少订单ID')
|
||||
return
|
||||
}
|
||||
startLoading.value = true
|
||||
try {
|
||||
await startOrderDisputeFlow({
|
||||
orderId: String(startRow.value.orderId),
|
||||
customerId: startRow.value.customerId != null ? String(startRow.value.customerId) : undefined,
|
||||
customerName: startRow.value.customerName || undefined,
|
||||
acceptUserId: startForm.acceptUserId != null ? String(startForm.acceptUserId) : undefined,
|
||||
physicalUserId: startForm.physicalUserId != null ? String(startForm.physicalUserId) : undefined,
|
||||
analysisUserId: startForm.analysisUserId != null ? String(startForm.analysisUserId) : undefined,
|
||||
planUserId: startForm.planUserId != null ? String(startForm.planUserId) : undefined,
|
||||
visitUserId: startForm.visitUserId != null ? String(startForm.visitUserId) : undefined
|
||||
})
|
||||
proxy.$modal.msgSuccess('已发起流程')
|
||||
startOpen.value = false
|
||||
notifyTaskChanged('start', String(startRow.value.orderId))
|
||||
} finally {
|
||||
startLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const summary = reactive({
|
||||
totalCount: 0,
|
||||
|
||||
Reference in New Issue
Block a user