完成了基本调整调整

This commit is contained in:
2026-09-18 20:55:21 +08:00
parent 864187e105
commit 57e0854a0e
56 changed files with 3122 additions and 105 deletions
+66 -1
View File
@@ -6,7 +6,12 @@ const api = {
share: '/partners.order/share',
qrCode: '/partners.order/qrCode',
oilCallback: '/partners.order/oilCallback',
useCallback: '/partners.order/useCallback'
useCallback: '/partners.order/useCallback',
refund: '/partners.order/refund',
refundConfirm: '/partners.order/refundConfirm',
refundResolve: '/partners.order/refundResolve',
refundLog: '/partners.order/refundLog',
invoice: '/partners.order/invoice'
}
// 列表记录
@@ -65,3 +70,63 @@ export function useCallback (data) {
data
})
}
/**
* 退款(整单全额:企油通 7.17 查凭证 + 7.16 调低额度)
* @param {*} data { orderId, reason }
*/
export function refund (data) {
return axios({
url: api.refund,
method: 'post',
data
})
}
/**
* 待人工确认的退款:向企油通取证确认
* @param {*} data { orderId }
*/
export function refundConfirm (data) {
return axios({
url: api.refundConfirm,
method: 'post',
data
})
}
/**
* 待人工确认的退款:人工判定
* @param {*} data { orderId, result: success|failed, remark }
*/
export function refundResolve (data) {
return axios({
url: api.refundResolve,
method: 'post',
data
})
}
/**
* 订单退款流水
* @param {*} params { orderId }
*/
export function refundLog (params) {
return axios({
url: api.refundLog,
method: 'get',
params
})
}
/**
* 订单关联发票(企油通 7.14,按企业+消费月份+司机)
* @param {*} params { orderId, refresh }
*/
export function invoice (params) {
return axios({
url: api.invoice,
method: 'get',
params
})
}
+23
View File
@@ -0,0 +1,23 @@
import { axios } from '@/utils/request'
// api接口列表
const api = {
list: '/qyt.driver/list',
detail: '/qyt.driver/detail',
upstream: '/qyt.driver/upstream'
}
// 司机列表
export function list (params) {
return axios({ url: api.list, method: 'get', params })
}
// 司机详情
export function detail (params) {
return axios({ url: api.detail, method: 'get', params })
}
// 实时查企油通:type = balance(7.12) / wallet(7.13) / voucher(7.17)
export function upstream (params) {
return axios({ url: api.upstream, method: 'get', params })
}
+7 -1
View File
@@ -11,7 +11,8 @@ const api = {
setDefault: '/qyt.enterprise/setDefault',
apply: '/qyt.enterprise/apply',
syncAccount: '/qyt.enterprise/syncAccount',
syncList: '/qyt.enterprise/syncList'
syncList: '/qyt.enterprise/syncList',
balance: '/qyt.enterprise/balance'
}
// 企业列表
@@ -63,3 +64,8 @@ export function syncAccount (data) {
export function syncList (data) {
return axios({ url: api.syncList, method: 'post', data })
}
// 7.11 企业账户余额(仅平安银行通道)
export function balance (params) {
return axios({ url: api.balance, method: 'get', params })
}
+17
View File
@@ -0,0 +1,17 @@
import { axios } from '@/utils/request'
// api接口列表
const api = {
list: '/qyt.invoice/list',
sync: '/qyt.invoice/sync'
}
// 发票记录
export function list (params) {
return axios({ url: api.list, method: 'get', params })
}
// 同步发票(企油通 7.14):enterpriseId 不传则批量同步所有启用企业当月全量
export function sync (data) {
return axios({ url: api.sync, method: 'post', data })
}
+4 -2
View File
@@ -55,9 +55,11 @@ export default {
'appKey', 'callback', 'create_time'
])
partnersFormData.callback = partnersFormData.callback.toString()
console.log(partnersFormData)
// 企油通企业:0 表示未绑定(走默认企业),下拉框要给 undefined 才会显示占位提示
const enterpriseId = Number(partnersInfo.qyt_enterprise_id || 0)
return {
...partnersFormData
...partnersFormData,
qyt_enterprise_id: enterpriseId > 0 ? enterpriseId : undefined
}
},
+10
View File
@@ -570,6 +570,16 @@ export const asyncRouterMap = [
component: () => import(/* webpackChunkName: "goods" */ '@/views/partners/order/Index'),
meta: { title: '订单列表', keepAlive: false, permission: ['/partners/order/index'] }
},
{
path: '/qyt/driver/index',
component: () => import(/* webpackChunkName: "goods" */ '@/views/qyt/driver/Index'),
meta: { title: '司机管理', keepAlive: false, permission: ['/qyt/driver/index'] }
},
{
path: '/qyt/invoice/index',
component: () => import(/* webpackChunkName: "goods" */ '@/views/qyt/invoice/Index'),
meta: { title: '发票管理', keepAlive: false, permission: ['/qyt/invoice/index'] }
},
{
path: '/partners/balance',
component: RouteView,
+1 -1
View File
@@ -152,7 +152,7 @@ export default {
onFormSubmit (values) {
this.isLoading = true
this.isBtnLoading = true
PartnersApi.add({ form: values })
PartnersApi.add({ form: { ...values, qyt_enterprise_id: values.qyt_enterprise_id || 0 } })
.then(result => {
// 显示提示信息
this.$message.success(result.message, 1.5)
+33 -1
View File
@@ -37,8 +37,24 @@
:data="loadData"
:rowSelection="rowSelection"
:pageSize="15"
:scroll="{ x: 1450 }"
:scroll="{ x: 1670 }"
>
<!-- 企油通企业:下单时实际使用的企业 -->
<div slot="qyt" slot-scope="text, item">
<template v-if="item.qyt_enterprise_source === 'bind'">
<p class="qyt-name">{{ item.qyt_enterprise_name }}</p>
<p class="qyt-sub">已绑定 · {{ item.qyt_enterprise_channel }}</p>
</template>
<template v-else-if="item.qyt_enterprise_source === 'default'">
<p class="qyt-name">{{ item.qyt_enterprise_name }}</p>
<p class="qyt-sub">未绑定,使用默认企业 · {{ item.qyt_enterprise_channel }}</p>
</template>
<template v-else-if="item.qyt_enterprise_source === 'invalid'">
<p class="qyt-name text-red">绑定失效:{{ item.qyt_bind_name || '企业已删除' }}</p>
<p class="qyt-sub">{{ item.qyt_enterprise_name ? '实际使用默认企业 ' + item.qyt_enterprise_name : '且没有默认企业,下单会被拒绝' }}</p>
</template>
<p v-else class="qyt-name text-red">无可用企业,下单会被拒绝</p>
</div>
<!-- 商品名称 -->
<span slot="address" slot-scope="text">
<p class="twoline-hide" style="width: 270px;">{{ text }}</p>
@@ -79,6 +95,11 @@ const columns = [
width: '150px',
dataIndex: 'name'
},
{
title: '企油通企业',
width: '220px',
scopedSlots: { customRender: 'qyt' }
},
{
title: '合作商地址',
width: '200px',
@@ -260,4 +281,15 @@ export default {
.tab-list {
margin-right: 20px;
}
.qyt-name {
margin: 0;
}
.qyt-sub {
margin: 2px 0 0;
color: #999;
font-size: 12px;
}
.text-red {
color: #f5222d;
}
</style>
+2 -1
View File
@@ -180,7 +180,8 @@ export default {
onFormSubmit (values) {
this.isLoading = true
this.isBtnLoading = true
PartnersApi.edit({ partnersId: this.partnersId, form: values })
// 清空企业下拉框时值是 undefined,JSON 序列化会丢掉该字段,后端就不会解绑;显式提交 0
PartnersApi.edit({ partnersId: this.partnersId, form: { ...values, qyt_enterprise_id: values.qyt_enterprise_id || 0 } })
.then(result => {
// 显示提示信息
this.$message.success(result.message, 1.5)
+68 -7
View File
@@ -43,6 +43,16 @@
>{{ item.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="退款状态">
<a-select v-decorator="['refund_status', { initialValue: -1 }]">
<a-select-option :value="-1">全部</a-select-option>
<a-select-option :value="0">未退款</a-select-option>
<a-select-option :value="1">退款中</a-select-option>
<a-select-option :value="2">已退款</a-select-option>
<a-select-option :value="3">退款失败</a-select-option>
<a-select-option :value="4">待人工确认</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="合作商">
<a-select v-decorator="['partners_id', { initialValue: -1 }]">
<a-select-option :value="-1">全部</a-select-option>
@@ -91,9 +101,9 @@
</div>
<div slot="consume" slot-scope="item">
<template v-if="item.use_status === 1">
<p class="mtb-2 f-13">{{ item.station_name || '-' }}</p>
<p class="mtb-2 f-13">{{ item.oil_name }} {{ item.litter }}L</p>
<p class="mtb-2 f-13">实付:¥{{ (item.pay_fee / 100).toFixed(2) }}</p>
<p class="mtb-2 f-13">已加油:¥{{ item.used_amount }}({{ item.used_num }} 次)</p>
<p class="mtb-2 f-13">剩余:¥{{ remainAmount(item) }}</p>
<p class="mtb-2 f-13">最近:{{ item.station_name || '-' }} {{ item.oil_name }} {{ item.litter }}L</p>
</template>
<span v-else>-</span>
</div>
@@ -115,6 +125,10 @@
<a-tag :color="qytColor(item.oil_status_zsy)">{{ qytText(item.oil_status_zsy) }}</a-tag>
</p>
<p v-if="item.oil_text" class="mtb-2 f-13 order-remark">{{ item.oil_text }}</p>
<p v-if="item.refund_status > 0" class="mtb-2">
<span class="f-13">退款:</span>
<a-tag :color="refundColor(item.refund_status)">{{ refundText(item.refund_status) }}</a-tag>
</p>
</div>
<div class="actions" slot="action" slot-scope="item">
<!--有付款权限,订单状态为进行中,已支付,并且未使用-->
@@ -123,7 +137,7 @@
@click="handleShare(item)"
>重新发放</a>
<a
v-if="(inArray(item.oil_status_zsy, [2])) && item.oil_status === 1 && item.use_status === 0 && item.oilCardType === 'zsy_ticket'"
v-if="(inArray(item.oil_status_zsy, [2])) && item.oil_status === 1 && item.refund_status !== 2 && Number(remainAmount(item)) > 0 && item.oilCardType === 'zsy_ticket'"
@click="handlePay(item)"
>加油链接</a>
<a
@@ -134,12 +148,16 @@
v-if="(inArray(item.oil_status_zsy, [2])) && item.oil_status === 1 && item.use_status === 1 && item.oilCardType === 'zsy_ticket'"
@click="handleUse(item)"
>重推加油结果</a>
<a v-if="canRefund(item)" @click="handleRefund(item)">{{ refundLabel(item) }}</a>
<a v-if="item.use_status === 1" @click="handleInvoice(item)">发票</a>
</div>
</s-table>
<ShareForm ref="ShareForm" @handleSubmit="handleRefresh" />
<PayForm ref="PayForm" @handleSubmit="handleRefresh" />
<OilForm ref="OilForm" @handleSubmit="handleRefresh" />
<UseForm ref="UseForm" @handleSubmit="handleRefresh" />
<RefundForm ref="RefundForm" @handleSubmit="handleRefresh" />
<InvoiceModal ref="InvoiceModal" />
</a-card>
</template>
@@ -149,7 +167,7 @@ import { STable } from '@/components/Table'
import {
OilStatusEnum, UseStatusEnum
} from '@/common/enum/order'
import { OilForm, UseForm, ShareForm, PayForm } from './modules'
import { OilForm, UseForm, ShareForm, PayForm, RefundForm, InvoiceModal } from './modules'
import { assignment, inArray } from '@/utils/util'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
import * as PartnersApi from '@/api/partners'
@@ -161,7 +179,9 @@ export default {
ShareForm,
PayForm,
OilForm,
UseForm
UseForm,
RefundForm,
InvoiceModal
},
data () {
return {
@@ -219,7 +239,7 @@ export default {
},
{
title: '操作',
width: '180px',
width: '220px',
scopedSlots: { customRender: 'action' }
},
{
@@ -300,6 +320,36 @@ export default {
this.$refs.UseForm.show(record)
},
// 退款状态文案 / 颜色
refundText (v) {
return { 0: '未退款', 1: '退款中', 2: '已退款', 3: '退款失败', 4: '待人工确认' }[v] || '未知'
},
refundColor (v) {
return { 1: 'blue', 2: 'green', 3: 'red', 4: 'orange' }[v] || ''
},
// 发放成功才可能退款;已经有退款记录的单总是可以打开看流水
canRefund (item) {
if (item.oilCardType !== 'zsy_ticket' || item.oil_status_zsy !== 2 || item.oil_status !== 1) {
return false
}
return item.refund_status > 0 || Number(this.remainAmount(item)) > 0
},
// 剩余未加油金额:订单金额 − 已加油 −(已退款时)退款金额
remainAmount (item) {
const refunded = item.refund_status === 2 ? Number(item.refund_amount || 0) : 0
const remain = Number(item.total_price || 0) - Number(item.used_amount || 0) - refunded
return Math.max(0, remain).toFixed(2)
},
refundLabel (item) {
return { 2: '退款记录', 4: '确认退款' }[item.refund_status] || '退款'
},
handleRefund (record) {
this.$refs.RefundForm.show(record)
},
handleInvoice (record) {
this.$refs.InvoiceModal.show(record)
},
/**
* 获取代理商列表
*/
@@ -325,6 +375,17 @@ export default {
.flex-box{
display: flex;
}
// 操作栏:按钮较多会换行,横向纵向都留出间距(覆盖全局 .actions>a 的 8px 右边距)
.actions {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
& > a {
margin-right: 0 !important;
white-space: nowrap;
}
}
.form-total{
color: #1890ff;
margin-right: 10px;
@@ -0,0 +1,105 @@
<template>
<a-modal
title="加油发票"
:width="620"
:visible="visible"
:footer="null"
@cancel="visible = false"
>
<a-spin :spinning="isLoading">
<a-alert type="info" show-icon class="block">
<div slot="message">
企油通发票按「企业 + 月份 + 司机」出具,和单笔订单没有一一对应关系。
这里展示的是<b>该订单所属企业、该司机在消费当月</b>的全部发票,同一司机当月的其他订单看到的是同一批。
</div>
</a-alert>
<p>消费月份:<b>{{ month || '-' }}</b>&nbsp;&nbsp;司机:{{ record.driver_name || '-' }} {{ record.mobile }}</p>
<p v-if="row.sync_time_text" class="sub">最近同步:{{ row.sync_time_text }}&nbsp;&nbsp;{{ row.sync_text }}</p>
<p v-else-if="message" class="sub">{{ message }}</p>
<div v-if="urls.length" class="links">
<p v-for="(url, index) in urls" :key="url">
<a :href="url" target="_blank" rel="noopener noreferrer">发票 {{ index + 1 }}</a>
<span class="url">{{ url }}</span>
</p>
</div>
<a-empty v-else-if="!isLoading" description="企油通暂未出具该月发票" />
<div class="btns">
<a-button :loading="isLoading" @click="load(true)">重新同步</a-button>
</div>
</a-spin>
</a-modal>
</template>
<script>
import * as Api from '@/api/partners/order'
export default {
data () {
return {
visible: false,
isLoading: false,
record: {},
month: '',
message: '',
row: {}
}
},
computed: {
urls () {
return this.row.invoice_urls || []
}
},
methods: {
show (record) {
this.visible = true
this.record = record
this.month = ''
this.message = ''
this.row = {}
this.load(false)
},
load (refresh) {
this.isLoading = true
Api.invoice({ orderId: this.record.order_id, refresh: refresh ? 1 : 0 })
.then(result => {
this.month = result.data.month
this.message = result.data.message
this.row = result.data.row || {}
refresh && this.$message.success(result.message, 1.5)
})
.finally(() => {
this.isLoading = false
})
}
}
}
</script>
<style lang="less" scoped>
.block {
margin-bottom: 12px;
}
.sub {
color: #999;
font-size: 12px;
}
.links p {
margin-bottom: 6px;
word-break: break-all;
}
.url {
margin-left: 8px;
color: #999;
font-size: 12px;
}
.btns {
margin-top: 12px;
text-align: right;
}
</style>
@@ -0,0 +1,235 @@
<template>
<a-modal
title="订单退款"
:width="760"
:visible="visible"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
>
<a-spin :spinning="isLoading">
<div class="info">
<p>油卡编号:{{ record.oilCardNo }}</p>
<p>司机:{{ record.driver_name || '-' }}&nbsp;&nbsp;{{ record.mobile }}</p>
<p>订单金额:¥{{ record.total_price }}&nbsp;&nbsp;已加油:¥{{ record.used_amount || '0.00' }}({{ record.used_num || 0 }} 次)</p>
<p>
退款金额:<b class="amount">¥{{ refundAmount }}</b>
<span v-if="status === 2">(实际退款)</span>
<span v-else>(剩余未加油金额,固定不可修改)</span>
</p>
<p>当前状态:<a-tag :color="statusColor">{{ statusText }}</a-tag></p>
<p v-if="record.refund_text" class="sub">{{ record.refund_text }}</p>
</div>
<a-alert v-if="(status === 0 || status === 3) && Number(refundAmount) <= 0" type="warning" show-icon class="block" message="订单金额已全部加油,没有可退金额" />
<!-- 可发起:未退款 / 退款失败,且还有剩余未加油金额 -->
<template v-if="(status === 0 || status === 3) && Number(refundAmount) > 0">
<a-alert type="info" show-icon class="block">
<div slot="message">
一张订单可以分多次加油,<b>退款金额固定为剩余未加油金额</b>,已加油的部分不退,每单只能成功退一次。
<br>退款会先用企油通 7.17 查询司机支付凭证,再用 7.16 把凭证额度调低退款金额;成功后原路回补合作商余额,并推送油卡变动通知。
<br>额度在凭证上按司机累计,若凭证可用余额不足退款金额(被该司机其他订单用掉),会直接拒绝。
</div>
</a-alert>
<a-textarea v-model="reason" :rows="2" :maxLength="100" placeholder="退款原因(选填,会写入企油通调额记录)" />
<div class="btns">
<a-button type="primary" :loading="isLoading" @click="handleRefund">确认退款</a-button>
</div>
</template>
<!-- 待人工确认 -->
<template v-if="status === 4">
<a-alert type="warning" show-icon class="block">
<div slot="message">
调额请求已发给企油通但没有拿到明确结果。企油通调额接口<b>没有幂等保护</b>,重复发起会被重复扣减,所以这里不允许直接重试。
<br>请先点「向企油通取证」:系统会查司机钱包流水(找退款单号)和凭证额度变化自动判定;判定不了再人工核对后提交。
</div>
</a-alert>
<div class="btns">
<a-button type="primary" :loading="isLoading" @click="handleConfirm">向企油通取证</a-button>
</div>
<a-divider orientation="left">人工判定</a-divider>
<a-radio-group v-model="resolveResult">
<a-radio value="success">企油通已调低额度(退款成功)</a-radio>
<a-radio value="failed">企油通未调额(退款失败,可重新发起)</a-radio>
</a-radio-group>
<a-textarea v-model="resolveRemark" class="mt-10" :rows="2" :maxLength="100" placeholder="判定依据(必填),如:已在企油通后台核对凭证 xxx 总额度由 300 变为 200" />
<div class="btns">
<a-button :loading="isLoading" @click="handleResolve">提交人工判定</a-button>
</div>
</template>
<a-divider orientation="left">退款流水</a-divider>
<a-table
size="small"
rowKey="refund_id"
:columns="columns"
:dataSource="logs"
:pagination="false"
>
<span slot="status" slot-scope="text, row">
<a-tag :color="logColor(row.status)">{{ row.status_text }}</a-tag>
</span>
</a-table>
</a-spin>
</a-modal>
</template>
<script>
import * as Api from '@/api/partners/order'
const STATUS_TEXT = { 0: '未退款', 1: '退款中', 2: '已退款', 3: '退款失败', 4: '待人工确认' }
const STATUS_COLOR = { 1: 'blue', 2: 'green', 3: 'red', 4: 'orange' }
export default {
data () {
return {
visible: false,
isLoading: false,
record: {},
reason: '',
resolveResult: 'success',
resolveRemark: '',
logs: [],
columns: [
{ title: '退款单号', dataIndex: 'refund_no' },
{ title: '状态', scopedSlots: { customRender: 'status' } },
{ title: '凭证', dataIndex: 'quota_id' },
{ title: '调额前可用', dataIndex: 'available_before' },
{ title: '说明', dataIndex: 'message' },
{ title: '操作人', dataIndex: 'operator' },
{ title: '时间', dataIndex: 'create_time_text' }
]
}
},
computed: {
status () {
return Number(this.record.refund_status || 0)
},
statusText () {
return STATUS_TEXT[this.status] || '-'
},
statusColor () {
return STATUS_COLOR[this.status] || ''
},
// 已退款显示实际退款金额;否则为剩余未加油金额(后端同口径计算)
refundAmount () {
if (this.status === 2) {
return Number(this.record.refund_amount || 0).toFixed(2)
}
const remain = Number(this.record.total_price || 0) - Number(this.record.used_amount || 0)
return Math.max(0, remain).toFixed(2)
}
},
methods: {
show (record) {
this.visible = true
this.record = { ...record }
this.reason = ''
this.resolveResult = 'success'
this.resolveRemark = ''
this.loadLogs()
},
handleCancel () {
this.visible = false
},
loadLogs () {
Api.refundLog({ orderId: this.record.order_id }).then(result => {
this.logs = result.data.list
// 流水状态码与订单退款状态一致:用最新一条同步弹窗状态,失败 / 转人工后界面跟着切换
if (this.logs.length) {
this.record = { ...this.record, refund_status: Number(this.logs[0].status), refund_text: this.logs[0].message }
}
})
},
logColor (status) {
return STATUS_COLOR[Number(status)] || ''
},
// 退款 / 取证 / 判定之后统一刷新:弹窗里的状态 + 父列表
afterAction (result, status) {
this.$message.success(result.message, 2)
if (status !== undefined) {
this.record = { ...this.record, refund_status: status, refund_text: result.message }
}
this.loadLogs()
this.$emit('handleSubmit')
},
// 业务失败时后端返回错误,request 拦截器已弹出提示;这里仍需刷新状态(可能已变成失败/待确认)
afterError () {
this.loadLogs()
this.$emit('handleSubmit')
},
handleRefund () {
this.$confirm({
title: '确认退款 ¥' + this.refundAmount + '?',
content: '退还剩余未加油金额:将调低该司机企油通凭证额度,成功后回补合作商余额。',
onOk: () => {
this.isLoading = true
return Api.refund({ orderId: this.record.order_id, reason: this.reason })
.then(result => this.afterAction(result, 2))
.catch(() => this.afterError())
.finally(() => {
this.isLoading = false
})
}
})
},
handleConfirm () {
this.isLoading = true
Api.refundConfirm({ orderId: this.record.order_id })
.then(result => this.afterAction(result, result.data.refundStatus))
.catch(() => this.afterError())
.finally(() => {
this.isLoading = false
})
},
handleResolve () {
if (!this.resolveRemark.trim()) {
this.$message.warning('请填写判定依据')
return
}
this.isLoading = true
Api.refundResolve({ orderId: this.record.order_id, result: this.resolveResult, remark: this.resolveRemark })
.then(result => this.afterAction(result, result.data.refundStatus))
.catch(() => this.afterError())
.finally(() => {
this.isLoading = false
})
}
}
}
</script>
<style lang="less" scoped>
.info p {
margin-bottom: 6px;
}
.amount {
color: #f5222d;
}
.sub {
color: #999;
font-size: 12px;
}
.block {
margin: 12px 0;
}
.btns {
margin-top: 12px;
text-align: right;
}
.mt-10 {
margin-top: 10px;
}
</style>
@@ -2,5 +2,7 @@ import OilForm from './OilForm'
import UseForm from './UseForm'
import ShareForm from './ShareForm'
import PayForm from './PayForm'
import RefundForm from './RefundForm'
import InvoiceModal from './InvoiceModal'
export { OilForm, UseForm, ShareForm, PayForm }
export { OilForm, UseForm, ShareForm, PayForm, RefundForm, InvoiceModal }
+288
View File
@@ -0,0 +1,288 @@
<template>
<a-card :bordered="false">
<div class="card-title">{{ $route.meta.title }}</div>
<a-alert type="info" show-icon class="tips-alert">
<div slot="message">
司机按身份证号归档。企油通发放到同一司机的额度会<b>在凭证上累计</b>,列表里的累计发放 / 已退款是按本地订单汇总的参考值,
<b>真实可用额度以详情里实时查询的企油通数据为准</b>。
标红的「信息冲突」会导致上游实名二要素核验失败,建议先让下单平台核对司机信息。
</div>
</a-alert>
<a-form layout="inline" :form="searchForm" class="search-form" @submit="handleSearch">
<a-form-item label="司机">
<a-input v-decorator="['keyword']" placeholder="姓名 / 手机号 / 完整身份证号" allowClear style="width: 240px" />
</a-form-item>
<a-form-item label="信息冲突">
<a-select v-decorator="['conflict', { initialValue: -1 }]" style="width: 110px">
<a-select-option :value="-1">全部</a-select-option>
<a-select-option :value="1">有冲突</a-select-option>
<a-select-option :value="0">正常</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" icon="search">搜索</a-button>
</a-form-item>
</a-form>
<s-table ref="table" rowKey="driver_id" :columns="columns" :data="loadData">
<template slot="driver" slot-scope="item">
<p class="mtb-2">
<b>{{ item.name || '-' }}</b>
<a-tag v-if="item.conflict" color="red" class="ml-6">信息冲突</a-tag>
</p>
<p class="mtb-2 sub">{{ item.mobile }}&nbsp;&nbsp;{{ item.id_card_mask }}</p>
<p v-if="item.conflict" class="mtb-2 text-red">{{ item.conflict_text }}</p>
</template>
<template slot="orders" slot-scope="item">
<p class="mtb-2">共 {{ item.order_num }} 单</p>
<p class="mtb-2 sub">已加油 {{ item.used_num }}&nbsp;&nbsp;发放中 {{ item.pending_num }}&nbsp;&nbsp;失败 {{ item.fail_num }}</p>
</template>
<template slot="amount" slot-scope="item">
<p class="mtb-2">累计发放 ¥{{ item.grant_amount }}</p>
<p class="mtb-2 sub">已加油 ¥{{ item.used_amount }}&nbsp;&nbsp;已退款 ¥{{ item.refund_amount }}&nbsp;&nbsp;净额 ¥{{ item.net_amount }}</p>
</template>
<template slot="action" slot-scope="item">
<a @click="openDetail(item)">详情 / 查额度</a>
</template>
</s-table>
<a-drawer
title="司机详情"
:width="960"
:visible="drawer"
@close="drawer = false"
>
<a-spin :spinning="detailLoading">
<template v-if="detail.driver_id">
<p class="head">
<b>{{ detail.name }}</b>&nbsp;&nbsp;{{ detail.mobile }}&nbsp;&nbsp;{{ detail.id_card_mask }}
<a-tag v-if="detail.conflict" color="red" class="ml-6">信息冲突</a-tag>
</p>
<a-alert v-if="detail.conflict" type="error" :message="detail.conflict_text" show-icon class="block" />
<p class="sub">
本地汇总:共 {{ detail.order_num }} 单,累计发放 ¥{{ detail.grant_amount }},已退款 ¥{{ detail.refund_amount }},
净额 ¥{{ detail.net_amount }};最近下单 {{ detail.last_order_time_text }}
</p>
<a-divider orientation="left">企油通实时数据</a-divider>
<div class="toolbar">
<span>企业:</span>
<a-select v-model="enterpriseId" style="width: 280px" placeholder="选择企业">
<a-select-option v-for="item in enterprises" :key="item.enterprise_id" :value="item.enterprise_id">
{{ item.company_name }}({{ item.passageway === 'pingan' ? '平安银行' : '支付宝企业码' }})
</a-select-option>
</a-select>
<span v-if="!enterprises.length" class="sub ml-6">该司机还没有关联企业的订单</span>
</div>
<a-tabs v-model="tab" @change="onTabChange">
<a-tab-pane key="balance" tab="钱包余额(7.12)">
<a-button :loading="upLoading" :disabled="!enterpriseId" @click="query('balance')">查询</a-button>
<template v-if="result.balance">
<p class="big">
钱包发放总额 ¥{{ result.balance.totalGrantAmountYuan || '0.00' }}
当前可用 <b class="green">¥{{ result.balance.availableBalanceYuan || '0.00' }}</b>
</p>
<a-table size="small" rowKey="business_license" :pagination="false" :columns="balanceColumns" :dataSource="result.balance.list || []" />
</template>
</a-tab-pane>
<a-tab-pane key="voucher" tab="支付凭证(7.17)">
<a-button :loading="upLoading" :disabled="!enterpriseId" @click="query('voucher')">查询</a-button>
<p class="sub mt-8">额度按凭证累计;退款时就是把其中「生效中、可用余额最大」的一张调低订单金额。</p>
<a-table
v-if="result.voucher"
size="small"
rowKey="quota_id"
:pagination="false"
:columns="voucherColumns"
:dataSource="result.voucher.list || []"
/>
</a-tab-pane>
<a-tab-pane key="wallet" tab="钱包流水(7.13)">
<a-button :loading="upLoading" :disabled="!enterpriseId" @click="query('wallet')">查询</a-button>
<p class="sub mt-8">企油通默认只回溯近 1 个月流水。</p>
<a-table
v-if="result.wallet"
size="small"
rowKey="transactionId"
:pagination="false"
:columns="walletColumns"
:dataSource="result.wallet.transactionDetails || []"
/>
</a-tab-pane>
<a-tab-pane key="orders" tab="最近订单">
<p class="sub">发放失败原因里能看到上游实名二要素核验的结果。</p>
<a-table size="small" rowKey="order_id" :pagination="false" :columns="orderColumns" :dataSource="orders">
<span slot="zsy" slot-scope="text, row">
<a-tag :color="{ 1: 'blue', 2: 'green', 3: 'red' }[row.oil_status_zsy] || ''">
{{ { 0: '未提交', 1: '发放中', 2: '发放成功', 3: '发放失败' }[row.oil_status_zsy] }}
</a-tag>
</span>
</a-table>
</a-tab-pane>
</a-tabs>
</template>
</a-spin>
</a-drawer>
</a-card>
</template>
<script>
import * as Api from '@/api/qyt/driver'
import { STable } from '@/components/Table'
export default {
name: 'QytDriverIndex',
components: { STable },
data () {
return {
searchForm: this.$form.createForm(this),
queryParam: {},
columns: [
{ title: 'ID', dataIndex: 'driver_id', width: 70 },
{ title: '司机', scopedSlots: { customRender: 'driver' } },
{ title: '订单', scopedSlots: { customRender: 'orders' } },
{ title: '金额(本地口径)', scopedSlots: { customRender: 'amount' } },
{ title: '最近下单', dataIndex: 'last_order_time_text', width: 170 },
{ title: '操作', width: 120, scopedSlots: { customRender: 'action' } }
],
loadData: param => {
return Api.list({ ...param, ...this.queryParam }).then(result => result.data.list)
},
drawer: false,
detailLoading: false,
detail: {},
enterprises: [],
orders: [],
enterpriseId: undefined,
tab: 'balance',
upLoading: false,
result: {},
balanceColumns: [
{ title: '企业', dataIndex: 'enterprise_name' },
{ title: '凭证总额(元)', dataIndex: 'total_amount_yuan' },
{ title: '可用(元)', dataIndex: 'available_amount_yuan' }
],
voucherColumns: [
{ title: '凭证ID', dataIndex: 'quota_id' },
{ title: '名称', dataIndex: 'quota_name' },
{ title: '总额度', dataIndex: 'total_amount' },
{ title: '可用', dataIndex: 'available_amount' },
{ title: '已用', dataIndex: 'used_amount' },
{ title: '冻结', dataIndex: 'freeze_amount' },
{ title: '状态', dataIndex: 'status_text' },
{ title: '有效期至', dataIndex: 'effective_end' }
],
walletColumns: [
{ title: '时间', dataIndex: 'transactionTime' },
{ title: '类型', dataIndex: 'transactionType' },
{ title: '金额(元)', dataIndex: 'amount_yuan' },
{ title: '备注', dataIndex: 'remark' },
{ title: '流水号', dataIndex: 'transactionId' }
],
orderColumns: [
{ title: '油卡编号', dataIndex: 'oilCardNo' },
{ title: '金额', dataIndex: 'total_price' },
{ title: '发放', scopedSlots: { customRender: 'zsy' } },
{ title: '说明', dataIndex: 'oil_text' },
{ title: '退款', dataIndex: 'refund_status_text' },
{ title: '下单时间', dataIndex: 'create_time_text' }
]
}
},
methods: {
handleSearch (e) {
e.preventDefault()
this.searchForm.validateFields((errors, values) => {
if (errors) return
this.queryParam = { keyword: values.keyword || '', conflict: values.conflict }
this.$refs.table.refresh(true)
})
},
openDetail (item) {
this.drawer = true
this.detail = {}
this.enterprises = []
this.orders = []
this.result = {}
this.tab = 'balance'
this.enterpriseId = undefined
this.detailLoading = true
Api.detail({ driverId: item.driver_id })
.then(result => {
this.detail = result.data.detail
this.enterprises = result.data.enterprises
this.orders = result.data.orders
this.enterpriseId = this.enterprises.length ? this.enterprises[0].enterprise_id : undefined
})
.finally(() => {
this.detailLoading = false
})
},
onTabChange () {
// 切换页签不自动查上游,避免无意义地打企油通;需要时点「查询」
},
query (type) {
this.upLoading = true
Api.upstream({ driverId: this.detail.driver_id, enterpriseId: this.enterpriseId, type })
.then(result => {
this.result = { ...this.result, [type]: result.data.data }
})
.finally(() => {
this.upLoading = false
})
}
}
}
</script>
<style lang="less" scoped>
.tips-alert {
margin-bottom: 16px;
}
.search-form {
margin-bottom: 16px;
}
.sub {
color: #999;
font-size: 12px;
}
.ml-6 {
margin-left: 6px;
}
.mt-8 {
margin-top: 8px;
}
.text-red {
color: #f5222d;
font-size: 12px;
}
.green {
color: #52c41a;
}
.head {
font-size: 15px;
}
.block {
margin: 10px 0;
}
.toolbar {
margin-bottom: 12px;
}
.big {
margin: 12px 0;
font-size: 15px;
}
</style>
+26 -7
View File
@@ -64,18 +64,15 @@
<div v-if="item.sync_text" class="sub">{{ item.sync_text }}</div>
</template>
<template slot="action" slot-scope="item">
<div slot="action" slot-scope="item" class="op-actions">
<a @click="handleUpdate(item)">编辑</a>
<a-divider type="vertical" />
<a @click="handleApply(item)">提交开户</a>
<a-divider type="vertical" />
<a @click="handleSyncAccount(item)">同步账户</a>
<a-divider type="vertical" />
<a v-if="item.passageway === 'pingan'" @click="handleBalance(item)">账户余额</a>
<a v-if="!item.is_default" @click="handleSetDefault(item)">设为默认</a>
<span v-else class="text-grey">设为默认</span>
<a-divider type="vertical" />
<a class="text-red" @click="handleDelete(item)">删除</a>
</template>
</div>
</s-table>
<a-modal
@@ -190,7 +187,7 @@ export default {
{ title: '企业', scopedSlots: { customRender: 'company' } },
{ title: '通道与机构号', scopedSlots: { customRender: 'passageway' } },
{ title: '企油通状态', scopedSlots: { customRender: 'status' } },
{ title: '操作', width: 260, scopedSlots: { customRender: 'action' } }
{ title: '操作', width: 280, scopedSlots: { customRender: 'action' } }
],
loadData: param => {
return Api.list({ ...param, ...this.queryParam }).then(result => result.data.list)
@@ -310,6 +307,17 @@ export default {
})
},
// 7.11 企业账户余额(平安银行银行侧实时查询,勿频繁点击)
handleBalance (item) {
Api.balance({ enterpriseId: item.enterprise_id }).then(result => {
this.$info({
title: item.company_name + ' · 平安银行账户余额',
content: '账面余额 ¥' + result.data.balance + ',可用余额 ¥' + result.data.available_balance +
'(可用余额已扣除冻结/止付金额,即当前可发放凭证的资金上限)'
})
})
},
handleSyncList () {
this.isSyncing = true
Api.syncList({})
@@ -351,4 +359,15 @@ export default {
.text-grey {
color: #bbb;
}
// 操作栏:按钮较多会换行,用 gap 统一横纵间距,不用竖线分隔(换行后竖线会悬在行首行尾)
.op-actions {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
& > a,
& > span {
white-space: nowrap;
}
}
</style>
+178
View File
@@ -0,0 +1,178 @@
<template>
<a-card :bordered="false">
<div class="card-title">{{ $route.meta.title }}</div>
<a-alert type="info" show-icon class="tips-alert">
<div slot="message">
企油通 7.14 按「企业 + 月份(+ 司机手机号)」出具发票,发票地址与单笔订单没有对应关系。
订单列表里点「发票」会自动按该订单的企业、消费月份、司机同步;这里可以按月批量同步或手动补同步。
</div>
</a-alert>
<a-form layout="inline" :form="syncForm" class="sync-form" @submit="handleSync">
<a-form-item label="企业">
<a-select v-decorator="['enterpriseId', { initialValue: 0 }]" style="width: 220px">
<a-select-option :value="0">全部启用企业(企业全量)</a-select-option>
<a-select-option v-for="item in enterpriseList" :key="item.enterprise_id" :value="item.enterprise_id">
{{ item.company_name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="月份">
<a-month-picker v-decorator="['month', { rules: [{ required: true, message: '请选择月份' }] }]" placeholder="选择月份" />
</a-form-item>
<a-form-item label="司机手机号">
<a-input v-decorator="['mobile']" placeholder="选填,仅指定企业时有效" allowClear />
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" icon="sync" :loading="isSyncing">同步发票</a-button>
</a-form-item>
</a-form>
<a-divider />
<a-form layout="inline" :form="searchForm" class="search-form" @submit="handleSearch">
<a-form-item label="企业">
<a-select v-decorator="['qyt_enterprise_id', { initialValue: 0 }]" style="width: 200px">
<a-select-option :value="0">全部</a-select-option>
<a-select-option v-for="item in enterpriseList" :key="item.enterprise_id" :value="item.enterprise_id">
{{ item.company_name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="月份">
<a-month-picker v-decorator="['month']" placeholder="全部" />
</a-form-item>
<a-form-item label="司机手机号">
<a-input v-decorator="['mobile']" placeholder="手机号" allowClear />
</a-form-item>
<a-form-item label="发票">
<a-select v-decorator="['has_invoice', { initialValue: -1 }]" style="width: 110px">
<a-select-option :value="-1">全部</a-select-option>
<a-select-option :value="1">已出具</a-select-option>
<a-select-option :value="0">暂无</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" icon="search">搜索</a-button>
</a-form-item>
</a-form>
<s-table ref="table" rowKey="invoice_id" :columns="columns" :data="loadData">
<template slot="dimension" slot-scope="item">
<p class="mtb-2"><b>{{ item.company_name }}</b></p>
<p class="mtb-2 sub">{{ item.passageway === 'pingan' ? '平安银行' : '支付宝企业码' }}&nbsp;&nbsp;{{ item.month }}&nbsp;&nbsp;{{ item.mobile_mask }}</p>
</template>
<template slot="invoices" slot-scope="item">
<template v-if="item.invoice_urls.length">
<p v-for="(url, index) in item.invoice_urls" :key="url" class="mtb-2">
<a :href="url" target="_blank" rel="noopener noreferrer">发票 {{ index + 1 }}</a>
</p>
</template>
<span v-else class="sub">暂无</span>
</template>
<template slot="sync" slot-scope="item">
<a-tag :color="{ 1: 'green', 2: 'red' }[item.sync_status] || ''">{{ { 0: '未同步', 1: '成功', 2: '失败' }[item.sync_status] }}</a-tag>
<p class="mtb-2 sub">{{ item.sync_text }}</p>
<p class="mtb-2 sub">{{ item.sync_time_text }}</p>
</template>
<template slot="action" slot-scope="item">
<a @click="handleResync(item)">重新同步</a>
</template>
</s-table>
</a-card>
</template>
<script>
import * as Api from '@/api/qyt/invoice'
import * as EnterpriseApi from '@/api/qyt/enterprise'
import { STable } from '@/components/Table'
export default {
name: 'QytInvoiceIndex',
components: { STable },
data () {
return {
syncForm: this.$form.createForm(this),
searchForm: this.$form.createForm(this),
enterpriseList: [],
isSyncing: false,
queryParam: {},
columns: [
{ title: 'ID', dataIndex: 'invoice_id', width: 70 },
{ title: '企业 / 通道 / 月份 / 维度', scopedSlots: { customRender: 'dimension' } },
{ title: '发票', scopedSlots: { customRender: 'invoices' } },
{ title: '加油明细条数', dataIndex: 'order_count', width: 110 },
{ title: '同步状态', scopedSlots: { customRender: 'sync' } },
{ title: '操作', width: 100, scopedSlots: { customRender: 'action' } }
],
loadData: param => {
return Api.list({ ...param, ...this.queryParam }).then(result => result.data.list)
}
}
},
created () {
EnterpriseApi.options().then(result => {
this.enterpriseList = result.data.list
})
},
methods: {
handleSync (e) {
e.preventDefault()
this.syncForm.validateFields((errors, values) => {
if (errors) return
this.doSync({
enterpriseId: values.enterpriseId || 0,
month: values.month.format('YYYY-MM'),
mobile: values.enterpriseId ? (values.mobile || '') : ''
})
})
},
handleResync (item) {
this.doSync({ enterpriseId: item.qyt_enterprise_id, month: item.month, mobile: item.mobile })
},
doSync (data) {
this.isSyncing = true
Api.sync(data)
.then(result => {
this.$message.success(result.message, 2)
this.$refs.table.refresh()
})
.finally(() => {
this.isSyncing = false
})
},
handleSearch (e) {
e.preventDefault()
this.searchForm.validateFields((errors, values) => {
if (errors) return
this.queryParam = {
qyt_enterprise_id: values.qyt_enterprise_id || '',
month: values.month ? values.month.format('YYYY-MM') : '',
mobile: values.mobile || '',
has_invoice: values.has_invoice
}
this.$refs.table.refresh(true)
})
}
}
}
</script>
<style lang="less" scoped>
.tips-alert {
margin-bottom: 16px;
}
.sub {
color: #999;
font-size: 12px;
}
.search-form {
margin-bottom: 16px;
}
</style>