feat(erp): 增加食材分类筛选功能

- 在产品列表页面添加食材分类筛选功能
- 实现食材分类树形结构展示和选择
- 修改产品查询接口,支持按分类筛选
- 优化产品选择页面布局和样式
This commit is contained in:
郑彪辉
2025-06-21 21:09:42 +08:00
parent 73b9a68605
commit d0729e926b
12 changed files with 1016 additions and 129 deletions

9
bash.exe.stackdump Normal file
View File

@@ -0,0 +1,9 @@
Stack trace:
Frame Function Args
000FFFFA2B0 00210062B0E (00210292088, 00210270E3E, 000FFFFA2B0, 000FFFF91B0)
000FFFFA2B0 0021004846A (00000000000, 00000000000, 00000000000, 00000000000)
000FFFFA2B0 002100484A2 (00210292139, 000FFFFA168, 000FFFFA2B0, 00000000000)
000FFFFA2B0 002100CEE4E (00000000000, 00000000000, 00000000000, 00000000000)
000FFFFA2B0 002100CEF75 (000FFFFA2C0, 00000000000, 00000000000, 00000000000)
000FFFFA580 002100D0535 (000FFFFA2C0, 00000000000, 00000000000, 00000000000)
End of stack trace

View File

@@ -191,4 +191,12 @@ export function deleteProduct(id : number) {
showErrorMessage: true, showErrorMessage: true,
showSuccessMessage: true, showSuccessMessage: true,
}); });
} }
/**
* 获取食材分类tree
* @param params
* @returns
*/
export function getProductClassifyTreeList(params : Record<string, any>) {
return request.get(`erp/product/tree`, params);
}

Binary file not shown.

View File

@@ -408,7 +408,7 @@
return redirect({ return redirect({
url: '/addon/erp/pages/product/product/product-select', url: '/addon/erp/pages/product/product/product-select',
param: { param: {
type: 'all' warehouse_id: formData.value.warehouse_id
}, },
}) })
} }

View File

@@ -443,7 +443,7 @@
return redirect({ return redirect({
url: '/addon/erp/pages/product/product/product-select', url: '/addon/erp/pages/product/product/product-select',
param: { param: {
type: 'all' warehouse_id: formData.value.warehouse_id
}, },
}) })
} }

View File

@@ -490,7 +490,7 @@
return data; return data;
}); });
// 保存 // 保存
const handSave = async() => { const handSave = async() => {
if (loading.value) return if (loading.value) return
if (!formData.value.customer_id) { if (!formData.value.customer_id) {

View File

@@ -1,8 +1,16 @@
<template> <template>
<view class="bg-[var(--page-bg-color)] min-h-[100vh]" :style="themeColor()"> <view class="bg-[var(--page-bg-color)] min-h-[100vh]" :style="themeColor()">
<view class="classify-box">
<mescroll-body ref="mescrollRef" @init="mescrollInit" :down="{ use: false }" height="auto" @up="getListFn" <u-form labelPosition="left" :model="productQuery" errorType='toast' ref="formRef" labelWidth="140rpx">
:top="mescrollTop"> <u-form-item label="食材分类" prop="type_name" required @click="showPicker">
<u-input fontSize="28rpx" v-model.trim="productQuery.type_name" readonly clearable
placeholderStyle="color: #888" placeholder="请选择分类" suffixIcon="arrow-down-fill"
suffixIconStyle="font-size: 22rpx;color: #c0c4cc" />
</u-form-item>
</u-form>
</view>
<mescroll-body ref="mescrollRef" @init="mescrollInit" :down="{ use: false,auto:false }" :up="{ auto:false }"
height="auto" @up="getListFn" :top="mescrollTop">
<view class="sidebar-margin body-bottom" v-if="productList.length"> <view class="sidebar-margin body-bottom" v-if="productList.length">
<view class="mb-[15rpx]" v-for="(item,index) in productList" :key="item.id"> <view class="mb-[15rpx]" v-for="(item,index) in productList" :key="item.id">
@@ -46,8 +54,9 @@
<view><up-button :text="t('info')" size="mini" <view><up-button :text="t('info')" size="mini"
@click="redirect({ url: '/addon/erp/pages/product/product/detail',param:{id: item.id} })"></up-button> @click="redirect({ url: '/addon/erp/pages/product/product/detail',param:{id: item.id} })"></up-button>
</view> </view>
<view v-if="hasPermission('erp_product_add')"><up-button type="error" @click="hanldeDelete(item.id)" :text="t('delete')" <view v-if="hasPermission('erp_product_add')"><up-button type="error"
size="mini"></up-button></view> @click="hanldeDelete(item.id)" :text="t('delete')" size="mini"></up-button>
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -66,7 +75,8 @@
<u-icon size="30" color="#fff" name="plus-circle-fill"></u-icon> <u-icon size="30" color="#fff" name="plus-circle-fill"></u-icon>
</up-button> </up-button>
</view> </view>
<ba-tree-picker ref="treePicker" :multiple='false' @select-change="selectChange" title="选择食材分类"
:localdata="productClassifyTreeList" valueKey="id" textKey="name" childrenKey="children" />
</view> </view>
</template> </template>
@@ -75,10 +85,11 @@
import { topTabar } from '@/utils/topTabbar'; import { topTabar } from '@/utils/topTabbar';
import { t } from '@/locale'; import { t } from '@/locale';
import { redirect, pxToRpx } from '@/utils/common'; import { redirect, pxToRpx } from '@/utils/common';
import baTreePicker from "@/components/ba-tree-picker/ba-tree-picker.vue"
import MescrollBody from '@/components/mescroll/mescroll-body/mescroll-body.vue'; import MescrollBody from '@/components/mescroll/mescroll-body/mescroll-body.vue';
import MescrollEmpty from '@/components/mescroll/mescroll-empty/mescroll-empty.vue'; import MescrollEmpty from '@/components/mescroll/mescroll-empty/mescroll-empty.vue';
import useMescroll from '@/components/mescroll/hooks/useMescroll.js'; import useMescroll from '@/components/mescroll/hooks/useMescroll.js';
import { getProductList, deleteProduct } from '@/addon/erp/api/product'; import { getProductList, deleteProduct, getProductClassifyTreeList } from '@/addon/erp/api/product';
import { onShow, onPageScroll, onReachBottom } from '@dcloudio/uni-app'; import { onShow, onPageScroll, onReachBottom } from '@dcloudio/uni-app';
import usePermission from '@/utils/usePermission' import usePermission from '@/utils/usePermission'
const { hasPermission } = usePermission() const { hasPermission } = usePermission()
@@ -97,10 +108,15 @@
const mescrollTop = "20rpx" const mescrollTop = "20rpx"
const productList = ref<Array<any>>([]), const productList = ref<Array<any>>([]),
loading = ref<boolean>(true), loading = ref<boolean>(false),
listLoading = ref<boolean>(true), listLoading = ref<boolean>(false),
mescrollRef = ref(null); mescrollRef = ref(null);
const productClassifyTreeList = ref([])
const treePicker = ref<any>(null)
const productQuery = ref<any>({
type_name: '',
type_id: '',
})
interface mescrollStructure { interface mescrollStructure {
num : number, num : number,
size : number, size : number,
@@ -112,7 +128,8 @@
listLoading.value = true; listLoading.value = true;
let data : Object = { let data : Object = {
page: mescroll.num, page: mescroll.num,
limit: mescroll.size limit: mescroll.size,
product_type_id: productQuery.value.type_id
}; };
getProductList(data).then((res : any) => { getProductList(data).then((res : any) => {
let newArr = res.data.data; let newArr = res.data.data;
@@ -134,7 +151,27 @@
mescroll.endErr(); // 请求失败, 结束加载 mescroll.endErr(); // 请求失败, 结束加载
}) })
} }
// 获取食材分类
const getProductClassifyTree = () => {
getProductClassifyTreeList({}).then((res : any) => {
productClassifyTreeList.value = res.data
}).catch(err => {
// 如果是4001没有绑定企业账号强制跳转绑定
if (err.code == 4001) {
redirect({ url: '/addon/erp/pages/member/bind' })
}
})
}
// 显示选择器
const showPicker = () => {
treePicker.value._show();
}
//监听选择ids为数组
const selectChange = (ids : any, names : any) => {
productQuery.value.type_id = ids.length > 0 ? ids[0] : ''
productQuery.value.type_name = names
getMescroll().resetUpScroll()
}
// 删除 // 删除
const del_id = ref<number>(0); const del_id = ref<number>(0);
const hanldeDelete = (id : number) => { const hanldeDelete = (id : number) => {
@@ -155,7 +192,13 @@
show.value = false; show.value = false;
}).catch(() => { }); }).catch(() => { });
} }
getProductClassifyTree()
</script> </script>
<style> <style scoped>
.classify-box {
padding: 20rpx 40rpx;
box-sizing: border-box;
background: #fff;
}
</style> </style>

