酒店 食堂
This commit is contained in:
1 parent
2447995bb8
commit
685c8ce589
9 files changed
+706
-30
No files matched your search
@@ -0,0 +1,47 @@
|
||||
import request from "/@/utils/request"
|
||||
|
||||
export function fetchList(query?: Object) {
|
||||
return request({
|
||||
url: "/supermarket/smHotelDownstreamApp/page",
|
||||
method: "get",
|
||||
params: query,
|
||||
})
|
||||
}
|
||||
|
||||
export function getObj(id: string | number) {
|
||||
return request({
|
||||
url: `/supermarket/smHotelDownstreamApp/${id}`,
|
||||
method: "get",
|
||||
})
|
||||
}
|
||||
|
||||
export function addObj(obj?: Object) {
|
||||
return request({
|
||||
url: "/supermarket/smHotelDownstreamApp",
|
||||
method: "post",
|
||||
data: obj,
|
||||
})
|
||||
}
|
||||
|
||||
export function putObj(obj?: Object) {
|
||||
return request({
|
||||
url: "/supermarket/smHotelDownstreamApp",
|
||||
method: "put",
|
||||
data: obj,
|
||||
})
|
||||
}
|
||||
|
||||
export function delObjs(ids?: Object) {
|
||||
return request({
|
||||
url: "/supermarket/smHotelDownstreamApp",
|
||||
method: "delete",
|
||||
data: ids,
|
||||
})
|
||||
}
|
||||
|
||||
export function rotateSecret(id: string | number) {
|
||||
return request({
|
||||
url: `/supermarket/smHotelDownstreamApp/rotateSecret/${id}`,
|
||||
method: "post",
|
||||
})
|
||||
}
|
||||
@@ -49,3 +49,11 @@ export function compensateOrder(id: string | number) {
|
||||
method: "post",
|
||||
})
|
||||
}
|
||||
|
||||
/** 订单来源筛选项(渠道下游 + 开放应用,各自显示名) */
|
||||
export function listOrderSourceOptions() {
|
||||
return request({
|
||||
url: "/supermarket/smHotelOrder/orderSourceOptions",
|
||||
method: "get",
|
||||
})
|
||||
}
|
||||
@@ -174,6 +174,8 @@ const emit = defineEmits(["refresh"])
|
||||
|
||||
const LS_EXPAND = "canteen_daily_menu_expand_combo"
|
||||
const LS_AUTO_SUM = "canteen_daily_menu_auto_sum_combo"
|
||||
/** 选择菜品弹窗:商品分类按商户记忆;无记忆取 sort 最大(排序最靠前) */
|
||||
const LS_SELECT_CATEGORY = "canteen_daily_menu_select_category"
|
||||
|
||||
const imageBaseUrl = import.meta.env.VITE_IMAGE_BASE_PATH
|
||||
const { spu_status } = useDict("spu_status")
|
||||
@@ -187,6 +189,8 @@ const loading = ref(false)
|
||||
const categoryOptions = ref<any>([])
|
||||
const summingId = ref<string | number | null>(null)
|
||||
const summingAll = ref(false)
|
||||
/** 打开选择弹窗时跳过一次分类记忆写入,避免 reset 误清本地偏好 */
|
||||
const skipRememberSelectCategory = ref(false)
|
||||
|
||||
/** 勾选偏好本地记住 */
|
||||
const expandComboItems = ref(localStorage.getItem(LS_EXPAND) !== "0")
|
||||
@@ -194,6 +198,27 @@ const autoSumComboPrice = ref(localStorage.getItem(LS_AUTO_SUM) === "1")
|
||||
watch(expandComboItems, (v) => localStorage.setItem(LS_EXPAND, v ? "1" : "0"))
|
||||
watch(autoSumComboPrice, (v) => localStorage.setItem(LS_AUTO_SUM, v ? "1" : "0"))
|
||||
|
||||
const selectCategoryKey = (merchantId: string | number | "") =>
|
||||
`${LS_SELECT_CATEGORY}:${merchantId || "_"}`
|
||||
|
||||
const pickPreferredCategoryId = (list: any[], saved?: string | number | null) => {
|
||||
if (!list?.length) return ""
|
||||
if (saved != null && saved !== "" && list.some((c) => String(c.id) === String(saved))) {
|
||||
return list.find((c) => String(c.id) === String(saved))?.id ?? ""
|
||||
}
|
||||
return list.reduce((best, cur) => {
|
||||
const bs = Number(best?.sort) || 0
|
||||
const cs = Number(cur?.sort) || 0
|
||||
return cs > bs ? cur : best
|
||||
}).id
|
||||
}
|
||||
|
||||
const resolveSelectCategory = (merchantId: string | number | "", list: any[]) => {
|
||||
if (!merchantId || !list?.length) return ""
|
||||
const saved = localStorage.getItem(selectCategoryKey(merchantId))
|
||||
return pickPreferredCategoryId(list, saved)
|
||||
}
|
||||
|
||||
/** comboSpuId -> 组成项(含单价) */
|
||||
const comboChildrenMap = ref<Record<string, any[]>>({})
|
||||
|
||||
@@ -215,11 +240,23 @@ const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
merchantId: "",
|
||||
startTime: "",
|
||||
hourType: "",
|
||||
categoryId: "" as string | number | "",
|
||||
name: "",
|
||||
},
|
||||
pageList: getCanteenDailyMenuList,
|
||||
dataList: [],
|
||||
})
|
||||
|
||||
watch(
|
||||
() => state.queryForm.categoryId,
|
||||
(cid) => {
|
||||
if (skipRememberSelectCategory.value) return
|
||||
const mid = state.queryForm.merchantId || form.merchantId
|
||||
if (!mid || cid == null || cid === "") return
|
||||
localStorage.setItem(selectCategoryKey(mid), String(cid))
|
||||
},
|
||||
)
|
||||
|
||||
const openDialog = (merchantId: string, time?: string, hourType?: number) => {
|
||||
visible.value = true
|
||||
selectedFood.value = []
|
||||
@@ -252,6 +289,7 @@ const handleClickSelect = async () => {
|
||||
useMessage().warning("请先选择餐段")
|
||||
return
|
||||
}
|
||||
skipRememberSelectCategory.value = true
|
||||
queryRef.value?.resetFields()
|
||||
foodMultipleTableRef.value?.clearSelection()
|
||||
selectDialogVisible.value = true
|
||||
@@ -259,22 +297,38 @@ const handleClickSelect = async () => {
|
||||
state.queryForm.merchantId = form.merchantId
|
||||
state.queryForm.startTime = form.startTime
|
||||
state.queryForm.hourType = form.hourTypes.length === 1 ? form.hourTypes[0] : ""
|
||||
state.queryForm.name = ""
|
||||
|
||||
getDataList()
|
||||
try {
|
||||
const res = await getCanteenCategory()
|
||||
const res = await getCanteenCategory(
|
||||
form.merchantId ? { merchantId: form.merchantId } : undefined,
|
||||
)
|
||||
if (res.code === 0) {
|
||||
categoryOptions.value = res.data
|
||||
categoryOptions.value = res.data || []
|
||||
} else {
|
||||
categoryOptions.value = []
|
||||
}
|
||||
} catch {
|
||||
categoryOptions.value = []
|
||||
}
|
||||
state.queryForm.categoryId = resolveSelectCategory(form.merchantId, categoryOptions.value)
|
||||
await nextTick()
|
||||
skipRememberSelectCategory.value = false
|
||||
|
||||
getDataList()
|
||||
}
|
||||
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle } = useTable(state)
|
||||
|
||||
const resetQuery = () => {
|
||||
queryRef.value?.resetFields()
|
||||
const resetQuery = async () => {
|
||||
skipRememberSelectCategory.value = true
|
||||
state.queryForm.name = ""
|
||||
state.queryForm.categoryId = resolveSelectCategory(
|
||||
form.merchantId || state.queryForm.merchantId,
|
||||
categoryOptions.value,
|
||||
)
|
||||
await nextTick()
|
||||
skipRememberSelectCategory.value = false
|
||||
getDataList()
|
||||
}
|
||||
|
||||
|
||||
@@ -305,7 +305,20 @@
|
||||
{{ row.offlineSold != null ? row.offlineSold : 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余总库存" prop="remainStock" width="100" align="center" sortable />
|
||||
<el-table-column label="剩余总库存" prop="remainStock" width="128" align="center" sortable>
|
||||
<template #default="{ row }">
|
||||
<div class="inline-flex items-center justify-center gap-1">
|
||||
<span>{{ row.remainStock != null ? row.remainStock : "-" }}</span>
|
||||
<el-tooltip
|
||||
v-if="row.remainLimitedByCombo"
|
||||
:content="row.remainStockHint || '受组成菜可拼份数限制'"
|
||||
placement="top"
|
||||
>
|
||||
<el-tag type="warning" size="small" effect="plain" class="cursor-help">组成菜</el-tag>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格" width="96" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="text-red-600">{{ displayPrice(row.price) }}</span>
|
||||
|
||||
@@ -59,7 +59,25 @@
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">剩余总库存</span>
|
||||
<span class="meta-value">{{ detail.remainStock ?? "-" }}</span>
|
||||
<span class="meta-value">
|
||||
{{ detail.remainStock ?? "-" }}
|
||||
<el-tag
|
||||
v-if="detail.remainLimitedByCombo"
|
||||
type="warning"
|
||||
size="small"
|
||||
effect="plain"
|
||||
class="ml-1"
|
||||
>组成菜限制</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="detail.remainLimitedByCombo" class="meta-item meta-item--full">
|
||||
<span class="meta-label">剩余说明</span>
|
||||
<span class="meta-value meta-value--hint">
|
||||
{{ detail.remainStockHint || "受同日同餐段组成菜可拼份数限制" }}
|
||||
<template v-if="detail.selfRemainStock != null || detail.comboFormableRemain != null">
|
||||
(本行 {{ detail.selfRemainStock ?? "-" }} / 组成可拼 {{ detail.comboFormableRemain ?? "-" }})
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -369,6 +387,10 @@ defineExpose({ openDialog })
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.meta-item--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
flex-shrink: 0;
|
||||
width: 4.5em;
|
||||
@@ -383,6 +405,11 @@ defineExpose({ openDialog })
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.meta-value--hint {
|
||||
color: var(--el-color-warning-dark-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dish-detail__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -53,7 +53,20 @@
|
||||
|
||||
<el-col :span="14" class="mb20">
|
||||
<el-form-item label="菜品价格" prop="price">
|
||||
<el-input-number v-model="form.price" :min="0" :precision="2" placeholder="请输入菜品价格" />
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<el-input-number v-model="form.price" :min="0" :precision="2" placeholder="请输入菜品价格" />
|
||||
<el-button
|
||||
v-if="form.spuType === 2 && form.comboItems.length"
|
||||
link
|
||||
type="primary"
|
||||
@click="applyComboPriceSum"
|
||||
>
|
||||
按组成合计
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="form.spuType === 2 && form.comboItems.length" class="field-hint" style="margin-left: 0; margin-top: 4px">
|
||||
组成合计 {{ formatYuan(comboSumFen) }},加入/改份数时自动填入,可再改
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
@@ -98,7 +111,13 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="份数" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.quantity" :min="1" :max="99" controls-position="right" />
|
||||
<el-input-number
|
||||
v-model="row.quantity"
|
||||
:min="1"
|
||||
:max="99"
|
||||
controls-position="right"
|
||||
@change="applyComboPriceSum"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
@@ -208,6 +227,11 @@ import Pagination from "/@/components/Pagination/index.vue"
|
||||
|
||||
const emit = defineEmits(["refresh"])
|
||||
|
||||
/** 组成菜筛选分类:按商户记忆;无记忆时取 sort 最大 */
|
||||
const LS_CHILD_CATEGORY = "canteen_spu_combo_child_category"
|
||||
/** 新增菜品/套餐时的菜品分类记忆 */
|
||||
const LS_FORM_CATEGORY = "canteen_spu_form_category"
|
||||
|
||||
const formatYuan = (price: any) => {
|
||||
if (price === null || price === undefined || price === "") return "—"
|
||||
const n = Number(price)
|
||||
@@ -215,6 +239,40 @@ const formatYuan = (price: any) => {
|
||||
return "¥" + (n / 100).toFixed(2)
|
||||
}
|
||||
|
||||
const categoryStorageKey = (prefix: string, merchantId: string | number | "") =>
|
||||
`${prefix}:${merchantId || "_"}`
|
||||
|
||||
const pickPreferredCategoryId = (list: any[], saved?: string | number | null) => {
|
||||
if (!list?.length) return ""
|
||||
if (saved != null && saved !== "" && list.some((c) => String(c.id) === String(saved))) {
|
||||
return list.find((c) => String(c.id) === String(saved))?.id ?? ""
|
||||
}
|
||||
return list.reduce((best, cur) => {
|
||||
const bs = Number(best?.sort) || 0
|
||||
const cs = Number(cur?.sort) || 0
|
||||
return cs > bs ? cur : best
|
||||
}).id
|
||||
}
|
||||
|
||||
const loadCategoryList = async (merchantId?: string | number | "") => {
|
||||
const res: any = await getAllSimple(
|
||||
merchantId != null && merchantId !== "" ? { merchantId } : undefined,
|
||||
)
|
||||
return (res?.data || []) as any[]
|
||||
}
|
||||
|
||||
const resolveRememberedCategory = async (prefix: string, merchantId: string | number | "") => {
|
||||
if (!merchantId) return ""
|
||||
const saved = localStorage.getItem(categoryStorageKey(prefix, merchantId))
|
||||
const list = await loadCategoryList(merchantId)
|
||||
return pickPreferredCategoryId(list, saved)
|
||||
}
|
||||
|
||||
const rememberCategory = (prefix: string, merchantId: string | number | "", categoryId: string | number | "") => {
|
||||
if (!merchantId || categoryId == null || categoryId === "") return
|
||||
localStorage.setItem(categoryStorageKey(prefix, merchantId), String(categoryId))
|
||||
}
|
||||
|
||||
type ComboLine = {
|
||||
childSpuId: string | number
|
||||
childSpuName?: string
|
||||
@@ -294,43 +352,78 @@ const dataRules = ref({
|
||||
],
|
||||
})
|
||||
|
||||
/** 组成单价×份数合计(分) */
|
||||
const comboSumFen = computed(() =>
|
||||
form.comboItems.reduce((sum, item) => {
|
||||
const price = Number(item.childSpuPrice) || 0
|
||||
const qty = Number(item.quantity) > 0 ? Number(item.quantity) : 1
|
||||
return sum + price * qty
|
||||
}, 0),
|
||||
)
|
||||
|
||||
/** 套餐:按组成自动填入菜品价格(元),仍可手改 */
|
||||
const applyComboPriceSum = () => {
|
||||
if (form.spuType !== 2 || !form.comboItems.length) return
|
||||
form.price = Number((comboSumFen.value / 100).toFixed(2))
|
||||
}
|
||||
|
||||
const onSpuTypeChange = () => {
|
||||
if (form.spuType !== 2) {
|
||||
form.comboItems = []
|
||||
} else if (form.comboItems.length) {
|
||||
applyComboPriceSum()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => form.merchantId,
|
||||
(mid, oldMid) => {
|
||||
async (mid, oldMid) => {
|
||||
if (mid === oldMid) return
|
||||
childQuery.merchantId = mid || ""
|
||||
childQuery.categoryId = ""
|
||||
// 仅用户手动换商户时清空分类;编辑回填时保留 categoryId
|
||||
// 仅用户手动换商户时重填分类;编辑回填时保留 categoryId
|
||||
if (!isHydrating.value) {
|
||||
form.categoryId = ""
|
||||
form.categoryId = mid ? await resolveRememberedCategory(LS_FORM_CATEGORY, mid) : ""
|
||||
childQuery.categoryId = mid ? await resolveRememberedCategory(LS_CHILD_CATEGORY, mid) : ""
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => form.categoryId,
|
||||
(cid) => {
|
||||
if (isHydrating.value || !form.merchantId || cid == null || cid === "") return
|
||||
rememberCategory(LS_FORM_CATEGORY, form.merchantId, cid)
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => childQuery.merchantId,
|
||||
(mid, oldMid) => {
|
||||
async (mid, oldMid) => {
|
||||
if (!childPickerVisible.value) return
|
||||
if (mid === oldMid) return
|
||||
childQuery.categoryId = ""
|
||||
childQuery.categoryId = mid ? await resolveRememberedCategory(LS_CHILD_CATEGORY, mid) : ""
|
||||
childQuery.current = 1
|
||||
searchChildren()
|
||||
},
|
||||
)
|
||||
|
||||
const openChildPicker = () => {
|
||||
watch(
|
||||
() => childQuery.categoryId,
|
||||
(cid) => {
|
||||
if (!childPickerVisible.value) return
|
||||
const mid = childQuery.merchantId
|
||||
if (!mid || cid == null || cid === "") return
|
||||
rememberCategory(LS_CHILD_CATEGORY, mid, cid)
|
||||
},
|
||||
)
|
||||
|
||||
const openChildPicker = async () => {
|
||||
if (!form.merchantId) {
|
||||
useMessage().warning("请先选择商户")
|
||||
return
|
||||
}
|
||||
childQuery.merchantId = form.merchantId
|
||||
childQuery.categoryId = ""
|
||||
childQuery.categoryId = await resolveRememberedCategory(LS_CHILD_CATEGORY, form.merchantId)
|
||||
childQuery.current = 1
|
||||
childPickerVisible.value = true
|
||||
searchChildren()
|
||||
@@ -394,10 +487,13 @@ onBeforeUnmount(() => {
|
||||
unbindChildPickerEnter()
|
||||
})
|
||||
|
||||
const resetChildQuery = () => {
|
||||
const resetChildQuery = async () => {
|
||||
childQuery.name = ""
|
||||
childQuery.categoryId = ""
|
||||
childQuery.merchantId = form.merchantId || ""
|
||||
// 重置名称,分类仍回到记忆/默认(不要求反复选)
|
||||
childQuery.categoryId = childQuery.merchantId
|
||||
? await resolveRememberedCategory(LS_CHILD_CATEGORY, childQuery.merchantId)
|
||||
: ""
|
||||
childQuery.current = 1
|
||||
searchChildren()
|
||||
}
|
||||
@@ -443,11 +539,15 @@ const confirmChildren = () => {
|
||||
})
|
||||
childPickerVisible.value = false
|
||||
dataFormRef.value?.validateField("comboItems")
|
||||
applyComboPriceSum()
|
||||
}
|
||||
|
||||
const removeComboItem = (index: number) => {
|
||||
form.comboItems.splice(index, 1)
|
||||
dataFormRef.value?.validateField("comboItems")
|
||||
if (form.comboItems.length) {
|
||||
applyComboPriceSum()
|
||||
}
|
||||
}
|
||||
|
||||
const openDialog = async (id: string) => {
|
||||
@@ -462,6 +562,12 @@ const openDialog = async (id: string) => {
|
||||
if (id) {
|
||||
form.id = id
|
||||
getcanteenSpuData(id)
|
||||
return
|
||||
}
|
||||
// 新增:商户就绪后填入记忆/默认分类(sort 最大)
|
||||
await nextTick()
|
||||
if (form.merchantId) {
|
||||
form.categoryId = await resolveRememberedCategory(LS_FORM_CATEGORY, form.merchantId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
@change="onQueryDistrictChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isRoleAdmin" label="归属部门" prop="deptId">
|
||||
<select-dept v-model="state.queryForm.deptId" style="width: 220px" placeholder="按部门筛选" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="getDataList">查询</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
@@ -154,6 +157,11 @@
|
||||
<dict-tag :options="sm_hotel_data_source" :value="scope.row.sheetName" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deptId" label="归属部门" min-width="140" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ deptNameOf(scope.row.deptId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="620" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
@@ -483,8 +491,10 @@ import {
|
||||
} from "/@/api/supermarket/smHotelMid"
|
||||
import { listEnabledChannelDept } from "/@/api/supermarket/smHotelChannelDept"
|
||||
import { getObj as getAreaObj } from "/@/api/admin/sysArea"
|
||||
import { deptTree } from "/@/api/admin/dept"
|
||||
import { useDict } from "/@/hooks/dict"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
import { useUserInfo } from "/@/stores/userInfo"
|
||||
import { useRouter } from "vue-router"
|
||||
|
||||
const router = useRouter()
|
||||
@@ -494,6 +504,32 @@ const DetailDrawer = defineAsyncComponent(() => import("./detail.vue"))
|
||||
const ChinaArea = defineAsyncComponent(() => import("/@/components/ChinaArea/index.vue"))
|
||||
const { sm_hotel_data_source } = useDict("sm_hotel_data_source")
|
||||
|
||||
/** 归属部门筛选仅平台管理员可用(渠道映射核查) */
|
||||
const isRoleAdmin = computed(() => !!useUserInfo().userInfos?.roleCodes?.includes?.("ROLE_ADMIN"))
|
||||
|
||||
/** 部门 id → 名称(列表「归属部门」展示) */
|
||||
const deptNameMap = ref<Record<string, string>>({})
|
||||
const flattenDeptTree = (nodes: any[], map: Record<string, string>) => {
|
||||
for (const n of nodes || []) {
|
||||
if (n?.id != null) map[String(n.id)] = n.name || String(n.id)
|
||||
if (n?.children?.length) flattenDeptTree(n.children, map)
|
||||
}
|
||||
}
|
||||
const loadDeptNameMap = async () => {
|
||||
try {
|
||||
const res: any = await deptTree()
|
||||
const map: Record<string, string> = {}
|
||||
flattenDeptTree(res?.data || [], map)
|
||||
deptNameMap.value = map
|
||||
} catch {
|
||||
deptNameMap.value = {}
|
||||
}
|
||||
}
|
||||
const deptNameOf = (deptId?: string | number | null) => {
|
||||
if (deptId == null || deptId === "") return "-"
|
||||
return deptNameMap.value[String(deptId)] || String(deptId)
|
||||
}
|
||||
|
||||
/** 查询区:位置区域级联 */
|
||||
const districtPath = ref("")
|
||||
const districtCascaderKey = ref(0)
|
||||
@@ -1077,6 +1113,7 @@ const defaultQuery = () => ({
|
||||
sheetName: "",
|
||||
districtAdcode: undefined as number | undefined,
|
||||
district: "",
|
||||
deptId: undefined as string | number | undefined,
|
||||
})
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
@@ -1086,6 +1123,17 @@ const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state)
|
||||
|
||||
/** 非管理员不带部门条件,避免手工拼参绕过筛选项 */
|
||||
watch(
|
||||
isRoleAdmin,
|
||||
(admin) => {
|
||||
if (!admin && state.queryForm) {
|
||||
state.queryForm.deptId = undefined
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const resetQuery = () => {
|
||||
queryRef.value?.resetFields()
|
||||
state.queryForm = defaultQuery()
|
||||
@@ -1416,6 +1464,10 @@ const submitImport = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDeptNameMap()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPoll()
|
||||
stopSyncPoll()
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-3"
|
||||
title="统一开放平台:新建应用后把 appKey/secret 发给下游;对方按我方开放 API 对接。环启/慧订仍走「渠道对接配置」特例协议。限流 QPS 按应用独立生效。"
|
||||
/>
|
||||
<el-row v-show="showSearch">
|
||||
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
|
||||
<el-form-item label="名称" prop="appName">
|
||||
<el-input v-model="state.queryForm.appName" clearable style="width: 140px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="appKey" prop="appKey">
|
||||
<el-input v-model="state.queryForm.appKey" clearable style="width: 160px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="伙伴编码" prop="partnerCode">
|
||||
<el-input v-model="state.queryForm.partnerCode" clearable style="width: 140px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="state.queryForm.status" clearable placeholder="全部" style="width: 110px">
|
||||
<el-option label="启用" value="1" />
|
||||
<el-option label="停用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="getDataList">查询</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
<el-row class="mb8">
|
||||
<div style="width: 100%">
|
||||
<el-button type="primary" icon="Plus" v-auth="'supermarket_smHotelDownstreamApp_edit'" @click="openForm()">
|
||||
新建应用
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="Delete"
|
||||
:disabled="!selectedIds.length"
|
||||
v-auth="'supermarket_smHotelDownstreamApp_edit'"
|
||||
@click="handleDelete()"
|
||||
>
|
||||
删除
|
||||
</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="onSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="40" />
|
||||
<el-table-column prop="appName" label="应用名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="partnerCode" label="订单来源编码" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="appKey" label="appKey" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="appSecretMasked" label="secret" width="120" />
|
||||
<el-table-column prop="callbackEnabled" label="回调" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.callbackEnabled === '1' ? 'success' : 'info'" size="small">
|
||||
{{ row.callbackEnabled === "1" ? "开" : "关" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="callbackBaseUrl" label="回调根地址" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
|
||||
{{ row.status === "1" ? "启用" : "停用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updateTime" label="更新时间" min-width="160" />
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button text type="primary" v-auth="'supermarket_smHotelDownstreamApp_edit'" @click="openForm(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button text type="warning" v-auth="'supermarket_smHotelDownstreamApp_edit'" @click="handleRotate(row)">
|
||||
轮换密钥
|
||||
</el-button>
|
||||
<el-button text type="danger" v-auth="'supermarket_smHotelDownstreamApp_edit'" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="formVisible" :title="form.id ? '编辑开放应用' : '新建开放应用'" width="720px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="应用名称" prop="appName">
|
||||
<el-input v-model="form.appName" maxlength="64" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单来源编码" prop="partnerCode">
|
||||
<el-input v-model="form.partnerCode" placeholder="空则用 appKey;订单列表按此筛选" maxlength="64" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!form.id" label="appKey" prop="appKey">
|
||||
<el-input v-model="form.appKey" placeholder="可空,系统自动生成" maxlength="64" />
|
||||
</el-form-item>
|
||||
<el-form-item v-else label="appKey">
|
||||
<el-input :model-value="form.appKey" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用" prop="status">
|
||||
<el-switch v-model="form.status" active-value="1" inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-divider content-position="left">回调(对方提供)</el-divider>
|
||||
<el-form-item label="启用回调">
|
||||
<el-switch v-model="form.callbackEnabled" active-value="1" inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="回调根地址">
|
||||
<el-input v-model="form.callbackBaseUrl" placeholder="https://partner.example.com" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单状态路径">
|
||||
<el-input v-model="form.callbackOrderPath" placeholder="/order/status/notify" />
|
||||
</el-form-item>
|
||||
<el-form-item label="资源变更路径">
|
||||
<el-input v-model="form.callbackResourcePath" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-divider content-position="left">限流 QPS(JSON,按接口独立)</el-divider>
|
||||
<el-form-item label="rateLimitJson">
|
||||
<el-input
|
||||
v-model="form.rateLimitJson"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder='{"default":20,"hotel.list":10,"order.create":5}'
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="formVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="secretVisible" title="请立即复制密钥" width="520px">
|
||||
<el-alert type="warning" :closable="false" show-icon class="mb-3" title="密钥仅展示一次,关闭后列表只显示脱敏值。" />
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="appKey">{{ secretInfo.appKey }}</el-descriptions-item>
|
||||
<el-descriptions-item label="secret">{{ secretInfo.appSecret }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="copySecret">复制</el-button>
|
||||
<el-button @click="secretVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="smHotelDownstreamApp">
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
import { addObj, delObjs, fetchList, putObj, rotateSecret } from "/@/api/supermarket/smHotelDownstreamApp"
|
||||
|
||||
const showSearch = ref(true)
|
||||
const queryRef = ref()
|
||||
const formRef = ref()
|
||||
const selectedIds = ref<any[]>([])
|
||||
const formVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const secretVisible = ref(false)
|
||||
const secretInfo = reactive({ appKey: "", appSecret: "" })
|
||||
|
||||
const DEFAULT_RATE =
|
||||
'{"default":20,"hotel.list":10,"hotel.detail":20,"hotel.daily":20,"order.check":10,"order.create":5,"order.cancel":5,"order.query":20}'
|
||||
|
||||
const form = reactive<any>({
|
||||
id: undefined,
|
||||
appName: "",
|
||||
partnerCode: "",
|
||||
appKey: "",
|
||||
status: "1",
|
||||
callbackEnabled: "0",
|
||||
callbackBaseUrl: "",
|
||||
callbackOrderPath: "/order/status/notify",
|
||||
callbackResourcePath: "",
|
||||
rateLimitJson: DEFAULT_RATE,
|
||||
remark: "",
|
||||
})
|
||||
|
||||
const rules = {
|
||||
appName: [{ required: true, message: "请输入应用名称", trigger: "blur" }],
|
||||
}
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {},
|
||||
pageList: fetchList,
|
||||
})
|
||||
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state)
|
||||
|
||||
const onSelectionChange = (rows: any[]) => {
|
||||
selectedIds.value = (rows || []).map((r) => r.id)
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryRef.value?.resetFields()
|
||||
state.queryForm = {}
|
||||
getDataList()
|
||||
}
|
||||
|
||||
const openForm = (row?: any) => {
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
appName: "",
|
||||
partnerCode: "",
|
||||
appKey: "",
|
||||
status: "1",
|
||||
callbackEnabled: "0",
|
||||
callbackBaseUrl: "",
|
||||
callbackOrderPath: "/order/status/notify",
|
||||
callbackResourcePath: "",
|
||||
rateLimitJson: DEFAULT_RATE,
|
||||
remark: "",
|
||||
})
|
||||
if (row?.id) {
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
appName: row.appName,
|
||||
partnerCode: row.partnerCode,
|
||||
appKey: row.appKey,
|
||||
status: row.status || "1",
|
||||
callbackEnabled: row.callbackEnabled || "0",
|
||||
callbackBaseUrl: row.callbackBaseUrl || "",
|
||||
callbackOrderPath: row.callbackOrderPath || "/order/status/notify",
|
||||
callbackResourcePath: row.callbackResourcePath || "",
|
||||
rateLimitJson: row.rateLimitJson || DEFAULT_RATE,
|
||||
remark: row.remark || "",
|
||||
})
|
||||
}
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
await formRef.value?.validate()
|
||||
saving.value = true
|
||||
try {
|
||||
if (form.id) {
|
||||
await putObj({ ...form })
|
||||
useMessage().success("已保存")
|
||||
} else {
|
||||
const res: any = await addObj({ ...form })
|
||||
const data = res?.data || {}
|
||||
useMessage().success("已创建")
|
||||
if (data.appSecret) {
|
||||
secretInfo.appKey = data.appKey
|
||||
secretInfo.appSecret = data.appSecret
|
||||
secretVisible.value = true
|
||||
}
|
||||
}
|
||||
formVisible.value = false
|
||||
getDataList(false)
|
||||
} catch (err: any) {
|
||||
useMessage().error(err?.msg || err?.message || "保存失败")
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row?: any) => {
|
||||
const ids = row?.id ? [row.id] : selectedIds.value
|
||||
if (!ids.length) return
|
||||
try {
|
||||
await useMessageBox().confirm(`确认删除 ${ids.length} 个开放应用?`)
|
||||
await delObjs(ids)
|
||||
useMessage().success("已删除")
|
||||
getDataList(false)
|
||||
} catch (err: any) {
|
||||
if (err === "cancel") return
|
||||
useMessage().error(err?.msg || err?.message || "删除失败")
|
||||
}
|
||||
}
|
||||
|
||||
const handleRotate = async (row: any) => {
|
||||
try {
|
||||
await useMessageBox().confirm(`确认轮换 ${row.appName} 的密钥?旧密钥将立即失效。`)
|
||||
const res: any = await rotateSecret(row.id)
|
||||
const data = res?.data || {}
|
||||
secretInfo.appKey = data.appKey || row.appKey
|
||||
secretInfo.appSecret = data.appSecret
|
||||
secretVisible.value = true
|
||||
getDataList(false)
|
||||
} catch (err: any) {
|
||||
if (err === "cancel") return
|
||||
useMessage().error(err?.msg || err?.message || "轮换失败")
|
||||
}
|
||||
}
|
||||
|
||||
const copySecret = async () => {
|
||||
const text = `appKey=${secretInfo.appKey}\nsecret=${secretInfo.appSecret}`
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
useMessage().success("已复制")
|
||||
} catch {
|
||||
useMessage().warning("复制失败,请手动选择")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -13,6 +13,22 @@
|
||||
<el-button size="small" @click="quickFilter('')">全部</el-button>
|
||||
</el-button-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="订单来源" prop="partnerCode">
|
||||
<el-select
|
||||
v-model="state.queryForm.partnerCode"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="全部来源"
|
||||
style="width: 180px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in orderSourceOptions"
|
||||
:key="item.partnerCode"
|
||||
:label="item.label || item.partnerCode"
|
||||
:value="item.partnerCode"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="酒店名称/编码" prop="hotelCode">
|
||||
<el-input v-model="state.queryForm.hotelCode" clearable placeholder="酒店编码" />
|
||||
</el-form-item>
|
||||
@@ -63,17 +79,22 @@
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="50" />
|
||||
<el-table-column prop="orderSource" label="来源" width="80">
|
||||
<el-table-column prop="orderSource" label="订单来源" min-width="140" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-tag v-if="resolveSource(scope.row) === 'APP'" type="success" size="small">App</el-tag>
|
||||
<el-tag v-else-if="resolveSource(scope.row) === 'ADMIN'" type="warning" size="small">后台</el-tag>
|
||||
<el-tag v-else-if="resolveSource(scope.row) === 'DOWNSTREAM'" type="info" size="small">下游</el-tag>
|
||||
<span v-else>{{ resolveSource(scope.row) || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="partnerCode" label="分销伙伴" width="110" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ scope.row.partnerCode || "-" }}
|
||||
<el-tag v-else-if="resolveSource(scope.row) === 'LOCAL'" type="warning" size="small" effect="plain">本地确认</el-tag>
|
||||
<el-tag v-else-if="resolveSource(scope.row) === 'DOWNSTREAM'" type="info" size="small">
|
||||
{{
|
||||
scope.row.orderSourceLabel
|
||||
|| (scope.row.partnerCode
|
||||
? `${protocolText(scope.row.partnerProtocol)}${scope.row.partnerCode}`
|
||||
: "")
|
||||
|| scope.row.downstreamAppName
|
||||
|| "下游"
|
||||
}}
|
||||
</el-tag>
|
||||
<span v-else>{{ scope.row.orderSourceLabel || resolveSource(scope.row) || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="channelNo" label="订单号" min-width="160" show-overflow-tooltip />
|
||||
@@ -177,8 +198,17 @@
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="订单号" :span="2">{{ detailOrder.channelNo || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="确认单号" :span="2">{{ detailOrder.platformNo || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="来源">{{ resolveSource(detailOrder) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分销伙伴">{{ detailOrder.partnerCode || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单来源" :span="2">
|
||||
{{
|
||||
detailOrder.orderSourceLabel
|
||||
|| (detailOrder.partnerCode
|
||||
? `${protocolText(detailOrder.partnerProtocol)}${detailOrder.partnerCode}`
|
||||
: "")
|
||||
|| detailOrder.downstreamAppName
|
||||
|| resolveSource(detailOrder)
|
||||
|| "-"
|
||||
}}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="下单用户">{{ detailOrder.createBy || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="支付状态">{{ payStatusText(detailOrder.payStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单状态">{{ orderStatusText(detailOrder.orderStatus) }}</el-descriptions-item>
|
||||
@@ -413,7 +443,14 @@
|
||||
|
||||
<script setup lang="ts" name="systemSmHotelOrder">
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import { fetchList, getObj, cancelOrder, compensateOrder, refundOrder } from "/@/api/supermarket/smHotelOrder"
|
||||
import {
|
||||
fetchList,
|
||||
getObj,
|
||||
cancelOrder,
|
||||
compensateOrder,
|
||||
refundOrder,
|
||||
listOrderSourceOptions,
|
||||
} from "/@/api/supermarket/smHotelOrder"
|
||||
import {
|
||||
listEnabledDownstreamPartners,
|
||||
pullHuanqiOrders,
|
||||
@@ -429,6 +466,9 @@ const detailActing = ref(false)
|
||||
const pullVisible = ref(false)
|
||||
const pullLoading = ref(false)
|
||||
const pullPartnerCode = ref("")
|
||||
/** 订单来源筛选项(环启/慧订/开放应用各自名称) */
|
||||
const orderSourceOptions = ref<Array<{ partnerCode: string; label?: string; protocol?: string }>>([])
|
||||
/** 拉取环启订单用的伙伴列表 */
|
||||
const downstreamPartnerOptions = ref<Array<{ partnerCode: string; label?: string }>>([])
|
||||
/** 详情接口完整 VO:order / hotel / product / dailyList ... */
|
||||
const detailVo = ref<any>(null)
|
||||
@@ -524,6 +564,7 @@ const mealText = (type: any) => {
|
||||
|
||||
const resolveSource = (row: any) => {
|
||||
if (row?.orderSource && row.orderSource !== "-") return row.orderSource
|
||||
if (row?.partnerCode || row?.downstreamAppId || row?.downstreamAppName) return "DOWNSTREAM"
|
||||
const ch = String(row?.channelNo || "")
|
||||
if (/^APP/i.test(ch)) return "APP"
|
||||
if (/^ADMIN/i.test(ch)) return "ADMIN"
|
||||
@@ -531,6 +572,14 @@ const resolveSource = (row: any) => {
|
||||
return ch ? "DOWNSTREAM" : "-"
|
||||
}
|
||||
|
||||
const protocolText = (protocol?: string) => {
|
||||
const p = String(protocol || "").toLowerCase()
|
||||
if (p === "huanqi") return "环启·"
|
||||
if (p === "huiding") return "慧订·"
|
||||
if (p === "open") return "开放·"
|
||||
return ""
|
||||
}
|
||||
|
||||
const dataSourceText = (ds?: string) => {
|
||||
const v = String(ds || "").toLowerCase()
|
||||
if (!v || v === "-") return "-"
|
||||
@@ -612,6 +661,15 @@ const channelOrderStatusText = (s: any) => {
|
||||
return map[String(s)] || String(s)
|
||||
}
|
||||
|
||||
const loadOrderSourceOptions = async () => {
|
||||
try {
|
||||
const res: any = await listOrderSourceOptions()
|
||||
orderSourceOptions.value = res?.data || []
|
||||
} catch {
|
||||
orderSourceOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const loadDownstreamPartners = async () => {
|
||||
try {
|
||||
const res: any = await listEnabledDownstreamPartners()
|
||||
@@ -621,6 +679,8 @@ const loadDownstreamPartners = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
loadOrderSourceOptions()
|
||||
|
||||
const openPullDialog = async () => {
|
||||
await loadDownstreamPartners()
|
||||
if (!downstreamPartnerOptions.value.length) {
|
||||
|
||||
Reference in new issue
Block a user