新增商品拼团

This commit is contained in:
gyq
2025-12-16 11:00:16 +08:00
parent bdbe45b8a3
commit 82c0008b2a
7 changed files with 526 additions and 66 deletions

View File

@@ -2,7 +2,8 @@
<template>
<el-upload v-model:file-list="fileList" list-type="picture-card" :before-upload="handleBeforeUpload"
:http-request="handleUpload" :on-success="handleSuccess" :on-progress="handleProgress" :on-error="handleError"
:on-exceed="handleExceed" :accept="props.accept" :limit="props.limit" multiple :auto-upload="true">
:on-exceed="handleExceed" :accept="props.accept" :limit="props.limit" multiple :auto-upload="true"
:on-change="handleFileChange">
<el-icon>
<Plus />
</el-icon>
@@ -32,9 +33,9 @@
</template>
<script setup lang="ts">
import { ref, watch, onMounted, PropType, computed } from "vue";
import { ref, watch, onMounted, PropType } from "vue";
import { ElMessage } from "element-plus";
import { UploadRawFile, UploadRequestOptions, UploadUserFile } from "element-plus";
import { UploadRawFile, UploadRequestOptions, UploadUserFile, UploadFileStatus } from "element-plus";
import FileAPI, { FileInfo } from "@/api/account/common";
// 定义Props
@@ -49,7 +50,7 @@ const props = defineProps({
},
limit: {
type: Number,
default: 10,
default: 9,
},
maxFileSize: {
type: Number,
@@ -62,7 +63,7 @@ const props = defineProps({
});
// 定义Emits
const emit = defineEmits(['upDataEvent', 'uploadAllSuccess']); // 新增:所有图片上传完成事件
const emit = defineEmits(['upDataEvent', 'uploadAllSuccess']);
// 响应式数据
const previewVisible = ref(false);
@@ -76,9 +77,8 @@ const modelValue = defineModel<string[]>("modelValue", {
});
const fileList = ref<UploadUserFile[]>([]);
// 新增:批量上传计数
const pendingUploadCount = ref(0); // 待上传文件数
const completedUploadCount = ref(0); // 已完成上传数
// 修复用Map跟踪每个文件的上传状态key=uidvalue=是否完成),替代计数
const uploadingFiles = ref<Map<string, { status: 'pending' | 'success' | 'error', file: UploadUserFile }>>(new Map());
/**
* 删除图片
@@ -87,6 +87,11 @@ function handleRemove(imageUrl: string) {
const index = modelValue.value.indexOf(imageUrl);
if (index !== -1) {
modelValue.value.splice(index, 1);
// 同步删除fileList和上传状态跟踪
const fileItem = fileList.value[index];
if (fileItem?.uid) {
uploadingFiles.value.delete(fileItem.uid);
}
fileList.value.splice(index, 1);
}
}
@@ -94,12 +99,28 @@ function handleRemove(imageUrl: string) {
/**
* 上传进度
*/
function handleProgress(event: any) {
console.log("文件上传进度", Math.round(event.percent));
function handleProgress(event: any, uploadFile: UploadUserFile) {
console.log(`文件${uploadFile.name}上传进度`, Math.round(event.percent));
}
/**
* 上传前校验 & 初始化待上传文件
* 修复文件状态变化时跟踪替代beforeUpload手动push
*/
function handleFileChange(file: UploadUserFile, files: UploadUserFile[]) {
// 仅跟踪新增的、待上传的文件
if (file.status === 'ready' && !uploadingFiles.value.has(file.uid)) {
uploadingFiles.value.set(file.uid, {
status: 'pending',
file
});
emit('upDataEvent', true); // 开始上传触发loading
}
// 同步fileList避免重复
fileList.value = files;
}
/**
* 上传前校验仅保留校验逻辑不再手动修改fileList
*/
function handleBeforeUpload(file: UploadRawFile) {
// 1. 文件类型校验
@@ -125,19 +146,6 @@ function handleBeforeUpload(file: UploadRawFile) {
return false;
}
// 3. 新增初始化待上传文件到fileList确保uid匹配
const uploadFile: UploadUserFile = {
uid: file.uid, // 关键复用文件原生uid
name: file.name,
status: "ready",
raw: file,
};
fileList.value.push(uploadFile);
// 4. 新增:增加待上传计数
pendingUploadCount.value++;
emit('upDataEvent', true); // 开始上传触发loading
return true;
}
@@ -173,46 +181,74 @@ function handleExceed(files: File[], uploadFiles: UploadUserFile[]) {
}
/**
* 单文件上传成功回调
* 修复:单文件上传成功回调(精准更新+批量完成判断)
*/
const handleSuccess = (fileInfo: FileInfo, uploadFile: UploadUserFile) => {
// 1. 修复通过uid精准匹配文件
const index = fileList.value.findIndex((file) => file.uid === uploadFile.uid);
if (index !== -1) {
fileList.value[index].url = fileInfo as string; // 确保fileInfo是字符串类型
fileList.value[index].status = "success";
// 同步更新modelValue若index超出当前长度push否则替换
if (index >= modelValue.value.length) {
modelValue.value.push(fileInfo as string);
} else {
modelValue.value[index] = fileInfo as string;
// 1. 精准匹配文件并更新状态
if (uploadingFiles.value.has(uploadFile.uid)) {
uploadingFiles.value.set(uploadFile.uid, {
status: 'success',
file: uploadFile
});
// 2. 更新fileList的url和状态
const index = fileList.value.findIndex((f) => f.uid === uploadFile.uid);
if (index !== -1) {
fileList.value[index].url = fileInfo as string;
fileList.value[index].status = "success" as UploadFileStatus;
// 3. 同步更新modelValue确保追加而非替换
if (!modelValue.value.includes(fileInfo as string)) {
modelValue.value.push(fileInfo as string);
}
}
}
// 2. 新增:完成计数+判断所有上传完成
completedUploadCount.value++;
if (completedUploadCount.value === pendingUploadCount.value) {
ElMessage.success("所有图片上传成功!");
emit('uploadAllSuccess', modelValue.value); // 所有图片上传完成emit成功事件
// 重置计数
pendingUploadCount.value = 0;
completedUploadCount.value = 0;
emit('upDataEvent', false); // 结束loading
}
// 4. 检查是否所有上传中的文件都完成
checkAllUploadCompleted();
};
/**
* 上传失败回调
*/
const handleError = (error: any) => {
console.error("上传失败:", error);
ElMessage.error("上传失败: " + (error.message || "未知错误"));
// 重置计数+结束loading
pendingUploadCount.value = 0;
completedUploadCount.value = 0;
emit('upDataEvent', false);
const handleError = (error: any, uploadFile: UploadUserFile) => {
console.error(`文件${uploadFile.name}上传失败:`, error);
ElMessage.error(`文件${uploadFile.name}上传失败: ` + (error.message || "未知错误"));
// 更新失败文件状态
if (uploadingFiles.value.has(uploadFile.uid)) {
uploadingFiles.value.set(uploadFile.uid, {
status: 'error',
file: uploadFile
});
}
// 检查是否所有上传中的文件都完成(失败也算完成)
checkAllUploadCompleted();
};
/**
* 新增:检查所有上传文件是否完成(成功/失败都算完成)
*/
function checkAllUploadCompleted() {
// 过滤出还在pending的文件
const pendingFiles = Array.from(uploadingFiles.value.values()).filter(item => item.status === 'pending');
// 没有pending文件 → 所有上传完成
if (pendingFiles.length === 0 && uploadingFiles.value.size > 0) {
// 过滤出成功的文件URL
const successUrls = Array.from(uploadingFiles.value.values())
.filter(item => item.status === 'success')
.map(item => item.file.url)
.filter(Boolean) as string[];
ElMessage.success(`上传完成!成功${successUrls.length}张,失败${uploadingFiles.value.size - successUrls.length}`);
emit('uploadAllSuccess', modelValue.value); // 触发全部完成事件
emit('upDataEvent', false); // 结束loading
// 重置上传状态跟踪(关键:避免多次触发)
uploadingFiles.value.clear();
}
}
/**
* 预览图片
*/
@@ -228,22 +264,22 @@ const handlePreviewClose = () => {
previewVisible.value = false;
};
// 监听modelValue同步fileList
// 修复:监听modelValue同步fileList避免重复uid
watch(modelValue, (newValue) => {
fileList.value = newValue.map((url) => ({
url,
uid: Date.now() + Math.random(), // 为已有图片生成唯一uid
status: "success"
}) as UploadUserFile);
uid: `init-${Date.now()}-${Math.random()}`, // 为初始化图片生成唯一uid(避免和上传文件冲突)
status: "success" as UploadFileStatus
}));
}, { immediate: true });
onMounted(() => {
// 初始化fileList
fileList.value = modelValue.value.map((url) => ({
url,
uid: Date.now() + Math.random(),
status: "success"
}) as UploadUserFile);
uid: `init-${Date.now()}-${Math.random()}`,
status: "success" as UploadFileStatus
}));
});
</script>

