Ver Fonte

套餐模块编写

“陈博论” há 10 meses atrás
pai
commit
a00297ebe9

+ 1 - 0
web/src/assets/icons/combo.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1766050217682" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7560" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M869.4 263.7H698L597.9 158.4c-9.6-10.1-25.7-10.5-35.8-0.9-10.1 9.6-10.5 25.6-0.9 35.8l66.9 70.4H401l66.9-70.4c9.6-10.1 9.2-26.1-0.9-35.8-10.1-9.6-26.2-9.2-35.8 0.9l-100 105.3H159.8c-14 0-25.3 11.3-25.3 25.3v181.6c0 14 11.3 25.3 25.3 25.3h40V827c0 14 11.3 25.3 25.3 25.3H804c14 0 25.3-11.3 25.3-25.3V495.9h40c14 0 25.3-11.3 25.3-25.3V289c0.1-14-11.3-25.3-25.2-25.3z m-90.7 538H250.5V495.9h528.2v305.8z m65.4-356.4h-659v-131H844v131z" p-id="7561"></path><path d="M347.5 738.6c14 0 25.3-11.3 25.3-25.3V572.9c0-14-11.3-25.3-25.3-25.3s-25.3 11.3-25.3 25.3v140.4c0 14 11.3 25.3 25.3 25.3zM514.6 738.6c14 0 25.3-11.3 25.3-25.3V572.9c0-14-11.3-25.3-25.3-25.3s-25.3 11.3-25.3 25.3v140.4c0 14 11.3 25.3 25.3 25.3zM681.7 738.6c14 0 25.3-11.3 25.3-25.3V572.9c0-14-11.3-25.3-25.3-25.3s-25.3 11.3-25.3 25.3v140.4c0 14 11.3 25.3 25.3 25.3z" p-id="7562"></path></svg>

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
web/src/assets/icons/product.svg


+ 330 - 0
web/src/views/backend/application/combo/detail.vue

