feat: 假单管理后台页面(商品管理权限)
商品管理菜单下新增假单 CRUD 页,按俱乐部配置各商品类型假单,支持头像上传。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -228,6 +228,11 @@ const routes = [
|
||||
component: () => import('@/views/product/Data.vue'),
|
||||
meta: { title: '商品数据分析' }
|
||||
},
|
||||
{
|
||||
path: 'product/fake-orders',
|
||||
component: () => import('@/views/product/FakeGrabOrderManage.vue'),
|
||||
meta: { title: '假单管理' }
|
||||
},
|
||||
{
|
||||
path: 'product/detail/:id',
|
||||
component: () => import('@/views/product/Detail.vue'),
|
||||
|
||||
@@ -317,6 +317,9 @@ export const JITUAN_API = {
|
||||
miniappIconList: '/jituan/houtai/miniapp-icon-list',
|
||||
miniappIconManage: '/jituan/houtai/miniapp-icon-manage',
|
||||
pindaoManage: '/jituan/houtai/pindao-manage',
|
||||
fakeGrabOrderList: '/jituan/houtai/fake-grab-order-list',
|
||||
fakeGrabOrderManage: '/jituan/houtai/fake-grab-order-manage',
|
||||
fakeGrabOrderAvatar: '/jituan/houtai/fake-grab-order-avatar',
|
||||
clubLeixingList: '/jituan/houtai/club-leixing-list',
|
||||
clubLeixingCatalog: '/jituan/houtai/club-leixing-catalog',
|
||||
clubLeixingEnable: '/jituan/houtai/club-leixing-enable',
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
<el-menu-item v-if="canShow('product.list') && isAllClubScope" index="/product/list">商品列表</el-menu-item>
|
||||
<el-menu-item v-if="canShow('product.type-zone') && isAllClubScope" index="/product/type-zone">商品类型专区管理</el-menu-item>
|
||||
<el-menu-item v-if="canShow('product.data') && isAllClubScope" index="/product/data">商品数据分析</el-menu-item>
|
||||
<el-menu-item v-if="canShow('product.fake-orders') && !isAllClubScope" index="/product/fake-orders">假单管理</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 会员管理 -->
|
||||
|
||||
522
src/views/product/FakeGrabOrderManage.vue
Normal file
522
src/views/product/FakeGrabOrderManage.vue
Normal file
@@ -0,0 +1,522 @@
|
||||
<template>
|
||||
<div class="fake-order-page">
|
||||
<div v-if="!hasPermission" class="no-permission">
|
||||
<div class="no-permission-icon">🔒</div>
|
||||
<div class="no-permission-text">您无权访问此页面</div>
|
||||
<div class="no-permission-desc">需要商品管理权限(2200a)</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isAllClubScope" class="scope-block">
|
||||
<el-alert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="请切换到具体俱乐部"
|
||||
description="假单按俱乐部独立配置。请先在顶栏选择具体小程序/俱乐部,不要用「集团汇总」。"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<ClubScopeHint />
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="tip"
|
||||
title="假单说明"
|
||||
description="无会员/低押金打手在抢单大厅仅能看到假单(不可真正接单)。按商品类型 Tab 展示:有什么类型就配什么类型的假单。所有全局商品类型均可绑定。"
|
||||
/>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-select
|
||||
v-model="filterLeixingId"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="全部商品类型"
|
||||
class="dark-select"
|
||||
style="width: 220px"
|
||||
@change="search"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in leixingOptions"
|
||||
:key="opt.id"
|
||||
:label="`${opt.jieshao || '类型'} (ID ${opt.id})`"
|
||||
:value="opt.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button type="primary" @click="openCreate">新建假单</el-button>
|
||||
<el-button @click="loadList" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap" v-loading="loading">
|
||||
<el-table :data="list" border class="fake-order-table" row-key="order_id">
|
||||
<el-table-column label="头像" width="72">
|
||||
<template #default="{ row }">
|
||||
<el-image :src="fullUrl(row.paidan_touxiang)" fit="cover" class="avatar-thumb" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="order_id" label="订单ID" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column label="商品类型" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.leixing_name }}</div>
|
||||
<div class="sub">ID {{ row.leixing_id }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格(打手分红)" width="120">
|
||||
<template #default="{ row }">¥{{ formatMoney(row.amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="platform_label" label="平台" width="90" />
|
||||
<el-table-column prop="shangjia_nicheng" label="商家名" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="优质" width="70">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_youzhi_shangjia" size="small" type="warning">是</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="jieshao" label="介绍" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="beizhu" label="备注" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="sort_order" label="排序" width="70" />
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" link type="danger" @click="confirmDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pager">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="loadList"
|
||||
@size-change="onSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑假单' : '新建假单'"
|
||||
width="680px"
|
||||
destroy-on-close
|
||||
@close="resetForm"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
|
||||
<el-form-item label="商品类型" prop="leixing_id">
|
||||
<el-select v-model="form.leixing_id" filterable placeholder="选择商品类型" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in leixingOptions"
|
||||
:key="opt.id"
|
||||
:label="`${opt.jieshao || '类型'} (ID ${opt.id})`"
|
||||
:value="opt.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="价格(元)" prop="amount">
|
||||
<el-input-number v-model="form.amount" :min="0" :precision="2" :step="1" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发单平台" prop="fadan_pingtai">
|
||||
<el-radio-group v-model="form.fadan_pingtai">
|
||||
<el-radio :value="1">平台发单</el-radio>
|
||||
<el-radio :value="2">商家发单</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<template v-if="form.fadan_pingtai === 2">
|
||||
<el-form-item label="商家名称">
|
||||
<el-input v-model="form.shangjia_nicheng" maxlength="50" placeholder="商家发单时展示" />
|
||||
</el-form-item>
|
||||
<el-form-item label="优质商家">
|
||||
<el-switch v-model="form.is_youzhi_shangjia" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="订单介绍">
|
||||
<el-input v-model="form.jieshao" type="textarea" :rows="4" placeholder="抢单大厅展示的介绍文案" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单备注">
|
||||
<el-input v-model="form.beizhu" type="textarea" :rows="2" placeholder="选填" />
|
||||
</el-form-item>
|
||||
<el-form-item label="派单用户ID">
|
||||
<el-input v-model="form.paidan_yonghuid" maxlength="32" placeholder="选填,展示用" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort_order" :min="0" :max="9999" style="width: 100%" />
|
||||
<span class="hint">数值越大越靠前(打乱前基准顺序)</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="展示头像">
|
||||
<div class="avatar-upload">
|
||||
<el-image v-if="form.paidan_touxiang" :src="fullUrl(form.paidan_touxiang)" fit="cover" class="avatar-preview" />
|
||||
<el-upload
|
||||
v-if="isEdit"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept="image/jpeg,image/png,image/jpg,image/webp"
|
||||
:on-change="uploadAvatar"
|
||||
>
|
||||
<el-button size="small" type="primary" :loading="uploading">上传头像</el-button>
|
||||
</el-upload>
|
||||
<span v-else-if="!form.paidan_touxiang" class="hint">创建后可编辑并上传</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isEdit" label="订单ID">
|
||||
<el-input :model-value="form.order_id" disabled />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
import ClubScopeHint from '@/components/ClubScopeHint.vue'
|
||||
import { JITUAN_API, getAdminClubScope } from '@/utils/club-context'
|
||||
|
||||
const username = ref(localStorage.getItem('username') || '')
|
||||
const hasPermission = ref(true)
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const uploading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const leixingOptions = ref([])
|
||||
const filterLeixingId = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref(null)
|
||||
const ossBaseUrl = ref(window.$ossURL || '')
|
||||
|
||||
const isAllClubScope = computed(() => getAdminClubScope() === 'all')
|
||||
|
||||
const defaultForm = () => ({
|
||||
order_id: '',
|
||||
leixing_id: '',
|
||||
amount: 0,
|
||||
fadan_pingtai: 1,
|
||||
shangjia_nicheng: '',
|
||||
is_youzhi_shangjia: false,
|
||||
jieshao: '',
|
||||
beizhu: '',
|
||||
paidan_yonghuid: '',
|
||||
paidan_touxiang: '',
|
||||
sort_order: 0,
|
||||
})
|
||||
|
||||
const form = ref(defaultForm())
|
||||
|
||||
const rules = {
|
||||
leixing_id: [{ required: true, message: '请选择商品类型', trigger: 'change' }],
|
||||
amount: [{ required: true, message: '请输入价格', trigger: 'blur' }],
|
||||
fadan_pingtai: [{ required: true, message: '请选择平台', trigger: 'change' }],
|
||||
}
|
||||
|
||||
function fullUrl(path) {
|
||||
if (!path) return ''
|
||||
if (path.startsWith('http')) return path
|
||||
return ossBaseUrl.value + path
|
||||
}
|
||||
|
||||
function formatMoney(val) {
|
||||
if (val === undefined || val === null || val === '') return '0.00'
|
||||
return Number(val).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
if (isAllClubScope.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await request.post(JITUAN_API.fakeGrabOrderList, {
|
||||
username: username.value,
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
leixing_id: filterLeixingId.value || undefined,
|
||||
})
|
||||
if (res.code === 403) {
|
||||
hasPermission.value = false
|
||||
return
|
||||
}
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
if (data.scope_error) {
|
||||
ElMessage.warning(data.scope_error)
|
||||
list.value = []
|
||||
return
|
||||
}
|
||||
list.value = data.list || []
|
||||
total.value = data.total || 0
|
||||
leixingOptions.value = data.leixing_options || []
|
||||
} else {
|
||||
ElMessage.error(res.msg || '加载失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function search() {
|
||||
page.value = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
function onSizeChange() {
|
||||
page.value = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
isEdit.value = false
|
||||
form.value = defaultForm()
|
||||
if (filterLeixingId.value) {
|
||||
form.value.leixing_id = filterLeixingId.value
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
isEdit.value = true
|
||||
form.value = {
|
||||
order_id: row.order_id,
|
||||
leixing_id: row.leixing_id,
|
||||
amount: Number(row.amount || 0),
|
||||
fadan_pingtai: row.fadan_pingtai || 1,
|
||||
shangjia_nicheng: row.shangjia_nicheng || '',
|
||||
is_youzhi_shangjia: Boolean(row.is_youzhi_shangjia),
|
||||
jieshao: row.jieshao || '',
|
||||
beizhu: row.beizhu || '',
|
||||
paidan_yonghuid: row.paidan_yonghuid || '',
|
||||
paidan_touxiang: row.paidan_touxiang || '',
|
||||
sort_order: row.sort_order || 0,
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.value = defaultForm()
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (form.value.amount < 0) {
|
||||
ElMessage.error('价格不能为负')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
username: username.value,
|
||||
action: isEdit.value ? 'update' : 'create',
|
||||
leixing_id: form.value.leixing_id,
|
||||
amount: form.value.amount,
|
||||
fadan_pingtai: form.value.fadan_pingtai,
|
||||
jieshao: form.value.jieshao,
|
||||
beizhu: form.value.beizhu,
|
||||
paidan_yonghuid: form.value.paidan_yonghuid,
|
||||
sort_order: form.value.sort_order,
|
||||
}
|
||||
if (form.value.fadan_pingtai === 2) {
|
||||
payload.shangjia_nicheng = form.value.shangjia_nicheng
|
||||
payload.is_youzhi_shangjia = form.value.is_youzhi_shangjia
|
||||
}
|
||||
if (isEdit.value) {
|
||||
payload.order_id = form.value.order_id
|
||||
}
|
||||
const res = await request.post(JITUAN_API.fakeGrabOrderManage, payload)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
await loadList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '保存失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadAvatar(file) {
|
||||
if (!file?.raw || !form.value.order_id) return
|
||||
uploading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('username', username.value)
|
||||
fd.append('order_id', form.value.order_id)
|
||||
fd.append('file', file.raw)
|
||||
const res = await request.post(JITUAN_API.fakeGrabOrderAvatar, fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
if (res.code === 0) {
|
||||
form.value.paidan_touxiang = res.data?.paidan_touxiang || ''
|
||||
ElMessage.success('头像已更新')
|
||||
await loadList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '上传失败')
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('上传失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除假单 ${row.order_id} 吗?`, '确认删除', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await request.post(JITUAN_API.fakeGrabOrderManage, {
|
||||
username: username.value,
|
||||
action: 'delete',
|
||||
order_id: row.order_id,
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('已删除')
|
||||
await loadList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadList)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fake-order-page {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
color: #c0e0ff;
|
||||
}
|
||||
.tip { margin-bottom: 16px; }
|
||||
.scope-block { padding: 24px 0; }
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.table-wrap {
|
||||
background: rgba(18, 25, 35, 0.85);
|
||||
border: 1px solid rgba(64, 158, 255, 0.15);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
.avatar-thumb {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
.avatar-preview {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.avatar-upload {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.sub {
|
||||
font-size: 12px;
|
||||
color: #9ec5ff;
|
||||
}
|
||||
.hint {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
color: #8fb8e8;
|
||||
}
|
||||
.pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.no-permission {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
}
|
||||
.no-permission-icon { font-size: 48px; }
|
||||
.no-permission-text { font-size: 20px; color: #6cc3e8; margin-top: 12px; }
|
||||
.no-permission-desc { color: #8fb8e8; margin-top: 8px; }
|
||||
|
||||
.fake-order-table :deep(.el-table) {
|
||||
--el-table-bg-color: transparent;
|
||||
--el-table-header-bg-color: rgba(20, 30, 45, 0.95);
|
||||
--el-table-text-color: #ffffff;
|
||||
--el-table-header-text-color: #9ec5ff;
|
||||
--el-table-border-color: rgba(64, 158, 255, 0.12);
|
||||
background: transparent;
|
||||
}
|
||||
.fake-order-table :deep(.el-table th.el-table__cell),
|
||||
.fake-order-table :deep(.el-table td.el-table__cell) {
|
||||
background: rgba(12, 18, 28, 0.55) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.fake-order-table :deep(.el-table__body tr:hover > td.el-table__cell) {
|
||||
background: rgba(64, 158, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.toolbar :deep(.el-input__wrapper),
|
||||
.toolbar :deep(.el-select .el-input__wrapper),
|
||||
:deep(.el-dialog .el-input__wrapper),
|
||||
:deep(.el-dialog .el-select .el-input__wrapper),
|
||||
:deep(.el-dialog .el-input-number .el-input__wrapper),
|
||||
:deep(.el-dialog .el-textarea__inner) {
|
||||
background: #0a121c !important;
|
||||
border: 1px solid #2a6f8f !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
:deep(.el-dialog .el-input__inner),
|
||||
:deep(.el-dialog .el-textarea__inner),
|
||||
:deep(.el-dialog .el-input-number__input) {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
:deep(.el-dialog) {
|
||||
background: rgba(18, 25, 35, 0.96);
|
||||
border: 1px solid #2a6f8f;
|
||||
}
|
||||
:deep(.el-dialog__title) {
|
||||
color: #6cc3e8;
|
||||
}
|
||||
:deep(.el-form-item__label) {
|
||||
color: #9ec5ff !important;
|
||||
}
|
||||
.pager :deep(.el-pagination .el-select .el-input__wrapper),
|
||||
.pager :deep(.el-pagination .btn-prev),
|
||||
.pager :deep(.el-pagination .btn-next),
|
||||
.pager :deep(.el-pagination .el-pager li) {
|
||||
background: #0a121c !important;
|
||||
border: 1px solid #2a6f8f !important;
|
||||
color: #9ec5ff !important;
|
||||
}
|
||||
.pager :deep(.el-pagination__total) {
|
||||
color: #9ec5ff !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user