唯有热爱,可抵岁月长河

一、商品列表

⒈ 接口封装

新建 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: numberlist: 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: numbername: string}[]cateList: {value: numberlabel: stringdisabled: number}[]productInfo: {cate_id: string[]is_sub: number[]activity: string[]label_id: string[]coupons: string[]description: stringitems: string[]attrs: string[]attr: {pic: stringvip_price: numberprice: numbercost: numberot_price: numberstock: numberbar_code: stringweight: numbervolume: numberbrokerage: numberbrokerage_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: numbermer_id: numberimage: stringrecommend_image: stringslider_image: string[]store_name: stringstore_info: stringkeyword: stringbar_code: stringcate_id: stringprice: stringvip_price: stringot_price: stringpostage: stringunit_name: stringsort: numbersales: numberstock: stringis_show: numberis_hot: numberis_benefit: numberis_best: numberis_new: numberadd_time: numberis_postage: numberis_del: numbermer_use: numbergive_integral: stringcost: stringis_seckill: numberis_bargain: nullis_good: numberis_sub: numberis_vip: numberficti: numberbrowse: numbercode_path: stringsoure_link: stringvideo_link: stringtemp_id: numberspec_type: numberactivity: stringspu: stringlabel_id: stringcommand_word: stringcollect: numberlikes: numbervisitor: numbercate_name: stringstock_attr: booleanstatusLoading?: boolean
}export interface ProductType {type: numbername: stringcount: number
}export interface ProductListParams {page?: numberlimit?: numbercate_id?: numbertype?: 0 | 1 | 2 | 3 | 4 | 5 | 6store_name?: stringsales?: 'normal' | 'desc' | 'asc'
}export interface ProductCategory {add_time: stringbig_pic: stringcate_name: stringhtml: stringid: numberis_show: 0 | 1pic: stringpid: numbersort: number
}export interface AttrRuleValue {value: stringdetail: string[]inputVisible?: booleaninputValue?: string
}export interface ProductAttrTpl {id: numberattr_name: stringrule_name: stringattr_value: string[]rule_value: AttrRuleValue[]
}export interface IExpressTemplate {id: numbername: string
}export interface AttrTableHeader {align: stringkey: stringminWidth: numbertitle: string
}export type ProductAttr = Record<string, any> & {pic: stringvip_price: numberprice: numbercost: numberot_price: numberstock: numberbar_code: stringweight: numbervolume: numberbrokerage: numberbrokerage_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-formref="form":model="listParams":disabled="listLoading"label-width="70px"@submit.prevent="handleQuery"><el-form-item label="商品分类"><el-selectv-model="listParams.cate_id"placeholder="请选择"clearable@change="loadList"><el-optionlabel="全部":value="0"/><el-optionv-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-inputv-model="listParams.store_name"placeholder="请输入商品名称关键字"clearablestyle="width: 300px;"><template #append><!-- <el-buttonicon="el-icon-search"@click="loadList"/> --><el-icon><search /></el-icon></template></el-input></el-form-item><el-form-item label="商品类目"><el-radio-groupv-model="listParams.type"@change="loadList"><el-radio :label="0">全部</el-radio><el-radiov-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-buttontype="primary"@click="$router.push('/admin/product/add_product')"><el-icon><plus /></el-icon>添加商品</el-button><el-buttonv-if="listParams.type === 2":loading="updateProductsShowLoading"@click="handleUpdateProductsShow">批量上架</el-button><el-buttonv-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-formlabel-position="left"inlineclass="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-columntype="selection"width="55"/><el-table-columnprop="id"label="商品ID"/><el-table-columnprop="id"label="商品图片"><template #default="scope"><el-imagestyle="width: 100px; height: 100px":src="scope.row.image":preview-src-list="[scope.row.image]"/></template></el-table-column><el-table-columnprop="store_name"label="商品名称"/><el-table-columnprop="price"label="商品售价"/><el-table-columnprop="sales"label="销量"sortable="custom"/><el-table-columnprop="stock"label="库存"/><el-table-columnprop="sort"label="排序"/><el-table-columnlabel="状态"width="150"><template #default="scope"><el-switchv-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-columnmin-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-paginationv-model:page="listParams.page"v-model:limit="listParams.limit":list-count="listCount":load-list="loadList":disabled="listLoading"/> --><el-paginationlayout="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 = trueconst data = await productApi.getProducts(listParams).finally(() => {listLoading.value = false})data.list.forEach(item => {item.statusLoading = false})list.value = data.listlistCount.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 = 1loadList()
}const handleUpdateStatus = async (item: Product) => {item.statusLoading = trueawait 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'breakcase 'descending':sales = 'desc'break}listParams.sales = salesloadList()
}const handleUpdateProductsShow = async () => { // 批量上架if (!selectionItems.value.length) {return ElMessage.warning('请选择商品')}updateProductsShowLoading.value = trueawait 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 = trueawait productApi.updateProductsUnshow(selectionItems.value.map(item => item.id)).finally(() => {updateProductsUnshowLoading.value = false})ElMessage.success('批量下架成功')loadList()
}const handleSizeChange = (size: number) => { // 每页条数listParams.limit = sizelistParams.page = 1loadList()
}const handleCurrentChange = (page: number) => { // 当前页数listParams.page = pageloadList()
}</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: stringbookType: xlsx.BookType
}) => {// 1、创建一个工作簿 workbookconst wb = xlsx.utils.book_new()// 2、创建工作表 worksheetif (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-formref="form":model="listParams":disabled="listLoading"label-width="70px"@submit.prevent="handleQuery"><el-form-item label="商品分类"><el-selectv-model="listParams.cate_id"placeholder="请选择"clearable@change="loadList"><el-optionlabel="全部":value="0"/><el-optionv-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-inputv-model="listParams.store_name"placeholder="请输入商品名称关键字"clearablestyle="width: 300px;"><template #append><!-- <el-buttonicon="el-icon-search"@click="loadList"/> --><el-icon><search /></el-icon></template></el-input></el-form-item><el-form-item label="商品类目"><el-radio-groupv-model="listParams.type"@change="loadList"><el-radio :label="0">全部</el-radio><el-radiov-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-buttontype="primary"@click="$router.push('/admin/product/add_product')"><el-icon><plus /></el-icon>添加商品</el-button><el-buttonv-if="listParams.type === 2":loading="updateProductsShowLoading"@click="handleUpdateProductsShow">批量上架</el-button><el-buttonv-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-formlabel-position="left"inlineclass="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-columntype="selection"width="55"/><el-table-columnprop="id"label="商品ID"/><el-table-columnprop="id"label="商品图片"><template #default="scope"><el-imagestyle="width: 100px; height: 100px":src="scope.row.image":preview-src-list="[scope.row.image]"/></template></el-table-column><el-table-columnprop="store_name"label="商品名称"/><el-table-columnprop="price"label="商品售价"/><el-table-columnprop="sales"label="销量"sortable="custom"/><el-table-columnprop="stock"label="库存"/><el-table-columnprop="sort"label="排序"/><el-table-columnlabel="状态"width="150"><template #default="scope"><el-switchv-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-columnmin-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-paginationv-model:page="listParams.page"v-model:limit="listParams.limit":list-count="listCount":load-list="loadList":disabled="listLoading"/> --><el-paginationlayout="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 = trueconst data = await productApi.getProducts(listParams).finally(() => {listLoading.value = false})data.list.forEach(item => {item.statusLoading = false})list.value = data.listlistCount.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 = 1loadList()
}const handleUpdateStatus = async (item: Product) => {item.statusLoading = trueawait 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'breakcase 'descending':sales = 'desc'break}listParams.sales = salesloadList()
}const handleUpdateProductsShow = async () => { // 批量上架if (!selectionItems.value.length) {return ElMessage.warning('请选择商品')}updateProductsShowLoading.value = trueawait 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 = trueawait 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 = truetry {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 = sizelistParams.page = 1loadList()
}const handleCurrentChange = (page: number) => { // 当前页数listParams.page = pageloadList()
}</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-formref="form":model="product":rules="formRules"label-width="100px"><el-form-itemlabel="商品名称"prop="store_name"><el-input v-model="product.store_name" /></el-form-item><el-form-itemlabel="商品分类"prop="cate_id"><el-selectv-model="product.cate_id"multiplestyle="width: 50%;"><el-optionv-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-itemlabel="商品关键字"prop="keyword"><el-input v-model="product.keyword" /></el-form-item><el-form-itemlabel="单位"prop="unit_name"><el-input v-model="product.unit_name" /></el-form-item><el-form-itemlabel="商品简介"prop="store_info"><el-inputtype="textarea"v-model="product.store_info"autosize/></el-form-item><el-form-itemlabel="商品封面图(最多1张)"prop="image">xxx</el-form-item><el-form-itemlabel="商品推荐图(最多1张)"prop="recommend_image">xxx</el-form-item><el-form-itemlabel="商品轮播图(最多10张)"prop="slider_image">xxx</el-form-item><el-form-itemlabel="商品规格"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><!-- 单规格 --><AttrTablev-if="product.spec_type === 0"v-model="singleAttrData"/></el-form-item><el-form-itemv-if="product.spec_type === 1"class="multi-attr-form_item"label="规格模板"><el-spacedirection="vertical"fillstyle="width: 100%;"alignment="flex-start"><AttrTemplate @confirm="attrTpl = $event" /><AttrEditv-if="attrTpl.length"v-model="attrTpl"@confirm="handleAttrEditConfirm"/><template v-if="multiAttrData.length"><div>批量设置:<AttrTablev-model="batchData"><template #append><el-table-columnlabel="操作"fixed="right"min-width="120"><template #default><el-buttontype="text"@click="handleBatchSet">批量设置</el-button><el-buttontype="text"@click="handleClearBatch">清除</el-button></template></el-table-column></template></AttrTable></div><div>商品属性</div><AttrTablev-model="multiAttrData"><template #prepend><el-table-columnv-for="item in tableHeader":key="item.key":prop="item.key":label="item.title"/></template><template #append><el-table-columnlabel="操作"fixed="right"><template #default="{ $index }"><el-buttontype="text"@click="handleDeleteAttr($index)">删除</el-button></template></el-table-column></template></AttrTable></template></el-space></el-form-item><el-form-itemlabel="商品详情"prop="description"><app-text-editor v-model="product.description" /></el-form-item><el-form-itemlabel="虚拟销量"prop="ficti"><el-input-numberv-model="product.ficti":min="0"label="请输入虚拟销量"/></el-form-item><el-form-itemlabel="额外赠送积分"prop="give_integral"><el-input-numberv-model="product.give_integral":min="0"label="请输入额外赠送积分"/></el-form-item><el-form-itemlabel="排序"prop="sort"><el-input-numberv-model="product.sort":min="0"label="请输入排序"/></el-form-item><el-form-itemlabel="商品状态"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-itemlabel="热卖单品"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-itemlabel="促销单品"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-itemlabel="精品推荐"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-itemlabel="首发新品"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-itemlabel="优品推荐"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-itemlabel="活动优先级"prop="activity"><el-space><!-- 拖拽元素列表和 v-model 的数据必须一致 --><app-draggablev-model="activities":options="{animation: 300}"><el-tagv-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-buttontype="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 falseawait 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.valueproduct.value.header = data.headerproduct.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 datafor (key in data) {if (data[key]) {validData[key] = data[key]}}// 批量设置 multiAttrDatamultiAttrData.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-formlabel-position="left"label-width="50px"><!-- v-model="props.modelValue" --><app-draggable:v-model="props.modelValue":options="{handle: '.el-icon-menu'}"><el-form-itemv-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-tagclosableeffect="dark"@close="handleTagClose(index)">{{ item.value }}</el-tag></div><div><app-draggablestyle="display: inline-block;"v-model="item.detail"><el-tagclass="detail-item"v-for="(detail, detailIndex) in item.detail":key="detail"closableeffect="plain"@close="item.detail.splice(detailIndex, 1)">{{ detail }}</el-tag></app-draggable><el-inputclass="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-buttonv-elseclass="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-buttontype="primary"@click="isAdd = true">添加新规格</el-button><el-buttontype="success"@click="handleGenerate">立即生成</el-button></el-form-item><el-form-item v-else><el-form:model="attrForm":rules="formRules"ref="form"inline><el-form-itemlabel="规格名称"prop="value"><el-input v-model="attrForm.value" /></el-form-item><el-form-itemlabel="规格值"prop="detail"><el-input v-model="attrForm.detail[0]" /></el-form-item><el-form-item><el-buttontype="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 = falseitem.inputValue = ''
}const showInput = async (item: AttrRuleValue) => {item.inputVisible = trueawait nextTick()saveTagInput.value?.focus()
}const handleAddAttr = async () => {const valid = await form.value?.validate()if (!valid) return// eslint-disable-next-line vue/no-mutating-propsprops.modelValue.push({value: attrForm.value.value,detail: attrForm.value.detail,inputVisible: false,inputValue: ''})isAdd.value = falseform.value?.resetFields()
}const handleTagClose = (index: number) => {// eslint-disable-next-line vue/no-mutating-propsprops.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-selectv-model="value"placeholder="请选择"><el-optionv-for="item in options":key="item.id":label="item.rule_name":value="item.id"/></el-select><el-buttontype="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-columnlabel="图片"min-width="100"><template #default="{ row }"><el-input v-model="row.pic" /></template></el-table-column><el-table-columnlabel="售价"min-width="155"><template #default="{ row }"><el-input-numberv-model.number="row.price"controls-position="right":min="0"/></template></el-table-column><el-table-columnmin-width="155"label="成本价"><template#default="{ row }"><el-input-numberv-model.number="row.cost"controls-position="right":min="0"/></template></el-table-column><el-table-columnmin-width="155"label="原价"><template #default="{ row }"><el-input-numberv-model.number="row.ot_price"controls-position="right":min="0"/></template></el-table-column><el-table-columnmin-width="155"label="库存"><template #default="{ row }"><el-input-numberv-model.number="row.stock"controls-position="right":min="0"/></template></el-table-column><el-table-columnlabel="商品编号"min-width="155"><template #default="{ row }"><el-input v-model="row.bar_code" /></template></el-table-column><el-table-columnmin-width="155"label="重量(KG)"><template #default="{ row }"><el-input-numberv-model.number="row.weight"controls-position="right":min="0"/></template></el-table-column><el-table-columnmin-width="155"label="体积(m³)"><template #default="{ row }"><el-input-numberv-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>

⒌ 插件

wangEditor 富文本编辑器

// 安装
npm i wangeditor --save

拖拽功能

// 安装
npm i -S vuedraggable

⒍ 页面展示


四、商品规格

Vue3+Vite+TS后台项目 ~ 10.商品管理相关推荐

  1. Vue3+Vite+TS后台项目 ~ 4. axios请求封装

    一.axios封装 1. request 请求封装 新建 src / utils / request.ts 文件: import axios from 'axios'const request = a ...

  2. vue3 +vite+ts实战项目添加 eslint + prettier + lint-staged 踩坑指南

    初始化项目 // 创建一个空的 vue3-ts 项目, yarn create vite my-vue-app --template vue-ts // 安装依赖 cd my-vue-app & ...

  3. vue3+vite+ts+echarts项目问题汇总

    一:引入echarts 安装依赖npm install echarts --save . 在main.ts中引入并挂载到实例上 import * as ECharts from 'echarts'co ...

  4. vue3+vite+TS配置项目别名“@/“

    安装依赖 npm i -D @types/node 一.在tsconfig.json里添加 "baseUrl": "./","paths": ...

  5. 【Vue3+Vite+TS项目集成ESlint +Prettier实现代码规范检查和代码格式化】

    目录 前言 创建项目 安装初始化ESlint 安装ESlint: 初始化ESlint: 安装配置Prettier 安装prettier: 配置prettier: 配置VScode保存时自动格式化代码 ...

  6. vue3:vue3+vite+ts+pinia

    一.背景 记录一套技术方案. 二.项目基础 2.1.创建项目 yarn create vite 输入名字后,这里出现了几个选项,不清楚都是干啥的,下来研究 选择后完成 2.2.vite.config. ...

  7. 如何搭建一个完整的Vue3.0 + ts 的项目

    如何搭建一个完整的Vue3.0 + ts 的项目 相信9月18日尤大大的关于Vue3.0的发表演讲大家一定有所关注,现在Vue3.0 也已经进入RC阶段(最终产品的候选版本,如果没有问题则可发布成为正 ...

  8. vue3+vite+antd——后台管理系统——基础模板

    2023年了,让我看看谁还不会用vue3,其实我也不太会.... 不会就学啊,别的不说,潜力无穷.下面就把我从网上找到的一个后台管理系统模板放在下面了,需要自取. 该系统模板还是不太完善,后续完善后, ...

  9. 如何在vue3+vite+ts中使用require

    vue3+vite+ts中不能使用require 之前使用vue2,去动态设置图片src属性时,采用require,但是vue3+vite+ts中使用require,项目能够运行,但浏览器中报错req ...

最新文章

  1. 【ES6】ES6编程规范 编程风格
  2. Java实现线性表-顺序表示和链式表示
  3. Ecplise中怎样导入Maven项目
  4. 一个专业搜索公司关于lucene+solar资料
  5. centos安装Python2.7
  6. 如果连这10个Python缩写都不知道,那你一定是Python新手
  7. B: Break Prime
  8. Julia: ...的作用,有意思!
  9. jsp的知识略解,只作为笔记,不作为知识参考,谢谢
  10. python有向图遍历_python – 获取有向图的所有边对. networkx
  11. 应用架构、业务架构、技术架构和业务流程图详解
  12. HTML2CANVAS 合成图片
  13. 键盘切换不出中文输入法的解决方法
  14. [LaTeX] 将minipage环境中的脚注(footnote)放到正文并添加超链接
  15. ShowType=0,● 交换机命令show interfaces type 0/port_# switchport|trunk用于显示中继连接的配置情况,下面 - 赏学吧...
  16. Java、Python、C++、PHP、JavaScript这5大编程语言,我究竟该选哪个?
  17. 程序设计基础实训报告
  18. 数据增强系列(1)top10数据增强技术:综合指南
  19. C#中File和FileInfo的区别和用法
  20. AV1的五种编码进展

热门文章

  1. Android小技巧 自动关闭输入法软键盘
  2. pandas---文件导入和导出细节详解
  3. Mirror for Samsung TV for mac(三星智能电视投屏软件)
  4. 将图片进行base64 编码后的数据进行读取,以io流的方式传给前台并显示出来并且不断刷新图片
  5. 电商网络推广是干什么,电商网络营销做什么
  6. 迅雷协议分析–多链接资源获取
  7. 用Python优雅地制作动态条形图
  8. MMORPG的常规技能系统
  9. php和android和mysql_如何使用JSON连接Android和PHP Mysql数据库
  10. 机器学习(一):概述