View File

@@ -52,16 +52,13 @@ async function roleTemplateListAjax() {
el.cehcked = false
});
list.value = res
if (list.value.length > 0) {
visible.value = true
}
} catch (error) {
console.log(error);
}
}
function show() {
visible.value = true
roleTemplateListAjax()
}

View File

@@ -172,6 +172,7 @@ import menuSelect from "./components/menus.vue";
import RoleApi, { SysRole, addRequest, getListRequest } from "@/api/account/role";
import MenuAPI, { type RouteVO, CashMenu } from "@/api/account/menu";
import roleTemplateDialog from "./components/roleTemplateDialog.vue";
import { roleTemplateList } from '@/api/account/roleTemplate'
const roleTemplateDialogRef = ref(null)
@@ -242,11 +243,18 @@ function handlePlatformTypeChange(e: string | number | boolean | undefined) {
function handleQuery() {
loading.value = true;
RoleApi.getList(queryParams)
.then((data) => {
.then(async (data) => {
roleList.value = data.records;
total.value = data.totalRow;
if (data.records.length == 0) {
console.log('data===', data);
const res = await roleTemplateList({ isEnable: 1 })
console.log('roleTemplateList===', res);
if (data.records.length == 0 && res.length > 0) {
roleTemplateDialogRef.value.show()
}
})

View File

@@ -52,7 +52,7 @@ export const newMenus = [
{
name: "商品拼团",
icon: "sppt",
pathName: "",
pathName: "group_booking",
intro: "拼团"
},
]

View File

@@ -0,0 +1,244 @@
<!-- 添加团购商品 -->
<template>
<div>
<el-dialog :title="form.id ? '编辑商品' : '添加商品'" width="800px" v-model="visible" @submit.prevent>
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120">
<el-form-item label="可用门店">
<el-radio-group v-model="form.useShopType">
<el-radio label="仅本店" value="only"></el-radio>
<el-radio label="全部门店" value="all"></el-radio>
<el-radio label="指定门店可用" value="custom"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.useShopType == 'custom'" prop="useShops">
<selectBranchs all v-model="form.useShops" />
</el-form-item>
<el-form-item label="商品名称" prop="wareName">
<div class="column">
<el-input v-model="form.wareName" placeholder="请输入商品名称" :maxlength="30" style="width: 300px;"></el-input>
<div>
<el-button link type="primary">导入已有商品信息</el-button>
</div>
</div>
</el-form-item>
<el-form-item label="商品描述">
<el-input type="textarea" :rows="5" placeholder="请输入商品描述" :maxlength="50" show-password
v-model="form.wareDetail" style="width: 300px;"></el-input>
</el-form-item>
<el-form-item label="商品图片" prop="wareImgs">
<MultiImageUpload v-model="wareImgs" @uploadAllSuccess="wareImgsMultiOnSuccess" />
</el-form-item>
<el-form-item label="原价" prop="originalPrice">
<el-input v-model="form.originalPrice" placeholder="请输入商品原价" :maxlength="8"
@input="e => form.originalPrice = filterNumberInput(e)"></el-input>
</el-form-item>
<el-form-item label="拼团价" prop="groupPrice">
<el-input v-model="form.groupPrice" placeholder="请输入拼团价" :maxlength="8"
@input="e => form.groupPrice = filterNumberInput(e)"></el-input>
</el-form-item>
<el-form-item label="成团人数" prop="groupPeopleNum">
<el-input v-model="form.groupPeopleNum" placeholder="当参与人数达到成团人数才能完成拼团" :maxlength="8"
@input="e => form.groupPeopleNum = filterNumberInput(e, 1)"></el-input>
</el-form-item>
<el-form-item label="成团期限" prop="groupTimeoutHour">
<el-input v-model="form.groupTimeoutHour" placeholder="最小不低于1小时最大不超过72小时" :maxlength="8"
@input="e => form.groupTimeoutHour = filterNumberInput(e, 1)"></el-input>
</el-form-item>
<el-form-item label="限购数量" prop="limitBuyNum">
<div class="center">
<el-switch v-model="limitBuyNumSwitch" @change="limitBuyNumSwitchChange"></el-switch>
<div class="ipt" v-if="limitBuyNumSwitch">
<el-input :maxlength="8" v-model="form.limitBuyNum" placeholder="每人最多购买次数"
@input="e => form.limitBuyNum = filterNumberInput(e, 1)"></el-input>
</div>
</div>
</el-form-item>
<el-form-item label="上架状态">
<el-switch v-model="form.onlineStatus" :active-value="1" :inactive-value="0"></el-switch>
</el-form-item>
<el-form-item label="商品详情">
<MultiImageUpload v-model="wareCommentImgs" @upload-all-success="wareCommentImgsMultiOnSuccess" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" :loading="loading" @click="submitHandle"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { filterNumberInput } from '@/utils'
const visible = ref(true)
const limitBuyNumSwitch = ref(false) // 如果limitBuyNum = -10086 就是false不限购true为限购
const loading = ref(false)
const wareImgs = ref([])
const wareCommentImgs = ref([])
const formRef = ref(null)
const form = ref({
useShopType: 'only', // only-仅本店 all全部 /custom 指定
useShops: '', // 可用门店指定门店时存储门店ID逗号分隔
wareName: '', // 商品名称
wareDetail: '', // 商品描述
wareImgs: '', // 商品图片(多个用逗号分隔)
originalPrice: '', // 原价
groupPrice: '', // 拼团价
groupPeopleNum: '', // 成团人数 最小为1
groupTimeoutHour: '', // 成团期限小时不低于1小时最大72小时
limitBuyNum: '', // 限购数量(每人最多购买次数) -10086
onlineStatus: 1, // 上架状态0下架 1上架
wareCommentImgs: '', // 商品详情图片(多个用逗号分隔)
})
const rules = ref({
useShops: [
{
required: true,
message: "请选择门店",
trigger: "blur",
}
],
wareName: [
{
required: true,
message: "请输入商品名称",
trigger: "blur",
}
],
wareImgs: [
{
required: true,
message: "请选择商品图片",
trigger: "change",
}
],
originalPrice: [
{
required: true,
validator: (rule, value, callback) => {
if (form.value.originalPrice <= 0 || !form.value.originalPrice) {
callback(new Error('请输入商品原价'))
} else {
callback()
}
},
trigger: "blur",
}
],
groupPrice: [
{
required: true,
validator: (rule, value, callback) => {
if (form.value.groupPrice <= 0 || !form.value.groupPrice) {
callback(new Error('请输入拼团价'))
} else {
callback()
}
},
trigger: "blur",
}
],
groupPeopleNum: [
{
required: true,
validator: (rule, value, callback) => {
if (form.value.groupPeopleNum < 1 || !form.value.groupPeopleNum) {
callback(new Error('请输入成团人数'))
} else {
callback()
}
},
trigger: "blur",
}
],
groupTimeoutHour: [
{
required: true,
validator: (rule, value, callback) => {
if (form.value.groupTimeoutHour < 1 || !form.value.groupTimeoutHour) {
callback(new Error('请输入成团人数'))
} else if (form.value.groupTimeoutHour > 72) {
callback(new Error('最大不超过72小时'))
} else {
callback()
}
},
trigger: "blur",
}
],
limitBuyNum: [
{
validator: (rule, value, callback) => {
if (form.value.limitBuyNumSwitch && !form.value.limitBuyNum) {
callback(new Error('请输入每人最多购买次数'))
} else {
callback()
}
},
trigger: "blur",
}
]
})
function limitBuyNumSwitchChange(e) {
if (e) {
form.value.limitBuyNum = ''
} else {
form.value.limitBuyNum = -10086
}
}
// 多图上传成功
function wareImgsMultiOnSuccess(response) {
form.value.wareImgs = wareImgs.value.join(',')
}
// 商品详情图片上传成功
function wareCommentImgsMultiOnSuccess(res) {
form.value.wareCommentImgs = wareCommentImgs.value.join(',')
}
// 提交保存
function submitHandle() {
console.log('form.value===', form.value);
return
formRef.value.vaildate(async vaild => {
try {
if (vaild) {
const data = { ...form.value }
if (form.value.useShopType == 'custom') {
data.useShops = form.value.useShops.join(',')
}
}
} catch (error) {
console.log(error);
}
})
}
function show() {
visible.value = true
}
defineExpose({
show
})
</script>
<style scoped lang="scss">
.column {
display: flex;
flex-direction: column;
}
.center {
display: flex;
align-items: center;
gap: 10px;
}
</style>

View File

@@ -0,0 +1,116 @@
<!-- 拼团活动 -->
<template>
<div>
<div class="row">
<el-form :model="queryForm" inline @submit.prevent>
<el-form-item>
<el-button type="primary" icon="Plus" @click="addGroupGoodsRef.show()">添加</el-button>
</el-form-item>
<el-form-item label="上架状态">
<el-select v-model="queryForm.status" style="width: 100px;">
<el-option :label="item.label" :value="item.value" v-for="item in statusList" :key="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker style="width: 300px" v-model="times" type="daterange" range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD"
@change="selectTimeChange"></el-date-picker>
</el-form-item>
<el-form-item label="商品名称">
<el-input v-model="queryForm.name" placeholder="请输入商品名称"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" :loading="tableData.loading" @click="searchHandle">搜索</el-button>
<el-button icon="Refresh" :loading="tableData.loading" @click="refreshHandle">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="row mt14">
<el-table :data="tableData.list" stripe border v-loading="tableData.loading">
<el-table-column label="商品图片"></el-table-column>
<el-table-column label="商品名称"></el-table-column>
<el-table-column label="原价(元)"></el-table-column>
<el-table-column label="拼团价(元)"></el-table-column>
<el-table-column label="成团期限(小时)"></el-table-column>
<el-table-column label="成团人数"></el-table-column>
<el-table-column label="上架状态"></el-table-column>
<el-table-column label="创建时间"></el-table-column>
<el-table-column label="操作">
<template v-slot="scope">
<el-button link type="primary">编辑</el-button>
<el-button link type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<addGroupGoods ref="addGroupGoodsRef" />
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import addGroupGoods from './addGroupGoods.vue';
const addGroupGoodsRef = ref(null)
const statusList = ref([
{
label: '上架',
value: 1,
type: 'success'
},
{
label: '下架',
value: 0,
type: 'info'
},
])
const times = ref([])
const selectTimeChange = (value) => {
if (value && value.length === 2) {
queryForm.startTime = dayjs(value[0]).format('YYYY-MM-DD 00:00:00');
queryForm.endTime = dayjs(value[1]).format('YYYY-MM-DD 23:59:59');
} else {
queryForm.startTime = '';
queryForm.endTime = '';
}
};
const queryForm = reactive({
status: '',
startTime: '',
endTime: '',
name: ''
})
// 搜索
function searchHandle() {
}
// 重置搜索
function refreshHandle() {
queryForm.status = ''
queryForm.startTime = ''
queryForm.endTime = ''
queryForm.name = ''
}
const tableData = reactive({
page: 1,
size: 10,
total: 0,
list: [],
loading: false
})
</script>
<style scoped lang="scss">
.row {
&.mt14 {
margin-top: 14px;
}
}
</style>

View File

@@ -0,0 +1,59 @@
<template>
<div class="gyq_container">
<div class="gyq_content">
<HeaderCard name="商品拼团" intro="拼团" icon="sppt" showSwitch v-model:isOpen="form.isEnable">
</HeaderCard>
<div class="row mt14">
<tabHeader v-model="tabActiveIndex" :list="tabList" />
</div>
<div class="row mt14">
<groupPage ref="groupPageRef" name="groupPage" key="groupPage" />
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import groupPage from './components/groupPage.vue'
const groupPageRef = ref(null)
const tabActiveIndex = ref(0)
const tabList = ref([
{
label: '拼团活动',
value: 0
},
{
label: '拼团订单',
value: 1
}
])
const form = ref({
isEnable: 0
})
</script>
<style scoped lang="scss">
.gyq_container {
padding: 14px;
.gyq_content {
padding: 14px;
background-color: #fff;
border-radius: 8px;
}
}
.row {
&.mt14 {
margin-top: 14px;
}
}
.tips {
margin-top: 14px;
}
</style>