Ver Fonte

update:商品列表调整

xuruhua há 1 ano atrás
pai
commit
238cb92da2

BIN
dist/LOGO 2.png


BIN
dist/LOGO-fb 2.png


BIN
dist/favicon 2.ico


BIN
dist/home 2.png


+ 141 - 0
dist/index 2.html

@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta
+      name="keywords"
+      content="芋道管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
+    />
+    <meta
+      name="description"
+      content="芋道管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
+    />
+    <title>中星</title>
+    <script type="module" crossorigin src="/assets/index-82e357b2.js"></script>
+    <link rel="stylesheet" href="/assets/index-8bea8334.css">
+  </head>
+  <body>
+    <div id="app">
+      <style>
+        .app-loading {
+          display: flex;
+          width: 100%;
+          height: 100%;
+          justify-content: center;
+          align-items: center;
+          flex-direction: column;
+          background: #f0f2f5;
+        }
+
+        .app-loading .app-loading-wrap {
+          position: absolute;
+          top: 50%;
+          left: 50%;
+          display: flex;
+          transform: translate3d(-50%, -50%, 0);
+          justify-content: center;
+          align-items: center;
+          flex-direction: column;
+        }
+
+        .app-loading .app-loading-title {
+          margin-bottom: 30px;
+          font-size: 20px;
+          font-weight: bold;
+          text-align: center;
+        }
+
+        .app-loading .app-loading-logo {
+          width: 100px;
+          margin: 0 auto 15px auto;
+          animation: app-loading-logo 5s cubic-bezier(0.42, 0.61, 0.58, 0.41) infinite;
+        } 
+
+        @keyframes app-loading-logo {
+          0% {
+            transform: rotate(0deg);
+          }
+
+          100% {
+            transform: rotate(360deg);
+          }
+        } 
+
+        .app-loading .app-loading-item {
+          position: relative;
+          display: inline-block;
+          width: 60px;
+          height: 60px;
+          vertical-align: middle;
+          border-radius: 50%;
+        }
+
+        .app-loading .app-loading-outter {
+          position: absolute;
+          width: 100%;
+          height: 100%;
+          border: 4px solid #2d8cf0;
+          border-bottom: 0;
+          border-left-color: transparent;
+          border-radius: 50%;
+          animation: loader-outter 1s cubic-bezier(0.42, 0.61, 0.58, 0.41) infinite;
+        }
+
+        .app-loading .app-loading-inner {
+          position: absolute;
+          top: calc(50% - 20px);
+          left: calc(50% - 20px);
+          width: 40px;
+          height: 40px;
+          border: 4px solid #87bdff;
+          border-right: 0;
+          border-top-color: transparent;
+          border-radius: 50%;
+          animation: loader-inner 1s cubic-bezier(0.42, 0.61, 0.58, 0.41) infinite;
+        }
+
+         
+
+        /* @-webkit-keyframes loader-inner {
+          0% {
+            -webkit-transform: rotate(0deg);
+            transform: rotate(0deg);
+          }
+
+          100% {
+            -webkit-transform: rotate(-360deg);
+            transform: rotate(-360deg);
+          }
+        } */
+
+        /* @keyframes loader-inner {
+          0% {
+            -webkit-transform: rotate(0deg);
+            transform: rotate(0deg);
+          }
+
+          100% {
+            -webkit-transform: rotate(-360deg);
+            transform: rotate(-360deg);
+          }
+        } */
+      </style>
+      <div class="app-loading">
+        <div class="app-loading-wrap">
+          <div class="app-loading-title">
+            <img src="/zxlogo.png" class="app-loading-logo" alt="Logo" />
+            <div class="app-loading-title">中星</div>
+          </div>
+          <!-- <div class="app-loading-item">
+            <div class="app-loading-outter"></div>
+            <div class="app-loading-inner"></div>
+          </div> -->
+        </div>
+      </div>
+    </div>
+    
+  </body>
+</html>

BIN
dist/logo 2.gif


BIN
dist/zxlogo 2.png


BIN
dist/中星图形纯白logo 2.png


BIN
dist/图形带中星彩色logo 2.png


BIN
dist/图形带中星纯白logo 2.png


+ 2 - 1
package.json

@@ -25,6 +25,7 @@
     "lint:lint-staged": "lint-staged -c "
   },
   "dependencies": {
+    "@element-plus/icons": "^0.0.11",
     "@element-plus/icons-vue": "^2.1.0",
     "@form-create/designer": "^3.1.3",
     "@form-create/element-ui": "^3.1.24",
@@ -82,8 +83,8 @@
     "@types/qs": "^6.9.10",
     "@typescript-eslint/eslint-plugin": "^6.11.0",
     "@typescript-eslint/parser": "^6.11.0",
-    "@unocss/transformer-variant-group": "^0.57.4",
     "@unocss/eslint-config": "^0.57.4",
+    "@unocss/transformer-variant-group": "^0.57.4",
     "@vitejs/plugin-legacy": "^4.1.1",
     "@vitejs/plugin-vue": "^4.4.1",
     "@vitejs/plugin-vue-jsx": "^3.0.2",

+ 13 - 13
src/router/modules/remaining.ts

@@ -361,19 +361,19 @@ const remainingRouter: AppRouteRecordRaw[] = [
           activeMenu: '/mall/product/spu'
         }
       },
