feat: 平台订单改抢单要求 + 商品列表批量改抢单要求

需权限 002ad;批量入口在商品列表页,按当前俱乐部类型预览后写入。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQueandCursor committed 2026-07-29 07:48:34 +08:00
1 parent 94a8a3b76b
commit f2d6b817f2
3 files changed
+435 -11

No files matched your search

+134 -7
View File
@@ -18,8 +18,16 @@
</div>
</div>
<!-- 操作按钮组(根据状态动态显示) -->
<div class="action-bar" v-if="showActionButtons || showRecoverButton">
<!-- 操作按钮组(根据状态动态显示;跨店只读) -->
<div v-if="!canMutateOrder" class="action-bar readonly-tip">
<el-alert
type="info"
:closable="false"
show-icon
title="该订单不属于当前俱乐部,仅可查看;请由对应俱乐部客服处理"
/>
</div>
<div class="action-bar" v-if="canMutateOrder && (showActionButtons || showRecoverButton)">
<el-row :gutter="12">
<!-- 恢复按钮:状态未知时显示(原逻辑) -->
<el-col :span="6" v-if="showRecoverButton">
@@ -110,9 +118,24 @@
<el-descriptions-item label="发单平台">
{{ orderData.fadanpingtai === 1 ? '平台发单' : orderData.fadanpingtai === 2 ? '商家发单' : '未知' }}
</el-descriptions-item>
<el-descriptions-item v-if="orderData.club_id || orderData.club_name" label="所属俱乐部">
{{ orderData.club_name || orderData.club_id }}
</el-descriptions-item>
<el-descriptions-item label="订单金额">
¥{{ formatPrice(orderData.jiage) }}
</el-descriptions-item>
<el-descriptions-item label="抢单要求">
<span v-if="Number(orderData.yaoqiuleixing) === 1">会员 {{ orderData.huiyuan_id || '--' }}</span>
<span v-else-if="Number(orderData.yaoqiuleixing) === 2">押金 ¥{{ formatPrice(orderData.yajin) }}</span>
<span v-else>--</span>
<el-button
v-if="canEditGrabReq"
type="warning"
link
style="margin-left: 8px"
@click="openGrabDialog"
>修改</el-button>
</el-descriptions-item>
<el-descriptions-item label="打手分成" v-if="orderData.dashou_fencheng">
¥{{ formatPrice(orderData.dashou_fencheng) }}
</el-descriptions-item>
@@ -240,14 +263,28 @@
</el-descriptions>
</el-card>
<!-- 退款/拒绝理由 -->
<el-card class="info-card" shadow="hover" v-if="orderData.tkly || orderData.tuikuan_liyou">
<!-- 退款/拒绝理由 + 商家提交图片 -->
<el-card class="info-card" shadow="hover" v-if="orderData.tkly || orderData.tuikuan_liyou || refundImages.length">
<template #header>
<div class="card-header">
<span>💰 退款/拒绝理由</span>
</div>
</template>
<div>{{ orderData.tkly || orderData.tuikuan_liyou }}</div>
<div v-if="orderData.tkly || orderData.tuikuan_liyou" style="margin-bottom: 12px;">
{{ orderData.tkly || orderData.tuikuan_liyou }}
</div>
<div v-if="refundImages.length" class="image-grid">
<el-image
v-for="(url, idx) in refundImagesFull"
:key="idx"
:src="url"
:preview-src-list="refundImagesFull"
:initial-index="idx"
fit="cover"
class="grid-image"
:preview-teleported="true"
/>
</div>
</el-card>
<!-- 打手上传图片 -->
@@ -349,6 +386,28 @@
</span>
</template>
</el-dialog>
<!-- 改抢单要求 -->
<el-dialog v-model="grabDialogVisible" title="修改订单抢单要求" width="440px" destroy-on-close>
<el-form label-width="100px">
<el-form-item label="要求类型">
<el-radio-group v-model="grabForm.yaoqiuleixing">
<el-radio :label="1">会员</el-radio>
<el-radio :label="2">押金</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="grabForm.yaoqiuleixing === 1" label="会员ID">
<el-input v-model="grabForm.huiyuan_id" placeholder="填写会员ID" maxlength="6" />
</el-form-item>
<el-form-item v-if="grabForm.yaoqiuleixing === 2" label="押金金额">
<el-input-number v-model="grabForm.yongjin" :min="0" :precision="2" :step="1" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="grabDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="operating" @click="submitGrabReq">确认修改</el-button>
</template>
</el-dialog>
</div>
</div>
</template>
@@ -360,6 +419,7 @@ import { ElMessage } from 'element-plus'
// 合并所有图标导入
import { Refresh, Money, CircleClose, Remove, Switch, Close, Warning, CircleCheck } from '@element-plus/icons-vue'
import request from '@/utils/request'
import { getMenuAccess, hasPermCode } from '@/utils/club-context'
const { proxy } = getCurrentInstance()
const route = useRoute()
@@ -367,6 +427,7 @@ const router = useRouter()
const loading = ref(false)
const operating = ref(false)
const canEditGrabReqBase = ref(false)
const dingdanId = ref('')
const username = ref(localStorage.getItem('username') || '')
@@ -390,14 +451,16 @@ const orderData = ref({
clkf: '',
chuangjianshijian: '',
genggaishijian: '',
yaoqiuleixing: null,
huiyuan_id: '',
yajin: null,
pingtai_kuozhan: null,
shangjia_kuozhan: null,
tuikuan_liyou: ''
})
const dashouImages = ref([])
const dashouImagesFull = computed(() => dashouImages.value.map(url => getFullImageUrl(url)))
const refundImages = ref([])
const getFullImageUrl = (url) => {
if (!url) return ''
@@ -405,6 +468,9 @@ const getFullImageUrl = (url) => {
return url.startsWith('http') ? url : ossUrl + url
}
const dashouImagesFull = computed(() => dashouImages.value.map(url => getFullImageUrl(url)))
const refundImagesFull = computed(() => refundImages.value.map(url => getFullImageUrl(url)))
const formatPrice = (val) => parseFloat(val || 0).toFixed(2)
const formatRemainText = (sec) => {
@@ -449,6 +515,18 @@ const showActionButtons = computed(() => {
return [1,2,4,7,8].includes(s)
})
/** 本店可处理;跨店只读(后端 can_mutate) */
const canMutateOrder = computed(() => {
const v = orderData.value?.can_mutate
return v === undefined || v === null ? true : !!v
})
const canEditGrabReq = computed(() => {
if (!canEditGrabReqBase.value || !canMutateOrder.value) return false
if (Number(orderData.value.fadanpingtai) !== 1) return false
return [1, 7].includes(Number(orderData.value.zhuangtai))
})
const copyText = (text) => {
navigator.clipboard.writeText(text).then(() => {
ElMessage.success('复制成功')
@@ -465,15 +543,62 @@ const refundDialogVisible = ref(false)
const jjtkDialogVisible = ref(false)
const jjjsDialogVisible = ref(false)
const punishDialogVisible = ref(false)
const grabDialogVisible = ref(false)
const newDashouId = ref('')
const tuikuanLiyou = ref('')
const jujueLiyou = ref('')
const jjjsLiyou = ref('')
const punishReason = ref('')
const grabForm = ref({
yaoqiuleixing: 1,
huiyuan_id: '',
yongjin: 0,
})
const refundDialogTitle = computed(() => orderData.value.zhuangtai === 4 ? '同意退款' : '立即退款')
const openGrabDialog = () => {
grabForm.value = {
yaoqiuleixing: Number(orderData.value.yaoqiuleixing) === 2 ? 2 : 1,
huiyuan_id: orderData.value.huiyuan_id || '',
yongjin: orderData.value.yajin != null ? Number(orderData.value.yajin) : 0,
}
grabDialogVisible.value = true
}
const submitGrabReq = async () => {
if (grabForm.value.yaoqiuleixing === 1 && !String(grabForm.value.huiyuan_id || '').trim()) {
ElMessage.warning('请填写会员ID')
return
}
operating.value = true
try {
const payload = {
phone: username.value,
dingdan_id: dingdanId.value,
yaoqiuleixing: grabForm.value.yaoqiuleixing,
}
if (grabForm.value.yaoqiuleixing === 1) {
payload.huiyuan_id = String(grabForm.value.huiyuan_id).trim()
} else {
payload.yongjin = grabForm.value.yongjin
}
const res = await request.post('/yonghu/kefu_gai_qiangdan_yaoqiu', payload)
if (res.code === 0) {
ElMessage.success(res.msg || '修改成功')
grabDialogVisible.value = false
fetchDetail()
} else {
ElMessage.error(res.msg || '修改失败')
}
} catch {
ElMessage.error('修改失败')
} finally {
operating.value = false
}
}
const fetchDetail = async () => {
if (!dingdanId.value) return
loading.value = true
@@ -485,6 +610,7 @@ const fetchDetail = async () => {
if (res.code === 0) {
orderData.value = res.data
dashouImages.value = res.data.dashou_images || []
refundImages.value = res.data.tuikuan_tupian || []
} else {
ElMessage.error(res.msg || '获取订单详情失败')
}
@@ -735,6 +861,7 @@ const showJjtkDialog = () => { jjtkDialogVisible.value = true; jujueLiyou.value
const showJjjsDialog = () => { jjjsDialogVisible.value = true; jjjsLiyou.value = '' }
onMounted(() => {
canEditGrabReqBase.value = hasPermCode(getMenuAccess(), '002ad')
dingdanId.value = route.params.id
if (!dingdanId.value) {
ElMessage.error('订单ID不存在')
+129 -2
View File
@@ -104,6 +104,9 @@
<!-- 订单表格 -->
<el-table :data="orderList" v-loading="loading" border stripe class="order-table">
<el-table-column prop="dingdan_id" label="订单ID" width="180" />
<el-table-column label="俱乐部" width="110" show-overflow-tooltip>
<template #default="{ row }">{{ row.club_name || row.club_id || '--' }}</template>
</el-table-column>
<el-table-column label="游戏昵称" width="120">
<template #default="{ row }">{{ row.youxi_nicheng || row.wanjiacheng || '--' }}</template>
</el-table-column>
@@ -111,6 +114,13 @@
<el-table-column prop="jiage" label="价格" width="100">
<template #default="{ row }">¥{{ formatPrice(row.jiage) }}</template>
</el-table-column>
<el-table-column label="抢单要求" width="130">
<template #default="{ row }">
<span v-if="Number(row.yaoqiuleixing) === 1">会员 {{ row.huiyuan_id || '--' }}</span>
<span v-else-if="Number(row.yaoqiuleixing) === 2">押金 ¥{{ formatPrice(row.yajin) }}</span>
<span v-else class="muted">--</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.zhuangtai)" :color="getStatusColor(row.zhuangtai)">
@@ -126,9 +136,15 @@
<span v-else class="muted">--</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click.stop="goToDetail(row.dingdan_id)">详情</el-button>
<el-button
v-if="canEditGrabReq && [1, 7].includes(Number(row.zhuangtai))"
type="warning"
link
@click.stop="openOrderGrabDialog(row)"
>改抢单要求</el-button>
</template>
</el-table-column>
</el-table>
@@ -145,6 +161,46 @@
@current-change="handlePageChange"
/>
</div>
<!-- 改单笔订单抢单要求 -->
<el-dialog v-model="orderGrabVisible" title="修改订单抢单要求" width="440px" destroy-on-close>
<el-form label-width="100px">
<el-form-item label="订单ID">
<span>{{ orderGrabForm.dingdan_id }}</span>
</el-form-item>
<el-form-item label="要求类型">
<el-radio-group v-model="orderGrabForm.yaoqiuleixing">
<el-radio :label="1">会员</el-radio>
<el-radio :label="2">押金</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="orderGrabForm.yaoqiuleixing === 1" label="会员">
<el-select
v-if="memberList.length"
v-model="orderGrabForm.huiyuan_id"
filterable
clearable
placeholder="选择会员"
style="width: 100%"
>
<el-option
v-for="m in memberList"
:key="m.huiyuan_id"
:label="`${m.jieshao || m.huiyuan_id}(${m.huiyuan_id})`"
:value="m.huiyuan_id"
/>
</el-select>
<el-input v-else v-model="orderGrabForm.huiyuan_id" placeholder="填写会员ID" maxlength="6" />
</el-form-item>
<el-form-item v-if="orderGrabForm.yaoqiuleixing === 2" label="押金金额">
<el-input-number v-model="orderGrabForm.yongjin" :min="0" :precision="2" :step="1" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="orderGrabVisible = false">取消</el-button>
<el-button type="primary" :loading="grabSubmitting" @click="submitOrderGrab">确认修改</el-button>
</template>
</el-dialog>
</div>
</div>
</template>
@@ -162,6 +218,17 @@ const route = useRoute()
const router = useRouter()
const loading = ref(false)
const hasPermission = ref(false)
const canEditGrabReq = ref(false)
const grabSubmitting = ref(false)
const memberList = ref([])
const orderGrabVisible = ref(false)
const orderGrabForm = reactive({
dingdan_id: '',
yaoqiuleixing: 1,
huiyuan_id: '',
yongjin: 0,
})
const PAGE_SIZE = 20
const pageNum = ref(1)
@@ -393,8 +460,68 @@ const getStatusType = (code) => {
watch(() => route.query, () => { restoreFromQuery(); fetchOrders() }, { deep: true })
const loadMemberList = async () => {
try {
const res = await request.post('/houtai/hthqsplxzq', {
username: localStorage.getItem('username'),
}, { skipErrorToast: true })
if (res.code === 0) {
memberList.value = res.data?.member_list || []
}
} catch {
memberList.value = []
}
}
const openOrderGrabDialog = (row) => {
orderGrabForm.dingdan_id = row.dingdan_id
orderGrabForm.yaoqiuleixing = Number(row.yaoqiuleixing) === 2 ? 2 : 1
orderGrabForm.huiyuan_id = row.huiyuan_id || ''
orderGrabForm.yongjin = row.yajin != null ? Number(row.yajin) : 0
orderGrabVisible.value = true
if (!memberList.value.length) loadMemberList()
}
const submitOrderGrab = async () => {
if (orderGrabForm.yaoqiuleixing === 1 && !String(orderGrabForm.huiyuan_id || '').trim()) {
ElMessage.warning('请填写会员ID')
return
}
if (orderGrabForm.yaoqiuleixing === 2 && (orderGrabForm.yongjin === null || orderGrabForm.yongjin === '')) {
ElMessage.warning('请填写押金金额')
return
}
grabSubmitting.value = true
try {
const payload = {
phone: localStorage.getItem('username'),
dingdan_id: orderGrabForm.dingdan_id,
yaoqiuleixing: orderGrabForm.yaoqiuleixing,
}
if (orderGrabForm.yaoqiuleixing === 1) {
payload.huiyuan_id = String(orderGrabForm.huiyuan_id).trim()
} else {
payload.yongjin = orderGrabForm.yongjin
}
const res = await request.post('/yonghu/kefu_gai_qiangdan_yaoqiu', payload)
if (res.code === 0) {
ElMessage.success(res.msg || '修改成功')
orderGrabVisible.value = false
fetchOrders()
} else {
ElMessage.error(res.msg || '修改失败')
}
} catch {
ElMessage.error('修改失败')
} finally {
grabSubmitting.value = false
}
}
onMounted(() => {
hasPermission.value = hasPermCode(getMenuAccess(), '002ab')
const access = getMenuAccess()
hasPermission.value = hasPermCode(access, '002ab')
canEditGrabReq.value = hasPermCode(access, '002ad')
if (!hasPermission.value) return
restoreFromQuery()
fetchOrderTypes()
+172 -2
View File
@@ -29,7 +29,15 @@
<!-- 顶部操作栏 -->
<div class="action-bar">
<el-button type="primary" @click="openAddDialog" :icon="Plus">添加商品</el-button>
<div class="action-left">
<el-button type="primary" @click="openAddDialog" :icon="Plus">添加商品</el-button>
<el-button
v-if="canEditGrabReq"
type="warning"
plain
@click="openBatchGrabDialog"
>批量改抢单要求</el-button>
</div>
<div class="audit-switch">
<span>显示审核专用</span>
<el-switch v-model="showAudit" @change="onAuditSwitchChange" />
@@ -282,13 +290,71 @@
<el-button type="primary" @click="submitAddProduct" :loading="submitting">确认发布</el-button>
</template>
</el-dialog>
<!-- 批量改本店某类型下商品的抢单要求 -->
<el-dialog v-model="batchGrabVisible" title="批量改抢单要求" width="480px" destroy-on-close>
<el-alert
type="warning"
:closable="false"
show-icon
title="只改当前俱乐部下所选类型商品的抢单要求(会员/押金),不改全局类型、不改已有订单、不改价格"
style="margin-bottom: 16px"
/>
<el-form label-width="100px">
<el-form-item label="商品类型">
<el-select v-model="batchGrabForm.leixing_id" placeholder="选择类型" style="width: 100%" filterable>
<el-option
v-for="t in typeListDisplay"
:key="t.id"
:label="t.jieshao || t.id"
:value="t.id"
/>
</el-select>
</el-form-item>
<el-form-item label="要求类型">
<el-radio-group v-model="batchGrabForm.yaoqiuleixing">
<el-radio :label="1">会员</el-radio>
<el-radio :label="2">押金</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="batchGrabForm.yaoqiuleixing === 1" label="会员">
<el-select
v-if="memberList.length"
v-model="batchGrabForm.huiyuan_id"
filterable
clearable
placeholder="选择会员"
style="width: 100%"
>
<el-option
v-for="m in memberList"
:key="m.huiyuan_id"
:label="`${m.jieshao || m.huiyuan_id}(${m.huiyuan_id})`"
:value="m.huiyuan_id"
/>
</el-select>
<el-input v-else v-model="batchGrabForm.huiyuan_id" placeholder="填写会员ID" maxlength="6" />
</el-form-item>
<el-form-item v-if="batchGrabForm.yaoqiuleixing === 2" label="押金金额">
<el-input-number v-model="batchGrabForm.yongjin" :min="0" :precision="2" :step="1" />
</el-form-item>
<el-form-item v-if="batchGrabPreview" label="预览">
<div>将影响 <b>{{ batchGrabPreview.affected_count }}</b> 个商品(俱乐部 {{ batchGrabPreview.club_id }})</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="batchGrabVisible = false">取消</el-button>
<el-button :loading="batchGrabSubmitting" @click="previewBatchGrab">预览数量</el-button>
<el-button type="danger" :loading="batchGrabSubmitting" :disabled="!batchGrabPreview" @click="confirmBatchGrab">确认写入</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { getAdminClubScope } from '@/utils/club-context'
import { getAdminClubScope, getMenuAccess, hasPermCode } from '@/utils/club-context'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search, CircleClose } from '@element-plus/icons-vue'
import request from '@/utils/request'
@@ -297,8 +363,19 @@ import Sortable from 'sortablejs'
const router = useRouter()
const username = ref(localStorage.getItem('username') || '')
const hasPermission = ref(true)
const canEditGrabReq = ref(false)
const isAllClubScope = computed(() => getAdminClubScope() === 'all')
const batchGrabVisible = ref(false)
const batchGrabSubmitting = ref(false)
const batchGrabPreview = ref(null)
const batchGrabForm = reactive({
leixing_id: null,
yaoqiuleixing: 1,
huiyuan_id: '',
yongjin: 0,
})
// ---------- 基础数据 ----------
const typeList = ref([]) // 原始数据,paixu 为后端倒序值(越大越靠前)
const zoneList = ref([]) // 同上
@@ -856,7 +933,94 @@ const goToDetail = (id) => {
router.push(`/product/detail/${id}`)
}
const openBatchGrabDialog = () => {
if (isAllClubScope.value) {
ElMessage.warning('请先切换到具体俱乐部后再批量修改')
return
}
batchGrabPreview.value = null
batchGrabForm.leixing_id = selectedTypeId.value || (typeListDisplay.value[0]?.id ?? null)
batchGrabForm.yaoqiuleixing = 1
batchGrabForm.huiyuan_id = ''
batchGrabForm.yongjin = 0
batchGrabVisible.value = true
}
const buildBatchGrabPayload = (confirm) => {
const payload = {
phone: username.value,
leixing_id: batchGrabForm.leixing_id,
yaoqiuleixing: batchGrabForm.yaoqiuleixing,
confirm: confirm ? 1 : 0,
}
if (batchGrabForm.yaoqiuleixing === 1) {
payload.huiyuan_id = String(batchGrabForm.huiyuan_id || '').trim()
} else {
payload.yongjin = batchGrabForm.yongjin
}
return payload
}
const previewBatchGrab = async () => {
if (!batchGrabForm.leixing_id) {
ElMessage.warning('请选择商品类型')
return
}
if (batchGrabForm.yaoqiuleixing === 1 && !String(batchGrabForm.huiyuan_id || '').trim()) {
ElMessage.warning('请选择或填写会员ID')
return
}
batchGrabSubmitting.value = true
try {
const res = await request.post('/yonghu/kefu_batch_qiangdan_yaoqiu', buildBatchGrabPayload(false))
if (res.code === 0) {
batchGrabPreview.value = res.data
ElMessage.success(`预览:将影响 ${res.data?.affected_count ?? 0} 个商品`)
} else {
ElMessage.error(res.msg || '预览失败')
}
} catch {
ElMessage.error('预览失败')
} finally {
batchGrabSubmitting.value = false
}
}
const confirmBatchGrab = async () => {
if (!batchGrabPreview.value) {
ElMessage.warning('请先预览')
return
}
try {
await ElMessageBox.confirm(
`确认将俱乐部 ${batchGrabPreview.value.club_id} 下该类型的 ${batchGrabPreview.value.affected_count} 个商品抢单要求写入?`,
'二次确认',
{ type: 'warning', confirmButtonText: '确认写入', cancelButtonText: '取消' },
)
} catch {
return
}
batchGrabSubmitting.value = true
try {
const res = await request.post('/yonghu/kefu_batch_qiangdan_yaoqiu', buildBatchGrabPayload(true))
if (res.code === 0) {
ElMessage.success(res.msg || '批量修改成功')
batchGrabVisible.value = false
batchGrabPreview.value = null
fetchBaseData()
if (selectedTypeId.value) selectType(selectedTypeId.value)
} else {
ElMessage.error(res.msg || '批量修改失败')
}
} catch {
ElMessage.error('批量修改失败')
} finally {
batchGrabSubmitting.value = false
}
}
onMounted(() => {
canEditGrabReq.value = hasPermCode(getMenuAccess(), '002ad')
fetchBaseData()
})
</script>
@@ -876,6 +1040,12 @@ onMounted(() => {
align-items: center;
margin-bottom: 20px;
}
.action-left {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.audit-switch {
display: flex;
align-items: center;