feat(project): sync all pending tenant ui changes
This commit is contained in:
564
apps/web-antd/src/views/product/addons/index.vue
Normal file
564
apps/web-antd/src/views/product/addons/index.vue
Normal file
@@ -0,0 +1,564 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件职责:加料管理页面。
|
||||
* 1. 管理加料组、加料项配置。
|
||||
* 2. 管理加料组与商品关联。
|
||||
*/
|
||||
import type { ProductSwitchStatus } from '#/api/product';
|
||||
import type { StoreListItemDto } from '#/api/store';
|
||||
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Drawer,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
changeProductAddonGroupStatusApi,
|
||||
deleteProductAddonGroupApi,
|
||||
getProductAddonGroupListApi,
|
||||
saveProductAddonGroupApi,
|
||||
searchProductPickerApi,
|
||||
} from '#/api/product';
|
||||
import { getStoreListApi } from '#/api/store';
|
||||
|
||||
type StatusFilter = '' | ProductSwitchStatus;
|
||||
|
||||
interface AddonItemForm {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
sort: number;
|
||||
status: ProductSwitchStatus;
|
||||
}
|
||||
|
||||
interface AddonGroupRow {
|
||||
description: string;
|
||||
id: string;
|
||||
items: AddonItemForm[];
|
||||
maxSelect: number;
|
||||
minSelect: number;
|
||||
name: string;
|
||||
productCount: number;
|
||||
productIds: string[];
|
||||
required: boolean;
|
||||
sort: number;
|
||||
status: ProductSwitchStatus;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const stores = ref<StoreListItemDto[]>([]);
|
||||
const selectedStoreId = ref('');
|
||||
const isStoreLoading = ref(false);
|
||||
|
||||
const rows = ref<AddonGroupRow[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const keyword = ref('');
|
||||
const statusFilter = ref<StatusFilter>('');
|
||||
|
||||
const pickerOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||
const isDrawerOpen = ref(false);
|
||||
const isDrawerSubmitting = ref(false);
|
||||
const editingGroupId = ref('');
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
required: false,
|
||||
minSelect: 0,
|
||||
maxSelect: 1,
|
||||
sort: 1,
|
||||
status: 'enabled' as ProductSwitchStatus,
|
||||
productIds: [] as string[],
|
||||
items: [] as AddonItemForm[],
|
||||
});
|
||||
|
||||
const storeOptions = computed(() =>
|
||||
stores.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
);
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '全部状态', value: '' },
|
||||
{ label: '启用', value: 'enabled' },
|
||||
{ label: '停用', value: 'disabled' },
|
||||
];
|
||||
|
||||
const drawerTitle = computed(() =>
|
||||
editingGroupId.value ? '编辑加料组' : '新增加料组',
|
||||
);
|
||||
|
||||
/** 控制抽屉开关。 */
|
||||
function setDrawerOpen(value: boolean) {
|
||||
isDrawerOpen.value = value;
|
||||
}
|
||||
|
||||
/** 加载门店列表。 */
|
||||
async function loadStores() {
|
||||
isStoreLoading.value = true;
|
||||
try {
|
||||
const result = await getStoreListApi({
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
});
|
||||
stores.value = result.items ?? [];
|
||||
if (stores.value.length === 0) {
|
||||
selectedStoreId.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const hasSelected = stores.value.some(
|
||||
(item) => item.id === selectedStoreId.value,
|
||||
);
|
||||
if (!hasSelected) {
|
||||
selectedStoreId.value = stores.value[0]?.id ?? '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error('加载门店失败');
|
||||
} finally {
|
||||
isStoreLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载加料组列表。 */
|
||||
async function loadAddonGroups() {
|
||||
if (!selectedStoreId.value) {
|
||||
rows.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const result = await getProductAddonGroupListApi({
|
||||
storeId: selectedStoreId.value,
|
||||
keyword: keyword.value.trim() || undefined,
|
||||
status: (statusFilter.value || undefined) as
|
||||
| ProductSwitchStatus
|
||||
| undefined,
|
||||
});
|
||||
rows.value = result as AddonGroupRow[];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
rows.value = [];
|
||||
message.error('加载加料组失败');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载商品选择器。 */
|
||||
async function loadPickerOptions() {
|
||||
if (!selectedStoreId.value) {
|
||||
pickerOptions.value = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const list = await searchProductPickerApi({
|
||||
storeId: selectedStoreId.value,
|
||||
limit: 500,
|
||||
});
|
||||
pickerOptions.value = list.map((item) => ({
|
||||
label: `${item.name}(${item.spuCode})`,
|
||||
value: item.id,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
pickerOptions.value = [];
|
||||
message.error('加载商品失败');
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单。 */
|
||||
function resetForm() {
|
||||
editingGroupId.value = '';
|
||||
form.name = '';
|
||||
form.description = '';
|
||||
form.required = false;
|
||||
form.minSelect = 0;
|
||||
form.maxSelect = 1;
|
||||
form.sort = rows.value.length + 1;
|
||||
form.status = 'enabled';
|
||||
form.productIds = [];
|
||||
form.items = [
|
||||
{
|
||||
id: '',
|
||||
name: '',
|
||||
price: 0,
|
||||
sort: 1,
|
||||
status: 'enabled',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 打开新增抽屉。 */
|
||||
async function openCreateDrawer() {
|
||||
resetForm();
|
||||
await loadPickerOptions();
|
||||
isDrawerOpen.value = true;
|
||||
}
|
||||
|
||||
/** 打开编辑抽屉。 */
|
||||
async function openEditDrawer(row: AddonGroupRow) {
|
||||
editingGroupId.value = row.id;
|
||||
form.name = row.name;
|
||||
form.description = row.description;
|
||||
form.required = row.required;
|
||||
form.minSelect = row.minSelect;
|
||||
form.maxSelect = row.maxSelect;
|
||||
form.sort = row.sort;
|
||||
form.status = row.status;
|
||||
form.productIds = [...row.productIds];
|
||||
form.items = row.items.map((item) => ({ ...item }));
|
||||
if (form.items.length === 0) {
|
||||
form.items = [{ id: '', name: '', price: 0, sort: 1, status: 'enabled' }];
|
||||
}
|
||||
await loadPickerOptions();
|
||||
isDrawerOpen.value = true;
|
||||
}
|
||||
|
||||
/** 添加加料项。 */
|
||||
function addAddonItem() {
|
||||
form.items.push({
|
||||
id: '',
|
||||
name: '',
|
||||
price: 0,
|
||||
sort: form.items.length + 1,
|
||||
status: 'enabled',
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除加料项。 */
|
||||
function removeAddonItem(index: number) {
|
||||
if (form.items.length <= 1) {
|
||||
message.warning('至少保留一个加料项');
|
||||
return;
|
||||
}
|
||||
form.items.splice(index, 1);
|
||||
form.items.forEach((item, idx) => {
|
||||
item.sort = idx + 1;
|
||||
});
|
||||
}
|
||||
|
||||
/** 保存加料组。 */
|
||||
async function submitDrawer() {
|
||||
if (!selectedStoreId.value) return;
|
||||
if (!form.name.trim()) {
|
||||
message.warning('请输入加料组名称');
|
||||
return;
|
||||
}
|
||||
if (form.items.some((item) => !item.name.trim())) {
|
||||
message.warning('加料项名称不能为空');
|
||||
return;
|
||||
}
|
||||
if (form.maxSelect < form.minSelect) {
|
||||
message.warning('最大可选数量不能小于最小可选数量');
|
||||
return;
|
||||
}
|
||||
|
||||
isDrawerSubmitting.value = true;
|
||||
try {
|
||||
await saveProductAddonGroupApi({
|
||||
storeId: selectedStoreId.value,
|
||||
id: editingGroupId.value || undefined,
|
||||
name: form.name.trim(),
|
||||
description: form.description.trim(),
|
||||
required: form.required,
|
||||
minSelect: form.minSelect,
|
||||
maxSelect: form.maxSelect,
|
||||
sort: form.sort,
|
||||
status: form.status,
|
||||
productIds: [...form.productIds],
|
||||
items: form.items.map((item) => ({
|
||||
id: item.id || undefined,
|
||||
name: item.name.trim(),
|
||||
price: item.price,
|
||||
sort: item.sort,
|
||||
status: item.status,
|
||||
})),
|
||||
});
|
||||
message.success(editingGroupId.value ? '加料组已更新' : '加料组已创建');
|
||||
isDrawerOpen.value = false;
|
||||
await loadAddonGroups();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
isDrawerSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除加料组。 */
|
||||
function removeAddonGroup(row: AddonGroupRow) {
|
||||
if (!selectedStoreId.value) return;
|
||||
Modal.confirm({
|
||||
title: `确认删除加料组「${row.name}」吗?`,
|
||||
async onOk() {
|
||||
await deleteProductAddonGroupApi({
|
||||
storeId: selectedStoreId.value,
|
||||
groupId: row.id,
|
||||
});
|
||||
message.success('加料组已删除');
|
||||
await loadAddonGroups();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 切换加料组状态。 */
|
||||
async function toggleAddonStatus(row: AddonGroupRow, checked: boolean) {
|
||||
if (!selectedStoreId.value) return;
|
||||
await changeProductAddonGroupStatusApi({
|
||||
storeId: selectedStoreId.value,
|
||||
groupId: row.id,
|
||||
status: checked ? 'enabled' : 'disabled',
|
||||
});
|
||||
row.status = checked ? 'enabled' : 'disabled';
|
||||
message.success('状态已更新');
|
||||
}
|
||||
|
||||
/** 重置筛选。 */
|
||||
function resetFilters() {
|
||||
keyword.value = '';
|
||||
statusFilter.value = '';
|
||||
loadAddonGroups();
|
||||
}
|
||||
|
||||
watch(selectedStoreId, loadAddonGroups);
|
||||
|
||||
onMounted(loadStores);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="加料管理" content-class="space-y-4 page-product-addons">
|
||||
<Card :bordered="false">
|
||||
<Space wrap>
|
||||
<Select
|
||||
v-model:value="selectedStoreId"
|
||||
:options="storeOptions"
|
||||
:loading="isStoreLoading"
|
||||
style="width: 240px"
|
||||
placeholder="请选择门店"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="keyword"
|
||||
style="width: 220px"
|
||||
placeholder="搜索加料组名称"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="statusFilter"
|
||||
:options="statusOptions"
|
||||
style="width: 140px"
|
||||
/>
|
||||
<Button type="primary" @click="loadAddonGroups">查询</Button>
|
||||
<Button @click="resetFilters">重置</Button>
|
||||
<Button type="primary" @click="openCreateDrawer">新增加料组</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card v-if="!selectedStoreId" :bordered="false">
|
||||
<Empty description="暂无门店,请先创建门店" />
|
||||
</Card>
|
||||
|
||||
<Card v-else :bordered="false">
|
||||
<Table
|
||||
row-key="id"
|
||||
:data-source="rows"
|
||||
:loading="isLoading"
|
||||
:pagination="false"
|
||||
size="middle"
|
||||
>
|
||||
<Table.Column
|
||||
title="加料组名称"
|
||||
data-index="name"
|
||||
key="name"
|
||||
:width="180"
|
||||
/>
|
||||
<Table.Column title="加料项" key="items">
|
||||
<template #default="{ record }">
|
||||
<Space wrap size="small">
|
||||
<Tag v-for="item in record.items" :key="item.id || item.name">
|
||||
{{ item.name }}(+{{ item.price }})
|
||||
</Tag>
|
||||
</Space>
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column title="选择规则" key="rule" :width="160">
|
||||
<template #default="{ record }">
|
||||
{{ record.required ? '必选' : '非必选' }},{{ record.minSelect }}~{{
|
||||
record.maxSelect
|
||||
}}
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="关联商品数"
|
||||
data-index="productCount"
|
||||
key="productCount"
|
||||
:width="100"
|
||||
/>
|
||||
<Table.Column title="状态" key="status" :width="90">
|
||||
<template #default="{ record }">
|
||||
<Switch
|
||||
:checked="record.status === 'enabled'"
|
||||
size="small"
|
||||
@change="(checked) => toggleAddonStatus(record, checked === true)"
|
||||
/>
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="更新时间"
|
||||
data-index="updatedAt"
|
||||
key="updatedAt"
|
||||
:width="170"
|
||||
/>
|
||||
<Table.Column title="操作" key="action" :width="170">
|
||||
<template #default="{ record }">
|
||||
<Space size="small">
|
||||
<Button size="small" @click="openEditDrawer(record)">编辑</Button>
|
||||
<Button danger size="small" @click="removeAddonGroup(record)">
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Table.Column>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<Drawer
|
||||
:open="isDrawerOpen"
|
||||
:title="drawerTitle"
|
||||
width="620"
|
||||
:destroy-on-close="true"
|
||||
@update:open="setDrawerOpen"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="加料组名称" required>
|
||||
<Input v-model:value="form.name" :maxlength="30" show-count />
|
||||
</Form.Item>
|
||||
<Form.Item label="描述">
|
||||
<Input v-model:value="form.description" :maxlength="100" show-count />
|
||||
</Form.Item>
|
||||
<Form.Item label="关联商品">
|
||||
<Select
|
||||
v-model:value="form.productIds"
|
||||
mode="multiple"
|
||||
:options="pickerOptions"
|
||||
placeholder="请选择商品"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="是否必选">
|
||||
<Switch v-model:checked="form.required" />
|
||||
</Form.Item>
|
||||
|
||||
<Space style="display: flex; width: 100%">
|
||||
<Form.Item label="最少可选" style="flex: 1">
|
||||
<InputNumber
|
||||
v-model:value="form.minSelect"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="最多可选" style="flex: 1">
|
||||
<InputNumber
|
||||
v-model:value="form.maxSelect"
|
||||
:min="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Divider orientation="left">加料项</Divider>
|
||||
<div
|
||||
v-for="(item, index) in form.items"
|
||||
:key="index"
|
||||
class="addon-item-row"
|
||||
>
|
||||
<Input v-model:value="item.name" placeholder="加料项名称,如:珍珠" />
|
||||
<InputNumber
|
||||
v-model:value="item.price"
|
||||
:min="0"
|
||||
:step="0.5"
|
||||
style="width: 120px"
|
||||
placeholder="加价"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="item.status"
|
||||
style="width: 110px"
|
||||
:options="[
|
||||
{ label: '启用', value: 'enabled' },
|
||||
{ label: '停用', value: 'disabled' },
|
||||
]"
|
||||
/>
|
||||
<Button danger @click="removeAddonItem(index)">删除</Button>
|
||||
</div>
|
||||
<Button @click="addAddonItem">新增加料项</Button>
|
||||
|
||||
<Divider />
|
||||
<Space style="display: flex; width: 100%">
|
||||
<Form.Item label="排序" style="flex: 1">
|
||||
<InputNumber
|
||||
v-model:value="form.sort"
|
||||
:min="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" style="flex: 1">
|
||||
<Select
|
||||
v-model:value="form.status"
|
||||
:options="[
|
||||
{ label: '启用', value: 'enabled' },
|
||||
{ label: '停用', value: 'disabled' },
|
||||
]"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="setDrawerOpen(false)">取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="isDrawerSubmitting"
|
||||
@click="submitDrawer"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Drawer>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
/* 文件职责:加料管理页面样式。 */
|
||||
.page-product-addons {
|
||||
.addon-item-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.ant-table-cell) {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user