-      {
-        path: 'spu/edit/:id(\\d+)',
-        component: () => import('@/views/mall/product/spu/form/index.vue'),
-        name: 'ProductSpuEdit',
-        meta: {
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          icon: 'ep:edit',
-          title: '商品编辑',
-          activeMenu: '/mall/product/spu'
-        }
-      },
+		{
+			path: 'spu/edit/:id(\\d+)',
+			component: () => import('@/views/mall/product/spu/form/index.vue'),
+			name: 'ProductSpuEdit',
+			meta: {
+				noCache: true,
+				hidden: true,
+				canTo: true,
+				icon: 'ep:edit',
+				title: '商品编辑',
+				activeMenu: '/mall/product/spu'
+			}
+		},
       {
         path: 'spu/detail/:id(\\d+)',
         component: () => import('@/views/mall/product/spu/form/index.vue'),

+ 275 - 204
src/views/mall/product/spu/form/index.vue

@@ -1,204 +1,275 @@
-<template>
-  <ContentWrap v-loading="formLoading">
-    <el-tabs v-model="activeName">
-      <el-tab-pane label="基础设置" name="info">
-        <InfoForm
-          ref="infoRef"
-          v-model:activeName="activeName"
-          :is-detail="isDetail"
-          :propFormData="formData"
-        />
-      </el-tab-pane>
-      <el-tab-pane label="价格库存" name="sku">
-        <SkuForm
-          ref="skuRef"
-          v-model:activeName="activeName"
-          :is-detail="isDetail"
-          :propFormData="formData"
-        />
-      </el-tab-pane>
-      <el-tab-pane label="物流设置" name="delivery">
-        <DeliveryForm
-          ref="deliveryRef"
-          v-model:activeName="activeName"
-          :is-detail="isDetail"
-          :propFormData="formData"
-        />
-      </el-tab-pane>
-      <el-tab-pane label="商品详情" name="description">
-        <DescriptionForm
-          ref="descriptionRef"
-          v-model:activeName="activeName"
-          :is-detail="isDetail"
-          :propFormData="formData"
-        />
-      </el-tab-pane>
-      <el-tab-pane label="其它设置" name="other">
-        <OtherForm
-          ref="otherRef"
-          v-model:activeName="activeName"
-          :is-detail="isDetail"
-          :propFormData="formData"
-        />
-      </el-tab-pane>
-    </el-tabs>
-    <el-form>
-      <el-form-item style="float: right">
-        <el-button v-if="!isDetail" :loading="formLoading" type="primary" @click="submitForm">
-          保存
-        </el-button>
-        <el-button @click="close">返回</el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-</template>
-<script lang="ts" setup>
-import { cloneDeep } from 'lodash-es'
-import { useTagsViewStore } from '@/store/modules/tagsView'
-import * as ProductSpuApi from '@/api/mall/product/spu'
-import InfoForm from './InfoForm.vue'
-import DescriptionForm from './DescriptionForm.vue'
-import OtherForm from './OtherForm.vue'
-import SkuForm from './SkuForm.vue'
-import DeliveryForm from './DeliveryForm.vue'
-import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
-
-defineOptions({ name: 'ProductSpuForm' })
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-const { push, currentRoute } = useRouter() // 路由
-const { params, name } = useRoute() // 查询参数
-const { delView } = useTagsViewStore() // 视图操作
-
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const activeName = ref('info') // Tag 激活的窗口
-const isDetail = ref(false) // 是否查看详情
-const infoRef = ref() // 商品信息 Ref
-const skuRef = ref() // 商品规格 Ref
-const deliveryRef = ref() // 物流设置 Ref
-const descriptionRef = ref() // 商品详情 Ref
-const otherRef = ref() // 其他设置 Ref
-// SPU 表单数据
-const formData = ref<ProductSpuApi.Spu>({
-  name: '', // 商品名称
-  categoryId: undefined, // 商品分类
-  keyword: '', // 关键字
-  picUrl: '', // 商品封面图
-  sliderPicUrls: [], // 商品轮播图
-  introduction: '', // 商品简介
-  deliveryTypes: [], // 配送方式数组
-  deliveryTemplateId: undefined, // 运费模版
-  brandId: undefined, // 商品品牌
-  specType: false, // 商品规格
-  subCommissionType: false, // 分销类型
-  skus: [
-    {
-      price: 0, // 商品价格
-      marketPrice: 0, // 市场价
-      costPrice: 0, // 成本价
-      barCode: '', // 商品条码
-      picUrl: '', // 图片地址
-      stock: 0, // 库存
-      weight: 0, // 商品重量
-      volume: 0, // 商品体积
-      firstBrokeragePrice: 0, // 一级分销的佣金
-      secondBrokeragePrice: 0 // 二级分销的佣金
-    }
-  ],
-  description: '', // 商品详情
-  sort: 0, // 商品排序
-  giveIntegral: 0, // 赠送积分
-  virtualSalesCount: 0 // 虚拟销量
-})
-
-/** 获得详情 */
-const getDetail = async () => {
-  if ('ProductSpuDetail' === name) {
-    isDetail.value = true
-  }
-  const id = params.id as unknown as number
-  if (id) {
-    formLoading.value = true
-    try {
-      const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.Spu
-      res.skus?.forEach((item) => {
-        if (isDetail.value) {
-          item.price = floatToFixed2(item.price)
-          item.marketPrice = floatToFixed2(item.marketPrice)
-          item.costPrice = floatToFixed2(item.costPrice)
-          item.firstBrokeragePrice = floatToFixed2(item.firstBrokeragePrice)
-          item.secondBrokeragePrice = floatToFixed2(item.secondBrokeragePrice)
-        } else {
-          // 回显价格分转元
-          item.price = formatToFraction(item.price)
-          item.marketPrice = formatToFraction(item.marketPrice)
-          item.costPrice = formatToFraction(item.costPrice)
-          item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice)
-          item.secondBrokeragePrice = formatToFraction(item.secondBrokeragePrice)
-        }
-      })
-      formData.value = res
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-
-/** 提交按钮 */
-const submitForm = async () => {
-  // 提交请求
-  formLoading.value = true
-  try {
-    // 校验各表单
-    await unref(infoRef)?.validate()
-    await unref(skuRef)?.validate()
-    await unref(deliveryRef)?.validate()
-    await unref(descriptionRef)?.validate()
-    await unref(otherRef)?.validate()
-    // 深拷贝一份, 这样最终 server 端不满足,不需要影响原始数据
-    const deepCopyFormData = cloneDeep(unref(formData.value)) as ProductSpuApi.Spu
-    deepCopyFormData.skus!.forEach((item) => {
-      // 给sku name赋值
-      item.name = deepCopyFormData.name
-      // sku相关价格元转分
-      item.price = convertToInteger(item.price)
-      item.marketPrice = convertToInteger(item.marketPrice)
-      item.costPrice = convertToInteger(item.costPrice)
-      item.firstBrokeragePrice = convertToInteger(item.firstBrokeragePrice)
-      item.secondBrokeragePrice = convertToInteger(item.secondBrokeragePrice)
-    })
-    // 处理轮播图列表
-    const newSliderPicUrls: any[] = []
-    deepCopyFormData.sliderPicUrls!.forEach((item: any) => {
-      // 如果是前端选的图
-      typeof item === 'object' ? newSliderPicUrls.push(item.url) : newSliderPicUrls.push(item)
-    })
-    deepCopyFormData.sliderPicUrls = newSliderPicUrls
-    // 校验都通过后提交表单
-    const data = deepCopyFormData as ProductSpuApi.Spu
-    const id = params.id as unknown as number
-    if (!id) {
-      await ProductSpuApi.createSpu(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await ProductSpuApi.updateSpu(data)
-      message.success(t('common.updateSuccess'))
-    }
-    close()
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 关闭按钮 */
-const close = () => {
-  delView(unref(currentRoute))
-  push({ name: 'ProductSpu' })
-}
-
-/** 初始化 */
-onMounted(async () => {
-  await getDetail()
-})
-</script>
+<template>
+
+	<el-dialog v-model="dialogVisible" :title="dialogTitle" width="70%" class="dialog">
+		<ContentWrap v-loading="formLoading">
+
+			<div class="left">
+				<img :src="picUrl" @error="handleError" />
+				<el-tabs v-model="activeName" tab-position="left">
+					<el-tab-pane label="基础设置" name="info" />
+					<el-tab-pane label="价格库存" name="sku" />
+					<el-tab-pane label="物流设置" name="delivery" />
+					<el-tab-pane label="商品详情" name="description" />
+					<el-tab-pane label="其它设置" name="other" />
+				</el-tabs>
+			</div>
+			<div class="right">
+				<div v-show="activeName == 'info'">
+					<InfoForm ref="infoRef" :is-detail="isDetail" :propFormData="formData" />
+				</div>
+				<div v-show="activeName == 'sku'">
+					<SkuForm ref="skuRef" :is-detail="isDetail" :propFormData="formData" />
+				</div>
+				<div v-show="activeName == 'delivery'">
+					<DeliveryForm ref="deliveryRef" :is-detail="isDetail" :propFormData="formData" />
+				</div>
+				<div v-show="activeName == 'description'">
+					<DescriptionForm ref="descriptionRef" :is-detail="isDetail" :propFormData="formData" />
+				</div>
+				<div v-show="activeName == 'other'">
+					<OtherForm ref="otherRef" :is-detail="isDetail" :propFormData="formData" />
+				</div>
+			</div>
+
+			<el-form style="clear: both;">
+				<el-form-item style="float: right">
+					<el-button @click="dialogVisible = false">返回</el-button>
+					<el-button v-if="!isDetail" :loading="formLoading" type="primary" @click="submitForm">
+						保存
+					</el-button>
+
+				</el-form-item>
+			</el-form>
+		</ContentWrap>
+	</el-dialog>
+	<!-- </div> -->
+
+</template>
+<script lang="ts" setup>
+	import { cloneDeep } from 'lodash-es'
+	import { useTagsViewStore } from '@/store/modules/tagsView'
+	import * as ProductSpuApi from '@/api/mall/product/spu'
+	import InfoForm from './InfoForm.vue'
+	import DescriptionForm from './DescriptionForm.vue'
+	import OtherForm from './OtherForm.vue'
+	import SkuForm from './SkuForm.vue'
+	import DeliveryForm from './DeliveryForm.vue'
+	import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
+
+	defineOptions({ name: 'ProductSpuForm' })
+
+	const { t } = useI18n() // 国际化
+	const message = useMessage() // 消息弹窗
+	const { push, currentRoute } = useRouter() // 路由
+	const { params, name } = useRoute() // 查询参数
+	const { delView } = useTagsViewStore() // 视图操作
+
+	const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+	const activeName = ref('info') // Tag 激活的窗口
+	const isDetail = ref(false) // 是否查看详情
+	const infoRef = ref() // 商品信息 Ref
+	const skuRef = ref() // 商品规格 Ref
+	const deliveryRef = ref() // 物流设置 Ref
+	const descriptionRef = ref() // 商品详情 Ref
+	const otherRef = ref() // 其他设置 Ref
+	// SPU 表单数据
+	const formData = ref<ProductSpuApi.Spu>({
+		name: '', // 商品名称
+		categoryId: undefined, // 商品分类
+		keyword: '', // 关键字
+		picUrl: '', // 商品封面图
+		sliderPicUrls: [], // 商品轮播图
+		introduction: '', // 商品简介
+		deliveryTypes: [], // 配送方式数组
+		deliveryTemplateId: undefined, // 运费模版
+		brandId: undefined, // 商品品牌
+		specType: false, // 商品规格
+		subCommissionType: false, // 分销类型
+		skus: [
+			{
+				price: 0, // 商品价格
+				marketPrice: 0, // 市场价
+				costPrice: 0, // 成本价
+				barCode: '', // 商品条码
+				picUrl: '', // 图片地址
+				stock: 0, // 库存
+				weight: 0, // 商品重量
+				volume: 0, // 商品体积
+				firstBrokeragePrice: 0, // 一级分销的佣金
+				secondBrokeragePrice: 0 // 二级分销的佣金
+			}
+		],
+		description: '', // 商品详情
+		sort: 0, // 商品排序
+		giveIntegral: 0, // 赠送积分
+		virtualSalesCount: 0 // 虚拟销量
+	})
+	const addFormData = ref<ProductSpuApi.Spu>({
+		name: '', // 商品名称
+		categoryId: undefined, // 商品分类
+		keyword: '', // 关键字
+		picUrl: '', // 商品封面图
+		sliderPicUrls: [], // 商品轮播图
+		introduction: '', // 商品简介
+		deliveryTypes: [], // 配送方式数组
+		deliveryTemplateId: undefined, // 运费模版
+		brandId: undefined, // 商品品牌
+		specType: false, // 商品规格
+		subCommissionType: false, // 分销类型
+		skus: [
+			{
+				price: 0, // 商品价格
+				marketPrice: 0, // 市场价
+				costPrice: 0, // 成本价
+				barCode: '', // 商品条码
+				picUrl: '', // 图片地址
+				stock: 0, // 库存
+				weight: 0, // 商品重量
+				volume: 0, // 商品体积
+				firstBrokeragePrice: 0, // 一级分销的佣金
+				secondBrokeragePrice: 0 // 二级分销的佣金
+			}
+		],
+		description: '', // 商品详情
+		sort: 0, // 商品排序
+		giveIntegral: 0, // 赠送积分
+		virtualSalesCount: 0 // 虚拟销量
+	})
+	const dialogVisible = ref(false) // 弹窗的是否展示
+	const dialogTitle = ref('') // 弹窗的标题
+	const productId = ref()
+	const picUrl = ref("")
+	const openType = ref("")
+	const handleError = (e) => {
+		e.target.src = "https://static.iocoder.cn/mall/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"
+	}
+	/** 打开弹窗 */
+	const open = async (id : number, type : string, imageUrl : string) => {
+		dialogVisible.value = true
+		productId.value = id
+		picUrl.value = imageUrl
+		console.log(id, type, imageUrl)
+		await getDetail()
+		activeName.value = 'info'
+		openType.value = type
+		dialogTitle.value = t('action.' + type)
+		if (type == "view") {
+			dialogTitle.value = "查看"
+		} else if (type == "create") {
+			formData.value = addFormData.value
+		}
+		// 判断打开状态,如果是view那就只能查看 否则可以编辑
+		if ('view' == openType.value) {
+			isDetail.value = true
+		} else {
+			isDetail.value = false
+		}
+	}
+	defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+	/** 获得详情 */
+	const getDetail = async () => {
+
+		const id = productId.value as any as number
+		if (id) {
+			formLoading.value = true
+			try {
+				const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.Spu
+				res.skus?.forEach((item) => {
+					if (isDetail.value) {
+						item.price = floatToFixed2(item.price)
+						item.marketPrice = floatToFixed2(item.marketPrice)
+						item.costPrice = floatToFixed2(item.costPrice)
+						item.firstBrokeragePrice = floatToFixed2(item.firstBrokeragePrice)
+						item.secondBrokeragePrice = floatToFixed2(item.secondBrokeragePrice)
+					} else {
+						// 回显价格分转元
+						item.price = formatToFraction(item.price)
+						item.marketPrice = formatToFraction(item.marketPrice)
+						item.costPrice = formatToFraction(item.costPrice)
+						item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice)
+						item.secondBrokeragePrice = formatToFraction(item.secondBrokeragePrice)
+					}
+				})
+				formData.value = res
+			} finally {
+				formLoading.value = false
+			}
+		}
+	}
+	const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+	/** 提交按钮 */
+	const submitForm = async () => {
+		// 提交请求
+		formLoading.value = true
+		try {
+			// 校验各表单
+			await unref(infoRef)?.validate()
+			await unref(skuRef)?.validate()
+			await unref(deliveryRef)?.validate()
+			await unref(descriptionRef)?.validate()
+			await unref(otherRef)?.validate()
+			// 深拷贝一份, 这样最终 server 端不满足,不需要影响原始数据
+			const deepCopyFormData = cloneDeep(unref(formData.value)) as ProductSpuApi.Spu
+			deepCopyFormData.skus!.forEach((item) => {
+				// 给sku name赋值
+				item.name = deepCopyFormData.name
+				// sku相关价格元转分
+				item.price = convertToInteger(item.price)
+				item.marketPrice = convertToInteger(item.marketPrice)
+				item.costPrice = convertToInteger(item.costPrice)
+				item.firstBrokeragePrice = convertToInteger(item.firstBrokeragePrice)
+				item.secondBrokeragePrice = convertToInteger(item.secondBrokeragePrice)
+			})
+			// 处理轮播图列表
+			const newSliderPicUrls : any[] = []
+			deepCopyFormData.sliderPicUrls!.forEach((item : any) => {
+				// 如果是前端选的图
+				typeof item === 'object' ? newSliderPicUrls.push(item.url) : newSliderPicUrls.push(item)
+			})
+			deepCopyFormData.sliderPicUrls = newSliderPicUrls
+			// 校验都通过后提交表单
+			const data = deepCopyFormData as ProductSpuApi.Spu
+			const id = productId.value as any as number
+			if (!id) {
+				await ProductSpuApi.createSpu(data)
+				message.success(t('common.createSuccess'))
+			} else {
+				await ProductSpuApi.updateSpu(data)
+				message.success(t('common.updateSuccess'))
+			}
+			close()
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 关闭按钮 */
+	const close = () => {
+		dialogVisible.value = false
+	}
+</script>
+<style type="text/css">
+	.el-dialog__body {
+		padding: unset;
+	}
+</style>
+<style lang="scss" scoped>
+	.left {
+		width: 96px;
+		float: left;
+
+		img {
+			width: 96%;
+			border: 2px solid #e4e7ee;
+			margin-bottom: -5px;
+		}
+	}
+
+	.right {
+		border-left: 2px solid #e4e7ee;
+		margin-left: -2px;
+		float: left;
+		width: calc(100% - 120px);
+	}
+</style>

+ 542 - 449
src/views/mall/product/spu/index.vue

@@ -1,449 +1,542 @@
-<!-- 商品中心 - 商品列表  -->
-<template>
-  <!-- 搜索工作栏 -->
-  <ContentWrap>
-    <el-form
-      ref="queryFormRef"
-      :inline="true"
-      :model="queryParams"
-      class="-mb-15px"
-      label-width="68px"
-    >
-      <el-form-item label="商品名称" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          class="!w-240px"
-          clearable
-          placeholder="请输入商品名称"
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="商品分类" prop="categoryId">
-        <el-cascader
-          v-model="queryParams.categoryId"
-          :options="categoryList"
-          :props="defaultProps"
-          class="w-1/1"
-          clearable
-          filterable
-          placeholder="请选择商品分类"
-        />
-      </el-form-item>
-      <el-form-item label="创建时间" prop="createTime">
-        <el-date-picker
-          v-model="queryParams.createTime"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-240px"
-          end-placeholder="结束日期"
-          start-placeholder="开始日期"
-          type="daterange"
-          value-format="YYYY-MM-DD HH:mm:ss"
-        />
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery">
-          <Icon class="mr-5px" icon="ep:search" />
-          搜索
-        </el-button>
-        <el-button @click="resetQuery">
-          <Icon class="mr-5px" icon="ep:refresh" />
-          重置
-        </el-button>
-        <el-button
-          v-hasPermi="['product:spu:create']"
-          plain
-          type="primary"
-          @click="openForm(undefined)"
-        >
-          <Icon class="mr-5px" icon="ep:plus" />
-          新增
-        </el-button>
-        <el-button
-          v-hasPermi="['product:spu:export']"
-          :loading="exportLoading"
-          plain
-          type="success"
-          @click="handleExport"
-        >
-          <Icon class="mr-5px" icon="ep:download" />
-          导出
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-tabs v-model="queryParams.tabType" @tab-click="handleTabClick">
-      <el-tab-pane
-        v-for="item in tabsData"
-        :key="item.type"
-        :label="item.name + '(' + item.count + ')'"
-        :name="item.type"
-      />
-    </el-tabs>
-    <el-table v-loading="loading" :data="list">
-      <el-table-column type="expand">
-        <template #default="{ row }">
-          <el-form class="spu-table-expand" label-position="left">
-            <el-row>
-              <el-col :span="24">
-                <el-row>
-                  <el-col :span="8">
-                    <el-form-item label="商品分类:">
-                      <span>{{ formatCategoryName(row.categoryId) }}</span>
-                    </el-form-item>
-                  </el-col>
-                  <el-col :span="8">
-                    <el-form-item label="市场价:">
-                      <span>{{ fenToYuan(row.marketPrice) }}</span>
-                    </el-form-item>
-                  </el-col>
-                  <el-col :span="8">
-                    <el-form-item label="成本价:">
-                      <span>{{ fenToYuan(row.costPrice) }}</span>
-                    </el-form-item>
-                  </el-col>
-                </el-row>
-              </el-col>
-            </el-row>
-            <el-row>
-              <el-col :span="24">
-                <el-row>
-                  <el-col :span="8">
-                    <el-form-item label="浏览量:">
-                      <span>{{ row.browseCount }}</span>
-                    </el-form-item>
-                  </el-col>
-                  <el-col :span="8">
-                    <el-form-item label="虚拟销量:">
-                      <span>{{ row.virtualSalesCount }}</span>
-                    </el-form-item>
-                  </el-col>
-                </el-row>
-              </el-col>
-            </el-row>
-          </el-form>
-        </template>
-      </el-table-column>
-      <el-table-column label="商品编号" min-width="140" prop="id" />
-      <el-table-column label="商品信息" min-width="300">
-        <template #default="{ row }">
-          <div class="flex">
-            <el-image
-              fit="cover"
-              :src="row.picUrl"
-              class="flex-none w-50px h-50px"
-              @click="imagePreview(row.picUrl)"
-            />
-            <div class="ml-4 overflow-hidden">
-              <el-tooltip effect="dark" :content="row.name" placement="top">
-                <div>
-                  {{ row.name }}
-                </div>
-              </el-tooltip>
-            </div>
-          </div>
-        </template>
-      </el-table-column>
-      <el-table-column align="center" label="价格" min-width="160" prop="price">
-        <template #default="{ row }"> ¥ {{ fenToYuan(row.price) }}</template>
-      </el-table-column>
-      <el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
-      <el-table-column align="center" label="库存" min-width="90" prop="stock" />
-      <el-table-column align="center" label="排序" min-width="70" prop="sort" />
-      <el-table-column align="center" label="销售状态" min-width="80">
-        <template #default="{ row }">
-          <template v-if="row.status >= 0">
-            <el-switch
-              v-model="row.status"
-              :active-value="1"
-              :inactive-value="0"
-              active-text="上架"
-              inactive-text="下架"
-              inline-prompt
-              @change="handleStatusChange(row)"
-            />
-          </template>
-          <template v-else>
-            <el-tag type="info">回收站</el-tag>
-          </template>
-        </template>
-      </el-table-column>
-      <el-table-column
-        :formatter="dateFormatter"
-        align="center"
-        label="创建时间"
-        prop="createTime"
-        width="180"
-      />
-      <el-table-column align="center" fixed="right" label="操作" min-width="200">
-        <template #default="{ row }">
-          <el-button link type="primary" @click="openDetail(row.id)"> 详情 </el-button>
-          <el-button
-            v-hasPermi="['product:spu:update']"
-            link
-            type="primary"
-            @click="openForm(row.id)"
-          >
-            修改
-          </el-button>
-          <template v-if="queryParams.tabType === 4">
-            <el-button
-              v-hasPermi="['product:spu:delete']"
-              link
-              type="danger"
-              @click="handleDelete(row.id)"
-            >
-              删除
-            </el-button>
-            <el-button
-              v-hasPermi="['product:spu:update']"
-              link
-              type="primary"
-              @click="handleStatus02Change(row, ProductSpuStatusEnum.DISABLE.status)"
-            >
-              恢复
-            </el-button>
-          </template>
-          <template v-else>
-            <el-button
-              v-hasPermi="['product:spu:update']"
-              link
-              type="danger"
-              @click="handleStatus02Change(row, ProductSpuStatusEnum.RECYCLE.status)"
-            >
-              回收
-            </el-button>
-          </template>
-        </template>
-      </el-table-column>
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      v-model:limit="queryParams.pageSize"
-      v-model:page="queryParams.pageNo"
-      :total="total"
-      @pagination="getList"
-    />
-  </ContentWrap>
-</template>
-<script lang="ts" setup>
-import { TabsPaneContext } from 'element-plus'
-import { createImageViewer } from '@/components/ImageViewer'
-import { dateFormatter } from '@/utils/formatTime'
-import { defaultProps, handleTree, treeToString } from '@/utils/tree'
-import { ProductSpuStatusEnum } from '@/utils/constants'
-import { fenToYuan } from '@/utils'
-import download from '@/utils/download'
-import * as ProductSpuApi from '@/api/mall/product/spu'
-import * as ProductCategoryApi from '@/api/mall/product/category'
-
-defineOptions({ name: 'ProductSpu' })
-
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-const { push } = useRouter() // 路由跳转
-
-const loading = ref(false) // 列表的加载中
-const exportLoading = ref(false) // 导出的加载中
-const total = ref(0) // 列表的总页数
-const list = ref<ProductSpuApi.Spu[]>([]) // 列表的数据
-// tabs 数据
-const tabsData = ref([
-  {
-    name: '出售中',
-    type: 0,
-    count: 0
-  },
-  {
-    name: '仓库中',
-    type: 1,
-    count: 0
-  },
-  {
-    name: '已售罄',
-    type: 2,
-    count: 0
-  },
-  {
-    name: '警戒库存',
-    type: 3,
-    count: 0
-  },
-  {
-    name: '回收站',
-    type: 4,
-    count: 0
-  }
-])
-
-const queryParams = ref({
-  pageNo: 1,
-  pageSize: 10,
-  tabType: 0,
-  name: '',
-  categoryId: undefined,
-  createTime: undefined
-}) // 查询参数
-const queryFormRef = ref() // 搜索的表单Ref
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await ProductSpuApi.getSpuPage(queryParams.value)
-    list.value = data.list
-    total.value = data.total
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 切换 Tab */
-const handleTabClick = (tab: TabsPaneContext) => {
-  queryParams.value.tabType = tab.paneName as number
-  getList()
-}
-
-/** 获得每个 Tab 的数量 */
-const getTabsCount = async () => {
-  const res = await ProductSpuApi.getTabsCount()
-  for (let objName in res) {
-    tabsData.value[Number(objName)].count = res[objName]
-  }
-}
-
-/** 添加到仓库 / 回收站的状态 */
-const handleStatus02Change = async (row: any, newStatus: number) => {
-  try {
-    // 二次确认
-    const text = newStatus === ProductSpuStatusEnum.RECYCLE.status ? '加入到回收站' : '恢复到仓库'
-    await message.confirm(`确认要"${row.name}"${text}吗?`)
-    // 发起修改
-    await ProductSpuApi.updateStatus({ id: row.id, status: newStatus })
-    message.success(text + '成功')
-    // 刷新 tabs 数据
-    await getTabsCount()
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-/** 更新上架/下架状态 */
-const handleStatusChange = async (row: any) => {
-  try {
-    // 二次确认
-    const text = row.status ? '上架' : '下架'
-    await message.confirm(`确认要${text}"${row.name}"吗?`)
-    // 发起修改
-    await ProductSpuApi.updateStatus({ id: row.id, status: row.status })
-    message.success(text + '成功')
-    // 刷新 tabs 数据
-    await getTabsCount()
-    // 刷新列表
-    await getList()
-  } catch {
-    // 异常时,需要重置回之前的值
-    row.status =
-      row.status === ProductSpuStatusEnum.DISABLE.status
-        ? ProductSpuStatusEnum.ENABLE.status
-        : ProductSpuStatusEnum.DISABLE.status
-  }
-}
-
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await ProductSpuApi.deleteSpu(id)
-    message.success(t('common.delSuccess'))
-    // 刷新tabs数据
-    await getTabsCount()
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-/** 商品图预览 */
-const imagePreview = (imgUrl: string) => {
-  createImageViewer({
-    urlList: [imgUrl]
-  })
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  handleQuery()
-}
-
-/** 新增或修改 */
-const openForm = (id?: number) => {
-  // 修改
-  if (typeof id === 'number') {
-    push({ name: 'ProductSpuEdit', params: { id } })
-    return
-  }
-  // 新增
-  push({ name: 'ProductSpuAdd' })
-}
-
-/** 查看商品详情 */
-const openDetail = (id: number) => {
-  push({ name: 'ProductSpuDetail', params: { id } })
-}
-
-/** 导出按钮操作 */
-const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await ProductSpuApi.exportSpu(queryParams)
-    download.excel(data, '商品列表.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
-}
-
-/** 获取分类的节点的完整结构 */
-const categoryList = ref() // 分类树
-const formatCategoryName = (categoryId: number) => {
-  return treeToString(categoryList.value, categoryId)
-}
-
-/** 激活时 */
-onActivated(() => {
-  getList()
-})
-
-/** 初始化 **/
-onMounted(async () => {
-  await getTabsCount()
-  await getList()
-  // 获得分类树
-  const data = await ProductCategoryApi.getCategoryList({})
-  categoryList.value = handleTree(data, 'id', 'parentId')
-})
-</script>
-<style lang="scss" scoped>
-.spu-table-expand {
-  padding-left: 42px;
-
-  :deep(.el-form-item__label) {
-    width: 82px;
-    font-weight: bold;
-    color: #99a9bf;
-  }
-}
-</style>
+<!-- 商品中心 - 商品列表  -->
+<template>
+	<!-- 搜索工作栏 -->
+	<!-- <ContentWrap>
+		<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="-mb-15px" label-width="68px">
+			<el-form-item label="商品名称" prop="name">
+				<el-input v-model="queryParams.name" class="!w-240px" clearable placeholder="请输入商品名称"
+					@keyup.enter="handleQuery" />
+			</el-form-item>
+			<el-form-item label="商品分类" prop="categoryId">
+				<el-cascader v-model="queryParams.categoryId" :options="categoryList" :props="defaultProps"
+					class="w-1/1" clearable filterable placeholder="请选择商品分类" />
+			</el-form-item>
+			<el-form-item label="创建时间" prop="createTime">
+				<el-date-picker v-model="queryParams.createTime"
+					:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-240px"
+					end-placeholder="结束日期" start-placeholder="开始日期" type="daterange"
+					value-format="YYYY-MM-DD HH:mm:ss" />
+			</el-form-item>
+			<el-form-item>
+				<el-button @click="handleQuery">
+					<Icon class="mr-5px" icon="ep:search" />
+					搜索
+				</el-button>
+				<el-button @click="resetQuery">
+					<Icon class="mr-5px" icon="ep:refresh" />
+					重置
+				</el-button>
+				<el-button v-hasPermi="['product:spu:create']" plain type="primary" @click="openForm(undefined)">
+					<Icon class="mr-5px" icon="ep:plus" />
+					新增
+				</el-button>
+				 <el-button v-hasPermi="['product:spu:export']" :loading="exportLoading" plain type="success"
+					@click="handleExport">
+					<Icon class="mr-5px" icon="ep:download" />
+					导出
+				</el-button> 
+			</el-form-item>
+		</el-form>
+	</ContentWrap> -->
+
+
+
+	<!-- 列表 -->
+	<ContentWrap style="position: relative;min-height:400px">
+		<div style="position: relative;">
+			<div style="text-align: right;background: #fafbfc;padding: 10px;">
+				<el-button v-hasPermi="['product:spu:create']" plain type="primary" @click="openForm(0,'create','')">
+					<Icon class="mr-5px" icon="ep:plus" />
+					新增
+				</el-button>
+				&nbsp;
+				<el-input v-model="queryParams.name" class="!w-240px" placeholder="搜索商品名称" @keyup.enter="handleQuery">
+					<template #suffix>
+						<el-icon class="el-input__icon" @click="handleQuery" style="cursor: pointer;">
+							<Search />
+						</el-icon>
+					</template>
+					<template #append>
+						<el-button :icon="Operation" @click="showSearchMore" />
+					</template>
+
+				</el-input>
+			</div>
+			<div class="searchMore" v-if="searchMoreShow">
+				<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="-mb-15px" label-width="68px">
+					<div>
+						<el-form-item label="商品分类" prop="categoryId">
+							<el-cascader v-model="queryParams.categoryId" :options="categoryList" :props="defaultProps"
+								class="w-1/1" clearable filterable placeholder="请选择商品分类" />
+						</el-form-item>
+						<el-form-item label="创建时间" prop="createTime">
+							<el-date-picker style="margin-right: unset;" v-model="queryParams.createTime"
+								:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-240px"
+								end-placeholder="结束日期" start-placeholder="开始日期" type="daterange"
+								value-format="YYYY-MM-DD HH:mm:ss" />
+						</el-form-item>
+					</div>
+					<div>
+						<el-button @click="hideSearchMore">
+							收起
+						</el-button>
+						<el-button @click="resetQuery">
+							重置
+						</el-button>
+						<el-button @click="handleQuery" plain type="primary">
+							<Icon class="mr-5px" icon="ep:search" />
+							搜索
+						</el-button>
+						<!-- <el-button v-hasPermi="['product:spu:export']" :loading="exportLoading" plain type="success"
+								@click="handleExport">
+								<Icon class="mr-5px" icon="ep:download" />
+								导出
+							</el-button> -->
+					</div>
+				</el-form>
+			</div>
+		</div>
+		<el-tabs v-model="queryParams.tabType" @tab-click="handleTabClick">
+			<el-tab-pane v-for="item in tabsData" :key="item.type" :label="item.name + '(' + item.count + ')'"
+				:name="item.type" />
+		</el-tabs>
+		<el-row>
+			<el-col v-for="(o, index) in list" class="product-card" :key="index" :span="7"
+				@mouseover="handleMouseOver(index)" @mouseout="handleMouseOut(index)">
+				<el-card @click="openDetail(o.id,'view',o.picUrl)">
+					<div style="display: flex;justify-content: space-between;align-items: center;margin-bottom: 5px;">
+						<p style="width: 100%;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;"
+							:title="o.name">{{o.name}}
+						</p>
+
+
+						<el-button :icon="Setting" plain type="primary" circle
+							@click.stop="openForm(o.id,'update',o.picUrl)" v-show="o.showSetting" class="setting" />
+
+					</div>
+					<div style="display:flex;align-items: center;">
+						<img :src="o.picUrl"
+							style="width: 100px;height: 100px;margin-right:10px;border:1px solid #303133;border-radius: 5px;"
+							@error="handleError" />
+						<div style='line-height: 25px;'>
+							<p>¥{{o.price}}</p>
+							<p>销量:{{o.salesCount}}</p>
+							<p>库存:{{o.stock}}</p>
+							<p>状态:{{o.status == 1?"上架":"下架"}}</p>
+						</div>
+					</div>
+				</el-card>
+			</el-col>
+		</el-row>
+		<div v-if="list.length == 0">
+			<p style="text-align: center;margin-top: 100px;">暂无商品</p>
+		</div>
+		<!--<el-table v-loading="loading" :data="list">
+			<el-table-column type="expand">
+				<template #default="{ row }">
+					<el-form class="spu-table-expand" label-position="left">
+						<el-row>
+							<el-col :span="24">
+								<el-row>
+									<el-col :span="8">
+										<el-form-item label="商品分类:">
+											<span>{{ formatCategoryName(row.categoryId) }}</span>
+										</el-form-item>
+									</el-col>
+									<el-col :span="8">
+										<el-form-item label="市场价:">
+											<span>{{ fenToYuan(row.marketPrice) }}</span>
+										</el-form-item>
+									</el-col>
+									<el-col :span="8">
+										<el-form-item label="成本价:">
+											<span>{{ fenToYuan(row.costPrice) }}</span>
+										</el-form-item>
+									</el-col>
+								</el-row>
+							</el-col>
+						</el-row>
+						<el-row>
+							<el-col :span="24">
+								<el-row>
+									<el-col :span="8">
+										<el-form-item label="浏览量:">
+											<span>{{ row.browseCount }}</span>
+										</el-form-item>
+									</el-col>
+									<el-col :span="8">
+										<el-form-item label="虚拟销量:">
+											<span>{{ row.virtualSalesCount }}</span>
+										</el-form-item>
+									</el-col>
+								</el-row>
+							</el-col>
+						</el-row>
+					</el-form>
+				</template>
+			</el-table-column>
+			<el-table-column label="商品编号" min-width="140" prop="id" />
+			<el-table-column label="商品信息" min-width="300">
+				<template #default="{ row }">
+					<div class="flex">
+						<el-image fit="cover" :src="row.picUrl" class="flex-none w-50px h-50px"
+							@click="imagePreview(row.picUrl)" />
+						<div class="ml-4 overflow-hidden">
+							<el-tooltip effect="dark" :content="row.name" placement="top">
+								<div>
+									{{ row.name }}
+								</div>
+							</el-tooltip>
+						</div>
+					</div>
+				</template>
+			</el-table-column>
+			<el-table-column align="center" label="价格" min-width="160" prop="price">
+				<template #default="{ row }"> ¥ {{ fenToYuan(row.price) }}</template>
+			</el-table-column>
+			<el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
+			<el-table-column align="center" label="库存" min-width="90" prop="stock" />
+			<el-table-column align="center" label="排序" min-width="70" prop="sort" />
+			<el-table-column align="center" label="销售状态" min-width="80">
+				<template #default="{ row }">
+					<template v-if="row.status >= 0">
+						<el-switch v-model="row.status" :active-value="1" :inactive-value="0" active-text="上架"
+							inactive-text="下架" inline-prompt @change="handleStatusChange(row)" />
+					</template>
+					<template v-else>
+						<el-tag type="info">回收站</el-tag>
+					</template>
+				</template>
+			</el-table-column>
+			<el-table-column :formatter="dateFormatter" align="center" label="创建时间" prop="createTime" width="180" />
+			<el-table-column align="center" fixed="right" label="操作" min-width="200">
+				<template #default="{ row }">
+					<el-button link type="primary" @click="openDetail(row.id)"> 详情 </el-button>
+					<el-button v-hasPermi="['product:spu:update']" link type="primary" @click="openForm(row.id)">
+						修改
+					</el-button>
+					<template v-if="queryParams.tabType === 4">
+						<el-button v-hasPermi="['product:spu:delete']" link type="danger" @click="handleDelete(row.id)">
+							删除
+						</el-button>
+						<el-button v-hasPermi="['product:spu:update']" link type="primary"
+							@click="handleStatus02Change(row, ProductSpuStatusEnum.DISABLE.status)">
+							恢复
+						</el-button>
+					</template>
+					<template v-else>
+						<el-button v-hasPermi="['product:spu:update']" link type="danger"
+							@click="handleStatus02Change(row, ProductSpuStatusEnum.RECYCLE.status)">
+							回收
+						</el-button>
+					</template>
+				</template>
+			</el-table-column>
+		</el-table>-->
+
+		<!-- 分页 -->
+		<Pagination v-model:limit="queryParams.pageSize" v-model:page="queryParams.pageNo" :total="total"
+			@pagination="getList" />
+		<!-- 表单弹窗:添加/修改 -->
+		<SpuIndex ref="formRef" @success="getList" />
+	</ContentWrap>
+</template>
+<script lang="ts" setup>
+	import { TabsPaneContext } from 'element-plus'
+	import { Setting, Search, Operation } from '@element-plus/icons-vue'
+	import { createImageViewer } from '@/components/ImageViewer'
+	import { dateFormatter } from '@/utils/formatTime'
+	import { defaultProps, handleTree, treeToString } from '@/utils/tree'
+	import { ProductSpuStatusEnum } from '@/utils/constants'
+	import { fenToYuan } from '@/utils'
+	import download from '@/utils/download'
+	import * as ProductSpuApi from '@/api/mall/product/spu'
+	import * as ProductCategoryApi from '@/api/mall/product/category'
+	import SpuIndex from "./form/index.vue"
+
+	defineOptions({ name: 'ProductSpu' })
+
+	const message = useMessage() // 消息弹窗
+	const { t } = useI18n() // 国际化
+	const { push } = useRouter() // 路由跳转
+
+	const loading = ref(false) // 列表的加载中
+	const exportLoading = ref(false) // 导出的加载中
+	const total = ref(0) // 列表的总页数
+	const list = ref<ProductSpuApi.Spu[]>([]) // 列表的数据
+	// tabs 数据
+	const tabsData = ref([
+		{
+			name: '出售中',
+			type: 0,
+			count: 0
+		},
+		{
+			name: '仓库中',
+			type: 1,
+			count: 0
+		},
+		{
+			name: '已售罄',
+			type: 2,
+			count: 0
+		},
+		{
+			name: '警戒库存',
+			type: 3,
+			count: 0
+		},
+		{
+			name: '回收站',
+			type: 4,
+			count: 0
+		}
+	])
+
+	const queryParams = ref({
+		pageNo: 1,
+		pageSize: 10,
+		tabType: 0,
+		name: '',
+		categoryId: undefined,
+		createTime: undefined
+	}) // 查询参数
+	const queryFormRef = ref() // 搜索的表单Ref
+	const searchMoreShow = ref(false)
+	// 鼠标移入显示变动图标
+	function handleMouseOver(index) {
+		list.value[index].showSetting = true;
+	}
+	// 鼠标移出不显示图标
+	function handleMouseOut(index) {
+		list.value[index].showSetting = false;
+	}
+	// 裂图替换
+	const handleError = (e) => {
+		e.target.src = "https://static.iocoder.cn/mall/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"
+	}
+	// 打开更多搜索条件
+	const showSearchMore = () => {
+		searchMoreShow.value = !searchMoreShow.value
+	}
+	const hideSearchMore = () => {
+		searchMoreShow.value = false
+	}
+
+	/** 查询列表 */
+	const getList = async () => {
+		loading.value = true
+		try {
+			const data = await ProductSpuApi.getSpuPage(queryParams.value)
+			data.list.forEach(obj => {
+				obj.showSetting = false;
+			});
+
+			list.value = data.list
+			// console.log(list.value)
+			total.value = data.total
+		} finally {
+			loading.value = false
+			searchMoreShow.value = false
+		}
+	}
+
+	/** 切换 Tab */
+	const handleTabClick = (tab : TabsPaneContext) => {
+		queryParams.value.tabType = tab.paneName as number
+		getList()
+	}
+
+	/** 获得每个 Tab 的数量 */
+	const getTabsCount = async () => {
+		const res = await ProductSpuApi.getTabsCount()
+		for (let objName in res) {
+			tabsData.value[Number(objName)].count = res[objName]
+		}
+	}
+
+	/** 添加到仓库 / 回收站的状态 */
+	const handleStatus02Change = async (row : any, newStatus : number) => {
+		try {
+			// 二次确认
+			const text = newStatus === ProductSpuStatusEnum.RECYCLE.status ? '加入到回收站' : '恢复到仓库'
+			await message.confirm(`确认要"${row.name}"${text}吗?`)
+			// 发起修改
+			await ProductSpuApi.updateStatus({ id: row.id, status: newStatus })
+			message.success(text + '成功')
+			// 刷新 tabs 数据
+			await getTabsCount()
+			// 刷新列表
+			await getList()
+		} catch { }
+	}
+
+	/** 更新上架/下架状态 */
+	const handleStatusChange = async (row : any) => {
+		try {
+			// 二次确认
+			const text = row.status ? '上架' : '下架'
+			await message.confirm(`确认要${text}"${row.name}"吗?`)
+			// 发起修改
+			await ProductSpuApi.updateStatus({ id: row.id, status: row.status })
+			message.success(text + '成功')
+			// 刷新 tabs 数据
+			await getTabsCount()
+			// 刷新列表
+			await getList()
+		} catch {
+			// 异常时,需要重置回之前的值
+			row.status =
+				row.status === ProductSpuStatusEnum.DISABLE.status
+					? ProductSpuStatusEnum.ENABLE.status
+					: ProductSpuStatusEnum.DISABLE.status
+		}
+	}
+
+	/** 删除按钮操作 */
+	const handleDelete = async (id : number) => {
+		try {
+			// 删除的二次确认
+			await message.delConfirm()
+			// 发起删除
+			await ProductSpuApi.deleteSpu(id)
+			message.success(t('common.delSuccess'))
+			// 刷新tabs数据
+			await getTabsCount()
+			// 刷新列表
+			await getList()
+		} catch { }
+	}
+
+	/** 商品图预览 */
+	const imagePreview = (imgUrl : string) => {
+		createImageViewer({
+			urlList: [imgUrl]
+		})
+	}
+
+	/** 搜索按钮操作 */
+	const handleQuery = () => {
+		getList()
+	}
+
+	/** 重置按钮操作 */
+	const resetQuery = () => {
+		queryParams.value.name = ""
+		queryFormRef.value.resetFields()
+		handleQuery()
+	}
+
+	const formRef = ref()
+	/** 新增或修改 */
+	const openForm = (id : number, type : string, imageUrl : string) => {
+		formRef.value.open(id, type, imageUrl)
+		// 修改
+		// if (typeof id === 'number') {
+		// 	push({ name: 'ProductSpuEdit', params: { id } })
+		// 	return
+		// }
+		// 新增
+		// push({ name: 'ProductSpuAdd' })
+	}
+
+	/** 查看商品详情 */
+	const openDetail = (id : number, type : string, imageUrl : string) => {
+		formRef.value.open(id, type, imageUrl)
+		// push({ name: 'ProductSpuDetail', params: { id } })
+	}
+
+	/** 导出按钮操作 */
+	const handleExport = async () => {
+		try {
+			// 导出的二次确认
+			await message.exportConfirm()
+			// 发起导出
+			exportLoading.value = true
+			const data = await ProductSpuApi.exportSpu(queryParams)
+			download.excel(data, '商品列表.xls')
+		} catch {
+		} finally {
+			exportLoading.value = false
+		}
+	}
+
+	/** 获取分类的节点的完整结构 */
+	const categoryList = ref() // 分类树
+	const formatCategoryName = (categoryId : number) => {
+		return treeToString(categoryList.value, categoryId)
+	}
+
+	/** 激活时 */
+	onActivated(() => {
+		getList()
+	})
+
+	/** 初始化 **/
+	onMounted(async () => {
+		await getTabsCount()
+		await getList()
+		// 获得分类树
+		const data = await ProductCategoryApi.getCategoryList({})
+		categoryList.value = handleTree(data, 'id', 'parentId')
+	})
+</script>
+<style lang="scss" scoped>
+	.searchMore {
+		text-align: right;
+		background: #f3f5f8;
+		padding: 10px;
+		position: absolute;
+		height: 100px;
+		width: calc(100% - 20px);
+		z-index: 111;
+		box-shadow: 0px 9px 6px rgba(0, 0, 0, 0.2);
+
+
+	}
+
+	.el-tabs {
+		position: absolute;
+		z-index: 111;
+		top: 25px;
+		left: 40px;
+	}
+
+	.product-card {
+
+		font-size: 14px;
+		margin: 10px;
+
+		.el-card {
+			padding: 15px;
+			cursor: pointer;
+			position: relative;
+
+			:deep(.el-card__body) {
+				padding: unset;
+			}
+		}
+
+		.setting {
+			top: 8px;
+			right: 8px;
+			position: absolute;
+		}
+
+		p,
+		div {
+			margin: unset;
+			padding: unset;
+		}
+	}
+
+	.spu-table-expand {
+		padding-left: 42px;
+
+		:deep(.el-form-item__label) {
+			width: 82px;
+			font-weight: bold;
+			color: #99a9bf;
+		}
+	}
+</style>

+ 118 - 124
src/views/system/dict/DictTypeForm.vue

@@ -1,124 +1,118 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="80px"
-    >
-      <el-form-item label="字典名称" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入字典名称" />
-      </el-form-item>
-      <el-form-item label="字典类型" prop="type">
-        <el-input
-          v-model="formData.type"
-          :disabled="typeof formData.id !== 'undefined'"
-          placeholder="请输入参数名称"
-        />
-      </el-form-item>
-      <el-form-item label="状态" prop="status">
-        <el-radio-group v-model="formData.status">
-          <el-radio
-            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
-            :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import * as DictTypeApi from '@/api/system/dict/dict.type'
-import { CommonStatusEnum } from '@/utils/constants'
-
-defineOptions({ name: 'SystemDictTypeForm' })
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined,
-  name: '',
-  type: '',
-  status: CommonStatusEnum.ENABLE,
-  remark: ''
-})
-const formRules = reactive({
-  name: [{ required: true, message: '字典名称不能为空', trigger: 'blur' }],
-  type: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }],
-  status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
-})
-const formRef = ref() // 表单 Ref
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await DictTypeApi.getDictType(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as DictTypeApi.DictTypeVO
-    if (formType.value === 'create') {
-      await DictTypeApi.createDictType(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await DictTypeApi.updateDictType(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    type: '',
-    name: '',
-    status: CommonStatusEnum.ENABLE,
-    remark: ''
-  }
-  formRef.value?.resetFields()
-}
-</script>
+<template>
+	<Dialog v-model="dialogVisible" :title="dialogTitle">
+		<el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules" label-width="80px">
+			<el-row :gutter="30">
+				<el-col span="12">
+					<el-form-item label="字典名称" prop="name">
+						<el-input v-model="formData.name" placeholder="请输入字典名称" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="字典类型" prop="type">
+						<el-input v-model="formData.type" :disabled="typeof formData.id !== 'undefined'"
+							placeholder="请输入参数名称" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-form-item label="状态" prop="status">
+				<el-radio-group v-model="formData.status">
+					<el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value"
+						:label="dict.value">
+						{{ dict.label }}
+					</el-radio>
+				</el-radio-group>
+			</el-form-item>
+			<el-form-item label="备注" prop="remark">
+				<el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+	import * as DictTypeApi from '@/api/system/dict/dict.type'
+	import { CommonStatusEnum } from '@/utils/constants'
+
+	defineOptions({ name: 'SystemDictTypeForm' })
+
+	const { t } = useI18n() // 国际化
+	const message = useMessage() // 消息弹窗
+
+	const dialogVisible = ref(false) // 弹窗的是否展示
+	const dialogTitle = ref('') // 弹窗的标题
+	const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+	const formType = ref('') // 表单的类型:create - 新增;update - 修改
+	const formData = ref({
+		id: undefined,
+		name: '',
+		type: '',
+		status: CommonStatusEnum.ENABLE,
+		remark: ''
+	})
+	const formRules = reactive({
+		name: [{ required: true, message: '字典名称不能为空', trigger: 'blur' }],
+		type: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }],
+		status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
+	})
+	const formRef = ref() // 表单 Ref
+
+	/** 打开弹窗 */
+	const open = async (type : string, id ?: number) => {
+		dialogVisible.value = true
+		dialogTitle.value = t('action.' + type)
+		formType.value = type
+		resetForm()
+		// 修改时,设置数据
+		if (id) {
+			formLoading.value = true
+			try {
+				formData.value = await DictTypeApi.getDictType(id)
+			} finally {
+				formLoading.value = false
+			}
+		}
+	}
+	defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+	/** 提交表单 */
+	const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+	const submitForm = async () => {
+		// 校验表单
+		if (!formRef) return
+		const valid = await formRef.value.validate()
+		if (!valid) return
+		// 提交请求
+		formLoading.value = true
+		try {
+			const data = formData.value as DictTypeApi.DictTypeVO
+			if (formType.value === 'create') {
+				await DictTypeApi.createDictType(data)
+				message.success(t('common.createSuccess'))
+			} else {
+				await DictTypeApi.updateDictType(data)
+				message.success(t('common.updateSuccess'))
+			}
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		formData.value = {
+			id: undefined,
+			type: '',
+			name: '',
+			status: CommonStatusEnum.ENABLE,
+			remark: ''
+		}
+		formRef.value?.resetFields()
+	}
+</script>

+ 197 - 183
src/views/system/dict/data/DictDataForm.vue

@@ -1,183 +1,197 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="80px"
-    >
-      <el-form-item label="字典类型" prop="type">
-        <el-input
-          v-model="formData.dictType"
-          :disabled="typeof formData.id !== 'undefined'"
-          placeholder="请输入参数名称"
-        />
-      </el-form-item>
-      <el-form-item label="数据标签" prop="label">
-        <el-input v-model="formData.label" placeholder="请输入数据标签" />
-      </el-form-item>
-      <el-form-item label="数据键值" prop="value">
-        <el-input v-model="formData.value" placeholder="请输入数据键值" />
-      </el-form-item>
-      <el-form-item label="显示排序" prop="sort">
-        <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
-      </el-form-item>
-      <el-form-item label="状态" prop="status">
-        <el-radio-group v-model="formData.status">
-          <el-radio
-            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
-            :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item label="颜色类型" prop="colorType">
-        <el-select v-model="formData.colorType">
-          <el-option
-            v-for="item in colorTypeOptions"
-            :key="item.value"
-            :label="item.label + '(' + item.value + ')'"
-            :value="item.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="CSS Class" prop="cssClass">
-        <el-input v-model="formData.cssClass" placeholder="请输入 CSS Class" />
-      </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import * as DictDataApi from '@/api/system/dict/dict.data'
-import { CommonStatusEnum } from '@/utils/constants'
-
-defineOptions({ name: 'SystemDictDataForm' })
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined,
-  sort: undefined,
-  label: '',
-  value: '',
-  dictType: '',
-  status: CommonStatusEnum.ENABLE,
-  colorType: '',
-  cssClass: '',
-  remark: ''
-})
-const formRules = reactive({
-  label: [{ required: true, message: '数据标签不能为空', trigger: 'blur' }],
-  value: [{ required: true, message: '数据键值不能为空', trigger: 'blur' }],
-  sort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }],
-  status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
-})
-const formRef = ref() // 表单 Ref
-
-// 数据标签回显样式
-const colorTypeOptions = readonly([
-  {
-    value: 'default',
-    label: '默认'
-  },
-  {
-    value: 'primary',
-    label: '主要'
-  },
-  {
-    value: 'success',
-    label: '成功'
-  },
-  {
-    value: 'info',
-    label: '信息'
-  },
-  {
-    value: 'warning',
-    label: '警告'
-  },
-  {
-    value: 'danger',
-    label: '危险'
-  }
-])
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number, dictType?: string) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  if (dictType) {
-    formData.value.dictType = dictType
-  }
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await DictDataApi.getDictData(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as DictDataApi.DictDataVO
-    if (formType.value === 'create') {
-      await DictDataApi.createDictData(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await DictDataApi.updateDictData(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    sort: undefined,
-    label: '',
-    value: '',
-    dictType: '',
-    status: CommonStatusEnum.ENABLE,
-    colorType: '',
-    cssClass: '',
-    remark: ''
-  }
-  formRef.value?.resetFields()
-}
-</script>
+<template>
+	<Dialog v-model="dialogVisible" :title="dialogTitle">
+		<el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules" label-width="80px">
+			<el-row :gutter="30">
+				<el-col span="12">
+					<el-form-item label="字典类型" prop="type">
+						<el-input v-model="formData.dictType" :disabled="typeof formData.id !== 'undefined'"
+							placeholder="请输入参数名称" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="数据标签" prop="label">
+						<el-input v-model="formData.label" placeholder="请输入数据标签" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-row :gutter="30">
+				<el-col span="12">
+					<el-form-item label="数据键值" prop="value">
+						<el-input v-model="formData.value" placeholder="请输入数据键值" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="显示排序" prop="sort">
+						<el-input-number v-model="formData.sort" :min="0" controls-position="right" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-row :gutter="30">
+
+				<el-col span="12">
+					<el-form-item label="颜色类型" prop="colorType">
+						<el-select v-model="formData.colorType">
+							<el-option v-for="item in colorTypeOptions" :key="item.value"
+								:label="item.label + '(' + item.value + ')'" :value="item.value" />
+						</el-select>
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="状态" prop="status">
+						<el-radio-group v-model="formData.status">
+							<el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value"
+								:label="dict.value">
+								{{ dict.label }}
+							</el-radio>
+						</el-radio-group>
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-form-item label="CSS Class" prop="cssClass">
+				<el-input v-model="formData.cssClass" placeholder="请输入 CSS Class" />
+			</el-form-item>
+			<el-form-item label="备注" prop="remark">
+				<el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+	import * as DictDataApi from '@/api/system/dict/dict.data'
+	import { CommonStatusEnum } from '@/utils/constants'
+
+	defineOptions({ name: 'SystemDictDataForm' })
+
+	const { t } = useI18n() // 国际化
+	const message = useMessage() // 消息弹窗
+
+	const dialogVisible = ref(false) // 弹窗的是否展示
+	const dialogTitle = ref('') // 弹窗的标题
+	const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+	const formType = ref('') // 表单的类型:create - 新增;update - 修改
+	const formData = ref({
+		id: undefined,
+		sort: undefined,
+		label: '',
+		value: '',
+		dictType: '',
+		status: CommonStatusEnum.ENABLE,
+		colorType: '',
+		cssClass: '',
+		remark: ''
+	})
+	const formRules = reactive({
+		label: [{ required: true, message: '数据标签不能为空', trigger: 'blur' }],
+		value: [{ required: true, message: '数据键值不能为空', trigger: 'blur' }],
+		sort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }],
+		status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
+	})
+	const formRef = ref() // 表单 Ref
+
+	// 数据标签回显样式
+	const colorTypeOptions = readonly([
+		{
+			value: 'default',
+			label: '默认'
+		},
+		{
+			value: 'primary',
+			label: '主要'
+		},
+		{
+			value: 'success',
+			label: '成功'
+		},
+		{
+			value: 'info',
+			label: '信息'
+		},
+		{
+			value: 'warning',
+			label: '警告'
+		},
+		{
+			value: 'danger',
+			label: '危险'
+		}
+	])
+
+	/** 打开弹窗 */
+	const open = async (type : string, id ?: number, dictType ?: string) => {
+		dialogVisible.value = true
+		dialogTitle.value = t('action.' + type)
+		formType.value = type
+		resetForm()
+		if (dictType) {
+			formData.value.dictType = dictType
+		}
+		// 修改时,设置数据
+		if (id) {
+			formLoading.value = true
+			try {
+				formData.value = await DictDataApi.getDictData(id)
+			} finally {
+				formLoading.value = false
+			}
+		}
+	}
+	defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+	/** 提交表单 */
+	const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+	const submitForm = async () => {
+		// 校验表单
+		if (!formRef) return
+		const valid = await formRef.value.validate()
+		if (!valid) return
+		// 提交请求
+		formLoading.value = true
+		try {
+			const data = formData.value as DictDataApi.DictDataVO
+			if (formType.value === 'create') {
+				await DictDataApi.createDictData(data)
+				message.success(t('common.createSuccess'))
+			} else {
+				await DictDataApi.updateDictData(data)
+				message.success(t('common.updateSuccess'))
+			}
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		formData.value = {
+			id: undefined,
+			sort: undefined,
+			label: '',
+			value: '',
+			dictType: '',
+			status: CommonStatusEnum.ENABLE,
+			colorType: '',
+			cssClass: '',
+			remark: ''
+		}
+		formRef.value?.resetFields()
+	}
+</script>
+<style lang="scss" scoped>
+	.el-row {
+
+		.el-input,
+		.el-select,
+		.el-input-number,
+		.el-date-editor {
+			width: 170px;
+		}
+	}
+</style>