feat: 虚拟商品

fix-0609-xwk
向文可 2 years ago
parent cfb3675f99
commit 24ba936ab2

@ -0,0 +1,223 @@
<template>
<div class="upload-box">
<div :class="sortable && 'upload-box__sortable'">
<ul v-if="sortable" ref="sortableRef" class="sortable">
<li v-for="(item, idx) in imgList" :key="item.uid" class="sortable--item">
<img :src="item.url" />
<span class="sortable--item-hover">
<el-icon class="sortable--item-icon" name="ZoomIn" @click="handlePreview(item)" />
<el-icon class="sortable--item-icon" name="Delete" @click="handleRemove(idx)" />
</span>
</li>
</ul>
<el-upload
v-bind="props"
action="none"
:before-upload="handleBeforeUpload"
:class="{ max: imgList.length === props.limit, 'sortable--submit': sortable }"
:file-list="imgList"
:http-request="handleUpload"
list-type="text"
:on-exceed="handleExceed"
:show-file-list="!sortable"
>
<el-button type="primary">
<el-icon name="Plus" style="top: 0" />
<span>上传文件</span>
</el-button>
</el-upload>
</div>
<div class="el-upload__tip">支持小于 {{ fmtSize }} 文件</div>
</div>
</template>
<script setup lang="jsx">
import { upload } from '@/api/file';
import { ElMessage } from '@/plugins/element-plus';
import 'element-plus/es/components/image/style/css';
import Sortable from 'sortablejs';
const props = defineProps({
configId: {
type: String,
required: true,
},
sortable: {
type: Boolean,
default: false,
},
serviceName: {
type: String,
default: 'mall-product',
},
drag: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
defualt: false,
},
disabled: {
type: Boolean,
defualt: false,
},
limit: {
type: Number,
default: 1,
},
size: {
type: Number,
default: 1024 * 1024 * 20,
},
accept: {
type: String,
default: 'image/*',
},
});
const emits = defineEmits(['update:modelValue']);
const imgList = ref([]);
const attrs = useAttrs();
watch(
() => attrs.modelValue,
(value) => {
value = value instanceof Array ? value : [value];
if (
unref(imgList)
.map((item) => item.url)
.join(',') !== value?.join(',')
) {
imgList.value = value
.filter((item) => item)
.map((item) => {
return {
name: item,
response: item,
url: item,
};
});
}
},
{ immediate: true, deep: true }
);
watch(
() => imgList,
() => {
const arr = unref(imgList).map((item) => item.response);
if (arr.every((item) => !!item)) {
const value = props.limit === 1 ? arr[0] : arr;
emits('update:modelValue', value);
}
},
{ deep: true }
);
const handleRemove = (idx) => {
imgList.value.splice(idx, 1);
};
const handleExceed = (list) => {
console.info('[upload] exceed', list);
ElMessage.error('超出最大上传数量');
};
const handleBeforeUpload = (file) => {
console.info('[upload] upload', file);
let res = true;
if (file.type.startsWith('image/')) {
if (file.size >= props.size) {
ElMessage.error('超出文件大小限制');
res = false;
}
} else {
ElMessage.error('只允许上传图片');
res = false;
}
return res;
};
const handleUpload = async ({ file }) => {
return await upload(props.serviceName, props.configId, file);
};
const fmtSize = computed(() => {
const units = ['byte', 'KB', 'MB', 'GB', 'TB'];
let res = props.size,
unit = 0;
while (res >= 800) {
res /= 1024;
unit++;
}
return res + units[unit];
});
const sortableRef = ref(null);
const sortableInit = () => {
new Sortable(sortableRef.value, {
animation: 150,
// swapThreshold: 1,
// fallbackOnBody: true,
onUpdate({ newIndex, oldIndex }) {
const newData = imgList.value[newIndex];
const oldData = imgList.value[oldIndex];
imgList.value[newIndex] = oldData;
imgList.value[oldIndex] = newData;
},
});
};
onMounted(() => {
if (props.sortable) {
sortableInit();
}
});
</script>
<style lang="less" scoped>
.max {
:deep(.el-upload) {
display: none;
}
}
.upload-box__sortable {
display: flex;
}
.sortable {
--img-size: 148px;
display: inline-flex;
flex-wrap: wrap;
margin: 0;
&--item {
overflow: hidden;
background-color: var(--el-fill-color-blank);
border: 1px solid #c0ccda;
border-radius: 6px;
box-sizing: border-box;
width: var(--img-size);
height: var(--img-size);
margin: 0 8px 8px 0;
padding: 0;
display: inline-flex;
position: relative;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
&-icon {
margin: 0 10px;
font-size: 20px;
}
&-hover {
background: rgba(0, 0, 0, 0.5);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
color: #fff;
align-items: center;
justify-content: center;
opacity: 0;
display: flex;
cursor: pointer;
}
&:hover {
.sortable--item-hover {
opacity: 1;
}
}
}
}
</style>

