feat: 新增柜机管理功能及相关接口和页面
- 新增柜机管理页面,支持查看柜机详情和开启柜口 - 添加柜机管理相关API接口和类型定义 - 修改路由配置,添加柜机管理页面路由 - 更新WxStore,添加isCabinetAdmin状态管理 - 修改Tabbar组件,支持动态显示柜机管理入口
This commit is contained in:
parent
9e15542d82
commit
9c066c0ad9
|
@ -30,6 +30,10 @@ onMounted(() => {
|
|||
const code = urlParams.get('code') || undefined;
|
||||
const state = urlParams.get('state') || undefined;
|
||||
const corpid = urlParams.get('corpid') || undefined;
|
||||
const isAdmin = urlParams.get('isAdmin') || undefined;
|
||||
if (isAdmin == '1') {
|
||||
wxStore.setIsCabinetAdmin(true);
|
||||
}
|
||||
if (code || state) {
|
||||
wxStore.handleWxCallback({ corpid, code, state })
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { request } from '@/http/axios'
|
||||
import type { CabinetDetailResponse } from './type'
|
||||
|
||||
/** 获取智能柜详情接口 */
|
||||
export function getCabinetDetailApi() {
|
||||
return request<CabinetDetailResponse>({
|
||||
url: 'cabinet/detail',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function openCabinet(lockControlNo: number, pinNo: number) {
|
||||
return request<ApiResponseData<void>>({
|
||||
url: `cabinet/openCabinet/${lockControlNo}/${pinNo}`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
export interface CabinetDetailDTO {
|
||||
cabinetId: number
|
||||
cabinetName: string
|
||||
lockControlNo: number
|
||||
cells: CellInfoDTO[]
|
||||
}
|
||||
|
||||
export interface CellInfoDTO {
|
||||
cellId: number
|
||||
pinNo: number
|
||||
product?: ProductInfoDTO
|
||||
}
|
||||
|
||||
export interface ProductInfoDTO {
|
||||
goodsId: number
|
||||
goodsName: string
|
||||
price: number
|
||||
coverImg: string
|
||||
}
|
||||
|
||||
export type CabinetDetailResponse = ApiResponseData<CabinetDetailDTO[]>
|
|
@ -32,7 +32,7 @@ export function getOpenIdApi(params: GetOpenIdRequestParams) {
|
|||
/** 企业微信登录 */
|
||||
export function qyLogin(params: QyLoginRequestParams) {
|
||||
return request<ApiResponseData<QyLoginDTO>>({
|
||||
url: "common/login/qy",
|
||||
url: "payment/login/qy",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
|
|
|
@ -3,12 +3,16 @@ const router = useRouter()
|
|||
|
||||
const tabbarItemList = computed(() => {
|
||||
const routes = router.getRoutes()
|
||||
return routes.filter(route => route.meta.layout?.tabbar?.showTabbar).map(route => ({
|
||||
return routes.filter(route => route.meta.layout?.tabbar?.showTabbar && route.path !== '/cabinet')
|
||||
.map(route => ({
|
||||
title: route.meta.title,
|
||||
icon: route.meta.layout?.tabbar?.icon,
|
||||
path: route.path
|
||||
}))
|
||||
})
|
||||
|
||||
import { useWxStoreOutside } from '@/pinia/stores/wx'
|
||||
const wxStore = useWxStoreOutside()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -18,6 +22,15 @@ const tabbarItemList = computed(() => {
|
|||
placeholder
|
||||
safe-area-inset-bottom
|
||||
>
|
||||
<van-tabbar-item
|
||||
v-if="wxStore.isCabinetAdmin"
|
||||
key="/cabinet"
|
||||
to="/cabinet"
|
||||
icon="cluster-o"
|
||||
replace
|
||||
>
|
||||
柜机管理
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item
|
||||
v-for="item in tabbarItemList"
|
||||
:key="item.path"
|
||||
|
|
|
@ -0,0 +1,260 @@
|
|||
<template>
|
||||
<div class="cabinet-container van-safe-area-bottom">
|
||||
<van-sidebar v-model="activeCabinet" class="cabinet-sidebar" @change="onCabinetChange">
|
||||
<van-sidebar-item v-for="cabinet in cabinetList" :key="cabinet.cabinetId" :title="cabinet.cabinetName" />
|
||||
</van-sidebar>
|
||||
|
||||
<div class="locker-grid">
|
||||
<van-grid :border="false" :column-num="1">
|
||||
<van-grid-item v-for="locker in lockerList" :key="locker.lockerId" class="locker-item">
|
||||
<template #icon>
|
||||
<div class="locker-status" :class="[locker.statusClass]">
|
||||
<van-image v-if="locker.coverImg" width="100%" height="120" :src="locker.coverImg"
|
||||
fit="cover" radius="4" class="locker-image" />
|
||||
<div v-else class="status-overlay">
|
||||
<div class="status-text">
|
||||
{{ locker.statusClass === 'available' ? '空闲' : '占用' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #text>
|
||||
<div class="locker-info">
|
||||
<div class="locker-number">格口 {{ locker.lockerNumber }}</div>
|
||||
<div v-if="locker.goodsName" class="goods-info">
|
||||
<div class="goods-name van-ellipsis">{{ locker.goodsName }}</div>
|
||||
<div class="goods-price">¥{{ locker.price?.toFixed(2) }}</div>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<van-button size="small" type="primary" class="detail-btn"
|
||||
@click="showLockerDetail(locker)">
|
||||
详情
|
||||
</van-button>
|
||||
<van-button size="small" plain hairline :loading="openingLockerId === locker.lockerId"
|
||||
@click="handleOpenLocker(locker)">
|
||||
立即开启
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { getCabinetDetailApi, openCabinet } from '@/common/apis/cabinet'
|
||||
import type { CabinetDetailDTO } from '@/common/apis/cabinet/type'
|
||||
import { useWxStoreOutside } from '@/pinia/stores/wx'
|
||||
|
||||
const wxStore = useWxStoreOutside()
|
||||
|
||||
const activeCabinet = ref(0)
|
||||
const cabinetList = ref<CabinetItem[]>([])
|
||||
const lockerList = ref<LockerItem[]>([])
|
||||
const openingLockerId = ref<number | null>(null)
|
||||
const cabinetData = ref<CabinetDetailDTO[]>([]);
|
||||
|
||||
interface CabinetItem {
|
||||
cabinetId: number
|
||||
cabinetName: string
|
||||
lockControlNo: number
|
||||
}
|
||||
|
||||
interface LockerItem {
|
||||
lockerId: number
|
||||
lockerNumber: number
|
||||
status: 0 | 1
|
||||
statusClass: 'available' | 'occupied'
|
||||
goodsName?: string
|
||||
price?: number
|
||||
coverImg?: string
|
||||
}
|
||||
|
||||
// 获取柜机列表
|
||||
const loadCabinetDetail = async () => {
|
||||
try {
|
||||
const { data } = await getCabinetDetailApi();
|
||||
cabinetData.value = data;
|
||||
cabinetList.value = data.map(cabinet => ({
|
||||
cabinetId: cabinet.cabinetId,
|
||||
cabinetName: cabinet.cabinetName,
|
||||
lockControlNo: cabinet.lockControlNo
|
||||
}))
|
||||
|
||||
// 根据当前选中柜机加载格口数据
|
||||
if (data.length > 0) {
|
||||
updateLockerList(data[activeCabinet.value])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取柜机详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新格口列表数据
|
||||
const updateLockerList = (cabinet: CabinetDetailDTO) => {
|
||||
lockerList.value = cabinet.cells.map(cell => ({
|
||||
lockerId: cell.cellId,
|
||||
lockerNumber: cell.pinNo,
|
||||
status: cell.product ? 1 : 0,
|
||||
statusClass: cell.product ? 'occupied' : 'available',
|
||||
goodsName: cell.product?.goodsName,
|
||||
price: cell.product?.price,
|
||||
coverImg: cell.product?.coverImg
|
||||
}))
|
||||
}
|
||||
|
||||
// 监听侧边栏切换事件
|
||||
const onCabinetChange = (index: number) => {
|
||||
activeCabinet.value = index
|
||||
if (cabinetList.value.length > index) {
|
||||
updateLockerList(cabinetData.value[index]);
|
||||
}
|
||||
}
|
||||
|
||||
const showLockerDetail = (locker: LockerItem) => {
|
||||
// 实现详情弹窗逻辑
|
||||
}
|
||||
|
||||
const handleOpenLocker = async (locker: LockerItem) => {
|
||||
openingLockerId.value = locker.lockerId
|
||||
try {
|
||||
// 调用打开柜口接口
|
||||
await openCabinet(cabinetList.value[activeCabinet.value].lockControlNo, locker.lockerNumber)
|
||||
} catch (error) {
|
||||
console.error('打开柜口失败:', error)
|
||||
} finally {
|
||||
openingLockerId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
loadCabinetDetail()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cabinet-container {
|
||||
display: flex;
|
||||
height: calc(100vh - var(--van-tabbar-height));
|
||||
}
|
||||
|
||||
.cabinet-sidebar {
|
||||
width: 120px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.locker-grid {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
|
||||
:deep(.van-grid) {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
|
||||
.van-grid-item {
|
||||
padding: 8px;
|
||||
|
||||
.van-grid-item__content {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.locker-item {
|
||||
width: 100%;
|
||||
margin: 8px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.locker-status {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.locker-image {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.status-overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.status-text {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.locker-info {
|
||||
padding: 8px;
|
||||
|
||||
.locker-number {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
background: #f7f8fa;
|
||||
border-radius: 4px;
|
||||
|
||||
.goods-name {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.goods-price {
|
||||
font-size: 14px;
|
||||
color: #e95d5d;
|
||||
font-weight: bold;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
|
||||
.detail-btn {
|
||||
flex: 1;
|
||||
background-color: #e95d5d;
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.van-button--default) {
|
||||
color: #e95d5d;
|
||||
border-color: #e95d5d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -2,6 +2,7 @@
|
|||
import { useRouter } from 'vue-router'
|
||||
import { useWxStore } from '@/pinia/stores/wx'
|
||||
import { computed } from 'vue'
|
||||
import { publicPath } from "@/common/utils/path"
|
||||
|
||||
const router = useRouter()
|
||||
const wxStore = useWxStore()
|
||||
|
@ -19,7 +20,7 @@ const balance = computed(() => wxStore.balance)
|
|||
round
|
||||
width="80"
|
||||
height="80"
|
||||
src="/img/1.jpg"
|
||||
:src="`${publicPath}img/1.jpg`"
|
||||
class="mr-4"
|
||||
/>
|
||||
<div>
|
||||
|
|
|
@ -27,7 +27,11 @@ export const useWxStore = defineStore("wx", () => {
|
|||
balance.value = amount;
|
||||
}
|
||||
|
||||
const handleWxCallback = async (params: { corpid?: string; code?: string; state?: string }) => {
|
||||
const setIsCabinetAdmin = (isAdmin: boolean) => {
|
||||
isCabinetAdmin.value = isAdmin;
|
||||
}
|
||||
|
||||
const handleWxCallback = async (params: { corpid?: string; code?: string; state?: string; }) => {
|
||||
console.log('handleWxCallback:', params)
|
||||
if (params.code) {
|
||||
code.value = params.code
|
||||
|
@ -78,7 +82,8 @@ export const useWxStore = defineStore("wx", () => {
|
|||
}
|
||||
}
|
||||
|
||||
return { code, state, openid, corpid, userid, balance, setOpenid, setBalance, handleWxCallback }
|
||||
return { code, state, openid, corpid, userid, balance, isCabinetAdmin,
|
||||
setOpenid, setBalance, handleWxCallback, setIsCabinetAdmin }
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
|
@ -82,6 +82,25 @@ export const routes: RouteRecordRaw[] = [
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/cabinet',
|
||||
component: () => import('@/pages/cabinet/index.vue'),
|
||||
name: "Cabinet",
|
||||
meta: {
|
||||
title: '柜机管理',
|
||||
keepAlive: true,
|
||||
layout: {
|
||||
navBar: {
|
||||
showNavBar: false,
|
||||
showLeftArrow: false
|
||||
},
|
||||
tabbar: {
|
||||
showTabbar: true,
|
||||
icon: "home-o"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/me",
|
||||
component: () => import("@/pages/me/index.vue"),
|
||||
|
|
Loading…
Reference in New Issue