master
RIceWqy 2 years ago
parent 8eb8354d71
commit 4b7cd48796

@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow npm",
"dev": "vite", "dev": "vite",
"dev:test": "vite --mode testing", "dev:test": "vite --mode testing",
"build": "vite build", "build": "vite build",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

@ -0,0 +1,128 @@
export const tableOption = {
searchMenuSpan: 6,
columnBtn: false,
border: true,
index: false,
selection: true,
indexLabel: '序号',
stripe: true,
menuAlign: 'center',
menuWidth: 350,
align: 'center',
refreshBtn: true,
searchSize: 'mini',
addBtn: false,
editBtn: false,
viewBtn: false,
delBtn: false,
props: {
label: 'label',
value: 'value'
},
column: [
{
label: '店铺id',
prop: 'shopId',
search: true
}, {
label: '店铺名称',
prop: 'shopName',
search: true
}, {
label: '店长用户id',
prop: 'userId',
search: true
}, {
label: '店铺类型',
prop: 'shopType',
search: true
}, {
label: '店铺简介',
prop: 'intro',
search: true
}, {
label: '店铺公告',
prop: 'shopNotice',
search: true
}, {
label: '店铺行业',
prop: 'shopIndustry',
search: true
}, {
label: '店长',
prop: 'shopOwner',
search: true
}, {
label: '店铺绑定的手机',
prop: 'mobile',
search: true
}, {
label: '店铺联系电话',
prop: 'tel',
search: true
}, {
label: '店铺所在纬度',
prop: 'shopLat',
search: true
}, {
label: '店铺所在经度',
prop: 'shopLng',
search: true
}, {
label: '店铺详细地址',
prop: 'shopAddress',
search: true
}, {
label: '店铺所在省份',
prop: 'province',
search: true
}, {
label: '店铺所在城市',
prop: 'city',
search: true
}, {
label: '店铺所在区域',
prop: 'area',
search: true
}, {
label: '店铺省市区代码',
prop: 'pcaCode',
search: true
}, {
label: '店铺logo',
prop: 'shopLogo',
search: true
}, {
label: '店铺相册',
prop: 'shopPhotos',
search: true
}, {
label: '每天营业时间段',
prop: 'openTime',
search: true
}, {
label: '店铺状态(-1:未开通 0: 停业中 1:营业中)',
prop: 'shopStatus',
search: true
}, {
label: '0:商家承担运费 1:买家承担运费',
prop: 'transportType'
}, {
label: '固定运费',
prop: 'fixedFreight'
}, {
label: '满X包邮',
prop: 'fullFreeShipping'
}, {
label: '创建时间',
prop: 'createTime'
}, {
label: '更新时间',
prop: 'updateTime'
}, {
label: '分销设置(0关闭 1开启)',
prop: 'isDistribution'
}
]
}

@ -15,14 +15,15 @@
v-if="!sidebarFold" v-if="!sidebarFold"
class="site-navbar-lg" class="site-navbar-lg"
> >
mall4j建站后台 金桐商户管理平台
</span> </span>
<span <span
v-else v-else
class="site-navbar-mini" class="site-navbar-mini"
:style="fontCloseSize" :style="fontCloseSize"
> >
mall4j 金桐商户管理平台
</span> </span>
</div> </div>
<!--右侧数据--> <!--右侧数据-->

@ -36,6 +36,11 @@ export const mainRoutes = {
path: '/prodInfo', path: '/prodInfo',
name: 'prodInfo', name: 'prodInfo',
component: () => import('@/views/modules/prod/prodInfo/index.vue') component: () => import('@/views/modules/prod/prodInfo/index.vue')
},
{
path: '/shopInfo',
name: 'shopInfo',
component: () => import('@/views/modules/shop/shopInfo/index.vue')
} }
], ],
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars

