feat: 转盘活动页与路由放行,轮播增加点单背景/横幅选项
修复侧栏可见但点转盘被踢到板块配置的问题;Carousel 增加 boss_center 与 accept_order_banner2。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
getMenuAccess,
|
||||
getDefaultLandingPath,
|
||||
getEffectiveVisiblePaths,
|
||||
isLocallyDefinedMenuPathAllowed,
|
||||
isMenuSessionFresh,
|
||||
hasMerchantOrderPerm,
|
||||
JITUAN_API,
|
||||
@@ -230,6 +231,11 @@ const routes = [
|
||||
component: () => import('@/views/product/TypeZone.vue'),
|
||||
meta: { title: '商品类型专区管理' }
|
||||
},
|
||||
{
|
||||
path: 'product/turntable',
|
||||
component: () => import('@/views/product/Turntable.vue'),
|
||||
meta: { title: '转盘活动' }
|
||||
},
|
||||
{
|
||||
path: 'product/data',
|
||||
component: () => import('@/views/product/Data.vue'),
|
||||
@@ -358,6 +364,10 @@ function isRouteAllowed(path, access) {
|
||||
return visiblePaths.some((p) => parents.includes(p))
|
||||
}
|
||||
}
|
||||
// 新菜单仅写在前端时:侧栏能显示则允许进入,避免被踢到板块配置等默认页
|
||||
if (isLocallyDefinedMenuPathAllowed(path)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ const CLUB_SCOPE_KEY = 'admin_club_scope';
|
||||
const CLUB_CONTEXT_KEY = 'admin_club_context';
|
||||
const MENU_ACCESS_KEY = 'kefu_menu_access';
|
||||
const MENU_SESSION_KEY = 'kefu_menu_session_v';
|
||||
const MENU_SESSION_VERSION = '24';
|
||||
const MENU_SESSION_VERSION = '25';
|
||||
const DEFAULT_CLUB_ID = 'xq';
|
||||
|
||||
let menuAccessInflight = null;
|
||||
@@ -146,8 +146,9 @@ const KEFU_MENU_ROWS = [
|
||||
{ page_id: 'gongdan', parent_id: '', path: '/gongdan', sort_order: 81, perm_codes: ['gdck666', 'gdcl666'] },
|
||||
{ page_id: 'product.list', parent_id: 'product', path: '/product/list', sort_order: 91, perm_codes: ['2200a'] },
|
||||
{ page_id: 'product.type-zone', parent_id: 'product', path: '/product/type-zone', sort_order: 92, perm_codes: ['7007a'] },
|
||||
{ page_id: 'product.data', parent_id: 'product', path: '/product/data', sort_order: 93, perm_codes: ['2200a'] },
|
||||
{ page_id: 'product.fake-orders', parent_id: 'product', path: '/product/fake-orders', sort_order: 94, perm_codes: ['2200a'] },
|
||||
{ page_id: 'product.turntable', parent_id: 'product', path: '/product/turntable', sort_order: 93, perm_codes: ['2200a'] },
|
||||
{ page_id: 'product.data', parent_id: 'product', path: '/product/data', sort_order: 94, perm_codes: ['2200a'] },
|
||||
{ page_id: 'product.fake-orders', parent_id: 'product', path: '/product/fake-orders', sort_order: 95, perm_codes: ['2200a'] },
|
||||
{ page_id: 'member.list', parent_id: 'member', path: '/member/list', sort_order: 101, perm_codes: ['3300a'] },
|
||||
{ page_id: 'member.data', parent_id: 'member', path: '/member/data', sort_order: 102, perm_codes: ['3300a'] },
|
||||
{ page_id: 'miniapp.carousel', parent_id: 'miniapp', path: '/miniapp/carousel', sort_order: 111, perm_codes: ['8080a'] },
|
||||
@@ -170,7 +171,7 @@ const KEFU_MENU_PARENT_IDS = {
|
||||
'cross-platform': ['cross-platform.order', 'cross-platform.settings', 'cross-platform.data', 'cross-platform.pre'],
|
||||
user: ['user.player', 'user.guanli', 'user.shangjia', 'user.zuzhang', 'user.identity-tag'],
|
||||
withdraw: ['withdraw.audit', 'withdraw.data', 'withdraw.settings'],
|
||||
product: ['product.list', 'product.type-zone', 'product.data', 'product.fake-orders'],
|
||||
product: ['product.list', 'product.type-zone', 'product.turntable', 'product.data', 'product.fake-orders'],
|
||||
member: ['member.list', 'member.data'],
|
||||
miniapp: ['miniapp.carousel', 'miniapp.assets', 'miniapp.popup', 'miniapp.leixing', 'miniapp.kaohe-tags', 'miniapp.dashou-exam', 'miniapp.withdraw-settings'],
|
||||
shop: ['shop.list', 'shop.withdraw', 'shop.products'],
|
||||
@@ -205,6 +206,19 @@ export function getEffectiveVisiblePaths(access = null) {
|
||||
return Array.isArray(paths) ? paths : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容尚未 seed 到服务端的新菜单:本地已定义且 canShowMenuPage 通过即可进路由。
|
||||
* 避免「侧栏看得见、点进去却被踢到默认页(如板块配置)」。
|
||||
*/
|
||||
export function isLocallyDefinedMenuPathAllowed(path) {
|
||||
if (!path) return false;
|
||||
const row = KEFU_MENU_ROWS.find(
|
||||
(r) => r.path && (path === r.path || path.startsWith(`${r.path}/`)),
|
||||
);
|
||||
if (!row) return false;
|
||||
return canShowMenuPage(row.page_id);
|
||||
}
|
||||
|
||||
export function hasPermCode(access, ...codes) {
|
||||
if (!access || !codes.length) return false;
|
||||
const list = access.permission_codes || [];
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
</template>
|
||||
<el-menu-item v-if="canShow('product.list')" index="/product/list">商品列表</el-menu-item>
|
||||
<el-menu-item v-if="canShow('product.type-zone')" index="/product/type-zone">商品类型专区管理</el-menu-item>
|
||||
<el-menu-item v-if="canShow('product.turntable')" index="/product/turntable">转盘活动</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')" index="/product/fake-orders">假单管理</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
@@ -109,9 +109,11 @@ const hasPermission = ref(true)
|
||||
const pageKey = ref('order_pool')
|
||||
const pageOptions = ref([
|
||||
{ key: 'order_pool', label: '抢单池' },
|
||||
{ key: 'accept_order', label: '点单页' },
|
||||
{ key: 'accept_order', label: '点单页主轮播' },
|
||||
{ key: 'accept_order_banner2', label: '点单页横幅轮播' },
|
||||
{ key: 'merchant_home', label: '商家首页' },
|
||||
{ key: 'dashou_center', label: '打手个人中心背景' },
|
||||
{ key: 'boss_center', label: '点单个人中心背景' },
|
||||
])
|
||||
const gonggaoTitle = computed(() => {
|
||||
const opt = pageOptions.value.find((o) => o.key === pageKey.value)
|
||||
@@ -169,9 +171,11 @@ const fetchLunbo = async () => {
|
||||
if (!pageOptions.value.length) {
|
||||
pageOptions.value = [
|
||||
{ key: 'order_pool', label: '抢单池' },
|
||||
{ key: 'accept_order', label: '点单页' },
|
||||
{ key: 'accept_order', label: '点单页主轮播' },
|
||||
{ key: 'accept_order_banner2', label: '点单页横幅轮播' },
|
||||
{ key: 'merchant_home', label: '商家首页' },
|
||||
{ key: 'dashou_center', label: '打手个人中心背景' },
|
||||
{ key: 'boss_center', label: '点单个人中心背景' },
|
||||
]
|
||||
}
|
||||
} else if (res.code === 403) {
|
||||
|
||||
269
src/views/product/Turntable.vue
Normal file
269
src/views/product/Turntable.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div class="zhuanpan-page">
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="tip"
|
||||
title="转盘活动按当前俱乐部隔离"
|
||||
description="切换顶部俱乐部后,此处活动/奖品只属于该俱乐部。任意俱乐部都可配置;小程序端接好转盘接口即可使用。"
|
||||
/>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="openHuodong()">新建转盘活动</el-button>
|
||||
<el-button @click="loadHuodong">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="huodongList" v-loading="loading" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="mingzi" label="活动名" min-width="140" />
|
||||
<el-table-column prop="shangpin_id" label="购票商品ID" width="120" />
|
||||
<el-table-column label="启用" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.qiyong ? 'success' : 'info'">{{ row.qiyong ? '启用' : '停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="beizhu" label="备注" min-width="120" />
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openHuodong(row)">编辑</el-button>
|
||||
<el-button link type="primary" @click="openJiangpin(row)">奖品/概率</el-button>
|
||||
<el-button link type="warning" @click="toggleHuodong(row)">{{ row.qiyong ? '停用' : '启用' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 活动弹窗 -->
|
||||
<el-dialog v-model="hdVisible" :title="hdForm.id ? '编辑活动' : '新建活动'" width="480px">
|
||||
<el-form label-width="110px">
|
||||
<el-form-item label="活动名称">
|
||||
<el-input v-model="hdForm.mingzi" maxlength="64" />
|
||||
</el-form-item>
|
||||
<el-form-item label="购票商品ID">
|
||||
<el-input-number v-model="hdForm.shangpin_id" :min="1" controls-position="right" />
|
||||
<div class="form-tip">请填本俱乐部商品 ID;保存后会标记该商品为转盘购票商品。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="hdForm.beizhu" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="hdForm.qiyong" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="hdVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="saveHuodong">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 奖品弹窗 -->
|
||||
<el-dialog v-model="jpVisible" :title="'奖品配置 - ' + (currentHd?.mingzi || '')" width="720px">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" size="small" @click="openJpEdit()">添加奖品</el-button>
|
||||
<el-button size="small" @click="loadJiangpin">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="jiangpinList" v-loading="jpLoading" border size="small">
|
||||
<el-table-column prop="paixu" label="格子序" width="80" />
|
||||
<el-table-column prop="mingzi" label="名称" min-width="120" />
|
||||
<el-table-column prop="quanzhong" label="权重" width="80" />
|
||||
<el-table-column prop="shangpin_id" label="履约商品ID" width="110" />
|
||||
<el-table-column label="谢谢参与" width="90">
|
||||
<template #default="{ row }">{{ row.shi_xiexie ? '是' : '否' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openJpEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="delJiangpin(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="jpEditVisible" :title="jpForm.id ? '编辑奖品' : '添加奖品'" width="480px">
|
||||
<el-form label-width="110px">
|
||||
<el-form-item label="名称"><el-input v-model="jpForm.mingzi" /></el-form-item>
|
||||
<el-form-item label="格子序号"><el-input-number v-model="jpForm.paixu" :min="0" :max="20" /></el-form-item>
|
||||
<el-form-item label="中奖权重"><el-input-number v-model="jpForm.quanzhong" :min="0" /></el-form-item>
|
||||
<el-form-item label="履约商品ID">
|
||||
<el-input-number v-model="jpForm.shangpin_id" :min="0" />
|
||||
<div class="form-tip">中奖后生成抢单池订单的商品;谢谢参与可留空。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="图标URL"><el-input v-model="jpForm.tupian_url" placeholder="相对路径或完整URL" /></el-form-item>
|
||||
<el-form-item label="谢谢参与"><el-switch v-model="jpForm.shi_xiexie" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="jpEditVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="saveJiangpin">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
const jpLoading = ref(false)
|
||||
const saving = ref(false)
|
||||
const huodongList = ref([])
|
||||
const jiangpinList = ref([])
|
||||
const currentHd = ref(null)
|
||||
|
||||
const hdVisible = ref(false)
|
||||
const hdForm = reactive({ id: null, mingzi: '幸运转盘', shangpin_id: null, beizhu: '', qiyong: true })
|
||||
|
||||
const jpVisible = ref(false)
|
||||
const jpEditVisible = ref(false)
|
||||
const jpForm = reactive({
|
||||
id: null, mingzi: '', paixu: 0, quanzhong: 1, shangpin_id: null, tupian_url: '', shi_xiexie: false
|
||||
})
|
||||
|
||||
async function loadHuodong() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await request.post('/shangpin/adzhuanpanhd', { action: 'list' })
|
||||
if (res.code === 0) huodongList.value = res.data || []
|
||||
else ElMessage.error(res.msg || '加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openHuodong(row) {
|
||||
if (row) {
|
||||
Object.assign(hdForm, {
|
||||
id: row.id,
|
||||
mingzi: row.mingzi,
|
||||
shangpin_id: row.shangpin_id,
|
||||
beizhu: row.beizhu || '',
|
||||
qiyong: !!row.qiyong
|
||||
})
|
||||
} else {
|
||||
Object.assign(hdForm, { id: null, mingzi: '幸运转盘', shangpin_id: null, beizhu: '', qiyong: true })
|
||||
}
|
||||
hdVisible.value = true
|
||||
}
|
||||
|
||||
async function saveHuodong() {
|
||||
if (!hdForm.shangpin_id) {
|
||||
ElMessage.warning('请填写购票商品ID')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const res = await request.post('/shangpin/adzhuanpanhd', {
|
||||
action: 'save',
|
||||
id: hdForm.id,
|
||||
mingzi: hdForm.mingzi,
|
||||
shangpin_id: hdForm.shangpin_id,
|
||||
beizhu: hdForm.beizhu,
|
||||
qiyong: hdForm.qiyong
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('已保存')
|
||||
hdVisible.value = false
|
||||
loadHuodong()
|
||||
} else ElMessage.error(res.msg || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleHuodong(row) {
|
||||
const res = await request.post('/shangpin/adzhuanpanhd', { action: 'toggle', id: row.id })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('已更新')
|
||||
loadHuodong()
|
||||
} else ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
|
||||
async function openJiangpin(row) {
|
||||
currentHd.value = row
|
||||
jpVisible.value = true
|
||||
await loadJiangpin()
|
||||
}
|
||||
|
||||
async function loadJiangpin() {
|
||||
if (!currentHd.value) return
|
||||
jpLoading.value = true
|
||||
try {
|
||||
const res = await request.post('/shangpin/adzhuanpanjp', {
|
||||
action: 'list',
|
||||
huodong_id: currentHd.value.id
|
||||
})
|
||||
if (res.code === 0) jiangpinList.value = res.data || []
|
||||
else ElMessage.error(res.msg || '加载失败')
|
||||
} finally {
|
||||
jpLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openJpEdit(row) {
|
||||
if (row) {
|
||||
Object.assign(jpForm, {
|
||||
id: row.id,
|
||||
mingzi: row.mingzi,
|
||||
paixu: row.paixu,
|
||||
quanzhong: row.quanzhong,
|
||||
shangpin_id: row.shangpin_id,
|
||||
tupian_url: row.tupian_url || '',
|
||||
shi_xiexie: !!row.shi_xiexie
|
||||
})
|
||||
} else {
|
||||
Object.assign(jpForm, {
|
||||
id: null, mingzi: '', paixu: jiangpinList.value.length, quanzhong: 1,
|
||||
shangpin_id: null, tupian_url: '', shi_xiexie: false
|
||||
})
|
||||
}
|
||||
jpEditVisible.value = true
|
||||
}
|
||||
|
||||
async function saveJiangpin() {
|
||||
if (!currentHd.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
const res = await request.post('/shangpin/adzhuanpanjp', {
|
||||
action: 'save',
|
||||
huodong_id: currentHd.value.id,
|
||||
id: jpForm.id,
|
||||
mingzi: jpForm.mingzi,
|
||||
paixu: jpForm.paixu,
|
||||
quanzhong: jpForm.quanzhong,
|
||||
shangpin_id: jpForm.shangpin_id || null,
|
||||
tupian_url: jpForm.tupian_url,
|
||||
shi_xiexie: jpForm.shi_xiexie
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('已保存')
|
||||
jpEditVisible.value = false
|
||||
loadJiangpin()
|
||||
} else ElMessage.error(res.msg || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function delJiangpin(row) {
|
||||
await ElMessageBox.confirm('确认删除该奖品?', '提示')
|
||||
const res = await request.post('/shangpin/adzhuanpanjp', {
|
||||
action: 'delete',
|
||||
huodong_id: currentHd.value.id,
|
||||
id: row.id
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('已删除')
|
||||
loadJiangpin()
|
||||
} else ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
|
||||
onMounted(loadHuodong)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.zhuanpan-page { padding: 8px; }
|
||||
.tip { margin-bottom: 12px; }
|
||||
.toolbar { margin-bottom: 12px; display: flex; gap: 8px; }
|
||||
.form-tip { color: #909399; font-size: 12px; line-height: 1.4; margin-top: 4px; }
|
||||
</style>
|
||||
@@ -213,6 +213,9 @@
|
||||
<el-form-item label="专区名称" prop="mingzi">
|
||||
<el-input v-model="zoneForm.mingzi" maxlength="45" />
|
||||
</el-form-item>
|
||||
<el-form-item label="专区图标">
|
||||
<el-input v-model="zoneForm.tupian_url" placeholder="相对路径或完整URL(点单端横滑图标)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属类型" prop="leixing_id">
|
||||
<el-select v-model="zoneForm.leixing_id" placeholder="请选择类型">
|
||||
<el-option
|
||||
@@ -585,7 +588,7 @@ const zoneDialogVisible = ref(false)
|
||||
const zoneDialogTitle = ref('')
|
||||
const isZoneEdit = ref(false)
|
||||
const originalZone = ref({})
|
||||
const zoneForm = reactive({ id: null, mingzi: '', leixing_id: null, shenhezhuangtai: 1 })
|
||||
const zoneForm = reactive({ id: null, mingzi: '', leixing_id: null, shenhezhuangtai: 1, tupian_url: '' })
|
||||
const zoneRules = {
|
||||
mingzi: [{ required: true, message: '请输入专区名称', trigger: 'blur' }],
|
||||
leixing_id: [{ required: true, message: '请选择所属类型', trigger: 'change' }]
|
||||
@@ -612,12 +615,13 @@ const editZone = (zone) => {
|
||||
id: zone.id,
|
||||
mingzi: zone.mingzi,
|
||||
leixing_id: zone.leixing_id,
|
||||
shenhezhuangtai: Number(zone.shenhezhuangtai)
|
||||
shenhezhuangtai: Number(zone.shenhezhuangtai),
|
||||
tupian_url: zone.tupian_url || ''
|
||||
})
|
||||
zoneDialogVisible.value = true
|
||||
}
|
||||
const resetZoneForm = () => {
|
||||
Object.assign(zoneForm, { id: null, mingzi: '', leixing_id: null, shenhezhuangtai: 1 })
|
||||
Object.assign(zoneForm, { id: null, mingzi: '', leixing_id: null, shenhezhuangtai: 1, tupian_url: '' })
|
||||
zoneFormRef.value?.clearValidate()
|
||||
}
|
||||
const submitZone = async () => {
|
||||
@@ -646,7 +650,8 @@ const submitZone = async () => {
|
||||
id: zoneForm.id,
|
||||
mingzi: zoneForm.mingzi,
|
||||
leixing_id: zoneForm.leixing_id,
|
||||
shenhezhuangtai: zoneForm.shenhezhuangtai
|
||||
shenhezhuangtai: zoneForm.shenhezhuangtai,
|
||||
tupian_url: zoneForm.tupian_url || ''
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(isZoneEdit.value ? '修改成功' : '添加成功')
|
||||
|
||||
Reference in New Issue
Block a user