小如重构
This commit is contained in:
1 parent
a984ca9574
commit
54bee11cd4
20 files changed
+1835
-409
No files matched your search
@@ -2,12 +2,14 @@ import request from '@/config/axios'
|
||||
|
||||
// 货道 VO
|
||||
export interface ChannelVO {
|
||||
id: number // 表编号,自增
|
||||
name: string // 名称
|
||||
deviceId: string // 设备IMEI
|
||||
cabinetChannel: string // 当前使用的货道或柜门编号
|
||||
maxNum: number // 最大货物数
|
||||
channelLine: number // 货道行号
|
||||
id?: number // 表编号,自增
|
||||
name?: string // 名称
|
||||
deviceId?: number | string // 设备ID(number)或设备IMEI(string,兼容字段)
|
||||
deviceImei?: string // 设备IMEI(兼容字段)
|
||||
cabinetChannel?: string // 当前使用的货道或柜门编号
|
||||
maxNum?: number // 最大货物数
|
||||
channelLine?: number // 货道行号
|
||||
createTime?: string // 创建时间
|
||||
}
|
||||
|
||||
// 货道 API
|
||||
@@ -40,5 +42,15 @@ export const ChannelApi = {
|
||||
// 导出货道 Excel
|
||||
exportChannel: async (params) => {
|
||||
return await request.download({ url: `/product/channel/export-excel`, params })
|
||||
},
|
||||
|
||||
// 测试货道
|
||||
testChannel: async (id: number) => {
|
||||
return await request.post({ url: `/product/channel/channel-test?id=` + id })
|
||||
},
|
||||
|
||||
// 批量新增货道
|
||||
batchCreateChannel: async (data: { channels: ChannelVO[] }) => {
|
||||
return await request.post({ url: `/product/channel/batch-create`, data })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import request from '@/config/axios'
|
||||
import type { Sku, Spu } from './spu'
|
||||
|
||||
// 售货机商品 API(与商城商品分离)
|
||||
export const DeviceSpuApi = {
|
||||
// 分页查询售货机商品
|
||||
getDeviceSpuPage: async (params: PageParam) => {
|
||||
return await request.get({ url: '/product/device-spu/page', params })
|
||||
},
|
||||
|
||||
// 获取售货机商品详情
|
||||
getDeviceSpu: async (id: number) => {
|
||||
return await request.get({ url: `/product/device-spu/get-detail?id=${id}` })
|
||||
},
|
||||
|
||||
// 创建售货机商品
|
||||
createDeviceSpu: async (data: Spu) => {
|
||||
return await request.post({ url: '/product/device-spu/create', data })
|
||||
},
|
||||
|
||||
// 更新售货机商品
|
||||
updateDeviceSpu: async (data: Spu) => {
|
||||
return await request.put({ url: '/product/device-spu/update', data })
|
||||
},
|
||||
|
||||
// 删除售货机商品
|
||||
deleteDeviceSpu: async (id: number) => {
|
||||
return await request.delete({ url: `/product/device-spu/delete?id=${id}` })
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类型,方便其他地方使用
|
||||
export type { Spu, Sku }
|
||||
@@ -50,8 +50,13 @@ export const DeviceApi = {
|
||||
return await request.download({ url: `/product/device/export-excel`, params })
|
||||
},
|
||||
|
||||
// 获取设备下拉列表(精简,自动根据当前登录用户过滤)
|
||||
// 获取设备下拉列表(精简,全部,不按用户过滤)
|
||||
getDeviceListAllSimple: async () => {
|
||||
return await request.get({ url: `/product/device/list-all-simple` })
|
||||
},
|
||||
|
||||
// 获取设备下拉列表(精简,按用户绑定过滤)
|
||||
getDeviceListSimpleByUser: async () => {
|
||||
return await request.get({ url: `/product/device/list-simple-by-user` })
|
||||
},
|
||||
}
|
||||
+31
-31
@@ -20,35 +20,35 @@
|
||||
</el-col>
|
||||
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
|
||||
<div class="h-70px flex items-center justify-end lt-sm:mt-10px">
|
||||
<div class="px-8px text-right">
|
||||
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.project') }}</div>
|
||||
<CountTo
|
||||
class="text-20px"
|
||||
:start-val="0"
|
||||
:end-val="totalSate.project"
|
||||
:duration="2600"
|
||||
/>
|
||||
</div>
|
||||
<el-divider direction="vertical" />
|
||||
<div class="px-8px text-right">
|
||||
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>
|
||||
<CountTo
|
||||
class="text-20px"
|
||||
:start-val="0"
|
||||
:end-val="totalSate.todo"
|
||||
:duration="2600"
|
||||
/>
|
||||
</div>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<div class="px-8px text-right">
|
||||
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.access') }}</div>
|
||||
<CountTo
|
||||
class="text-20px"
|
||||
:start-val="0"
|
||||
:end-val="totalSate.access"
|
||||
:duration="2600"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="px-8px text-right">-->
|
||||
<!-- <div class="mb-16px text-14px text-gray-400">{{ t('workplace.project') }}</div>-->
|
||||
<!-- <CountTo-->
|
||||
<!-- class="text-20px"-->
|
||||
<!-- :start-val="0"-->
|
||||
<!-- :end-val="totalSate.project"-->
|
||||
<!-- :duration="2600"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
<!-- <el-divider direction="vertical" />-->
|
||||
<!-- <div class="px-8px text-right">-->
|
||||
<!-- <div class="mb-16px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>-->
|
||||
<!-- <CountTo-->
|
||||
<!-- class="text-20px"-->
|
||||
<!-- :start-val="0"-->
|
||||
<!-- :end-val="totalSate.todo"-->
|
||||
<!-- :duration="2600"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
<!-- <el-divider direction="vertical" border-style="dashed" />-->
|
||||
<!-- <div class="px-8px text-right">-->
|
||||
<!-- <div class="mb-16px text-14px text-gray-400">{{ t('workplace.access') }}</div>-->
|
||||
<!-- <CountTo-->
|
||||
<!-- class="text-20px"-->
|
||||
<!-- :start-val="0"-->
|
||||
<!-- :end-val="totalSate.access"-->
|
||||
<!-- :duration="2600"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -56,7 +56,7 @@
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-row class="mt-8px" :gutter="8" justify="space-between">
|
||||
<!-- <el-row class="mt-8px" :gutter="8" justify="space-between">
|
||||
<el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-8px">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
@@ -169,7 +169,7 @@
|
||||
</el-skeleton>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-row>-->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { set } from 'lodash-es'
|
||||
|
||||
@@ -50,11 +50,18 @@
|
||||
<el-table-column label="访问商品地址" align="center" prop="productUrl" />
|
||||
<el-table-column label="文件地址" align="center" prop="fileUrl" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" :formatter="dateFormatter" width="180px" />
|
||||
<el-table-column label="操作" align="center" min-width="120px">
|
||||
<el-table-column label="操作" align="center" min-width="200px">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openForm('update', row.id)" v-hasPermi="['product:device:update']">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="success"
|
||||
@click="handleChannelManage(row)"
|
||||
>
|
||||
<Icon icon="ep:grid" class="mr-5px" />货道管理
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row.id)" v-hasPermi="['product:device:delete']">
|
||||
删除
|
||||
</el-button>
|
||||
@@ -68,6 +75,8 @@
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<DeviceForm ref="formRef" @success="getList" />
|
||||
<!-- 货道管理对话框 -->
|
||||
<ChannelManageDialog ref="channelManageRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -75,6 +84,7 @@ import { dateFormatter } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import { DeviceApi, DeviceVO } from '@/api/product/device'
|
||||
import DeviceForm from './DeviceForm.vue'
|
||||
import ChannelManageDialog from '@/views/mall/product/device/ChannelManageDialog.vue'
|
||||
|
||||
/** 贩卖机信息 列表 */
|
||||
defineOptions({ name: 'Device' })
|
||||
@@ -156,6 +166,12 @@ const handleExport = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 货道管理 */
|
||||
const channelManageRef = ref()
|
||||
const handleChannelManage = (row: DeviceVO) => {
|
||||
channelManageRef.value.open(row)
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
|
||||
@@ -2,33 +2,50 @@
|
||||
<!-- 情况一:添加/修改 -->
|
||||
<el-table
|
||||
v-if="!isDetail && !isActivityComponent"
|
||||
:data="isBatch ? skuList : formData!.skus!"
|
||||
:data="formData?.skus || []"
|
||||
border
|
||||
class="tabNumWidth"
|
||||
max-height="500"
|
||||
size="small"
|
||||
:row-key="getRowKey"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column align="center" label="图片" min-width="65">
|
||||
<template #default="{ row }">
|
||||
<UploadImg v-model="row.picUrl" height="50px" width="50px" />
|
||||
<el-table-column width="55" align="center">
|
||||
<template #header>
|
||||
<el-checkbox
|
||||
v-model="isCheckAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="handleCheckAll"
|
||||
/>
|
||||
</template>
|
||||
<template #default="{ row, $index }">
|
||||
<el-checkbox
|
||||
:model-value="checkedStatus[getRowKey(row, $index)] || false"
|
||||
@update:model-value="(val: boolean) => { checkedStatus[getRowKey(row, $index)] = val; handleCheckOne() }"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="货道号" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.cabinetChannel"
|
||||
class="w-100%"
|
||||
placeholder="请选择货道"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="!props.deviceId"
|
||||
@change="handleChannelChange(row)"
|
||||
>
|
||||
<el-option
|
||||
v-for="channel in availableChannels"
|
||||
:key="channel.cabinetChannel"
|
||||
:label="channel.maxNum != null ? `${channel.cabinetChannel} (最大${channel.maxNum})` : (channel.cabinetChannel ?? '')"
|
||||
:value="channel.cabinetChannel"
|
||||
:disabled="isChannelUsed(channel.cabinetChannel, row)"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template v-if="formData!.specType && !isBatch">
|
||||
<!-- 根据商品属性动态添加 -->
|
||||
<el-table-column
|
||||
v-for="(item, index) in tableHeaders"
|
||||
:key="index"
|
||||
:label="item.label"
|
||||
align="center"
|
||||
min-width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span style="font-weight: bold; color: #40aaff">
|
||||
{{ row.properties?.[index]?.valueName }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<el-table-column align="center" label="商品条码" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.barCode" class="w-100%" />
|
||||
@@ -72,69 +89,75 @@
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="库存" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.stock" :min="0" class="w-100%" controls-position="right" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="重量(kg)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.weight"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-100%"
|
||||
<el-input-number
|
||||
v-model="row.stock"
|
||||
:min="0"
|
||||
:max="getMaxStock(row)"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
@change="handleStockChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="体积(m^3)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.volume"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template v-if="formData!.subCommissionType">
|
||||
<el-table-column align="center" label="一级返佣(元)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.firstBrokeragePrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="二级返佣(元)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.secondBrokeragePrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<el-table-column v-if="formData?.specType" align="center" fixed="right" label="操作" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="isBatch" link size="small" type="primary" @click="batchAdd">
|
||||
批量添加
|
||||
</el-button>
|
||||
<el-button v-else link size="small" type="primary" @click="deleteSku(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 批量新增货道对话框 -->
|
||||
<Dialog
|
||||
v-model="batchAddDialogVisible"
|
||||
title="批量新增货道"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="batch-add-channels">
|
||||
<div class="batch-add-header">
|
||||
<div class="selected-info">
|
||||
<span class="selected-count">已选择:<strong>{{ batchAddForm.selectedChannels.length }}</strong> 个货道</span>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleSelectAll"
|
||||
:disabled="availableChannelsForBatch.length === 0"
|
||||
>
|
||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="channels-grid">
|
||||
<div
|
||||
v-for="channel in availableChannelsForBatch"
|
||||
:key="channel.cabinetChannel"
|
||||
class="channel-card"
|
||||
:class="{ 'selected': batchAddForm.selectedChannels.includes(channel.cabinetChannel) }"
|
||||
@click="toggleChannel(channel.cabinetChannel)"
|
||||
>
|
||||
<el-checkbox
|
||||
:model-value="batchAddForm.selectedChannels.includes(channel.cabinetChannel)"
|
||||
@change="(val: boolean) => toggleChannel(channel.cabinetChannel, val)"
|
||||
@click.stop
|
||||
/>
|
||||
<div class="channel-info">
|
||||
<div class="channel-number">货道号:<strong>{{ channel.cabinetChannel }}</strong></div>
|
||||
<div class="channel-max">最大数量:<span>{{ channel.maxNum || 0 }}</span></div>
|
||||
<div v-if="channel.channelLine" class="channel-line">行号:{{ channel.channelLine }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="availableChannelsForBatch.length === 0" class="empty-tip">
|
||||
暂无可用货道
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="batchAddDialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleBatchAddConfirm"
|
||||
:disabled="batchAddForm.selectedChannels.length === 0"
|
||||
>
|
||||
确定(已选 {{ batchAddForm.selectedChannels.length }} 个)
|
||||
</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- 情况二:详情 -->
|
||||
<el-table
|
||||
v-if="isDetail"
|
||||
@@ -147,14 +170,9 @@
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column v-if="isComponent" type="selection" width="45" />
|
||||
<el-table-column align="center" label="图片" min-width="80">
|
||||
<el-table-column align="center" label="货道号" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row.picUrl"
|
||||
:src="row.picUrl"
|
||||
class="h-50px w-50px"
|
||||
@click="imagePreview(row.picUrl)"
|
||||
/>
|
||||
{{ row.cabinetChannel || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template v-if="formData!.specType && !isBatch">
|
||||
@@ -198,16 +216,6 @@
|
||||
{{ row.stock }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="重量(kg)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.weight }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="体积(m^3)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.volume }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template v-if="formData!.subCommissionType">
|
||||
<el-table-column align="center" label="一级返佣(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
@@ -283,16 +291,18 @@
|
||||
</el-table>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { PropType, Ref } from 'vue'
|
||||
import { PropType, Ref, watch, computed } from 'vue'
|
||||
import { copyValueToTarget, formatToFraction } from '@/utils'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { UploadImg } from '@/components/UploadFile'
|
||||
import type { Property, Sku, Spu } from '@/api/mall/product/spu'
|
||||
import type { Sku, Spu } from '@/api/mall/product/device-spu'
|
||||
import type { Property } from '@/api/mall/product/spu'
|
||||
import { createImageViewer } from '@/components/ImageViewer'
|
||||
import { RuleConfig } from '@/views/mall/product/spu/components/index'
|
||||
import { PropertyAndValues } from './index'
|
||||
import { ElTable } from 'element-plus'
|
||||
import { isEmpty } from '@/utils/is'
|
||||
import { Dialog } from '@/components/Dialog'
|
||||
import type { ChannelVO } from '@/api/mall/product/channel'
|
||||
|
||||
defineOptions({ name: 'SkuList' })
|
||||
const message = useMessage() // 消息弹窗
|
||||
@@ -302,10 +312,6 @@ const props = defineProps({
|
||||
type: Object as PropType<Spu>,
|
||||
default: () => {}
|
||||
},
|
||||
propertyList: {
|
||||
type: Array as PropType<PropertyAndValues[]>,
|
||||
default: () => []
|
||||
},
|
||||
ruleConfig: {
|
||||
type: Array as PropType<RuleConfig[]>,
|
||||
default: () => []
|
||||
@@ -313,23 +319,55 @@ const props = defineProps({
|
||||
isBatch: propTypes.bool.def(false), // 是否作为批量操作组件
|
||||
isDetail: propTypes.bool.def(false), // 是否作为 sku 详情组件
|
||||
isComponent: propTypes.bool.def(false), // 是否作为 sku 选择组件
|
||||
isActivityComponent: propTypes.bool.def(false) // 是否作为 sku 活动配置组件
|
||||
isActivityComponent: propTypes.bool.def(false), // 是否作为 sku 活动配置组件
|
||||
channelList: {
|
||||
type: Array as PropType<ChannelVO[]>,
|
||||
default: () => []
|
||||
},
|
||||
deviceId: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
}
|
||||
})
|
||||
const formData: Ref<Spu | undefined> = ref<Spu>() // 表单数据
|
||||
const skuList = ref<Sku[]>([
|
||||
{
|
||||
price: 0, // 商品价格
|
||||
marketPrice: 0, // 市场价
|
||||
costPrice: 0, // 成本价
|
||||
barCode: '', // 商品条码
|
||||
picUrl: '', // 图片地址
|
||||
stock: 0, // 库存
|
||||
weight: 0, // 商品重量
|
||||
volume: 0, // 商品体积
|
||||
firstBrokeragePrice: 0, // 一级分销的佣金
|
||||
secondBrokeragePrice: 0 // 二级分销的佣金
|
||||
const selectedSkus = ref<Sku[]>([]) // 选中的货道列表
|
||||
const checkedStatus = ref<Record<string, boolean>>({}) // 复选框选中状态
|
||||
const isCheckAll = ref(false) // 全选状态
|
||||
const isIndeterminate = ref(false) // 半选状态
|
||||
const tableHeaders = ref<Array<{ prop: string; label: string }>>([]) // 表头列表
|
||||
const skuList = ref<Sku[]>([]) // 批量操作时的 sku 列表
|
||||
const batchAddDialogVisible = ref(false) // 批量新增对话框显示状态
|
||||
const batchAddForm = ref<{ selectedChannels: string[] }>({
|
||||
selectedChannels: []
|
||||
}) // 批量新增表单数据
|
||||
|
||||
/** 获取行的唯一标识 */
|
||||
const getRowKey = (row: Sku, index: number) => {
|
||||
// 如果有 id,使用 id
|
||||
if (row.id !== undefined && row.id !== null) {
|
||||
return row.id
|
||||
}
|
||||
]) // 批量添加时的临时数据
|
||||
// 如果有 rowKey,使用 rowKey
|
||||
if ((row as any).rowKey) {
|
||||
return (row as any).rowKey
|
||||
}
|
||||
// 否则使用索引作为唯一标识
|
||||
return `row_${index}`
|
||||
}
|
||||
|
||||
const defaultSku = (): Sku => ({
|
||||
cabinetChannel: '', // 货道号
|
||||
price: 0, // 商品价格
|
||||
marketPrice: 0, // 市场价
|
||||
costPrice: 0, // 成本价
|
||||
barCode: '', // 商品条码
|
||||
picUrl: '', // 图片地址
|
||||
stock: 0, // 库存
|
||||
weight: 0, // 商品重量
|
||||
volume: 0, // 商品体积
|
||||
firstBrokeragePrice: 0, // 一级分销的佣金
|
||||
secondBrokeragePrice: 0 // 二级分销的佣金
|
||||
})
|
||||
|
||||
/** 商品图预览 */
|
||||
const imagePreview = (imgUrl: string) => {
|
||||
@@ -339,38 +377,112 @@ const imagePreview = (imgUrl: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
/** 批量添加 */
|
||||
const batchAdd = () => {
|
||||
validateProperty()
|
||||
formData.value!.skus!.forEach((item) => {
|
||||
copyValueToTarget(item, skuList.value[0])
|
||||
/** 新增一行货道 */
|
||||
const addSku = () => {
|
||||
if (!formData.value!.skus) {
|
||||
formData.value!.skus = []
|
||||
}
|
||||
const newSku = defaultSku()
|
||||
// 给新 sku 添加唯一标识
|
||||
;(newSku as any).rowKey = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
formData.value!.skus!.push(newSku)
|
||||
}
|
||||
|
||||
/** 处理表格选中变化 */
|
||||
const handleSelectionChange = (selection: Sku[]) => {
|
||||
selectedSkus.value = selection
|
||||
}
|
||||
|
||||
/** 处理单个复选框变化 */
|
||||
const handleCheckOne = () => {
|
||||
updateCheckStatus()
|
||||
}
|
||||
|
||||
/** 处理全选/取消全选 */
|
||||
const handleCheckAll = (checked: boolean) => {
|
||||
if (!formData.value?.skus) return
|
||||
formData.value.skus.forEach((sku, index) => {
|
||||
const key = getRowKey(sku, index)
|
||||
checkedStatus.value[key] = checked
|
||||
})
|
||||
updateCheckStatus()
|
||||
}
|
||||
|
||||
/** 更新选中状态 */
|
||||
const updateCheckStatus = () => {
|
||||
if (!formData.value?.skus) {
|
||||
isCheckAll.value = false
|
||||
isIndeterminate.value = false
|
||||
return
|
||||
}
|
||||
const total = formData.value.skus.length
|
||||
const checkedCount = Object.values(checkedStatus.value).filter(Boolean).length
|
||||
isCheckAll.value = checkedCount === total && total > 0
|
||||
isIndeterminate.value = checkedCount > 0 && checkedCount < total
|
||||
// 更新 selectedSkus
|
||||
selectedSkus.value = formData.value.skus.filter((sku, index) => {
|
||||
const key = getRowKey(sku, index)
|
||||
return checkedStatus.value[key]
|
||||
})
|
||||
}
|
||||
/** 校验商品属性属性值 */
|
||||
const validateProperty = () => {
|
||||
// 校验商品属性属性值是否为空,有一个为空都不给过
|
||||
const warningInfo = '存在属性属性值为空,请先检查完善属性值后重试!!!'
|
||||
for (const item of props.propertyList) {
|
||||
if (!item.values || isEmpty(item.values)) {
|
||||
message.warning(warningInfo)
|
||||
throw new Error(warningInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听数据变化,更新复选框状态 */
|
||||
watch(
|
||||
() => formData.value?.skus,
|
||||
(skus) => {
|
||||
if (!skus) return
|
||||
// 初始化复选框状态
|
||||
const newCheckedStatus: Record<string, boolean> = {}
|
||||
skus.forEach((sku, index) => {
|
||||
const key = getRowKey(sku, index)
|
||||
newCheckedStatus[key] = checkedStatus.value[key] || false
|
||||
})
|
||||
checkedStatus.value = newCheckedStatus
|
||||
updateCheckStatus()
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
/** 删除 sku */
|
||||
const deleteSku = (row) => {
|
||||
const index = formData.value!.skus!.findIndex(
|
||||
// 直接把列表转成字符串比较
|
||||
(sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
|
||||
)
|
||||
formData.value!.skus!.splice(index, 1)
|
||||
if (!formData.value!.skus || formData.value!.skus.length <= 1) {
|
||||
message.warning('至少需要保留一行货道数据')
|
||||
return
|
||||
}
|
||||
const index = formData.value!.skus!.indexOf(row)
|
||||
if (index > -1) {
|
||||
formData.value!.skus!.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除选中的货道 */
|
||||
const deleteSelectedSkus = () => {
|
||||
if (!selectedSkus.value || selectedSkus.value.length === 0) {
|
||||
message.warning('请先选择要删除的货道')
|
||||
return
|
||||
}
|
||||
if (!formData.value!.skus) {
|
||||
return
|
||||
}
|
||||
// 检查删除后是否至少保留一行
|
||||
if (formData.value!.skus.length - selectedSkus.value.length < 1) {
|
||||
message.warning('至少需要保留一行货道数据')
|
||||
return
|
||||
}
|
||||
// 删除选中的货道
|
||||
selectedSkus.value.forEach((selectedSku) => {
|
||||
const index = formData.value!.skus!.indexOf(selectedSku)
|
||||
if (index > -1) {
|
||||
formData.value!.skus!.splice(index, 1)
|
||||
}
|
||||
})
|
||||
// 清空选中状态
|
||||
selectedSkus.value = []
|
||||
}
|
||||
const tableHeaders = ref<{ prop: string; label: string }[]>([]) // 多属性表头
|
||||
/**
|
||||
* 保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种。
|
||||
*/
|
||||
const validateSku = () => {
|
||||
validateProperty()
|
||||
let warningInfo = '请检查商品各行相关属性配置,'
|
||||
let validate = true // 默认通过
|
||||
for (const sku of formData.value!.skus!) {
|
||||
@@ -407,13 +519,6 @@ const getValue = (obj, arg) => {
|
||||
const emit = defineEmits<{
|
||||
(e: 'selectionChange', value: Sku[]): void
|
||||
}>()
|
||||
/**
|
||||
* 选择时触发
|
||||
* @param Sku 传递过来的选中的 sku 是一个数组
|
||||
*/
|
||||
const handleSelectionChange = (val: Sku[]) => {
|
||||
emit('selectionChange', val)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将传进来的值赋值给 skuList
|
||||
@@ -423,6 +528,23 @@ watch(
|
||||
(data) => {
|
||||
if (!data) return
|
||||
formData.value = data
|
||||
// 确保每个 sku 都有唯一的 rowKey
|
||||
if (formData.value?.skus) {
|
||||
formData.value.skus.forEach((sku, index) => {
|
||||
if (!sku.id && !(sku as any).rowKey) {
|
||||
// 给没有 id 的 sku 添加一个持久的唯一标识(使用索引)
|
||||
;(sku as any).rowKey = `row_${index}_${Date.now()}`
|
||||
}
|
||||
})
|
||||
// 初始化复选框状态
|
||||
const newCheckedStatus: Record<string, boolean> = {}
|
||||
formData.value.skus.forEach((sku, index) => {
|
||||
const key = getRowKey(sku, index)
|
||||
newCheckedStatus[key] = checkedStatus.value[key] || false
|
||||
})
|
||||
checkedStatus.value = newCheckedStatus
|
||||
updateCheckStatus()
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
@@ -477,15 +599,10 @@ const generateTableData = (propertyList: any[]) => {
|
||||
*/
|
||||
const validateData = (propertyList: any[]) => {
|
||||
const skuPropertyIds: number[] = []
|
||||
formData.value!.skus!.forEach((sku) =>
|
||||
sku.properties
|
||||
?.map((property) => property.propertyId)
|
||||
?.forEach((propertyId) => {
|
||||
if (skuPropertyIds.indexOf(propertyId!) === -1) {
|
||||
skuPropertyIds.push(propertyId!)
|
||||
}
|
||||
})
|
||||
)
|
||||
formData.value!.skus!.forEach((sku) => {
|
||||
// 容器商品场景不再使用多属性组合,这里直接跳过
|
||||
return sku
|
||||
})
|
||||
const propertyIds = propertyList.map((item) => item.id)
|
||||
return skuPropertyIds.length === propertyIds.length
|
||||
}
|
||||
@@ -571,6 +688,246 @@ const activitySkuListRef = ref<InstanceType<typeof ElTable>>()
|
||||
const getSkuTableRef = () => {
|
||||
return activitySkuListRef.value
|
||||
}
|
||||
/** 可用货道列表(含当前已选货道,保证编辑时能回显) */
|
||||
const availableChannels = computed(() => {
|
||||
const list = props.channelList || []
|
||||
const fromSkus = (formData.value?.skus || [])
|
||||
.map((s) => s.cabinetChannel)
|
||||
.filter(Boolean) as string[]
|
||||
const inListSet = new Set(list.map((ch) => ch.cabinetChannel))
|
||||
const extra = [...new Set(fromSkus.filter((ch) => !inListSet.has(ch)))].map(
|
||||
(cabinetChannel) => ({ cabinetChannel, maxNum: undefined as number | undefined })
|
||||
)
|
||||
return [...list, ...extra]
|
||||
})
|
||||
|
||||
/** 批量新增时可用的货道列表(排除已使用的) */
|
||||
const availableChannelsForBatch = computed(() => {
|
||||
const usedChannels = new Set(
|
||||
(formData.value?.skus || [])
|
||||
.map((sku) => sku.cabinetChannel)
|
||||
.filter(Boolean)
|
||||
)
|
||||
return (props.channelList || []).filter(
|
||||
(channel) => !usedChannels.has(channel.cabinetChannel)
|
||||
)
|
||||
})
|
||||
|
||||
/** 检查货道是否已被使用 */
|
||||
const isChannelUsed = (cabinetChannel: string | undefined, currentRow: Sku) => {
|
||||
if (!cabinetChannel) return false
|
||||
return (formData.value?.skus || []).some(
|
||||
(sku) => sku.cabinetChannel === cabinetChannel && sku !== currentRow
|
||||
)
|
||||
}
|
||||
|
||||
/** 根据选择的货道获取最大库存 */
|
||||
const getMaxStock = (row: Sku) => {
|
||||
if (!row.cabinetChannel) return undefined
|
||||
const channel = availableChannels.value.find(
|
||||
(ch) => ch.cabinetChannel === row.cabinetChannel
|
||||
)
|
||||
return channel?.maxNum
|
||||
}
|
||||
|
||||
/** 货道变化时的处理 */
|
||||
const handleChannelChange = (row: Sku) => {
|
||||
if (!row.cabinetChannel) {
|
||||
row.stock = 0
|
||||
return
|
||||
}
|
||||
const channel = availableChannels.value.find(
|
||||
(ch) => ch.cabinetChannel === row.cabinetChannel
|
||||
)
|
||||
if (channel && channel.maxNum !== undefined) {
|
||||
// 货道的最大数默认等于库存
|
||||
row.stock = channel.maxNum
|
||||
}
|
||||
}
|
||||
|
||||
/** 库存变化时的处理 */
|
||||
const handleStockChange = (row: Sku) => {
|
||||
const maxStock = getMaxStock(row)
|
||||
if (maxStock !== undefined && row.stock > maxStock) {
|
||||
row.stock = maxStock
|
||||
message.warning(`库存不能超过货道最大数量 ${maxStock}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开批量新增对话框 */
|
||||
const openBatchAddDialog = (channels: ChannelVO[]) => {
|
||||
batchAddForm.value.selectedChannels = []
|
||||
batchAddDialogVisible.value = true
|
||||
}
|
||||
|
||||
/** 切换货道选择状态 */
|
||||
const toggleChannel = (cabinetChannel: string, checked?: boolean) => {
|
||||
const index = batchAddForm.value.selectedChannels.indexOf(cabinetChannel)
|
||||
if (checked !== undefined) {
|
||||
if (checked && index === -1) {
|
||||
batchAddForm.value.selectedChannels.push(cabinetChannel)
|
||||
} else if (!checked && index > -1) {
|
||||
batchAddForm.value.selectedChannels.splice(index, 1)
|
||||
}
|
||||
} else {
|
||||
// 点击卡片切换
|
||||
if (index > -1) {
|
||||
batchAddForm.value.selectedChannels.splice(index, 1)
|
||||
} else {
|
||||
batchAddForm.value.selectedChannels.push(cabinetChannel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 全选/取消全选 */
|
||||
const isAllSelected = computed(() => {
|
||||
return availableChannelsForBatch.value.length > 0 &&
|
||||
batchAddForm.value.selectedChannels.length === availableChannelsForBatch.value.length
|
||||
})
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (isAllSelected.value) {
|
||||
batchAddForm.value.selectedChannels = []
|
||||
} else {
|
||||
batchAddForm.value.selectedChannels = availableChannelsForBatch.value.map(ch => ch.cabinetChannel).filter(Boolean) as string[]
|
||||
}
|
||||
}
|
||||
|
||||
/** 确认批量新增 */
|
||||
const handleBatchAddConfirm = () => {
|
||||
if (!batchAddForm.value.selectedChannels || batchAddForm.value.selectedChannels.length === 0) {
|
||||
message.warning('请至少选择一个货道')
|
||||
return
|
||||
}
|
||||
if (!formData.value?.skus) {
|
||||
formData.value.skus = []
|
||||
}
|
||||
const selectedCount = batchAddForm.value.selectedChannels.length
|
||||
// 为每个选中的货道创建 SKU
|
||||
batchAddForm.value.selectedChannels.forEach((cabinetChannel) => {
|
||||
const channel = availableChannels.value.find((ch) => ch.cabinetChannel === cabinetChannel)
|
||||
if (channel) {
|
||||
const newSku: Sku = {
|
||||
cabinetChannel: channel.cabinetChannel,
|
||||
price: 0,
|
||||
marketPrice: 0,
|
||||
costPrice: 0,
|
||||
barCode: '',
|
||||
picUrl: '',
|
||||
stock: channel.maxNum || 0, // 默认等于货道最大数
|
||||
weight: 0,
|
||||
volume: 0,
|
||||
firstBrokeragePrice: 0,
|
||||
secondBrokeragePrice: 0
|
||||
}
|
||||
;(newSku as any).rowKey = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
formData.value.skus.push(newSku)
|
||||
}
|
||||
})
|
||||
batchAddDialogVisible.value = false
|
||||
batchAddForm.value.selectedChannels = []
|
||||
message.success(`成功添加 ${selectedCount} 个货道`)
|
||||
}
|
||||
|
||||
// 暴露出生成 sku 方法,给添加属性成功时调用
|
||||
defineExpose({ generateTableData, validateSku, getSkuTableRef })
|
||||
defineExpose({ generateTableData, validateSku, getSkuTableRef, deleteSelectedSkus, openBatchAddDialog })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.batch-add-channels {
|
||||
.batch-add-header {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
|
||||
.selected-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.selected-count {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
strong {
|
||||
color: var(--el-color-primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channels-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
|
||||
.channel-card {
|
||||
border: 2px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: var(--el-bg-color);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary-light-7);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.channel-info {
|
||||
flex: 1;
|
||||
|
||||
.channel-number {
|
||||
font-size: 15px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
strong {
|
||||
font-size: 18px;
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-max {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
margin-bottom: 4px;
|
||||
|
||||
span {
|
||||
color: var(--el-color-success);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-line {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
import SkuList from './SkuList.vue'
|
||||
import { Spu } from '@/api/mall/product/spu'
|
||||
import type { Spu } from '@/api/mall/product/device-spu'
|
||||
|
||||
interface PropertyAndValues {
|
||||
id: number
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
import { PropType } from 'vue'
|
||||
import { copyValueToTarget } from '@/utils'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import type { Spu } from '@/api/mall/product/spu'
|
||||
import type { Spu } from '@/api/mall/product/device-spu'
|
||||
import * as ExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { DeliveryTypeEnum } from '@/utils/constants'
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</el-form>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { Spu } from '@/api/mall/product/spu'
|
||||
import type { Spu } from '@/api/mall/product/device-spu'
|
||||
import { Editor } from '@/components/Editor'
|
||||
import { PropType } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
|
||||
@@ -24,44 +24,34 @@
|
||||
placeholder="请选择商品分类"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品品牌" prop="brandId">
|
||||
<el-select v-model="formData.brandId" class="w-80" placeholder="请选择商品品牌">
|
||||
<el-option
|
||||
v-for="item in brandList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id as number"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品关键字" prop="keyword">
|
||||
<el-input v-model="formData.keyword" class="w-80!" placeholder="请输入商品关键字" />
|
||||
</el-form-item>
|
||||
<el-form-item label="商品简介" prop="introduction">
|
||||
<el-input
|
||||
v-model="formData.introduction"
|
||||
:autosize="{ minRows: 2, maxRows: 2 }"
|
||||
:clearable="true"
|
||||
:show-word-limit="true"
|
||||
class="w-80!"
|
||||
maxlength="128"
|
||||
placeholder="请输入商品简介"
|
||||
type="textarea"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品封面图" prop="picUrl">
|
||||
<UploadImg v-model="formData.picUrl" :disabled="isDetail" height="80px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="商品轮播图" prop="sliderPicUrls">
|
||||
<UploadImgs v-model="formData.sliderPicUrls!" :disabled="isDetail" />
|
||||
</el-form-item>
|
||||
<el-form-item label="设备号">
|
||||
<el-input
|
||||
v-model="formData.deviceId"
|
||||
<el-form-item label="商品排序" prop="sort">
|
||||
<el-input-number
|
||||
v-model="formData.sort"
|
||||
:min="0"
|
||||
placeholder="请输入商品排序"
|
||||
class="w-80!"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="赠送积分" prop="giveIntegral">
|
||||
<el-input-number
|
||||
v-model="formData.giveIntegral"
|
||||
:min="0"
|
||||
placeholder="请输入赠送积分"
|
||||
class="w-80!"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="虚拟销量" prop="virtualSalesCount">
|
||||
<el-input-number
|
||||
v-model="formData.virtualSalesCount"
|
||||
:min="0"
|
||||
placeholder="请输入虚拟销量"
|
||||
class="w-80!"
|
||||
clearable
|
||||
placeholder="请输入设备号"
|
||||
type="number"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -71,11 +61,9 @@ import { PropType } from 'vue'
|
||||
import { copyValueToTarget } from '@/utils'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { defaultProps, handleTree } from '@/utils/tree'
|
||||
import type { Spu } from '@/api/mall/product/spu'
|
||||
import type { Spu } from '@/api/mall/product/device-spu'
|
||||
import * as ProductCategoryApi from '@/api/mall/product/category'
|
||||
import { CategoryVO } from '@/api/mall/product/category'
|
||||
import * as ProductBrandApi from '@/api/mall/product/brand'
|
||||
import { BrandVO } from '@/api/mall/product/brand'
|
||||
|
||||
defineOptions({ name: 'ProductSpuInfoForm' })
|
||||
const props = defineProps({
|
||||
@@ -92,21 +80,21 @@ const formRef = ref() // 表单 Ref
|
||||
const formData = reactive<Spu>({
|
||||
name: '', // 商品名称
|
||||
categoryId: undefined, // 商品分类
|
||||
keyword: '', // 关键字
|
||||
picUrl: '', // 商品封面图
|
||||
sliderPicUrls: [], // 商品轮播图
|
||||
introduction: '', // 商品简介
|
||||
brandId: undefined, // 商品品牌
|
||||
deviceId: undefined // 设备号
|
||||
deviceId: undefined, // 设备号
|
||||
sort: 0, // 商品排序
|
||||
giveIntegral: 0, // 赠送积分
|
||||
virtualSalesCount: 0 // 虚拟销量
|
||||
})
|
||||
const rules = reactive({
|
||||
name: [required],
|
||||
categoryId: [required],
|
||||
keyword: [required],
|
||||
introduction: [required],
|
||||
picUrl: [required],
|
||||
sliderPicUrls: [required],
|
||||
brandId: [required]
|
||||
sort: [required],
|
||||
giveIntegral: [required],
|
||||
virtualSalesCount: [required]
|
||||
})
|
||||
|
||||
/** 将传进来的值赋值给 formData */
|
||||
@@ -140,13 +128,10 @@ const validate = async () => {
|
||||
defineExpose({ validate })
|
||||
|
||||
/** 初始化 */
|
||||
const brandList = ref<BrandVO[]>([]) // 商品品牌列表
|
||||
const categoryList = ref<CategoryVO[]>([]) // 商品分类树
|
||||
onMounted(async () => {
|
||||
// 获得分类树
|
||||
const data = await ProductCategoryApi.getCategoryList({})
|
||||
categoryList.value = handleTree(data, 'id')
|
||||
// 获取商品品牌列表
|
||||
brandList.value = await ProductBrandApi.getSimpleBrandList()
|
||||
})
|
||||
</script>
|
||||
@@ -28,7 +28,7 @@
|
||||
</el-form>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { Spu } from '@/api/mall/product/spu'
|
||||
import type { Spu } from '@/api/mall/product/device-spu'
|
||||
import { PropType } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { copyValueToTarget } from '@/utils'
|
||||
|
||||
@@ -8,71 +8,72 @@
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="分销类型" prop="subCommissionType">
|
||||
<el-radio-group
|
||||
v-model="formData.subCommissionType"
|
||||
<el-form-item label="设备号" prop="deviceId">
|
||||
<el-select
|
||||
v-model="formData.deviceId"
|
||||
class="w-80"
|
||||
@change="changeSubCommissionType"
|
||||
placeholder="请选择设备"
|
||||
clearable
|
||||
filterable
|
||||
>
|
||||
<el-radio :value="false">默认设置</el-radio>
|
||||
<el-radio :value="true" class="radio">单独设置</el-radio>
|
||||
</el-radio-group>
|
||||
<el-option
|
||||
v-for="item in deviceOptions"
|
||||
:key="item.id"
|
||||
:label="`${item.name}(${item.deviceId}` + ')'"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品规格" prop="specType">
|
||||
<el-radio-group v-model="formData.specType" class="w-80" @change="onChangeSpec">
|
||||
<el-radio :value="false" class="radio">单规格</el-radio>
|
||||
<el-radio :value="true">多规格</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<!-- 多规格添加-->
|
||||
<el-form-item v-if="!formData.specType">
|
||||
<el-form-item label="货道列表">
|
||||
<div class="mb-10px">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleAddSku"
|
||||
:disabled="isDetail || !formData.deviceId"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增货道
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
@click="handleBatchAddChannels"
|
||||
:disabled="isDetail || !formData.deviceId"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 批量新增
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
@click="handleDeleteSelected"
|
||||
:disabled="isDetail || !formData.deviceId"
|
||||
>
|
||||
<Icon icon="ep:delete" class="mr-5px" /> 删除选中
|
||||
</el-button>
|
||||
</div>
|
||||
<SkuList
|
||||
ref="skuListRef"
|
||||
:prop-form-data="formData"
|
||||
:property-list="propertyList"
|
||||
:property-list="[]"
|
||||
:rule-config="ruleConfig"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.specType" label="商品属性">
|
||||
<el-button class="mb-10px mr-15px" @click="attributesAddFormRef.open">添加属性</el-button>
|
||||
<ProductAttributes
|
||||
:is-detail="isDetail"
|
||||
:property-list="propertyList"
|
||||
@success="generateSkus"
|
||||
:channel-list="channelList"
|
||||
:device-id="formData.deviceId"
|
||||
/>
|
||||
</el-form-item>
|
||||
<template v-if="formData.specType && propertyList.length > 0">
|
||||
<el-form-item v-if="!isDetail" label="批量设置">
|
||||
<SkuList :is-batch="true" :prop-form-data="formData" :property-list="propertyList" />
|
||||
</el-form-item>
|
||||
<el-form-item label="规格列表">
|
||||
<SkuList
|
||||
ref="skuListRef"
|
||||
:is-detail="isDetail"
|
||||
:prop-form-data="formData"
|
||||
:property-list="propertyList"
|
||||
:rule-config="ruleConfig"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
|
||||
<!-- 商品属性添加 Form 表单 -->
|
||||
<ProductPropertyAddForm ref="attributesAddFormRef" :propertyList="propertyList" />
|
||||
<!-- 不再使用商品属性添加 Form 表单 -->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue'
|
||||
import { copyValueToTarget } from '@/utils'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import {
|
||||
getPropertyList,
|
||||
PropertyAndValues,
|
||||
RuleConfig,
|
||||
SkuList
|
||||
} from '@/views/mall/product/spu/components/index'
|
||||
import ProductAttributes from './ProductAttributes.vue'
|
||||
import ProductPropertyAddForm from './ProductPropertyAddForm.vue'
|
||||
import type { Spu } from '@/api/mall/product/spu'
|
||||
import { RuleConfig } from '@/views/mall/product/spu/components/index'
|
||||
import { SkuList } from '@/views/mall/product/containergoods/components/index'
|
||||
import type { Spu, Sku } from '@/api/mall/product/device-spu'
|
||||
import { DeviceApi, type DeviceVO } from '@/api/product/device'
|
||||
import { ChannelApi, type ChannelVO } from '@/api/mall/product/channel'
|
||||
|
||||
defineOptions({ name: 'ProductSpuSkuForm' })
|
||||
|
||||
@@ -110,36 +111,95 @@ const props = defineProps({
|
||||
},
|
||||
isDetail: propTypes.bool.def(false) // 是否作为详情组件
|
||||
})
|
||||
const attributesAddFormRef = ref() // 添加商品属性表单
|
||||
const formRef = ref() // 表单 Ref
|
||||
const propertyList = ref<PropertyAndValues[]>([]) // 商品属性列表
|
||||
const skuListRef = ref() // 商品属性列表 Ref
|
||||
const formData = reactive<Spu>({
|
||||
specType: false, // 商品规格
|
||||
subCommissionType: false, // 分销类型
|
||||
deviceId: undefined, // 设备号(下拉选择)
|
||||
subCommissionType: false, // 分销类型(容器商品不展示配置,固定为默认)
|
||||
skus: []
|
||||
})
|
||||
const rules = reactive({
|
||||
specType: [required],
|
||||
subCommissionType: [required]
|
||||
deviceId: [required]
|
||||
})
|
||||
|
||||
/** 将传进来的值赋值给 formData */
|
||||
watch(
|
||||
() => props.propFormData,
|
||||
(data) => {
|
||||
async (data) => {
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
isInitializing.value = true
|
||||
const oldDeviceId = formData.deviceId
|
||||
copyValueToTarget(formData, data)
|
||||
// 将 SKU 的属性,整理成 PropertyAndValues 数组
|
||||
propertyList.value = getPropertyList(data)
|
||||
|
||||
// 如果设备号发生变化或首次加载时有设备号,需要加载货道列表
|
||||
if (formData.deviceId && formData.deviceId !== oldDeviceId) {
|
||||
await loadChannelsByDevice(formData.deviceId)
|
||||
}
|
||||
|
||||
// 确保至少有一行货道数据
|
||||
if (!formData.skus || formData.skus.length === 0) {
|
||||
const defaultSku = createDefaultSku()
|
||||
// 给默认 sku 添加唯一标识
|
||||
;(defaultSku as any).rowKey = `row_0_${Date.now()}`
|
||||
formData.skus = [defaultSku]
|
||||
} else {
|
||||
// 确保每个 sku 都有唯一标识
|
||||
formData.skus.forEach((sku, index) => {
|
||||
if (!sku.id && !(sku as any).rowKey) {
|
||||
;(sku as any).rowKey = `row_${index}_${Date.now()}`
|
||||
}
|
||||
})
|
||||
}
|
||||
isInitializing.value = false
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
/** 创建默认的货道数据 */
|
||||
const createDefaultSku = (): Sku => ({
|
||||
cabinetChannel: '', // 货道号
|
||||
price: 0,
|
||||
marketPrice: 0,
|
||||
costPrice: 0,
|
||||
barCode: '',
|
||||
picUrl: '',
|
||||
stock: 0,
|
||||
weight: 0,
|
||||
volume: 0,
|
||||
firstBrokeragePrice: 0,
|
||||
secondBrokeragePrice: 0
|
||||
})
|
||||
|
||||
/** 新增货道 */
|
||||
const handleAddSku = () => {
|
||||
if (!formData.deviceId) {
|
||||
message.warning('请先选择设备号')
|
||||
return
|
||||
}
|
||||
if (!formData.skus) {
|
||||
formData.skus = []
|
||||
}
|
||||
const newSku = createDefaultSku()
|
||||
// 给新 sku 添加唯一标识,用于表格多选
|
||||
;(newSku as any).rowKey = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
formData.skus.push(newSku)
|
||||
}
|
||||
|
||||
/** 删除选中的货道 */
|
||||
const handleDeleteSelected = () => {
|
||||
if (!formData.deviceId) {
|
||||
message.warning('请先选择设备号')
|
||||
return
|
||||
}
|
||||
if (skuListRef.value) {
|
||||
skuListRef.value.deleteSelectedSkus()
|
||||
}
|
||||
}
|
||||
|
||||
/** 表单校验 */
|
||||
const emit = defineEmits(['update:activeName'])
|
||||
const validate = async () => {
|
||||
@@ -158,38 +218,92 @@ const validate = async () => {
|
||||
}
|
||||
defineExpose({ validate })
|
||||
|
||||
/** 分销类型 */
|
||||
const changeSubCommissionType = () => {
|
||||
// 默认为零,类型切换后也要重置为零
|
||||
for (const item of formData.skus!) {
|
||||
item.firstBrokeragePrice = 0
|
||||
item.secondBrokeragePrice = 0
|
||||
/** 设备下拉数据 */
|
||||
const deviceOptions = ref<DeviceVO[]>([])
|
||||
/** 当前设备的货道列表 */
|
||||
const channelList = ref<ChannelVO[]>([])
|
||||
|
||||
/** 加载设备货道列表 */
|
||||
const loadChannelsByDevice = async (deviceId: number | undefined) => {
|
||||
if (!deviceId) {
|
||||
channelList.value = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
// 分页加载所有货道
|
||||
let allChannels: ChannelVO[] = []
|
||||
let pageNo = 1
|
||||
const pageSize = 100 // 最大值为 100
|
||||
let hasMore = true
|
||||
|
||||
while (hasMore) {
|
||||
const data = await ChannelApi.getChannelPage({
|
||||
pageNo,
|
||||
pageSize,
|
||||
deviceId: deviceId
|
||||
})
|
||||
if (data.list && data.list.length > 0) {
|
||||
allChannels = allChannels.concat(data.list)
|
||||
// 如果返回的数据少于 pageSize,说明已经是最后一页
|
||||
if (data.list.length < pageSize) {
|
||||
hasMore = false
|
||||
} else {
|
||||
pageNo++
|
||||
}
|
||||
} else {
|
||||
hasMore = false
|
||||
}
|
||||
}
|
||||
channelList.value = allChannels
|
||||
} catch (e) {
|
||||
console.error('加载货道列表失败', e)
|
||||
channelList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择规格 */
|
||||
const onChangeSpec = () => {
|
||||
// 重置商品属性列表
|
||||
propertyList.value = []
|
||||
// 重置sku列表
|
||||
formData.skus = [
|
||||
{
|
||||
price: 0,
|
||||
marketPrice: 0,
|
||||
costPrice: 0,
|
||||
barCode: '',
|
||||
picUrl: '',
|
||||
stock: 0,
|
||||
weight: 0,
|
||||
volume: 0,
|
||||
firstBrokeragePrice: 0,
|
||||
secondBrokeragePrice: 0
|
||||
/** 监听设备号变化(用户手动切换时) */
|
||||
const isInitializing = ref(false) // 标记是否正在初始化数据
|
||||
watch(
|
||||
() => formData.deviceId,
|
||||
async (deviceId, oldDeviceId) => {
|
||||
// 如果正在初始化,不执行清空操作
|
||||
if (isInitializing.value) {
|
||||
return
|
||||
}
|
||||
]
|
||||
await loadChannelsByDevice(deviceId)
|
||||
// 只有在用户手动切换设备时才清空货道列表
|
||||
if (deviceId && oldDeviceId && deviceId !== oldDeviceId) {
|
||||
formData.skus = []
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
/** 批量新增货道 */
|
||||
const handleBatchAddChannels = () => {
|
||||
if (!formData.deviceId) {
|
||||
message.warning('请先选择设备号')
|
||||
return
|
||||
}
|
||||
if (channelList.value.length === 0) {
|
||||
message.warning('该设备暂无可用货道')
|
||||
return
|
||||
}
|
||||
// 打开批量新增对话框
|
||||
if (skuListRef.value) {
|
||||
skuListRef.value.openBatchAddDialog(channelList.value)
|
||||
}
|
||||
}
|
||||
|
||||
/** 调用 SkuList generateTableData 方法*/
|
||||
const generateSkus = (propertyList: any[]) => {
|
||||
skuListRef.value.generateTableData(propertyList)
|
||||
}
|
||||
onMounted(async () => {
|
||||
try {
|
||||
deviceOptions.value = await DeviceApi.getDeviceListSimpleByUser()
|
||||
// 如果已有设备ID,加载货道列表
|
||||
if (formData.deviceId) {
|
||||
await loadChannelsByDevice(formData.deviceId)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载设备列表失败', e)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -9,7 +9,7 @@
|
||||
:propFormData="formData"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="价格库存" name="sku">
|
||||
<el-tab-pane label="货道上架" name="sku">
|
||||
<SkuForm
|
||||
ref="skuRef"
|
||||
v-model:activeName="activeName"
|
||||
@@ -17,30 +17,6 @@
|
||||
:propFormData="formData"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="物流设置" name="delivery">
|
||||
<DeliveryForm
|
||||
ref="deliveryRef"
|
||||
v-model:activeName="activeName"
|
||||
:is-detail="isDetail"
|
||||
:propFormData="formData"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="商品详情" name="description">
|
||||
<DescriptionForm
|
||||
ref="descriptionRef"
|
||||
v-model:activeName="activeName"
|
||||
:is-detail="isDetail"
|
||||
:propFormData="formData"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="其它设置" name="other">
|
||||
<OtherForm
|
||||
ref="otherRef"
|
||||
v-model:activeName="activeName"
|
||||
:is-detail="isDetail"
|
||||
:propFormData="formData"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-form>
|
||||
<el-form-item style="float: right">
|
||||
@@ -55,12 +31,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import * as ProductSpuApi from '@/api/mall/product/spu'
|
||||
import { DeviceSpuApi, type Spu } from '@/api/mall/product/device-spu'
|
||||
import InfoForm from './InfoForm.vue'
|
||||
import DescriptionForm from './DescriptionForm.vue'
|
||||
import OtherForm from './OtherForm.vue'
|
||||
import SkuForm from './SkuForm.vue'
|
||||
import DeliveryForm from './DeliveryForm.vue'
|
||||
import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
|
||||
|
||||
defineOptions({ name: 'ProductPickupAdd' })
|
||||
@@ -76,19 +49,17 @@ const activeName = ref('info') // Tag 激活的窗口
|
||||
const isDetail = ref(false) // 是否查看详情
|
||||
const infoRef = ref() // 商品信息 Ref
|
||||
const skuRef = ref() // 商品规格 Ref
|
||||
const deliveryRef = ref() // 物流设置 Ref
|
||||
const descriptionRef = ref() // 商品详情 Ref
|
||||
const otherRef = ref() // 其他设置 Ref
|
||||
// SPU 表单数据
|
||||
const formData = ref<ProductSpuApi.Spu>({
|
||||
const formData = ref<Spu>({
|
||||
name: '', // 商品名称
|
||||
categoryId: undefined, // 商品分类
|
||||
keyword: '', // 关键字
|
||||
picUrl: '', // 商品封面图
|
||||
sliderPicUrls: [], // 商品轮播图
|
||||
introduction: '', // 商品简介
|
||||
deliveryTypes: [], // 配送方式数组
|
||||
deliveryTemplateId: undefined, // 运费模版
|
||||
// 配送方式固定为自提柜(3),不再在表单中配置
|
||||
deliveryTypes: [3], // 配送方式数组
|
||||
deliveryTemplateId: undefined, // 运费模版(自提柜无需模板)
|
||||
brandId: undefined, // 商品品牌
|
||||
specType: false, // 商品规格
|
||||
subCommissionType: false, // 分销类型
|
||||
@@ -122,7 +93,7 @@ const getDetail = async () => {
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.Spu
|
||||
const res = (await DeviceSpuApi.getDeviceSpu(id)) as Spu
|
||||
res.skus?.forEach((item) => {
|
||||
if (isDetail.value) {
|
||||
item.price = floatToFixed2(item.price)
|
||||
@@ -154,11 +125,11 @@ const submitForm = async () => {
|
||||
// 校验各表单
|
||||
await unref(infoRef)?.validate()
|
||||
await unref(skuRef)?.validate()
|
||||
await unref(deliveryRef)?.validate()
|
||||
await unref(descriptionRef)?.validate()
|
||||
await unref(otherRef)?.validate()
|
||||
// 深拷贝一份, 这样最终 server 端不满足,不需要影响原始数据
|
||||
const deepCopyFormData = cloneDeep(unref(formData.value)) as ProductSpuApi.Spu
|
||||
const deepCopyFormData = cloneDeep(unref(formData.value)) as Spu
|
||||
// 物流设置:静默固定为自提柜
|
||||
deepCopyFormData.deliveryTypes = [3]
|
||||
deepCopyFormData.deliveryTemplateId = undefined
|
||||
deepCopyFormData.skus!.forEach((item) => {
|
||||
// 给sku name赋值
|
||||
item.name = deepCopyFormData.name
|
||||
@@ -181,14 +152,14 @@ const submitForm = async () => {
|
||||
})
|
||||
deepCopyFormData.sliderPicUrls = newSliderPicUrls
|
||||
// 校验都通过后提交表单
|
||||
const data = deepCopyFormData as ProductSpuApi.Spu;
|
||||
const data = deepCopyFormData as Spu;
|
||||
data.types = 1;
|
||||
const id = params.id as unknown as number
|
||||
if (!id) {
|
||||
await ProductSpuApi.createSpu(data)
|
||||
await DeviceSpuApi.createDeviceSpu(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await ProductSpuApi.updateSpu(data)
|
||||
await DeviceSpuApi.updateDeviceSpu(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
close()
|
||||
|
||||
@@ -29,6 +29,22 @@
|
||||
placeholder="请选择商品分类"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="设备号" prop="deviceId">
|
||||
<el-select
|
||||
v-model="queryParams.deviceId"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择设备"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in deviceOptions"
|
||||
:key="item.id"
|
||||
:label="`${item.name}(${item.deviceId}` + ')'"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
@@ -214,7 +230,7 @@
|
||||
<template #default="{ row }"> ¥ {{ fenToYuan(row.price) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="排序" min-width="80" prop="sort" />
|
||||
<el-table-column label="创建人" min-width="100" prop="username" />
|
||||
<el-table-column label="创建人" min-width="100" prop="creatorName" />
|
||||
<el-table-column label="商品编号" min-width="100" prop="id" />
|
||||
<el-table-column label="销量" min-width="80" prop="salesCount" />
|
||||
<el-table-column
|
||||
@@ -300,8 +316,9 @@ import { defaultProps, handleTree, treeToString } from '@/utils/tree'
|
||||
import { ProductSpuStatusEnum } from '@/utils/constants'
|
||||
import { fenToYuan } from '@/utils'
|
||||
import download from '@/utils/download'
|
||||
import * as ProductSpuApi from '@/api/mall/product/spu'
|
||||
import { DeviceSpuApi, type Spu } from '@/api/mall/product/device-spu'
|
||||
import * as ProductCategoryApi from '@/api/mall/product/category'
|
||||
import { DeviceApi, type DeviceVO } from '@/api/product/device'
|
||||
|
||||
defineOptions({ name: 'ProductPickup' })
|
||||
|
||||
@@ -313,8 +330,8 @@ const { push } = useRouter() // 路由跳转
|
||||
const loading = ref(false) // 列表的加载中
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref<ProductSpuApi.Spu[]>([]) // 列表的数据
|
||||
const selectedRows = ref<ProductSpuApi.Spu[]>([]) // 选中的商品
|
||||
const list = ref<Spu[]>([]) // 列表的数据
|
||||
const selectedRows = ref<Spu[]>([]) // 选中的商品
|
||||
const copyLoading = ref(false) // 复制操作的加载中
|
||||
const channelDialogVisible = ref(false) // 修改货道号弹窗显示状态
|
||||
const channelLoading = ref(false) // 修改货道号加载中
|
||||
@@ -358,10 +375,14 @@ const queryParams = ref({
|
||||
name: '',
|
||||
categoryId: undefined,
|
||||
createTime: undefined,
|
||||
types:1
|
||||
deviceId: undefined as number | undefined,
|
||||
types: 1,
|
||||
}) // 查询参数
|
||||
const queryFormRef = ref() // 搜索的表单Ref
|
||||
|
||||
/** 设备下拉选项 */
|
||||
const deviceOptions = ref<DeviceVO[]>([])
|
||||
|
||||
const normalizeNumericString = (value?: number | string | null) => {
|
||||
if (value === undefined || value === null) {
|
||||
return ''
|
||||
@@ -369,7 +390,7 @@ const normalizeNumericString = (value?: number | string | null) => {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const getDeviceIdValue = (spu: ProductSpuApi.Spu) => {
|
||||
const getDeviceIdValue = (spu: Spu) => {
|
||||
if (spu.deviceId !== undefined && spu.deviceId !== null) {
|
||||
return normalizeNumericString(spu.deviceId)
|
||||
}
|
||||
@@ -391,7 +412,7 @@ const getCabinetChannelValue = (spu: ProductSpuApi.Spu) => {
|
||||
return sortedChannels[0]
|
||||
}
|
||||
|
||||
const compareByDeviceAndCabinet = (a: ProductSpuApi.Spu, b: ProductSpuApi.Spu) => {
|
||||
const compareByDeviceAndCabinet = (a: Spu, b: Spu) => {
|
||||
const deviceA = getDeviceIdValue(a)
|
||||
const deviceB = getDeviceIdValue(b)
|
||||
const deviceCompare = deviceA.localeCompare(deviceB, undefined, { numeric: true, sensitivity: 'base' })
|
||||
@@ -411,13 +432,16 @@ const compareByDeviceAndCabinet = (a: ProductSpuApi.Spu, b: ProductSpuApi.Spu) =
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await ProductSpuApi.getSpuPage(queryParams.value)
|
||||
const data = await DeviceSpuApi.getDeviceSpuPage(queryParams.value)
|
||||
const sortedList = [...data.list].sort(compareByDeviceAndCabinet)
|
||||
// 过滤掉没有设备号的商品
|
||||
const filteredList = sortedList.filter((item) => !!getDeviceIdValue(item))
|
||||
list.value = filteredList
|
||||
total.value = filteredList.length
|
||||
console.log('查询列表',data)
|
||||
// 使用后端返回的总数,而不是过滤后的列表长度
|
||||
// 注意:如果前端过滤导致当前页数据减少,分页总数可能不准确
|
||||
// 理想情况下,过滤逻辑应该在后端完成
|
||||
total.value = data.total || 0
|
||||
console.log('查询列表', data)
|
||||
const cabinetChannels = filteredList
|
||||
.flatMap((item) => item.skus ?? [])
|
||||
.map((sku) => sku.cabinetChannel)
|
||||
@@ -440,10 +464,11 @@ const handleTabClick = (tab: TabsPaneContext) => {
|
||||
|
||||
/** 获得每个 Tab 的数量 */
|
||||
const getTabsCount = async () => {
|
||||
const res = await ProductSpuApi.getTabsCount(1);
|
||||
for (let objName in res) {
|
||||
tabsData.value[Number(objName)].count = res[objName]
|
||||
}
|
||||
// TODO: 新 API 中暂无此接口,暂时注释
|
||||
// const res = await DeviceSpuApi.getTabsCount(1);
|
||||
// for (let objName in res) {
|
||||
// tabsData.value[Number(objName)].count = res[objName]
|
||||
// }
|
||||
}
|
||||
|
||||
/** 添加到仓库 / 回收站的状态 */
|
||||
@@ -452,8 +477,8 @@ const handleStatus02Change = async (row: any, newStatus: number) => {
|
||||
// 二次确认
|
||||
const text = newStatus === ProductSpuStatusEnum.RECYCLE.status ? '加入到回收站' : '恢复到仓库'
|
||||
await message.confirm(`确认要"${row.name}"${text}吗?`)
|
||||
// 发起修改
|
||||
await ProductSpuApi.updateStatus({ id: row.id, status: newStatus })
|
||||
// TODO: 新 API 中暂无此接口,暂时注释
|
||||
// await DeviceSpuApi.updateStatus({ id: row.id, status: newStatus })
|
||||
message.success(text + '成功')
|
||||
// 刷新 tabs 数据
|
||||
await getTabsCount()
|
||||
@@ -468,8 +493,8 @@ const handleStatusChange = async (row: any) => {
|
||||
// 二次确认
|
||||
const text = row.status ? '上架' : '下架'
|
||||
await message.confirm(`确认要${text}"${row.name}"吗?`)
|
||||
// 发起修改
|
||||
await ProductSpuApi.updateStatus({ id: row.id, status: row.status })
|
||||
// TODO: 新 API 中暂无此接口,暂时注释
|
||||
// await DeviceSpuApi.updateStatus({ id: row.id, status: row.status })
|
||||
message.success(text + '成功')
|
||||
// 刷新 tabs 数据
|
||||
await getTabsCount()
|
||||
@@ -490,7 +515,7 @@ const handleDelete = async (id: number) => {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await ProductSpuApi.deleteSpu(id)
|
||||
await DeviceSpuApi.deleteDeviceSpu(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新tabs数据
|
||||
await getTabsCount()
|
||||
@@ -529,7 +554,7 @@ const openForm = (id?: number) => {
|
||||
}
|
||||
|
||||
/** 处理表格选择变化 */
|
||||
const handleSelectionChange = (selection: ProductSpuApi.Spu[]) => {
|
||||
const handleSelectionChange = (selection: Spu[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
@@ -548,11 +573,11 @@ const handleCopy = async () => {
|
||||
for (const row of selectedRows.value) {
|
||||
try {
|
||||
// 获取商品详情
|
||||
const productDetail = await ProductSpuApi.getSpu(row.id!)
|
||||
const productDetail = await DeviceSpuApi.getDeviceSpu(row.id!)
|
||||
|
||||
// 准备新商品数据,复制所有内容,但不传递商品编号(id)和SKU的id,后端会自动生成
|
||||
const { id, createTime, skus, ...spuData } = productDetail
|
||||
const newProduct: ProductSpuApi.Spu = {
|
||||
const newProduct: Spu = {
|
||||
...spuData, // 复制所有SPU字段(除了id和createTime)
|
||||
// 复制SKU时不传递id和spuId,其他所有数据保持原样(包括name字段)
|
||||
skus: skus?.map(sku => {
|
||||
@@ -568,7 +593,7 @@ const handleCopy = async () => {
|
||||
console.log('复制新增的商品数据:', newProduct)
|
||||
|
||||
// 创建新商品
|
||||
await ProductSpuApi.createSpu(newProduct)
|
||||
await DeviceSpuApi.createDeviceSpu(newProduct)
|
||||
} catch (error) {
|
||||
console.error(`复制商品 ${row.name} 失败:`, error)
|
||||
message.error(`复制商品 "${row.name}" 失败`)
|
||||
@@ -618,7 +643,7 @@ const handleConfirmModifyChannel = async () => {
|
||||
for (const row of selectedRows.value) {
|
||||
try {
|
||||
// 获取商品详情
|
||||
const productDetail = await ProductSpuApi.getSpu(row.id!)
|
||||
const productDetail = await DeviceSpuApi.getDeviceSpu(row.id!)
|
||||
|
||||
// 更新 skus 中的货道号,name 使用 spu 的 name,其他参数保持不变
|
||||
const updatedSkus = productDetail.skus?.map(sku => ({
|
||||
@@ -628,13 +653,13 @@ const handleConfirmModifyChannel = async () => {
|
||||
})) || []
|
||||
|
||||
// 准备更新数据,只修改 skus 的货道号,其他参数保持原样
|
||||
const updateData: ProductSpuApi.Spu = {
|
||||
const updateData: Spu = {
|
||||
...productDetail,
|
||||
skus: updatedSkus
|
||||
}
|
||||
|
||||
// 调用更新接口
|
||||
await ProductSpuApi.updateSpu(updateData)
|
||||
await DeviceSpuApi.updateDeviceSpu(updateData)
|
||||
} catch (error) {
|
||||
console.error(`修改商品 ${row.name} 货道号失败:`, error)
|
||||
message.error(`修改商品 "${row.name}" 货道号失败`)
|
||||
@@ -666,8 +691,10 @@ const handleExport = async () => {
|
||||
await message.exportConfirm()
|
||||
// 发起导出
|
||||
exportLoading.value = true
|
||||
const data = await ProductSpuApi.exportSpu(queryParams.value)
|
||||
download.excel(data, '商品列表.xls')
|
||||
// TODO: 新 API 中暂无此接口,暂时注释
|
||||
// const data = await DeviceSpuApi.exportDeviceSpu(queryParams.value)
|
||||
// download.excel(data, '商品列表.xls')
|
||||
message.warning('导出功能暂未实现')
|
||||
} catch {
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
@@ -695,6 +722,12 @@ onMounted(async () => {
|
||||
queryParams.value.categoryId = id as any
|
||||
}
|
||||
}
|
||||
// 获取设备下拉数据
|
||||
try {
|
||||
deviceOptions.value = await DeviceApi.getDeviceListSimpleByUser()
|
||||
} catch (e) {
|
||||
console.error('加载设备列表失败', e)
|
||||
}
|
||||
// 获得商品信息
|
||||
await getTabsCount()
|
||||
await getList()
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<Dialog :title="'批量新增货道'" v-model="dialogVisible" width="80%" :close-on-click-modal="false">
|
||||
<div v-loading="formLoading">
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px"
|
||||
>
|
||||
<template #default>
|
||||
<div>设备:{{ deviceInfo?.name }} ({{ deviceInfo?.deviceId }})</div>
|
||||
<div style="margin-top: 8px;">提示:可以添加多个货道,每个货道需要填写货道号、最大数量、行号等信息</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<el-form ref="formRef" :model="formData" label-width="100px">
|
||||
<el-form-item label="货道列表">
|
||||
<div class="channel-list-container">
|
||||
<div
|
||||
v-for="(channel, index) in formData.channels"
|
||||
:key="index"
|
||||
class="channel-item-row"
|
||||
>
|
||||
<div class="item-header">
|
||||
<span class="item-number">货道 {{ index + 1 }}</span>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="removeChannel(index)"
|
||||
:disabled="formData.channels.length <= 1"
|
||||
>
|
||||
<Icon icon="ep:delete" /> 删除
|
||||
</el-button>
|
||||
</div>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-form-item
|
||||
:prop="`channels.${index}.name`"
|
||||
:rules="[{ required: false, message: '请输入名称', trigger: 'blur' }]"
|
||||
label="名称"
|
||||
>
|
||||
<el-input v-model="channel.name" placeholder="货道名称(可选)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item
|
||||
:prop="`channels.${index}.cabinetChannel`"
|
||||
:rules="[{ required: true, message: '请输入货道号', trigger: 'blur' }]"
|
||||
label="货道号"
|
||||
>
|
||||
<el-input v-model="channel.cabinetChannel" placeholder="如:A01" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item
|
||||
:prop="`channels.${index}.maxNum`"
|
||||
:rules="[
|
||||
{ required: true, message: '请输入最大数量', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '数量必须大于0', trigger: 'blur' }
|
||||
]"
|
||||
label="最大数量"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="channel.maxNum"
|
||||
:min="1"
|
||||
:max="999"
|
||||
placeholder="最大货物数"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item
|
||||
:prop="`channels.${index}.channelLine`"
|
||||
:rules="[
|
||||
{ required: true, message: '请输入行号', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '行号必须大于0', trigger: 'blur' }
|
||||
]"
|
||||
label="行号"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="channel.channelLine"
|
||||
:min="1"
|
||||
:max="999"
|
||||
placeholder="货道行号"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="addChannel"
|
||||
style="width: 100%; margin-top: 16px"
|
||||
>
|
||||
<Icon icon="ep:plus" /> 添加货道
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitForm" :disabled="formLoading" :loading="formLoading">
|
||||
批量创建
|
||||
</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChannelApi, ChannelVO } from '@/api/mall/product/channel'
|
||||
import { DeviceVO } from '@/api/product/device'
|
||||
|
||||
defineOptions({ name: 'ChannelBatchForm' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const formLoading = ref(false)
|
||||
const deviceInfo = ref<DeviceVO | null>(null)
|
||||
const formRef = ref()
|
||||
|
||||
const formData = ref({
|
||||
channels: [
|
||||
{
|
||||
name: undefined,
|
||||
cabinetChannel: undefined,
|
||||
maxNum: undefined,
|
||||
channelLine: undefined
|
||||
}
|
||||
] as Partial<ChannelVO>[]
|
||||
})
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = (device: DeviceVO) => {
|
||||
dialogVisible.value = true
|
||||
deviceInfo.value = device
|
||||
resetForm()
|
||||
}
|
||||
defineExpose({ open })
|
||||
|
||||
/** 添加货道 */
|
||||
const addChannel = () => {
|
||||
formData.value.channels.push({
|
||||
name: undefined,
|
||||
cabinetChannel: undefined,
|
||||
maxNum: undefined,
|
||||
channelLine: undefined
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除货道 */
|
||||
const removeChannel = (index: number) => {
|
||||
if (formData.value.channels.length > 1) {
|
||||
formData.value.channels.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success'])
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
if (!deviceInfo.value) {
|
||||
message.error('设备信息不存在')
|
||||
return
|
||||
}
|
||||
|
||||
// 构建提交数据
|
||||
const channels = formData.value.channels.map(channel => ({
|
||||
name: channel.name,
|
||||
deviceId: deviceInfo.value!.id,
|
||||
deviceImei: deviceInfo.value!.deviceId,
|
||||
cabinetChannel: channel.cabinetChannel,
|
||||
maxNum: channel.maxNum,
|
||||
channelLine: channel.channelLine
|
||||
}))
|
||||
|
||||
formLoading.value = true
|
||||
const result = await ChannelApi.batchCreateChannel({ channels })
|
||||
|
||||
// 显示结果
|
||||
if (result?.data) {
|
||||
const { successIds = [], failureInfos = {}, successCount = 0, failureCount = 0 } = result.data
|
||||
|
||||
if (failureCount > 0) {
|
||||
// 有失败的情况,显示详细信息
|
||||
const failureDetails = Object.entries(failureInfos)
|
||||
.map(([name, reason]) => `"${name}": ${reason}`)
|
||||
.join('\n')
|
||||
|
||||
message.warning(
|
||||
`批量创建完成!\n成功:${successCount} 个\n失败:${failureCount} 个\n\n失败详情:\n${failureDetails}`,
|
||||
{ duration: 8000 }
|
||||
)
|
||||
} else {
|
||||
// 全部成功
|
||||
message.success(`批量创建成功!共创建 ${successCount} 个货道`)
|
||||
}
|
||||
} else {
|
||||
message.success('批量创建成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} catch (error: any) {
|
||||
if (error?.message && !error.message.includes('validate')) {
|
||||
message.error(error.message || '批量创建失败')
|
||||
}
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
channels: [
|
||||
{
|
||||
name: undefined,
|
||||
cabinetChannel: undefined,
|
||||
maxNum: undefined,
|
||||
channelLine: undefined
|
||||
}
|
||||
]
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.channel-list-container {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.channel-item-row {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid #e4e7ed;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px dashed #e4e7ed;
|
||||
|
||||
.item-number {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+55
-11
@@ -10,8 +10,14 @@
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="设备IMEI" prop="deviceId">
|
||||
<el-input v-model="formData.deviceId" placeholder="请输入设备IMEI" />
|
||||
<el-form-item label="设备" prop="deviceId">
|
||||
<el-input
|
||||
v-if="deviceInfo"
|
||||
:value="`${deviceInfo.name} (${deviceInfo.deviceId})`"
|
||||
disabled
|
||||
placeholder="设备信息"
|
||||
/>
|
||||
<el-input v-else v-model="formData.deviceId" placeholder="请输入设备IMEI" />
|
||||
</el-form-item>
|
||||
<el-form-item label="货道号" prop="cabinetChannel">
|
||||
<el-input v-model="formData.cabinetChannel" placeholder="请输入货道号" />
|
||||
@@ -31,6 +37,7 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ChannelApi, ChannelVO } from '@/api/mall/product/channel'
|
||||
import { DeviceVO } from '@/api/product/device'
|
||||
|
||||
// import { ChannelApi, ChannelVO } from '@/api/product/channel'
|
||||
|
||||
@@ -44,30 +51,46 @@ const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||
const deviceInfo = ref<DeviceVO | null>(null) // 设备信息
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
deviceId: undefined,
|
||||
deviceId: undefined as number | string | undefined,
|
||||
deviceImei: undefined as string | undefined,
|
||||
cabinetChannel: undefined,
|
||||
maxNum: undefined,
|
||||
channelLine: undefined
|
||||
})
|
||||
const formRules = reactive({
|
||||
deviceId: [{ required: true, message: '设备IMEI不能为空', trigger: 'blur' }]
|
||||
deviceId: [{ required: true, message: '设备不能为空', trigger: 'blur' }],
|
||||
cabinetChannel: [{ required: true, message: '货道号不能为空', trigger: 'blur' }],
|
||||
maxNum: [{ required: true, message: '最大数量不能为空', trigger: 'blur' }],
|
||||
channelLine: [{ required: true, message: '货道行号不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number) => {
|
||||
const open = async (type: string, id?: number, device?: DeviceVO) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = t('action.' + type)
|
||||
dialogTitle.value = type === 'create' ? '新增货道' : '修改货道'
|
||||
formType.value = type
|
||||
deviceInfo.value = device || null
|
||||
resetForm()
|
||||
// 如果有设备信息,自动填充
|
||||
if (device) {
|
||||
formData.value.deviceId = device.id
|
||||
formData.value.deviceImei = device.deviceId
|
||||
}
|
||||
// 修改时,设置数据
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
formData.value = await ChannelApi.getChannel(id)
|
||||
const channelData = await ChannelApi.getChannel(id)
|
||||
formData.value = {
|
||||
...channelData,
|
||||
deviceId: device?.id || channelData.deviceId,
|
||||
deviceImei: device?.deviceId || channelData.deviceId
|
||||
}
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
@@ -83,12 +106,32 @@ const submitForm = async () => {
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = formData.value as unknown as ChannelVO
|
||||
// 构建提交数据,优先使用deviceId(number),如果没有则使用deviceImei
|
||||
const submitData: any = {
|
||||
...formData.value
|
||||
}
|
||||
|
||||
// 如果有设备ID(number类型),优先使用
|
||||
if (typeof submitData.deviceId === 'number') {
|
||||
// 保留deviceId
|
||||
} else if (submitData.deviceImei) {
|
||||
// 如果没有deviceId但有deviceImei,使用deviceImei
|
||||
submitData.deviceImei = submitData.deviceImei
|
||||
delete submitData.deviceId
|
||||
}
|
||||
|
||||
// 清理undefined字段
|
||||
Object.keys(submitData).forEach(key => {
|
||||
if (submitData[key] === undefined) {
|
||||
delete submitData[key]
|
||||
}
|
||||
})
|
||||
|
||||
if (formType.value === 'create') {
|
||||
await ChannelApi.createChannel(data)
|
||||
await ChannelApi.createChannel(submitData)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await ChannelApi.updateChannel(data)
|
||||
await ChannelApi.updateChannel(submitData)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
@@ -104,7 +147,8 @@ const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
deviceId: undefined,
|
||||
deviceId: deviceInfo.value?.id || undefined,
|
||||
deviceImei: deviceInfo.value?.deviceId || undefined,
|
||||
cabinetChannel: undefined,
|
||||
maxNum: undefined,
|
||||
channelLine: undefined
|
||||
File renamed without changes.
@@ -0,0 +1,419 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" :title="`货道管理 - ${deviceInfo?.name || ''}`" width="90%" :close-on-click-modal="false">
|
||||
<div v-loading="loading" class="channel-manage-container">
|
||||
<!-- 设备信息栏 -->
|
||||
<div class="device-info-bar">
|
||||
<div class="device-info-item">
|
||||
<span class="label">设备名称:</span>
|
||||
<span class="value">{{ deviceInfo?.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="device-info-item">
|
||||
<span class="label">设备IMEI:</span>
|
||||
<span class="value">{{ deviceInfo?.deviceId || '-' }}</span>
|
||||
</div>
|
||||
<div class="device-info-item">
|
||||
<span class="label">货道总数:</span>
|
||||
<span class="value highlight">{{ channelList.length }}</span>
|
||||
</div>
|
||||
<div class="device-info-actions">
|
||||
<el-button type="primary" @click="handleAddChannel" v-hasPermi="['product:channel:create']">
|
||||
<Icon icon="ep:plus" /> 新增货道
|
||||
</el-button>
|
||||
<el-button type="warning" @click="handleBatchAddChannel" v-hasPermi="['product:channel:create']">
|
||||
<Icon icon="ep:plus" /> 批量新增
|
||||
</el-button>
|
||||
<el-button type="success" @click="handleRefresh">
|
||||
<Icon icon="ep:refresh" /> 刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 售货机可视化区域 -->
|
||||
<div class="vending-machine-view">
|
||||
<div class="machine-header">
|
||||
<div class="machine-title">{{ deviceInfo?.name || '售货机' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 按行号分组的货道展示 -->
|
||||
<div v-if="groupedChannels.size > 0" class="channels-by-line">
|
||||
<div
|
||||
v-for="[lineNumber, channels] in groupedChannels"
|
||||
:key="lineNumber"
|
||||
class="channel-line-group"
|
||||
>
|
||||
<div class="line-header">
|
||||
<span class="line-label">第 {{ lineNumber }} 行</span>
|
||||
<span class="line-count">共 {{ channels.length }} 个货道</span>
|
||||
</div>
|
||||
<div class="channels-grid">
|
||||
<div
|
||||
v-for="channel in channels"
|
||||
:key="channel.id"
|
||||
class="channel-item"
|
||||
:class="{ 'has-product': channel.maxNum > 0 }"
|
||||
@click="handleEditChannel(channel)"
|
||||
>
|
||||
<div class="channel-number">{{ channel.cabinetChannel }}</div>
|
||||
<div class="channel-name">{{ channel.name || `货道${channel.cabinetChannel}` }}</div>
|
||||
<div class="channel-info">
|
||||
<div class="info-row">
|
||||
<span class="info-label">行号:</span>
|
||||
<span class="info-value">{{ channel.channelLine }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">容量:</span>
|
||||
<span class="info-value">{{ channel.maxNum }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="channel-actions">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click.stop="handleEditChannel(channel)"
|
||||
v-hasPermi="['product:channel:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="success"
|
||||
size="small"
|
||||
@click.stop="handleTestChannel(channel)"
|
||||
v-hasPermi="['product:channel:test']"
|
||||
>
|
||||
测试
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click.stop="handleDeleteChannel(channel)"
|
||||
v-hasPermi="['product:channel:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty v-else description="暂无货道,请点击上方按钮新增货道" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 货道表单对话框 -->
|
||||
<ChannelForm ref="channelFormRef" @success="handleRefresh" />
|
||||
<!-- 批量新增货道对话框 -->
|
||||
<ChannelBatchForm ref="batchFormRef" @success="handleRefresh" />
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DeviceVO } from '@/api/product/device'
|
||||
import { ChannelApi, ChannelVO } from '@/api/mall/product/channel'
|
||||
import ChannelForm from './ChannelForm.vue'
|
||||
import ChannelBatchForm from './ChannelBatchForm.vue'
|
||||
|
||||
defineOptions({ name: 'ChannelManageDialog' })
|
||||
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const deviceInfo = ref<DeviceVO | null>(null)
|
||||
const channelList = ref<ChannelVO[]>([])
|
||||
const channelFormRef = ref()
|
||||
|
||||
/** 打开对话框 */
|
||||
const open = async (device: DeviceVO) => {
|
||||
dialogVisible.value = true
|
||||
deviceInfo.value = device
|
||||
await loadChannels()
|
||||
}
|
||||
defineExpose({ open })
|
||||
|
||||
/** 加载货道列表 */
|
||||
const loadChannels = async () => {
|
||||
if (!deviceInfo.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await ChannelApi.getChannelPage({
|
||||
pageNo: 1,
|
||||
pageSize: 100, // 最大值为 100
|
||||
deviceId: deviceInfo.value.id,
|
||||
deviceImei: deviceInfo.value.deviceId // 兼容字段
|
||||
})
|
||||
channelList.value = data.list || []
|
||||
} catch (error) {
|
||||
console.error('加载货道列表失败:', error)
|
||||
message.error('加载货道列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 按行号分组的货道列表 */
|
||||
const groupedChannels = computed(() => {
|
||||
const groups = new Map<number, ChannelVO[]>()
|
||||
|
||||
// 先排序
|
||||
const sorted = [...channelList.value].sort((a, b) => {
|
||||
if (a.channelLine !== b.channelLine) {
|
||||
return a.channelLine - b.channelLine
|
||||
}
|
||||
return Number(a.cabinetChannel) - Number(b.cabinetChannel)
|
||||
})
|
||||
|
||||
// 按行号分组
|
||||
sorted.forEach(channel => {
|
||||
const line = channel.channelLine || 0
|
||||
if (!groups.has(line)) {
|
||||
groups.set(line, [])
|
||||
}
|
||||
groups.get(line)!.push(channel)
|
||||
})
|
||||
|
||||
// 转换为有序的 Map
|
||||
return new Map([...groups.entries()].sort((a, b) => a[0] - b[0]))
|
||||
})
|
||||
|
||||
/** 新增货道 */
|
||||
const handleAddChannel = () => {
|
||||
if (!deviceInfo.value) return
|
||||
channelFormRef.value.open('create', undefined, deviceInfo.value)
|
||||
}
|
||||
|
||||
/** 批量新增货道 */
|
||||
const batchFormRef = ref()
|
||||
const handleBatchAddChannel = () => {
|
||||
if (!deviceInfo.value) return
|
||||
batchFormRef.value.open(deviceInfo.value)
|
||||
}
|
||||
|
||||
/** 编辑货道 */
|
||||
const handleEditChannel = (channel: ChannelVO) => {
|
||||
channelFormRef.value.open('update', channel.id, deviceInfo.value)
|
||||
}
|
||||
|
||||
/** 删除货道 */
|
||||
const handleDeleteChannel = async (channel: ChannelVO) => {
|
||||
try {
|
||||
await message.delConfirm(`确定要删除货道"${channel.name || channel.cabinetChannel}"吗?`)
|
||||
await ChannelApi.deleteChannel(channel.id)
|
||||
message.success(t('common.delSuccess'))
|
||||
await loadChannels()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 测试货道 */
|
||||
const handleTestChannel = async (channel: ChannelVO) => {
|
||||
try {
|
||||
await message.confirm(`确定要测试货道"${channel.name || channel.cabinetChannel}"吗?`)
|
||||
loading.value = true
|
||||
const result = await ChannelApi.testChannel(channel.id)
|
||||
if (result?.data) {
|
||||
message.success(result.data)
|
||||
} else {
|
||||
message.success('测试成功')
|
||||
}
|
||||
} catch {}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新 */
|
||||
const handleRefresh = () => {
|
||||
loadChannels()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.channel-manage-container {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.device-info-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
color: white;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
|
||||
.device-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
&.highlight {
|
||||
color: #ffd700;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.device-info-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.vending-machine-view {
|
||||
background: #f5f7fa;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.machine-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.machine-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.channels-by-line {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.channel-line-group {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.line-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid #e4e7ed;
|
||||
|
||||
.line-label {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.line-count {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.channels-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.channel-item {
|
||||
background: white;
|
||||
border: 2px solid #e4e7ed;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.has-product {
|
||||
border-color: #67c23a;
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
||||
}
|
||||
|
||||
.channel-number {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.channel-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.channel-info {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
|
||||
.info-label {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channel-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-empty) {
|
||||
padding: 60px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -70,6 +70,14 @@ type="success" plain @click="handleExport" :loading="exportLoading"
|
||||
<el-button link type="primary" @click="refreshWechatCode(row)">
|
||||
刷新小程序二维码
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="success"
|
||||
@click="handleChannelManage(row)"
|
||||
v-hasPermi="['product:channel:query']"
|
||||
>
|
||||
<Icon icon="ep:grid" class="mr-5px" />货道管理
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="openForm('update', row.id)" v-hasPermi="['product:device:update']">
|
||||
编辑
|
||||
</el-button>
|
||||
@@ -88,6 +96,8 @@ type="success" plain @click="handleExport" :loading="exportLoading"
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<DeviceForm ref="formRef" @success="handleFormSuccess" />
|
||||
<!-- 货道管理对话框 -->
|
||||
<ChannelManageDialog ref="channelManageRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -95,6 +105,7 @@ import { dateFormatter } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import { DeviceApi, DeviceVO } from '@/api/product/device'
|
||||
import DeviceForm from './DeviceForm.vue'
|
||||
import ChannelManageDialog from './ChannelManageDialog.vue'
|
||||
import { createImageViewer } from '@/components/ImageViewer'
|
||||
import { ElLoading } from 'element-plus'
|
||||
|
||||
@@ -204,6 +215,12 @@ const handleExport = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 货道管理 */
|
||||
const channelManageRef = ref()
|
||||
const handleChannelManage = (row: DeviceVO) => {
|
||||
channelManageRef.value.open(row)
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" title="绑定设备">
|
||||
<el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
|
||||
<el-form-item label="用户名称">
|
||||
<el-input v-model="formData.username" :disabled="true" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户昵称">
|
||||
<el-input v-model="formData.nickname" :disabled="true" />
|
||||
</el-form-item>
|
||||
<el-form-item label="绑定设备">
|
||||
<el-select
|
||||
v-model="formData.deviceIds"
|
||||
multiple
|
||||
placeholder="请选择设备"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="device in deviceList"
|
||||
:key="device.id"
|
||||
:label="`${device.name} (${device.deviceId})`"
|
||||
:value="device.id"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="tip" style="margin-top: 8px; color: #909399; font-size: 12px">
|
||||
可绑定多个设备,设备下拉列表会根据当前用户绑定的设备自动过滤
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import { DeviceApi, DeviceVO } from '@/api/product/device'
|
||||
|
||||
defineOptions({ name: 'SystemUserDeviceBindingForm' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formData = ref({
|
||||
id: -1,
|
||||
nickname: '',
|
||||
username: '',
|
||||
deviceIds: [] as number[]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
const deviceList = ref([] as DeviceVO[]) // 设备列表
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (row: UserApi.UserVO) => {
|
||||
dialogVisible.value = true
|
||||
resetForm()
|
||||
// 设置数据
|
||||
formData.value.id = row.id
|
||||
formData.value.username = row.username
|
||||
formData.value.nickname = row.nickname
|
||||
// 加载用户已绑定的设备
|
||||
formLoading.value = true
|
||||
try {
|
||||
// 获取用户已绑定的设备ID列表
|
||||
const deviceIdsRes = await UserApi.getUserDeviceIds(row.id)
|
||||
if (deviceIdsRes?.code === 0) {
|
||||
formData.value.deviceIds = deviceIdsRes.data?.deviceIds || []
|
||||
} else if (deviceIdsRes?.deviceIds) {
|
||||
// 兼容直接返回数据的情况
|
||||
formData.value.deviceIds = deviceIdsRes.deviceIds || []
|
||||
}
|
||||
// 获取设备下拉列表(自动根据当前登录用户过滤)
|
||||
const deviceRes = await DeviceApi.getDeviceListAllSimple()
|
||||
if (deviceRes?.code === 0) {
|
||||
deviceList.value = deviceRes.data || []
|
||||
} else {
|
||||
// 兼容直接返回数据的情况
|
||||
deviceList.value = deviceRes || []
|
||||
}
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
// 先获取当前已绑定的设备
|
||||
const currentDeviceIdsRes = await UserApi.getUserDeviceIds(formData.value.id)
|
||||
let currentDeviceIds: number[] = []
|
||||
if (currentDeviceIdsRes?.code === 0) {
|
||||
currentDeviceIds = currentDeviceIdsRes.data?.deviceIds || []
|
||||
} else if (currentDeviceIdsRes?.deviceIds) {
|
||||
// 兼容直接返回数据的情况
|
||||
currentDeviceIds = currentDeviceIdsRes.deviceIds || []
|
||||
}
|
||||
|
||||
// 计算需要绑定的设备(新增的)
|
||||
const newDeviceIds = formData.value.deviceIds.filter(id => !currentDeviceIds.includes(id))
|
||||
// 计算需要解绑的设备(删除的)
|
||||
const removeDeviceIds = currentDeviceIds.filter(id => !formData.value.deviceIds.includes(id))
|
||||
|
||||
// 执行绑定操作
|
||||
if (newDeviceIds.length > 0) {
|
||||
await UserApi.bindUserDevices({
|
||||
userId: formData.value.id,
|
||||
deviceIds: newDeviceIds
|
||||
})
|
||||
}
|
||||
|
||||
// 执行解绑操作
|
||||
if (removeDeviceIds.length > 0) {
|
||||
await UserApi.unbindUserDevices({
|
||||
userId: formData.value.id,
|
||||
deviceIds: removeDeviceIds
|
||||
})
|
||||
}
|
||||
|
||||
message.success(t('common.updateSuccess'))
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success', true)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: -1,
|
||||
nickname: '',
|
||||
username: '',
|
||||
deviceIds: []
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in new issue
Block a user