@ -1,134 +1,9 @@
<template> <template>
<div class="mod-home"> <div class="mod-home" ><h1>欢迎</h1> </div>
<p>一个基于spring bootspring oauth2.0mybatisredis的轻量级前后端分离拥有完整sku和下单流程的完全开源商城</p>
<p>&nbsp;</p>
<p>该项目仅供学习参考可供个人学习使用如需商用联系作者进行授权否则必将追究法律责任</p>
<p>&nbsp;</p>
<h2>前言</h2>
<p>
<code>mall4j商城</code>项目致力于为中小企业打造一个完整易于维护的开源的电商系统采用现阶段流行技术实现后台管理系统包含商品管理订单管理运费模板规格管理会员管理运营管理内容管理统计报表权限管理设置等模块
</p>
<p>&nbsp;</p>
<h2>技术选型</h2>
<figure>
<table
border="1"
cellspacing="0"
cellpadding="5px"
>
<thead>
<tr>
<th>技术</th>
<th>版本</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>Spring Boot</td>
<td>3.0.4</td>
<td>MVC核心框架</td>
</tr>
<tr>
<td>MyBatis</td>
<td>3.5.0</td>
<td>ORM框架</td>
</tr>
<tr>
<td>MyBatisPlus</td>
<td>3.5.3.1</td>
<td>基于mybatis使用lambda表达式的</td>
</tr>
<tr>
<td>Swagger-UI</td>
<td>4.0.0</td>
<td>文档生产工具</td>
</tr>
<tr>
<td>redisson</td>
<td>3.19.3</td>
<td>对redis进行封装集成分布式锁等</td>
</tr>
<tr>
<td>hikari</td>
<td>3.2.0</td>
<td>数据库连接池</td>
</tr>
<tr>
<td>log4j2</td>
<td>2.17.2</td>
<td>更快的log日志工具</td>
</tr>
<tr>
<td>lombok</td>
<td>1.18.8</td>
<td>简化对象封装工具</td>
</tr>
<tr>
<td>hutool</td>
<td>5.8.15</td>
<td>更适合国人的java工具集</td>
</tr>
<tr>
<td>xxl-job</td>
<td>2.3.1</td>
<td>定时任务</td>
</tr>
</tbody>
</table>
</figure>
<p>&nbsp;</p>
<h2>部署教程</h2>
<p>&nbsp;</p>
<h3>1.开发环境</h3>
<figure>
<table
border="1"
cellspacing="0"
cellpadding="5px"
>
<thead>
<tr>
<th>工具</th>
<th>版本</th>
</tr>
</thead>
<tbody>
<tr>
<td>jdk</td>
<td>17</td>
</tr>
<tr>
<td>mysql</td>
<td>5.7+</td>
</tr>
<tr>
<td>redis</td>
<td>3.2+</td>
</tr>
</tbody>
</table>
</figure>
<h3>2.启动</h3>
<ul>
<li>推荐使用idea安装lombok插件使用idea导入maven项目</li>
<li>
将shop.sql导入到mysql中修改
<code>application-dev.yml</code>更改 datasource.urluserpassword
</li>
<li>启动redis</li>
<li>
通过
<code>WebApplication</code>启动项目后台接口
<code>ApiApplication</code> 启动项目前端接口
</li>
</ul>
<p>&nbsp;</p>
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.mod-home { .mod-home {
line-height: 1.5; line-height: 1.5;
} }
</style> </style>

@ -107,6 +107,7 @@
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { isMobile } from '@/utils/validate' import { isMobile } from '@/utils/validate'
import { Debounce } from '@/utils/debounce' import { Debounce } from '@/utils/debounce'
const emit = defineEmits(['refreshDataList']) const emit = defineEmits(['refreshDataList'])
const visible = ref(false) const visible = ref(false)
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
@ -119,21 +120,56 @@ const validateMobile = (rule, value, callback) => {
} }
const dataRule = { const dataRule = {
addrName: [ addrName: [
{ required: true, message: '自提点名称不能为空', trigger: 'blur' }, {
{ pattern: /\s\S+|S+\s|\S/, message: '请输入正确的自提点名称', trigger: 'blur' } required: true,
message: '自提点名称不能为空',
trigger: 'blur'
},
{
pattern: /\s\S+|S+\s|\S/,
message: '请输入正确的自提点名称',
trigger: 'blur'
}
], ],
addr: [ addr: [
{ required: true, message: '地址不能为空', trigger: 'blur' }, {
{ pattern: /\s\S+|S+\s|\S/, message: '请输入正确的地址', trigger: 'blur' } required: true,
message: '地址不能为空',
trigger: 'blur'
},
{
pattern: /\s\S+|S+\s|\S/,
message: '请输入正确的地址',
trigger: 'blur'
}
], ],
city: [{ required: true, message: '城市不能为空', trigger: 'blur' }], city: [{
required: true,
message: '城市不能为空',
trigger: 'blur'
}],
province: [ province: [
{ required: true, message: '省份不能为空', trigger: 'blur' } {
required: true,
message: '省份不能为空',
trigger: 'blur'
}
], ],
area: [{ required: true, message: '区/县不能为空', trigger: 'blur' }], area: [{
required: true,
message: '区/县不能为空',
trigger: 'blur'
}],
mobile: [ mobile: [
{ required: true, message: '手机号不能为空', trigger: 'blur' }, {
{ validator: validateMobile, trigger: 'blur' } required: true,
message: '手机号不能为空',
trigger: 'blur'
},
{
validator: validateMobile,
trigger: 'blur'
}
] ]
} }
const provinceList = ref([]) const provinceList = ref([])

