|
@@ -1,7 +1,7 @@
|
|
|
<template>
|
|
<template>
|
|
|
<!-- 对话框表单 -->
|
|
<!-- 对话框表单 -->
|
|
|
<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :destroy-on-close="true" v-model="dialogVisible"
|
|
<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>
|
|
<template #header>
|
|
|
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
|
<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-form-item label="套餐名称" prop="name">
|
|
|
<el-input v-model="formData.name" placeholder="请输入套餐名称" />
|
|
<el-input v-model="formData.name" placeholder="请输入套餐名称" />
|
|
|
</el-form-item>
|
|
</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>
|
|
</el-form>
|
|
|
</div>
|
|
</div>
|
|
|
</el-scrollbar>
|
|
</el-scrollbar>
|
|
|
<template #footer>
|
|
<template #footer>
|
|
|
<div class="dialog-footer">
|
|
<div class="dialog-footer">
|
|
|
<el-button @click="handleClose" size="large" class="cancel-btn">{{ t('Cancel') }}</el-button>
|
|
<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>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
</el-dialog>
|
|
</el-dialog>
|
|
@@ -33,7 +117,6 @@
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { ref, reactive, computed } from 'vue'
|
|
import { ref, reactive, computed } from 'vue'
|
|
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
|
|
-import { Check } from '@element-plus/icons-vue';
|
|
|
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
|
|
|
|
const { t } = useI18n()
|
|
const { t } = useI18n()
|
|
@@ -41,7 +124,9 @@ const { t } = useI18n()
|
|
|
// 显示隐藏
|
|
// 显示隐藏
|
|
|
const dialogVisible = ref(false)
|
|
const dialogVisible = ref(false)
|
|
|
// 类型:add、edit
|
|
// 类型:add、edit
|
|
|
-const dialogType = ref('add')
|
|
|
|
|
|
|
+const dialogType = ref<'add' | 'edit'>('add')
|
|
|
|
|
+// 当前编辑的套餐 ID(新增时为 null)
|
|
|
|
|
+const currentEditId = ref<number | null>(null)
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
const loading = ref(false)
|
|
|
|
|
|
|
@@ -52,7 +137,7 @@ const emits = defineEmits(['success'])
|
|
|
|
|
|
|
|
const formData = reactive({
|
|
const formData = reactive({
|
|
|
name: '',
|
|
name: '',
|
|
|
- specificationsList:[],
|
|
|
|
|
|
|
+ specificationsList: [] as SpecificationItem[],
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
@@ -63,27 +148,68 @@ const rules: FormRules = {
|
|
|
{ required: true, message: '请输入套餐名称', trigger: 'change' }
|
|
{ required: true, message: '请输入套餐名称', trigger: 'change' }
|
|
|
],
|
|
],
|
|
|
specificationsList: [
|
|
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 = () => {
|
|
const openDialog = () => {
|
|
|
dialogVisible.value = true
|
|
dialogVisible.value = true
|
|
|
dialogType.value = 'add'
|
|
dialogType.value = 'add'
|
|
|
|
|
+ currentEditId.value = null
|
|
|
resetForm()
|
|
resetForm()
|
|
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 编辑
|
|
|
|
|
-const editDialog = (row: TableRow) => {
|
|
|
|
|
|
|
+// 编辑:接收列表行数据,回填到表单
|
|
|
|
|
+const editDialog = (row: TableRow & { id?: number; name?: string; specificationsList?: SpecificationItem[] }) => {
|
|
|
dialogVisible.value = true
|
|
dialogVisible.value = true
|
|
|
dialogType.value = 'edit'
|
|
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 = () => {
|
|
const resetForm = () => {
|
|
|
formRef.value?.clearValidate()
|
|
formRef.value?.clearValidate()
|
|
|
|
|
+ formData.name = ''
|
|
|
|
|
+ formData.specificationsList = []
|
|
|
|
|
+ addSpecification()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 提交表单
|
|
// 提交表单
|
|
@@ -94,14 +220,15 @@ const onSubmit = async () => {
|
|
|
await formRef.value.validate()
|
|
await formRef.value.validate()
|
|
|
loading.value = true
|
|
loading.value = true
|
|
|
|
|
|
|
|
- // 构建提交数据
|
|
|
|
|
|
|
+ // 构建提交数据,传给父组件(index.vue)
|
|
|
const submitData = {
|
|
const submitData = {
|
|
|
|
|
+ id: currentEditId.value,
|
|
|
|
|
+ type: dialogType.value,
|
|
|
name: formData.name,
|
|
name: formData.name,
|
|
|
specificationsList: formData.specificationsList
|
|
specificationsList: formData.specificationsList
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
- emits('success')
|
|
|
|
|
|
|
+ emits('success', submitData)
|
|
|
handleClose()
|
|
handleClose()
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
ElMessage({
|
|
ElMessage({
|
|
@@ -119,10 +246,191 @@ const handleClose = () => {
|
|
|
resetForm()
|
|
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>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
<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>
|
|
</style>
|