@@ -0,0 +1,330 @@
+<template>
+    <el-dialog
+        v-model="visible"
+        class="ba-operate-dialog"
+        width="950px"
+        :close-on-click-modal="false"
+        :destroy-on-close="true"
+        @close="handleClose"
+    >
+        <template #header>
+            <div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
+                套餐详情 --- {{ detailData?.name || '-' }}
+            </div>
+        </template>
+
+        <el-scrollbar class="ba-table-form-scrollbar">
+            <div class="detail-wrapper">
+                <el-descriptions
+                    v-if="detailData"
+                    :column="3"
+                    border
+                    class="detail-base-info"
+                    size="small"
+                >
+                    <el-descriptions-item label="套餐名称">
+                        {{ detailData.name }}
+                    </el-descriptions-item>
+                    <el-descriptions-item label="当前库存状态">
+                        <el-tag :type="inventoryStatusTag" effect="plain">
+                            {{ inventoryStatusText }}
+                        </el-tag>
+                    </el-descriptions-item>
+                    <el-descriptions-item label="总剩余库存">
+                        {{ totalStock }}
+                    </el-descriptions-item>
+                    <el-descriptions-item label="已卖数量">
+                        {{ detailData.sold_quantity }}
+                    </el-descriptions-item>
+                    <el-descriptions-item label="创建时间">
+                        {{ detailData.create_time }}
+                    </el-descriptions-item>
+                </el-descriptions>
+
+                <!-- 规格明细表格 -->
+                <div class="spec-table-title">
+                    套餐规格明细
+                </div>
+                <el-table
+                    v-if="specList.length"
+                    :data="specList"
+                    border
+                    size="small"
+                    style="width: 100%"
+                >
+                    <el-table-column
+                        prop="specName"
+                        label="规格名称"
+                        align="center"
+                        min-width="120"
+                    />
+                    <el-table-column
+                        label="库存状态"
+                        align="center"
+                        min-width="120"
+                    >
+                        <template #default="{ row }">
+                            <el-tag :type="getRowStatusTag(row.stock)" effect="plain">
+                                {{ getRowStatusText(row.stock) }}
+                            </el-tag>
+                        </template>
+                    </el-table-column>
+                    <el-table-column
+                        label="剩余库存"
+                        align="center"
+                        min-width="140"
+                    >
+                        <template #default="{ row, $index }">
+                            <el-input-number
+                                v-model.number="specList[$index].stock"
+                                :min="0"
+                                :max="999999"
+                                controls-position="right"
+                                size="small"
+                            />
+                        </template>
+                    </el-table-column>
+                    <el-table-column
+                        prop="price"
+                        label="价格(元)"
+                        align="center"
+                        min-width="120"
+                    />
+                    <el-table-column
+                        label="套餐类型"
+                        align="center"
+                        min-width="120"
+                    >
+                        <template #default="{ row }">
+                            {{ packageTypeText(row.packageType) }}
+                        </template>
+                    </el-table-column>
+                    <el-table-column
+                        label="收费方式"
+                        align="center"
+                        min-width="140"
+                    >
+                        <template #default="{ row }">
+                            {{ chargeTypeText(row.chargeType) }}
+                            <span v-if="showQuantity(row.chargeType) && row.quantity">
+                                ({{ row.quantity }}{{ getUnitLabel(row.chargeType) }})
+                            </span>
+                        </template>
+                    </el-table-column>
+                    <el-table-column
+                        prop="remark"
+                        label="说明"
+                        align="center"
+                        min-width="200"
+                        show-overflow-tooltip
+                    />
+                </el-table>
+
+                <!-- 汇总信息 -->
+                <div class="summary-row">
+                    <div class="summary-item">
+                        <span class="label">规格数量:</span>
+                        <span>{{ specList.length }}</span>
+                    </div>
+                    <div class="summary-item">
+                        <span class="label">总剩余库存:</span>
+                        <span class="value-number">{{ totalStock }}</span>
+                    </div>
+                    <div class="summary-item">
+                        <span class="label">整体库存状态:</span>
+                        <el-tag :type="inventoryStatusTag" effect="plain">
+                            {{ inventoryStatusText }}
+                        </el-tag>
+                    </div>
+                </div>
+            </div>
+        </el-scrollbar>
+
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button size="large" class="cancel-btn" @click="handleClose">
+                    取消
+                </el-button>
+                <el-button
+                    size="large"
+                    type="primary"
+                    class="submit-btn"
+                    @click="handleConfirm"
+                >
+                    确认修改库存
+                </el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { computed, reactive, ref } from 'vue'
+
+interface SpecificationItem {
+    specName: string
+    price: number | undefined
+    stock: number | undefined
+    packageType: string
+    chargeType: string
+    quantity?: number | undefined
+    remark?: string
+}
+
+interface DetailRow {
+    id: number
+    name: string
+    inventory_status: string
+    remaining_stock: number
+    sold_quantity: number
+    create_time: string
+    specificationsList?: SpecificationItem[]
+}
+
+const visible = ref(false)
+const detailData = ref<DetailRow | null>(null)
+const specList = reactive<SpecificationItem[]>([])
+
+const emits = defineEmits<{
+    (e: 'confirm', payload: { id: number; specificationsList: SpecificationItem[]; totalStock: number }): void
+}>()
+
+const open = (row: DetailRow) => {
+    detailData.value = { ...row }
+
+    // 重置规格明细
+    specList.splice(0, specList.length)
+    const list = (row.specificationsList || []) as SpecificationItem[]
+    list.forEach((item) => {
+        specList.push({ ...item })
+    })
+
+    visible.value = true
+}
+
+const handleClose = () => {
+    visible.value = false
+}
+
+const totalStock = computed(() =>
+    specList.reduce((sum, item) => sum + (Number(item.stock) || 0), 0)
+)
+
+const inventoryStatusText = computed(() => {
+    if (totalStock.value === 0) return '售罄'
+    if (totalStock.value < 10) return '即将售罄'
+    return '充足'
+})
+
+const inventoryStatusTag = computed(() => {
+    if (totalStock.value === 0) return 'danger'
+    if (totalStock.value < 10) return 'warning'
+    return 'success'
+})
+
+const getRowStatusText = (stock?: number) => {
+    const value = Number(stock) || 0
+    if (value === 0) return '售罄'
+    if (value < 10) return '即将售罄'
+    return '充足'
+}
+
+const getRowStatusTag = (stock?: number) => {
+    const value = Number(stock) || 0
+    if (value === 0) return 'danger'
+    if (value < 10) return 'warning'
+    return 'success'
+}
+
+const packageTypeText = (type: string) => {
+    if (type === '2') return '团队套餐'
+    return '个人套餐'
+}
+
+const chargeTypeText = (type: string) => {
+    const map: Record<string, string> = {
+        '1': '按次数',
+        '2': '按积分',
+        '3': '日卡',
+        '4': '周卡',
+        '5': '月卡',
+        '6': '季卡',
+        '7': '年卡',
+    }
+    return map[type] || '-'
+}
+
+const showQuantity = (chargeType: string) => {
+    return chargeType === '1' || chargeType === '2'
+}
+
+const getUnitLabel = (chargeType: string) => {
+    if (chargeType === '1') return '次'
+    if (chargeType === '2') return '分'
+    return ''
+}
+
+const handleConfirm = () => {
+    if (!detailData.value) return
+    emits('confirm', {
+        id: detailData.value.id,
+        specificationsList: specList.map((item) => ({ ...item })),
+        totalStock: totalStock.value,
+    })
+    handleClose()
+}
+
+defineExpose({ open })
+</script>
+
+<style scoped lang="scss">
+.detail-wrapper {
+    padding: 12px 16px 4px;
+}
+
+.detail-base-info {
+    margin-bottom: 16px;
+}
+
+.spec-table-title {
+    margin-bottom: 8px;
+    font-weight: 600;
+    font-size: 14px;
+    color: #333;
+}
+
+.summary-row {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: flex-end;
+    gap: 16px;
+    margin-top: 12px;
+    padding-top: 8px;
+    border-top: 1px dashed #e5e5e5;
+
+    .summary-item {
+        display: inline-flex;
+        align-items: center;
+        font-size: 13px;
+
+        .label {
+            color: #666;
+            margin-right: 4px;
+        }
+
+        .value-number {
+            font-weight: 600;
+            color: #333;
+        }
+    }
+}
+
+.dialog-footer {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    gap: 10px;
+}
+</style>
+
+

