Vue3+Vite+TS后台项目 ~ 10.商品管理

唯有热爱,可抵岁月长河


一、商品列表

⒈ 接口封装

新建 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>

⒌ 插件

wangEditor 富文本编辑器

// 安装
npm i wangeditor --save

拖拽功能

// 安装
npm i -S vuedraggable

⒍ 页面展示

在这里插入图片描述



四、商品规格

### ⒈ 主页新建  文件:


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

后海 0_o

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值