@ -65,6 +65,7 @@ import { isAuth } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import AddOrUpdate from './add-or-update.vue' import AddOrUpdate from './add-or-update.vue'
import { tableOption } from '@/crud/shop/pickAddr.js' import { tableOption } from '@/crud/shop/pickAddr.js'
const permission = { const permission = {
delBtn: isAuth('prod:prod:delete') delBtn: isAuth('prod:prod:delete')
} }
@ -145,7 +146,8 @@ const onDelete = (id) => {
}) })
}) })
}) })
.catch(() => { }) .catch(() => {
})
} }
/** /**

@ -0,0 +1,159 @@
<template>
<div class="mod-prod-prod-transport">
<el-form-item
label="运费设置"
:rules="[{ required: true, message: '运费模板不能为空'}]"
>
<el-select
v-model="transportId"
placeholder="请选择"
@change="changeTransport"
>
<el-option
v-for="transport in transportList"
:key="transport.transportId"
:label="transport.transName"
:value="transport.transportId"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-table
v-if="transportInfo.transfees"
:data="transportInfo.transfees"
style="width: 100%"
>
<el-table-column
label="配送区域"
width="350"
>
<template #default="scope">
<span v-if="!scope.row.cityList.length"></span>
<el-tag
v-for="city in scope.row.cityList"
v-else
:key="city.areaId"
>
{{ city.areaName }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="firstPiece"
:label="tableTitle[0]"
/>
<el-table-column
prop="firstFee"
:label="tableTitle[1]"
/>
<el-table-column
prop="continuousPiece"
:label="tableTitle[2]"
/>
<el-table-column
prop="continuousFee"
:label="tableTitle[3]"
/>
</el-table>
</el-form-item>
<el-form-item v-if="transportInfo.hasFreeCondition === 1">
<el-table
:data="transportInfo.transfeeFrees"
style="width: 100%"
>
<el-table-column
label="指定区域"
width="350"
>
<template #default="scope">
<el-tag
v-for="city in scope.row.freeCityList"
:key="city.areaId"
>
{{ city.areaName }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="freeType"
label="包邮条件"
>
<template #default="scope">
<span v-if="scope.row.freeType === 0">//</span>
<span v-if="scope.row.freeType === 1"></span>
<span v-if="scope.row.freeType === 2">//</span>
</template>
</el-table-column>
<el-table-column prop="amount">
<template #default="scope">
<span v-if="scope.row.freeType === 1">{{ scope.row.amount }}</span>
<span v-if="scope.row.freeType === 0">{{ scope.row.piece }}//</span>
<span v-if="scope.row.freeType === 2">{{ scope.row.piece }}//{{ scope.row.amount }}</span>
</template>
</el-table-column>
</el-table>
</el-form-item>
</div>
</template>
<script setup>
const props = defineProps({
modelValue: {
default: null,
type: Number
}
})
const emit = defineEmits(['input', 'update:modelValue'])
const transportId = ref(null)
const tableTitle = computed(() => {
const titles = [['首件(个)', '运费(元)', '续件(个)', '续费(元)'], ['首重(kg)', '运费(元)', '续重(kg)', '续费(元)'], ['首体积(m³)', '运费(元)', '续体积(m³)', '续费(元)']]
if (transportInfo.value.chargeType) {
return titles[transportInfo.value.chargeType]
}
return titles[0]
})
watch(
() => props.modelValue,
(id) => {
transportId.value = id
})
onMounted(() => {
getTransportList()
})
const transportList = ref([{
transportId: null,
transName: ''
}])
const getTransportList = () => {
http({
url: http.adornUrl('/shop/transport/list'),
method: 'get',
params: http.adornParams({})
})
.then(({ data }) => {
transportList.value = data
})
}
const transportInfo = ref({
hasFreeCondition: false,
transfeeFrees: [{ freeCityList: [] }]
})
const changeTransport = (id) => {
emit('update:modelValue', id)
if (!id) {
return
}
http({
url: http.adornUrl(`/shop/transport/info/${transportId.value}`),
method: 'get',
params: http.adornParams({})
})
.then(({ data }) => {
transportInfo.value = data
})
}
</script>

@ -0,0 +1,246 @@
<template>
<div class="mod-prod-sku-table">
<el-form-item>
<el-table
:data="modelValue"
border
style="width: 100%; margin-top: 20px"
>
<el-table-column
v-for="(leftTitle, index) in tableLeftTitles"
:key="index"
:label="leftTitle"
>
<template #default="scope">
{{ scope.row.properties.split(';')[index].split(':')[1] }}
</template>
</el-table-column>
<el-table-column
v-if="tableLeftTitles.length"
prop="pic"
label="sku图片"
width="180"
>
<template #default="scope">
<pic-upload v-model="scope.row.pic" />
</template>
</el-table-column>
<el-table-column
v-if="tableLeftTitles.length"
prop="prodName"
label="商品名称"
width="250"
>
<template #default="scope">
<el-input
v-model="scope.row.prodName"
type="textarea"
:disabled="!scope.row.status"
/>
</template>
</el-table-column>
<el-table-column
prop="price"
label="销售价"
width="160"
>
<template #default="scope">
<el-input-number
v-model="scope.row.price"
controls-position="right"
:precision="2"
:max="1000000000"
:min="0.01"
:disabled="!scope.row.status"
/>
</template>
</el-table-column>
<el-table-column
prop="oriPrice"
label="市场价"
width="160"
>
<template #default="scope">
<el-input-number
v-model="scope.row.oriPrice"
controls-position="right"
:precision="2"
:max="1000000000"
:min="0.01"
:disabled="!scope.row.status"
/>
</template>
</el-table-column>
<el-table-column
prop="stocks"
label="库存"
width="160"
>
<template #default="scope">
<el-input-number
v-model="scope.row.stocks"
:min="0"
controls-position="right"
type="number"
:disabled="!scope.row.status"
/>
</template>
</el-table-column>
<el-table-column
prop="weight"
label="商品重量(kg)"
width="210"
>
<template #default="scope">
<el-input-number
v-model="scope.row.weight"
:precision="2"
:min="0"
controls-position="right"
:disabled="!scope.row.status"
/>
</template>
</el-table-column>
<el-table-column
prop="volume"
label="商品体积(m³)"
width="210"
>
<template #default="scope">
<el-input-number
v-model="scope.row.volume"
:precision="2"
:min="0"
controls-position="right"
:disabled="!scope.row.status"
/>
</template>
</el-table-column>
<el-table-column
label="操作"
>
<template #default="scope">
<el-button
v-if="scope.row.status"
type="text"
@click="changeSkuStatus(`${scope.$index}`)"
>
禁用
</el-button>
<el-button
v-else
type="text"
@click="changeSkuStatus(`${scope.$index}`)"
>
启用
</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
</div>
</template>
<script setup>
import { scoreProdStore } from '@/stores/prod.js'
const props = defineProps({
modelValue: {
default: () => [],
type: Array
},
prodName: {
default: '',
type: String
}
})
const emit = defineEmits(['update:modelValue'])
const dbSpecs = ref([]) //
let initing = false
const tableLeftTitles = computed(() => {
const res = []
for (let i = 0; i < skuTags.value.length; i++) {
const skuTag = skuTags.value[i]
res.push(skuTag.tagName)
}
return res
})
const prod = scoreProdStore()
const skuTags = computed({
get () { return prod.skuTags }
})
watch(() => props.prodName,
() => {
skuAddProdName()
})
onMounted(() => {
http({
url: http.adornUrl('/prod/spec/list'),
method: 'get',
params: http.adornParams()
})
.then(({ data }) => {
dbSpecs.value = data
})
})
const init = () => {
initing = true
}
defineExpose({ init })
const changeSkuStatus = (tagIndex) => {
// eslint-disable-next-line vue/no-mutating-props
props.modelValue[tagIndex].status = props.modelValue[tagIndex].status ? 0 : 1
}
const skuAddProdName = () => {
if (initing) return
const skuList = []
for (let i = 0; i < props.modelValue.length; i++) {
const sku = Object.assign({}, props.modelValue[i])
if (!sku.properties) {
return
}
sku.skuName = ''
const properties = sku.properties.split(';')
for (const propertiesKey in properties) {
sku.skuName += properties[propertiesKey].split(':')[1] + ' '
}
sku.prodName = props.prodName + ' ' + sku.skuName
skuList.push(sku)
}
emit('update:modelValue', skuList)
}
</script>
<style lang="scss" scoped>
.mod-prod-sku-table{
:deep(.pic-uploader-component .el-upload) {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
.pic-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 120px;
height: 120px;
line-height: 120px;
text-align: center;
}
.pic {
width: 120px;
height: 120px;
display: block;
}
}
:deep(.pic-uploader-component .el-upload:hover) {
border-color: #409EFF;
}
}
</style>

@ -0,0 +1,534 @@
<template>
<div class="mod-prod-sku-tag">
<el-form-item label="商品规格">
<el-button @click="shopTagInput()">
添加规格
</el-button>
<div
v-for="(tag, tagIndex) in skuTags"
:key="tagIndex"
>
<span>{{ tag.tagName }}</span>
<el-button
class="button-new-tag"
type="text"
icon="el-icon-delete"
@click="removeTag(tagIndex)"
>
删除
</el-button>
<br>
<el-tag
v-for="(tagItem, tagItemIndex) in tag.tagItems"
:key="tagItem.valueId"
closable
:disable-transitions="false"
@close="handleTagClose(tagIndex, tagItemIndex)"
>
{{ tagItem.propValue }}
</el-tag>
<el-input
v-if="tagItemInputs[tagIndex] && tagItemInputs[tagIndex].visible"
:ref="`saveTagInput${tagIndex}`"
v-model="tagItemInputs[tagIndex].value"
class="input-new-tag"
@keyup.enter="handleInputConfirm(tagIndex)"
@blur="handleInputConfirm(tagIndex)"
/>
<el-button
v-else
class="button-new-tag"
@click="showTagInput(tagIndex)"
>
+ 添加
</el-button>
</div>
</el-form-item>
<el-form-item
v-show="isShowTagInput"
label="规格名"
>
<el-col :span="8">
<el-select
v-model="addTagInput.propName"
filterable
allow-create
default-first-option
placeholder="请选择"
@change="handleTagClick"
>
<el-option
v-for="item in unUseTags"
:key="item.propId"
:label="item.propName"
:value="item.propName"
/>
</el-select>
</el-col>
</el-form-item>
<el-form-item
v-show="isShowTagInput"
label="规格值"
>
<el-col :span="8">
<el-select
v-model="addTagInput.selectValues"
multiple
filterable
allow-create
default-first-option
placeholder="请选择"
>
<el-option
v-for="item in dbTagValues"
:key="item.valueId"
:label="item.propValue"
:value="item.propValue"
/>
</el-select>
</el-col>
</el-form-item>
<el-form-item>
<el-button
v-show="isShowTagInput"
type="primary"
@click="addTag()"
>
确定
</el-button>
<el-button
v-show="isShowTagInput"
@click="hideTagInput()"
>
取消
</el-button>
</el-form-item>
</div>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { scoreProdStore } from '@/stores/prod.js'
const prod = scoreProdStore()
const props = defineProps({
skuList: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['change'])
const isShowTagInput = ref(false)
const addTagInput = ref({
propName: '',
selectValues: []
})
const type = ref(0)
const tagItemName = ref('')
const tagItemInputs = ref([])
const dbTagValues = ref([]) //
const dbTags = ref([]) //
let tagName = ''
let tagNameIndex = 0
let maxValueId = 0 // id
let maxPropId = 0 // id
let initing = false
const skuTags = computed({
get () { return prod.skuTags },
set (val) { prod.updateSkuTags(val) }
})
/**
* 未使用的规格, 通过计算属性计算
*/
const unUseTags = computed(() => {
const res = []
for (let i = 0; i < dbTags.value.length; i++) {
const dbTag = dbTags.value[i]
const specIndex = skuTags.value?.findIndex(tag => tag.tagName === dbTag.propName)
if (specIndex === -1) {
res.push(dbTag)
}
}
return res
})
const defalutSku = computed(() => {
return prod.defalutSku
})
watch(() => skuTags.value,
(val) => {
if (initing) {
initing = false
return
}
let skuListArr = []
if (type.value === 4) {
//
props.skuList?.forEach(sku => {
const propertiesArray = sku.properties.split(';')
if (tagItemName.value !== propertiesArray[tagNameIndex].split(':')[1]) {
skuListArr.push(sku)
}
})
} else if (type.value === 2) {
//
const properties = tagName + ':' + tagItemName.value
//
let tempSkuList = []
val?.forEach(tag => {
if (skuListArr.length === 0) {
if (tagName === tag.tagName) {
const sku = Object.assign({}, defalutSku.value)
sku.properties = properties //
skuListArr.push(sku)
} else {
tag.tagItems.forEach(tagItem => {
const sku = Object.assign({}, defalutSku.value)
sku.properties = `${tag.tagName}:${tagItem.propValue}` //
skuListArr.push(sku)
})
}
if (val.length === 1) {
skuListArr = props.skuList.concat(skuListArr)
}
} else {
tempSkuList = []
if (tagName === tag.tagName) {
skuListArr.forEach(sku => {
if (sku.properties.indexOf(tagName) === -1) {
const newSku = Object.assign({}, sku)
newSku.properties = `${sku.properties};${properties}`
tempSkuList.push(newSku)
}
})
} else {
tag.tagItems.forEach(tagItem => {
skuListArr.forEach(sku => {
if (sku.properties.indexOf(tag.tagName) === -1) {
const newSku = Object.assign({}, sku)
newSku.properties = `${sku.properties};${tag.tagName}:${tagItem.propValue}`
tempSkuList.push(newSku)
}
})
})
}
skuListArr = props.skuList.concat(tempSkuList)
}
})
} else {
//
let tempSkuList = []
val?.forEach(tag => {
if (skuListArr.length === 0) {
tag.tagItems.forEach(tagItem => {
const sku = Object.assign({}, defalutSku.value)
sku.properties = `${tag.tagName}:${tagItem.propValue}` //
skuListArr.push(sku)
})
} else {
tempSkuList = []
tag.tagItems.forEach(tagItem => {
skuListArr.forEach(sku => {
const newSku = Object.assign({}, sku)
newSku.properties = `${sku.properties};${tag.tagName}:${tagItem.propValue}`
tempSkuList.push(newSku)
})
})
skuListArr = tempSkuList
}
})
}
if (!skuListArr.length) {
skuListArr.push(Object.assign({}, defalutSku.value))
}
// debugger
emit('change', skuListArr)
},
{
deep: true
}
)
onMounted(() => {
http({
url: http.adornUrl('/prod/spec/list'),
method: 'get',
params: http.adornParams()
})
.then(({ data }) => {
dbTags.value = data
if (data) {
maxPropId = Math.max.apply(Math, data.map(item => item.propId))
} else {
maxPropId = 0
}
})
http({
url: http.adornUrl('/prod/spec/listSpecMaxValueId'),
method: 'get',
params: http.adornParams()
})
.then(({ data }) => {
if (data) {
maxValueId = data
} else {
maxValueId = 0
}
})
})
const init = (skuList) => {
if (!skuList || !skuList.length) {
skuTags.value = []
emit('change', [Object.assign({}, defalutSku.value)])
return
}
initing = true
const skuTagsParam = []
for (let i = 0; i < skuList.length; i++) {
const sku = skuList[i]
if (!sku.properties) break
const propertiesArray = sku.properties.split(';')
for (const j in propertiesArray) {
const properties = propertiesArray[j].split(':')
if (!skuTagsParam[j]) {
skuTagsParam[j] = {
tagName: properties[0],
tagItems: []
}
tagItemInputs.value.push({ visible: false, value: '' })
}
const tagItemNameIndex = skuTagsParam[j].tagItems.findIndex((tagItemName) => tagItemName.propValue === properties[1])
if (tagItemNameIndex === -1) {
skuTagsParam[j].tagItems.push({ propValue: properties[1] })
}
}
}
skuTags.value = skuTagsParam
}
defineExpose({ init })
/**
* 显示规格名规格值输入框
*/
const shopTagInput = () => {
isShowTagInput.value = true
}
/**
* 隐藏规格名规格值输入框
*/
const hideTagInput = () => {
isShowTagInput.value = false
cleanTagInput()
}
const addTag = () => {
const selectValues = addTagInput.value.selectValues
if (!addTagInput.value.propName) {
ElMessage.error('请输入规格名')
return
}
if (!selectValues.length) {
ElMessage.error('请输入规格值')
return
}
isShowTagInput.value = false
for (let i = 0; i < selectValues.length; i++) {
const element = selectValues[i]
const is = Object.prototype.toString.call(element) === '[object Object]'
if (!is) {
maxPropId = maxPropId + 1
break
}
}
const tagItems = []
for (let i = 0; i < selectValues.length; i++) {
const element = selectValues[i]
const is = Object.prototype.toString.call(element) === '[object Object]'
if (is) {
tagItems.push(element)
} else {
maxValueId = maxValueId + 1
tagItems.push({ propId: maxPropId, propValue: element, valueId: maxValueId })
}
}
//
prod.addSkuTag({
tagName: addTagInput.value.propName,
tagItems
})
type.value = 1
cleanTagInput()
}
/**
* 清除规格值输入框内容
*/
const cleanTagInput = () => {
addTagInput.value = {
propName: '',
selectValues: []
}
dbTagValues.value = []
}
/**
* 规格名输入框选中规格事件
*/
const handleTagClick = () => {
//
dbTagValues.value = []
addTagInput.value.selectValues = []
//
const specsIndex = dbTags.value?.findIndex(spec => spec.propName === addTagInput.value.propName)
// 便
if (specsIndex === -1) return
// id
http({
url: http.adornUrl(`/prod/spec/listSpecValue/${dbTags.value[specsIndex].propId}`),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
dbTagValues.value = data
})
}
/**
* 规格名输入框选中规格事件
*/
const handleTagClose = (tagIndex, tagItemIndex) => {
tagName = skuTags.value[tagIndex].tagName
tagNameIndex = tagIndex
tagItemName.value = skuTags.value[tagIndex].tagItems[tagItemIndex].propValue
if (skuTags.value[tagIndex].tagItems.length === 1) {
return
}
type.value = 4
prod.removeSkuTagItem(tagIndex, tagItemIndex)
}
/**
* 标签输入框确定时调用
*/
const handleInputConfirm = (tagIndex) => {
if (checkTagItem(tagIndex)) {
return
}
const tagItems = skuTags.value[tagIndex].tagItems
const itemValue = tagItemInputs.value[tagIndex].value
const index = tagItems.length - 1
tagName = skuTags.value[tagIndex].tagName
tagItemName.value = tagItemInputs.value[tagIndex].value
const maxValue = getMaxValueId(skuTags.value[tagIndex].tagItems)
const tagItem = { propId: index === -1 ? 0 : skuTags.value[tagIndex].tagItems[index].propId, propValue: itemValue, valueId: index === -1 ? 0 : (maxValue + 1) }
if (tagItem) {
prod.addSkuTagItem({ tagIndex, tagItem })
}
tagItemInputs.value[tagIndex].visible = false
tagItemInputs.value[tagIndex].value = ''
type.value = 2
}
/**
* 显示标签输入框
*/
const showTagInput = (tagIndex) => {
tagItemInputs.value.push({ visible: false, value: '' })
tagItemInputs.value[tagIndex].visible = true
nextTick(() => {
[`saveTagInput${tagIndex}`][0].value.input.focus()
})
}
/**
* 获取数据集合所有对象中某个属性的最大值
*/
const getMaxValueId = (list) => {
return Math.max.apply(Math, list.map(item => item.valueId))
}
/**
* 删除 规格
* @param tagIndex
*/
const removeTag = (tagIndex) => {
type.value = 3
prod.removeSkuTag(tagIndex)
}
/**
* 新增规格值时判断是否存在同名的规格值
*/
const checkTagItem = (tagIndex) => {
const tagItem = tagItemInputs.value[tagIndex].value
if (!tagItem) {
tagItemInputs.value[tagIndex].visible = false
tagItemInputs.value[tagIndex].value = ''
return true
}
let isSame = false
skuTags.value?.forEach(tag => {
const arr = tag.tagItems.map((item) => {
return item.propValue
})
if (arr.indexOf(tagItem) > -1) {
isSame = true
ElMessage.error('product.specificationValue')
return false
}
})
return isSame
}
</script>
<style lang="scss" scoped>
.mod-prod-sku-tag {
:deep(.el-tag + .el-tag) {
margin-left: 10px;
}
.button-new-tag {
margin-left: 10px;
height: 32px;
line-height: 30px;
padding-top: 0;
padding-bottom: 0;
}
.input-new-tag {
width: 90px;
margin-left: 10px;
vertical-align: bottom;
}
}
//
:deep(.sku-border) {
border: 1px solid #EBEEF5;
width:70%
}
:deep(.sku-background){
background-color: #F6f6f6;
margin: 12px 12px;
.el-button{
margin-left: 10px;
span{
color:#000 !important;
}
}
.el-form-item__label{
padding:0 24px 0 0
}
}
:deep(.sku-tag){
margin: 12px 12px;
}
:deep(.tagTree){
margin-left: 18px;
padding-bottom:8px;
}
.el-form-item__content div {
width: 100%;
}
</style>

