【uniapp】uview-plus常用组件
常用下拉单选
com-select.vue
<!--
@Author: clp
@objectDescription: common select component
@Date: 2024-05-06
-->
<template>
<view class="slect-page">
<up-icon
v-if="mode === 'text'"
name="arrow-down"
:label="inputValue || placeholder"
labelPos="left"
:labelSize="fontSize"
:size="14"
@click="openActionSheet"
></up-icon>
<up-input
v-else
v-model="inputValue"
clearable
:fontSize="fontSize"
suffixIcon="arrow-down"
:placeholder="placeholder"
@click="openActionSheet"
/>
<up-picker
:show="singleVisible"
:columns="columns"
:closeOnClickOverlay="true"
cancelText="清空"
:title="title"
:keyName="labelName"
@confirm="onSelectAction"
@close="singleVisible = false"
@cancel="handleClear()"
/>
<u-popup
:show="multipleVisible"
@close="multipleVisible = false"
class="picker-popup"
>
<view class="picker-toolbar">
<view class="picker-toolbar__cancel" @click="handleClear"> 清空 </view>
<view class="picker-toolbar__title">
{{ title }}
</view>
<view class="picker-toolbar__confirm" @click="onSelectAction">
确定
</view>
</view>
<view class="picker-columns">
<view
v-for="(column, columnIndex) in columns"
:key="'column-' + columnIndex"
class="picker-column"
>
<view
v-for="(item, index) in column"
:key="'item-' + index"
class="picker-column__item"
:class="{
active: isSelected(item)
}"
@click="pickColumnItem(item)"
>
{{ props.labelName ? item[props.labelName] : item }}
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script setup lang="ts">
import { ref, toRaw } from 'vue';
let singleVisible = ref(false);
let multipleVisible = ref(false);
let inputValue = ref('');
let realValue = ref([]);
// 定义props
const props = defineProps({
value: [String, Array],
columns: Array,
placeholder: String,
labelName: String,
valueName: String,
mode: String,
fontSize: [String, Number],
multiple: Boolean,
title: String
});
props.labelName ? (inputValue.value = '全部') : (inputValue.value = '');
// 点击文本框,显示底部选项面板
const openActionSheet = () => {
if (props.multiple) {
multipleVisible.value = true;
} else {
singleVisible.value = true;
}
};
// 选择某个选项,关闭选项面板
const emit = defineEmits(['input']);
const onSelectAction = (action: any) => {
if (props.multiple) {
inputValue.value = realValue.value.map((v) => v.label).join(',');
multipleVisible.value = false;
emit('input', [...realValue.value]);
} else {
singleVisible.value = false;
props.labelName
? (inputValue.value = action.value[0]?.[props.labelName])
: (inputValue.value = action.value[0]);
const value = props.valueName
? action.value[0]?.[props.valueName || 'value']
: inputValue.value;
emit('input', value);
}
};
/* 多选:是否选中 */
const isSelected = (item: any) => {
if (!props.multiple) return false;
return (
realValue.value.findIndex(
(v) => v.value === (props.valueName ? item[props.valueName] : item)
) !== -1
);
};
/* 多选:点击候选项 */
const pickColumnItem = (item: any) => {
const selectIndex = realValue.value.findIndex(
(v) => v.value === (props.valueName ? item[props.valueName] : item)
);
if (selectIndex === -1) {
realValue.value.push({
label: props.labelName ? item[props.labelName] : item,
value: props.valueName ? item[props.valueName] : item
});
} else {
realValue.value.splice(selectIndex, 1);
}
};
/** 清空 */
const handleClear = () => {
if (props.multiple) {
multipleVisible.value = false;
realValue.value = [];
props.labelName ? (inputValue.value = '全部') : (inputValue.value = '');
emit('input', toRaw(realValue));
} else {
singleVisible.value = false;
props.labelName ? (inputValue.value = '全部') : (inputValue.value = '');
emit('input', '');
}
};
</script>
<style lang="scss" scoped>
.picker-popup {
.picker-toolbar {
height: 42px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
&__cancel {
font-size: 15px;
padding: 0 15px;
color: $uni-text-color-grey;
}
&__title {
color: $uni-text-color;
padding: 0 22px;
font-size: 16px;
flex: 1;
text-align: center;
}
&__confirm {
color: $uni-color-primary;
font-size: 15px;
padding: 0 15px;
}
}
.picker-columns {
width: 100%;
height: 220px;
display: flex;
flex-direction: row;
justify-content: center;
overflow: hidden;
}
.picker-column {
width: 0;
flex: 1;
height: 100%;
overflow: auto;
padding: 0 15px;
&__item {
width: 100%;
height: 44px;
line-height: 44px;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
&.active {
color: $uni-color-primary;
font-weight: bold;
}
}
}
}
</style>
常用tag
common-tag.vue
<template>
<up-tag
class="com-tag"
:text="text"
:type="currentItem.color"
:plain="currentItem.plain"
></up-tag>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface ListItem {
value?: string;
label?: string;
color?: string;
plain?: boolean;
}
interface Props {
value: string | number;
list: ListItem[];
labelName?: string;
valueName?: String;
shape?: String;
}
// 定义props
const props = withDefaults(defineProps<Props>(), {
list: () => [],
labelName: () => 'label',
valueName: () => 'value'
});
const currentItem = computed(() => {
return (
(props.list || []).find(
(item: ListItem) => String(item[props.valueName]) === String(props.value)
) || {}
);
});
const text = computed(() => {
return currentItem.value[props.labelName];
});
</script>
<style lang="scss"></style>
树形
common-tree.vue
<template>
<view class="tree-panel">
<tree-node
class="tree-box"
:data="treeShowData"
:labelKey="labelKey"
:idKey="idKey"
@click="checkNode"
>
<template #default="{ node }">
<slot :node="node">{{ node[labelKey] }}</slot>
</template>
</tree-node>
<view v-show="!treeShowData.length" class="empty">
暂无数据
</view >
</view>
</template>
<script setup lang="ts">
import TreeNode from './tree-node.vue';
import { ref, watch, computed, defineExpose } from 'vue';
export interface Props {
data: any[];
treeProp?: any;
checkable?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
treeProp: {},
checkable: false,
});
const emit = defineEmits<{
(e: 'check', checkList: any[]): void;
(e: 'click', current: object): void;
}>();
defineExpose({
clearCheck,
inputSearch,
inputClear,
checkNodeByKey,
});
const labelKey = computed(() => {
return props?.treeProp?.labelKey || 'label';
});
const idKey = computed(() => {
return props?.treeProp?.idKey || 'id';
});
const keyword = ref(''); //查询关键字
const checkList = ref([] as any); //选中列表
const treeShowData = ref(props.data as any); //树形数据-实际显示
watch(
() => props.data,
(val) => {
treeShowData.value = val || [];
},
{ immediate: true },
);
// 搜索-确认
const inputSearch = (e: any) => {
keyword.value = e.value;
if (!keyword.value) {
treeShowData.value = props.data;
} else {
treeShowData.value = searchTree(props.data, keyword.value, true);
}
};
// 搜索-清空
const inputClear = () => {
treeShowData.value = props.data;
};
// 搜索-树形数据处理
const searchTree = (tree: any[], keyword: string, includeChildren: boolean = false) => {
const newTree = [] as any;
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node[labelKey.value]?.includes(keyword)) {
// 如果当前节点符合条件,则将其复制到新的树形结构中,并根据 includeChildren 参数决定是否将其所有子节点也复制到新的树形结构中
newTree.push({
...node,
children: includeChildren ? searchTree(node.children || [], '', true) : [],
});
} else if (node.children) {
// 如果当前节点不符合条件且存在子节点,则递归遍历子节点,以继续搜索
const result = searchTree(node.children, keyword, true);
if (result.length > 0) {
// 如果子节点中存在符合条件的节点,则将其复制到新的树形结构中
newTree.push({ ...node, children: result });
}
}
}
return newTree;
};
// 点击树节点
const checkNode = (item: any, checked?: boolean) => {
emit('click', item); // 点击事件
if (!props.checkable) return;
const isCheckable = !!props.treeProp?.checkFn?.(item);
if (!isCheckable) return;
if (typeof checked !== 'undefined') {
item._checked = checked;
} else {
if (typeof item._checked === 'undefined') {
item._checked = false;
}
item._checked = !item._checked;
}
const checkIndex = checkList.value.findIndex(
(it: any) => it[idKey.value] === item[idKey.value],
);
if (checkIndex === -1 && item._checked) {
checkList.value.push(item);
}
if (checkIndex !== -1 && !item._checked) {
checkList.value.splice(checkIndex, 1);
}
emit('check', checkList.value);
};
// 清空选中
const clearCheck = () => {
checkList.value.forEach((item: any) => {
item._checked = false;
});
checkList.value = [];
emit('check', checkList.value, null);
};
// 根据id查找树结点
const getNodeById = (treeNodes: any[], id: string): any => {
for (let node of treeNodes) {
if (node[idKey.value] === id) return node;
else if (node.children?.length) {
const result2 = getNodeById(node.children, id);
if (result2) {
return result2;
}
}
}
return null;
};
const checkNodeByKey = (id: string, checked: boolean) => {
const item = getNodeById(props.data, id);
if (!item) return;
checkNode(item, checked);
};
</script>
<style lang="scss" scoped>
.tree-panel .tree-box {
height: calc(100% - 56px - 4px); //uniapp内部写死
overflow: auto;
}
</style>
tree-node.vue
<template>
<view class="tree-node-box">
<view
v-for="(item, index) in props.data"
:key="index"
class="tree-node-level"
:class="{ 'is-expanded': item._expanded }"
>
<view
class="tree-node-item"
@click="clickNode(item)"
:class="['level-' + level, { 'is-checked': item._checked }]"
>
<view class="tree-node-item__block" :style="{ width: `${(level - 1) * 48}rpx` }"></view>
<view
v-if="item.children && item.children.length"
class="tree-node-item__expand-icon"
@click.stop="handleOpenClose(item)"
>
<sl-icon
:name="item._expanded ? 'icon_arrow_expanded' : 'icon_arrow_folded'"
color="primary"
:size="32"
/>
</view>
<view v-else class="tree-node-item__space-icon"></view>
<view class="tree-node-item__content">
<slot name="default" :node="item">
{{ item[props.labelKey] }}
</slot>
</view>
<sl-icon
class="tree-node-item__checked-icon"
v-if="item._checked"
color="primary"
name="icon_check"
:size="36"
/>
</view>
<!-- 使用组件本身渲染子项 -->
<template v-if="item.children && item.children.length">
<tree-node
v-show="item._expanded"
:data="item.children"
:labelKey="labelKey"
:idKey="idKey"
:level="level + 1"
@click="clickNode"
>
<template #default="{ node }">
<slot :node="node">{{ node[props.labelKey] }}</slot>
</template>
</tree-node>
</template>
</view>
</view>
</template>
<script setup lang="ts">
import TreeNode from './tree-node.vue'; // 引入当前组件
export interface Props {
data: any;
labelKey: string;
idKey: string;
level?: number;
}
const props = withDefaults(defineProps<Props>(), {
data: [],
level: 1,
});
const emit = defineEmits<{
(e: 'click', item: any): void;
}>();
// 选中
function clickNode(item: any) {
emit('click', item);
}
// 处理展开或收起,item, index
function handleOpenClose(item: any) {
// 如果不存在_expanded属性就添加该属性。
if (typeof item._expanded === 'undefined') {
item._expanded = false;
}
item._expanded = !item._expanded;
}
</script>
<style scoped lang="scss">
.tree-node-box {
background: $ui-bg-page-white;
}
.tree-node-level {
position: relative;
// &.is-expanded{
// background: $ui-bg-page-white;
// }
}
.tree-node-item {
display: flex;
align-items: center;
padding: $ui-gap-xs $ui-gap-d $ui-gap-xs 0;
box-sizing: border-box;
font-size: 28rpx;
&.level-1,
&.level-2 {
font-size: 32rpx;
}
&:not(:last-child) {
border-bottom: 1rpx solid $ui-color-line-deep;
}
&.is-checked {
background: $ui-color-primary-hover;
color: $uni-primary;
}
}
.tree-node-item__expand-icon {
color: $uni-info;
width: 56rpx;
height: 56rpx;
line-height: 56rpx;
text-align: center;
}
.tree-node-item__space-icon {
width: 10rpx;
height: 56rpx;
line-height: 56rpx;
}
.tree-node-item__block,
.tree-node-item__expand-icon,
.tree-node-item__space-icon{
flex-shrink: 0;
flex-grow: 0;
}
.tree-node-item__checked-icon {
color: $uni-primary !important;
}
.tree-node-item__content {
width: 80%;
flex: 1;
overflow: hidden;
}
</style>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通