View File

@@ -1,56 +1,65 @@
<template> <template>
<view class="min-h-[100vh]" :style="themeColor()"> <view class="min-h-[100vh]" :style="themeColor()">
<view class="classify-box">
<mescroll-body ref="mescrollRef" @init="mescrollInit" :down="{ use: false }" height="auto" @up="getListFn" <u-form labelPosition="left" :model="productQuery" errorType='toast' ref="formRef"
:top="mescrollTop"> labelWidth="140rpx">
<u-form-item label="食材分类" prop="type_name" required @click="showPicker">
<u-input fontSize="28rpx" v-model.trim="productQuery.type_name" readonly clearable
placeholderStyle="color: #888" placeholder="请选择分类" suffixIcon="arrow-down-fill"
suffixIconStyle="font-size: 22rpx;color: #c0c4cc" />
</u-form-item>
</u-form>
</view>
<mescroll-body ref="mescrollRef" @init="mescrollInit" :down="{ use: false,auto:false }" :up="{ auto:false }"
height="auto" @up="getListFn" :top="mescrollTop">
<view class="sidebar-margin body-bottom" v-if="productList.length"> <view class="sidebar-margin body-bottom" v-if="productList.length">
<u-checkbox-group class="w-full" v-model="checkProductList" placement="column"> <u-checkbox-group class="w-full" v-model="checkProductList" placement="column">
<view class="mb-[15rpx]" v-for="(item,index) in productList" :key="index"> <view class="mb-[15rpx]" v-for="(item,index) in productList" :key="index">
<up-collapse class="bg-[#ffffff] rounded-10rpx"> <up-collapse class="bg-[#ffffff] rounded-10rpx">
<up-collapse-item> <up-collapse-item>
<template #title> <template #title>
<view class="flex" style="display: flex;align-items: center;"> <view class="flex" style="display: flex;align-items: center;">
<u-checkbox :name="item"></u-checkbox> <u-checkbox :name="item"></u-checkbox>
<view>{{item.product?.name}}</view> <view>{{item.product?.name}}</view>
</view>
</template>
<view class="body">
<view class="flex justify-start flex-col p-[20rpx]">
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
食材编号:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.product?.code}}</text>
</view> </view>
</template> <view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
<view class="body"> 食材类型:<text
<view class="flex justify-start flex-col p-[20rpx]"> class="ml-[20rpx] text-[var(--primary-color)]">{{item.product?.productType?.name}}</text>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]"> </view>
食材编号:<text <view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
class="ml-[20rpx] text-[var(--primary-color)]">{{item.product?.code}}</text> 食材规格:<text
</view> class="ml-[20rpx] text-[var(--primary-color)]">{{item.product?.spec}}</text>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]"> </view>
食材类型:<text <view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
class="ml-[20rpx] text-[var(--primary-color)]">{{item.product?.productType?.name}}</text> 批次号:<text
</view> class="ml-[20rpx] text-[var(--primary-color)]">{{item.batch_number}}</text>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]"> </view>
食材规格:<text <view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
class="ml-[20rpx] text-[var(--primary-color)]">{{item.product?.spec}}</text> 库存数量:<text
</view> class="ml-[20rpx] text-[var(--primary-color)]">{{item.inventory}}</text>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]"> </view>
批次号:<text <view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
class="ml-[20rpx] text-[var(--primary-color)]">{{item.batch_number}}</text> 食材单位:<text
</view> class="ml-[20rpx] text-[var(--primary-color)]">{{item.product?.productUnit?.name}}</text>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]"> </view>
库存数量:<text <view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
class="ml-[20rpx] text-[var(--primary-color)]">{{item.inventory}}</text> 供应商:<text
</view> class="ml-[20rpx] text-[var(--primary-color)]">{{item.supplier?.name}}</text>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]"> </view>
食材单位:<text <view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
class="ml-[20rpx] text-[var(--primary-color)]">{{item.product?.productUnit?.name}}</text> 创建时间:<text
</view> class="ml-[20rpx] text-[var(--primary-color)]">{{item.create_time}}</text>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
供应商:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.supplier?.name}}</text>
</view>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
创建时间:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.create_time}}</text>
</view>
</view> </view>
</view> </view>
</up-collapse-item> </view>
</up-collapse-item>
</up-collapse> </up-collapse>
</view> </view>
</u-checkbox-group> </u-checkbox-group>
@@ -62,7 +71,8 @@
<view class="sidebar-margin mt-[20rpx] body-bottom"> <view class="sidebar-margin mt-[20rpx] body-bottom">
<u-button @click="onConfirm" type="primary" shape="circle" text="确认选择"></u-button> <u-button @click="onConfirm" type="primary" shape="circle" text="确认选择"></u-button>
</view> </view>
<ba-tree-picker ref="treePicker" :multiple='false' @select-change="selectChange" title="选择食材分类"
:localdata="productClassifyTreeList" valueKey="id" textKey="name" childrenKey="children" />
</view> </view>
</template> </template>
@@ -71,22 +81,26 @@
import { topTabar } from '@/utils/topTabbar'; import { topTabar } from '@/utils/topTabbar';
import { t } from '@/locale'; import { t } from '@/locale';
import { redirect, pxToRpx } from '@/utils/common'; import { redirect, pxToRpx } from '@/utils/common';
import baTreePicker from "@/components/ba-tree-picker/ba-tree-picker.vue"
import MescrollBody from '@/components/mescroll/mescroll-body/mescroll-body.vue'; import MescrollBody from '@/components/mescroll/mescroll-body/mescroll-body.vue';
import MescrollEmpty from '@/components/mescroll/mescroll-empty/mescroll-empty.vue'; import MescrollEmpty from '@/components/mescroll/mescroll-empty/mescroll-empty.vue';
import useMescroll from '@/components/mescroll/hooks/useMescroll.js'; import useMescroll from '@/components/mescroll/hooks/useMescroll.js';
import { getProductList,getProductListByWarehouseNew} from '@/addon/erp/api/product'; import { getProductListByWarehouseNew, getProductClassifyTreeList } from '@/addon/erp/api/product';
import { onLoad, onPageScroll, onReachBottom } from '@dcloudio/uni-app'; import { onLoad, onPageScroll, onReachBottom } from '@dcloudio/uni-app';
const { downCallback, mescrollInit, getMescroll } = useMescroll(onPageScroll, onReachBottom); const { downCallback, mescrollInit, getMescroll } = useMescroll(onPageScroll, onReachBottom);
const productClassifyTreeList = ref([])
const checkProductList = ref<any[]>([]); const checkProductList = ref<any[]>([]);
const queryType = ref<any>('all'); // 查询方式 all 获取全部食材, warehouse 根据仓库ID ,获取对应的食材
const warehouse_id = ref<any>(undefined); // 仓库Id const warehouse_id = ref<any>(undefined); // 仓库Id
const supplier_id= ref<any>(undefined); // 供应商Id const supplier_id = ref<any>(undefined); // 供应商Id
const treePicker = ref<any>(null)
const productQuery = ref<any>({
type_name:'',
type_id:'',
})
// 页面加载 // 页面加载
onLoad((option : any) => { onLoad((option : any) => {
queryType.value = option.type ? option.type : '';
warehouse_id.value = option.warehouse_id ? option.warehouse_id : undefined warehouse_id.value = option.warehouse_id ? option.warehouse_id : undefined
supplier_id.value = option.supplier_id ? option.supplier_id : undefined supplier_id.value = option.supplier_id ? option.supplier_id : undefined
}) })
@@ -95,8 +109,8 @@
const mescrollTop = "20rpx" const mescrollTop = "20rpx"
const productList = ref<Array<any>>([]), const productList = ref<Array<any>>([]),
loading = ref<boolean>(true), loading = ref<boolean>(false),
listLoading = ref<boolean>(true), listLoading = ref<boolean>(false),
mescrollRef = ref(null); mescrollRef = ref(null);
interface mescrollStructure { interface mescrollStructure {
@@ -106,18 +120,28 @@
[propName : string] : any [propName : string] : any
} }
// 获取食材分类
const getProductClassifyTree = () => {
getProductClassifyTreeList({}).then((res : any) => {
productClassifyTreeList.value = res.data
}).catch(err => {
// 如果是4001没有绑定企业账号强制跳转绑定
if (err.code == 4001) {
redirect({ url: '/addon/erp/pages/member/bind' })
}
})
}
const getListFn = (mescroll : mescrollStructure) => { const getListFn = (mescroll : mescrollStructure) => {
listLoading.value = true; listLoading.value = true;
loading.value = true;
let data : Object = { let data : Object = {
page: mescroll.num, page: mescroll.num,
limit: mescroll.size, limit: mescroll.size,
warehouse_id: warehouse_id.value, warehouse_id: warehouse_id.value,
supplier_id: supplier_id.value supplier_id: supplier_id.value,
product_type_id: productQuery.value.type_id
}; };
let getDataApi = getProductListByWarehouseNew(data).then((res : any) => {
queryType.value == "all" ? getProductList : getProductListByWarehouseNew;
getDataApi(data).then((res : any) => {
let newArr = res.data.data; let newArr = res.data.data;
mescroll.endSuccess(newArr.length); mescroll.endSuccess(newArr.length);
//设置列表数据 //设置列表数据
@@ -137,7 +161,16 @@
mescroll.endErr(); // 请求失败, 结束加载 mescroll.endErr(); // 请求失败, 结束加载
}) })
} }
// 显示选择器
const showPicker = () => {
treePicker.value._show();
}
//监听选择ids为数组
const selectChange = (ids : any, names : any) => {
productQuery.value.type_id = ids.length > 0 ? ids[0] : ''
productQuery.value.type_name = names
getMescroll().resetUpScroll()
}
// 确认选择 // 确认选择
const onConfirm = () => { const onConfirm = () => {
uni.$emit('choose_product-out', checkProductList.value); uni.$emit('choose_product-out', checkProductList.value);
@@ -146,7 +179,12 @@
delta: 1 delta: 1
}); });
} }
getProductClassifyTree()
</script> </script>
<style> <style scoped>
.classify-box {
padding: 20rpx 40rpx;
box-sizing: border-box;
}
</style> </style>

