feat(商品参数): 新增参数管理功能并优化相关页面

- 新增参数编辑页面,支持参数名称、必填、索引等属性配置
- 优化参数列表页面,增加搜索和分页功能
- 重构商品发布页面的参数展示逻辑
- 移除商品模板相关功能
- 优化品牌管理页面的分类关联功能
- 调整商品详情页参数展示样式
This commit is contained in:
pikachu1995@126.com
2025-12-22 14:38:52 +08:00
parent 8a0f5ff120
commit b37e12b7a7
9 changed files with 1065 additions and 778 deletions

View File

@@ -87,14 +87,12 @@
</div> </div>
</TabPane> </TabPane>
<TabPane label="商品参数"> <TabPane label="商品参数">
<template v-if="detail.goodsParamsDTOList && detail.goodsParamsDTOList.length"> <template v-if="goodsParamsList.length">
<div class="goods-params" style="height:inherit;" v-for="item in detail.goodsParamsDTOList" :key="item.groupId"> <div class="item-param-container">
<span class="ml_10">{{item.groupName}}</span> <div class="item-param-box" v-for="param in goodsParamsList" :key="param.paramId || param.paramName">
<table class="mb_10" cellpadding='0' cellspacing="0" > <span class="item-param-title">{{ param.paramName }}</span>
<tr v-for="param in item.goodsParamsItemDTOList" :key="param.paramId"> <span class="item-param-content">{{ param.paramValue || '-' }}</span>
<td style="text-align: center">{{param.paramName}}</td><td>{{param.paramValue}}</td> </div>
</tr>
</table>
</div> </div>
</template> </template>
<div v-else>暂无商品参数</div> <div v-else>暂无商品参数</div>
@@ -134,6 +132,29 @@ export default {
// 商品详情 // 商品详情
skuDetail () { skuDetail () {
return this.detail.data; return this.detail.data;
},
goodsParamsList () {
const list = this.detail && Array.isArray(this.detail.goodsParamsDTOList) ? this.detail.goodsParamsDTOList : [];
const flat = [];
list.forEach((item) => {
if (!item) return;
if (Array.isArray(item.goodsParamsItemDTOList)) {
flat.push(...item.goodsParamsItemDTOList);
} else {
flat.push(item);
}
});
const cleaned = flat.filter((p) => p && (p.paramName || p.paramId));
const hasSort = cleaned.some((p) => p && p.sort !== undefined && p.sort !== null);
if (!hasSort) return cleaned;
return cleaned.slice().sort((a, b) => {
const sa = Number(a && a.sort) || 0;
const sb = Number(b && b.sort) || 0;
return sa - sb;
});
} }
}, },
methods: { methods: {
@@ -336,14 +357,12 @@ export default {
/************* 商品参数 *************/ /************* 商品参数 *************/
.item-param-container { .item-param-container {
display: flex; display: flex;
flex-wrap: wrap; flex-direction: column;
flex-direction: row;
justify-content: space-between;
} }
.item-param-box { .item-param-box {
padding: 5px; padding: 5px;
padding-left: 30px; padding-left: 30px;
width: 240px; width: 100%;
height: 36px; height: 36px;
font-size: 14px; font-size: 14px;
} }

View File

@@ -32,6 +32,26 @@ export const getCategoryBrandListData = (category_id, params) => {
export const saveCategoryBrand = (category_id, params) => { export const saveCategoryBrand = (category_id, params) => {
return postRequest(`/goods/categoryBrand/${category_id}`, params) return postRequest(`/goods/categoryBrand/${category_id}`, params)
} }
// 根据品牌id获取关联分类
export const getBrandCategoryListData = (brand_id, params) => {
return getRequest(`/goods/categoryBrand/${brand_id}`, params)
}
// 保存品牌分类关联
export const saveBrandCategory = (brand_id, categoryIds) => {
return postRequest(`/goods/categoryBrand/${brand_id}`, categoryIds, {
"Content-Type": "application/json"
})
}
export const getParameterCategoryListData = (parameter_id, params) => {
return getRequest(`/goods/parameters/category/${parameter_id}`, params)
}
export const saveParameterCategory = (parameter_id, categoryIds) => {
return postRequest(`/goods/parameters/category/${parameter_id}`, categoryIds, {
"Content-Type": "application/json"
})
}
//保存获取关联规格 //保存获取关联规格
export const saveCategorySpec = (category_id, params) => { export const saveCategorySpec = (category_id, params) => {
return postRequest(`/goods/categorySpec/${category_id}`, params) return postRequest(`/goods/categorySpec/${category_id}`, params)
@@ -140,17 +160,28 @@ export const getCategoryParamsListData = (id, params) => {
return getRequest(`/goods/categoryParameters/${id}`, params) return getRequest(`/goods/categoryParameters/${id}`, params)
} }
// 参数组分页列表
export const getCategoryParametersGroupPage = (params) => {
return getRequest(`/goods/categoryParameters`, params)
}
//查询商品绑定参数信息 //查询商品绑定参数信息
export const getCategoryParamsByGoodsId = (goodsId, categoryId) => { export const getCategoryParamsByGoodsId = (goodsId, categoryId) => {
return getRequest(`/goods/parameters/${goodsId}/${categoryId}`) return getRequest(`/goods/parameters/${goodsId}/${categoryId}`)
} }
export const getGoodsParamsPage = (params) => {
return getRequest(`/goods/parameters`, params)
}
export const getGoodsParamsDetail = (id, params) => {
return getRequest(`/goods/parameters/${id}`, params)
}
//保存参数 //保存参数
export const insertGoodsParams = (params) => { export const insertGoodsParams = (params, headers) => {
return postRequest('/goods/parameters', params) return postRequest('/goods/parameters', params, headers)
} }
//更新参数 //更新参数
export const updateGoodsParams = (params) => { export const updateGoodsParams = (params, headers) => {
return putRequest('/goods/parameters', params) return putRequest('/goods/parameters', params, headers)
} }
//删除参数 //删除参数
export const deleteParams = (id, params) => { export const deleteParams = (id, params) => {

View File

@@ -97,6 +97,12 @@ export const otherRouter = {
name: "goods-parameter", name: "goods-parameter",
component: () => import("@/views/goods/goods-manage/parameter.vue") component: () => import("@/views/goods/goods-manage/parameter.vue")
}, },
{
path: "goods-parameter-edit",
title: "商品参数维护",
name: "goods-parameter-edit",
component: () => import("@/views/goods/goods-manage/parameter-edit.vue")
},
{ {
path: "goods-spec", path: "goods-spec",
title: "商品参数", title: "商品参数",

View File

@@ -26,7 +26,14 @@
<Input v-model="form.name" clearable style="width: 100%"/> <Input v-model="form.name" clearable style="width: 100%"/>
</FormItem> </FormItem>
<FormItem label="品牌图标" prop="logo"> <FormItem label="品牌图标" prop="logo">
<upload-pic-input v-model="form.logo" style="width: 100%"></upload-pic-input> <div style="display: flex; align-items: center; gap: 12px;">
<img
:src="form.logo || defaultPic"
alt="品牌图标"
style="width: 80px; height: 60px; object-fit: contain; border: 1px solid #dcdee2; border-radius: 4px; background: #fff;"
/>
<Button type="text" @click="openLogoPicker">修改</Button>
</div>
</FormItem> </FormItem>
</Form> </Form>
<div slot="footer"> <div slot="footer">
@@ -34,6 +41,34 @@
<Button type="primary" :loading="submitLoading" @click="handleSubmit">提交</Button> <Button type="primary" :loading="submitLoading" @click="handleSubmit">提交</Button>
</div> </div>
</Modal> </Modal>
<Modal width="1200px" v-model="picModelFlag" footer-hide>
<ossManage @callback="callbackSelected" :isComponent="true" :initialize="picModelFlag" ref="ossManage" />
</Modal>
<Modal
:title="categoryModalTitle"
v-model="categoryModalVisible"
:mask-closable="false"
:width="700"
>
<div style="position: relative; max-height: 520px; overflow: auto;">
<Spin size="large" fix v-if="categoryTreeLoading"></Spin>
<Tree
ref="categoryTree"
:key="categoryTreeKey"
:data="categoryTreeData"
show-checkbox
@on-check-change="onCategoryTreeCheckChange"
></Tree>
</div>
<div slot="footer">
<Button type="text" @click="categoryModalVisible = false">取消</Button>
<Button type="primary" :loading="categorySubmitLoading" @click="submitBrandCategory"
>提交</Button
>
</div>
</Modal>
</div> </div>
</template> </template>
@@ -44,22 +79,35 @@ import {
updateBrand, updateBrand,
disableBrand, disableBrand,
delBrand, delBrand,
getCategoryTree,
getBrandCategoryListData,
saveBrandCategory,
} from "@/api/goods"; } from "@/api/goods";
import uploadPicInput from "@/components/lili/upload-pic-input"; import ossManage from "@/views/sys/oss-manage/ossManage";
import {regular} from "@/utils"; import {regular} from "@/utils";
export default { export default {
name: "brand", name: "brand",
components: { components: {
uploadPicInput ossManage
}, },
data() { data() {
return { return {
defaultPic: require("@/assets/default.png"),
loading: true, // 表单加载状态 loading: true, // 表单加载状态
modalType: 0, // 添加或编辑标识 modalType: 0, // 添加或编辑标识
modalVisible: false, // 添加或编辑显示 modalVisible: false, // 添加或编辑显示
modalTitle: "", // 添加或编辑标题 modalTitle: "", // 添加或编辑标题
picModelFlag: false, // 图片选择器
categoryModalVisible: false,
categoryModalTitle: "关联分类",
categoryTreeLoading: false,
categoryTreeData: [],
categoryTreeKey: 0,
categorySubmitLoading: false,
currentBrandId: "",
selectedCategoryIds: [],
searchForm: { searchForm: {
// 搜索框初始化对象 // 搜索框初始化对象
pageNumber: 1, // 当前页数 pageNumber: 1, // 当前页数
@@ -118,11 +166,24 @@ export default {
key: "deleteFlag", key: "deleteFlag",
align: "left", align: "left",
render: (h, params) => { render: (h, params) => {
if (params.row.deleteFlag == 0) { return h(
return h("Tag", {props: {color: "green",},}, "启用"); "i-switch",
} else if (params.row.deleteFlag == 1) { {
return h("Tag", {props: {color: "volcano",},}, "禁用"); props: {
} value: params.row.deleteFlag == 0,
size: "large",
},
on: {
"on-change": (checked) => {
this.handleStatusSwitchChange(params.row, checked);
},
},
},
[
h("span", { slot: "open" }, "启用"),
h("span", { slot: "close" }, "禁用"),
]
);
}, },
filters: [ filters: [
{ {
@@ -146,48 +207,10 @@ export default {
{ {
title: "操作", title: "操作",
key: "action", key: "action",
width: 180, width: 210,
align: "center", align: "center",
fixed: "right", fixed: "right",
render: (h, params) => { render: (h, params) => {
let enableOrDisable = "";
if (params.row.deleteFlag == 0) {
enableOrDisable = h(
"a",
{
style: {
color: "#2d8cf0",
cursor: "pointer",
textDecoration: "none",
marginRight: "5px",
},
on: {
click: () => {
this.disable(params.row);
},
},
},
"禁用"
);
} else {
enableOrDisable = h(
"a",
{
style: {
color: "#2d8cf0",
cursor: "pointer",
textDecoration: "none",
marginRight: "5px",
},
on: {
click: () => {
this.enable(params.row);
},
},
},
"启用"
);
}
return h("div", [ return h("div", [
h( h(
"a", "a",
@@ -196,7 +219,6 @@ export default {
color: "#2d8cf0", color: "#2d8cf0",
cursor: "pointer", cursor: "pointer",
textDecoration: "none", textDecoration: "none",
marginRight: "5px",
}, },
on: { on: {
click: () => { click: () => {
@@ -211,7 +233,22 @@ export default {
{ style: { margin: "0 8px", color: "#dcdee2" } }, { style: { margin: "0 8px", color: "#dcdee2" } },
"|" "|"
), ),
enableOrDisable, h(
"a",
{
style: {
color: "#2d8cf0",
cursor: "pointer",
textDecoration: "none",
},
on: {
click: () => {
this.openCategoryModal(params.row);
},
},
},
"关联分类"
),
h( h(
"span", "span",
{ style: { margin: "0 8px", color: "#dcdee2" } }, { style: { margin: "0 8px", color: "#dcdee2" } },
@@ -242,6 +279,100 @@ export default {
}; };
}, },
methods: { methods: {
openLogoPicker() {
this.$refs.ossManage.selectImage = true;
this.picModelFlag = true;
},
callbackSelected(val) {
this.picModelFlag = false;
this.form.logo = val.url;
},
buildCategoryTreeNodes(list, selectedSet) {
if (!Array.isArray(list) || list.length === 0) return [];
return list.map((item) => {
const children = this.buildCategoryTreeNodes(item.children || [], selectedSet);
return {
id: item.id,
title: item.name,
expand: true,
checked: selectedSet.has(item.id),
children,
};
});
},
async openCategoryModal(row) {
this.currentBrandId = row.id;
this.categoryModalTitle = "关联分类 - " + (row.name || "");
this.categoryModalVisible = true;
this.categoryTreeLoading = true;
this.categoryTreeKey += 1;
this.categoryTreeData = [];
this.selectedCategoryIds = [];
try {
const [treeRes, bindRes] = await Promise.all([
getCategoryTree(),
getBrandCategoryListData(row.id),
]);
const selectedIds = Array.isArray(bindRes?.result)
? bindRes.result
.map((x) => (typeof x === "string" ? x : x && x.id))
.filter(Boolean)
: [];
this.selectedCategoryIds = selectedIds;
const selectedSet = new Set(selectedIds);
this.categoryTreeData = treeRes && treeRes.success
? this.buildCategoryTreeNodes(treeRes.result || [], selectedSet)
: [];
} finally {
this.categoryTreeLoading = false;
}
},
onCategoryTreeCheckChange(checkedNodes) {
if (!Array.isArray(checkedNodes)) {
this.selectedCategoryIds = [];
return;
}
this.selectedCategoryIds = checkedNodes
.map((node) => node && node.id)
.filter(Boolean);
},
submitBrandCategory() {
if (!this.currentBrandId) return;
this.categorySubmitLoading = true;
saveBrandCategory(
this.currentBrandId,
(this.selectedCategoryIds || []).map((id) => String(id))
).then((res) => {
this.categorySubmitLoading = false;
if (res && res.success) {
this.$Message.success("操作成功");
this.categoryModalVisible = false;
return;
}
}).catch(() => {
this.categorySubmitLoading = false;
});
},
handleStatusSwitchChange(row, checked) {
const disable = !checked;
this.$Modal.confirm({
title: disable ? "确认禁用" : "确认启用",
content: "您确认要" + (disable ? "禁用" : "启用") + "品牌 " + row.name + " ?",
loading: true,
onOk: () => {
disableBrand(row.id, { disable }).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("操作成功");
}
this.getDataList();
});
},
onCancel: () => {
this.$nextTick(() => this.$forceUpdate());
},
});
},
// 删除品牌 // 删除品牌
async delBrand(id) { async delBrand(id) {
let res = await delBrand(id); let res = await delBrand(id);
@@ -337,40 +468,6 @@ export default {
this.form = data; this.form = data;
this.modalVisible = true; this.modalVisible = true;
}, },
// 启用品牌
enable(v) {
this.$Modal.confirm({
title: "确认启用",
content: "您确认要启用品牌 " + v.name + " ?",
loading: true,
onOk: () => {
disableBrand(v.id, {disable: false}).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
}
});
},
});
},
// 禁用
disable(v) {
this.$Modal.confirm({
title: "确认禁用",
content: "您确认要禁用品牌 " + v.name + " ?",
loading: true,
onOk: () => {
disableBrand(v.id, {disable: true}).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("操作成功");
this.getDataList();
}
});
},
});
},
}, },
mounted() { mounted() {
this.init(); this.init();

View File

@@ -14,32 +14,14 @@
:columns="columns" :columns="columns"
> >
<template slot="action" slot-scope="scope"> <template slot="action" slot-scope="scope">
<Dropdown v-show="scope.row.level == 2" trigger="click"> <div class="ops">
<a class="ops-link"> <template v-if="scope.row.level == 2">
绑定 <a class="ops-link" @click="parameterOperation(scope.row)">编辑绑定参数</a>
<Icon type="ios-arrow-down"></Icon> </template>
</a> <a class="ops-link" @click="edit(scope.row)">编辑</a>
<DropdownMenu slot="list"> <a class="ops-link" @click="remove(scope.row)">删除</a>
<DropdownItem @click.native="brandOperation(scope.row)">编辑绑定品牌</DropdownItem> <a v-show="scope.row.level != 2" class="ops-link" @click="addChildren(scope.row)">添加子分类</a>
<DropdownItem @click.native="specOperation(scope.row)">编辑绑定规格</DropdownItem> </div>
<DropdownItem @click.native="parameterOperation(scope.row)">编辑绑定参数</DropdownItem>
</DropdownMenu>
</Dropdown>
<span class="ops-sep">|</span>
<Dropdown trigger="click">
<a class="ops-link">
操作
<Icon type="ios-arrow-down"></Icon>
</a>
<DropdownMenu slot="list">
<DropdownItem @click.native="edit(scope.row)">编辑</DropdownItem>
<DropdownItem v-if="scope.row.deleteFlag == 1" @click.native="enable(scope.row)">启用</DropdownItem>
<DropdownItem v-if="scope.row.deleteFlag == 0" @click.native="disable(scope.row)">禁用</DropdownItem>
<DropdownItem @click.native="remove(scope.row)">删除</DropdownItem>
</DropdownMenu>
</Dropdown>
<span v-if="scope.row.level != 2" class="ops-sep">|</span>
<a v-show="scope.row.level != 2" class="ops-link" @click="addChildren(scope.row)">添加子分类</a>
</template> </template>
<template slot="commissionRate" slot-scope="scope"> <template slot="commissionRate" slot-scope="scope">
@@ -49,12 +31,17 @@
</template> </template>
<template slot="deleteFlag" slot-scope="{ row }"> <template slot="deleteFlag" slot-scope="{ row }">
<Tag <i-switch
:class="{ ml_10: row.deleteFlag }" size="large"
:color="row.deleteFlag == false ? 'success' : 'error'" v-model="row.deleteFlag"
> :true-value="false"
{{ row.deleteFlag == false ? "正常启用" : "禁用" }}</Tag :false-value="true"
:loading="row._statusLoading"
@on-change="onStatusSwitchChange(row, $event)"
> >
<span slot="open">开启</span>
<span slot="close">关闭</span>
</i-switch>
</template> </template>
</Table> </Table>
@@ -249,6 +236,52 @@ export default {
}; };
}, },
methods: { methods: {
normalizeCategoryTree(list) {
if (!Array.isArray(list) || list.length === 0) return;
list.forEach((item) => {
if (!item || typeof item !== "object") return;
if (item.deleteFlag === 0) item.deleteFlag = false;
else if (item.deleteFlag === 1) item.deleteFlag = true;
else item.deleteFlag = !!item.deleteFlag;
if (Array.isArray(item.children) && item.children.length) {
this.normalizeCategoryTree(item.children);
}
});
},
onStatusSwitchChange(row, nextDeleteFlag) {
const previousDeleteFlag = !nextDeleteFlag;
const isClosing = nextDeleteFlag === true;
this.$Modal.confirm({
title: isClosing ? "确认关闭" : "确认开启",
content:
"您是否要" +
(isClosing ? "关闭" : "开启") +
"当前分类 " +
row.name +
" 及其子分类?",
loading: true,
okText: "是",
cancelText: "否",
onOk: () => {
this.$set(row, "_statusLoading", true);
disableCategory(row.id, { enableOperations: isClosing ? true : 0 }).then(
(res) => {
this.$Modal.remove();
this.$set(row, "_statusLoading", false);
if (res && res.success) {
this.$Message.success("操作成功");
this.getAllList(0);
return;
}
row.deleteFlag = previousDeleteFlag;
}
);
},
onCancel: () => {
row.deleteFlag = previousDeleteFlag;
},
});
},
// 初始化数据 // 初始化数据
init() { init() {
this.getAllList(0); this.getAllList(0);
@@ -411,6 +444,7 @@ export default {
child._loading = false; child._loading = false;
child.children = []; child.children = [];
}); });
this.normalizeCategoryTree(val.children);
// 模拟加载 // 模拟加载
setTimeout(() => { setTimeout(() => {
callback(val.children); callback(val.children);
@@ -419,6 +453,7 @@ export default {
}); });
} else { } else {
this.deepCategoryChildren(item.id, this.categoryList); this.deepCategoryChildren(item.id, this.categoryList);
this.normalizeCategoryTree(this.checkedCategoryChildren);
setTimeout(() => { setTimeout(() => {
callback(this.checkedCategoryChildren); callback(this.checkedCategoryChildren);
}, 100); }, 100);
@@ -446,6 +481,7 @@ export default {
this.loading = false; this.loading = false;
if (res.success) { if (res.success) {
localStorage.setItem("category", JSON.stringify(res.result)); localStorage.setItem("category", JSON.stringify(res.result));
this.normalizeCategoryTree(res.result);
this.categoryList = JSON.parse(JSON.stringify(res.result)); this.categoryList = JSON.parse(JSON.stringify(res.result));
this.tableData = res.result.map((item) => { this.tableData = res.result.map((item) => {
if(this.recordLevel[0] && item.id === this.recordLevel[0]) { if(this.recordLevel[0] && item.id === this.recordLevel[0]) {
@@ -532,9 +568,18 @@ export default {
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
} }
.ops-sep { .ops {
display: inline-block; display: flex;
margin: 0 8px; flex-wrap: wrap;
}
.ops-link + .ops-link {
margin-left: 16px;
position: relative;
}
.ops-link + .ops-link::before {
content: "|";
position: absolute;
left: -10px;
color: #dcdee2; color: #dcdee2;
} }
</style> </style>

View File

@@ -0,0 +1,442 @@
<template>
<div class="search">
<Card>
<Form ref="form" :model="form" :label-width="100" :rules="formValidate">
<FormItem label="参数名称" prop="paramName">
<Input v-model="form.paramName" clearable style="width: 520px" />
</FormItem>
<FormItem label="是否必填" prop="required">
<RadioGroup v-model="form.required">
<Radio :label="0"></Radio>
<Radio :label="1"></Radio>
</RadioGroup>
<span style="margin-left: 10px; color: #999; font-size: 12px"
>商品发布时参数是否必填</span
>
</FormItem>
<FormItem label="是否索引" prop="isIndex">
<RadioGroup v-model="form.isIndex">
<Radio :label="0"></Radio>
<Radio :label="1"></Radio>
</RadioGroup>
<span style="margin-left: 10px; color: #999; font-size: 12px"
>开启索引后用户将可以通过该参数筛选商品索引开关不影响商详页的参数展示</span
>
</FormItem>
<FormItem label="排序" prop="sort">
<InputNumber :min="0" type="number" v-model="form.sort" style="width: 520px" />
</FormItem>
<FormItem label="参数值" prop="options">
<Table :columns="optionColumns" :data="form.options" border size="small" style="width: 520px" />
<div style="margin-top: 10px">
<Button type="primary" @click="addOptionRow">新增</Button>
</div>
</FormItem>
<FormItem label="关联分类">
<Button type="default" @click="openCategoryModal">选择分类</Button>
<span v-if="selectedCategoryNamesText" style="margin-left: 10px; color: #999; font-size: 12px"
>{{ selectedCategoryNamesText }}</span
>
<span v-else style="margin-left: 10px; color: #999; font-size: 12px"
>已选择{{ selectedCategoryIds.length }}个分类</span
>
</FormItem>
<FormItem>
<Button type="default" @click="back">返回</Button>
<Button type="primary" :loading="submitLoading" @click="handleSubmit">保存</Button>
</FormItem>
</Form>
</Card>
<Modal
:title="categoryModalTitle"
v-model="categoryModalVisible"
:mask-closable="false"
:width="700"
>
<div style="position: relative; max-height: 520px; overflow: auto;">
<Spin size="large" fix v-if="categoryTreeLoading"></Spin>
<Tree
:key="categoryTreeKey"
:data="categoryTreeData"
show-checkbox
@on-check-change="onCategoryTreeCheckChange"
></Tree>
</div>
<div slot="footer">
<Button type="text" @click="categoryModalVisible = false">取消</Button>
<Button type="primary" style="margin-left: 8px" @click="categoryModalVisible = false">确定</Button>
</div>
</Modal>
</div>
</template>
<script>
import {
getCategoryTree,
getGoodsParamsDetail,
insertGoodsParams,
updateGoodsParams,
} from "@/api/goods";
import { regular } from "@/utils";
const getOptionText = (value) => {
if (value && typeof value === "object") return value.value;
return value;
};
const normalizeOptions = (value) => {
const arr = Array.isArray(value)
? value
: typeof value === "string"
? value.split(",")
: [];
const cleaned = arr
.map((x) => String(getOptionText(x) ?? "").trim())
.filter((x) => x.length > 0);
return Array.from(new Set(cleaned));
};
const validateOptions = (rule, value, callback) => {
const arr = Array.isArray(value)
? value
: typeof value === "string"
? value.split(",")
: [];
const options = normalizeOptions(arr);
if (options.length === 0) {
callback(new Error("请填写参数值"));
return;
}
const joined = options.join(",");
if (!/^.{1,255}$/.test(joined)) {
callback(new Error("超出最大长度限制"));
return;
}
callback();
};
const normalize01 = (value, fallback = 0) => {
const n = Number(value);
if (n === 0 || n === 1) return n;
return fallback;
};
const buildSpringFormPayload = (payload) => {
const out = {};
if (payload && payload.id !== undefined && payload.id !== null && String(payload.id)) {
out.id = String(payload.id);
}
out.paramName = payload && payload.paramName !== undefined && payload.paramName !== null ? String(payload.paramName) : "";
out.options = payload && payload.options !== undefined && payload.options !== null ? String(payload.options) : "";
out.required = payload ? Number(payload.required) : 0;
out.isIndex = payload ? Number(payload.isIndex) : 0;
out.sort = payload ? Number(payload.sort) : 0;
const categoryList = payload && Array.isArray(payload.categoryParameterList) ? payload.categoryParameterList : [];
const categoryIds = categoryList
.map((x) => (x && x.categoryId !== undefined && x.categoryId !== null ? String(x.categoryId) : ""))
.filter((x) => x.length > 0);
categoryIds.forEach((categoryId, index) => {
out[`categoryParameterList[${index}].categoryId`] = categoryId;
});
return out;
};
const validateRadioRequired = (message) => (rule, value, callback) => {
const n = normalize01(value, NaN);
if (!(n === 0 || n === 1)) {
callback(new Error(message));
return;
}
callback();
};
const toStringArray = (arr) => {
if (!Array.isArray(arr)) return [];
return arr.map((x) => String(x)).filter((x) => x.length > 0);
};
const cacheKey = (id) => `goods-parameter-edit:${id}`;
const buildCategoryIdNameMap = (list, map) => {
if (!Array.isArray(list) || list.length === 0) return;
list.forEach((item) => {
if (!item) return;
const id = item.id !== undefined && item.id !== null ? String(item.id) : "";
if (id) map[id] = item.name;
buildCategoryIdNameMap(item.children || [], map);
});
};
export default {
name: "parameterEdit",
data() {
return {
submitLoading: false,
modalType: 0,
categoryModalVisible: false,
categoryModalTitle: "关联分类",
categoryTreeLoading: false,
categoryTreeData: [],
categoryTreeSource: [],
categoryIdNameMap: {},
categoryTreeKey: 0,
selectedCategoryIds: [],
form: {
paramName: "",
options: [{ value: "" }],
required: 0,
isIndex: 0,
sort: 0,
},
optionColumns: [
{
title: "参数值",
key: "value",
minWidth: 420,
render: (h, params) => {
return h("Input", {
props: {
value: params.row.value,
clearable: true,
},
on: {
input: (val) => {
if (!Array.isArray(this.form.options)) this.form.options = [];
if (!this.form.options[params.index]) return;
this.form.options[params.index].value = val;
},
"on-blur": () => {
this.touchOptionsValidate();
},
},
});
},
},
{
title: "操作",
width: 90,
align: "center",
render: (h, params) => {
return h(
"Button",
{
props: {
type: "error",
size: "small",
},
on: {
click: () => this.removeOptionRow(params.index),
},
},
"删除",
);
},
},
],
formValidate: {
paramName: [regular.REQUIRED, regular.VARCHAR5],
options: [{ required: true, validator: validateOptions, trigger: "change" }],
required: [{ required: true, validator: validateRadioRequired("请选择是否必填"), trigger: "change" }],
isIndex: [{ required: true, validator: validateRadioRequired("请选择是否索引"), trigger: "change" }],
sort: [regular.REQUIRED, regular.INTEGER],
},
};
},
computed: {
id() {
return this.$route.query && this.$route.query.id ? String(this.$route.query.id) : "";
},
selectedCategoryNamesText() {
if (!Array.isArray(this.categoryTreeSource) || this.categoryTreeSource.length === 0) return "";
const selectedSet = new Set(toStringArray(this.selectedCategoryIds));
const isSelected = (node) => {
if (!node) return false;
const id = node.id !== undefined && node.id !== null ? String(node.id) : "";
if (id && selectedSet.has(id)) return true;
const children = Array.isArray(node.children) ? node.children : [];
if (children.length === 0) return false;
return children.every(isSelected);
};
const collect = (node, out) => {
if (!node) return;
if (isSelected(node)) {
if (node.name) out.push(node.name);
return;
}
const children = Array.isArray(node.children) ? node.children : [];
children.forEach((c) => collect(c, out));
};
const names = [];
this.categoryTreeSource.forEach((n) => collect(n, names));
return names.join("");
},
},
watch: {
"form.required"(val) {
const n = normalize01(val, 0);
if (val !== n) this.$set(this.form, "required", n);
},
"form.isIndex"(val) {
const n = normalize01(val, 0);
if (val !== n) this.$set(this.form, "isIndex", n);
},
},
methods: {
back() {
this.$router.push({ name: "goods-parameter" });
},
touchOptionsValidate() {
if (this.$refs.form && this.$refs.form.validateField) {
this.$refs.form.validateField("options");
}
},
addOptionRow() {
if (!Array.isArray(this.form.options)) this.form.options = [];
this.form.options.push({ value: "" });
this.touchOptionsValidate();
},
removeOptionRow(index) {
if (!Array.isArray(this.form.options)) this.form.options = [];
this.form.options.splice(index, 1);
this.touchOptionsValidate();
},
buildCategoryTreeNodes(list, selectedSet) {
if (!Array.isArray(list) || list.length === 0) return [];
return list.map((item) => {
const children = this.buildCategoryTreeNodes(item.children || [], selectedSet);
return {
id: item.id,
title: item.name,
expand: true,
checked: selectedSet.has(String(item.id)),
children,
};
});
},
async loadDetail(parameterId) {
if (!parameterId) return;
const res = await getGoodsParamsDetail(parameterId).catch(() => null);
if (!(res && res.success && res.result)) return;
const dto = res.result;
const opts = normalizeOptions(dto.options);
this.form = {
id: dto.id || parameterId,
paramName: dto.paramName || "",
options: opts.map((x) => ({ value: x })),
required: normalize01(dto.required, 0),
isIndex: normalize01(dto.isIndex, 0),
sort: Number(dto.sort ?? 0) || 0,
};
const list = Array.isArray(dto.categoryParameterList) ? dto.categoryParameterList : [];
this.selectedCategoryIds = toStringArray(list.map((x) => x && x.categoryId).filter(Boolean));
},
async openCategoryModal() {
this.categoryModalVisible = true;
this.categoryTreeLoading = true;
this.categoryTreeKey += 1;
try {
const selectedSet = new Set(toStringArray(this.selectedCategoryIds));
if (!Array.isArray(this.categoryTreeSource) || this.categoryTreeSource.length === 0) {
const treeRes = await getCategoryTree();
this.categoryTreeSource = treeRes && treeRes.success ? treeRes.result || [] : [];
const map = {};
buildCategoryIdNameMap(this.categoryTreeSource, map);
this.categoryIdNameMap = map;
}
this.categoryTreeData = this.buildCategoryTreeNodes(this.categoryTreeSource || [], selectedSet);
} finally {
this.categoryTreeLoading = false;
}
},
onCategoryTreeCheckChange(checkedNodes) {
if (!Array.isArray(checkedNodes)) {
this.selectedCategoryIds = [];
return;
}
this.selectedCategoryIds = toStringArray(checkedNodes.map((node) => node && node.id).filter(Boolean));
},
initForm() {
if (this.id) {
this.modalType = 1;
const cached = window.sessionStorage.getItem(cacheKey(this.id));
if (cached) {
try {
const row = JSON.parse(cached);
const opts = normalizeOptions(row.options);
this.form = {
id: row.id,
paramName: row.paramName || "",
options: opts.map((x) => ({ value: x })),
required: normalize01(row.required, 0),
isIndex: normalize01(row.isIndex, 0),
sort: Number(row.sort ?? 0) || 0,
};
} catch (e) {
this.modalType = 0;
}
}
this.loadDetail(String(this.id));
} else {
this.modalType = 0;
if (!Array.isArray(this.form.options) || this.form.options.length === 0) {
this.form.options = [{ value: "" }];
}
this.selectedCategoryIds = [];
}
},
handleSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return;
this.submitLoading = true;
const options = normalizeOptions(this.form.options);
const categoryIds = toStringArray(this.selectedCategoryIds);
const payload = {
...this.form,
options: options.join(","),
required: Number(this.form.required),
isIndex: Number(this.form.isIndex),
sort: Number(this.form.sort || 0),
categoryParameterList: categoryIds.map((categoryId) => ({ categoryId })),
};
if (this.modalType === 0) {
delete payload.id;
insertGoodsParams(buildSpringFormPayload(payload))
.then((res) => {
if (!(res && res.success)) return;
this.$Message.success("操作成功");
this.back();
})
.finally(() => {
this.submitLoading = false;
});
} else {
updateGoodsParams(buildSpringFormPayload(payload))
.then((res) => {
if (!(res && res.success)) return;
this.$Message.success("操作成功");
this.back();
})
.finally(() => {
this.submitLoading = false;
});
}
});
},
},
mounted() {
getCategoryTree().then((res) => {
if (res && res.success) {
this.categoryTreeSource = res.result || [];
const map = {};
buildCategoryIdNameMap(this.categoryTreeSource, map);
this.categoryIdNameMap = map;
}
});
this.initForm();
},
};
</script>
<style lang="scss"></style>

View File

@@ -1,385 +1,202 @@
<template> <template>
<div style="width: 100%"> <div class="search">
<Card> <Card>
<Button @click="handleAddParamsGroup" type="primary">添加</Button> <Form
ref="searchForm"
@submit.native.prevent
@keydown.enter.native="handleSearch"
:model="searchForm"
inline
:label-width="70"
class="search-form"
>
<Form-item label="参数名称">
<Input
type="text"
v-model="searchForm.paramName"
placeholder="请输入参数名称"
clearable
style="width: 240px"
/>
</Form-item>
<Button @click="handleSearch" type="primary">搜索</Button>
</Form>
</Card> </Card>
<div class="row"> <Card>
<Card v-if="paramsGroup.length == 0"> 暂无参数绑定信息 </Card> <Row class="operation padding-row">
<div class="paramsGroup" v-else> <Button @click="goAdd" type="primary">添加</Button>
<Card </Row>
style="width: 350px; margin: 7px" <Table :loading="loading" border :columns="columns" :data="data" ref="table"></Table>
v-for="(group, index) in paramsGroup" <Row type="flex" justify="end" class="mt_10">
:key="index" <Page
:bordered="false" :current="searchForm.pageNumber"
> :total="total"
<p slot="title"> :page-size="searchForm.pageSize"
<Icon type="ios-film-outline"></Icon>&nbsp;{{ group.groupName }} @on-change="changePage"
</p> @on-page-size-change="changePageSize"
<p slot="extra"> :page-size-opts="[20, 50, 100]"
<Dropdown slot="extra"> size="small"
<a href="javascript:void(0)"> show-total
操作 show-elevator
<Icon type="ios-arrow-down"></Icon> show-sizer
</a> ></Page>
<Dropdown-menu slot="list"> </Row>
<Dropdown-item @click.native="handleEditParamsGroup(group)" </Card>
>编辑</Dropdown-item
>
<Dropdown-item @click.native="handleDeleteParamGroup(group)"
>删除</Dropdown-item
>
</Dropdown-menu>
</Dropdown>
<Icon type="arrow-down-b"></Icon>
</p>
<template v-if="group.params && group.params.length > 0">
<div
v-for="(param, paramId) in group.params"
:key="paramId"
class="params"
>
<span>{{ param.paramName }}</span>
<span>
<i-button type="text" @click="handleEditParams(group, param)"
>编辑</i-button
>
<i-button
type="text"
size="small"
style="color: #f56c6c"
@click="handleDeleteParam(group, param)"
>删除</i-button
>
</span>
</div>
</template>
<div v-else style="align-content: center">暂无数据...</div>
<div style="text-align: center">
<i-button type="text" @click="handleAddParams(group)"
>添加</i-button
>
</div>
</Card>
</div>
</div>
<div>
<Modal
:title="modalTitle"
v-model="dialogParamsVisible"
:mask-closable="false"
:width="500"
>
<Form
ref="paramForm"
:model="paramForm"
:label-width="100"
:rules="formValidate"
>
<FormItem label="参数名称" prop="paramName">
<Input v-model="paramForm.paramName" style="width: 100%" />
</FormItem>
<FormItem label="可选值" prop="options">
<Select
v-model="paramForm.options"
placeholder="输入后回车添加"
multiple
filterable
allow-create
:popper-append-to-body="false"
popper-class="spec-values-popper"
style="width: 100%; text-align: left; margin-right: 10px"
>
<Option
v-for="(item, itemIndex) in ops.options"
:value="item"
:key="itemIndex"
:label="item"
>
{{ item }}
</Option>
</Select>
</FormItem>
<FormItem label="选项" prop="specName3">
<Checkbox label="1" v-model="paramForm.required">必填</Checkbox>
<Checkbox label="1" v-model="paramForm.isIndex">可索引</Checkbox>
</FormItem>
<FormItem label="排序" prop="sort">
<InputNumber
:min="0"
type="number"
v-model="paramForm.sort"
style="width: 100%"
/>
</FormItem>
</Form>
<div slot="footer">
<Button type="text" @click="dialogParamsVisible = false">取消</Button>
<Button
type="primary"
:loading="submitLoading"
@click="submitParamForm"
>提交</Button
>
</div>
</Modal>
</div>
<div>
<Modal
:title="modalTitle"
v-model="dialogParamsGroupVisible"
:mask-closable="false"
:width="500"
>
<Form
@submit.native.prevent
@keydown.enter.native="submitParamGroupForm"
ref="paramGroupForm"
:model="paramGroupForm"
:label-width="100"
:rules="paramGroupValidate"
>
<FormItem label="参数名称" prop="groupName">
<Input v-model="paramGroupForm.groupName" style="width: 100%" />
</FormItem>
</Form>
<div slot="footer">
<Button type="text" @click="dialogParamsGroupVisible = false"
>取消</Button
>
<Button
type="primary"
:loading="submitLoading"
@click="submitParamGroupForm"
>提交</Button
>
</div>
</Modal>
</div>
</div> </div>
</template> </template>
<script> <script>
import { import {
getCategoryParamsListData,
insertGoodsParams,
updateGoodsParams,
deleteParams, deleteParams,
insertParamsGroup, getGoodsParamsPage,
updateParamsGroup,
deleteParamsGroup,
} from "@/api/goods"; } from "@/api/goods";
import { regular } from "@/utils";
export default { export default {
name: "categoryParams", name: "categoryParams",
data() { data() {
return { return {
submitLoading: false, submitLoading: false,
/** 分类ID */ loading: true,
categoryId: this.$route.query.id, total: 0,
/** 参数组 */ searchForm: {
paramsGroup: [], pageNumber: 1,
/** 添加或编辑标识 */ pageSize: 20,
modalType: 0, sort: "createTime",
/** 添加或编辑标题 */ order: "desc",
modalTitle: "", paramName: "",
/** 参数添加或编辑弹出框 */
dialogParamsVisible: false,
/** 参数组添加或编辑弹出框 */
dialogParamsGroupVisible: false,
//参数表单
paramForm: {
sort: 1,
},
/** 参数值 **/
ops: {
options: [],
},
// 参数表单
paramGroupForm: {},
/** 添加、编辑参数 规格 */
formValidate: {
paramName: [regular.REQUIRED, regular.VARCHAR5],
options: [regular.REQUIRED, regular.VARCHAR255],
sort: [regular.REQUIRED, regular.INTEGER],
},
/** 参数组*/
paramGroupValidate: {
groupName: [regular.REQUIRED, regular.VARCHAR5],
}, },
data: [],
columns: [
{
title: "参数名称",
key: "paramName",
width: 300,
resizable: true,
sortable: false,
},
{
title: "参数值",
key: "options",
minWidth: 260,
tooltip: true,
},
{
title: "必填",
key: "required",
width: 300,
align: "center",
render: (h, params) => {
const val = params.row.required;
const on = val === 1 || val === "1" || val === true;
return h("span", on ? "是" : "否");
},
},
{
title: "可索引",
key: "isIndex",
width: 300,
align: "center",
render: (h, params) => {
const val = params.row.isIndex;
const on = val === 1 || val === "1" || val === true;
return h("span", on ? "是" : "否");
},
},
{
title: "操作",
key: "action",
width: 300,
align: "center",
fixed: "right",
render: (h, params) => {
return h("div", [
h(
"a",
{
style: {
color: "#2d8cf0",
cursor: "pointer",
textDecoration: "none",
},
on: {
click: () => {
this.goEdit(params.row);
},
},
},
"编辑"
),
h("span", { style: { margin: "0 8px", color: "#dcdee2" } }, "|"),
h(
"a",
{
style: {
color: "#2d8cf0",
cursor: "pointer",
textDecoration: "none",
},
on: {
click: () => {
this.remove(params.row);
},
},
},
"删除"
),
]);
},
},
],
}; };
}, },
filters: {
paramTypeFilter(val) {
return val === 1 ? "输入项" : "选择项";
},
},
methods: { methods: {
// 初始化数据
init() { init() {
this.getDataList(); this.getDataList();
}, },
//弹出添加参数框 changePage(v) {
handleAddParams(group) { this.searchForm.pageNumber = v;
this.paramForm = { this.getDataList();
paramName: "",
paramType: 1,
options: "",
required: false,
isIndex: false,
sort: 0,
groupId: group.groupId,
categoryId: this.categoryId,
};
this.modalTitle = "添加参数";
this.modalType = 0;
this.dialogParamsVisible = true;
}, },
//弹出修改参数框 changePageSize(v) {
handleEditParams(group, param) { this.searchForm.pageNumber = 1;
console.log(group, param); this.searchForm.pageSize = v;
this.paramForm = { this.getDataList();
paramName: param.paramName,
options: param.options.split(","),
required: param.required == 1 ? true : false,
isIndex: param.isIndex == 1 ? true : false,
groupId: param.groupId || "",
categoryId: param.categoryId || "",
sort: param.sort || 1,
id: param.id,
};
this.ops.options = this.paramForm.options;
this.modalType = 1;
this.modalTitle = "修改参数";
this.dialogParamsVisible = true;
}, },
//弹出修改参数组框 handleSearch() {
handleEditParamsGroup(group) { this.searchForm.pageNumber = 1;
this.paramGroupForm = { this.searchForm.pageSize = 20;
groupName: group.groupName, this.getDataList();
categoryId: this.categoryId,
id: group.groupId,
};
this.modalType = 1;
this.modalTitle = "修改参数组";
this.dialogParamsGroupVisible = true;
}, },
// 添加参数
handleAddParamsGroup() {
this.paramGroupForm = {};
this.ops = {};
(this.paramGroupForm.categoryId = this.categoryId), (this.modalType = 0);
this.modalTitle = "添加参数组";
this.dialogParamsGroupVisible = true;
},
//保存参数组
submitParamGroupForm() {
this.$refs.paramGroupForm.validate((valid) => {
if (valid) {
if (this.modalType === 0) {
insertParamsGroup(this.paramGroupForm).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("参数组修改成功");
this.getDataList();
this.dialogParamsVisible = false;
}
});
} else {
console.warn(this.paramGroupForm);
updateParamsGroup(this.paramGroupForm).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("参数组修改成功");
this.getDataList();
this.dialogParamsVisible = false;
}
});
}
this.dialogParamsGroupVisible = false;
}
});
},
//保存参数
submitParamForm() {
this.$refs.paramForm.validate((valid) => {
if (valid) {
this.submitLoading = true;
let data = JSON.parse(JSON.stringify(this.paramForm));
data.isIndex = Number(data.isIndex);
data.required = Number(data.required);
if (this.modalType === 0) {
insertGoodsParams(data).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("参数添加成功");
this.getDataList();
this.dialogParamsVisible = false;
}
});
} else {
console.warn(data.isIndex);
data.isIndex = Number(data.isIndex);
data.required = Number(data.required);
updateGoodsParams(data).then((res) => {
this.submitLoading = false;
if (res.success) {
this.$Message.success("参数修改成功");
this.getDataList();
this.dialogParamsVisible = false;
}
});
}
}
});
},
// 获取分类列表
getDataList() { getDataList() {
getCategoryParamsListData(this.categoryId).then((res) => { this.loading = true;
if (res.success) { getGoodsParamsPage(this.searchForm).then((res) => {
this.paramsGroup = res.result; this.loading = false;
if (res && res.success) {
this.data = (res.result && res.result.records) || [];
this.total = (res.result && res.result.total) || 0;
} }
}); });
}, },
//删除参数方法 goAdd() {
handleDeleteParam(group, param) { this.$router.push({ name: "goods-parameter-edit" });
this.$Modal.confirm({
title: "确认删除",
// 记得确认修改此处
content: "您确认要删除 " + param.paramName + " ?",
loading: true,
onOk: () => {
// 删除
deleteParams(param.id).then((res) => {
this.$Modal.remove();
if (res.success) {
this.$Message.success("删除参数成功");
this.getDataList();
}
});
},
});
}, },
//删除参数组方法 goEdit(row) {
handleDeleteParamGroup(group) { if (!row || !row.id) return;
try {
window.sessionStorage.setItem(`goods-parameter-edit:${row.id}`, JSON.stringify(row));
} catch (e) {}
this.$router.push({ name: "goods-parameter-edit", query: { id: row.id } });
},
remove(row) {
this.$Modal.confirm({ this.$Modal.confirm({
title: "确认删除", title: "确认删除",
// 记得确认修改此处 content: "您确认要删除 " + (row.paramName || "") + " ?",
content: "您确认要删除 " + group.groupName + " ?",
loading: true, loading: true,
onOk: () => { onOk: () => {
// 删除 deleteParams(row.id).then((res) => {
deleteParamsGroup(group.groupId).then((res) => {
this.$Modal.remove(); this.$Modal.remove();
if (res.success) { if (res && res.success) {
this.$Message.success("删除参数成功"); this.$Message.success("删除成功");
this.getDataList(); this.getDataList();
} }
}); });
@@ -392,33 +209,4 @@ export default {
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss"></style>
.row {
overflow: hidden;
margin: 20px 0;
}
.params {
align-items: center;
display: flex;
padding: 3px;
background-color: #f5f7fa;
font-size: 14px;
justify-content: space-between;
}
.ivu-card-head {
background-color: #f5f7fa;
}
.ivu-btn {
font-size: 13px;
}
.paramsGroup {
flex-wrap: wrap;
display: flex;
flex-direction: row;
width: 100%;
}
</style>

View File

@@ -2,7 +2,7 @@
<div> <div>
<!-- 选择商品类型 --> <!-- 选择商品类型 -->
<Modal v-model="selectGoodsType" width="550" :closable="false"> <Modal v-model="selectGoodsType" width="550" :closable="false">
<div class="goods-type-list" v-if="!showGoodsTemplates"> <div class="goods-type-list">
<div <div
class="goods-type-item" class="goods-type-item"
:class="{ 'active-goods-type': item.check }" :class="{ 'active-goods-type': item.check }"
@@ -17,25 +17,6 @@
</div> </div>
</div> </div>
</div> </div>
<div v-else class="goods-type-list">
<h2 @click="showGoodsTemplates = !showGoodsTemplates">返回</h2>
<div class="goods-list-box">
<Scroll :on-reach-bottom="handleReachBottom">
<div
class="goods-type-item template-item"
@click="handleClickGoodsTemplate(item)"
v-for="(item, tempIndex) in goodsTemplates"
:key="tempIndex"
>
<img :src="item.thumbnail" />
<div>
<h2>{{ item.goodsName }}</h2>
<p>{{ item.sellingPoint || "" }}</p>
</div>
</div>
</Scroll>
</div>
</div>
</Modal> </Modal>
<!-- 商品分类 --> <!-- 商品分类 -->
<div class="content-goods-publish"> <div class="content-goods-publish">
@@ -79,9 +60,6 @@
<span v-show="category[1].name">> {{ category[1].name }}</span> <span v-show="category[1].name">> {{ category[1].name }}</span>
<span v-show="category[2].name">> {{ category[2].name }}</span> <span v-show="category[2].name">> {{ category[2].name }}</span>
</p> </p>
<template v-if="selectedTemplate.goodsName">
<Divider>已选商品模版:{{ selectedTemplate.goodsName }}</Divider>
</template>
</div> </div>
<!-- 底部按钮 --> <!-- 底部按钮 -->
<div class="footer"> <div class="footer">
@@ -97,10 +75,7 @@ import * as API_GOODS from "@/api/goods";
export default { export default {
data() { data() {
return { return {
selectedTemplate: {}, // 已选模板
selectGoodsType: false, // 展示选择商品分类modal selectGoodsType: false, // 展示选择商品分类modal
goodsTemplates: [], // 商品模板列表
showGoodsTemplates: false, //是否显示选择商品模板
goodsTypeWay: [ goodsTypeWay: [
{ {
title: "实物商品", title: "实物商品",
@@ -116,12 +91,6 @@ export default {
type: "VIRTUAL_GOODS", type: "VIRTUAL_GOODS",
check: false, check: false,
}, },
{
title: "商品模板导入",
img: require("@/assets/goodsTypeTpl.png"),
desc: "商品模板,一键导入",
check: false,
},
], ],
// 商品分类选择数组 // 商品分类选择数组
category: [ category: [
@@ -137,29 +106,9 @@ export default {
categoryListLevel2: [], categoryListLevel2: [],
/** 3级分类列表*/ /** 3级分类列表*/
categoryListLevel3: [], categoryListLevel3: [],
searchParams: {
saveType: "TEMPLATE",
sort: "create_time",
order: "desc",
pageSize: 10,
pageNumber: 1,
},
templateTotal: 0,
}; };
}, },
methods: { methods: {
// 商品模版触底加载
handleReachBottom() {
setTimeout(() => {
if (
this.searchParams.pageNumber * this.searchParams.pageSize <=
this.templateTotal
) {
this.searchParams.pageNumber++;
this.GET_GoodsTemplate();
}
}, 1000);
},
// 点击商品类型 // 点击商品类型
handleClickGoodsType(val) { handleClickGoodsType(val) {
this.goodsTypeWay.map((item) => { this.goodsTypeWay.map((item) => {
@@ -167,28 +116,7 @@ export default {
}); });
val.check = !val.check; val.check = !val.check;
if (!val.type) { this.goodsType = val.type;
this.GET_GoodsTemplate();
this.showGoodsTemplates = true;
} else {
this.goodsType = val.type;
this.selectedTemplate = {};
}
},
// 点击商品模板
handleClickGoodsTemplate(val) {
this.selectedTemplate = val;
this.selectGoodsType = false;
this.$emit("change", { tempId: val.id });
},
// 获取商品模板
GET_GoodsTemplate() {
API_GOODS.getDraftGoodsListData(this.searchParams).then((res) => {
if (res.success) {
this.goodsTemplates.push(...res.result.records);
this.templateTotal = res.result.total;
}
});
}, },
/** 选择商城商品分类 */ /** 选择商城商品分类 */
handleSelectCategory(row, index, level) { handleSelectCategory(row, index, level) {
@@ -223,7 +151,7 @@ export default {
// 下一步 // 下一步
next() { next() {
window.scrollTo(0, 0); window.scrollTo(0, 0);
if (!this.goodsType && !this.selectedTemplate.goodsName) { if (!this.goodsType) {
this.$Message.error("请选择商品类型"); this.$Message.error("请选择商品类型");
return; return;
} }
@@ -238,12 +166,7 @@ export default {
category: this.category, category: this.category,
goodsType: this.goodsType, goodsType: this.goodsType,
}; };
if (this.selectedTemplate.id) { this.$emit("change", params);
params.tempId = this.selectedTemplate.id;
this.$emit("change", params);
} else {
this.$emit("change", params);
}
} }
}, },
}, },
@@ -254,7 +177,4 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./addGoods.scss"; @import "./addGoods.scss";
::v-deep .ivu-scroll-container {
height: 450px !important;
}
</style> </style>

View File

@@ -32,10 +32,8 @@
<Button class="refresh-icon" icon="md-refresh" shape="circle" type="text" <Button class="refresh-icon" icon="md-refresh" shape="circle" type="text"
@click="refresh('brand')"></Button> @click="refresh('brand')"></Button>
</FormItem> </FormItem>
</div>
<h4>商品交易信息</h4> <FormItem class="form-item-view-el" label="计量单位" prop="goodsUnit">
<div class="form-item-view">
<FormItem class="form-item-view-el" label="计量单位" prop="goodsUnit">
<Select v-model="baseInfoForm.goodsUnit" style="width: 100px"> <Select v-model="baseInfoForm.goodsUnit" style="width: 100px">
<Option v-for="(item, index) in goodsUnitList" :key="index" :value="item">{{ item }} <Option v-for="(item, index) in goodsUnitList" :key="index" :value="item">{{ item }}
</Option> </Option>
@@ -97,7 +95,18 @@
</div> </div>
</div> </div>
</FormItem> </FormItem>
<FormItem class="form-item-view-el" label="商品发布" prop="release">
<RadioGroup v-model="baseInfoForm.release" button-style="solid" type="button">
<Radio :label="1" title="上架">
<span>上架</span>
</Radio>
<Radio :label="0" title="下架">
<span>下架</span>
</Radio>
</RadioGroup>
</FormItem>
</div> </div>
<h4>商品规格及图片</h4> <h4>商品规格及图片</h4>
<div class="form-item-view"> <div class="form-item-view">
<FormItem class="form-item-view-el required" label="主图" prop="goodsGalleryFiles"> <FormItem class="form-item-view-el required" label="主图" prop="goodsGalleryFiles">
@@ -380,56 +389,18 @@
<span slot="append">kg</span></Input> <span slot="append">kg</span></Input>
</FormItem> </FormItem>
</div> </div>
<h4>其他信息</h4> <h4>参数信息</h4>
<div class="form-item-view"> <div class="form-item-view">
<FormItem class="form-item-view-el" label="商品发布" prop="release"> <FormItem v-for="(paramsItem, paramsIndex) in goodsParams" :key="paramsItem.id || paramsIndex"
<RadioGroup v-model="baseInfoForm.release" button-style="solid" type="button"> :label="`${paramsItem.paramName}`"
<Radio :label="1" title="立即发布"> :rules="{ required: !!paramsItem.required, message: '参数不能为空', trigger: 'change' }">
<span>立即发布</span> <Select v-model="paramsItem.paramValue" clearable placeholder="请选择" style="width: 200px"
</Radio> @on-change="(val) => selectParams(paramsItem, val)">
<Radio :label="0" title="放入仓库"> <Option v-for="option in getParamOptions(paramsItem.options)" :key="option" :label="option"
<span>放入仓库</span> :value="option">
</Radio> </Option>
</RadioGroup> </Select>
</FormItem> </FormItem>
<FormItem class="form-item-view-el" label="商品推荐" prop="skuList">
<RadioGroup v-model="baseInfoForm.recommend" button-style="solid" type="button">
<Radio :label="1" title="推荐">
<span>推荐</span>
</Radio>
<Radio :label="0" title="不推荐">
<span>不推荐</span>
</Radio>
</RadioGroup>
</FormItem>
</div>
<div class="form-item-view-bottom">
<Collapse v-for="(paramsGroup, groupIndex) in goodsParams" :key="paramsGroup.groupName"
v-model="params_panel" :title="paramsGroup.groupName" class="mb_10" style="text-align: left">
<Panel :name="paramsGroup.groupName">
{{ paramsGroup.groupName }}
<p slot="content">
<FormItem v-for="(paramsItem, paramsIndex) in paramsGroup.params" :key="paramsIndex"
:label="`${paramsItem.paramName}`"
:rules="{ required: paramsItem.required, message: '参数不能为空', trigger: 'blur' }">
<Select v-model="paramsItem.paramValue" clearable placeholder="请选择" style="width: 200px"
@on-change="
selectParams(
paramsGroup,
groupIndex,
paramsItem,
paramsIndex,
paramsItem.paramValue
)
">
<Option v-for="option in paramsItem.options.split(',')" :key="option" :label="option"
:value="option">
</Option>
</Select>
</FormItem>
</p>
</Panel>
</Collapse>
</div> </div>
</div> </div>
</div> </div>
@@ -442,7 +413,6 @@
<Button :loading="submitLoading" type="primary" @click="save"> <Button :loading="submitLoading" type="primary" @click="save">
{{ this.$route.query.id ? "保存" : "保存商品" }} {{ this.$route.query.id ? "保存" : "保存商品" }}
</Button> </Button>
<Button type="primary" @click="saveToDraft">保存为模版</Button>
</ButtonGroup> </ButtonGroup>
</div> </div>
@@ -519,7 +489,7 @@ export default {
previewImage: '', // 预览图片地址 previewImage: '', // 预览图片地址
global: 0, global: 0,
accessToken: "", //令牌token accessToken: "", //令牌token
goodsParams: "", goodsParams: [],
categoryId: "", // 商品分类第三级id categoryId: "", // 商品分类第三级id
//提交状态 //提交状态
submitLoading: false, submitLoading: false,
@@ -763,54 +733,41 @@ export default {
mouseLeave() { mouseLeave() {
// this.showContent = false // this.showContent = false
}, },
/** getParamOptions(options) {
* 选择参数 if (!options) return [];
* @paramsGroup 参数分组 return String(options)
* @groupIndex 参数分组下标 .split(",")
* @params 参数选项 .map((i) => i.trim())
* @paramIndex 参数下标值 .filter((i) => i);
* @value 参数选项值 },
*/ selectParams(params, value) {
selectParams(paramsGroup, groupIndex, params, paramsIndex, value) { if (!Array.isArray(this.baseInfoForm.goodsParamsDTOList)) {
if (!this.baseInfoForm.goodsParamsDTOList[groupIndex]) { this.$set(this.baseInfoForm, "goodsParamsDTOList", []);
this.baseInfoForm.goodsParamsDTOList[groupIndex] = {
groupId: "",
groupName: "",
goodsParamsItemDTOList: [],
};
} }
//赋予分组id、分组名称 const list = this.baseInfoForm.goodsParamsDTOList;
this.baseInfoForm.goodsParamsDTOList[groupIndex].groupId = const paramId = params && params.id ? String(params.id) : "";
paramsGroup.groupId; const index = list.findIndex((i) => String(i.paramId) === paramId);
this.baseInfoForm.goodsParamsDTOList[groupIndex].groupName =
paramsGroup.groupName;
//参数详细为空,则赋予 if (!value && value !== 0) {
if ( if (index >= 0) {
!this.baseInfoForm.goodsParamsDTOList[groupIndex] list.splice(index, 1);
.goodsParamsItemDTOList[paramsIndex] }
) { return;
this.baseInfoForm.goodsParamsDTOList[groupIndex].goodsParamsItemDTOList[
paramsIndex
] = {
paramName: "",
paramValue: "",
isIndex: "",
// required: "",
paramId: "",
sort: "",
};
} }
this.baseInfoForm.goodsParamsDTOList[groupIndex].goodsParamsItemDTOList[ const newItem = {
paramsIndex paramId,
] = {
paramName: params.paramName, paramName: params.paramName,
paramValue: value, paramValue: value,
isIndex: params.isIndex, isIndex: params.isIndex || 0,
// required: params.required, required: params.required || 0,
paramId: params.id, sort: params.sort || 0,
sort: params.sort,
}; };
if (index >= 0) {
this.$set(list, index, newItem);
} else {
list.push(newItem);
}
}, },
// 编辑sku图片 // 编辑sku图片
editSkuPicture(row) { editSkuPicture(row) {
@@ -1191,40 +1148,75 @@ export default {
/** 根据当前分类id查询商品应包含的参数 */ /** 根据当前分类id查询商品应包含的参数 */
GET_GoodsParams() { GET_GoodsParams() {
this.goodsParams = [] this.goodsParams = [];
this.params_panel = [];
API_GOODS.getCategoryParamsListDataSeller(this.categoryId).then( API_GOODS.getCategoryParamsListDataSeller(this.categoryId).then(
(response) => { (response) => {
if (!response || response.length <= 0) { if (!response || response.length <= 0) {
return; return;
} }
this.goodsParams = response;
//展开选项卡 if (!Array.isArray(this.baseInfoForm.goodsParamsDTOList)) {
this.goodsParams.forEach((item) => {
this.params_panel.push(item.groupName);
});
if (this.baseInfoForm.goodsParamsDTOList) {
// 已选值集合
const paramsArr = [];
this.baseInfoForm.goodsParamsDTOList.forEach((group) => {
group.goodsParamsItemDTOList.forEach((param) => {
param.groupId = group.groupId;
paramsArr.push(param);
});
});
// 循环参数分组
this.goodsParams.forEach((paramsGroup) => {
paramsGroup.params.forEach((param) => {
paramsArr.forEach((arr) => {
if (param.paramName === arr.paramName) {
param.paramValue = arr.paramValue;
}
});
});
});
} else {
this.baseInfoForm.goodsParamsDTOList = []; this.baseInfoForm.goodsParamsDTOList = [];
} }
const mergedSelected = new Map();
const selectedList = [];
this.baseInfoForm.goodsParamsDTOList.forEach((item) => {
if (!item) return;
if (Array.isArray(item.goodsParamsItemDTOList)) {
selectedList.push(...item.goodsParamsItemDTOList);
} else if (item.paramId || item.paramName) {
selectedList.push(item);
}
});
selectedList.forEach((param) => {
if (!param) return;
const key = param.paramId ? String(param.paramId) : param.paramName;
if (!key) return;
mergedSelected.set(key, param);
});
this.baseInfoForm.goodsParamsDTOList = Array.from(mergedSelected.values()).map((p) => {
return {
paramId: p.paramId ? String(p.paramId) : "",
paramName: p.paramName,
paramValue: p.paramValue,
isIndex: p.isIndex || 0,
required: p.required || 0,
sort: p.sort || 0,
};
});
const findSelectedValue = (param) => {
if (!param) return undefined;
const byId = mergedSelected.get(String(param.id));
if (byId) return byId.paramValue;
const byName = mergedSelected.get(param.paramName);
if (byName) return byName.paramValue;
return undefined;
};
const isGrouped = response[0] && Array.isArray(response[0].params);
const flatParams = isGrouped
? response.reduce((acc, g) => {
if (g && Array.isArray(g.params)) {
acc.push(...g.params);
}
return acc;
}, [])
: response;
this.goodsParams = flatParams
.map((p) => {
const selectedValue = findSelectedValue(p);
return {
...p,
paramValue:
selectedValue !== undefined ? selectedValue : p.paramValue,
};
})
.sort((a, b) => Number(a.sort || 0) - Number(b.sort || 0));
} }
); );
}, },
@@ -1935,18 +1927,16 @@ export default {
return; return;
} }
let checkFlag = false; let checkFlag = false;
this.goodsParams.forEach(group => { this.goodsParams.forEach((param) => {
group.params.forEach(param => { if (!param || !param.required) return;
if (param.required) { const check = this.baseInfoForm.goodsParamsDTOList.some((paramsItem) => {
const check = this.baseInfoForm.goodsParamsDTOList.some(paramsGroup => if (String(paramsItem.paramId) !== String(param.id)) return false;
paramsGroup.goodsParamsItemDTOList.some(paramsItem => paramsItem.paramId === param.id) return !!paramsItem.paramValue;
); });
if (!check) { if (!check) {
checkFlag = !check; checkFlag = true;
} }
} });
})
})
if (checkFlag) { if (checkFlag) {
this.$Message.error("存在未填写的参数项"); this.$Message.error("存在未填写的参数项");
return; return;
@@ -2078,57 +2068,6 @@ export default {
} }
}); });
}, },
/** 保存为模板 */
saveToDraft() {
this.baseInfoForm.skuList = this.skuTableData;
if (this.baseInfoForm.goodsGalleryFiles.length > 0) {
this.baseInfoForm.goodsGalleryList =
this.baseInfoForm.goodsGalleryFiles.map((i) => i);
}
this.baseInfoForm.categoryName = [];
this.baseInfoForm.saveType = "TEMPLATE";
if (this.$route.query.draftId) {
this.baseInfoForm.id = this.$route.query.draftId;
this.$Modal.confirm({
title: "当前模板已存在",
content: "当前模板已存在保存为新模板或替换原模板",
okText: "保存新模板",
cancelText: "替换旧模板",
closable: true,
onOk: () => {
delete this.baseInfoForm.id;
this.SAVE_DRAFT_GOODS();
},
onCancel: () => {
this.SAVE_DRAFT_GOODS();
},
});
return;
}
this.$Modal.confirm({
title: "保存模板",
content: "是否确定保存",
okText: "保存",
closable: true,
onOk: () => {
this.SAVE_DRAFT_GOODS();
},
});
},
SAVE_DRAFT_GOODS() {
if (this.baseInfoForm.salesModel === "WHOLESALE") {
this.baseInfoForm.wholesaleList = this.wholesaleData;
}
// 保存模板
API_GOODS.saveDraftGoods(this.baseInfoForm).then((res) => {
if (res.success) {
this.$Message.info("保存成功");
this.$router.push({ name: "template-goods" });
}
});
},
GET_ShipTemplate(type) { GET_ShipTemplate(type) {
// 获取物流模板 // 获取物流模板
API_Shop.getShipTemplate().then((res) => { API_Shop.getShipTemplate().then((res) => {