初始版本,目前线上可用

This commit is contained in:
2025-11-19 12:49:16 +08:00
commit cb7f1c45e8
178 changed files with 30336 additions and 0 deletions

View File

@@ -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>