View File

@@ -1,48 +1,58 @@
<template> <template>
<view class="min-h-[100vh]" :style="themeColor()"> <view class="min-h-[100vh]" :style="themeColor()">
<mescroll-body ref="mescrollRef" @init="mescrollInit" :down="{ use: false }" height="auto" @up="getListFn" <view class="classify-box">
:top="mescrollTop"> <u-form labelPosition="left" :model="productQuery" errorType='toast' ref="formRef"
labelWidth="140rpx">
<u-form-item label="食材分类" prop="type_name" required @click="showPicker">
<u-input fontSize="28rpx" v-model.trim="productQuery.type_name" readonly clearable
placeholderStyle="color: #888" placeholder="请选择分类" suffixIcon="arrow-down-fill"
suffixIconStyle="font-size: 22rpx;color: #c0c4cc" />
</u-form-item>
</u-form>
</view>
<mescroll-body ref="mescrollRef" @init="mescrollInit" :down="{ use: false,auto:false }" :up="{ auto:false }"
height="auto" @up="getListFn" :top="mescrollTop">
<view class="sidebar-margin body-bottom" v-if="productList.length"> <view class="sidebar-margin body-bottom" v-if="productList.length">
<u-checkbox-group class="w-full" v-model="checkProductList" placement="column"> <u-checkbox-group class="w-full" v-model="checkProductList" placement="column">
<view class="mb-[15rpx]" v-for="(item,index) in productList" :key="index"> <view class="mb-[15rpx]" v-for="(item,index) in productList" :key="index">
<up-collapse class="bg-[#ffffff] rounded-10rpx"> <up-collapse class="bg-[#ffffff] rounded-10rpx">
<up-collapse-item> <up-collapse-item>
<template #title> <template #title>
<view class="flex" style="display: flex;align-items: center;"> <view class="flex" style="display: flex;align-items: center;">
<u-checkbox :name="item"></u-checkbox> <u-checkbox :name="item"></u-checkbox>
<view>{{item.name}}</view> <view>{{item.name}}</view>
</view>
</template>
<view class="body">
<view class="flex justify-start flex-col p-[20rpx]">
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
{{ t('code') }}:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.code}}</text>
</view>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
{{ t('productTypeId') }}:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.productType?.name}}</text>
</view>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
{{ t('spec') }}:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.spec}}</text>
</view>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
{{ t('inventory') }}:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.inventory}}</text>
</view>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
{{ t('productUnitId') }}:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.productUnit?.name}}</text>
</view>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
{{ t('createTime') }}:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.create_time}}</text>
</view>
</view>
</view> </view>
</up-collapse-item> </template>
<view class="body">
<view class="flex justify-start flex-col p-[20rpx]">
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
{{ t('code') }}:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.code}}</text>
</view>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
{{ t('productTypeId') }}:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.productType?.name}}</text>
</view>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
{{ t('spec') }}:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.spec}}</text>
</view>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
{{ t('inventory') }}:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.inventory}}</text>
</view>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
{{ t('productUnitId') }}:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.productUnit?.name}}</text>
</view>
<view class="flex justify-start mb-[18rpx] text-[12px] text-[#6a6a6a]">
{{ t('createTime') }}:<text
class="ml-[20rpx] text-[var(--primary-color)]">{{item.create_time}}</text>
</view>
</view>
</view>
</up-collapse-item>
</up-collapse> </up-collapse>
</view> </view>
</u-checkbox-group> </u-checkbox-group>
@@ -54,7 +64,8 @@
<view class="sidebar-margin mt-[20rpx] body-bottom"> <view class="sidebar-margin mt-[20rpx] body-bottom">
<u-button @click="onConfirm" type="primary" shape="circle" :text="t('confirmChoose')"></u-button> <u-button @click="onConfirm" type="primary" shape="circle" :text="t('confirmChoose')"></u-button>
</view> </view>
<ba-tree-picker ref="treePicker" :multiple='false' @select-change="selectChange" title="选择食材分类"
:localdata="productClassifyTreeList" valueKey="id" textKey="name" childrenKey="children" />
</view> </view>
</template> </template>
@@ -63,18 +74,24 @@
import { topTabar } from '@/utils/topTabbar'; import { topTabar } from '@/utils/topTabbar';
import { t } from '@/locale'; import { t } from '@/locale';
import { redirect, pxToRpx } from '@/utils/common'; import { redirect, pxToRpx } from '@/utils/common';
import baTreePicker from "@/components/ba-tree-picker/ba-tree-picker.vue"
import MescrollBody from '@/components/mescroll/mescroll-body/mescroll-body.vue'; import MescrollBody from '@/components/mescroll/mescroll-body/mescroll-body.vue';
import MescrollEmpty from '@/components/mescroll/mescroll-empty/mescroll-empty.vue'; import MescrollEmpty from '@/components/mescroll/mescroll-empty/mescroll-empty.vue';
import useMescroll from '@/components/mescroll/hooks/useMescroll.js'; import useMescroll from '@/components/mescroll/hooks/useMescroll.js';
import { getProductList, getProductListByWarehouse,getProductListByWarehouseNew} from '@/addon/erp/api/product'; import { getProductListByWarehouse, getProductClassifyTreeList } from '@/addon/erp/api/product';
import { onLoad, onPageScroll, onReachBottom } from '@dcloudio/uni-app'; import { onLoad, onPageScroll, onReachBottom } from '@dcloudio/uni-app';
const { downCallback, mescrollInit, getMescroll } = useMescroll(onPageScroll, onReachBottom); const { downCallback, mescrollInit, getMescroll } = useMescroll(onPageScroll, onReachBottom);
const productClassifyTreeList = ref<any[]>([]);
const checkProductList = ref<any[]>([]); const checkProductList = ref<any[]>([]);
const queryType = ref<any>('all'); // 查询方式 all 获取全部食材, warehouse 根据仓库ID ,获取对应的食材 const queryType = ref<any>('all'); // 查询方式 all 获取全部食材, warehouse 根据仓库ID ,获取对应的食材
const warehouse_id = ref<any>(undefined); // 仓库Id const warehouse_id = ref<any>(undefined); // 仓库Id
const treePicker = ref<any>(null)
const productQuery = ref<any>({
type_name:'',
type_id:'',
})
// 页面加载 // 页面加载
onLoad((option : any) => { onLoad((option : any) => {
queryType.value = option.type ? option.type : ''; queryType.value = option.type ? option.type : '';
@@ -83,8 +100,8 @@
const mescrollTop = "20rpx" const mescrollTop = "20rpx"
const productList = ref<Array<any>>([]), const productList = ref<Array<any>>([]),
loading = ref<boolean>(true), loading = ref<boolean>(false),
listLoading = ref<boolean>(true), listLoading = ref<boolean>(false),
mescrollRef = ref(null); mescrollRef = ref(null);
interface mescrollStructure { interface mescrollStructure {
@@ -94,17 +111,28 @@
[propName : string] : any [propName : string] : any
} }
// 获取食材分类
const getProductClassifyTree = () => {
getProductClassifyTreeList({}).then((res : any) => {
productClassifyTreeList.value = res.data
}).catch(err => {
// 如果是4001没有绑定企业账号强制跳转绑定
if (err.code == 4001) {
redirect({ url: '/addon/erp/pages/member/bind' })
}
})
}
const getListFn = (mescroll : mescrollStructure) => { const getListFn = (mescroll : mescrollStructure) => {
listLoading.value = true; listLoading.value = true;
loading.value = true;
let data : Object = { let data : Object = {
page: mescroll.num, page: mescroll.num,
limit: mescroll.size, limit: mescroll.size,
warehouse_id: warehouse_id.value warehouse_id: warehouse_id.value,
product_type_id: productQuery.value.type_id
}; };
let getDataApi = getProductListByWarehouse(data).then((res : any) => {
queryType.value == "all" ? getProductList :queryType.value == "new" ?getProductListByWarehouseNew: getProductListByWarehouse;
getDataApi(data).then((res : any) => {
let newArr = res.data.data; let newArr = res.data.data;
mescroll.endSuccess(newArr.length); mescroll.endSuccess(newArr.length);
//设置列表数据 //设置列表数据
@@ -124,7 +152,16 @@
mescroll.endErr(); // 请求失败, 结束加载 mescroll.endErr(); // 请求失败, 结束加载
}) })
} }
// 显示选择器
const showPicker = () => {
treePicker.value._show();
}
//监听选择ids为数组
const selectChange = (ids : any, names : any) => {
productQuery.value.type_id = ids.length > 0 ? ids[0] : ''
productQuery.value.type_name = names
getMescroll().resetUpScroll()
}
// 确认选择 // 确认选择
const onConfirm = () => { const onConfirm = () => {
uni.$emit('choose_product', checkProductList.value); uni.$emit('choose_product', checkProductList.value);
@@ -133,7 +170,12 @@
delta: 1 delta: 1
}); });
} }
getProductClassifyTree()
</script> </script>
<style> <style scoped>
.classify-box {
padding: 20rpx 40rpx;
box-sizing: border-box;
}
</style> </style>

