forked from admin/deShanXiao
初始版本,目前线上可用
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div class="service-selection">
|
||||
<!-- 标题 -->
|
||||
<h2 class="title">服务项目选择</h2>
|
||||
<!-- 搜索框 -->
|
||||
|
||||
<el-form>
|
||||
<el-row :gutter="15" style="margin-top: 15px">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="商品名称">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="请输入商品名称"
|
||||
clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-form-item label="价格">
|
||||
<el-input-number
|
||||
v-model="price"
|
||||
:min="0"
|
||||
clearable
|
||||
placeholder="请输入价格"
|
||||
type="number"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<el-divider style="margin: 8px 0; margin-bottom: 30px" />
|
||||
<!-- 两列布局 -->
|
||||
<div
|
||||
class="content"
|
||||
v-loading="lodaing"
|
||||
element-loading-text="数据加载中....">
|
||||
<!-- 左侧树结构 -->
|
||||
<div class="left">
|
||||
<el-tree
|
||||
ref="tree"
|
||||
:data="categoryTree"
|
||||
:props="treeProps"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
@check="getCheckedKeys" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧商品列表 -->
|
||||
<div class="right">
|
||||
<div
|
||||
v-for="(item, index) in categoryServiceList"
|
||||
:key="index"
|
||||
class="item">
|
||||
<!-- 商品信息 -->
|
||||
<div class="item-info">
|
||||
<div class="item-name">商品名称:{{ item.name }}</div>
|
||||
<div class="item-group">
|
||||
所属分类:{{
|
||||
categoryTreeAll.find((fitem) => fitem.id === item.parentId)
|
||||
?.name || "无"
|
||||
}}
|
||||
</div>
|
||||
<div class="item-price">
|
||||
{{ item.price }} 元 / {{ item.unit || "(暂无单位)" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/删除按钮 -->
|
||||
<div class="item-actions">
|
||||
<!-- <el-button
|
||||
v-if="!item.quantity || item.quantity === 0"
|
||||
type="primary"
|
||||
@click="addItem(item)">
|
||||
<span style="color: #fff">+ 添加</span>
|
||||
</el-button>
|
||||
<div v-else class="quantity-control">
|
||||
<el-button type="danger" @click="removeItem(item)">-</el-button>
|
||||
<span class="quantity">{{ item.quantity }}</span>
|
||||
<el-button type="primary" @click="addItem(item)">+ </el-button>
|
||||
</div> -->
|
||||
|
||||
<el-input-number
|
||||
style="margin-top: 5px"
|
||||
v-model="item.quantity"
|
||||
:min="0"
|
||||
@change="methods.quantityChange(item)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部悬浮框 -->
|
||||
<div class="footer">
|
||||
<div class="footer-item">
|
||||
<span>总金额:</span>
|
||||
<span
|
||||
><span class="red-font">{{ totalAmount }} </span>元</span
|
||||
>
|
||||
</div>
|
||||
<!-- <div class="footer-item">
|
||||
<span>减免金额:</span>
|
||||
<span
|
||||
><span class="green-font">
|
||||
{{ discountAmount }}
|
||||
</span>
|
||||
元</span
|
||||
>
|
||||
</div>
|
||||
<div class="footer-item">
|
||||
<span>实收金额:</span>
|
||||
<span
|
||||
><span class="red-font">
|
||||
{{ actualAmount }}
|
||||
</span>
|
||||
元</span
|
||||
>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import api from "@/lib/request";
|
||||
|
||||
const emits = defineEmits(["selectedProduct"]);
|
||||
const products = defineModel<Product[]>();
|
||||
|
||||
// 搜索关键词
|
||||
const searchKeyword = ref("");
|
||||
const price = ref(0);
|
||||
|
||||
const lodaing = ref(false);
|
||||
|
||||
// 分类树数据
|
||||
const categoryTree = ref([]);
|
||||
const categoryTreeAll = ref([]);
|
||||
const categoryServiceAll = ref<Product[]>([]);
|
||||
const selectedCategory = ref([]);
|
||||
const tree = ref();
|
||||
|
||||
const categoryServiceList = computed(() => {
|
||||
let resData: Product[] = [];
|
||||
selectedCategory.value.forEach((item) => {
|
||||
let findData = categoryServiceAll.value.filter(
|
||||
(fitem) => fitem.parentId === item.id
|
||||
);
|
||||
if (findData) {
|
||||
resData = [...resData, ...findData];
|
||||
}
|
||||
});
|
||||
let res = selectedCategory.value.length ? resData : categoryServiceAll.value;
|
||||
|
||||
if (searchKeyword.value) {
|
||||
res = res.filter((item) => item.name.includes(searchKeyword.value));
|
||||
let includeCategory = categoryTreeAll.value.filter((category) =>
|
||||
category.name.includes(searchKeyword.value)
|
||||
);
|
||||
categoryServiceAll.value.forEach((item) => {
|
||||
includeCategory.forEach((iitem) => {
|
||||
if (
|
||||
item.parentId === iitem.id &&
|
||||
!res.find((fitem) => fitem.name === item.name)
|
||||
) {
|
||||
res.unshift(item);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (price.value && price.value !== 0) {
|
||||
res = res.filter((fitem) => Number(fitem.price) === price.value);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
// 树结构配置
|
||||
const treeProps = {
|
||||
children: "children",
|
||||
label: "name",
|
||||
};
|
||||
|
||||
const syncProductsWithCategoryServiceAll = () => {
|
||||
if (!products.value) return; // 防止 products 为空时报错
|
||||
|
||||
categoryServiceAll.value.forEach((item) => {
|
||||
const findItem = products.value?.find(
|
||||
(newItem) => newItem.name === item.name
|
||||
);
|
||||
if (findItem) {
|
||||
item.quantity = findItem.quantity;
|
||||
} else {
|
||||
item.quantity = 0;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => products.value,
|
||||
(newVal) => {
|
||||
syncProductsWithCategoryServiceAll();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
// 总金额
|
||||
const totalAmount = computed(() => {
|
||||
return categoryServiceAll.value.reduce(
|
||||
(sum, item) => sum + item.price * (item.quantity || 0),
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
const methods = {
|
||||
quantityChange(item: any) {
|
||||
let findData = products.value?.find(
|
||||
(findItem) => findItem.name === item.name
|
||||
);
|
||||
if (!findData) {
|
||||
products.value?.push(item);
|
||||
} else {
|
||||
findData.quantity = item.quantity;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const getCheckedKeys = (data: any) => {
|
||||
selectedCategory.value = tree.value.getCheckedNodes();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
lodaing.value = true;
|
||||
let treeList = await api().get("/service-category/list");
|
||||
let allTreeList = await api().get("/service-category/list", {
|
||||
params: { all: true },
|
||||
});
|
||||
let allService = await api().get("/service-item/list", {
|
||||
params: { all: true },
|
||||
});
|
||||
|
||||
categoryTree.value = treeList.data.list;
|
||||
categoryServiceAll.value = allService.data.list;
|
||||
categoryTreeAll.value = allTreeList.data.list;
|
||||
|
||||
syncProductsWithCategoryServiceAll();
|
||||
|
||||
lodaing.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.service-selection {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.search-box {
|
||||
margin-bottom: 20px;
|
||||
width: 50%;
|
||||
}
|
||||
.left {
|
||||
width: 200px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 30px;
|
||||
max-height: calc(100vh - 150px);
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: calc(50% - 10px);
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
flex-direction: column;
|
||||
// .item-image {
|
||||
// width: 100px;
|
||||
// height: 100px;
|
||||
// margin-right: 10px;
|
||||
// }
|
||||
.item-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-category,
|
||||
.item-group,
|
||||
.item-price {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.item-actions {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quantity-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quantity {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 50%;
|
||||
z-index: 10;
|
||||
background: #fff;
|
||||
padding: 10px 20px;
|
||||
border-top: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||
.footer-item {
|
||||
font-size: 14px;
|
||||
}
|
||||
.red-font,
|
||||
.green-font {
|
||||
font-size: 18px;
|
||||
color: #f04b22;
|
||||
font-weight: bold;
|
||||
padding-right: 8px;
|
||||
}
|
||||
.green-font {
|
||||
color: #67c23a;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user