Loading

【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>
posted @   Ping5-1  阅读(353)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示