@ -0,0 +1,218 @@
<template>
<div class="mod-prod-info">
123123
</div>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { treeDataTranslate, idList } from '@/utils'
import ProdTransport from './components/prod-transport.vue'
import SkuTag from './components/sku-tag.vue'
import SkuTable from './components/sku-table.vue'
import { Debounce } from '@/utils/debounce'
const emit = defineEmits(['refreshDataList'])
//
const category = reactive({
list: [],
selected: [],
props: {
value: 'categoryId',
label: 'categoryName'
}
})
//
const dataForm = ref({
prodName: '',
brief: '',
pic: '',
imgs: '',
categoryId: 0,
prodId: 0,
skuList: [],
tagList: [],
content: '',
status: 1,
deliveryMode: {
hasShopDelivery: false,
hasUserPickUp: false
},
deliveryTemplateId: null
})
const tags = ref([])
onMounted(() => {
dataForm.value.prodId = useRoute().query.prodId
getDataList()
})
const skuTableRef = ref(null)
const skuTagRef = ref(null)
/**
* 获取分类数据
*/
const getDataList = () => {
getTagList()
getCategoryList().then(() => {
if (dataForm.value.prodId) {
//
http({
url: http.adornUrl(`/prod/prod/info/${dataForm.value.prodId}`),
method: 'get',
params: http.adornParams()
})
.then(({ data }) => {
dataForm.value = data
dataForm.value.deliveryMode = JSON.parse(data.deliveryMode)
skuTagRef.value?.init(data.skuList)
skuTableRef.value?.init()
category.selected = idList(category.list, dataForm.value.categoryId, 'categoryId', 'children').reverse()
dataForm.value.tagList = data.tagList
})
} else {
nextTick(() => {
dataFormRef.value?.resetFields()
skuTagRef.value?.init()
dataForm.value.pic = ''
dataForm.value.imgs = ''
})
}
})
}
/**
* 获取分类信息
*/
const getCategoryList = () => {
return http({
url: http.adornUrl('/prod/category/listCategory'),
method: 'get',
params: http.adornParams()
})
.then(({ data }) => {
category.list = treeDataTranslate(data, 'categoryId', 'parentId')
})
}
/**
* 选择分类改变事件
* @param val
*/
const handleCategoryChange = (val) => {
dataForm.value.categoryId = val[val.length - 1]
}
const router = useRouter()
const dataFormRef = ref(null)
/**
* 表单提交
*/
const onSubmit = Debounce(() => {
dataFormRef.value?.validate((valid) => {
if (!valid) {
return
}
if (!dataForm.value.imgs) {
errorMsg('请选择图片上传')
return
}
if (!dataForm.value.deliveryMode) {
errorMsg('请选择配送方式')
return
}
if (dataForm.value.deliveryMode.hasShopDelivery && !dataForm.value.deliveryTemplateId) {
errorMsg('请选择运费模板')
return
}
const param = Object.assign({}, dataForm.value)
//
paramSetPriceAndStocks(param)
param.deliveryMode = undefined
param.deliveryModeVo = dataForm.value.deliveryMode
//
param.pic = dataForm.value.imgs.split(',')[0]
http({
url: http.adornUrl('/prod/prod'),
method: param.prodId ? 'put' : 'post',
data: http.adornData(param)
})
.then(() => {
ElMessage({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
router.push({
path: '/prod/prodList'
})
emit('refreshDataList')
}
})
})
})
})
const paramSetPriceAndStocks = (param) => {
//
param.totalStocks = 0
//
param.price = 0
//
param.oriPrice = 0
//
for (let i = 0; i < param.skuList.length; i++) {
const element = param.skuList[i]
if (element.status !== 1) {
continue
}
if (param.price === 0) {
param.price = element.price ? Number.parseFloat(element.price) : 0
}
//
param.price = Math.min(param.price, element.price)
if (param.price === element.price) {
param.oriPrice = element.oriPrice ? Number.parseFloat(element.oriPrice) : 0
}
param.totalStocks += element.stocks ? Number.parseInt(element.stocks) : 0
}
// sku使
if (param.skuList.length === 1) {
param.skuList[0].prodName = dataForm.value.prodName
}
}
const skuTagChangeSkuHandler = (skuList) => {
const prodName = dataForm.value.prodName
skuList.forEach(sku => {
if (sku.properties) {
sku.skuName = ''
const properties = sku.properties.split(';')
for (const propertiesKey in properties) {
sku.skuName += properties[propertiesKey].split(':')[1] + ' '
}
sku.prodName = prodName + ' ' + sku.skuName
}
})
dataForm.value.skuList = skuList
}
const errorMsg = (message) => {
ElMessage({
message,
type: 'error',
duration: 1500
})
}
/**
* 获取所有的分组信息
*/
const getTagList = () => {
http({
url: http.adornUrl('/prod/prodTag/listTagList'),
method: 'get',
params: http.adornParams()
})
.then(({ data }) => {
tags.value = data
})
}
</script>