@ -1,25 +1,26 @@
<template>
<div class="upload-box">
<div :class="sortable && 'upload-box__sortable'">
<ul class="sortable" ref="sortableRef" v-if="sortable">
<li class="sortable--item" v-for="(item, idx) in imgList" :key="item.uid">
<ul v-if="sortable" ref="sortableRef" class="sortable">
<li v-for="(item, idx) in imgList" :key="item.uid" class="sortable--item">
<img :src="item.url" />
<span class="sortable--item-hover">
<el-icon class="sortable--item-icon" name="ZoomIn" @click="handlePreview(item)"></el-icon>
<el-icon class="sortable--item-icon" name="Delete" @click="handleRemove(idx)"></el-icon>
<el-icon class="sortable--item-icon" name="ZoomIn" @click="handlePreview(item)" />
<el-icon class="sortable--item-icon" name="Delete" @click="handleRemove(idx)" />
</span>
</li>
</ul>
<el-upload
v-bind="props"
action="none"
:before-upload="handleBeforeUpload"
:class="{ max: imgList.length === props.limit, 'sortable--submit': sortable }"
:file-list="imgList"
:show-file-list="!sortable"
:list-type="!sortable && 'picture-card'"
:http-request="handleUpload"
:list-type="!sortable ? 'picture-card' : 'picture'"
:on-exceed="handleExceed"
:on-preview="handlePreview"
:show-file-list="!sortable"
>
<el-icon name="Plus" />
</el-upload>
@ -233,7 +234,7 @@
}
}
::v-deep .el-upload {
:deep(.el-upload) {
width: 148px;
height: 148px;
border: 1px dashed #c0ccda;

@ -44,6 +44,16 @@ const state = () => ({
value: 3,
},
],
type: [
{
label: '实物商品',
value: 1,
},
{
label: '虚拟商品',
value: 2,
},
],
limit: [
{
label: '不限购',

@ -16,6 +16,9 @@
}"
/>
</el-form-item>
<el-form-item label="商品类型" prop="productType">
<el-radio-group v-model="form.productType" :opts="opts.type" />
</el-form-item>
<el-form-item label="商品名称" prop="name">
<el-input v-model="form.name" maxlength="50" />
</el-form-item>
@ -29,12 +32,31 @@
<el-radio-group v-model="form.limit" :opts="opts.limit" />
<el-input-number v-show="form.limit === 1" v-model="form.singleBuyLimit" :min="1" />
</el-form-item>
<el-form-item label="邮费设置" prop="remoteAreaPostage">
<el-form-item v-if="form.productType === 1" label="邮费设置" prop="remoteAreaPostage">
<el-radio-group v-model="form.postage" :opts="opts.postage" />
<el-input-number v-show="form.postage === 0" v-model="form.remoteAreaPostage" :min="0" />
</el-form-item>
<el-form-item
v-if="form.productType === 2"
label="自动发货内容"
prop="virtualProductModifyDTOList"
required="true"
>
<el-upload-file
v-model="form.fileList"
config-id="product"
:limit="5"
multiple
:size="1024 * 1024 * 1024"
/>
<el-input
v-model="form.autoSend"
placeholder="自动发货内容将在用户付款后以商城内客服IM消息自动发货"
type="textarea"
/>
</el-form-item>
<el-form-item label="商品图片" prop="pictureList">
<el-upload-image v-model="form.pictureList" config-id="product" :limit="5" sortable multiple />
<el-upload-image v-model="form.pictureList" config-id="product" :limit="5" multiple sortable />
</el-form-item>
<el-form-item label="商品详情" prop="detail">
<el-editor v-model="form.detail" />
@ -63,6 +85,7 @@
form: {
id: null,
categoryId: null,
productType: 2,
name: null,
remark: null,
isRecommend: false,
@ -71,11 +94,15 @@
postage: 0,
remoteAreaPostage: 10,
pictureList: [],
fileList: [],
autoSend: null,
detail: null,
isEnable: false,
virtualProductModifyDTOList: [],
},
rules: {
categoryId: [{ required: true, message: '商品分类不能为空' }],
productType: [{ required: true, message: '商品类型不能为空' }],
name: [{ required: true, message: '商品名称不能为空' }],
remark: [{ required: true, message: '商品备注不能为空' }],
isRecommend: [{ required: true, message: '商家推荐不能为空' }],
@ -83,6 +110,17 @@
remoteAreaPostage: [{ required: true, message: '邮费设置不能为空' }],
pictureList: [{ required: true, message: '商品图片不能为空' }],
detail: [{ required: true, message: '商品详情不能为空' }],
virtualProductModifyDTOList: [
{
validator(rule, value, cb) {
if (!state.form.fileList.length && !state.form.autoSend) {
cb('自动发货内容不能为空');
} else {
cb();
}
},
},
],
},
});
const opts = computed(() => store.state.product.opts);

Loading…
Cancel
Save