Bladeren bron

feat(goods): 添加商品分类和商品列表功能

- 新增商品分类页面,包括分类列表、新增分类、编辑分类等功能
- 新增商品列表页面,包括商品搜索、分类筛选、审核状态筛选等功能
- 新增商品编辑页面,支持商品信息的详细编辑
- 优化用户登录和登出逻辑,增加对不同用户类型的处理- 调整页面标题为"电商后台管理系统"
tangy 1 maand geleden
bovenliggende
commit
960afa782f

+ 7 - 7
index.html

@@ -4,7 +4,7 @@
     <meta charset="UTF-8" />
     <link rel="icon" href="/favicon.ico" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>后台管理系统</title>
+    <title>电商后台管理系统</title>
     <style>
       * {
         margin: 0;
@@ -17,13 +17,13 @@
         height: 100vh;
         width: 100vw;
       }
-  
+
       .circular {
         height: 42px;
         width: 42px;
         animation: loading-rotate 2s linear infinite;
       }
-  
+
       .circular .path {
         animation: loading-dash 1.5s ease-in-out infinite;
         stroke-dasharray: 90, 150;
@@ -32,20 +32,20 @@
         stroke: #4073fa;
         stroke-linecap: round;
       }
-  
+
       @keyframes loading-rotate {
         100% {
           transform: rotate(1turn);
         }
       }