+ 154 - 9
web/src/views/backend/application/combo/index.vue

@@ -49,17 +49,26 @@
                 </el-table-column>
             </template>
         </Table>
+
+        <!-- 添加编辑弹框 -->
+        <PopupForm ref="popupFormRef" @success="handleFormSuccess" />
+
+        <!-- 套餐详情 / 库存调整弹框 -->
+        <Detail ref="detailRef" @confirm="handleDetailConfirm" />
+
     </div>
 </template>
 
 <script setup lang="ts">
 // 引入响应式工具与生命周期钩子
-import { onMounted, provide, reactive } from 'vue'
+import { onMounted, provide, reactive, ref } from 'vue'
 // 引入表格封装类
 import baTableClass from '/@/utils/baTable'
 // 引入通用表格组件
 import Table from '/@/components/table/index.vue'
 import TableHeader from '/@/components/table/header/index.vue'
+import PopupForm from './popupForm.vue'
+import Detail from './detail.vue'
 // 引入接口封装
 import { baTableApi } from '/@/api/common'
 // 引入国际化
@@ -75,6 +84,9 @@ defineOptions({
 // 国际化方法
 const { t } = useI18n()
 
+const popupFormRef = ref<InstanceType<typeof PopupForm>>()
+const detailRef = ref<InstanceType<typeof Detail>>()
+
 // 筛选表单数据,包含套餐名称与库存状态
 const searchForm = reactive({
     name: '', // 套餐名称关键词
@@ -88,7 +100,8 @@ const inventoryOptions = [
     { label: '售罄', value: '3' },
 ]
 
-// 静态模拟数据,来源于原型图
+// 静态模拟数据,来源于原型图 + 规格示例数据
+// 规格字段与 popupForm 中的 SpecificationItem 一致,方便后续联动编辑
 const baseData = [
     {
         id: 1,
@@ -97,6 +110,26 @@ const baseData = [
         remaining_stock: 120,
         sold_quantity: 12,
         create_time: '2025-10-19',
+        specificationsList: [
+            {
+                specName: '个人基础版',
+                price: 99,
+                stock: 80,
+                packageType: '1', // 个人套餐
+                chargeType: '1', // 按次数
+                quantity: 10,
+                remark: '适合个人日常使用',
+            },
+            {
+                specName: '个人进阶版',
+                price: 199,
+                stock: 40,
+                packageType: '1',
+                chargeType: '2', // 按积分
+                quantity: 100,
+                remark: '附赠额外积分',
+            },
+        ],
     },
     {
         id: 2,
@@ -105,6 +138,26 @@ const baseData = [
         remaining_stock: 12,
         sold_quantity: 88,
         create_time: '2025-10-19',
+        specificationsList: [
+            {
+                specName: '团队标准版',
+                price: 599,
+                stock: 8,
+                packageType: '2', // 团队套餐
+                chargeType: '3', // 日卡
+                quantity: undefined,
+                remark: '适合小团队短期使用',
+            },
+            {
+                specName: '团队增强版',
+                price: 1599,
+                stock: 4,
+                packageType: '2',
+                chargeType: '5', // 月卡
+                quantity: undefined,
+                remark: '按月计费,支持多人使用',
+            },
+        ],
     },
     {
         id: 3,
@@ -113,6 +166,17 @@ const baseData = [
         remaining_stock: 0,
         sold_quantity: 120,
         create_time: '2025-10-19',
+        specificationsList: [
+            {
+                specName: '体验版',
+                price: 49,
+                stock: 0,
+                packageType: '1',
+                chargeType: '1',
+                quantity: 3,
+                remark: '促销活动已售罄',
+            },
+        ],
     },
 ]
 
@@ -133,7 +197,12 @@ const baTable = new baTableClass(
                 effect: 'plain',
                 minWidth: 120,
             },
-            { label: '剩余库存', prop: 'remaining_stock', align: 'center', minWidth: 100 },
+            {
+                label: '剩余库存',
+                prop: 'remaining_stock',
+                align: 'center',
+                minWidth: 140,
+            },
             { label: '已卖数量', prop: 'sold_quantity', align: 'center', minWidth: 100 },
             { label: '创建时间', prop: 'create_time', align: 'center', minWidth: 160 },
             { label: '操作', align: 'center', render: 'slot', slotName: 'operate', operator: false, minWidth: 180 },
@@ -174,19 +243,94 @@ const del = (row:TableRow) => {
     ElMessage.success(`已删除套餐:${row.name}`)
 }
 
-// 编辑动作,展示提示
+// 编辑动作:打开弹框并带入当前行
 const onEdit = (row:TableRow ) => {
-    ElMessage.info(`编辑套餐:${row.name}`)
+    popupFormRef.value?.editDialog(row as any)
 }
 
 // 新增动作,展示提示
 const onAdd = () => {
-    ElMessage.info('点击添加新套餐')
+    popupFormRef.value?.openDialog()
 }
 
-// 套餐详情动作,展示提示
-const viewDetail = (row: Record<string, any>) => {
-    ElMessage.success(`查看套餐详情:${row.name}`)
+// 处理弹框提交成功(新增 / 编辑)
+const handleFormSuccess = (payload: {
+    id: number | null
+    type: 'add' | 'edit'
+    name: string
+    specificationsList: any[]
+}) => {
+    const { id, type, name, specificationsList } = payload
+
+    // 根据规格列表计算总库存
+    const totalStock = (specificationsList || []).reduce(
+        (sum, item) => sum + (Number(item.stock) || 0),
+        0
+    )
+
+    // 根据库存计算库存状态
+    let inventoryStatus = '1' // 充足
+    if (totalStock === 0) {
+        inventoryStatus = '3' // 售罄
+    } else if (totalStock < 10) {
+        inventoryStatus = '2' // 即将售罄
+    }
+
+    if (type === 'edit' && id != null) {
+        // 编辑:更新静态数据中的对应项
+        const target = baseData.find((item) => item.id === id)
+        if (target) {
+            target.name = name
+            target.remaining_stock = totalStock
+            target.inventory_status = inventoryStatus
+            // 把规格明细也一起带上,方便下次再编辑
+            ;(target as any).specificationsList = specificationsList
+            ElMessage.success(`已更新套餐:${name}`)
+        }
+    } else {
+        // 新增:简单提示(此处仍以静态数据为主,不追加行)
+        ElMessage.success(`已添加套餐:${name}`)
+    }
+
+    // 重新根据筛选条件刷新一次表格
+    baTable.getData()
+}
+
+// 套餐详情动作,弹出详情弹框,可从按钮或库存单元格进入
+const viewDetail = (row: Record<string, any>, fromStockClick = false) => {
+    if (!row) return
+    detailRef.value?.open(row as any)
+    if (!fromStockClick) {
+        ElMessage.success(`查看套餐详情:${row.name}`)
+    }
+}
+
+// 详情弹框确认修改库存
+const handleDetailConfirm = (payload: {
+    id: number
+    specificationsList: any[]
+    totalStock: number
+}) => {
+    const target = baseData.find((item) => item.id === payload.id)
+    if (!target) return
+
+    // 更新规格明细
+    ;(target as any).specificationsList = payload.specificationsList
+
+    // 更新总库存
+    target.remaining_stock = payload.totalStock
+
+    // 根据总库存刷新库存状态
+    if (payload.totalStock === 0) {
+        target.inventory_status = '3'
+    } else if (payload.totalStock < 10) {
+        target.inventory_status = '2'
+    } else {
+        target.inventory_status = '1'
+    }
+
+    ElMessage.success(`已更新「${target.name}」的库存信息`)
+    baTable.getData()
 }
 
 // 输入或选择变化时触发过滤
@@ -210,3 +354,4 @@ onMounted(() => {
     margin: 0 6px;
 }
 </style>
+

+ 326 - 18
web/src/views/backend/application/combo/popupForm.vue

@@ -1,7 +1,7 @@
 <template>
 	<!-- 对话框表单 -->
 	<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :destroy-on-close="true" v-model="dialogVisible"
-		@close="handleClose" width="600px">
+		@close="handleClose" width="880px">
 		<template #header>
 			<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
 				套餐配置
@@ -13,18 +13,102 @@
 					<el-form-item label="套餐名称" prop="name">
 						<el-input v-model="formData.name" placeholder="请输入套餐名称" />
 					</el-form-item>
+					<!-- 套餐规格配置 -->
+					<el-form-item prop="specificationsList">
+						<div class="spec-section">
+							<div class="spec-header">
+								<span class="spec-title">套餐规格:</span>
+								<el-button
+									type="primary"
+									size="small"
+									plain
+									class="spec-add-btn"
+									@click="addSpecification"
+								>
+									<span class="spec-add-icon">+</span>
+									<span>新增规格</span>
+								</el-button>
+							</div>
+							<div class="spec-row" v-for="(item, index) in formData.specificationsList" :key="index">
+								<!-- 第一行:规格名称 / 价格 / 库存 / 套餐类型 -->
+								<div class="spec-row-line spec-row-line--top">
+									<el-input
+										v-model="item.specName"
+										placeholder="规格名称"
+										class="spec-input spec-name"
+									/>
+									<div class="spec-group">
+										<el-input-number
+											v-model.number="item.price"
+											:min="0"
+											placeholder="价格"
+											class="spec-input spec-price"
+										/>
+										<span class="spec-unit">元</span>
+									</div>
+									<el-select v-model="item.packageType" placeholder="套餐类型"
+										class="spec-input spec-select">
+										<el-option v-for="opt in packageTypeOptions" :key="opt.value" :label="opt.label"
+											:value="opt.value" />
+									</el-select>
+									<div class="spec-group">
+										<span class="spec-label">库存:</span>
+										<el-input-number v-model.number="item.stock" :min="0" placeholder="库存数量"
+											class="spec-input"  />
+									</div>
+
+								</div>
+
+								<!-- 第二行:计费方式 / 次数 / 说明 / 删除 -->
+								<div class="spec-row-line spec-row-line--bottom">
+									<el-select
+										v-model="item.chargeType"
+										placeholder="计费方式"
+										class="spec-input spec-select"
+									>
+										<el-option
+											v-for="opt in chargeTypeOptions"
+											:key="opt.value"
+											:label="opt.label"
+											:value="opt.value"
+										/>
+									</el-select>
+									<div class="spec-group" v-if="showQuantity(item.chargeType)">
+										<el-input-number
+											v-model.number="item.quantity"
+											:min="0"
+											placeholder="数量"
+											class="spec-input spec-quantity"
+										/>
+										<span class="spec-unit">{{ getUnitLabel(item.chargeType) }}</span>
+									</div>
+									<el-input
+										v-model="item.remark"
+										placeholder="说明"
+										class="spec-input spec-remark"
+									/>
+									<el-button
+										type="danger"
+										size="small"
+										text
+										class="spec-delete-btn"
+										@click="removeSpecification(index)"
+									>
+										<span class="spec-delete-icon">×</span>
+										<span>删除</span>
+									</el-button>
+								</div>
+							</div>
+						</div>
+					</el-form-item>
 				</el-form>
 			</div>
 		</el-scrollbar>
 		<template #footer>
 			<div class="dialog-footer">
 				<el-button @click="handleClose" size="large" class="cancel-btn">{{ t('Cancel') }}</el-button>
-				<el-button @click="onSubmit()" type="primary" size="large" :loading="loading" class="submit-btn">
-					<el-icon>
-						<Check />
-					</el-icon>
-					确定
-				</el-button>
+				<el-button @click="onSubmit()" type="primary" size="large" :loading="loading"
+					class="submit-btn">确定</el-button>
 			</div>
 		</template>
 	</el-dialog>
@@ -33,7 +117,6 @@
 <script setup lang="ts">
 import { ref, reactive, computed } from 'vue'
 import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
-import { Check } from '@element-plus/icons-vue';
 import { useI18n } from 'vue-i18n'
 
 const { t } = useI18n()
@@ -41,7 +124,9 @@ const { t } = useI18n()
 // 显示隐藏
 const dialogVisible = ref(false)
 // 类型:add、edit
-const dialogType = ref('add')
+const dialogType = ref<'add' | 'edit'>('add')
+// 当前编辑的套餐 ID(新增时为 null)
+const currentEditId = ref<number | null>(null)
 
 const loading = ref(false)
 
@@ -52,7 +137,7 @@ const emits = defineEmits(['success'])
 
 const formData = reactive({
 	name: '',
-	specificationsList:[],
+	specificationsList: [] as SpecificationItem[],
 })
 
 
@@ -63,27 +148,68 @@ const rules: FormRules = {
 		{ required: true, message: '请输入套餐名称', trigger: 'change' }
 	],
 	specificationsList: [
-		{ required: true, message: '请选择规格', trigger: 'blur' }
+		{
+			validator: (_rule, value, callback) => {
+				if (!value || !value.length) {
+					return callback(new Error('请至少添加一个套餐规格'))
+				}
+				const invalid = value.some((item: SpecificationItem) => {
+					const needQuantity = showQuantity(item.chargeType)
+					return (
+						!item.specName ||
+						item.price == null ||
+						item.stock == null ||
+						!item.packageType ||
+						!item.chargeType ||
+						(needQuantity && item.quantity == null)
+					)
+				})
+				if (invalid) {
+					return callback(new Error('请完整填写每一行套餐规格'))
+				}
+				return callback()
+			},
+			trigger: 'change',
+		},
 	]
 }
 
-// 打开对话框
+// 打开对话框(新增)
 const openDialog = () => {
 	dialogVisible.value = true
 	dialogType.value = 'add'
+	currentEditId.value = null
 	resetForm()
 
 }
 
-// 编辑
-const editDialog = (row: TableRow) => {
+// 编辑:接收列表行数据,回填到表单
+const editDialog = (row: TableRow & { id?: number; name?: string; specificationsList?: SpecificationItem[] }) => {
 	dialogVisible.value = true
 	dialogType.value = 'edit'
+	currentEditId.value = row.id ?? null
+
+	// 清理校验状态
+	formRef.value?.clearValidate()
+
+	// 回填基础信息
+	formData.name = row.name ?? ''
+
+	// 回填规格列表
+	if (row.specificationsList && row.specificationsList.length) {
+		formData.specificationsList = row.specificationsList.map((item) => ({ ...item }))
+	} else {
+		formData.specificationsList = []
+		addSpecification()
+	}
 }
 
 // 重置表单
 const resetForm = () => {
 	formRef.value?.clearValidate()
+	formData.name = ''
+	formData.specificationsList = []
+	addSpecification()
 }
 
 // 提交表单
@@ -94,14 +220,15 @@ const onSubmit = async () => {
 		await formRef.value.validate()
 		loading.value = true
 
-		// 构建提交数据
+		// 构建提交数据,传给父组件(index.vue)
 		const submitData = {
+			id: currentEditId.value,
+			type: dialogType.value,
 			name: formData.name,
 			specificationsList: formData.specificationsList
 		}
 
-
-		emits('success')
+		emits('success', submitData)
 		handleClose()
 	} catch (error) {
 		ElMessage({
@@ -119,10 +246,191 @@ const handleClose = () => {
 	resetForm()
 }
 
+// 规格项类型
+interface SpecificationItem {
+	specName: string
+	price: number | undefined
+	stock: number | undefined
+	packageType: string
+	chargeType: string
+	quantity: number | undefined
+	remark: string
+}
+
+// 套餐类型选项(值改为数字编码,方便与后端枚举对应)
+const packageTypeOptions = [
+	{ label: '个人套餐', value: '1' },
+	{ label: '团队套餐', value: '2' },
+]
+
+// 计费方式选项(值使用数字编码,便于与后端对接)
+const chargeTypeOptions = [
+	{ label: '按次数', value: '1' },
+	{ label: '按积分', value: '2' },
+	{ label: '日卡', value: '3' },
+	{ label: '周卡', value: '4' },
+	{ label: '月卡', value: '5' },
+	{ label: '季卡', value: '6' },
+	{ label: '年卡', value: '7' },
+]
+
+// 新增一行规格
+const addSpecification = () => {
+	formData.specificationsList.push({
+		specName: '',
+		price: undefined,
+		stock: undefined,
+		packageType: packageTypeOptions[0].value,
+		chargeType: chargeTypeOptions[0].value,
+		quantity: undefined,
+		remark: '',
+	})
+}
+
+// 删除一行规格
+const removeSpecification = (index: number) => {
+	formData.specificationsList.splice(index, 1)
+}
+
+// 是否需要展示“次数 / 积分”等数量输入框
+const showQuantity = (chargeType: string) => {
+	// 1: 按次数,2: 按积分
+	return chargeType === '1' || chargeType === '2'
+}
+
+// 根据计费方式返回数量单位
+const getUnitLabel = (chargeType: string) => {
+	if (chargeType === '1') return '次'
+	if (chargeType === '2') return '分'
+	return ''
+}
+
 // 导出方法
-defineExpose({ openDialog,editDialog });
+defineExpose({ openDialog, editDialog });
 </script>
 
 <style scoped lang="scss">
+.form-container {
+	.spec-section {
+		width: 100%;
+		border: 1px solid #f0f0f0;
+		padding: 12px 16px;
+		border-radius: 4px;
+		background-color: #fafafa;
 
+		.spec-header {
+			display: flex;
+			justify-content: space-between;
+			align-items: center;
+			margin-bottom: 8px;
+
+			.spec-title {
+				font-weight: 600;
+				color: #333;
+			}
+
+			.spec-add-btn {
+				display: inline-flex;
+				align-items: center;
+				gap: 4px;
+				border-radius: 999px;
+				padding: 0 10px;
+
+				.spec-add-icon {
+					font-size: 14px;
+					transform: translateY(-1px);
+				}
+			}
+		}
+
+		.spec-row {
+			position: relative;
+			display: flex;
+			flex-direction: column;
+			row-gap: 6px;
+			margin-bottom: 12px;
+			padding: 10px 12px 10px 36px;
+			border-radius: 6px;
+			background-color: #fff;
+			box-shadow: 0 0 0 1px #f0f0f0;
+
+			.spec-row-line {
+				display: flex;
+				align-items: center;
+				column-gap: 10px;
+				flex-wrap: wrap;
+			}
+
+			.spec-input {
+				margin-right: 0;
+			}
+
+			.spec-name {
+				width: 140px;
+			}
+
+			.spec-price {
+				width: 120px;
+			}
+
+			.spec-stock {
+				width: 100px;
+			}
+
+			.spec-select {
+				width: 140px;
+			}
+
+			.spec-quantity {
+				width: 120px;
+			}
+
+			.spec-remark {
+				flex: 1;
+				min-width: 260px;
+			}
+
+			.spec-group {
+				display: inline-flex;
+				align-items: center;
+				column-gap: 4px;
+			}
+
+			.spec-delete-btn {
+				display: inline-flex;
+				align-items: center;
+				gap: 2px;
+				padding: 0 6px;
+
+				.spec-delete-icon {
+					font-size: 14px;
+					transform: translateY(-1px);
+				}
+			}
+
+			.spec-unit,
+			.spec-label {
+				color: #666;
+				white-space: nowrap;
+			}
+
+			.spec-badge {
+				position: absolute;
+				left: 10px;
+				top: 50%;
+				transform: translateY(-50%);
+				width: 20px;
+				height: 20px;
+				border-radius: 10px;
+				background: linear-gradient(135deg, #409eff, #66b1ff);
+				color: #fff;
+				font-size: 12px;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.8);
+			}
+		}
+	}
+}
 </style>

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff