# Conflicts:
#	.env.local
This commit is contained in:
dzc committed 2025-12-17 16:15:13 +08:00
commit c553dbd5f8
44 files changed
+788 -190

No files matched your search

+29
View File
@@ -24,6 +24,7 @@
"benz-amr-recorder": "^1.1.5",
"bpmn-js-token-simulation": "^0.36.0",
"camunda-bpmn-moddle": "^7.0.1",
"compressorjs": "^1.2.1",
"cropperjs": "^1.6.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
@@ -7298,6 +7299,12 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true
},
"node_modules/blueimp-canvas-to-blob": {
"version": "3.29.0",
"resolved": "https://registry.npmmirror.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz",
"integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg==",
"license": "MIT"
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz",
@@ -8020,6 +8027,16 @@
"resolved": "https://registry.npmmirror.com/component-event/-/component-event-0.2.1.tgz",
"integrity": "sha512-wGA++isMqiDq1jPYeyv2as/Bt/u+3iLW0rEa+8NQ82jAv3TgqMiCM+B2SaBdn2DfLilLjjq736YcezihRYhfxw=="
},
"node_modules/compressorjs": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/compressorjs/-/compressorjs-1.2.1.tgz",
"integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==",
"license": "MIT",
"dependencies": {
"blueimp-canvas-to-blob": "^3.29.0",
"is-blob": "^2.1.0"
}
},
"node_modules/compute-scroll-into-view": {
"version": "1.0.20",
"resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
@@ -11515,6 +11532,18 @@
"node": ">=8"
}
},
"node_modules/is-blob": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/is-blob/-/is-blob-2.1.0.tgz",
"integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-boolean-object": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+2 -1
View File
@@ -40,6 +40,7 @@
"benz-amr-recorder": "^1.1.5",
"bpmn-js-token-simulation": "^0.36.0",
"camunda-bpmn-moddle": "^7.0.1",
"compressorjs": "^1.2.1",
"cropperjs": "^1.6.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
@@ -152,4 +153,4 @@
"node": ">= 16.0.0",
"pnpm": ">=8.6.0"
}
}
}
+2 -1
View File
@@ -1,5 +1,4 @@
import request from '@/config/axios'
import { useUserStore } from '@/store/modules/user';
export interface Property {
propertyId?: number // 属性编号
@@ -27,6 +26,7 @@ export interface Sku {
cabinetChannel?: string // 货道或柜门编号
cabinetType?: string // 通道类型
purchaseBizType?: string
deviceId?: number
}
export interface GiveCouponTemplate {
@@ -63,6 +63,7 @@ export interface Spu {
createTime?: Date // 商品创建时间
status?: number // 商品状态
types?: number // 商品类型
deviceId?: number // 设备号
}
// 获得 Spu 列表
@@ -1,31 +1,29 @@
import request from '@/config/axios'
// request.get 返回的是 res.data,所以这里直接定义 data 的结构
export interface GetStatisticsDetailRes {
code: number;
data: {
list: {
id: number;
userId: number;
orderId: number;
spuId: number;
spuName: string;
skuId: number;
deviceId: string;
barCode: string;
cabinetChannel: string;
picUrl: string;
count: number;
price: number;
discountPrice: number;
payPrice: number;
orderPartPrice: number;
orderDividePrice: number;
afterSaleStatus: number;
no: string;
createTime: string; // 修改为正确的类型
}[];
total: number;
};
list: {
id: number;
userId: number;
orderId: number;
spuId: number;
spuName: string;
skuId: number;
deviceId: string;
barCode: string;
cabinetChannel: string;
picUrl: string;
count: number;
price: number;
discountPrice: number;
payPrice: number;
orderPartPrice: number;
orderDividePrice: number;
afterSaleStatus: number;
no: string;
createTime: string;
}[];
total: number;
}
// 查询参数接口
+1
View File
@@ -535,6 +535,7 @@ const inputChange = () => {
append-to-body
destroy-on-close
title="cron规则生成器"
:close-on-click-modal="false"
>
<div class="sc-cron">
<el-tabs>
+1 -1
View File
@@ -60,7 +60,7 @@ const dialogStyle = computed(() => {
<template>
<ElDialog
v-bind="getBindValue"
:close-on-click-modal="true"
:close-on-click-modal="false"
:fullscreen="isFullscreen"
:width="width"
destroy-on-close
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<ElDialog v-if="isModal" v-model="showSearch" :show-close="false" title="菜单搜索">
<ElDialog v-if="isModal" v-model="showSearch" :show-close="false" title="菜单搜索" :close-on-click-modal="false">
<el-select
filterable
:reserve-keyword="false"
@@ -4,7 +4,7 @@
<span class="node-fixed-name" title="结束">结束</span>
</div>
</div>
<el-dialog title="审批信息" v-model="dialogVisible" width="1000px" append-to-body>
<el-dialog title="审批信息" v-model="dialogVisible" width="1000px" append-to-body :close-on-click-modal="false">
<el-row>
<el-table
:data="processInstanceInfos"
@@ -49,7 +49,7 @@
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
<template #default="scope">
{{ formatPast2(scope.row.durationInMillis) }}
@@ -83,7 +83,7 @@ const dialogVisible = ref(false) // 弹窗可见性
const processInstanceInfos = ref<any[]>([]) // 流程的审批信息
const nodeClick = () => {
if (readonly) {
if (readonly) {
if(processInstance && processInstance.value){
processInstanceInfos.value = [
{
@@ -50,6 +50,7 @@
v-model="dialogVisible"
width="1000px"
append-to-body
:close-on-click-modal="false"
>
<el-row>
<el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray">
@@ -65,6 +65,7 @@
v-model="dialogVisible"
width="1000px"
append-to-body
:close-on-click-modal="false"
>
<el-row>
<el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray">
+24 -21
View File
@@ -1,19 +1,8 @@
<template>
<div class="upload-box">
<el-upload
:id="uuid"
:accept="fileType.join(',')"
:action="uploadUrl"
:before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']"
:disabled="disabled"
:drag="drag"
:http-request="httpRequest"
:multiple="false"
:on-error="uploadError"
:on-success="uploadSuccess"
:show-file-list="false"
>
<el-upload :id="uuid" :accept="fileType.join(',')" :action="uploadUrl" :before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']" :disabled="disabled" :drag="drag" :http-request="httpRequest"
:multiple="false" :on-error="uploadError" :on-success="uploadSuccess" :show-file-list="false">
<template v-if="modelValue">
<img :src="modelValue" class="upload-image" />
<div class="upload-handle" @click.stop>
@@ -52,7 +41,7 @@ import type { UploadProps } from 'element-plus'
import { generateUUID } from '@/utils'
import { propTypes } from '@/utils/propTypes'
import { createImageViewer } from '@/components/ImageViewer'
import { useUpload } from '@/components/UploadFile/src/useUpload'
import { compressImage, useUpload } from '@/components/UploadFile/src/useUpload'
defineOptions({ name: 'UploadImg' })
@@ -106,15 +95,27 @@ const editImg = () => {
dom && dom.dispatchEvent(new MouseEvent('click'))
}
const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
const beforeUpload: UploadProps['beforeUpload'] = async (rawFile) => {
const imgSize = rawFile.size / 1024 / 1024 < props.fileSize
const imgType = props.fileType
if (!imgType.includes(rawFile.type as FileTypes))
message.notifyWarning('上传图片不符合所需的格式!')
if (!imgSize) message.notifyWarning(`上传图片大小不能超过 ${props.fileSize}M!`)
return imgType.includes(rawFile.type as FileTypes) && imgSize
}
if (!imgType.includes(rawFile.type as FileTypes)) {
message.notifyWarning('上传图片不符合所需的格式!')
return false
}
if (!imgSize) {
message.notifyWarning(`上传图片大小不能超过 ${props.fileSize}M!`)
return false
}
// ===== 新增:压缩图片 =====
try {
const compressed = await compressImage(rawFile, 0.8)
return compressed
} catch {
return rawFile // 失败则用原图
}
}
// 图片上传成功提示
const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
message.success('上传成功')
@@ -129,6 +130,7 @@ const uploadError = () => {
<style lang="scss" scoped>
.is-error {
.upload {
:deep(.el-upload),
:deep(.el-upload-dragger) {
border: 1px dashed var(--el-color-danger) !important;
@@ -141,6 +143,7 @@ const uploadError = () => {
}
:deep(.disabled) {
.el-upload,
.el-upload-dragger {
cursor: not-allowed !important;
+24 -21
View File
@@ -1,21 +1,9 @@
<template>
<div class="upload-box">
<el-upload
v-model:file-list="fileList"
:accept="fileType.join(',')"
:action="uploadUrl"
:before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']"
:disabled="disabled"
:drag="drag"
:http-request="httpRequest"
:limit="limit"
:multiple="true"
:on-error="uploadError"
:on-exceed="handleExceed"
:on-success="uploadSuccess"
list-type="picture-card"
>
<el-upload v-model:file-list="fileList" :accept="fileType.join(',')" :action="uploadUrl"
:before-upload="beforeUpload" :class="['upload', drag ? 'no-border' : '']" :disabled="disabled" :drag="drag"
:http-request="httpRequest" :limit="limit" :multiple="true" :on-error="uploadError" :on-exceed="handleExceed"
:on-success="uploadSuccess" list-type="picture-card">
<div class="upload-empty">
<slot name="empty">
<Icon icon="ep:plus" />
@@ -47,7 +35,7 @@ import { ElNotification } from 'element-plus'
import { createImageViewer } from '@/components/ImageViewer'
import { propTypes } from '@/utils/propTypes'
import { useUpload } from '@/components/UploadFile/src/useUpload'
import { compressImage, useUpload } from '@/components/UploadFile/src/useUpload'
defineOptions({ name: 'UploadImgs' })
@@ -93,23 +81,36 @@ const uploadList = ref<UploadUserFile[]>([])
* @description 文件上传之前判断
* @param rawFile 上传的文件
* */
const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
const beforeUpload: UploadProps['beforeUpload'] = async (rawFile) => {
const imgSize = rawFile.size / 1024 / 1024 < props.fileSize
const imgType = props.fileType
if (!imgType.includes(rawFile.type as FileTypes))
if (!imgType.includes(rawFile.type as FileTypes)) {
ElNotification({
title: '温馨提示',
message: '上传图片不符合所需的格式!',
type: 'warning'
})
if (!imgSize)
return false
}
if (!imgSize) {
ElNotification({
title: '温馨提示',
message: `上传图片大小不能超过 ${props.fileSize}M!`,
type: 'warning'
})
return false
}
uploadNumber.value++
return imgType.includes(rawFile.type as FileTypes) && imgSize
// ===== 新增:压缩图片 =====
try {
const compressed = await compressImage(rawFile, 0.8)
return compressed
} catch {
return rawFile // 失败则用原图
}
}
// 图片上传成功
@@ -186,6 +187,7 @@ const handleExceed = () => {
<style lang="scss" scoped>
.is-error {
.upload {
:deep(.el-upload--picture-card),
:deep(.el-upload-dragger) {
border: 1px dashed var(--el-color-danger) !important;
@@ -198,6 +200,7 @@ const handleExceed = () => {
}
:deep(.disabled) {
.el-upload--picture-card,
.el-upload-dragger {
cursor: not-allowed;
@@ -2,6 +2,7 @@ import * as FileApi from '@/api/infra/file'
import CryptoJS from 'crypto-js'
import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
import axios from 'axios'
import Compressor from 'compressorjs'
/**
* 获得上传 URL
@@ -104,3 +105,29 @@ enum UPLOAD_TYPE {
// 客户端发送到后端上传
SERVER = 'server'
}
export const compressImage = (file: File, quality = 0.6): Promise<File> => {
return new Promise((resolve, reject) => {
new Compressor(file, {
quality,
maxWidth: 1920,
maxHeight: 1920,
mimeType: 'image/jpeg',
convertSize: 5 * 1024 * 1024,
checkOrientation: true,
success(result) {
const compressedFile = new File([result], file.name, {
type: result.type,
lastModified: Date.now()
})
console.log(`压缩: ${(file.size / 1024).toFixed(2)}KB → ${(compressedFile.size / 1024).toFixed(2)}KB`)
resolve(compressedFile)
},
error(err) {
console.error('压缩失败:', err)
reject(err)
}
})
})
}
@@ -36,7 +36,7 @@
</defs>
<!-- 审批记录 -->
<el-dialog :title="dialogTitle || '审批记录'" v-model="dialogVisible" width="1000px">
<el-dialog :title="dialogTitle || '审批记录'" v-model="dialogVisible" width="1000px" :close-on-click-modal="false">
<el-row>
<el-table
:data="selectTasks"
@@ -195,6 +195,7 @@
width="600px"
append-to-body
destroy-on-close
:close-on-click-modal="false"
>
<el-form
:model="listenerFieldForm"
@@ -239,6 +239,7 @@
width="600px"
append-to-body
destroy-on-close
:close-on-click-modal="false"
>
<el-form
:model="listenerFieldForm"
@@ -36,6 +36,7 @@
width="600px"
append-to-body
destroy-on-close
:close-on-click-modal="false"
>
<el-form :model="propertyForm" label-width="80px" ref="attributeFormRef">
<el-form-item label="属性名:" prop="name">
@@ -125,6 +125,7 @@
width="600px"
append-to-body
destroy-on-close
:close-on-click-modal="false"
>
<el-form :model="varialbeFormData" label-width="80px" ref="varialbeFormRef">
<el-form-item label="源:" prop="source">
+8 -1
View File
@@ -1,11 +1,18 @@
import type { App } from 'vue'
// 需要全局引入一些组件,如ElScrollbar,不然一些下拉项样式有问题
import { ElLoading, ElScrollbar, ElButton } from 'element-plus'
import { ElLoading, ElScrollbar, ElButton, ElConfigProvider } from 'element-plus'
const plugins = [ElLoading]
const components = [ElScrollbar, ElButton]
// 设置全局配置
export const elementPlusConfig = {
button: {
autoInsertSpace: true
}
}
export const setupElementPlus = (app: App<Element>) => {
plugins.forEach((plugin) => {
app.use(plugin)
+46
View File
@@ -264,10 +264,32 @@ const handleLogin = async (params: any) => {
}
const loginDataLoginForm = { ...loginData.loginForm }
loginDataLoginForm.captchaVerification = params.captchaVerification
// 打印登录信息
console.log('========== 用户登录信息 ==========')
console.log('登录表单数据:', {
username: loginDataLoginForm.username,
tenantName: loginDataLoginForm.tenantName,
rememberMe: loginDataLoginForm.rememberMe,
captchaVerification: loginDataLoginForm.captchaVerification ? '已填写' : '未填写'
})
const res = await LoginApi.login(loginDataLoginForm)
if (!res) {
return
}
// 打印登录返回的Token信息
console.log('登录返回Token信息:', {
id: res.id,
userId: res.userId,
userType: res.userType,
clientId: res.clientId,
expiresTime: res.expiresTime ? new Date(res.expiresTime).toLocaleString() : '无',
accessToken: res.accessToken ? `${res.accessToken.substring(0, 20)}...` : '无',
refreshToken: res.refreshToken ? `${res.refreshToken.substring(0, 20)}...` : '无'
})
loading.value = ElLoading.service({
lock: true,
text: '正在加载系统中...',
@@ -279,6 +301,30 @@ const handleLogin = async (params: any) => {
authUtil.removeLoginForm()
}
authUtil.setToken(res)
// 获取用户详细信息并打印
try {
const userInfo = await LoginApi.getInfo()
console.log('用户详细信息:', {
用户ID: userInfo.user?.id,
用户名: userInfo.user?.username,
昵称: userInfo.user?.nickname,
头像: userInfo.user?.avatar,
部门ID: userInfo.user?.deptId,
邮箱: userInfo.user?.email,
手机号: userInfo.user?.mobile,
性别: userInfo.user?.sex,
登录IP: userInfo.user?.loginIp,
登录时间: userInfo.user?.loginDate,
角色: userInfo.roles,
权限数量: userInfo.permissions?.length || 0
})
console.log('所有权限:', userInfo.permissions)
console.log('菜单列表:', userInfo.menus)
} catch (error) {
console.error('获取用户信息失败:', error)
}
console.log('================================')
if (!redirect.value) {
redirect.value = '/'
}
@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="signDialogVisible" title="签名" width="935">
<el-dialog v-model="signDialogVisible" title="签名" width="935" :close-on-click-modal="false">
<div class="position-relative">
<Vue3Signature class="b b-solid b-gray" ref="signature" w="900px" h="400px" />
<el-button
+25 -17
View File
@@ -1,21 +1,9 @@
<template>
<Dialog v-model="dialogVisible" title="上传文件">
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="uploadUrl"
:auto-upload="false"
:data="data"
:disabled="formLoading"
:limit="1"
:on-change="handleFileChange"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
:http-request="httpRequest"
accept=".jpg, .png, .gif"
drag
>
<el-upload ref="uploadRef" v-model:file-list="fileList" :action="uploadUrl" :auto-upload="false"
:before-upload="beforeUpload" :data="data" :disabled="formLoading" :limit="1" :on-change="handleFileChange"
:on-error="submitFormError" :on-exceed="handleExceed" :on-success="submitFormSuccess" :http-request="httpRequest"
accept=".jpg, .png, .gif" drag>
<i class="el-icon-upload"></i>
<div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em></div>
<template #tip>
@@ -31,7 +19,8 @@
</Dialog>
</template>
<script lang="ts" setup>
import { useUpload } from '@/components/UploadFile/src/useUpload'
import { useUpload, compressImage } from '@/components/UploadFile/src/useUpload'
import type { UploadProps, UploadRawFile } from 'element-plus'
defineOptions({ name: 'InfraFileForm' })
@@ -53,6 +42,25 @@ const open = async () => {
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 文件上传之前进行压缩 */
const beforeUpload: UploadProps['beforeUpload'] = async (file: UploadRawFile) => {
// 验证文件类型
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']
if (!allowedTypes.includes(file.type)) {
message.error('仅允许上传 jpg、png、gif 格式文件!')
return false
}
// 压缩图片
try {
const compressed = await compressImage(file, 0.8)
return compressed
} catch (error) {
message.warning('图片压缩失败,将使用原图上传')
return file
}
}
/** 处理上传的文件发生变化 */
const handleFileChange = (file) => {
data.value.path = file.name
+102 -3
View File
@@ -70,10 +70,12 @@
<el-button
type="primary"
plain
@click="openForm('create')"
:disabled="selectedRows.length === 0"
:loading="copyLoading"
@click="handleCopy"
v-hasPermi="['product:channel:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
<Icon icon="ep:copy-document" class="mr-5px" /> 复制
</el-button>
<el-button
type="success"
@@ -90,7 +92,14 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="名称" align="center" prop="name" />
<el-table-column label="设备IMEI" align="center" prop="deviceId" />
@@ -154,6 +163,8 @@ const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<ChannelVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const selectedRows = ref<ChannelVO[]>([]) // 选中的货道
const copyLoading = ref(false) // 复制操作的加载中
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
@@ -174,6 +185,8 @@ const getList = async () => {
const data = await ChannelApi.getChannelPage(queryParams)
list.value = data.list
total.value = data.total
// 清空选中状态(因为列表数据已更新)
selectedRows.value = []
} finally {
loading.value = false
}
@@ -191,6 +204,92 @@ const resetQuery = () => {
handleQuery()
}
/** 处理表格选择变化 */
const handleSelectionChange = (selection: ChannelVO[]) => {
selectedRows.value = selection
}
/** 查找可用的货道号 */
const findAvailableChannel = async (originalChannel: string, deviceId: string): Promise<string> => {
// 获取所有货道列表用于检查
const allChannels = await ChannelApi.getChannelPage({ pageNo: 1, pageSize: 100 })
const existingChannels = allChannels.list
.filter((ch: ChannelVO) => ch.deviceId === deviceId)
.map((ch: ChannelVO) => ch.cabinetChannel)
// 解析原始货道号(可能是纯数字或包含其他字符)
const match = originalChannel.match(/(\d+)$/)
if (!match) {
// 如果无法解析数字,直接返回原值加1
return originalChannel + '1'
}
const baseNumber = parseInt(match[1], 10)
const prefix = originalChannel.substring(0, originalChannel.length - match[1].length)
let newNumber = baseNumber + 1
// 如果新货道号已存在,继续递增
while (existingChannels.includes(prefix + newNumber)) {
newNumber++
}
return prefix + newNumber
}
/** 复制货道 */
const handleCopy = async () => {
if (selectedRows.value.length === 0) {
message.warning('请至少选择一个货道进行复制')
return
}
try {
await message.confirm(`确认要复制选中的 ${selectedRows.value.length} 个货道吗?`)
copyLoading.value = true
// 遍历选中的货道,逐个复制
for (const row of selectedRows.value) {
try {
// 获取货道详情
const channelDetail = await ChannelApi.getChannel(row.id)
// 查找可用的货道号(在原有基础上加1,如果存在则继续加1)
const newCabinetChannel = await findAvailableChannel(
channelDetail.cabinetChannel,
channelDetail.deviceId
)
// 准备新货道数据,明确复制指定字段,货道号递增
// 不复制 id 和 skuId,只复制需要的字段
const newChannel = {
name: channelDetail.name, // 复制名称
maxNum: channelDetail.maxNum, // 复制最大数量
deviceId: channelDetail.deviceId, // 复制设备IMEI
channelLine: channelDetail.channelLine, // 复制货道行号
cabinetChannel: newCabinetChannel // 货道号加一
// 编号(id)和 skuId 不复制,由系统自动生成
} as ChannelVO
// 创建新货道
await ChannelApi.createChannel(newChannel)
} catch (error) {
console.error(`复制货道 ${row.name} 失败:`, error)
message.error(`复制货道 "${row.name}" 失败`)
}
}
message.success(`成功复制 ${selectedRows.value.length} 个货道`)
// 清空选中
selectedRows.value = []
// 刷新列表
await getList()
} catch {
// 用户取消操作
} finally {
copyLoading.value = false
}
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
@@ -53,7 +53,16 @@
<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" />
<UploadImgs v-model="formData.sliderPicUrls!" :disabled="isDetail" />
</el-form-item>
<el-form-item label="设备号">
<el-input
v-model="formData.deviceId"
class="w-80!"
clearable
placeholder="请输入设备号"
type="number"
/>
</el-form-item>
</el-form>
</template>
@@ -87,7 +96,8 @@ const formData = reactive<Spu>({
picUrl: '', // 商品封面图
sliderPicUrls: [], // 商品轮播图
introduction: '', // 商品简介
brandId: undefined // 商品品牌
brandId: undefined, // 商品品牌
deviceId: undefined // 设备号
})
const rules = reactive({
name: [required],
@@ -109,7 +109,8 @@ const formData = ref<ProductSpuApi.Spu>({
description: '', // 商品详情
sort: 0, // 商品排序
giveIntegral: 0, // 赠送积分
virtualSalesCount: 0 // 虚拟销量
virtualSalesCount: 0, // 虚拟销量
deviceId: undefined // 设备号
})
/** 获得详情 */
@@ -172,7 +173,11 @@ const submitForm = async () => {
const newSliderPicUrls: any[] = []
deepCopyFormData.sliderPicUrls!.forEach((item: any) => {
// 如果是前端选的图
typeof item === 'object' ? newSliderPicUrls.push(item.url) : newSliderPicUrls.push(item)
if (typeof item === 'object') {
newSliderPicUrls.push(item.url)
} else {
newSliderPicUrls.push(item)
}
})
deepCopyFormData.sliderPicUrls = newSliderPicUrls
// 校验都通过后提交表单
+299 -50
View File
@@ -1,9 +1,7 @@
<!-- 商品中心 - 商品列表 -->
<template>
<doc-alert title="【商品】商品 SPU 与 SKU" url="https://doc.iocoder.cn/mall/product-spu-sku/" />
<!-- 搜索工作栏 -->
<ContentWrap>
<ContentWrap class="search-wrap">
<el-form
ref="queryFormRef"
:inline="true"
@@ -55,11 +53,32 @@
v-hasPermi="['product:spu:create']"
plain
type="primary"
@click="openForm(undefined)"
@click="openForm()"
>
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
<el-button
v-hasPermi="['product:spu:create']"
plain
type="primary"
:disabled="selectedRows.length === 0"
:loading="copyLoading"
@click="handleCopy"
>
<Icon class="mr-5px" icon="ep:copy-document" />
复制
</el-button>
<el-button
v-hasPermi="['product:spu:update']"
plain
type="primary"
:disabled="selectedRows.length === 0"
@click="handleModifyChannel"
>
<Icon class="mr-5px" icon="ep:edit" />
修改货道号
</el-button>
<el-button
v-hasPermi="['product:spu:export']"
:loading="exportLoading"
@@ -84,7 +103,12 @@
:name="item.type"
/>
</el-tabs>
<el-table v-loading="loading" :data="list">
<el-table
v-loading="loading"
:data="list"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column type="expand">
<template #default="{ row }">
<el-form class="spu-table-expand" label-position="left">
@@ -128,9 +152,45 @@
</el-form>
</template>
</el-table-column>
<el-table-column label="创建人" min-width="140" prop="username" />
<el-table-column label="商品编号" min-width="140" prop="id" />
<el-table-column label="商品信息" min-width="300">
<el-table-column align="center" label="设备号" min-width="160">
<template #default="{ row }">
<template v-if="getDeviceIdValue(row)">{{ getDeviceIdValue(row) }}</template>
</template>
</el-table-column>
<el-table-column align="center" label="货道号" min-width="80">
<template #default="{ row }">
<div v-if="row.skus && row.skus.length > 0">
<el-tag
v-for="(sku, index) in row.skus.filter(s => s.cabinetChannel)"
:key="index"
size="small"
class="mr-1 mb-1"
>
{{ sku.cabinetChannel }}
</el-tag>
</div>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column align="center" label="销售状态" min-width="100">
<template #default="{ row }">
<template v-if="row.status >= 0">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
active-text="上架"
inactive-text="下架"
inline-prompt
@change="handleStatusChange(row)"
/>
</template>
<template v-else>
<el-tag type="info">回收站</el-tag>
</template>
</template>
</el-table-column>
<el-table-column label="商品信息" min-width="280">
<template #default="{ row }">
<div class="flex">
<el-image
@@ -149,46 +209,14 @@
</div>
</template>
</el-table-column>
<el-table-column align="center" label="货道号" min-width="120">
<template #default="{ row }">
<div v-if="row.skus && row.skus.length > 0">
<el-tag
v-for="(sku, index) in row.skus.filter(s => s.cabinetChannel)"
:key="index"
size="small"
class="mr-1 mb-1"
>
{{ sku.cabinetChannel }}
</el-tag>
</div>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column align="center" label="价格" min-width="160" prop="price">
<el-table-column align="center" label="库存" min-width="80" prop="stock" />
<el-table-column align="center" label="价格" min-width="100" prop="price">
<template #default="{ row }"> ¥ {{ fenToYuan(row.price) }}</template>
</el-table-column>
<el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
<el-table-column align="center" label="库存" min-width="90" prop="stock" />
<el-table-column align="center" label="排序" min-width="70" prop="sort" />
<el-table-column align="center" label="销售状态" min-width="80">
<template #default="{ row }">
<template v-if="row.status >= 0">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
active-text="上架"
inactive-text="下架"
inline-prompt
@change="handleStatusChange(row)"
/>
</template>
<template v-else>
<el-tag type="info">回收站</el-tag>
</template>
</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="id" />
<el-table-column label="销量" min-width="80" prop="salesCount" />
<el-table-column
:formatter="dateFormatter"
align="center"
@@ -246,6 +274,23 @@
@pagination="getList"
/>
</ContentWrap>
<!-- 修改货道号弹窗 -->
<el-dialog v-model="channelDialogVisible" title="修改货道号" width="500px">
<el-form ref="channelFormRef" :model="channelForm" label-width="100px">
<el-form-item label="货道号" prop="cabinetChannel" :rules="[{ required: true, message: '请输入货道号', trigger: 'blur' }]">
<el-input
v-model="channelForm.cabinetChannel"
placeholder="请输入货道号"
clearable
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="channelDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="channelLoading" @click="handleConfirmModifyChannel">确定</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { TabsPaneContext } from 'element-plus'
@@ -269,6 +314,14 @@ const loading = ref(false) // 列表的加载中
const exportLoading = ref(false) // 导出的加载中
const total = ref(0) // 列表的总页数
const list = ref<ProductSpuApi.Spu[]>([]) // 列表的数据
const selectedRows = ref<ProductSpuApi.Spu[]>([]) // 选中的商品
const copyLoading = ref(false) // 复制操作的加载中
const channelDialogVisible = ref(false) // 修改货道号弹窗显示状态
const channelLoading = ref(false) // 修改货道号加载中
const channelFormRef = ref() // 货道号表单引用
const channelForm = ref({
cabinetChannel: '' // 货道号
})
// tabs 数据
const tabsData = ref([
{
@@ -309,14 +362,70 @@ const queryParams = ref({
}) // 查询参数
const queryFormRef = ref() // 搜索的表单Ref
const normalizeNumericString = (value?: number | string | null) => {
if (value === undefined || value === null) {
return ''
}
return String(value)
}
const getDeviceIdValue = (spu: ProductSpuApi.Spu) => {
if (spu.deviceId !== undefined && spu.deviceId !== null) {
return normalizeNumericString(spu.deviceId)
}
const skuDevice = spu.skus?.find((sku) => sku.deviceId !== undefined && sku.deviceId !== null)
return normalizeNumericString(skuDevice?.deviceId ?? '')
}
const getCabinetChannelValue = (spu: ProductSpuApi.Spu) => {
const channels =
spu.skus
?.map((sku) => sku.cabinetChannel)
.filter((channel): channel is string => !!channel) ?? []
if (channels.length === 0) {
return ''
}
const sortedChannels = channels
.map((channel) => normalizeNumericString(channel))
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }))
return sortedChannels[0]
}
const compareByDeviceAndCabinet = (a: ProductSpuApi.Spu, b: ProductSpuApi.Spu) => {
const deviceA = getDeviceIdValue(a)
const deviceB = getDeviceIdValue(b)
const deviceCompare = deviceA.localeCompare(deviceB, undefined, { numeric: true, sensitivity: 'base' })
if (deviceCompare !== 0) {
return deviceCompare
}
const cabinetA = getCabinetChannelValue(a)
const cabinetB = getCabinetChannelValue(b)
const cabinetCompare = cabinetA.localeCompare(cabinetB, undefined, { numeric: true, sensitivity: 'base' })
if (cabinetCompare !== 0) {
return cabinetCompare
}
return ((a.id ?? 0) as number) - ((b.id ?? 0) as number)
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ProductSpuApi.getSpuPage(queryParams.value)
list.value = data.list
total.value = data.total
const sortedList = [...data.list].sort(compareByDeviceAndCabinet)
// 过滤掉没有设备号的商品
const filteredList = sortedList.filter((item) => !!getDeviceIdValue(item))
list.value = filteredList
total.value = filteredList.length
console.log('查询列表',data)
const cabinetChannels = filteredList
.flatMap((item) => item.skus ?? [])
.map((sku) => sku.cabinetChannel)
.filter((channel): channel is string => !!channel)
.map((channel) => normalizeNumericString(channel))
console.log('货道号数据:', cabinetChannels)
// 清空选中状态(因为列表数据已更新)
selectedRows.value = []
} finally {
loading.value = false
}
@@ -325,6 +434,7 @@ const getList = async () => {
/** 切换 Tab */
const handleTabClick = (tab: TabsPaneContext) => {
queryParams.value.tabType = tab.paneName as number
selectedRows.value = [] // 清空选中
getList()
}
@@ -418,6 +528,132 @@ const openForm = (id?: number) => {
push({ name: 'ProductPickupAdd' });
}
/** 处理表格选择变化 */
const handleSelectionChange = (selection: ProductSpuApi.Spu[]) => {
selectedRows.value = selection
}
/** 复制商品 */
const handleCopy = async () => {
if (selectedRows.value.length === 0) {
message.warning('请至少选择一个商品进行复制')
return
}
try {
await message.confirm(`确认要复制选中的 ${selectedRows.value.length} 个商品吗?`)
copyLoading.value = true
// 遍历选中的商品,逐个复制
for (const row of selectedRows.value) {
try {
// 获取商品详情
const productDetail = await ProductSpuApi.getSpu(row.id!)
// 准备新商品数据,复制所有内容,但不传递商品编号(id)和SKU的id,后端会自动生成
const { id, createTime, skus, ...spuData } = productDetail
const newProduct: ProductSpuApi.Spu = {
...spuData, // 复制所有SPU字段(除了id和createTime)
// 复制SKU时不传递id和spuId,其他所有数据保持原样(包括name字段)
skus: skus?.map(sku => {
const { id: skuId, spuId, ...skuData } = sku
return {
...skuData, // 复制SKU的所有业务字段(包括name),排除id、spuId
name: sku.name || productDetail.name // 确保name字段被复制,如果没有则使用SPU的name
}
})
}
// 在控制台打印新增的商品数据
console.log('复制新增的商品数据:', newProduct)
// 创建新商品
await ProductSpuApi.createSpu(newProduct)
} catch (error) {
console.error(`复制商品 ${row.name} 失败:`, error)
message.error(`复制商品 "${row.name}" 失败`)
}
}
message.success(`成功复制 ${selectedRows.value.length} 个商品`)
// 清空选中
selectedRows.value = []
// 刷新列表
await getTabsCount()
await getList()
} catch {
// 用户取消操作
} finally {
copyLoading.value = false
}
}
/** 修改货道号 */
const handleModifyChannel = () => {
if (selectedRows.value.length === 0) {
message.warning('请至少选择一个商品进行修改货道号')
return
}
// 重置表单
channelForm.value.cabinetChannel = ''
channelDialogVisible.value = true
}
/** 确认修改货道号 */
const handleConfirmModifyChannel = async () => {
if (!channelFormRef.value) return
try {
// 表单验证
await channelFormRef.value.validate()
if (!channelForm.value.cabinetChannel) {
message.warning('请输入货道号')
return
}
channelLoading.value = true
// 遍历选中的商品,逐个修改货道号
for (const row of selectedRows.value) {
try {
// 获取商品详情
const productDetail = await ProductSpuApi.getSpu(row.id!)
// 更新 skus 中的货道号,name 使用 spu 的 name,其他参数保持不变
const updatedSkus = productDetail.skus?.map(sku => ({
...sku,
name: productDetail.name, // 使用外层 spu 的 name
cabinetChannel: channelForm.value.cabinetChannel
})) || []
// 准备更新数据,只修改 skus 的货道号,其他参数保持原样
const updateData: ProductSpuApi.Spu = {
...productDetail,
skus: updatedSkus
}
// 调用更新接口
await ProductSpuApi.updateSpu(updateData)
} catch (error) {
console.error(`修改商品 ${row.name} 货道号失败:`, error)
message.error(`修改商品 "${row.name}" 货道号失败`)
}
}
message.success(`成功修改 ${selectedRows.value.length} 个商品的货道号`)
// 关闭弹窗
channelDialogVisible.value = false
// 清空选中
selectedRows.value = []
} catch (error) {
console.error('修改货道号失败:', error)
} finally {
channelLoading.value = false
await getList()
}
}
/** 查看商品详情 */
const openDetail = (id: number) => {
push({ name: 'ProductPickupDetail', params: { id } })
@@ -452,8 +688,12 @@ onActivated(() => {
/** 初始化 **/
onMounted(async () => {
// 解析路由的 categoryId
if (route.query.categoryId) {
queryParams.value.categoryId = route.query.categoryId
const categoryId = route.query.categoryId
if (categoryId) {
const id = Array.isArray(categoryId) ? Number(categoryId[0]) : Number(categoryId)
if (!isNaN(id)) {
queryParams.value.categoryId = id as any
}
}
// 获得商品信息
await getTabsCount()
@@ -464,6 +704,15 @@ onMounted(async () => {
})
</script>
<style lang="scss" scoped>
.search-wrap {
position: sticky;
// top: 20px;
top: 0;
z-index: 100;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 16px;
}
.spu-table-expand {
padding-left: 42px;
+5 -5
View File
@@ -25,15 +25,15 @@
<el-form-item v-if="formData.adType === 'link'" label="广告图链接" prop="adUrl">
<el-input v-model="formData.adUrl" placeholder="请输入广告图链接" />
</el-form-item>
<el-form-item v-if="false" label="访问商品地址" prop="productUrl">
<!-- <el-form-item v-if="false" label="访问商品地址" prop="productUrl">
<el-input v-model="formData.productUrl" placeholder="请输入访问商品地址" />
</el-form-item>
<el-form-item v-if="true" label="商品地址" prop="productUrl">
</el-form-item> -->
<!-- <el-form-item v-if="true" label="商品地址" prop="productUrl">
<el-input v-model="formData.productUrl" placeholder="请输入访问商品地址" />
</el-form-item>
<el-form-item v-if="true" label="文件地址" prop="fileUrl">
<el-input v-model="formData.fileUrl" placeholder="请输入文件地址" />
</el-form-item>
</el-form-item> -->
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
@@ -67,7 +67,7 @@ const formData = ref({
})
const formRules = reactive({
deviceId: [{ required: true, message: '设备IMEI不能为空', trigger: 'blur' }],
productUrl: [{ required: true, message: '访问商品地址不能为空', trigger: 'blur' }],
// productUrl: [{ required: true, message: '访问商品地址不能为空', trigger: 'blur' }],
})
const formRef = ref() // 表单 Ref
+1 -1
View File
@@ -53,7 +53,7 @@ type="success" plain @click="handleExport" :loading="exportLoading"
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="设备名称" align="center" prop="name" />
<el-table-column label="设备IMEI" align="center" prop="deviceId" />
<el-table-column label="访问商品地址" width="250" align="center" prop="productUrl" />
<!-- <el-table-column label="访问商品地址" width="250" align="center" prop="productUrl" /> -->
<el-table-column label="广告" align="center" prop="fileUrl">
<template #default="{ row }">
<el-image :src="row.fileUrl" class="mr-10px h-30px w-30px" @click="imagePreview(row.fileUrl)" />
@@ -4,6 +4,7 @@
title="选择商品类型"
width="600px"
:before-close="handleDialogClose"
:close-on-click-modal="false"
>
<div class="product-type-container">
<div class="type-card-list">
+62 -15
View File
@@ -187,9 +187,9 @@
<el-table-column label="销量" prop="salesCount" />
<!-- 展开详情表格中的创建时间列 -->
<el-table-column label="创建时间" prop="createTime" width="180">
<template #default="{ row }">
<template #default="{ row: sourceRow }">
<span>{{
row.createTime ? dateFormatter(null, null, new Date(row.createTime)) : ''
sourceRow.createTime ? formatDate(new Date(sourceRow.createTime)) : ''
}}</span>
</template>
</el-table-column>
@@ -274,7 +274,7 @@
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import { formatDate } from '@/utils/formatTime'
import download from '@/utils/download'
import { SummaryApi, SummaryVO } from '@/api/product/summary'
import * as ProductSpuApi from '@/api/mall/product/spu'
@@ -295,7 +295,16 @@ const total = ref(0) // 列表的总页数
const showProductTypeDialog = ref(false) // 商品类型选择弹窗显示状态
// 查询参数
const queryParams = ref({
const queryParams = ref<{
pageNo: number
pageSize: number
tabType: number
name: string
categoryId: number | undefined
createTime: any
types: number
barCode: string | undefined
}>({
pageNo: 1,
pageSize: 10,
tabType: 0,
@@ -325,8 +334,8 @@ const fetchAllProducts = async () => {
// console.log('获取商品数据,参数:', largePageParams)
const [mallResponse, containerResponse] = await Promise.all([
ProductSpuApi.getSpuPage({ ...largePageParams, types: 0 }), // 商城商品
ProductSpuApi.getSpuPage({ ...largePageParams, types: 1 }) // 提货柜商品
ProductSpuApi.getSpuPage({ ...largePageParams, types: 0 } as any), // 商城商品
ProductSpuApi.getSpuPage({ ...largePageParams, types: 1 } as any) // 提货柜商品
])
// console.log('商城商品响应:', mallResponse)
@@ -345,7 +354,7 @@ const fetchAllProducts = async () => {
/**
* 获取商品详细信息 - 优化版本
*/
const fetchProductDetails = async (products) => {
const fetchProductDetails = async (products: any[]) => {
if (!products || products.length === 0) {
// console.log('没有商品数据需要获取详情')
return []
@@ -388,7 +397,7 @@ const getList = async () => {
// console.log('获取到的详细商品数据:', detailedProducts)
// Step 3: 获取汇总数据(如果需要)
let summaryData = []
let summaryData: any[] = []
try {
const summaryResponse = await SummaryApi.getSummaryPage({
pageNo: 1,
@@ -424,7 +433,9 @@ const getList = async () => {
}
// Step 4: 按barCode分组合并商品
const searchBarCode = queryParams.value.barCode?.trim() || null
const searchBarCode = (queryParams.value.barCode && typeof queryParams.value.barCode === 'string')
? queryParams.value.barCode.trim() || null
: null
const allGroupedProducts = groupProductsByBarCode(detailedProducts, searchBarCode)
console.log('分组后的商品数据:', allGroupedProducts)
@@ -437,7 +448,43 @@ const getList = async () => {
const combinedProducts = [...allGroupedProducts, ...uniqueSummaryData]
console.log('合并后的最终数据:', combinedProducts)
// Step 6: 手动分页处理
// Step 6: 排序处理 - 先按商品来源排序(提货柜商品在前,商城商品在后),再按SKU条形码从小到大排序
combinedProducts.sort((a, b) => {
// 获取商品来源类型
const getSourceType = (item) => {
const sourceType = item.sources?.[0]?.type || ''
if (sourceType.includes('提货柜')) return 0 // 提货柜商品排在前面
if (sourceType.includes('商城')) return 1 // 商城商品排在中间
if (sourceType.includes('汇总')) return 2 // 汇总商品排在最后
return 2 // 其他类型排在最后
}
const sourceA = getSourceType(a)
const sourceB = getSourceType(b)
// 先按商品来源排序
if (sourceA !== sourceB) {
return sourceA - sourceB
}
// 如果来源相同,按SKU条形码从小到大排序
const barCodeA = a.barCode || ''
const barCodeB = b.barCode || ''
// 尝试将条形码转换为数字进行比较,如果转换失败则按字符串比较
const numA = Number(barCodeA)
const numB = Number(barCodeB)
if (!isNaN(numA) && !isNaN(numB) && barCodeA === String(numA) && barCodeB === String(numB)) {
// 如果都是纯数字,按数字大小排序
return numA - numB
} else {
// 否则按字符串排序
return barCodeA.localeCompare(barCodeB, 'zh-CN', { numeric: true })
}
})
// Step 7: 手动分页处理
const { pageNo, pageSize } = queryParams.value
const startIndex = (pageNo - 1) * pageSize
const endIndex = startIndex + pageSize
@@ -446,9 +493,9 @@ const getList = async () => {
total.value = combinedProducts.length
console.log(`分页处理: 总数=${total.value}, 当前页=${pageNo}, 每页=${pageSize}, 展示=${list.value.length}条`)
} catch (error) {
} catch (error: any) {
console.error('获取商品数据失败:', error)
message.error('获取商品数据失败: ' + (error.message || '未知错误'))
message.error('获取商品数据失败: ' + (error?.message || '未知错误'))
// 设置默认值避免页面报错
list.value = []
@@ -461,10 +508,10 @@ const getList = async () => {
/**
* 按barCode分组合并商品 - 优化版本
*/
const groupProductsByBarCode = (products, searchBarCode = null) => {
const groupProductsByBarCode = (products: any[], searchBarCode: string | null = null) => {
const groupedMap = new Map()
const addToGroup = (key, productData, skuData = null) => {
const addToGroup = (key: string, productData: any, skuData: any = null) => {
if (!groupedMap.has(key)) {
groupedMap.set(key, {
id: productData.id,
@@ -566,7 +613,7 @@ const handleDelete = async (id: number) => {
deletingIds.value.add(id)
await SummaryApi.deleteSummary(id)
message.success(t('common.delSuccess'))
message.success('删除成功')
// 检查分页
const currentPageItemCount = list.value.length
@@ -106,7 +106,7 @@
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
<el-dialog v-model="mapDialogVisible" title="获取经纬度" append-to-body>
<el-dialog v-model="mapDialogVisible" title="获取经纬度" append-to-body :close-on-click-modal="false">
<IFrame class="h-609px" :src="tencentLbsUrl" />
</el-dialog>
</Dialog>
+62 -13
View File
@@ -168,22 +168,59 @@ const queryParams = reactive<StatisticsDetailPageReqVO>({
const getAllData = async () => {
loading.value = true
try {
// 先获取第一页数据,了解总数
const firstPageData = await SalesStatisticsDetailsApi.getStatisticsDetailPage({
...queryParams,
pageNo: 1,
pageSize: 1000
})
if (firstPageData && firstPageData.list) {
const sortedData = firstPageData.list.sort((a: any, b: any) => {
console.log('API 返回的完整数据:', firstPageData)
const responseData = firstPageData as any
if (responseData && responseData.list) {
let allItems = [...responseData.list]
const totalCount = responseData.total || 0
const pageSize = 10
// 如果总数超过第一页的数量,需要继续获取剩余数据
if (totalCount > pageSize) {
const totalPages = Math.ceil(totalCount / pageSize)
console.log(`总数据 ${totalCount} 条,需要获取 ${totalPages} 页`)
// 并发获取剩余页面的数据
const promises: Promise<any>[] = []
for (let page = 2; page <= totalPages; page++) {
promises.push(
SalesStatisticsDetailsApi.getStatisticsDetailPage({
...queryParams,
pageNo: page,
pageSize: pageSize
})
)
}
const remainingPages = await Promise.all(promises)
remainingPages.forEach((pageData: any) => {
if (pageData && pageData.list) {
allItems = allItems.concat(pageData.list)
}
})
}
// 打印 createTime 参数
console.log('createTime 数据:', allItems.map((item: any) => item.createTime))
console.log(`实际获取数据条数: ${allItems.length},总数据: ${totalCount}`)
const sortedData = allItems.sort((a: any, b: any) => {
return new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
})
allData.value = sortedData
total.value = firstPageData.total || 0
total.value = totalCount
updateCurrentPageData()
console.log("销售订单", firstPageData)
} else {
console.warn('数据格式异常,未找到 list 数据', firstPageData)
}
} catch (error) {
console.error('获取数据失败:', error)
@@ -194,10 +231,18 @@ const getAllData = async () => {
}
const updateCurrentPageData = () => {
const startIndex = (queryParams.pageNo - 1) * queryParams.pageSize
const endIndex = startIndex + queryParams.pageSize
const pageNo = queryParams.pageNo ?? 1
const pageSize = queryParams.pageSize ?? 10
const startIndex = (pageNo - 1) * pageSize
const endIndex = startIndex + pageSize
if (queryParams.no || queryParams.deviceId || queryParams.barCode || queryParams.createTime) {
// 检查是否有搜索条件
const hasSearchCondition = (queryParams.no && queryParams.no.trim()) ||
(queryParams.deviceId && queryParams.deviceId.trim()) ||
(queryParams.barCode && queryParams.barCode.trim()) ||
(queryParams.createTime && Array.isArray(queryParams.createTime) && queryParams.createTime.length === 2)
if (hasSearchCondition) {
handleQuery()
} else {
list.value = allData.value.slice(startIndex, endIndex)
@@ -220,13 +265,13 @@ const handleQuery = () => {
if (queryParams.deviceId && queryParams.deviceId.trim()) {
filteredData = filteredData.filter((item: any) =>
item.deviceId && String(item.deviceId).toLowerCase().includes(queryParams.deviceId.toLowerCase())
item.deviceId && String(item.deviceId).toLowerCase().includes(queryParams.deviceId!.toLowerCase())
)
}
if (queryParams.barCode && queryParams.barCode.trim()) {
filteredData = filteredData.filter((item: any) =>
item.barCode && String(item.barCode).toLowerCase().includes(queryParams.barCode.toLowerCase())
item.barCode && String(item.barCode).toLowerCase().includes(queryParams.barCode!.toLowerCase())
)
}
@@ -244,8 +289,10 @@ const handleQuery = () => {
})
total.value = sortedData.length
const startIndex = (queryParams.pageNo - 1) * queryParams.pageSize
const endIndex = startIndex + queryParams.pageSize
const pageNo = queryParams.pageNo ?? 1
const pageSize = queryParams.pageSize ?? 10
const startIndex = (pageNo - 1) * pageSize
const endIndex = startIndex + pageSize
list.value = sortedData.slice(startIndex, endIndex)
}
@@ -280,7 +327,9 @@ const handleExport = async () => {
const formatTime = (timestamp: number | string) => {
if (!timestamp) return '-'
const date = new Date(Number(timestamp))
// 如果是字符串格式的日期,直接使用;如果是数字时间戳,需要转换
const date = typeof timestamp === 'string' ? new Date(timestamp) : new Date(Number(timestamp))
if (isNaN(date.getTime())) return '-'
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
+1
View File
@@ -58,6 +58,7 @@
v-model="showDialog"
width="800px"
destroy-on-close
:close-on-click-modal="false"
>
<ReplyForm v-model="replyForm" v-model:reply="reply" :msg-type="msgType" ref="formRef" />
<template #footer>
@@ -23,6 +23,7 @@
width="90%"
append-to-body
destroy-on-close
:close-on-click-modal="false"
>
<WxMaterialSelect
type="image"
@@ -37,6 +37,7 @@
width="80%"
append-to-body
destroy-on-close
:close-on-click-modal="false"
>
<WxMaterialSelect
type="image"
@@ -20,7 +20,7 @@
</el-col>
</el-row>
</el-col>
<el-dialog title="选择图文" v-model="showDialog" width="90%" append-to-body destroy-on-close>
<el-dialog title="选择图文" v-model="showDialog" width="90%" append-to-body destroy-on-close :close-on-click-modal="false">
<WxMaterialSelect
type="news"
:account-id="reply.accountId"
@@ -19,6 +19,7 @@
width="90%"
append-to-body
destroy-on-close
:close-on-click-modal="false"
>
<WxMaterialSelect
type="video"
@@ -21,6 +21,7 @@
width="90%"
append-to-body
destroy-on-close
:close-on-click-modal="false"
>
<WxMaterialSelect
type="voice"
@@ -19,7 +19,7 @@
</div>
<!-- 弹窗播放 -->
<el-dialog v-model="dialogVideo" title="视频播放" append-to-body>
<el-dialog v-model="dialogVideo" title="视频播放" append-to-body :close-on-click-modal="false">
<video-player
v-if="dialogVideo"
class="video-player vjs-big-play-centered"
@@ -48,6 +48,7 @@
width="80%"
append-to-body
destroy-on-close
:close-on-click-modal="false"
>
<WxMaterialSelect
type="image"
+1
View File
@@ -52,6 +52,7 @@
v-model="showDialog"
:before-close="onBeforeDialogClose"
destroy-on-close
:close-on-click-modal="false"
>
<NewsForm v-model="newsList" v-loading="isSubmitting" :is-creating="isCreating" />
<template #footer>
@@ -1,5 +1,5 @@
<template>
<el-dialog title="新建视频" v-model="showDialog" width="600px">
<el-dialog title="新建视频" v-model="showDialog" width="600px" :close-on-click-modal="false">
<el-upload
:action="UPLOAD_URL"
:headers="HEADERS"
+1 -1
View File
@@ -92,7 +92,7 @@
</el-col>
</el-row>
</div>
<el-dialog title="选择图文" v-model="showNewsDialog" width="80%" destroy-on-close>
<el-dialog title="选择图文" v-model="showNewsDialog" width="80%" destroy-on-close :close-on-click-modal="false">
<WxMaterialSelect
type="news"
:account-id="props.accountId"
+1
View File
@@ -75,6 +75,7 @@
@click="messageBox.show = true"
width="50%"
destroy-on-close
:close-on-click-modal="false"
>
<WxMsg :user-id="messageBox.userId" />
</el-dialog>