View File

@@ -328,7 +328,6 @@
return redirect({ return redirect({
url: '/addon/erp/pages/product/product/out-product-select', url: '/addon/erp/pages/product/product/out-product-select',
param: { param: {
type: 'warehouse',
warehouse_id: formData.value.warehouse_id, warehouse_id: formData.value.warehouse_id,
supplier_id: formData.value.supplier_id, supplier_id: formData.value.supplier_id,
}, },

View File

@@ -0,0 +1,124 @@
## 树形层级选择器
### 简介
为统一样式而生树形层级选择器picker弹窗形式的样式和比例参照uniapp的picker和uni-data-picker组件
* 支持单选、多选、父级选择,当然也支持单层选择
* 支持Object对象属性自定义映射
* 支持显示全部选中、部分选中、未选中三种状态
* 支持快速自定义简单样式分割线、按钮、标题、对齐等深入样式可复写css
### 使用方法
`script` 中引入组件
``` javascript
import baTreePicker from "@/components/ba-tree-picker/ba-tree-picker.vue"
export default {
components: {
baTreePicker
}
```
在 `template` 中使用组件
``` javascript
<ba-tree-picker ref="treePicker" :multiple='false' @select-change="selectChange" title="选择城市"
:localdata="listData" valueKey="value" textKey="label" childrenKey="children" />
```
在 `script` 中定义打开方法,和选择监听
``` javascript
methods: {
// 显示选择器
showPicker() {
this.$refs.treePicker._show();
},
//监听选择ids为数组
selectChange(ids, names) {
console.log(ids, names)
}
}
```
在 `template` 中调用打开
``` javascript
<view @click="showPicker">调用选择器</view>
```
### 属性
|属性名|类型|默认值|说明|
|:-|:-:|:--:|-:|
|localdata|Array|[]|源数据目前支持tree结构后续会考虑支持扁平化结构|
|valueKey|String|id|指定 Object 中 key 的值作为节点数据id|
|textKey|String|name|指定 Object 中 key 的值作为节点显示内容|
|childrenKey|String|children|指定 Object 中 key 的值作为节点子集|
|multiple|Boolean|false|是否多选,默认单选|
|selectParent|Boolean|true|是否可以选父级,默认可以|
|title|String| |标题|
|titleColor|String||标题颜色|
|confirmColor|String|#0055ff|确定按钮颜色|
|cancelColor|String|#757575|取消按钮颜色|
|switchColor|String|#666|节点切换图标颜色|
|border|Boolean|false|是否有分割线,默认无|
### 数据格式
注意必须有id、name(id可通过valueKey来配置为其它键值如value)字段,且唯一
``` json
[
{
id: 1,
name: '公司1',
children: [{
id: 11,
name: '研发部',
children: [{
id: 111,
name: '张三',
},{
id: 112,
name: '李四',
}]
},{
id: 12,
name: '综合部',
} ]
},
{
id: 2,
name: '公司2',
children: [{
id: 21,
name: '研发部',
},{
id: 22,
name: '综合部',
},{
id: 23,
name: '财务部',
}, ]
},
{
id: 3,
name: '公司3'
},
{
id: 4,
name: '公司4',
children: [{
id: 41,
name: '研发部',
}]
}
]
```
</details>
### 方法
|方法名|参数|默认值|说明|
|:-|:-:|:--:|-:|
|_show()| | |显示选择器|
|_hide()| | |隐藏选择器|

View File

@@ -0,0 +1,624 @@
<!-- 树形层级选择器-->
<!-- 1支持单选多选 -->
<template>
<view>
<view class="tree-cover" :class="{'show':showDialog}" @tap="_cancel"></view>
<view class="tree-dialog" :class="{'show':showDialog}">
<view class="tree-bar">
<view class="tree-bar-cancel" :style="{'color':cancelColor}" hover-class="hover-c" @tap="_cancel">取消
</view>
<view class="tree-bar-title" :style="{'color':titleColor}">{{title}}</view>
<view class="tree-bar-confirm" :style="{'color':confirmColor}" hover-class="hover-c" @tap="_confirm">
{{multiple?'确定':''}}
</view>
</view>
<view class="tree-view">
<scroll-view class="tree-list" :scroll-y="true">
<block v-for="(item, index) in treeList" :key="index">
<view class="tree-item" :style="[{
paddingLeft: item.level*30 + 'rpx'
}]" :class="{
itemBorder: border === true,
show: item.isShow
}">
<view class="item-label">
<view class="item-icon uni-inline-item" @tap.stop="_onItemSwitch(item, index)">
<view v-if="!item.isLastLevel&&item.isShowChild" class="switch-on"
:style="{'border-left-color':switchColor}">
</view>
<view v-else-if="!item.isLastLevel&&!item.isShowChild" class="switch-off"
:style="{'border-top-color':switchColor}">
</view>
<view v-else class="item-last-dot" :style="{'border-top-color':switchColor}">
</view>
</view>
<view class="uni-flex-item uni-inline-item" @tap.stop="_onItemSelect(item, index)">
<view class="item-name"> {{item.name+(item.childCount?"("+item.childCount+")":'')}}
</view>
<view class="item-check" v-if="selectParent?true:item.isLastLevel">
<view class="item-check-yes" v-if="item.checkStatus==1"
:class="{'radio':!multiple}" :style="{'border-color':confirmColor}">
<view class="item-check-yes-part"
:style="{'background-color':confirmColor}">
</view>
</view>
<view class="item-check-yes" v-else-if="item.checkStatus==2"
:class="{'radio':!multiple}" :style="{'border-color':confirmColor}">
<view class="item-check-yes-all" :style="{'background-color':confirmColor}">
</view>
</view>
<view class="item-check-no" v-else :class="{'radio':!multiple}"
:style="{'border-color':confirmColor}"></view>
</view>
</view>
</view>
</view>
</block>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
export default {
emits: ['select-change'],
name: "ba-tree-picker",
props: {
valueKey: {
type: String,
default: 'id'
},
textKey: {
type: String,
default: 'name'
},
childrenKey: {
type: String,
default: 'children'
},
localdata: {
type: Array,
default: function() {
return []
}
},
localTreeList: { //在已经格式化好的数据
type: Array,
default: function() {
return []
}
},
selectedData: {
type: Array,
default: function() {
return []
}
},
title: {
type: String,
default: ''
},
multiple: { // 是否可以多选
type: Boolean,
default: true
},
selectParent: { //是否可以选父级
type: Boolean,
default: true
},
confirmColor: { // 确定按钮颜色
type: String,
default: '' // #0055ff
},
cancelColor: { // 取消按钮颜色
type: String,
default: '' // #757575
},
titleColor: { // 标题颜色
type: String,
default: '' //
},
switchColor: { // 节点切换图标颜色
type: String,
default: '' // #666
},
border: { // 是否有分割线
type: Boolean,
default: false
},
},
data() {
return {
showDialog: false,
treeList: []
}
},
computed: {},
methods: {
_show() {
this.showDialog = true
},
_hide() {
this.showDialog = false
},
_cancel() {
this._hide()
this.$emit("cancel", '');
},
_confirm() { //多选
let selectedList = []; //如果子集全部选中,只返回父级 id
let selectedNames;
let currentLevel = -1;
this.treeList.forEach((item, index) => {
if (currentLevel >= 0 && item.level > currentLevel) {
} else {
if (item.checkStatus === 2) {
currentLevel = item.level;
selectedList.push(item.id);
selectedNames = selectedNames ? selectedNames + ' / ' + item.name : item.name;
} else {
currentLevel = -1;
}
}
})
//console.log('_confirm', selectedList);
this._hide()
this.$emit("select-change", selectedList, selectedNames);
},
//格式化原数据原数据为tree结构
_formatTreeData(list = [], level = 0, parentItem, isShowChild = true) {
let nextIndex = 0;
let parentId = -1;
let initCheckStatus = 0;
if (parentItem) {
nextIndex = this.treeList.findIndex(item => item.id === parentItem.id) + 1;
parentId = parentItem.id;
if (!this.multiple) { //单选
initCheckStatus = 0;
} else
initCheckStatus = parentItem.checkStatus == 2 ? 2 : 0;
}
list.forEach(item => {
let isLastLevel = true;
if (item && item[this.childrenKey]) {
let children = item[this.childrenKey];
if (Array.isArray(children) && children.length > 0) {
isLastLevel = false;
}
}
let itemT = {
id: item[this.valueKey],
name: item[this.textKey],
level,
isLastLevel,
isShow: isShowChild,
isShowChild: false,
checkStatus: initCheckStatus,
orCheckStatus: 0,
parentId,
children: item[this.childrenKey],
childCount: item[this.childrenKey] ? item[this.childrenKey].length : 0,
childCheckCount: 0,
childCheckPCount: 0
};
if (this.selectedData.indexOf(itemT.id) >= 0) {
itemT.checkStatus = 2;
itemT.orCheckStatus = 2;
itemT.childCheckCount = itemT.children ? itemT.children.length : 0;
this._onItemParentSelect(itemT, nextIndex);
}
this.treeList.splice(nextIndex, 0, itemT);
nextIndex++;
})
//console.log(this.treeList);
},
// 节点打开、关闭切换
_onItemSwitch(item, index) {
// console.log(item)
//console.log('_itemSwitch')
if (item.isLastLevel === true) {
return;
}
item.isShowChild = !item.isShowChild;
if (item.children) {
this._formatTreeData(item.children, item.level + 1, item);
item.children = undefined;
} else {
this._onItemChildSwitch(item, index);
}
},
_onItemChildSwitch(item, index) {
//console.log('_onItemChildSwitch')
const firstChildIndex = index + 1;
if (firstChildIndex > 0)
for (var i = firstChildIndex; i < this.treeList.length; i++) {
let itemChild = this.treeList[i];
if (itemChild.level > item.level) {
if (item.isShowChild) {
if (itemChild.parentId === item.id) {
itemChild.isShow = item.isShowChild;
if (!itemChild.isShow) {
itemChild.isShowChild = false;
}
}
} else {
itemChild.isShow = item.isShowChild;
itemChild.isShowChild = false;
}
} else {
return;
}
}
},
// 节点选中、取消选中
_onItemSelect(item, index) {
//console.log('_onItemSelect')
//console.log(item)
if (!this.multiple) { //单选
item.checkStatus = item.checkStatus == 0 ? 2 : 0;
this.treeList.forEach((v, i) => {
if (i != index) {
this.treeList[i].checkStatus = 0
} else {
this.treeList[i].checkStatus = 2
}
})
let selectedList = [];
let selectedNames;
selectedList.push(item.id);
selectedNames = item.name;
this._hide()
this.$emit("select-change", selectedList, selectedNames);
return
}
let oldCheckStatus = item.checkStatus;
switch (oldCheckStatus) {
case 0:
item.checkStatus = 2;
item.childCheckCount = item.childCount;
item.childCheckPCount = 0;
break;
case 1:
case 2:
item.checkStatus = 0;
item.childCheckCount = 0;
item.childCheckPCount = 0;
break;
default:
break;
}
//子节点 全部选中
this._onItemChildSelect(item, index);
//父节点 选中状态变化
this._onItemParentSelect(item, index, oldCheckStatus);
},
_onItemChildSelect(item, index) {
//console.log('_onItemChildSelect')
let allChildCount = 0;
if (item.childCount && item.childCount > 0) {
index++;
while (index < this.treeList.length && this.treeList[index].level > item.level) {
let itemChild = this.treeList[index];
itemChild.checkStatus = item.checkStatus;
if (itemChild.checkStatus == 2) {
itemChild.childCheckCount = itemChild.childCount;
itemChild.childCheckPCount = 0;
} else if (itemChild.checkStatus == 0) {
itemChild.childCheckCount = 0;
itemChild.childCheckPCount = 0;
}
// console.log('>>>>index', index, 'item', itemChild.name, ' status', itemChild
// .checkStatus)
index++;
}
}
},
_onItemParentSelect(item, index, oldCheckStatus) {
//console.log('_onItemParentSelect')
//console.log(item)
const parentIndex = this.treeList.findIndex(itemP => itemP.id == item.parentId);
//console.log('parentIndex' + parentIndex)
if (parentIndex >= 0) {
let itemParent = this.treeList[parentIndex];
let count = itemParent.childCheckCount;
let oldCheckStatusParent = itemParent.checkStatus;
if (oldCheckStatus == 1) {
itemParent.childCheckPCount -= 1;
} else if (oldCheckStatus == 2) {
itemParent.childCheckCount -= 1;
}
if (item.checkStatus == 1) {
itemParent.childCheckPCount += 1;
} else if (item.checkStatus == 2) {
itemParent.childCheckCount += 1;
}
if (itemParent.childCheckCount <= 0 && itemParent.childCheckPCount <= 0) {
itemParent.childCheckCount = 0;
itemParent.childCheckPCount = 0;
itemParent.checkStatus = 0;
} else if (itemParent.childCheckCount >= itemParent.childCount) {
itemParent.childCheckCount = itemParent.childCount;
itemParent.childCheckPCount = 0;
itemParent.checkStatus = 2;
} else {
itemParent.checkStatus = 1;
}
//console.log('itemParent', itemParent)
this._onItemParentSelect(itemParent, parentIndex, oldCheckStatusParent);
}
},
// 重置数据
_reTreeList() {
this.treeList.forEach((v, i) => {
this.treeList[i].checkStatus = v.orCheckStatus
})
},
_initTree() {
this.treeList = [];
this._formatTreeData(this.localdata);
}
},
watch: {
localdata() {
this._initTree();
},
localTreeList() {
this.treeList = this.localTreeList;
}
},
mounted() {
this._initTree();
}
}
</script>
<style scoped>
.tree-cover {
position: fixed;
top: 0rpx;
right: 0rpx;
bottom: 0rpx;
left: 0rpx;
z-index: 100;
background-color: rgba(0, 0, 0, .4);
opacity: 0;
transition: all 0.3s ease;
visibility: hidden;
}
.tree-cover.show {
visibility: visible;
opacity: 1;
}
.tree-dialog {
position: fixed;
top: 0rpx;
right: 0rpx;
bottom: 0rpx;
left: 0rpx;
background-color: #fff;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
z-index: 102;
top: 20%;
transition: all 0.3s ease;
transform: translateY(100%);
}
.tree-dialog.show {
transform: translateY(0);
}
.tree-bar {
/* background-color: #fff; */
height: 90rpx;
padding-left: 25rpx;
padding-right: 25rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
border-bottom-width: 1rpx !important;
border-bottom-style: solid;
border-bottom-color: #f5f5f5;
font-size: 32rpx;
color: #757575;
line-height: 1;
}
.tree-bar-confirm {
color: #0055ff;
padding: 15rpx;
}
.tree-bar-title {}
.tree-bar-cancel {
color: #757575;
padding: 15rpx;
}
.tree-view {
flex: 1;
padding: 20rpx;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
overflow: hidden;
height: 100%;
}
.tree-list {
flex: 1;
height: 100%;
overflow: hidden;
}
.tree-item {
display: flex;
justify-content: space-between;
align-items: center;
line-height: 1;
height: 0;
opacity: 0;
transition: 0.2s;
overflow: hidden;
}
.tree-item.show {
height: 90rpx;
opacity: 1;
}
.tree-item.showchild:before {
transform: rotate(90deg);
}
.tree-item.last:before {
opacity: 0;
}
.switch-on {
width: 0;
height: 0;
border-left: 10rpx solid transparent;
border-right: 10rpx solid transparent;
border-top: 15rpx solid #666;
}
.switch-off {
width: 0;
height: 0;
border-bottom: 10rpx solid transparent;
border-top: 10rpx solid transparent;
border-left: 15rpx solid #666;
}
.item-last-dot {
position: absolute;
width: 10rpx;
height: 10rpx;
border-radius: 100%;
background: #666;
}
.item-icon {
width: 26rpx;
height: 26rpx;
margin-right: 8rpx;
padding-right: 20rpx;
padding-left: 20rpx;
}
.item-label {
flex: 1;
display: flex;
align-items: center;
height: 100%;
line-height: 1.2;
}
.uni-flex-item{
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 450rpx;
}
.item-check {
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
}
.item-check-yes,
.item-check-no {
width: 20px;
height: 20px;
border-top-left-radius: 20%;
border-top-right-radius: 20%;
border-bottom-right-radius: 20%;
border-bottom-left-radius: 20%;
border-top-width: 1rpx;
border-left-width: 1rpx;
border-bottom-width: 1rpx;
border-right-width: 1rpx;
border-style: solid;
border-color: #0055ff;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.item-check-yes-part {
width: 12px;
height: 12px;
border-top-left-radius: 20%;
border-top-right-radius: 20%;
border-bottom-right-radius: 20%;
border-bottom-left-radius: 20%;
background-color: #0055ff;
}
.item-check-yes-all {
margin-bottom: 5px;
border: 2px solid #007aff;
border-left: 0;
border-top: 0;
height: 12px;
width: 6px;
transform-origin: center;
/* #ifndef APP-NVUE */
transition: all 0.3s;
/* #endif */
transform: rotate(45deg);
}
.item-check .radio {
border-top-left-radius: 50%;
border-top-right-radius: 50%;
border-bottom-right-radius: 50%;
border-bottom-left-radius: 50%;
}
.item-check .radio .item-check-yes-b {
border-top-left-radius: 50%;
border-top-right-radius: 50%;
border-bottom-right-radius: 50%;
border-bottom-left-radius: 50%;
}
.hover-c {
opacity: 0.6;
}
.itemBorder {
border-bottom: 1px solid #e5e5e5;
}
</style>