-  
+
       @keyframes loading-dash {
-  
+
         0% {
           stroke-dasharray: 90, 150;
           stroke-dashoffset: -40px;
         }
-  
+
         100% {
           stroke-dasharray: 90, 150;
           stroke-dashoffset: -120px;

+ 22 - 0
src/api/goods/category.ts

@@ -0,0 +1,22 @@
+import request from '@/utils/request'
+
+// 商品分类列表
+export function goodsCategoryPage(params?: any) {
+    return request.get({url: '/product/listCategory', params})
+}
+
+// 添加商品分类
+export function goodsCategoryAdd(params: any) {
+    return request.post({url: '/product/saveCategory', params})
+}
+
+// 编辑商品分类
+export function goodsCategoryEdit(params: any) {
+    return request.post({url: '/product/saveCategory', params})
+}
+
+// 删除商品分类
+export function goodsCategoryDelete(params: any) {
+    return request.post({url: '/product/saveCategory', params})
+}
+

+ 17 - 0
src/api/goods/list.ts

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+// 商品列表
+export function goodsPage(params?: any) {
+    return request.get({url: '/product/list', params})
+}
+
+// 商品列表
+export function goodsEdit(params?: any) {
+    return request.get({url: '/product/save', params})
+}
+
+// 添加商品
+export function goodsDetail(params: any) {
+    return request.get({url: '/product/queryById', params})
+}
+

+ 8 - 0
src/enums/cacheEnums.ts

@@ -6,3 +6,11 @@ export const TOKEN_KEY = 'token'
 export const ACCOUNT_KEY = 'account'
 //设置
 export const SETTING_KEY = 'setting'
+//设置
+export const LIKE_ADMIN_TOKEN = 'like_admin_token'
+//设置
+export const LIKE_ADMIN_BRANDID = 'like_admin_brandId'
+//设置
+export const LIKE_ADMIN_USERID = 'like_admin_userId'
+//设置
+export const LIKE_ADMIN_HOUSEID = 'like_admin_houseId'

+ 3 - 3
src/layout/default/components/header/user-drop-down.vue

@@ -3,17 +3,17 @@
         <div class="flex items-center">
             <el-avatar :size="34" :src="userInfo.avatar" />
             <div class="ml-3 mr-1">{{ userInfo.nickname }}</div>
-            <!-- <icon name="el-icon-ArrowDown" /> -->
+             <icon name="el-icon-ArrowDown" />
         </div>
 
-        <!-- <template #dropdown>
+         <template #dropdown>
             <el-dropdown-menu>
                 <router-link to="/user/setting">
                     <el-dropdown-item>个人设置</el-dropdown-item>
                 </router-link>
                 <el-dropdown-item command="logout">退出登录</el-dropdown-item>
             </el-dropdown-menu>
-        </template> -->
+        </template>
     </el-dropdown>
 </template>
 

+ 7 - 2
src/stores/modules/user.ts

@@ -57,6 +57,9 @@ const useUserStore = defineStore({
                         window.localStorage.setItem("like_admin_token",data.single.token)
                         window.localStorage.setItem("like_admin_brandId",data.single.brandId)
                         window.localStorage.setItem("like_admin_userId",data.single.dataUserId)
+                        if(data.single.type === 2){
+                            window.localStorage.setItem("like_admin_houseId",data.single.houseList[0].houseId)
+                        }
                         resolve(data.single)
                     })
                     .catch((error) => {
@@ -65,7 +68,7 @@ const useUserStore = defineStore({
             })
         },
         logout() {
-            return new Promise((resolve, reject) => {
+            /*return new Promise((resolve, reject) => {
                 logout()
                     .then(async (data) => {
                         this.token = ''
@@ -76,7 +79,9 @@ const useUserStore = defineStore({
                     .catch((error) => {
                         reject(error)
                     })
-            })
+            })*/
+            clearAuthInfo()
+            router.push(PageEnum.LOGIN)
         },
         getUserInfo() {
             return new Promise((resolve, reject) => {

+ 8 - 23
src/utils/auth.ts

@@ -1,15 +1,5 @@
-import { TOKEN_KEY } from '@/enums/cacheEnums'
-import { resetRouter } from '@/router'
-import useTabsStore from '@/stores/modules/multipleTabs'
-import useUserStore from '@/stores/modules/user'
+import {LIKE_ADMIN_BRANDID, LIKE_ADMIN_TOKEN, LIKE_ADMIN_USERID, LIKE_ADMIN_HOUSEID, TOKEN_KEY} from '@/enums/cacheEnums'
 import cache from './cache'
-import {
-    ElMessage,
-    ElMessageBox,
-    ElNotification,
-    ElLoading,
-    type ElMessageBoxOptions
-} from 'element-plus'
 
 export function getToken() {
     return cache.get(TOKEN_KEY)
@@ -27,22 +17,17 @@ export function getUserId() {
     return cache.get("userId")
 }
 export function clearAuthInfo() {
-    // const userStore = useUserStore()
-    // const tabsStore = useTabsStore()
-    // userStore.resetState()
-    // tabsStore.$reset()
-    // cache.remove(TOKEN_KEY)
-    // cache.remove("brandId")
-    // cache.remove("houseId")
-    // cache.remove("userId")
-    // resetRouter()
-    console.warn("***clearAuthInfo***")
+    cache.remove(TOKEN_KEY)
+    cache.remove(LIKE_ADMIN_TOKEN)
+    cache.remove(LIKE_ADMIN_BRANDID)
+    cache.remove(LIKE_ADMIN_USERID)
+    cache.remove(LIKE_ADMIN_HOUSEID)
     // 确认窗体
-    ElMessageBox.confirm("已经在其他地方登录", '温馨提示', {
+    /*ElMessageBox.confirm("已经在其他地方登录", '温馨提示', {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
         type: 'warning'
     }).then(() => {
         window.close();
-    })
+    })*/
 }

+ 4 - 4
src/views/error/403.vue

@@ -1,12 +1,12 @@
 <template>
     <div class="error404">
-        <!-- <error code="403" title="您的账号权限不足,请联系管理员添加权限!" :show-btn="false">
+         <error code="403" title="您的账号权限不足,请联系管理员添加权限!" :show-btn="false">
             <template #content>
                 <div class="flex justify-center">
                     <img class="w-[150px] h-[150px]" src="@/assets/images/no_perms.png" alt="" />
                 </div>
             </template>
-        </error> -->
+        </error>
     </div>
 </template>
 
@@ -26,10 +26,10 @@ onMounted(() => {
     if (redirectUrl) {
         try {
             const url = new URL(decodeURIComponent(redirectUrl))
-            // const isAllowed = allowedDomains.some((domain: string) => 
+            // const isAllowed = allowedDomains.some((domain: string) =>
             //     url.hostname.endsWith(domain)
             // )
-            
+
             if (url) {
                 setTimeout(() => {
                     window.location.href = url.toString()

+ 88 - 0
src/views/goods/category/edit.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="edit-popup">
+    <popup
+        ref="popupRef"
+        :title="popupTitle"
+        :async="true"
+        width="550px"
+        @confirm="handleSubmit"
+        @close="handleClose"
+    >
+      <el-form ref="formRef" :model="formData" label-width="84px" :rules="formRules">
+        <el-form-item label="分类名称" prop="name">
+          <el-input v-model="formData.name" placeholder="请输入分类名称" clearable maxlength="10"/>
+        </el-form-item>
+        <el-form-item label="排序" prop="sort">
+          <div>
+            <el-input-number v-model="formData.sort" :min="0" :max="9999"/>
+            <div class="form-tips">默认为0, 数值越大越排前</div>
+          </div>
+        </el-form-item>
+        <el-form-item label="状态" prop="isShow">
+          <el-switch v-model="formData.isShow" :active-value="1" :inactive-value="0"/>
+        </el-form-item>
+      </el-form>
+    </popup>
+  </div>
+</template>
+<script lang="ts" setup>
+import type {FormInstance} from 'element-plus'
+import {goodsCategoryAdd, goodsCategoryEdit} from '@/api/goods/category'
+import Popup from '@/components/popup/index.vue'
+import feedback from '@/utils/feedback'
+
+const emit = defineEmits(['success', 'close'])
+const formRef = shallowRef<FormInstance>()
+const popupRef = shallowRef<InstanceType<typeof Popup>>()
+const mode = ref('add')
+const popupTitle = computed(() => {
+  return mode.value == 'edit' ? '编辑分类' : '新增分类'
+})
+const formData = reactive({
+  id: '',
+  name: '',
+  sort: 0,
+  isShow: 1
+})
+
+const formRules = {
+  name: [
+    {
+      required: true,
+      message: '请输入栏目名称',
+      trigger: ['blur']
+    }
+  ]
+}
+
+const handleSubmit = async () => {
+  await formRef.value?.validate()
+  mode.value == 'edit' ? await goodsCategoryEdit(formData) : await goodsCategoryAdd(formData)
+  feedback.msgSuccess('操作成功')
+  popupRef.value?.close()
+  emit('success')
+}
+
+const open = (type = 'add') => {
+  mode.value = type
+  popupRef.value?.open()
+}
+
+const setFormData = (data: Record<any, any>) => {
+  for (const key in formData) {
+    if (data[key] != null && data[key] != undefined) {
+      //@ts-ignore
+      formData[key] = data[key]
+    }
+  }
+}
+
+const handleClose = () => {
+  emit('close')
+}
+
+defineExpose({
+  open,
+  setFormData
+})
+</script>

+ 104 - 0
src/views/goods/category/index.vue

@@ -0,0 +1,104 @@
+<template>
+  <div>
+    <el-card class="!border-none mt-4" shadow="never" v-loading="pager.loading">
+      <div>
+        <el-button v-perms="['goods:category:add']" type="primary" @click="handleAdd()">
+          <template #icon>
+            <icon name="el-icon-Plus"/>
+          </template>
+          新增
+        </el-button>
+      </div>
+      <el-table class="mt-4" size="large" :data="pager.lists">
+        <el-table-column type="selection" width="55"/>
+        <el-table-column
+            label="分类名称"
+            prop="name"
+            min-width="120"
+            show-tooltip-when-overflow
+        />
+        <el-table-column label="商品数" prop="number" min-width="120"/>
+        <el-table-column label="状态" min-width="120">
+          <template #default="{ row }">
+            <el-switch
+                v-perms="['article:cate:change']"
+                v-model="row.isShow"
+                :active-value="1"
+                :inactive-value="0"
+                @change="changeStatus(row.id, row.isShow)"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="排序" prop="sort" min-width="120"/>
+        <el-table-column label="操作" width="120" fixed="right">
+          <template #default="{ row }">
+            <el-button
+                v-perms="['goods:category:edit']"
+                type="primary"
+                link
+                @click="handleEdit(row)"
+            >
+              编辑
+            </el-button>
+            <el-button
+                v-perms="['goods:category:delete']"
+                type="danger"
+                link
+                @click="handleDelete(row.id)"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="flex justify-end mt-4">
+        <pagination v-model="pager" @change="getLists"/>
+      </div>
+    </el-card>
+    <edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false"/>
+  </div>
+</template>
+<script lang="ts" setup name="articleColumn">
+import {usePaging} from '@/hooks/usePaging'
+import feedback from '@/utils/feedback'
+import EditPopup from './edit.vue'
+import {goodsCategoryDelete, goodsCategoryEdit, goodsCategoryPage} from "@/api/goods/category";
+
+const editRef = shallowRef<InstanceType<typeof EditPopup>>()
+const showEdit = ref(false)
+
+const {pager, getLists} = usePaging({
+  fetchFun: goodsCategoryPage
+})
+const handleAdd = async () => {
+  showEdit.value = true
+  await nextTick()
+  editRef.value?.open('add')
+}
+
+const handleEdit = async (data: any) => {
+  showEdit.value = true
+  await nextTick()
+  editRef.value?.open('edit')
+  editRef.value?.setFormData(data)
+}
+
+const handleDelete = async (id: number) => {
+  await feedback.confirm('确定要删除?')
+  await goodsCategoryDelete({id, isDelete: 1})
+  feedback.msgSuccess('删除成功')
+  getLists()
+}
+
+const changeStatus = async (id: number, isShow: number) => {
+  try {
+    await goodsCategoryEdit({id, isShow})
+    feedback.msgSuccess('修改成功')
+    getLists()
+  } catch (error) {
+    getLists()
+  }
+}
+
+getLists()
+</script>

+ 402 - 0
src/views/goods/list/edit.vue

@@ -0,0 +1,402 @@
+<template>
+  <div class="article-edit">
+    <el-card class="!border-none" shadow="never">
+      <el-page-header content="商品编辑" @back="$router.back()"/>
+    </el-card>
+    <el-card class="mt-4 !border-none" shadow="never">
+      <el-form
+          ref="formRef"
+          class="ls-form"
+          :inline="true"
+          :model="formData"
+          label-width="100px"
+          :rules="rules"
+      >
+        <el-divider content-position="left"><span style="font-size: 18px">基础信息</span></el-divider>
+        <div class="xl:flex">
+          <div>
+            <el-form-item label="商品名称" prop="title">
+              <div class="w-32vw">
+                <el-input
+                    v-model="formData.title"
+                    placeholder="请输入商品名称"
+                    clearable
+                />
+              </div>
+            </el-form-item>
+            <el-form-item label="商品分类" prop="title">
+              <div class="w-32vw">
+                <el-select class="w-[100%]" v-model="formData.productCategoryId">
+                  <el-option label="全部" value/>
+                  <el-option
+                      v-for="item in optionsData.goodsCategory"
+                      :key="item.id"
+                      :label="item.name"
+                      :value="item.id"
+                  />
+                </el-select>
+              </div>
+            </el-form-item>
+            <el-form-item label="商品售价" prop="price">
+              <div class="w-32vw">
+                <el-input
+                    v-model="formData.price"
+                    placeholder="请输入商品售价"
+                    clearable
+                />
+              </div>
+            </el-form-item>
+            <el-form-item label="划线价格" prop="originalPrice">
+              <div class="w-32vw">
+                <el-input
+                    v-model="formData.originalPrice"
+                    placeholder="请输入划线价格"
+                    clearable
+                />
+              </div>
+            </el-form-item>
+            <el-form-item label="商品库存" prop="stock">
+              <div class="w-32vw">
+                <el-input-number
+                    v-model="formData.stock"
+                    placeholder="请输入商品库存"
+                    clearable
+                />
+              </div>
+            </el-form-item>
+            <el-form-item label="商品排序" prop="sort">
+              <div class="w-32vw">
+                <el-input-number
+                    v-model="formData.sort"
+                    placeholder="请输入商品排序"
+                    show-word-limit
+                    clearable
+                />
+              </div>
+            </el-form-item>
+            <el-form-item label="商品单位" prop="unit">
+              <div class="w-32vw">
+                <el-input
+                    v-model="formData.unit"
+                    placeholder="请输入商品单位"
+                    show-word-limit
+                    clearable
+                />
+              </div>
+            </el-form-item>
+            <el-form-item label="商品标签" prop="tags">
+              <div class="w-32vw">
+                <el-select
+                    style="width: 100%"
+                    v-model="formData.tags"
+                    multiple
+                    filterable
+                    allow-create
+                    default-first-option
+                    placeholder="选择或创建商品标签">
+                  <el-option
+                      v-for="item in goodsTagOption"
+                      :key="item.value"
+                      :label="item.label"
+                      :value="item.value">
+                  </el-option>
+                </el-select>
+              </div>
+            </el-form-item>
+            <el-form-item label="购买须知" prop="purchaseNotice">
+              <div class="w-32vw">
+                <el-input
+                    v-model="formData.purchaseNotice"
+                    placeholder="请输入购买须知"
+                    show-word-limit
+                    clearable
+                />
+              </div>
+            </el-form-item>
+            <el-form-item label="商品保障" prop="guarantee">
+              <div class="w-32vw">
+                <el-input
+                    v-model="formData.guarantee"
+                    placeholder="请输入商品保障"
+                    show-word-limit
+                    clearable
+                />
+              </div>
+            </el-form-item>
+          </div>
+        </div>
+        <el-divider content-position="left"><span style="font-size: 18px">详细信息</span></el-divider>
+        <div class="xl:flex">
+          <div>
+            <el-form-item label="商品主图" prop="image">
+              <div class="w-32vw">
+                <material-picker v-model="formData.image" :limit="1"/>
+              </div>
+            </el-form-item>
+            <el-form-item label="商品轮播图" prop="bannerImages">
+              <div class="w-32vw">
+                <material-picker v-model="formData.bannerImages" :limit="3"/>
+              </div>
+            </el-form-item>
+            <el-form-item label="商品视频" prop="video">
+              <div class="w-32vw">
+                <material-picker v-model="formData.video" :limit="3"/>
+              </div>
+            </el-form-item>
+          </div>
+        </div>
+        <div class="xl:flex">
+          <el-form-item label="商品详情" prop="content">
+            <div style="width: 100%">
+              <editor v-model="formData.content" :height="500"/>
+            </div>
+          </el-form-item>
+        </div>
+        <!--<el-divider content-position="left"><span style="font-size: 18px">其他</span></el-divider>
+        <div class="xl:flex">
+          <div>
+            <el-form-item label="赠送优惠券" prop="title">
+              <div class="w-32vw">
+                <el-input-number
+                    v-model="formData.summary"
+                    :precision="2"
+                    :min="0.01"
+                    controls-position="right"
+                />
+              </div>
+            </el-form-item>
+            <el-form-item label="已售数量" prop="title">
+              <div class="w-32vw">
+                <el-input-number
+                    v-model="formData.summary"
+                    :precision="2"
+                    :min="0.01"
+                    controls-position="right"
+                />
+              </div>
+            </el-form-item>
+            <el-form-item label="商品推荐" prop="title">
+              <div class="w-32vw">
+                <el-input-number
+                    v-model="formData.summary"
+                    :precision="2"
+                    :min="0.01"
+                    controls-position="right"
+                />
+              </div>
+            </el-form-item>
+            <el-form-item label="会员专属" prop="title">
+              <div class="w-32vw">
+                <el-input-number
+                    v-model="formData.summary"
+                    :precision="2"
+                    :min="0.01"
+                    controls-position="right"
+                />
+              </div>
+            </el-form-item>
+            <el-form-item label="买就送" prop="title">
+              <div class="w-32vw">
+                <el-input-number
+                    v-model="formData.summary"
+                    :precision="2"
+                    :min="0.01"
+                    controls-position="right"
+                />
+              </div>
+            </el-form-item>
+          </div>
+        </div>-->
+      </el-form>
+    </el-card>
+    <footer-btns>
+      <el-button type="primary" @click="handleSave">保存</el-button>
+    </footer-btns>
+  </div>
+</template>
+
+<script lang="ts" setup name="articleListsEdit">
+import type {FormInstance} from 'element-plus'
+import feedback from '@/utils/feedback'
+import {useDictOptions} from '@/hooks/useDictOptions'
+import {articleAdd, articleCateAll, articleDetail, articleEdit} from '@/api/article'
+import useMultipleTabs from '@/hooks/useMultipleTabs'
+import Editor from '@/components/editor/index.vue'
+import {goodsCategoryPage} from "@/api/goods/category";
+
+const route = useRoute()
+const router = useRouter()
+const formData = reactive<any>({
+  id: '',
+  title: '',
+  image: '',
+  cid: '',
+  intro: '',
+  author: '',
+  content: '',
+  visit: 0,
+  sort: 0,
+  isShow: 1,
+  summary: 0,
+  specs: [] // 规格
+})
+const goodsTagOption = [{
+  value: '日用品',
+  label: '日用品'
+}]
+
+const {optionsData} = useDictOptions<{
+  goodsCategory: any[]
+}>({
+  goodsCategory: {
+    api: goodsCategoryPage,
+    params: {pageNo: 1, pageSize: 100},
+    transformData(data: any) {
+      return data.lists
+    }
+  }
+})
+
+const {removeTab} = useMultipleTabs()
+const formRef = shallowRef<FormInstance>()
+const rules = reactive({
+  title: [{required: true, message: '请输入商品名称', trigger: 'blur'}],
+  cid: [{required: true, message: '请选择商品分类', trigger: 'blur'}],
+  summary: [{required: true, message: '请输入价格', trigger: 'blur'}],
+  image: [{required: true, message: '添加商品图片', trigger: 'change'}]
+})
+
+// getDetails 函数修改
+const getDetails = async () => {
+  const data = await articleDetail({
+    id: route.query.id
+  })
+  Object.keys(formData).forEach((key) => {
+    //@ts-ignore
+    formData[key] = data[key];
+    if (key == 'summary') {
+      formData[key] = Number(formData[key])
+    }
+  })
+
+  // 转换规格数据
+  if (data.specsList && data.specsList.length > 0) {
+    const specsMap = new Map()
+    data.specsList.forEach((spec: { name: string; value: string; id: number }) => {
+      if (!specsMap.has(spec.name)) {
+        specsMap.set(spec.name, {
+          name: spec.name,
+          items: [],
+          inputVisible: false,
+          inputValue: ''
+        })
+      }
+      specsMap.get(spec.name).items.push({
+        id: spec.id,
+        tag: spec.value
+      })
+    })
+    formData.specs = Array.from(specsMap.values())
+  }
+}
+
+const handleSave = async () => {
+  await formRef.value?.validate()
+  if (formData.specs && formData.specs.length > 5) {
+    feedback.msgWarning('最多添加5个规格')
+    return
+  }
+  const specsList: Array<{
+    name: string;
+    value: string;
+    status: number;
+    articleId: number | null;
+    id: number | null;
+    img: string | null;
+  }> = [];
+  formData.specs?.forEach((spec: { name: string; items: Array<{ id: number | null, tag: string }> }) => {
+    spec.items.forEach(item => {
+      specsList.push({
+        name: spec.name,
+        value: item.tag,
+        status: 1,
+        articleId: formData.id || null,
+        id: item.id || null,
+        img: null
+      })
+    })
+  })
+
+  const submitData = {
+    ...formData,
+    specsList
+  }
+
+  if (route.query.id) {
+    await articleEdit(submitData)
+  } else {
+    await articleAdd(submitData)
+  }
+  feedback.msgSuccess('操作成功')
+  removeTab()
+  router.back()
+}
+
+route.query.id && getDetails()
+
+// 添加规格
+const handleSpecs = () => {
+  if (!formData.specs) {
+    formData.specs = []
+  }
+  formData.specs.push({
+    name: '',
+    items: [],
+    inputVisible: false,
+    inputValue: ''
+  })
+}
+
+// 删除规格
+const removeSpec = (index: number) => {
+  formData.specs.splice(index, 1)
+}
+
+// 显示输入框
+const showInput = (specIndex: number) => {
+  formData.specs[specIndex].inputVisible = true
+  nextTick(() => {
+    InputRef.value?.input?.focus()
+  })
+}
+
+// 确认输入
+const handleInputConfirm = (specIndex: number) => {
+  const spec = formData.specs[specIndex]
+  if (spec.inputValue) {
+    spec.items.push({
+      id: null,
+      tag: spec.inputValue
+    })
+  }
+  spec.inputVisible = false
+  spec.inputValue = ''
+}
+
+// 删除标签
+const handleClose = (specIndex: number, tagIndex: number) => {
+  formData.specs[specIndex].items.splice(tagIndex, 1)
+}
+
+// 添加规格项
+const handleAddSpecItem = (index: number) => {
+  showInput(index)
+}
+
+const InputRef = ref()
+</script>
+<style>
+.w-32vw {
+  width: 32vw;
+}
+</style>

+ 165 - 0
src/views/goods/list/index.vue

@@ -0,0 +1,165 @@
+<template>
+  <div class="article-lists" v-perms="['goods:list:query']">
+    <el-card class="!border-none" shadow="never">
+      <el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
+        <el-form-item label="商品名称">
+          <el-input
+              class="w-[280px]"
+              v-model="queryParams.title"
+              clearable
+              @keyup.enter="resetPage"
+          />
+        </el-form-item>
+        <el-form-item label="商品分类">
+          <el-select class="w-[280px]" v-model="queryParams.categoryId">
+            <el-option label="全部" value/>
+            <el-option
+                v-for="item in optionsData.goodsCategory"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="商品状态">
+          <el-select class="w-[280px]" v-model="queryParams.auditStatus">
+            <el-option label="全部" value/>
+            <el-option label="待审核" :value="0"/>
+            <el-option label="审核通过" :value="1"/>
+            <el-option label="审核拒绝" :value="2"/>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="resetPage">查询</el-button>
+          <el-button @click="resetParams">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+    <el-card class="!border-none mt-4" shadow="never">
+      <div>
+        <router-link v-perms="['goods:list:add', 'goods:list:add/edit']" :to="{ path: getRoutePath('goods:list:add/edit') }">
+          <el-button class="mr-3" type="primary">
+            <template #icon>
+              <icon name="el-icon-Plus"/>
+            </template>
+            添加商品
+          </el-button>
+        </router-link>
+        <!-- <multi-del-btn></multi-del-btn> -->
+      </div>
+      <el-table class="mt-4" size="large" v-loading="pager.loading" :data="pager.lists">
+        <el-table-column label="ID" type="index" min-width="80"/>
+        <el-table-column label="图片" min-width="100">
+          <template #default="{ row }">
+            <image-contain
+                v-if="row.image"
+                :src="row.image"
+                :width="60"
+                :height="45"
+                :preview-src-list="[row.image]"
+                preview-teleported
+                fit="contain"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column
+            label="商品名称"
+            prop="title"
+            min-width="160"
+            show-tooltip-when-overflow
+        />
+        <el-table-column label="商品分类" prop="categoryName" min-width="150" show-tooltip-when-overflow/>
+        <el-table-column label="售价/元" prop="price" min-width="70"/>
+        <el-table-column
+            label="审核状态"
+            prop="auditStatus"
+            min-width="100"
+            show-tooltip-when-overflow
+        />
+        <el-table-column
+            label="库存"
+            prop="stock"
+            min-width="100"
+            show-tooltip-when-overflow
+        />
+        <el-table-column label="排序" prop="sort" min-width="80"/>
+        <el-table-column label="添加时间" prop="createTime" min-width="120"/>
+        <el-table-column label="操作" width="120" fixed="right">
+          <template #default="{ row }">
+            <el-button v-perms="['goods:list:edit', 'goods:list:add/edit']" type="primary" link>
+              <router-link :to="{ path: getRoutePath('goods:list:add/edit'), query: { id: row.id } }">
+                编辑
+              </router-link>
+            </el-button>
+            <el-button
+                v-perms="['goods:list:delete']"
+                type="danger"
+                link
+                @click="handleDelete(row.id)"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="flex justify-end mt-4">
+        <pagination v-model="pager" @change="getLists"/>
+      </div>
+    </el-card>
+  </div>
+</template>
+<script lang="ts" setup name="goodsPage">
+import {goodsPage} from '@/api/goods/list'
+import {goodsCategoryPage} from '@/api/goods/category'
+import {useDictOptions} from '@/hooks/useDictOptions'
+import {usePaging} from '@/hooks/usePaging'
+import {getRoutePath} from '@/router'
+import feedback from '@/utils/feedback'
+
+const queryParams = reactive({
+  title: '',
+  cid: '',
+  isShow: ''
+})
+const {pager, getLists, resetPage, resetParams} = usePaging({
+  fetchFun: goodsPage,
+  params: queryParams
+})
+
+const {optionsData} = useDictOptions<{
+  goodsCategory: any[]
+}>({
+  goodsCategory: {
+    api: goodsCategoryPage,
+    params: {pageNo: 1, pageSize: 100},
+    transformData(data: any) {
+      return data.lists
+    }
+  }
+})
+
+const handleDelete = async (id: number) => {
+  await feedback.confirm('确定要删除?')
+  // await goodsEdit({id})
+  feedback.msgSuccess('删除成功')
+  getLists()
+}
+
+onActivated(() => {
+  getLists()
+})
+
+getLists()
+</script>
+<style lang="scss" scoped>
+.remark-text {
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  word-break: break-all;
+  line-height: 1.5;
+  max-height: 3em; // 2行的高度
+}
+</style>