| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- <template>
- <div class="hotel-dish-spec-page">
- <AiCrudPage
- ref="crudRef"
- api="/hotel/dish-spec"
- :api-config="{
- list: 'get@/hotel/dish-spec/page',
- detail: 'get@/hotel/dish-spec/:id',
- add: 'post@/hotel/dish-spec',
- update: 'put@/hotel/dish-spec',
- delete: 'post@/hotel/dish-spec/remove/:id',
- }"
- :load-detail-on-edit="true"
- :search-schema="searchSchema"
- :columns="tableColumns"
- :edit-schema="editSchema"
- row-key="id"
- add-button-text="新增规格组"
- />
- <!-- 规格选项管理弹窗 -->
- <NModal
- v-model:show="optionDialogVisible"
- title="规格选项管理"
- preset="dialog"
- style="width: 650px;"
- :show-icon="false"
- :mask-closable="false"
- >
- <div v-if="currentGroup">
- <div class="mb-3 flex items-center gap-2">
- <span>规格组:<strong>{{ currentGroup.groupName }}</strong></span>
- <NTag :type="currentGroup.required === 1 ? 'error' : 'info'" size="small">
- {{ currentGroup.required === 1 ? '必选' : '可选' }}
- </NTag>
- </div>
- <div class="mb-3">
- <NButton type="primary" size="small" @click="openOptionForm('add')">
- 新增选项
- </NButton>
- </div>
- <NDataTable
- :columns="optionColumns"
- :data="optionList"
- :loading="optionLoading"
- :row-key="(row) => row.id"
- bordered
- size="small"
- />
- </div>
- </NModal>
- <!-- 规格选项表单弹窗 -->
- <NModal
- v-model:show="optionFormVisible"
- :title="optionFormTitle"
- preset="dialog"
- style="width: 460px;"
- :show-icon="false"
- :mask-closable="false"
- >
- <NForm
- ref="optionFormRef"
- :model="optionForm"
- :rules="optionRules"
- label-placement="left"
- label-width="80"
- class="mt-4"
- >
- <NFormItem label="选项名称" path="optionName">
- <NInput v-model:value="optionForm.optionName" placeholder="如:三分熟、黑椒汁" />
- </NFormItem>
- <NFormItem label="加价金额" path="priceExtra">
- <NInputNumber v-model:value="optionForm.priceExtra" :min="0" :max="9999" :precision="2" style="width: 100%;" placeholder="0表示不加价" />
- </NFormItem>
- <NFormItem label="排序号" path="sortOrder">
- <NInputNumber v-model:value="optionForm.sortOrder" :min="0" :max="999" style="width: 100%;" />
- </NFormItem>
- </NForm>
- <template #action>
- <NButton @click="optionFormVisible = false">
- 取消
- </NButton>
- <NButton type="primary" :loading="optionFormLoading" @click="submitOptionForm">
- 确定
- </NButton>
- </template>
- </NModal>
- </div>
- </template>
- <script setup>
- import { NButton, NDataTable, NForm, NFormItem, NInput, NInputNumber, NModal, NTag, useDialog, useMessage } from 'naive-ui'
- import { computed, h, onMounted, reactive, ref } from 'vue'
- import {
- createDishSpecOption,
- deleteDishSpecOption,
- getDishPage,
- getDishSpecOptionList,
- updateDishSpecOption,
- } from '@/api/hotel'
- import { AiCrudPage } from '@/components/ai-form'
- defineOptions({ name: 'HotelDishSpec' })
- const message = useMessage()
- const dialog = useDialog()
- const crudRef = ref(null)
- // ==================== 菜品下拉(仅加载在售菜品) ====================
- const dishOptions = ref([])
- async function loadDishOptions() {
- try {
- const res = await getDishPage({ pageNum: 1, pageSize: 10000, status: 'ON_SALE' })
- if (res.code === 200) {
- dishOptions.value = (res.data?.records || []).map(d => ({
- label: d.name,
- value: d.id,
- }))
- }
- }
- catch {
- // ignore
- }
- }
- // ==================== 搜索表单配置 ====================
- const searchSchema = computed(() => [
- {
- field: 'dishName',
- label: '菜品名称',
- type: 'input',
- props: { placeholder: '请输入菜品名称' },
- },
- {
- field: 'groupName',
- label: '规格组名称',
- type: 'input',
- props: { placeholder: '请输入规格组名称' },
- },
- ])
- // ==================== 表格列配置 ====================
- const tableColumns = computed(() => [
- {
- label: '序号',
- width: 60,
- render: (_, index) => h('span', {}, index + 1),
- },
- { prop: 'dishName', label: '菜品名称', minWidth: 150 },
- { prop: 'groupName', label: '规格组名称', minWidth: 150 },
- {
- prop: 'required',
- label: '是否必选',
- width: 100,
- render: row => h(NTag, { type: row.required === 1 ? 'error' : 'info', size: 'small' }, { default: () => row.required === 1 ? '必选' : '可选' }),
- },
- {
- label: '选项数量',
- width: 120,
- render: row => h('a', {
- class: 'text-primary cursor-pointer hover:opacity-80',
- onClick: () => openOptionDialog(row),
- }, `${(row.options || []).length} 个选项`),
- },
- { prop: 'sortOrder', label: '排序', width: 80 },
- {
- label: '操作',
- width: 220,
- render: (row) => {
- return h('div', { class: 'flex gap-3' }, [
- h('a', {
- class: 'text-primary cursor-pointer hover:opacity-80',
- onClick: () => crudRef.value?.handleEdit(row),
- }, '编辑'),
- h('a', {
- class: 'text-primary cursor-pointer hover:opacity-80',
- onClick: () => openOptionDialog(row),
- }, '管理选项'),
- h('a', {
- class: 'text-error cursor-pointer hover:opacity-80',
- onClick: () => crudRef.value?.handleDelete(row),
- }, '删除'),
- ])
- },
- },
- ])
- // ==================== 编辑表单配置 ====================
- const editSchema = computed(() => [
- {
- field: 'dishId',
- label: '关联菜品',
- type: 'select',
- rules: [{ required: true, message: '请选择关联菜品', trigger: 'change', type: 'number' }],
- props: { placeholder: '请选择菜品', filterable: true, clearable: true, options: dishOptions.value },
- },
- {
- field: 'groupName',
- label: '规格组名称',
- type: 'input',
- rules: [{ required: true, message: '请输入规格组名称', trigger: 'blur' }],
- props: { placeholder: '如:熟度、酱汁、份量' },
- },
- {
- field: 'required',
- label: '是否必选',
- type: 'radio',
- defaultValue: 0,
- props: {
- options: [
- { label: '必选', value: 1 },
- { label: '可选', value: 0 },
- ],
- },
- },
- {
- field: 'sortOrder',
- label: '排序',
- type: 'inputNumber',
- defaultValue: 0,
- props: { min: 0, step: 1, precision: 0, placeholder: '排序号' },
- },
- ])
- // ==================== 规格选项管理 ====================
- const optionDialogVisible = ref(false)
- const currentGroup = ref(null)
- const optionList = ref([])
- const optionLoading = ref(false)
- async function openOptionDialog(group) {
- currentGroup.value = group
- optionDialogVisible.value = true
- await loadOptions()
- }
- async function loadOptions() {
- if (!currentGroup.value)
- return
- optionLoading.value = true
- try {
- const res = await getDishSpecOptionList(currentGroup.value.id)
- if (res.code === 200) {
- optionList.value = res.data || []
- }
- }
- finally {
- optionLoading.value = false
- }
- }
- // ==================== 规格选项表格列 ====================
- const optionColumns = computed(() => [
- {
- title: '序号',
- width: 60,
- render: (_, index) => h('span', {}, index + 1),
- },
- { title: '选项名称', key: 'optionName', minWidth: 150, align: 'center' },
- {
- title: '加价金额',
- key: 'priceExtra',
- width: 120,
- align: 'center',
- render: row => h('span', {}, row.priceExtra > 0 ? `+¥${row.priceExtra}` : '-'),
- },
- { title: '排序号', key: 'sortOrder', width: 80, align: 'center' },
- {
- title: '操作',
- width: 130,
- align: 'center',
- render: row => h('div', { class: 'flex gap-3 justify-center' }, [
- h('a', { class: 'text-primary cursor-pointer hover:opacity-80', onClick: () => openOptionForm('edit', row) }, '编辑'),
- h('a', { class: 'text-error cursor-pointer hover:opacity-80', onClick: () => handleDeleteOption(row) }, '删除'),
- ]),
- },
- ])
- // ==================== 规格选项表单 ====================
- const optionFormVisible = ref(false)
- const optionFormTitle = ref('')
- const optionFormLoading = ref(false)
- const optionFormRef = ref(null)
- const optionFormMode = ref('add')
- const optionForm = reactive({ id: null, groupId: null, optionName: '', priceExtra: 0, sortOrder: 0 })
- const optionRules = {
- optionName: [{ required: true, message: '请输入选项名称', trigger: 'blur' }],
- }
- function openOptionForm(mode, row) {
- optionFormMode.value = mode
- if (mode === 'add') {
- optionFormTitle.value = '新增规格选项'
- optionForm.id = null
- optionForm.groupId = currentGroup.value?.id
- optionForm.optionName = ''
- optionForm.priceExtra = 0
- optionForm.sortOrder = 0
- }
- else {
- optionFormTitle.value = '编辑规格选项'
- optionForm.id = row.id
- optionForm.groupId = row.groupId
- optionForm.optionName = row.optionName
- optionForm.priceExtra = row.priceExtra ?? 0
- optionForm.sortOrder = row.sortOrder ?? 0
- }
- optionFormVisible.value = true
- }
- async function submitOptionForm() {
- try {
- await optionFormRef.value?.validate()
- }
- catch {
- return
- }
- optionFormLoading.value = true
- try {
- const data = { ...optionForm }
- let res
- if (optionFormMode.value === 'add') {
- res = await createDishSpecOption(data)
- }
- else {
- res = await updateDishSpecOption(data)
- }
- if (res.code === 200) {
- message.success(optionFormMode.value === 'add' ? '新增成功' : '修改成功')
- optionFormVisible.value = false
- loadOptions()
- // 刷新规格组列表以更新选项数量
- crudRef.value?.refresh()
- }
- else {
- message.error(res.msg || '操作失败')
- }
- }
- finally {
- optionFormLoading.value = false
- }
- }
- function handleDeleteOption(row) {
- dialog.warning({
- title: '确认删除',
- content: `确定删除规格选项「${row.optionName}」吗?`,
- positiveText: '确定',
- negativeText: '取消',
- onPositiveClick: async () => {
- const res = await deleteDishSpecOption(row.id)
- if (res.code === 200) {
- message.success('删除成功')
- loadOptions()
- // 刷新规格组列表以更新选项数量
- crudRef.value?.refresh()
- }
- else {
- message.error(res.msg || '删除失败')
- }
- },
- })
- }
- // ==================== 初始化 ====================
- onMounted(() => {
- loadDishOptions()
- })
- </script>
- <style scoped>
- .hotel-dish-spec-page {
- height: 100%;
- }
- </style>
|