@ -0,0 +1,184 @@
<template>
<div class="mod-pickAddr">
<avue-crud
ref="crudRef"
:page="page"
:data="dataList"
:option="tableOption"
:permission="permission"
@search-change="onSearch"
@selection-change="selectionChange"
@on-load="getDataList"
>
<template #menu-left>
<!-- <el-button
v-if="isAuth('shop:pickAddr:save')"
type="primary"
icon="el-icon-plus"
@click.stop="onAddOrUpdate()"
>
新增
</el-button>
<el-button
v-if="isAuth('shop:pickAddr:delete')"
type="danger"
:disabled="dataListSelections.length <= 0"
@click="onDelete()"
>
批量删除
</el-button> -->
</template>
<template #menu="scope">
<el-button
v-if="isAuth('shop:shopDetail:info')"
type="primary"
@click.stop="onAddOrUpdate(scope.row.shopId)"
>
查看
</el-button>
<el-button
v-if="isAuth('shop:shopDetail:shoutDown')"
type="danger"
@click.stop="onShouDown(scope.row.shopId)"
>
下架
</el-button>
<!-- <el-button
v-if="isAuth('shop:pickAddr:update')"
type="primary"
icon="el-icon-edit"
@click.stop="onAddOrUpdate(scope.row.addrId)"
>
编辑
</el-button>
<el-button
v-if="isAuth('shop:pickAddr:delete')"
type="danger"
icon="el-icon-delete"
@click.stop="onDelete(scope.row.addrId)"
>
删除
</el-button> -->
</template>
</avue-crud>
<!-- 弹窗, 新增 / 修改 -->
</div>
</template>
<script setup>
import { isAuth } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus'
import { tableOption } from '@/crud/shop/shopList.js'
const permission = {
delBtn: isAuth('prod:prod:delete')
}
const dataList = ref([])
const dataListSelections = ref([])
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
const router = useRouter()
/**
* 新增 / 修改
* @param id
*/
const onAddOrUpdate = (id) => {
router.push({
path: '/shopInfo',
query: { shopId: id }
})
}
/**
* 获取数据列表
*/
const getDataList = (pageParam, params, done) => {
http({
url: http.adornUrl('/shop/shopDetail/page'),
method: 'get',
params: http.adornParams(
Object.assign(
{
current: pageParam == null ? page.currentPage : pageParam.currentPage,
size: pageParam == null ? page.pageSize : pageParam.pageSize
},
params
)
)
})
.then(({ data }) => {
dataList.value = data.records
page.total = data.total
if (done) done()
})
}
const addOrUpdateVisible = ref(false)
const addOrUpdateRef = ref(null)
/* const onAddOrUpdate = (id) => {
addOrUpdateVisible.value = true
nextTick(() => {
addOrUpdateRef.value?.init(id)
})
} */
/**
* 删除
* @param id
*/
const onShouDown = (id) => {
const ids = id ? [id] : dataListSelections.value?.map(item => {
return item.addrId
})
ElMessageBox.confirm(
'确定进行下架操作?', '提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(() => {
http({
url: http.adornUrl('/shop/shopDetail/shoutDown'),
method: 'delete',
data: http.adornData(ids, false)
})
.then(() => {
ElMessage({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
getDataList(page)
}
})
})
})
.catch(() => {
})
}
/**
* 条件查询
* @param params
* @param done
*/
const onSearch = (params, done) => {
getDataList(page, params, done)
}
/**
* 多选变化
* @param val
*/
const selectionChange = (val) => {
dataListSelections.value = val
}
</script>
Loading…
Cancel
Save