菜品必点配置

This commit is contained in:
zjf committed 2026-04-09 18:18:51 +08:00
1 parent be639340aa
commit 1afc82492e
7 files changed
+569 -12

No files matched your search

@@ -0,0 +1,44 @@
import request from "/@/utils/request"
/** 分页查询分组(merchantId 不传则按数据权限返回可见范围全部) */
export function fetchPage(query?: object) {
return request({
url: "/canteen/canteenOnlineSpuChoiceGroup/page",
method: "get",
params: query,
})
}
/** 按商户查询分组列表(用于下拉;merchantId 必填) */
export function fetchList(merchantId: string | number) {
return request({
url: "/canteen/canteenOnlineSpuChoiceGroup/list",
method: "get",
params: { merchantId },
})
}
export function addObj(obj?: object) {
return request({
url: "/canteen/canteenOnlineSpuChoiceGroup",
method: "post",
data: obj,
})
}
export function putObj(obj?: object) {
return request({
url: "/canteen/canteenOnlineSpuChoiceGroup",
method: "put",
data: obj,
})
}
/** Body:id 数组 */
export function delObjs(ids: (string | number)[]) {
return request({
url: "/canteen/canteenOnlineSpuChoiceGroup",
method: "delete",
data: ids,
})
}
+2 -1
View File
@@ -154,10 +154,11 @@ export function backEndComponent(routes: any) {
*/
export function dynamicImport(dynamicViewsModules: Record<string, Function>, component: string) {
const keys = Object.keys(dynamicViewsModules)
const normalized = component?.startsWith("/") ? component : `/${component}`
const matchKeys = keys.filter((key) => {
const k = key.replace(/..\/views|../, "")
return k.startsWith(`${component}.vue`)
return k.startsWith(`${normalized}.vue`)
})
if (matchKeys?.length === 1) {
const matchKey = matchKeys[0]
@@ -78,9 +78,29 @@
/>
<template #footer>
<div class="flex items-center w-1/3">
<div class="w-16 text-blue-600 text-base font-bold">商户:</div>
<select-mch v-model="merchantId" :apiParams="{ types: '1' }" />
<div class="footer-batch-row">
<div class="footer-field">
<span class="footer-label">商户:</span>
<select-mch v-model="merchantId" :apiParams="{ types: '1' }" />
</div>
<div class="footer-field">
<span class="footer-label">择一/组合分组:</span>
<el-select
v-model="batchChoiceGroupId"
clearable
filterable
placeholder="默认不绑定"
:disabled="!merchantId"
style="width: 260px"
>
<el-option
v-for="g in choiceGroupOptions"
:key="String(g.id)"
:label="`${g.name}(ID:${g.id})`"
:value="String(g.id)"
/>
</el-select>
</div>
</div>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="dialogConfirm" :disabled="loading">确认</el-button>
@@ -96,6 +116,7 @@ import { useDict } from "/@/hooks/dict"
import { getAllSimple as getCanteenCategory } from "/@/api/canteen/canteenCategory"
import { useMessage } from "/@/hooks/message"
import { saveCanteenSpuBatch } from "/@/api/canteen/canteenOnlineSpu"
import { fetchList as fetchChoiceGroupList } from "/@/api/canteen/canteenOnlineSpuChoiceGroup"
const emit = defineEmits(["refresh"])
@@ -113,6 +134,22 @@ const total = ref<number>(0)
const dataList = ref<any[]>([]) // 对话框多选表格数据
const categoryOptions = ref<any>([]) // 菜品分类选项
const merchantId = ref<string>("")
const batchChoiceGroupId = ref<string | null>(null)
const choiceGroupOptions = ref<any[]>([])
watch(merchantId, async (mid) => {
batchChoiceGroupId.value = null
if (!mid) {
choiceGroupOptions.value = []
return
}
try {
const res: any = await fetchChoiceGroupList(mid)
choiceGroupOptions.value = Array.isArray(res.data) ? res.data : []
} catch {
choiceGroupOptions.value = []
}
})
onMounted(async () => {
try {
@@ -127,6 +164,7 @@ onMounted(async () => {
const openDialog = async () => {
dialogVisible.value = true
merchantId.value = ""
batchChoiceGroupId.value = null
getDataList(false)
tableRef.value.clearSelection()
}
@@ -183,6 +221,11 @@ const dialogConfirm = async () => {
status: item.status,
price: item.price,
stock: item.stock,
requiredChoice: 0,
choiceGroupId:
batchChoiceGroupId.value != null && batchChoiceGroupId.value !== ""
? String(batchChoiceGroupId.value)
: null,
}
})
@@ -214,3 +257,24 @@ defineExpose({
openDialog,
})
</script>
<style scoped>
.footer-batch-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px 24px;
margin-bottom: 12px;
text-align: left;
}
.footer-field {
display: flex;
align-items: center;
gap: 8px;
}
.footer-label {
font-weight: 600;
color: var(--el-color-primary);
white-space: nowrap;
}
</style>
+90 -8
View File
@@ -54,6 +54,31 @@
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="单品必选" prop="requiredChoice">
<el-switch v-model="form.requiredChoice" :active-value="1" :inactive-value="0" inline-prompt active-text="是" inactive-text="否" />
<span class="field-hint">与分组规则独立,仅强制本菜品必买时用</span>
</el-form-item>
</el-col>
<el-col :span="14" class="mb20">
<el-form-item label="择一/组合分组" prop="choiceGroupId">
<el-select
v-model="form.choiceGroupId"
clearable
filterable
placeholder="不绑定"
:disabled="!form.merchantId"
style="width: 100%"
>
<el-option
v-for="g in choiceGroupOptions"
:key="String(g.id)"
:label="`${g.name}(ID:${g.id})`"
:value="String(g.id)"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="商品详情" prop="description">
<editor v-if="visible" v-model:get-html="form.description" placeholder="请输入商品详情" />
@@ -75,10 +100,27 @@
import { useDict } from "/@/hooks/dict"
import { useMessage } from "/@/hooks/message"
import { getObj, addObj, putObj } from "/@/api/canteen/canteenOnlineSpu"
import { fetchList as fetchChoiceGroupList } from "/@/api/canteen/canteenOnlineSpuChoiceGroup"
import { displayPrice, storePrice } from "/@/utils/formatMoney"
const emit = defineEmits(["refresh"])
const choiceGroupOptions = ref<any[]>([])
const isHydrating = ref(false)
async function loadChoiceGroups(merchantId: string) {
if (!merchantId) {
choiceGroupOptions.value = []
return
}
try {
const res: any = await fetchChoiceGroupList(merchantId)
choiceGroupOptions.value = Array.isArray(res.data) ? res.data : []
} catch {
choiceGroupOptions.value = []
}
}
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
@@ -98,6 +140,8 @@ const form = reactive({
sliderPicUrls: "",
sort: 0,
status: "0",
requiredChoice: 0,
choiceGroupId: null as string | null,
price: 0,
marketPrice: 0,
stock: 0,
@@ -108,6 +152,18 @@ const form = reactive({
deptId: "",
})
watch(
() => form.merchantId,
async (mid) => {
if (isHydrating.value) {
await loadChoiceGroups(String(mid || ""))
return
}
form.choiceGroupId = null
await loadChoiceGroups(String(mid || ""))
},
)
// 定义校验规则
const dataRules = ref({
merchantId: [{ required: true, message: "商户不能为空", trigger: "blur" }],
@@ -142,7 +198,14 @@ const onSubmit = async () => {
try {
loading.value = true
const _pa = { ...form, price: storePrice(form.price), marketPrice: storePrice(form.marketPrice) }
const _pa = {
...form,
price: storePrice(form.price),
marketPrice: storePrice(form.marketPrice),
requiredChoice: form.requiredChoice === 1 ? 1 : 0,
choiceGroupId:
form.choiceGroupId != null && form.choiceGroupId !== "" ? String(form.choiceGroupId) : null,
}
form.id ? await putObj(_pa) : await addObj(_pa)
useMessage().success(form.id ? "修改成功" : "添加成功")
visible.value = false
@@ -159,14 +222,24 @@ const getcanteenOnlineSpuData = (id: string) => {
// 获取数据
loading.value = true
getObj(id)
.then((res: any) => {
.then(async (res: any) => {
const d = res.data
Object.assign(form, d, {
price: Number(displayPrice(d.price, "")),
marketPrice: Number(displayPrice(d.marketPrice, "")),
status: "" + d.status,
description: d.description ?? "",
})
isHydrating.value = true
try {
Object.assign(form, d, {
price: Number(displayPrice(d.price, "")),
marketPrice: Number(displayPrice(d.marketPrice, "")),
status: "" + d.status,
description: d.description ?? "",
requiredChoice: Number(d.requiredChoice) === 1 ? 1 : 0,
choiceGroupId: d.choiceGroupId != null && d.choiceGroupId !== "" ? String(d.choiceGroupId) : null,
})
await nextTick()
await loadChoiceGroups(String(form.merchantId || ""))
form.choiceGroupId = d.choiceGroupId != null && d.choiceGroupId !== "" ? String(d.choiceGroupId) : null
} finally {
isHydrating.value = false
}
})
.finally(() => {
loading.value = false
@@ -178,3 +251,12 @@ defineExpose({
openDialog,
})
</script>
<style scoped>
.field-hint {
margin-left: 12px;
font-size: 12px;
color: var(--el-text-color-secondary);
vertical-align: middle;
}
</style>
@@ -100,6 +100,17 @@
<dict-tag :options="spu_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column prop="requiredChoice" label="单品必选" show-overflow-tooltip min-width="90" align="center">
<template #default="scope">
<el-tag v-if="Number(scope.row.requiredChoice) === 1" type="warning" size="small">是</el-tag>
<el-tag v-else type="info" size="small">否</el-tag>
</template>
</el-table-column>
<el-table-column prop="choiceGroupId" label="分组ID" show-overflow-tooltip min-width="88" align="center">
<template #default="scope">
{{ scope.row.choiceGroupId != null && scope.row.choiceGroupId !== "" ? scope.row.choiceGroupId : "-" }}
</template>
</el-table-column>
<el-table-column prop="sort" label="排序字段" show-overflow-tooltip min-width="100" />
<el-table-column prop="createTime" label="创建时间" show-overflow-tooltip min-width="160" />
<el-table-column label="操作" width="150" fixed="right">
@@ -0,0 +1,186 @@
<template>
<el-dialog :title="form.id ? '编辑分组' : '新增分组'" v-model="visible" width="640px" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="130px" v-loading="loading">
<el-form-item label="商户" prop="merchantId">
<select-mch v-model="form.merchantId" :apiParams="{ types: '1' }" :disabled="!!form.id" />
</el-form-item>
<el-form-item v-if="form.id && form.deptId !== '' && form.deptId != null" label="部门ID">
<el-input :model-value="String(form.deptId)" disabled />
</el-form-item>
<el-form-item label="分组名称" prop="name">
<el-input v-model.trim="form.name" placeholder="如:主食、饮料" maxlength="64" show-word-limit />
</el-form-item>
<el-form-item label="种数下限 minKinds" prop="minKinds">
<el-input-number v-model="form.minKinds" :min="0" :precision="0" style="width: 100%" />
<div class="form-tip">0 表示整组可选(顾客可不选组内任何菜)</div>
</el-form-item>
<el-form-item label="种数上限 maxKinds" prop="maxKinds">
<el-input-number v-model="form.maxKinds" :min="0" :precision="0" style="width: 100%" />
<div class="form-tip">0 表示不限制种数上限</div>
</el-form-item>
<el-form-item label="件数合计下限" prop="minTotalQty">
<el-input-number v-model="form.minTotalQty" :min="0" :precision="0" style="width: 100%" />
<div class="form-tip">0 表示不校验</div>
</el-form-item>
<el-form-item label="件数合计上限" prop="maxTotalQty">
<el-input-number v-model="form.maxTotalQty" :min="0" :precision="0" style="width: 100%" />
<div class="form-tip">0 表示不限制</div>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :precision="0" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="CanteenOnlineSpuChoiceGroupDialog">
import type { FormRules } from "element-plus"
import { useMessage } from "/@/hooks/message"
import { addObj, putObj } from "/@/api/canteen/canteenOnlineSpuChoiceGroup"
const props = defineProps<{
listMerchantId?: string
}>()
const emit = defineEmits(["refresh"])
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const form = reactive({
id: "" as string | number | "",
merchantId: "",
name: "",
deptId: "" as string | number | "",
minKinds: 0,
maxKinds: 0,
minTotalQty: 0,
maxTotalQty: 0,
sort: 0,
})
const ruleKinds = (_rule: unknown, _val: unknown, callback: (e?: Error) => void) => {
const maxK = Number(form.maxKinds) || 0
const minK = Number(form.minKinds) || 0
if (maxK > 0 && minK > maxK) {
callback(new Error("maxKinds 不能小于 minKinds"))
return
}
callback()
}
const ruleQty = (_rule: unknown, _val: unknown, callback: (e?: Error) => void) => {
const maxQ = Number(form.maxTotalQty) || 0
const minQ = Number(form.minTotalQty) || 0
if (maxQ > 0 && minQ > maxQ) {
callback(new Error("maxTotalQty 不能小于 minTotalQty"))
return
}
callback()
}
const dataRules: FormRules = {
merchantId: [{ required: true, message: "请选择商户", trigger: "change" }],
name: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
minKinds: [{ validator: ruleKinds, trigger: "blur" }],
maxKinds: [{ validator: ruleKinds, trigger: "blur" }],
minTotalQty: [{ validator: ruleQty, trigger: "blur" }],
maxTotalQty: [{ validator: ruleQty, trigger: "blur" }],
}
const resetFormModel = () => {
form.id = ""
form.merchantId = ""
form.name = ""
form.deptId = ""
form.minKinds = 0
form.maxKinds = 0
form.minTotalQty = 0
form.maxTotalQty = 0
form.sort = 0
}
/** 编辑传入表格行;新建不传 */
const openDialog = (row?: Record<string, unknown>) => {
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields?.()
resetFormModel()
if (row && row.id != null && row.id !== "") {
form.id = row.id as string
form.merchantId = row.merchantId != null ? String(row.merchantId) : ""
form.name = (row.name as string) ?? ""
form.deptId = row.deptId != null ? (row.deptId as string | number) : ""
form.minKinds = Number(row.minKinds) || 0
form.maxKinds = Number(row.maxKinds) || 0
form.minTotalQty = Number(row.minTotalQty) || 0
form.maxTotalQty = Number(row.maxTotalQty) || 0
form.sort = Number(row.sort) || 0
} else {
form.merchantId = props.listMerchantId || ""
}
})
}
const buildPayload = () => {
const base = {
name: form.name.trim(),
merchantId: form.merchantId,
minKinds: Number(form.minKinds) || 0,
maxKinds: Number(form.maxKinds) || 0,
minTotalQty: Number(form.minTotalQty) || 0,
maxTotalQty: Number(form.maxTotalQty) || 0,
sort: Number(form.sort) || 0,
}
if (form.id) {
return {
...base,
id: form.id,
...(form.deptId !== "" && form.deptId != null ? { deptId: form.deptId } : {}),
}
}
return base
}
const onSubmit = async () => {
const valid = await dataFormRef.value?.validate().catch(() => false)
if (!valid) return
try {
loading.value = true
const payload = buildPayload()
if (form.id) {
await putObj(payload)
} else {
await addObj(payload)
}
useMessage().success(form.id ? "修改成功" : "添加成功")
visible.value = false
emit("refresh", String(form.merchantId || ""))
} catch (err: any) {
useMessage().error(err.msg || err.message || "操作失败")
} finally {
loading.value = false
}
}
defineExpose({
openDialog,
})
</script>
<style scoped>
.form-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.4;
margin-top: 4px;
}
</style>
@@ -0,0 +1,169 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch" class="mb8">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item label="商户" prop="merchantId">
<select-mch v-model="state.queryForm.merchantId" :apiParams="{ types: '1' }" clearable style="width: 260px" />
</el-form-item>
<el-form-item label="分组名称" prop="name">
<el-input v-model="state.queryForm.name" placeholder="请输入分组名称" clearable style="width: 220px" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList" v-auth="'canteen_canteenOnlineSpu_view'">查询</el-button>
<el-button icon="Refresh" @click="resetQuery" v-auth="'canteen_canteenOnlineSpu_view'">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button icon="folder-add" type="primary" class="ml10" v-auth="'canteen_canteenOnlineSpu_add'" @click="handleAdd">
新增分组
</el-button>
<el-button
plain
:disabled="multiple"
icon="Delete"
type="primary"
class="ml10"
v-auth="'canteen_canteenOnlineSpu_del'"
@click="handleDelete(selectObjs)"
>
删除
</el-button>
<right-toolbar v-model:showSearch="showSearch" class="ml10 mr20" style="float: right" @queryTable="getDataList" />
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle"
>
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="name" label="分组名称" show-overflow-tooltip min-width="120" />
<el-table-column prop="merchantId" label="商户" show-overflow-tooltip min-width="140">
<template #default="{ row }">
{{ merchantNameMap[String(row.merchantId)] ?? row.merchantId }}
</template>
</el-table-column>
<el-table-column prop="minKinds" label="种数下限" width="88" align="center" />
<el-table-column prop="maxKinds" label="种数上限" width="88" align="center" />
<el-table-column prop="minTotalQty" label="件数下限" width="88" align="center" />
<el-table-column prop="maxTotalQty" label="件数上限" width="88" align="center" />
<el-table-column prop="sort" label="排序" width="72" align="center" />
<el-table-column prop="createTime" label="创建时间" show-overflow-tooltip min-width="160" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="scope">
<el-button icon="edit-pen" text type="primary" v-auth="'canteen_canteenOnlineSpu_edit'" @click="openEdit(scope.row)">
编辑
</el-button>
<el-button icon="delete" text type="primary" v-auth="'canteen_canteenOnlineSpu_del'" @click="handleDelete([scope.row.id])">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
<FormDialog ref="formDialogRef" :list-merchant-id="state.queryForm.merchantId" @refresh="onRefresh" />
</div>
</template>
<script setup lang="ts" name="CanteenOnlineSpuChoiceGroup">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { fetchPage, delObjs } from "/@/api/canteen/canteenOnlineSpuChoiceGroup"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { select } from "/@/api/wallet/mch-manage"
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
const formDialogRef = ref()
const queryRef = ref()
const showSearch = ref(true)
const selectObjs = ref<string[]>([])
const multiple = ref(true)
const merchantNameMap = ref<Record<string, string>>({})
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: { merchantId: undefined, name: "" },
pageList: fetchPage,
})
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state)
onMounted(async () => {
try {
const res = await select({ current: 1, size: 9999, types: 1 })
const records: any[] = res?.data?.records ?? []
const map: Record<string, string> = {}
records.forEach((m: any) => {
if (m?.id != null) map[String(m.id)] = m?.name ?? String(m.id)
})
merchantNameMap.value = map
} catch {
merchantNameMap.value = {}
}
})
const resetQuery = () => {
queryRef.value?.resetFields?.()
selectObjs.value = []
multiple.value = true
getDataList()
}
const handleAdd = () => {
if (!formDialogRef.value?.openDialog) {
useMessage().error("页面组件未就绪,请刷新后重试")
return
}
formDialogRef.value.openDialog()
}
const openEdit = (row: any) => {
if (!formDialogRef.value?.openDialog) {
useMessage().error("页面组件未就绪,请刷新后重试")
return
}
formDialogRef.value.openDialog(row)
}
const selectionChangHandle = (rows: { id: string }[]) => {
selectObjs.value = rows.map((r) => r.id)
multiple.value = !rows.length
}
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().delConfirm(
"删除前请先在「在线菜品」中解除绑定该分组的商品(choiceGroupId),否则可能产生脏数据。确定删除所选分组吗?",
"提示",
)
} catch {
return
}
try {
await delObjs(ids)
await getDataList(false)
useMessage().success("删除成功")
} catch (err: any) {
useMessage().error(err.msg)
}
}
const onRefresh = async (merchantId?: string) => {
if (state.queryForm && !state.queryForm.merchantId && merchantId) {
state.queryForm.merchantId = merchantId
}
await getDataList(false)
}
</script>