唯有热爱,可抵岁月长河
一、商品列表
⒈ 接口封装
新建 src / api / product.ts
文件
import request from '@/utils/request'
import {
Product,
ProductType,
ProductListParams,
ProductCategory,
AttrRuleValue,
ProductAttrTpl,
IExpressTemplate,
AttrTableHeader,
ProductAttr
} from './types/product'
/**
* 获取商品类目列表
* @returns 商品类目列表
*/
export const getProductTypes = () => {
return request<{
list: ProductType[]
}>({
method: 'GET',
url: '/product/product/type_header'
})
}
/**
* 获取商品列表
* @param params 查询参数
* @returns 商品列表
*/
export const getProducts = (params?: ProductListParams) => {
return request<{
count: number
list: Product[]
}>({
method: 'GET',
url: '/product/product',
params
})
}
/**
* 保存新增或编辑
* @param id 商品ID
* @param data 商品数据
* @returns null
*/
export const saveProduct = (id: number, data: any) => {
return request({
method: 'POST',
url: `/product/product/${id}`,
data
})
}
/**
* 获取商品
* @param id 商品id
* @returns 商品
*/
export const getProduct = (id: number) => {
return request<{
tempList: {
id: number
name: string
}[]
cateList: {
value: number
label: string
disabled: number
}[]
productInfo: {
cate_id: string[]
is_sub: number[]
activity: string[]
label_id: string[]
coupons: string[]
description: string
items: string[]
attrs: string[]
attr: {
pic: string
vip_price: number
price: number
cost: number
ot_price: number
stock: number
bar_code: string
weight: number
volume: number
brokerage: number
brokerage_two: number
}
} & Omit<Product, 'cate_id' | 'is_sub' | 'activity' | 'label_id' | 'collect' | 'likes' | 'visitor' | 'cate_name' | 'stock_attr'>
}>({
method: 'GET',
url: `/product/product/${id}`
})
}
export const getCategoryTree = (type: 0 | 1) => {
return request<ProductCategory[]>({
method: 'GET',
url: `/product/category/tree/${type}`
})
}
/**
* 商品上下架操作
*/
export const updateProductStatus = (id: number, status: number) => {
return request({
method: 'PUT',
url: `/product/product/set_show/${id}/${status}`
})
}
/**
* 商品加入/移除回收站
*/
export const removeProduct = (id: number) => {
return request({
method: 'DELETE',
url: `/product/product/${id}`
})
}
/**
* 获取商品规格模板
*/
export const getAttrs = () => {
return request<ProductAttrTpl[]>({
method: 'GET',
url: '/product/product/get_rule'
})
}
/**
* 生成商品属性
*/
export const generateAttr = (id: number, type: 0 | 1, data: {
attrs: AttrRuleValue[]
}) => {
return request<{
info: {
attr: AttrRuleValue[]
header: AttrTableHeader[]
value: ProductAttr[]
}
}>({
method: 'POST',
url: `/product/generate_attr/${id}/${type}`,
data
})
}
/**
* 获取运费模板
*/
export const getExpressTemplate = () => {
return request<IExpressTemplate[]>({
method: 'GET',
url: '/product/product/get_template'
})
}
/**
* 获取商品规格列表
*/
export const getProductRules = () => {
return request({
method: 'GET',
url: '/product/product/rule'
})
}
/**
* 批量上架
*/
export const updateProductsShow = (ids: number[]) => {
return request({
method: 'PUT',
url: '/product/product/product_show',
data: {
ids
}
})
}
/**
* 批量下架
*/
export const updateProductsUnshow = (ids: number[]) => {
return request({
method: 'PUT',
url: '/product/product/product_unshow',
data: {
ids
}
})
}
新建 src / api / types / product.ts
文件
export interface Product {
id: number
mer_id: number
image: string
recommend_image: string
slider_image: string[]
store_name: string
store_info: string
keyword: string
bar_code: string
cate_id: string
price: string
vip_price: string
ot_price: string
postage: string
unit_name: string
sort: number
sales: number
stock: string
is_show: number
is_hot: number
is_benefit: number
is_best: number
is_new: number
add_time: number
is_postage: number
is_del: number
mer_use: number
give_integral: string
cost: string
is_seckill: number
is_bargain: null
is_good: number
is_sub: number
is_vip: number
ficti: number
browse: number
code_path: string
soure_link: string
video_link: string
temp_id: number
spec_type: number
activity: string
spu: string
label_id: string
command_word: string
collect: number
likes: number
visitor: number
cate_name: string
stock_attr: boolean
statusLoading?: boolean
}
export interface ProductType {
type: number
name: string
count: number
}
export interface ProductListParams {
page?: number
limit?: number
cate_id?: number
type?: 0 | 1 | 2 | 3 | 4 | 5 | 6
store_name?: string
sales?: 'normal' | 'desc' | 'asc'
}
export interface ProductCategory {
add_time: string
big_pic: string
cate_name: string
html: string
id: number
is_show: 0 | 1
pic: string
pid: number
sort: number
}
export interface AttrRuleValue {
value: string
detail: string[]
inputVisible?: boolean
inputValue?: string
}
export interface ProductAttrTpl {
id: number
attr_name: string
rule_name: string
attr_value: string[]
rule_value: AttrRuleValue[]
}
export interface IExpressTemplate {
id: number
name: string
}
export interface AttrTableHeader {
align: string
key: string
minWidth: number
title: string
}
export type ProductAttr = Record<string, any> & {
pic: string
vip_price: number
price: number
cost: number
ot_price: number
stock: number
bar_code: string
weight: number
volume: number
brokerage: number
brokerage_two: number
}
// export interface ProductAttr {
// bar_code: string
// brokerage: number
// brokerage_two: number
// cost: number
// ot_price: number
// pic: string
// price: number
// stock: number
// vip_price: number
// volume: number
// weight: number
// }
⒉ 商品列表页
新建 src / views / product / list / index.vue
文件
<template>
<!-- <page-container> -->
<el-card>
<template #header>
数据筛选
</template>
<el-form
ref="form"
:model="listParams"
:disabled="listLoading"
label-width="70px"
@submit.prevent="handleQuery"
>
<el-form-item label="商品分类">
<el-select
v-model="listParams.cate_id"
placeholder="请选择"
clearable
@change="loadList"
>
<el-option
label="全部"
:value="0"
/>
<el-option
v-for="item in productCates"
:key="item.id"
:label="`${item.html}${item.cate_name}`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="商品名称">
<el-input
v-model="listParams.store_name"
placeholder="请输入商品名称关键字"
clearable
style="width: 300px;"
>
<template #append>
<!-- <el-button
icon="el-icon-search"
@click="loadList"
/> -->
<el-icon><search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="商品类目">
<el-radio-group
v-model="listParams.type"
@change="loadList"
>
<el-radio :label="0">
全部
</el-radio>
<el-radio
v-for="item in productTypes"
:key="item.type"
:label="item.type"
>
{{ `${item.name}(${item.count})` }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</el-card>
<el-card style="margin-top: 20px;">
<template #header>
<!-- icon="el-icon-plus" -->
<el-button
type="primary"
@click="$router.push('/admin/product/add_product')"
>
<el-icon><plus /></el-icon>
添加商品
</el-button>
<el-button
v-if="listParams.type === 2"
:loading="updateProductsShowLoading"
@click="handleUpdateProductsShow"
>
批量上架
</el-button>
<el-button
v-else
:loading="updateProductsUnshowLoading"
@click="handleUpdateProductsUnshow"
>
批量下架
</el-button>
<!-- @click="handleExportExcel" -->
<!-- icon="el-icon-document" -->
<el-button
:loading="exportExcelLoading"
>
<el-icon><download /></el-icon>
导出表格
</el-button>
</template>
<el-table
:data="list"
v-loading="listLoading"
style="width: 100%"
@sort-change="handleSortChange"
@selection-change="handleSelectionChange"
>
<el-table-column type="expand">
<template #default="props">
<el-form
label-position="left"
inline
class="demo-table-expand"
>
<el-form-item label="商品分类">
<span>{{ props.row.cate_name }}</span>
</el-form-item>
<el-form-item label="市场价格">
<span>{{ props.row.ot_price }}</span>
</el-form-item>
<el-form-item label="成本价">
<span>{{ props.row.cost }}</span>
</el-form-item>
<el-form-item label="收藏数量">
<span>{{ props.row.collect }}</span>
</el-form-item>
<el-form-item label="虚拟销量">
<span>{{ props.row.ficti }}</span>
</el-form-item>
</el-form>
</template>
</el-table-column>
<el-table-column
type="selection"
width="55"
/>
<el-table-column
prop="id"
label="商品ID"
/>
<el-table-column
prop="id"
label="商品图片"
>
<template #default="scope">
<el-image
style="width: 100px; height: 100px"
:src="scope.row.image"
:preview-src-list="[scope.row.image]"
/>
</template>
</el-table-column>
<el-table-column
prop="store_name"
label="商品名称"
/>
<el-table-column
prop="price"
label="商品售价"
/>
<el-table-column
prop="sales"
label="销量"
sortable="custom"
/>
<el-table-column
prop="stock"
label="库存"
/>
<el-table-column
prop="sort"
label="排序"
/>
<el-table-column
label="状态"
width="150"
>
<template #default="scope">
<el-switch
v-model="scope.row.is_show"
active-color="#13ce66"
inactive-color="#ff4949"
:active-value="1"
:inactive-value="0"
:loading="scope.row.statusLoading"
active-text="上架"
inactive-text="下架"
@change="handleUpdateStatus(scope.row)"
/>
</template>
</el-table-column>
<el-table-column
min-width="120px"
label="操作"
fixed="right"
align="center"
>
<template #default="{ row }">
<el-button type="text">
编辑
</el-button>
<el-button type="text">
查看评论
</el-button>
<el-popconfirm
:title="row.is_del ? '确定恢复商品吗?' : '确定移到回收站吗?'"
@confirm="handleDelete(row.id)"
>
<template #reference>
<el-button type="text">
{{ row.is_del ? '恢复商品' : '移到回收站' }}
</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- <app-pagination
v-model:page="listParams.page"
v-model:limit="listParams.limit"
:list-count="listCount"
:load-list="loadList"
:disabled="listLoading"
/> -->
<el-pagination
layout="total, sizes, prev, pager, next, jumper"
:total="listCount"
:page-sizes="[2, 4, 6]"
:disabled="listLoading"
v-model:currentPage="listParams.page"
v-model:pageSize="listParams.limit"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-card>
<!-- </page-container> -->
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'
import * as productApi from '@/api/product'
import { ElMessage } from 'element-plus'
import type { Product, ProductListParams, ProductType, ProductCategory } from '@/api/types/product'
// import { jsonToExcel } from '@/utils/export-to-excel'
import { Search, Plus, Download } from '@element-plus/icons-vue'
const productTypes = ref<ProductType[]>([])
const productCates = ref<ProductCategory[]>([])
const list = ref<Product[]>([])
const listCount = ref(0)
const listLoading = ref(false)
const listParams = reactive<ProductListParams>({
page: 1,
limit: 10,
cate_id: 0,
type: 0,
store_name: '',
sales: 'normal'
})
const selectionItems = ref<Product[]>([])
const updateProductsShowLoading = ref(false)
const updateProductsUnshowLoading = ref(false)
const exportExcelLoading = ref(false)
onMounted(() => {
loadList()
loadProductCates()
})
const handleSelectionChange = (val: Product[]) => {
selectionItems.value = val
}
const loadList = async () => {
listLoading.value = true
const data = await productApi.getProducts(listParams).finally(() => {
listLoading.value = false
})
data.list.forEach(item => {
item.statusLoading = false
})
list.value = data.list
listCount.value = data.count
// 更新商品类型
loadProductTypes()
}
const loadProductCates = async () => {
const data = await productApi.getCategoryTree(1)
productCates.value = data
}
const loadProductTypes = async () => {
const data = await productApi.getProductTypes()
productTypes.value = data.list
}
const handleQuery = () => {
listParams.page = 1
loadList()
}
const handleUpdateStatus = async (item: Product) => {
item.statusLoading = true
await productApi.updateProductStatus(item.id, item.is_show).finally(() => {
item.statusLoading = false
})
ElMessage.success(`${item.is_show ? '上架' : '下架'}成功`)
loadList()
}
const handleDelete = async (id: number) => {
await productApi.removeProduct(id)
loadList()
}
const handleSortChange = ({ prop, order }: { prop: string, order: 'descending' | 'ascending' | null }) => {
let sales: ProductListParams['sales'] = 'normal'
switch (order) {
case 'ascending':
sales = 'asc'
break
case 'descending':
sales = 'desc'
break
}
listParams.sales = sales
loadList()
}
const handleUpdateProductsShow = async () => { // 批量上架
if (!selectionItems.value.length) {
return ElMessage.warning('请选择商品')
}
updateProductsShowLoading.value = true
await productApi.updateProductsShow(selectionItems.value.map(item => item.id)).finally(() => {
updateProductsShowLoading.value = false
})
ElMessage.success('批量上架成功')
loadList()
}
const handleUpdateProductsUnshow = async () => { // 批量下架
if (!selectionItems.value.length) {
return ElMessage.warning('请选择商品')
}
updateProductsUnshowLoading.value = true
await productApi.updateProductsUnshow(selectionItems.value.map(item => item.id)).finally(() => {
updateProductsUnshowLoading.value = false
})
ElMessage.success('批量下架成功')
loadList()
}
const handleSizeChange = (size: number) => { // 每页条数
listParams.limit = size
listParams.page = 1
loadList()
}
const handleCurrentChange = (page: number) => { // 当前页数
listParams.page = page
loadList()
}
</script>
<style lang="scss" scoped>
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.demo-table-expand {
font-size: 0;
:deep(label) {
width: 90px;
color: #99a9bf;
}
:deep(.el-form-item) {
margin-right: 0;
margin-bottom: 0;
width: 50%;
}
}
.el-pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>
二、导出 Excel 表格
⒈ 插件
// 安装
npm install xlsx
⒉ 封装方法
新建 src / utils / export-to-excel.ts
文件
import xlsx from 'xlsx'
export const jsonToExcel = (options: {
data: any[]
header: Record<string, string>
fileName: string
bookType: xlsx.BookType
}) => {
// 1、创建一个工作簿 workbook
const wb = xlsx.utils.book_new()
// 2、创建工作表 worksheet
if (options.header) {
options.data = options.data.map(item => {
const obj: Record<string, any> = {}
for (const key in item) {
if (options.header[key]) { // 修改数据头部名称
obj[options.header[key]] = item[key]
} else {
obj[key] = item[key]
}
}
return obj
})
}
const ws = xlsx.utils.json_to_sheet(options.data)
// 3. 把工作表放到工作簿中
xlsx.utils.book_append_sheet(wb, ws)
// 4、生成数据保存
xlsx.writeFile(wb, options.fileName, {
bookType: options.bookType || 'xlsx'
})
}
⒊ 组件引用
新建 src / views / product / list / index.vue
文件
<template>
<!-- <page-container> -->
<el-card>
<template #header>
数据筛选
</template>
<el-form
ref="form"
:model="listParams"
:disabled="listLoading"
label-width="70px"
@submit.prevent="handleQuery"
>
<el-form-item label="商品分类">
<el-select
v-model="listParams.cate_id"
placeholder="请选择"
clearable
@change="loadList"
>
<el-option
label="全部"
:value="0"
/>
<el-option
v-for="item in productCates"
:key="item.id"
:label="`${item.html}${item.cate_name}`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="商品名称">
<el-input
v-model="listParams.store_name"
placeholder="请输入商品名称关键字"
clearable
style="width: 300px;"
>
<template #append>
<!-- <el-button
icon="el-icon-search"
@click="loadList"
/> -->
<el-icon><search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="商品类目">
<el-radio-group
v-model="listParams.type"
@change="loadList"
>
<el-radio :label="0">
全部
</el-radio>
<el-radio
v-for="item in productTypes"
:key="item.type"
:label="item.type"
>
{{ `${item.name}(${item.count})` }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</el-card>
<el-card style="margin-top: 20px;">
<template #header>
<!-- icon="el-icon-plus" -->
<el-button
type="primary"
@click="$router.push('/admin/product/add_product')"
>
<el-icon><plus /></el-icon>
添加商品
</el-button>
<el-button
v-if="listParams.type === 2"
:loading="updateProductsShowLoading"
@click="handleUpdateProductsShow"
>
批量上架
</el-button>
<el-button
v-else
:loading="updateProductsUnshowLoading"
@click="handleUpdateProductsUnshow"
>
批量下架
</el-button>
<!-- icon="el-icon-document" -->
<el-button
@click="handleExportExcel"
:loading="exportExcelLoading"
>
<el-icon><download /></el-icon>
导出表格
</el-button>
</template>
<el-table
:data="list"
v-loading="listLoading"
style="width: 100%"
@sort-change="handleSortChange"
@selection-change="handleSelectionChange"
>
<el-table-column type="expand">
<template #default="props">
<el-form
label-position="left"
inline
class="demo-table-expand"
>
<el-form-item label="商品分类">
<span>{{ props.row.cate_name }}</span>
</el-form-item>
<el-form-item label="市场价格">
<span>{{ props.row.ot_price }}</span>
</el-form-item>
<el-form-item label="成本价">
<span>{{ props.row.cost }}</span>
</el-form-item>
<el-form-item label="收藏数量">
<span>{{ props.row.collect }}</span>
</el-form-item>
<el-form-item label="虚拟销量">
<span>{{ props.row.ficti }}</span>
</el-form-item>
</el-form>
</template>
</el-table-column>
<el-table-column
type="selection"
width="55"
/>
<el-table-column
prop="id"
label="商品ID"
/>
<el-table-column
prop="id"
label="商品图片"
>
<template #default="scope">
<el-image
style="width: 100px; height: 100px"
:src="scope.row.image"
:preview-src-list="[scope.row.image]"
/>
</template>
</el-table-column>
<el-table-column
prop="store_name"
label="商品名称"
/>
<el-table-column
prop="price"
label="商品售价"
/>
<el-table-column
prop="sales"
label="销量"
sortable="custom"
/>
<el-table-column
prop="stock"
label="库存"
/>
<el-table-column
prop="sort"
label="排序"
/>
<el-table-column
label="状态"
width="150"
>
<template #default="scope">
<el-switch
v-model="scope.row.is_show"
active-color="#13ce66"
inactive-color="#ff4949"
:active-value="1"
:inactive-value="0"
:loading="scope.row.statusLoading"
active-text="上架"
inactive-text="下架"
@change="handleUpdateStatus(scope.row)"
/>
</template>
</el-table-column>
<el-table-column
min-width="120px"
label="操作"
fixed="right"
align="center"
>
<template #default="{ row }">
<el-button type="text">
编辑
</el-button>
<el-button type="text">
查看评论
</el-button>
<el-popconfirm
:title="row.is_del ? '确定恢复商品吗?' : '确定移到回收站吗?'"
@confirm="handleDelete(row.id)"
>
<template #reference>
<el-button type="text">
{{ row.is_del ? '恢复商品' : '移到回收站' }}
</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- <app-pagination
v-model:page="listParams.page"
v-model:limit="listParams.limit"
:list-count="listCount"
:load-list="loadList"
:disabled="listLoading"
/> -->
<el-pagination
layout="total, sizes, prev, pager, next, jumper"
:total="listCount"
:page-sizes="[2, 4, 6]"
:disabled="listLoading"
v-model:currentPage="listParams.page"
v-model:pageSize="listParams.limit"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-card>
<!-- </page-container> -->
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'
import * as productApi from '@/api/product'
import { ElMessage } from 'element-plus'
import type { Product, ProductListParams, ProductType, ProductCategory } from '@/api/types/product'
// import { jsonToExcel } from '@/utils/export-to-excel'
import { Search, Plus, Download } from '@element-plus/icons-vue'
const productTypes = ref<ProductType[]>([])
const productCates = ref<ProductCategory[]>([])
const list = ref<Product[]>([])
const listCount = ref(0)
const listLoading = ref(false)
const listParams = reactive<ProductListParams>({
page: 1,
limit: 10,
cate_id: 0,
type: 0,
store_name: '',
sales: 'normal'
})
const selectionItems = ref<Product[]>([])
const updateProductsShowLoading = ref(false)
const updateProductsUnshowLoading = ref(false)
const exportExcelLoading = ref(false)
onMounted(() => {
loadList()
loadProductCates()
})
const handleSelectionChange = (val: Product[]) => {
selectionItems.value = val
}
const loadList = async () => {
listLoading.value = true
const data = await productApi.getProducts(listParams).finally(() => {
listLoading.value = false
})
data.list.forEach(item => {
item.statusLoading = false
})
list.value = data.list
listCount.value = data.count
// 更新商品类型
loadProductTypes()
}
const loadProductCates = async () => {
const data = await productApi.getCategoryTree(1)
productCates.value = data
}
const loadProductTypes = async () => {
const data = await productApi.getProductTypes()
productTypes.value = data.list
}
const handleQuery = () => {
listParams.page = 1
loadList()
}
const handleUpdateStatus = async (item: Product) => {
item.statusLoading = true
await productApi.updateProductStatus(item.id, item.is_show).finally(() => {
item.statusLoading = false
})
ElMessage.success(`${item.is_show ? '上架' : '下架'}成功`)
loadList()
}
const handleDelete = async (id: number) => {
await productApi.removeProduct(id)
loadList()
}
const handleSortChange = ({ prop, order }: { prop: string, order: 'descending' | 'ascending' | null }) => {
let sales: ProductListParams['sales'] = 'normal'
switch (order) {
case 'ascending':
sales = 'asc'
break
case 'descending':
sales = 'desc'
break
}
listParams.sales = sales
loadList()
}
const handleUpdateProductsShow = async () => { // 批量上架
if (!selectionItems.value.length) {
return ElMessage.warning('请选择商品')
}
updateProductsShowLoading.value = true
await productApi.updateProductsShow(selectionItems.value.map(item => item.id)).finally(() => {
updateProductsShowLoading.value = false
})
ElMessage.success('批量上架成功')
loadList()
}
const handleUpdateProductsUnshow = async () => { // 批量下架
if (!selectionItems.value.length) {
return ElMessage.warning('请选择商品')
}
updateProductsUnshowLoading.value = true
await productApi.updateProductsUnshow(selectionItems.value.map(item => item.id)).finally(() => {
updateProductsUnshowLoading.value = false
})
ElMessage.success('批量下架成功')
loadList()
}
const handleExportExcel = async () => { // 导出表格
if (!selectionItems.value.length) {
return ElMessage.warning('请选择商品')
}
exportExcelLoading.value = true
try {
const { jsonToExcel } = await import('@/utils/export-to-excel')
jsonToExcel({
data: selectionItems.value,
header: {
id: '编号',
store_name: '商品名称',
price: '价格'
},
fileName: '测试.xlsx',
bookType: 'xlsx'
})
} catch (err) {
console.error(err)
}
exportExcelLoading.value = false
}
const handleSizeChange = (size: number) => { // 每页条数
listParams.limit = size
listParams.page = 1
loadList()
}
const handleCurrentChange = (page: number) => { // 当前页数
listParams.page = page
loadList()
}
</script>
<style lang="scss" scoped>
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.demo-table-expand {
font-size: 0;
:deep(label) {
width: 90px;
color: #99a9bf;
}
:deep(.el-form-item) {
margin-right: 0;
margin-bottom: 0;
width: 50%;
}
}
.el-pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>
三、添加商品
⒈ 主页
新建 src / views / product / add / index.vue
文件:
<template>
<!-- <page-container> -->
<app-card>
<template #header>
<!-- icon="el-icon-back" -->
<el-button
@click="$router.back()"
>
<el-icon><arrow-left /></el-icon>
返回
</el-button>
</template>
<el-form
ref="form"
:model="product"
:rules="formRules"
label-width="100px"
>
<el-form-item
label="商品名称"
prop="store_name"
>
<el-input v-model="product.store_name" />
</el-form-item>
<el-form-item
label="商品分类"
prop="cate_id"
>
<el-select
v-model="product.cate_id"
multiple
style="width: 50%;"
>
<el-option
v-for="item in productCates"
:key="item.id"
:label="item.cate_name"
:value="item.id"
:disabled="item.pid === 0"
/>
</el-select>
</el-form-item>
<el-form-item
label="商品关键字"
prop="keyword"
>
<el-input v-model="product.keyword" />
</el-form-item>
<el-form-item
label="单位"
prop="unit_name"
>
<el-input v-model="product.unit_name" />
</el-form-item>
<el-form-item
label="商品简介"
prop="store_info"
>
<el-input
type="textarea"
v-model="product.store_info"
autosize
/>
</el-form-item>
<el-form-item
label="商品封面图(最多1张)"
prop="image"
>
xxx
</el-form-item>
<el-form-item
label="商品推荐图(最多1张)"
prop="recommend_image"
>
xxx
</el-form-item>
<el-form-item
label="商品轮播图(最多10张)"
prop="slider_image"
>
xxx
</el-form-item>
<el-form-item
label="商品规格"
prop="spec_type"
class="auto-scroll"
>
<el-radio-group v-model="product.spec_type">
<el-radio :label="0">
单规格
</el-radio>
<el-radio :label="1">
多规格
</el-radio>
</el-radio-group>
<!-- 单规格 -->
<AttrTable
v-if="product.spec_type === 0"
v-model="singleAttrData"
/>
</el-form-item>
<el-form-item
v-if="product.spec_type === 1"
class="multi-attr-form_item"
label="规格模板"
>
<el-space
direction="vertical"
fill
style="width: 100%;"
alignment="flex-start"
>
<AttrTemplate @confirm="attrTpl = $event" />
<AttrEdit
v-if="attrTpl.length"
v-model="attrTpl"
@confirm="handleAttrEditConfirm"
/>
<template v-if="multiAttrData.length">
<div>
批量设置:
<AttrTable
v-model="batchData"
>
<template #append>
<el-table-column
label="操作"
fixed="right"
min-width="120"
>
<template #default>
<el-button
type="text"
@click="handleBatchSet"
>
批量设置
</el-button>
<el-button
type="text"
@click="handleClearBatch"
>
清除
</el-button>
</template>
</el-table-column>
</template>
</AttrTable>
</div>
<div>商品属性</div>
<AttrTable
v-model="multiAttrData"
>
<template #prepend>
<el-table-column
v-for="item in tableHeader"
:key="item.key"
:prop="item.key"
:label="item.title"
/>
</template>
<template #append>
<el-table-column
label="操作"
fixed="right"
>
<template #default="{ $index }">
<el-button
type="text"
@click="handleDeleteAttr($index)"
>
删除
</el-button>
</template>
</el-table-column>
</template>
</AttrTable>
</template>
</el-space>
</el-form-item>
<el-form-item
label="商品详情"
prop="description"
>
<app-text-editor v-model="product.description" />
</el-form-item>
<el-form-item
label="虚拟销量"
prop="ficti"
>
<el-input-number
v-model="product.ficti"
:min="0"
label="请输入虚拟销量"
/>
</el-form-item>
<el-form-item
label="额外赠送积分"
prop="give_integral"
>
<el-input-number
v-model="product.give_integral"
:min="0"
label="请输入额外赠送积分"
/>
</el-form-item>
<el-form-item
label="排序"
prop="sort"
>
<el-input-number
v-model="product.sort"
:min="0"
label="请输入排序"
/>
</el-form-item>
<el-form-item
label="商品状态"
prop="is_show"
>
<el-radio-group v-model="product.is_show">
<el-radio :label="1">
上架
</el-radio>
<el-radio :label="0">
下架
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="热卖单品"
prop="is_hot"
>
<el-radio-group v-model="product.is_hot">
<el-radio :label="1">
开启
</el-radio>
<el-radio :label="0">
关闭
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="促销单品"
prop="is_benefit"
>
<el-radio-group v-model="product.is_benefit">
<el-radio :label="1">
开启
</el-radio>
<el-radio :label="0">
关闭
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="精品推荐"
prop="is_best"
>
<el-radio-group v-model="product.is_best">
<el-radio :label="1">
开启
</el-radio>
<el-radio :label="0">
关闭
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="首发新品"
prop="is_new"
>
<el-radio-group v-model="product.is_new">
<el-radio :label="1">
开启
</el-radio>
<el-radio :label="0">
关闭
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="优品推荐"
prop="is_good"
>
<el-radio-group v-model="product.is_good">
<el-radio :label="1">
开启
</el-radio>
<el-radio :label="0">
关闭
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="活动优先级"
prop="activity"
>
<el-space>
<!-- 拖拽元素列表和 v-model 的数据必须一致 -->
<app-draggable
v-model="activities"
:options="{
animation: 300
}"
>
<el-tag
v-for="item in activities"
:key="item.name"
:type="item.type"
effect="dark"
>
{{ item.name }}
</el-tag>
</app-draggable>
</el-space>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handleSubmit"
>
保存
</el-button>
</el-form-item>
</el-form>
</app-card>
<!-- </page-container> -->
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import { getCategoryTree, saveProduct } from '@/api/product'
import type { ProductAttr, ProductCategory, AttrRuleValue, AttrTableHeader } from '@/api/types/product'
import type { IElForm } from '@/types/element-plus'
import { ElMessage } from 'element-plus'
import AttrTable from './AttrTable.vue'
import AttrTemplate from './AttrTemplate.vue'
import AttrEdit from './AttrEdit.vue'
import { ArrowLeft } from '@element-plus/icons-vue'
const attrTpl = ref<AttrRuleValue[]>([])
const activities = ref([
{ type: 'danger', name: '秒杀' },
{ type: 'info', name: '默认' },
{ type: 'warning', name: '砍价' },
{ type: 'success', name: '拼团' }
])
const productCates = ref<ProductCategory[]>([]) // 商品分类
const product = ref({
activity: computed(() => activities.value.map(item => item.name)),
attrs: [] as ProductAttr[], // 商品规格
cate_id: [] as number[],
command_word: '',
couponName: [],
coupon_ids: [],
description: '',
ficti: 0,
give_integral: 0,
header: [] as AttrTableHeader[],
id: 0,
image: 'https://shop.fed.lagou.com/uploads/attach/2021/07/20210727/82b80d1996848be8294c6aaa609c4f0b.jpg',
is_benefit: 0,
is_best: 0,
is_good: 0,
is_hot: 0,
is_new: 0,
is_postage: 0,
is_show: 1,
is_sub: [],
items: [] as AttrRuleValue[],
keyword: '',
label_id: [],
recommend_image: 'https://shop.fed.lagou.com/uploads/attach/2021/07/20210727/82b80d1996848be8294c6aaa609c4f0b.jpg',
selectRule: '',
slider_image: [
'https://shop.fed.lagou.com/uploads/attach/2021/07/20210719/512f2ee75f883f46e718bd9496edcc22.jpg',
'https://shop.fed.lagou.com/uploads/attach/2021/07/20210719/512f2ee75f883f46e718bd9496edcc22.jpg',
'https://shop.fed.lagou.com/uploads/attach/2021/07/20210719/512f2ee75f883f46e718bd9496edcc22.jpg',
'https://shop.fed.lagou.com/uploads/attach/2021/07/20210719/512f2ee75f883f46e718bd9496edcc22.jpg',
'https://shop.fed.lagou.com/uploads/attach/2021/07/20210719/512f2ee75f883f46e718bd9496edcc22.jpg'
],
sort: 0,
spec_type: 0 as 0 | 1, // 0 单规格、1 多规格
store_info: '',
store_name: '',
temp_id: '',
unit_name: '',
video_link: ''
})
const singleAttrData = ref([{
pic: '',
vip_price: 0,
price: 0,
cost: 0,
ot_price: 0,
stock: 0,
bar_code: '',
weight: 0,
volume: 0,
brokerage: 0,
brokerage_two: 0
}])
const multiAttrData = ref<ProductAttr[]>([])
watch([singleAttrData, multiAttrData, () => product.value.spec_type], () => {
product.value.attrs = product.value.spec_type === 0
? singleAttrData.value
: multiAttrData.value
}, {
immediate: true, // 立即执行
deep: true // 深度监视
})
const defaultAttrData = [{
pic: '',
vip_price: 0,
price: 0,
cost: 0,
ot_price: 0,
stock: 0,
bar_code: '',
weight: 0,
volume: 0,
brokerage: 0,
brokerage_two: 0
}]
const batchData = ref(JSON.parse(JSON.stringify(defaultAttrData)))
const formRules = ref({
store_name: [
{ required: true, message: '请输入商品名称', trigger: 'blur' }
],
cate_id: [
{ required: true, message: '请选择商品分类', trigger: 'change', type: 'array', min: '1' }
],
keyword: [
{ required: true, message: '请输入商品关键字', trigger: 'blur' }
],
unit_name: [
{ required: true, message: '请输入单位', trigger: 'blur' }
],
store_info: [
{ required: true, message: '请输入商品简介', trigger: 'blur' }
],
image: [
{ required: true, message: '请上传商品图', trigger: 'change' }
],
slider_image: [
{ required: true, message: '请上传商品轮播图', type: 'array', trigger: 'change' }
],
spec_type: [
{ required: true, message: '请选择商品规格', trigger: 'change' }
],
selectRule: [
{ required: true, message: '请选择商品规格属性', trigger: 'change' }
],
temp_id: [
{ required: true, message: '请选择运费模板', trigger: 'change', type: 'number' }
],
give_integral: [
{ type: 'integer', message: '请输入整数' }
]
})
const form = ref<IElForm | null>(null)
onMounted(() => {
loadCates()
})
const handleSubmit = async () => {
const valid = await form.value?.validate()
if (!valid) return false
await saveProduct(0, product.value)
ElMessage.success('保存成功')
}
const loadCates = async () => {
const data = await getCategoryTree(1)
productCates.value = data
}
const handleAttrEditConfirm = (data: {
attr: AttrRuleValue[]
header: AttrTableHeader[]
value: ProductAttr[]
}) => {
multiAttrData.value = data.value
product.value.header = data.header
product.value.items = data.attr
}
const tableHeader = computed(() => {
return product.value.header.filter(item => item.key && item.key.startsWith('value'))
})
const handleDeleteAttr = (index: number) => {
multiAttrData.value.splice(index, 1)
}
const handleBatchSet = () => {
// 过滤无效数据
const data = batchData.value[0]
const validData: Record<string, any> = {}
let key: keyof typeof data
for (key in data) {
if (data[key]) {
validData[key] = data[key]
}
}
// 批量设置 multiAttrData
multiAttrData.value.forEach(item => {
Object.assign(item, validData)
})
}
const handleClearBatch = () => {
batchData.value = JSON.parse(JSON.stringify(defaultAttrData))
}
</script>
<!-- <script lang="ts">
export default {
name: 'ProductNew'
}
</script> -->
<style lang="scss" scoped>
:deep(.el-form-item__content) {
overflow: hidden;
}
:deep(.el-space) {
max-width: 100%;
.el-space__item {
max-width: 100%;
}
}
</style>
⒉ 组件 编辑
新建 src / views / product / add / AttrEdit.vue
文件:
<template>
<el-form
label-position="left"
label-width="50px"
>
<!-- v-model="props.modelValue" -->
<app-draggable
:v-model="props.modelValue"
:options="{
handle: '.el-icon-menu'
}"
>
<el-form-item
v-for="(item, index) in props.modelValue"
:key="item.value"
:label="item.value"
>
<template #label>
<i class="el-icon-menu" />
</template>
<div>
<!-- @close="props.modelValue.splice(index, 1)" -->
<el-tag
closable
effect="dark"
@close="handleTagClose(index)"
>
{{ item.value }}
</el-tag>
</div>
<div>
<app-draggable
style="display: inline-block;"
v-model="item.detail"
>
<el-tag
class="detail-item"
v-for="(detail, detailIndex) in item.detail"
:key="detail"
closable
effect="plain"
@close="item.detail.splice(detailIndex, 1)"
>
{{ detail }}
</el-tag>
</app-draggable>
<el-input
class="input-new-tag"
v-if="item.inputVisible"
v-model="item.inputValue"
ref="saveTagInput"
size="small"
@keyup.enter.prevent="handleInputConfirm(item)"
@blur.prevent="handleInputConfirm(item)"
/>
<el-button
v-else
class="button-new-tag"
size="small"
@click="showInput(item)"
>
+ New Tag
</el-button>
</div>
</el-form-item>
</app-draggable>
<el-form-item v-if="!isAdd">
<el-button
type="primary"
@click="isAdd = true"
>
添加新规格
</el-button>
<el-button
type="success"
@click="handleGenerate"
>
立即生成
</el-button>
</el-form-item>
<el-form-item v-else>
<el-form
:model="attrForm"
:rules="formRules"
ref="form"
inline
>
<el-form-item
label="规格名称"
prop="value"
>
<el-input v-model="attrForm.value" />
</el-form-item>
<el-form-item
label="规格值"
prop="detail"
>
<el-input v-model="attrForm.detail[0]" />
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handleAddAttr"
>
确认
</el-button>
<el-button @click="isAdd = false">
取消
</el-button>
</el-form-item>
</el-form>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { nextTick, ref } from 'vue'
import type { PropType } from 'vue'
import type { AttrRuleValue } from '@/api/types/product'
import { generateAttr } from '@/api/product'
import type { IElForm } from '@/types/element-plus'
const emit = defineEmits(['confirm', 'update:model-value'])
const props = defineProps({
modelValue: {
type: Array as PropType<AttrRuleValue[]>,
default: () => []
}
})
const saveTagInput = ref<HTMLInputElement | null>(null)
const attrForm = ref({
value: '',
detail: ['']
})
const isAdd = ref(false)
const formRules = {
value: [
{ required: true, message: '请输入规则名称', trigger: 'change' }
],
detail: [
{ required: true, message: '请输入规则值', trigger: 'change' }
]
}
const form = ref<IElForm | null>(null)
const handleGenerate = async () => {
const data = await generateAttr(0, 1, {
attrs: props.modelValue
})
emit('confirm', data.info)
}
const handleInputConfirm = (item: AttrRuleValue) => {
if (item.inputValue?.length) {
item.detail.push(item.inputValue)
}
item.inputVisible = false
item.inputValue = ''
}
const showInput = async (item: AttrRuleValue) => {
item.inputVisible = true
await nextTick()
saveTagInput.value?.focus()
}
const handleAddAttr = async () => {
const valid = await form.value?.validate()
if (!valid) return
// eslint-disable-next-line vue/no-mutating-props
props.modelValue.push({
value: attrForm.value.value,
detail: attrForm.value.detail,
inputVisible: false,
inputValue: ''
})
isAdd.value = false
form.value?.resetFields()
}
const handleTagClose = (index: number) => {
// eslint-disable-next-line vue/no-mutating-props
props.modelValue.splice(index, 1)
}
</script>
<style lang="scss" scoped>
.el-icon-menu {
font-size: 20px;
cursor: move;
}
.detail-item {
margin-right: 10px;
}
.el-form-item {
align-items: center;
}
.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;
}
</style>
⒊ 组件 模板
新建 src / views / product / add / AttrTemplate.vue
文件:
<template>
<el-space>
<el-select
v-model="value"
placeholder="请选择"
>
<el-option
v-for="item in options"
:key="item.id"
:label="item.rule_name"
:value="item.id"
/>
</el-select>
<el-button
type="primary"
@click="handleConfirm"
>
确定
</el-button>
<el-button>添加规格模板</el-button>
</el-space>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { getAttrs } from '@/api/product'
import type { ProductAttrTpl, AttrRuleValue } from '@/api/types/product'
interface EmitsType {
(e: 'confirm', value: AttrRuleValue[]): void
}
const emit = defineEmits<EmitsType>()
const value = ref<number | null>(null)
const options = ref<ProductAttrTpl[]>([])
onMounted(() => {
loadAttrs()
})
const loadAttrs = async () => {
const data = await getAttrs()
options.value = data
}
const handleConfirm = () => {
if (value.value) {
const item = options.value.find(item => item.id === value.value)
if (item) {
emit('confirm', item.rule_value)
}
}
}
</script>
<style lang="scss" scoped></style>
⒋ 组件 表格
新建 src / views / product / add / AttrTable.vue
文件:
<template>
<el-table
:data="props.modelValue"
border
>
<slot name="prepend" />
<el-table-column
label="图片"
min-width="100"
>
<template #default="{ row }">
<el-input v-model="row.pic" />
</template>
</el-table-column>
<el-table-column
label="售价"
min-width="155"
>
<template #default="{ row }">
<el-input-number
v-model.number="row.price"
controls-position="right"
:min="0"
/>
</template>
</el-table-column>
<el-table-column
min-width="155"
label="成本价"
>
<template
#default="{ row }"
>
<el-input-number
v-model.number="row.cost"
controls-position="right"
:min="0"
/>
</template>
</el-table-column>
<el-table-column
min-width="155"
label="原价"
>
<template #default="{ row }">
<el-input-number
v-model.number="row.ot_price"
controls-position="right"
:min="0"
/>
</template>
</el-table-column>
<el-table-column
min-width="155"
label="库存"
>
<template #default="{ row }">
<el-input-number
v-model.number="row.stock"
controls-position="right"
:min="0"
/>
</template>
</el-table-column>
<el-table-column
label="商品编号"
min-width="155"
>
<template #default="{ row }">
<el-input v-model="row.bar_code" />
</template>
</el-table-column>
<el-table-column
min-width="155"
label="重量(KG)"
>
<template #default="{ row }">
<el-input-number
v-model.number="row.weight"
controls-position="right"
:min="0"
/>
</template>
</el-table-column>
<el-table-column
min-width="155"
label="体积(m³)"
>
<template #default="{ row }">
<el-input-number
v-model.number="row.volume"
controls-position="right"
:min="0"
/>
</template>
</el-table-column>
<slot name="append" />
</el-table>
</template>
<script lang="ts" setup>
// import { ref } from 'vue'
import type { PropType } from 'vue'
import type { ProductAttr } from '@/api/types/product'
const props = defineProps({
modelValue: {
type: Array as PropType<ProductAttr[]>,
default: () => []
}
})
// const data = ref([])
</script>
<style lang="scss" scoped>
</style>
⒌ 插件
// 安装
npm i wangeditor --save
// 安装
npm i -S vuedraggable