Initial commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
## 1.0.2(2026-05-18)
|
||||
- 修复类名拼写错误
|
||||
## 1.0.1(2026-04-30)
|
||||
- 更新文档
|
||||
## 1.0.0(2026-04-30)
|
||||
- 支持 uni-badge-view 组件
|
||||
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<!-- 如果不显示badge,则不包裹,直接显示子内容。保证常态下的高性能。但如果设置了alwaysWrapWithContent且有slot内容,则仍然保持包裹层 -->
|
||||
<slot v-if="text == '0' && alwaysWrapWithContent== false"></slot>
|
||||
<!-- 如果有多个子slot,且不设badge,去掉包裹层直接露出slot,会导致flex方向错乱。此时应设置alwaysWrapWithContent为true,总是保留包裹层 -->
|
||||
|
||||
<!-- 微信小程序不支持外部绑定事件 -->
|
||||
|
||||
<template v-else>
|
||||
<view v-if="hasDefaultSlot" style="overflow: visible;" class="badge-wrap" v-bind="$attrs">
|
||||
<!-- 目标内容插槽 -->
|
||||
<slot></slot>
|
||||
|
||||
<!-- badge 文字模式 -->
|
||||
<text v-if="text != '' && text != '0' " :class="['text', 'position', badgeClass]" :style="badgeStyle">
|
||||
{{ text }}
|
||||
</text>
|
||||
|
||||
<!-- badge 红点模式 -->
|
||||
<view v-else-if="text != '0'" :class="['dot', 'position', badgeClass]" :style="badgeStyle"></view>
|
||||
</view>
|
||||
|
||||
<!-- 不包裹,直接显示badge 文字模式 -->
|
||||
<text v-else-if="text != ''" :class="['text', badgeClass]" :style="badgeStyle">
|
||||
{{ text }}
|
||||
</text>
|
||||
|
||||
<!-- 不包裹,直接显示badge 红点模式 -->
|
||||
<view v-else :class="['dot', badgeClass]" :style="badgeStyle"></view>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Badge 数字角标
|
||||
* @description 数字角标(徽章)组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.08"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
// 禁用自动属性继承,手动控制属性绑定位置
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
externalClasses: ['badge-class']
|
||||
})
|
||||
|
||||
// 定义组件属性接口
|
||||
type Props = {
|
||||
/**
|
||||
* @description badge 的内容。为 "0" 时不显示 badge;为空字符串时显示圆点;有文字则显示文字
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
text? : string,
|
||||
/**
|
||||
* @description badge 的内联样式,用于覆盖内置 style
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
badgeStyle? : string,
|
||||
/**
|
||||
* @description badge 的样式类,用于覆盖内置 class
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
badgeClass? : string.ClassString,
|
||||
/**
|
||||
* @description 当有 slot 内容时,即使 text 为 "0" 也保留包裹层(badge-wrap),避免多个子 slot 时 flex 方向错乱
|
||||
* @default false
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
alwaysWrapWithContent? : boolean
|
||||
}
|
||||
|
||||
// 使用 withDefaults 设置默认值
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
text: '',
|
||||
badgeStyle: '',
|
||||
badgeClass: '',
|
||||
alwaysWrapWithContent: false
|
||||
})
|
||||
|
||||
// 1. 获取插槽对象
|
||||
const slots = useSlots();
|
||||
// 2. 定义计算属性来判断默认插槽是否有内容
|
||||
// TODO 这种判断方式的性能有待商榷,这利用了模板的隐式转换
|
||||
const hasDefaultSlot = computed(() => {
|
||||
// console.log('slot: ',slots['default']);
|
||||
return slots['default'] != null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.text {
|
||||
/* background-color: rgba(225, 80, 80, 0.9); */ /* 有透明度*/
|
||||
background-color: rgb(225, 80, 80); /* 无透明度*/
|
||||
min-width: 14px;
|
||||
height: 14px;
|
||||
line-height: 14px;
|
||||
border-radius: 7px;
|
||||
/* padding-left: 4px; *//* 保证圆角不裁剪文字 */
|
||||
/* padding-right: 4px; *//* 保证圆角不裁剪文字 */
|
||||
font-size: 10px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dot {
|
||||
background-color: rgb(225, 80, 80);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 有包裹内容,才定位在右上角。所以这个样式是一个单独的class,在slot有内容时才生效 */
|
||||
.position {
|
||||
position: absolute;
|
||||
top: 0px; /* 定位在顶部*/
|
||||
right: 0px; /* 定位在右边*/
|
||||
transform: translate(50%, -50%); /* 中心向右上角偏移,保证中心在元素右上角顶点*/
|
||||
}
|
||||
|
||||
.badge-wrap{
|
||||
/* #ifdef WEB */
|
||||
/* width: fit-content; */
|
||||
/* #endif */
|
||||
align-self: flex-start; /* 默认这样,不然width会变100%。但需要居中时得覆盖重设*/
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"id": "uni-badge-view",
|
||||
"displayName": "uni-badge-view 数字角标",
|
||||
"version": "1.0.2",
|
||||
"description": "数字角标(徽章)组件,在元素周围展示消息提醒,一般用于列表、九宫格、按钮等地方。",
|
||||
"isExt": true,
|
||||
"extUrl": "https://ext.dcloud.net.cn/plugin?id=27844",
|
||||
"keywords": [
|
||||
"badge",
|
||||
"uni-ui",
|
||||
"uniui",
|
||||
"数字角标",
|
||||
"徽章"
|
||||
],
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"engines": {
|
||||
"HBuilderX": "^5.08",
|
||||
"uni-app-x": "^5.08",
|
||||
"uni-app": ""
|
||||
},
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"type": "component-vue",
|
||||
"darkmode": "√",
|
||||
"i18n": "√",
|
||||
"widescreen": "√",
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": "√",
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
badge角标,可以用于显示红点、被红圈或其他颜色圈包裹的数字、文字。提醒用户此时有需要注意的新信息。
|
||||
|
||||
### 文件路径
|
||||
项目下有一个 /pages/uni-ui/badge/badge.uvue 页面,里面演示了badge组件的各种用法。
|
||||
|
||||
### 设计
|
||||
#### badge组件有多种用法:
|
||||
1. 独立显示
|
||||
如果没有slot内容,视为要显示独立的红点和数字,此时不是角标,就是普通的红点和圆背景数字。
|
||||
在按钮或列表项的右侧,独立显示数字。也可以显示红点,但这种场景数字更多一些。
|
||||
|
||||
2. 作为一个容器组件,包含一个slot,在包裹内容的右上角显示一个角标view。
|
||||
badge组件包裹一个目标view时,badge组件内部的角标view的中心点位于目标view的右上角顶点。
|
||||
此时角标view是通过transform位移到右上角的,实际内容已经超出了容器本身。
|
||||
所以需要注意容器的父级的设置,留出超出容器的尺寸(即top和right边距需要留出角标view的高度和宽度的一半),避免被父级裁剪。
|
||||
|
||||
#### badge的text属性的用法:
|
||||
在badge作为容器组件时,text属性的不同设置,可以代表不同的效果。具体见属性章节。
|
||||
|
||||
#### 样式自定义
|
||||
badge组件的badge-class和badge-style属性,直接作用在角标view上,可以修改角标view的样式。
|
||||
|
||||
角标view本质是一个text组件,可以覆盖前景、背景、位置等各种样式。
|
||||
|
||||
#### badge组件有如下属性:
|
||||
- text
|
||||
类型为 string|null
|
||||
* 值为"0"时,不显示角标;所以永远不会显示一个为0的角标。此时直接显示slot子内容,不包裹外层,减少层级,提高性能;
|
||||
* 值为null或""时,代表一个长宽为8px的红色圆点;
|
||||
* 否则显示为该文字内容的圆角红底圈套文字。
|
||||
- badge-class
|
||||
直接作用于角标view的class
|
||||
- badge-style
|
||||
直接作用于角标view的style
|
||||
@@ -0,0 +1,4 @@
|
||||
## 1.0.1(2026-05-14)
|
||||
- 支持 content-wrap-class 定义内层容器样式
|
||||
## 1.0.0(2026-05-07)
|
||||
- 支持 uni-collapse 组件
|
||||
@@ -0,0 +1,3 @@
|
||||
export const UNI_COLLAPSE_REGISTER_CHILD = 'uni-collapse-register-child'
|
||||
export const UNI_COLLAPSE_UNREGISTER_CHILD = 'uni-collapse-unregister-child'
|
||||
export const UNI_COLLAPSE_CHILD_TOGGLE = 'uni-collapse-child-toggle'
|
||||
@@ -0,0 +1,6 @@
|
||||
export type ItemChildType = {
|
||||
openCollapse?: (show: boolean) => void;
|
||||
openOrClose: (show: boolean) => void;
|
||||
is_open: Ref<boolean>;
|
||||
elId: string;
|
||||
};
|
||||
@@ -0,0 +1,739 @@
|
||||
<template>
|
||||
<view>
|
||||
<view class="uni-collapse-item__title" :class="props.titleWrapClass" @click="openCollapse(!is_open)">
|
||||
<view class="uni-collapse-item__title-main">
|
||||
<slot name="title" :open="is_open" :disabled="props.disabled">
|
||||
<text class="uni-collapse-item__title-text"
|
||||
:class="[{'open--active': is_open, 'is-disabled': props.disabled}, props.titleClass, is_open ? props.titleOpenClass : '', props.disabled ? props.titleDisabledClass : '']">{{ props.title }}</text>
|
||||
</slot>
|
||||
</view>
|
||||
<view class="down_arrow"
|
||||
:class="[arrowPlatformClass, arrowStateClass, arrowDisabledClass, props.arrowClass, is_open ? props.arrowOpenClass : '', props.disabled ? props.arrowDisabledClass : '']"></view>
|
||||
</view>
|
||||
<view ref="contentWrapRef" class="uni-collapse-item__content"
|
||||
:class="[transition_enabled ? 'content-transition--active' : '', box_is_open ? 'content-state-open' : 'content-state-close', contentWrapClass]">
|
||||
<view ref="contentRef" class="uni-collapse-item__content-box"
|
||||
:class="[transition_enabled ? 'content-transition--active' : '', box_is_open ? 'content-box-open' : 'content-box-close']">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 折叠面板子项
|
||||
* @description 折叠面板子项组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
import type { ItemChildType } from '../type.ts'
|
||||
import {
|
||||
UNI_COLLAPSE_REGISTER_CHILD,
|
||||
UNI_COLLAPSE_UNREGISTER_CHILD,
|
||||
UNI_COLLAPSE_CHILD_TOGGLE
|
||||
} from '../common.ts'
|
||||
|
||||
defineOptions({
|
||||
name: 'UniCollapseItem',
|
||||
styleIsolation: 'app',
|
||||
externalClasses: ['title-wrap-class', 'title-class', 'title-open-class', 'title-disabled-class', 'content-wrap-class', 'arrow-class', 'arrow-open-class', 'arrow-disabled-class']
|
||||
})
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* @description 折叠项标题文字
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
title? : string,
|
||||
/**
|
||||
* @description 是否默认展开
|
||||
* @default false
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
open? : boolean,
|
||||
/**
|
||||
* @description 是否禁用,禁用后点击标题不会展开或收起
|
||||
* @default false
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
disabled? : boolean,
|
||||
/**
|
||||
* @description 标题行(整行容器)的样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
titleWrapClass? : string.ClassString,
|
||||
/**
|
||||
* @description 标题文字的基础样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
titleClass? : string.ClassString,
|
||||
/**
|
||||
* @description 展开状态下叠加到标题文字的样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
titleOpenClass? : string.ClassString,
|
||||
/**
|
||||
* @description 禁用状态下叠加到标题文字的样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
titleDisabledClass? : string.ClassString,
|
||||
/**
|
||||
* @description 内容外层容器的样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
contentWrapClass? : string.ClassString,
|
||||
/**
|
||||
* @description 箭头图标的基础样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
arrowClass? : string.ClassString,
|
||||
/**
|
||||
* @description 展开状态下叠加到箭头图标的样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
arrowOpenClass? : string.ClassString,
|
||||
/**
|
||||
* @description 禁用状态下叠加到箭头图标的样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
arrowDisabledClass? : string.ClassString
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '',
|
||||
open: false,
|
||||
disabled: false,
|
||||
titleWrapClass: '',
|
||||
titleClass: '',
|
||||
titleOpenClass: '',
|
||||
titleDisabledClass: '',
|
||||
contentWrapClass: '',
|
||||
arrowClass: '',
|
||||
arrowOpenClass: '',
|
||||
arrowDisabledClass: ''
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [open: boolean]
|
||||
}>()
|
||||
|
||||
defineSlots<{
|
||||
default(props : {}) : any,
|
||||
title(props : { open : boolean, disabled : boolean }) : any
|
||||
}>()
|
||||
|
||||
let is_open = ref<boolean>(props.open)
|
||||
let box_is_open = ref<boolean>(props.open)
|
||||
let transition_enabled = ref<boolean>(false)
|
||||
let contentWrapRef = ref<UniViewElement | null>(null)
|
||||
let contentRef = ref<UniViewElement | null>(null)
|
||||
|
||||
const elId = ref(`uni_collapse_item_${Math.ceil(Math.random() * 10e5).toString(36)}`)
|
||||
const contentTransitionDelay = 16
|
||||
|
||||
let enableTransitionTimer = 0
|
||||
let isAndroidArrowRenderFix = false
|
||||
|
||||
// #ifdef APP-ANDROID
|
||||
// Android arrow fix:
|
||||
// keep transform rotation, but avoid animating border colors for border-drawn arrows.
|
||||
// Keep split border-* properties here. Do not use border shorthand with var().
|
||||
isAndroidArrowRenderFix = true
|
||||
// #endif
|
||||
|
||||
const registerChild = inject<((child : ItemChildType) => void) | null>(UNI_COLLAPSE_REGISTER_CHILD, null)
|
||||
const unregisterChild = inject<((elId : string) => void) | null>(UNI_COLLAPSE_UNREGISTER_CHILD, null)
|
||||
const collapseToggle = inject<((elId : string) => void) | null>(UNI_COLLAPSE_CHILD_TOGGLE, null)
|
||||
|
||||
const arrowPlatformClass = computed(() : string => {
|
||||
return isAndroidArrowRenderFix ? 'down_arrow--android-fix' : ''
|
||||
})
|
||||
|
||||
const arrowStateClass = computed(() : string => {
|
||||
if (!is_open.value) {
|
||||
return ''
|
||||
}
|
||||
return isAndroidArrowRenderFix ? 'down_arrow--android-active' : 'down_arrow--active'
|
||||
})
|
||||
|
||||
const arrowDisabledClass = computed(() : string => {
|
||||
return props.disabled ? 'down_arrow--disabled' : ''
|
||||
})
|
||||
|
||||
function clearEnableTransitionTimer() {
|
||||
if (enableTransitionTimer != 0) {
|
||||
clearTimeout(enableTransitionTimer)
|
||||
enableTransitionTimer = 0
|
||||
}
|
||||
}
|
||||
|
||||
function openOrClose(open : boolean) {
|
||||
box_is_open.value = open
|
||||
}
|
||||
|
||||
function applyOpenState(open : boolean, syncSiblings : boolean, triggerEmit : boolean) {
|
||||
if (is_open.value == open && box_is_open.value == open) {
|
||||
return
|
||||
}
|
||||
|
||||
if (open && syncSiblings && collapseToggle != null) {
|
||||
collapseToggle(elId.value)
|
||||
}
|
||||
|
||||
is_open.value = open
|
||||
openOrClose(open)
|
||||
|
||||
if (triggerEmit) {
|
||||
emit('change', open)
|
||||
}
|
||||
}
|
||||
|
||||
function openCollapse(open : boolean) {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
applyOpenState(open, true, true)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (registerChild != null) {
|
||||
const child : ItemChildType = {
|
||||
is_open,
|
||||
elId: elId.value,
|
||||
openOrClose
|
||||
}
|
||||
registerChild(child)
|
||||
}
|
||||
|
||||
if (is_open.value && collapseToggle != null) {
|
||||
collapseToggle(elId.value)
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
enableTransitionTimer = setTimeout(() => {
|
||||
enableTransitionTimer = 0
|
||||
transition_enabled.value = true
|
||||
}, contentTransitionDelay)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearEnableTransitionTimer()
|
||||
if (unregisterChild != null) {
|
||||
unregisterChild(elId.value)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() : boolean => props.open, (value : boolean) => {
|
||||
applyOpenState(value, false, false)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
is_open,
|
||||
openOrClose,
|
||||
openCollapse
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.uni-collapse-item__title {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 12px 18px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.uni-collapse-item__title-main {
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.uni-collapse-item__title-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.open--active {
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.is-disabled {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.down_arrow {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-left: 12px;
|
||||
margin-top: -3px;
|
||||
transform: rotate(45deg);
|
||||
border-right-width: 1px;
|
||||
border-right-style: solid;
|
||||
border-right-color: #999999;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #999999;
|
||||
transition-property: transform, border-right-color, border-bottom-color;
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.down_arrow--active {
|
||||
margin-top: 0px;
|
||||
transform: rotate(-135deg);
|
||||
}
|
||||
|
||||
.down_arrow--android-fix {
|
||||
transition-property: transform;
|
||||
}
|
||||
|
||||
.down_arrow--android-active {
|
||||
margin-top: 0px;
|
||||
transform: rotate(-135deg);
|
||||
}
|
||||
|
||||
.down_arrow--disabled {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.uni-collapse-item__content {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-height: 0px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.uni-collapse-item__content-box {
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
.content-state-open {
|
||||
max-height: 2000px;
|
||||
}
|
||||
|
||||
.content-state-close {
|
||||
max-height: 0px;
|
||||
}
|
||||
|
||||
.content-box-open {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
|
||||
.content-box-close {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
.content-transition--active {
|
||||
transition-duration: 180ms;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
/* 不要恢复旧的“运行时测量高度 + 内联 style.height”方案。
|
||||
该方案在 HarmonyOS 上会出现默认展开时外层容器仍为 0 高度,导致内容不显示。 */
|
||||
.uni-collapse-item__content {
|
||||
transition-property: max-height;
|
||||
}
|
||||
|
||||
.uni-collapse-item__content-box {
|
||||
transition-property: opacity, transform;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<view>
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 折叠面板
|
||||
* @description 折叠面板组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
import type { ItemChildType } from '../type.ts'
|
||||
import {
|
||||
UNI_COLLAPSE_REGISTER_CHILD,
|
||||
UNI_COLLAPSE_UNREGISTER_CHILD,
|
||||
UNI_COLLAPSE_CHILD_TOGGLE
|
||||
} from '../common.ts'
|
||||
|
||||
defineOptions({
|
||||
name: 'UniCollapse'
|
||||
})
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* @description 是否开启手风琴模式,开启后同一时间仅允许一个子项展开
|
||||
* @default true
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
accordion? : boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
accordion: true
|
||||
})
|
||||
|
||||
let childNodes = [] as ItemChildType[]
|
||||
|
||||
function registerChild(child : ItemChildType) {
|
||||
const existIndex = childNodes.findIndex((item : ItemChildType) : boolean => item.elId == child.elId)
|
||||
if (existIndex == -1) {
|
||||
childNodes.push(child)
|
||||
}
|
||||
}
|
||||
|
||||
function unregisterChild(elId : string) {
|
||||
childNodes = childNodes.filter((item : ItemChildType) : boolean => item.elId != elId)
|
||||
}
|
||||
|
||||
function toggle(elId : string) {
|
||||
if (props.accordion && childNodes.length > 0) {
|
||||
childNodes.forEach((item : ItemChildType) => {
|
||||
const isOpen = item.is_open.value as boolean
|
||||
if (isOpen && item.elId != elId) {
|
||||
item.is_open.value = false
|
||||
item.openOrClose(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
provide(UNI_COLLAPSE_REGISTER_CHILD, registerChild)
|
||||
provide(UNI_COLLAPSE_UNREGISTER_CHILD, unregisterChild)
|
||||
provide(UNI_COLLAPSE_CHILD_TOGGLE, toggle)
|
||||
</script>
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"id": "uni-collapse-x",
|
||||
"displayName": "uni-collapse 折叠面板",
|
||||
"version": "1.0.1",
|
||||
"description": "Collapse 折叠面板组件,可自定义折叠面板的展开和收起。",
|
||||
"isExt": true,
|
||||
"extUrl": "https://ext.dcloud.net.cn/plugin?id=27899",
|
||||
"keywords": [
|
||||
"uni-ui",
|
||||
"uniui",
|
||||
"collapse",
|
||||
"折叠面板",
|
||||
"uni-app-x"
|
||||
],
|
||||
"engines": {
|
||||
"HBuilderX": "",
|
||||
"uni-app": "",
|
||||
"uni-app-x": "^5.07"
|
||||
},
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"type": "component-vue",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x",
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"encrypt": [],
|
||||
"dependencies": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": "√",
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
折叠面板组件,包含:
|
||||
|
||||
- `uni-collapse`:父容器
|
||||
- `uni-collapse-item`:子面板
|
||||
|
||||
一个折叠面板组件可以包含若干uni-collapse-item。每个uni-collapse-item包括顶部标题栏和通过slot传入的内容区。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```html
|
||||
<uni-collapse :accordion="true">
|
||||
<uni-collapse-item title="标题 1" :open="true">
|
||||
<view><text>内容 1</text></view>
|
||||
</uni-collapse-item>
|
||||
<uni-collapse-item title="标题 2">
|
||||
<view><text>内容 2</text></view>
|
||||
</uni-collapse-item>
|
||||
</uni-collapse>
|
||||
```
|
||||
|
||||
uni-collapse-item的标题栏的自定义:
|
||||
- 标题栏容器可通过 `title-wrap-class` 自定义背景等容器样式。
|
||||
- 标题栏支持title属性,可通过title-class自定义文字样式。
|
||||
- 标题展开态可通过 `title-open-class` 自定义激活文字样式。组件默认使用透明度区分状态,便于外部继续覆盖文字颜色。
|
||||
- 标题禁用态可通过 `title-disabled-class` 自定义禁用文字样式。若同时存在展开态和禁用态,以禁用态为最终状态。
|
||||
- 内容外层容器可通过 `content-wrap-class` 自定义背景等容器样式。
|
||||
- 标题栏右边默认带有箭头,可通过arrow-class自定义箭头样式。该箭头为一个view包括2个直角边的边框,然后旋转而成。
|
||||
- 箭头展开态和禁用态可分别通过 `arrow-open-class`、`arrow-disabled-class` 覆盖。组件默认也使用透明度区分状态,便于外部继续覆盖箭头颜色。
|
||||
- 如果完全不想要默认包含的title和arrow,也可以传入一个名为title的具名插槽来替换。
|
||||
|
||||
### 具名插槽标题
|
||||
|
||||
```html
|
||||
<uni-collapse-item>
|
||||
<template #title="{ open, disabled }">
|
||||
<view class="custom-title">
|
||||
<view class="custom-title-icon"></view>
|
||||
<text class="custom-title-text">{{ open ? '已展开' : '未展开' }}</text>
|
||||
</view>
|
||||
</template>
|
||||
<view><text>自定义标题内容</text></view>
|
||||
</uni-collapse-item>
|
||||
```
|
||||
|
||||
### externalClass
|
||||
|
||||
```html
|
||||
<uni-collapse-item
|
||||
title="自定义样式"
|
||||
title-wrap-class="my-title-wrap-class"
|
||||
title-class="my-title-class"
|
||||
title-open-class="my-title-open-class"
|
||||
content-wrap-class="my-content-wrap-class"
|
||||
arrow-class="my-arrow-class"
|
||||
arrow-open-class="my-arrow-open-class"
|
||||
>
|
||||
<view><text>内容区域</text></view>
|
||||
</uni-collapse-item>
|
||||
```
|
||||
|
||||
### Slots
|
||||
|
||||
#### uni-collapse-item
|
||||
|
||||
| 插槽名 | 说明 | 插槽参数 |
|
||||
| --- | --- | --- |
|
||||
| default | 面板内容 | - |
|
||||
| title | 自定义标题区域 | `open: boolean`、`disabled: boolean` |
|
||||
|
||||
### Events
|
||||
|
||||
#### uni-collapse-item
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
| --- | --- | --- |
|
||||
| change | 面板展开状态变化时触发 | `open: boolean` |
|
||||
|
||||
### Expose
|
||||
|
||||
#### uni-collapse-item
|
||||
|
||||
| 方法名 | 说明 |
|
||||
| --- | --- |
|
||||
| openCollapse(open: boolean) | 执行展开/收起,会遵循 `disabled` 和手风琴逻辑 |
|
||||
| openOrClose(open: boolean) | 直接切换内容显示状态 |
|
||||
|
||||
### 暗黑模式适配
|
||||
|
||||
建议通过组件 `class` 和 externalClass 组合传入暗色样式:
|
||||
|
||||
- 用组件 `class` 统一控制根节点和内容区等外层视觉。
|
||||
- 用 `title-wrap-class`、`title-class`、`title-open-class`、`content-wrap-class`、`arrow-class`、`arrow-open-class` 等 externalClass 覆盖标题区、内容外层和箭头样式。
|
||||
- 默认展开态和禁用态主要通过透明度表达,减少颜色覆盖时的交叉冲突。
|
||||
|
||||
### 注意
|
||||
- 本组件在Android上需HBuilderX 5.09+。在低版本上箭头变颜色+旋转时,在Android上会消失
|
||||
@@ -0,0 +1,6 @@
|
||||
## 1.0.2 (2026-10-08)
|
||||
- 修复安卓蒸汽模式会出现内容不显示的问题
|
||||
## 1.0.1(2026-08-28)
|
||||
- 修复 web 运行报错的问题
|
||||
## 1.0.0(2026-08-28)
|
||||
- 新增 uni-combox 组合框组件。
|
||||
@@ -0,0 +1,660 @@
|
||||
<template>
|
||||
<view :id="anchorId" class="uni-combox">
|
||||
<view
|
||||
class="uni-combox__field"
|
||||
@click="onFieldClick"
|
||||
<!-- #ifdef WEB -->
|
||||
tabindex="0"
|
||||
@keydown="onInputKeydown"
|
||||
<!-- #endif -->
|
||||
>
|
||||
<view v-if="inputMode" class="uni-combox__input-slot">
|
||||
<slot
|
||||
name="input"
|
||||
:value="currentValue"
|
||||
:input="onInput"
|
||||
:focus="onInputFocus"
|
||||
></slot>
|
||||
</view>
|
||||
<text
|
||||
v-else
|
||||
class="uni-combox__result"
|
||||
:class="[resultTextClass, currentValue == '' ? 'uni-combox__result--placeholder' : '']"
|
||||
max-lines="1"
|
||||
>{{ displayText }}</text>
|
||||
<view class="uni-combox__arrow-wrap" @click.stop="toggleOptionsByArrow">
|
||||
<view class="uni-combox__arrow" :class="[arrowClass, opened ? 'uni-combox__arrow--open' : '']"></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #ifdef WEB || (APP-ANDROID && !VUE3-VAPOR) -->
|
||||
<teleport v-if="opened && popupReady" to="body">
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef WEB || (APP-ANDROID && !VUE3-VAPOR) -->
|
||||
<teleport v-if="opened && popupReady" to="page">
|
||||
<!-- #endif -->
|
||||
<view class="uni-combox__mask" @click="closeOptions">
|
||||
<view
|
||||
class="uni-combox__options"
|
||||
:class="optionsViewClass"
|
||||
:style="optionsStyle"
|
||||
@click.stop="noop"
|
||||
>
|
||||
<scroll-view
|
||||
direction="vertical"
|
||||
class="uni-combox__options-scroll"
|
||||
:scroll-into-view="scrollIntoViewId"
|
||||
>
|
||||
<view class="uni-combox__options-content">
|
||||
<view
|
||||
v-if="visibleOptions.length == 0"
|
||||
class="uni-combox__option-row"
|
||||
>
|
||||
<text
|
||||
class="uni-combox__option uni-combox__option--empty"
|
||||
:class="optionTextClass"
|
||||
max-lines="1"
|
||||
>无匹配项</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="(item, index) in visibleOptions"
|
||||
:id="getOptionId(index)"
|
||||
:key="item + '_' + index.toString()"
|
||||
class="uni-combox__option-row"
|
||||
:class="index == highlightedIndex ? 'uni-combox__option-row--highlight' : ''"
|
||||
@click.stop="selectOption(index)"
|
||||
>
|
||||
<text
|
||||
class="uni-combox__option"
|
||||
:class="[optionTextClass, index == highlightedIndex ? optionTextHighlightClass : '']"
|
||||
max-lines="1"
|
||||
>{{ item }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</teleport>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Combox 组合框
|
||||
* @description 默认是单选选择框,通过 input 具名插槽切换为可输入、可选择的组合框。
|
||||
* @uniPlatform {
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* @property {Array<String>} options 候选字符串数组
|
||||
* @property {String} modelValue 当前值,支持 v-model
|
||||
* @property {String} placeholder 占位文字
|
||||
* @property {String} arrowClass 右侧箭头自定义样式类
|
||||
* @property {String} optionsViewClass 候选浮层外层自定义样式类
|
||||
* @property {String} optionTextClass 候选项文字自定义样式类
|
||||
* @property {String} optionTextHighlightClass 候选项高亮自定义样式类
|
||||
* @property {String} resultTextClass 单选模式结果文字自定义样式类
|
||||
* @event {Function} update:modelValue 值更新时触发
|
||||
* @event {Function} selectchange 从候选项选中时触发
|
||||
*/
|
||||
|
||||
defineOptions({
|
||||
externalClasses: [
|
||||
'arrow-class',
|
||||
'options-view-class',
|
||||
'option-text-class',
|
||||
'option-text-highlight-class',
|
||||
'result-text-class'
|
||||
]
|
||||
})
|
||||
|
||||
type Props = {
|
||||
options? : string[],
|
||||
modelValue? : string,
|
||||
placeholder? : string,
|
||||
arrowClass? : string.ClassString,
|
||||
optionsViewClass? : string.ClassString,
|
||||
optionTextClass? : string.ClassString,
|
||||
optionTextHighlightClass? : string.ClassString,
|
||||
resultTextClass? : string.ClassString
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
options: () : string[] => [] as string[],
|
||||
modelValue: '',
|
||||
placeholder: '',
|
||||
arrowClass: '',
|
||||
optionsViewClass: '',
|
||||
optionTextClass: '',
|
||||
optionTextHighlightClass: '',
|
||||
resultTextClass: ''
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value : string],
|
||||
selectchange: [value : string]
|
||||
}>()
|
||||
|
||||
defineSlots<{
|
||||
input(props : {
|
||||
value : string,
|
||||
input : (event : UniInputEvent) => void,
|
||||
focus : (event : UniInputFocusEvent) => void
|
||||
}) : any
|
||||
}>()
|
||||
|
||||
const slots = useSlots()
|
||||
const anchorId = `uni_combox_x_${Math.ceil(Math.random() * 10e8).toString(36)}`
|
||||
const optionIdPrefix = `uni_combox_x_option_${Math.ceil(Math.random() * 10e8).toString(36)}_`
|
||||
const optionHeight = 36
|
||||
const defaultMaxOptionsHeight = 200
|
||||
const popupVerticalPadding = 8
|
||||
const viewportGap = 8
|
||||
const popupGap = 4
|
||||
|
||||
function getOptionId(index : number) : string {
|
||||
return optionIdPrefix + index.toString()
|
||||
}
|
||||
|
||||
const currentValue = ref<string>(props.modelValue)
|
||||
const opened = ref<boolean>(false)
|
||||
const popupReady = ref<boolean>(false)
|
||||
const filterEnabled = ref<boolean>(false)
|
||||
const highlightedIndex = ref<number>(-1)
|
||||
const popupLeft = ref<number>(0)
|
||||
const popupTop = ref<number>(0)
|
||||
const popupWidth = ref<number>(0)
|
||||
const popupHeight = ref<number>(optionHeight + popupVerticalPadding)
|
||||
const keyboardHeight = ref<number>(0)
|
||||
let positionRequestId = 0
|
||||
let isUnmounted = false
|
||||
|
||||
const inputMode = computed(() : boolean => {
|
||||
return slots['input'] != null
|
||||
})
|
||||
|
||||
const displayText = computed(() : string => {
|
||||
if (currentValue.value == '') {
|
||||
return props.placeholder
|
||||
}
|
||||
return currentValue.value
|
||||
})
|
||||
|
||||
const visibleOptions = computed(() : string[] => {
|
||||
if (!filterEnabled.value || currentValue.value == '') {
|
||||
return props.options
|
||||
}
|
||||
return props.options.filter((item : string) : boolean => {
|
||||
return item.indexOf(currentValue.value) >= 0
|
||||
})
|
||||
})
|
||||
|
||||
const optionsStyle = computed(() : string => {
|
||||
return `left: ${popupLeft.value}px; top: ${popupTop.value}px; width: ${popupWidth.value}px; height: ${popupHeight.value}px;`
|
||||
})
|
||||
|
||||
const scrollIntoViewId = ref<string>('')
|
||||
|
||||
function findMatchedIndex(list : string[], keyword : string) : number {
|
||||
if (keyword == '') {
|
||||
return -1
|
||||
}
|
||||
const exactIndex = list.indexOf(keyword)
|
||||
if (exactIndex >= 0) {
|
||||
return exactIndex
|
||||
}
|
||||
return list.findIndex((item : string) : boolean => {
|
||||
return item.indexOf(keyword) == 0
|
||||
})
|
||||
}
|
||||
|
||||
function syncHighlight() {
|
||||
const list = visibleOptions.value
|
||||
if (list.length == 0) {
|
||||
highlightedIndex.value = -1
|
||||
return
|
||||
}
|
||||
const matchedIndex = findMatchedIndex(list, currentValue.value)
|
||||
highlightedIndex.value = matchedIndex
|
||||
}
|
||||
|
||||
function refreshScrollTarget() {
|
||||
const targetId = highlightedIndex.value >= 0 ? getOptionId(highlightedIndex.value) : ''
|
||||
scrollIntoViewId.value = ''
|
||||
if (targetId == '') {
|
||||
return
|
||||
}
|
||||
nextTick(() => {
|
||||
if (opened.value) {
|
||||
scrollIntoViewId.value = targetId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function emitValue(value : string) {
|
||||
currentValue.value = value
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
function noop() {
|
||||
}
|
||||
|
||||
function applyPopupRect(rect : DOMRect) {
|
||||
const rectTop = rect.top != null ? rect.top : 0
|
||||
const rectLeft = rect.left != null ? rect.left : 0
|
||||
let rectWidth = rect.width != null ? rect.width : 0
|
||||
const rectHeight = rect.height != null ? rect.height : 0
|
||||
const rectBottom = rect.bottom != null ? rect.bottom : rectTop + rectHeight
|
||||
if (rectWidth <= 0) {
|
||||
rectWidth = uni.getWindowInfo().windowWidth - 32
|
||||
}
|
||||
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
const windowHeight = Math.max(0, windowInfo.windowHeight - keyboardHeight.value)
|
||||
const optionCount = Math.max(1, visibleOptions.value.length)
|
||||
const expectedHeight = Math.min(defaultMaxOptionsHeight, optionCount * optionHeight) + popupVerticalPadding
|
||||
const minOptionsHeight = optionHeight + popupVerticalPadding
|
||||
const belowSpace = windowHeight - rectBottom - viewportGap
|
||||
const aboveSpace = rectTop - viewportGap
|
||||
|
||||
let height = expectedHeight
|
||||
let top = rectBottom + popupGap
|
||||
if (belowSpace < expectedHeight && aboveSpace > belowSpace) {
|
||||
height = Math.max(minOptionsHeight, Math.min(expectedHeight, aboveSpace - popupGap))
|
||||
top = rectTop - popupGap - height
|
||||
} else {
|
||||
height = Math.max(minOptionsHeight, Math.min(expectedHeight, belowSpace - popupGap))
|
||||
}
|
||||
|
||||
popupLeft.value = rectLeft
|
||||
popupTop.value = top
|
||||
popupWidth.value = rectWidth
|
||||
popupHeight.value = height
|
||||
}
|
||||
|
||||
function updatePosition() {
|
||||
const element = uni.getElementById(anchorId)
|
||||
if (element == null) {
|
||||
return
|
||||
}
|
||||
const rectPromise = element.getBoundingClientRectAsync()
|
||||
if (rectPromise == null) {
|
||||
return
|
||||
}
|
||||
rectPromise.then((rect : DOMRect) => {
|
||||
if (isUnmounted || !opened.value) {
|
||||
return
|
||||
}
|
||||
applyPopupRect(rect)
|
||||
})
|
||||
}
|
||||
|
||||
function updatePositionThenOpen() {
|
||||
const requestId = positionRequestId + 1
|
||||
positionRequestId = requestId
|
||||
const element = uni.getElementById(anchorId)
|
||||
if (element == null) {
|
||||
if (isUnmounted || requestId != positionRequestId) {
|
||||
return
|
||||
}
|
||||
popupReady.value = true
|
||||
opened.value = true
|
||||
refreshScrollTarget()
|
||||
return
|
||||
}
|
||||
const rectPromise = element.getBoundingClientRectAsync()
|
||||
if (rectPromise == null) {
|
||||
if (isUnmounted || requestId != positionRequestId) {
|
||||
return
|
||||
}
|
||||
popupReady.value = true
|
||||
opened.value = true
|
||||
refreshScrollTarget()
|
||||
return
|
||||
}
|
||||
rectPromise.then((rect : DOMRect) => {
|
||||
if (isUnmounted || requestId != positionRequestId) {
|
||||
return
|
||||
}
|
||||
applyPopupRect(rect)
|
||||
popupReady.value = true
|
||||
opened.value = true
|
||||
refreshScrollTarget()
|
||||
})
|
||||
}
|
||||
|
||||
function openOptionsWithMode(filter : boolean) {
|
||||
filterEnabled.value = filter
|
||||
syncHighlight()
|
||||
popupReady.value = false
|
||||
updatePositionThenOpen()
|
||||
}
|
||||
|
||||
function openOptions() {
|
||||
openOptionsWithMode(false)
|
||||
}
|
||||
|
||||
function closeOptions() {
|
||||
positionRequestId += 1
|
||||
opened.value = false
|
||||
popupReady.value = false
|
||||
}
|
||||
|
||||
function toggleOptionsByArrow() {
|
||||
if (opened.value) {
|
||||
closeOptions()
|
||||
return
|
||||
}
|
||||
openOptions()
|
||||
}
|
||||
|
||||
function onFieldClick() {
|
||||
if (inputMode.value) {
|
||||
openOptionsWithMode(true)
|
||||
return
|
||||
}
|
||||
openOptions()
|
||||
}
|
||||
|
||||
function onInput(event : UniInputEvent) {
|
||||
const value = event.detail.value
|
||||
emitValue(value)
|
||||
openOptionsWithMode(true)
|
||||
}
|
||||
|
||||
function onInputFocus(_event : UniInputFocusEvent) {
|
||||
openOptionsWithMode(true)
|
||||
}
|
||||
|
||||
function onInputBlur() {
|
||||
}
|
||||
|
||||
function onKeyboardHeightChange(res : OnKeyboardHeightChangeCallbackResult) {
|
||||
if (isUnmounted) {
|
||||
return
|
||||
}
|
||||
keyboardHeight.value = Math.max(0, res.height)
|
||||
if (opened.value) {
|
||||
nextTick(() => {
|
||||
if (!isUnmounted && opened.value) {
|
||||
updatePosition()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let keyboardListenerId = 0
|
||||
|
||||
onMounted(() => {
|
||||
keyboardListenerId = uni.onKeyboardHeightChange(onKeyboardHeightChange)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isUnmounted = true
|
||||
positionRequestId += 1
|
||||
opened.value = false
|
||||
popupReady.value = false
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (keyboardListenerId != 0) {
|
||||
uni.offKeyboardHeightChange(keyboardListenerId)
|
||||
keyboardListenerId = 0
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// #ifdef WEB
|
||||
function onInputKeydown(event : KeyboardEvent) {
|
||||
const key = event.key as string
|
||||
if (key == 'Escape') {
|
||||
event.preventDefault()
|
||||
closeOptions()
|
||||
return
|
||||
}
|
||||
if (key == 'ArrowDown') {
|
||||
event.preventDefault()
|
||||
if (!opened.value) {
|
||||
openOptions()
|
||||
return
|
||||
}
|
||||
if (visibleOptions.value.length == 0) {
|
||||
return
|
||||
}
|
||||
highlightedIndex.value = Math.min(visibleOptions.value.length - 1, highlightedIndex.value + 1)
|
||||
refreshScrollTarget()
|
||||
return
|
||||
}
|
||||
if (key == 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
if (!opened.value) {
|
||||
openOptions()
|
||||
return
|
||||
}
|
||||
if (visibleOptions.value.length == 0) {
|
||||
return
|
||||
}
|
||||
highlightedIndex.value = Math.max(0, highlightedIndex.value - 1)
|
||||
refreshScrollTarget()
|
||||
return
|
||||
}
|
||||
if (key == 'Enter' && opened.value && highlightedIndex.value >= 0) {
|
||||
event.preventDefault()
|
||||
selectOption(highlightedIndex.value)
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
function selectOption(index : number) {
|
||||
if (index < 0 || index >= visibleOptions.value.length) {
|
||||
return
|
||||
}
|
||||
const value = visibleOptions.value[index]
|
||||
emitValue(value)
|
||||
emit('selectchange', value)
|
||||
closeOptions()
|
||||
}
|
||||
|
||||
function setInputValue(value : string) {
|
||||
emitValue(value)
|
||||
openOptionsWithMode(true)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
setInputValue,
|
||||
openOptions,
|
||||
closeOptions
|
||||
})
|
||||
|
||||
watch(() : string => props.modelValue, (value : string) => {
|
||||
currentValue.value = value
|
||||
if (opened.value) {
|
||||
syncHighlight()
|
||||
nextTick(() => {
|
||||
updatePosition()
|
||||
refreshScrollTarget()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(() : string[] => props.options, () => {
|
||||
if (opened.value) {
|
||||
syncHighlight()
|
||||
nextTick(() => {
|
||||
updatePosition()
|
||||
refreshScrollTarget()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onBackPress(() : boolean => {
|
||||
if (opened.value) {
|
||||
closeOptions()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.uni-combox {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.uni-combox__field {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: 36px;
|
||||
border: 1px solid #DCDFE6;
|
||||
border-radius: 4px;
|
||||
background-color: #ffffff;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.uni-combox__input-slot {
|
||||
flex: 1;
|
||||
height: 22px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.uni-combox__result {
|
||||
flex: 1;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
padding-right: 10px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.uni-combox__result--placeholder {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.uni-combox__arrow-wrap {
|
||||
width: 28px;
|
||||
height: 36px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.uni-combox__arrow {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-right: 1px solid #999999;
|
||||
border-bottom: 1px solid #999999;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.uni-combox__arrow--open {
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
|
||||
.uni-combox__mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 999;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.uni-combox__options {
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #EBEEF5;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.uni-combox__options-scroll {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.uni-combox__options-content {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.uni-combox__option-row {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.uni-combox__option-row--highlight {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.uni-combox__option {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.uni-combox__option--empty {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.uni-combox__field {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
.uni-combox__result {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.uni-combox__option {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.uni-combox__result--placeholder {
|
||||
color: #8f8f8f;
|
||||
}
|
||||
|
||||
.uni-combox__option--empty {
|
||||
color: #8f8f8f;
|
||||
}
|
||||
|
||||
.uni-combox__arrow {
|
||||
border-right-color: #8f8f8f;
|
||||
border-bottom-color: #8f8f8f;
|
||||
}
|
||||
|
||||
.uni-combox__options {
|
||||
background-color: #2d2d2d;
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.uni-combox__option-row--highlight {
|
||||
background-color: #3b3b3b;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"id": "uni-combox-x",
|
||||
"displayName": "uni-combox 组合框",
|
||||
"version": "1.0.2",
|
||||
"description": "Combox 组合框组件,支持默认单选和通过 input 插槽启用可输可选模式。",
|
||||
"isExt": true,
|
||||
"extUrl": "https://ext.dcloud.net.cn/plugin?id=29441",
|
||||
"keywords": [
|
||||
"组合框",
|
||||
"combox",
|
||||
"uni-app-x"
|
||||
],
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"engines": {
|
||||
"HBuilderX": "",
|
||||
"uni-app": "",
|
||||
"uni-app-x": "^5.31"
|
||||
},
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"type": "component-vue",
|
||||
"darkmode": "√",
|
||||
"i18n": "x",
|
||||
"widescreen": "√",
|
||||
"npmurl": "",
|
||||
"vapor": "√"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": {
|
||||
"extVersion": "1.0.2",
|
||||
"minVersion": ""
|
||||
},
|
||||
"chrome": {
|
||||
"extVersion": "1.0.2",
|
||||
"minVersion": ""
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "1.0.2",
|
||||
"minVersion": ""
|
||||
},
|
||||
"ios": {
|
||||
"extVersion": "1.0.2",
|
||||
"minVersion": ""
|
||||
},
|
||||
"harmony": {
|
||||
"extVersion": "1.0.2",
|
||||
"minVersion": ""
|
||||
}
|
||||
},
|
||||
"mp": {
|
||||
"weixin": {
|
||||
"extVersion": "1.0.2",
|
||||
"minVersion": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
uni-combox 组合框组件。
|
||||
|
||||
默认为单选模式,类似 select 组件,只能选择不能输入。通过具名插槽 `input` 传入 input 组件后,切换为可输入、可选择的组合模式。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```html
|
||||
<uni-combox :options="options" placeholder="请选择" v-model="value" />
|
||||
```
|
||||
|
||||
### options
|
||||
|
||||
```html
|
||||
<uni-combox :options="gradeOptions" placeholder="请选择年级" v-model="grade" />
|
||||
```
|
||||
|
||||
### modelValue / v-model
|
||||
|
||||
```html
|
||||
<uni-combox :options="cityOptions" v-model="city" placeholder="请选择城市" />
|
||||
```
|
||||
|
||||
### placeholder
|
||||
|
||||
```html
|
||||
<uni-combox :options="cityOptions" placeholder="这里展示 placeholder" v-model="value" />
|
||||
```
|
||||
|
||||
### 组合模式
|
||||
|
||||
```html
|
||||
<uni-combox :options="options" v-model="city">
|
||||
<template #input="{ value, input }">
|
||||
<input
|
||||
:value="value"
|
||||
placeholder="选择或输入城市"
|
||||
@input="input"
|
||||
/>
|
||||
</template>
|
||||
</uni-combox>
|
||||
```
|
||||
|
||||
输入时会打开候选浮层,并按 startsWith 规则过滤候选项。点击右侧箭头打开时展示完整候选列表,并高亮当前输入匹配项。
|
||||
|
||||
### 自定义样式
|
||||
|
||||
```html
|
||||
<uni-combox
|
||||
:options="options"
|
||||
v-model="value"
|
||||
arrow-class="demo-arrow"
|
||||
options-view-class="demo-options-view"
|
||||
option-text-class="demo-option-text"
|
||||
option-text-highlight-class="demo-option-highlight"
|
||||
result-text-class="demo-result-text"
|
||||
/>
|
||||
```
|
||||
|
||||
也可以单独传入任意一个样式类属性。
|
||||
|
||||
```html
|
||||
<uni-combox :options="options" v-model="value" arrow-class="demo-arrow" />
|
||||
<uni-combox :options="options" v-model="value" options-view-class="demo-options-view" />
|
||||
<uni-combox :options="options" v-model="value" option-text-class="demo-option-text" />
|
||||
<uni-combox :options="options" v-model="value" option-text-highlight-class="demo-option-highlight" />
|
||||
<uni-combox :options="options" v-model="value" result-text-class="demo-result-text" />
|
||||
```
|
||||
|
||||
### 事件
|
||||
|
||||
```html
|
||||
<uni-combox
|
||||
:options="options"
|
||||
v-model="value"
|
||||
@update:modelValue="onUpdateModelValue"
|
||||
@selectchange="onSelectChange"
|
||||
/>
|
||||
```
|
||||
|
||||
### 浮层
|
||||
|
||||
候选浮层通过 `teleport` 渲染到页面层,不受组件自身尺寸裁剪。下方空间不足且上方空间更充足时,会自动向上展开。默认最大高度为 200px。
|
||||
|
||||
候选浮层内部使用 `scroll-view`。
|
||||
|
||||
### External Classes
|
||||
|
||||
| 类名 | 说明 |
|
||||
| --- | --- |
|
||||
| arrow-class | 右侧箭头样式 |
|
||||
| options-view-class | 候选浮层外层样式 |
|
||||
| option-text-class | 候选项文字样式 |
|
||||
| option-text-highlight-class | 候选项高亮样式 |
|
||||
| result-text-class | 单选模式结果文字样式 |
|
||||
|
||||
### Props
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| options | Array\<String\> | [] | 候选字符串数组 |
|
||||
| modelValue | String | '' | 当前值,支持 v-model |
|
||||
| placeholder | String | '' | 占位文字 |
|
||||
| arrowClass | String | '' | 右侧箭头自定义样式类 |
|
||||
| optionsViewClass | String | '' | 候选浮层外层自定义样式类 |
|
||||
| optionTextClass | String | '' | 候选项文字自定义样式类 |
|
||||
| optionTextHighlightClass | String | '' | 候选项高亮自定义样式类 |
|
||||
| resultTextClass | String | '' | 单选模式结果文字自定义样式类 |
|
||||
|
||||
### Events
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
| --- | --- | --- |
|
||||
| update:modelValue | 值更新时触发 | value: string |
|
||||
| selectchange | 从候选项选中时触发 | value: string |
|
||||
|
||||
### Expose
|
||||
|
||||
| 方法名 | 说明 |
|
||||
| --- | --- |
|
||||
| setInputValue(value: string) | 设置当前值并打开过滤候选 |
|
||||
| openOptions() | 打开候选浮层 |
|
||||
| closeOptions() | 关闭候选浮层 |
|
||||
@@ -0,0 +1,2 @@
|
||||
## 1.0.0(2026-04-30)
|
||||
- 支持 uni-drag-cell 组件
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"id": "uni-drag-cell",
|
||||
"displayName": "uni-drag-cell",
|
||||
"version": "1.0.0",
|
||||
"description": "可拖拽排序组件",
|
||||
"isExt": true,
|
||||
"extUrl": "https://ext.dcloud.net.cn/plugin?id=27845",
|
||||
"keywords": [
|
||||
"uni-drag-cell",
|
||||
"uni-app-x",
|
||||
"uni-app x",
|
||||
"组件"
|
||||
],
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"engines": {
|
||||
"HBuilderX": "^5.08",
|
||||
"uni-app-x": "^5.08",
|
||||
"uni-app": ""
|
||||
},
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x",
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": "√",
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
`uni-drag-cell` 是一个基于 `uni-app x + uvue + UTS` 实现的可拖拽排序组件。
|
||||
|
||||
它适合这些场景:
|
||||
- 标签排序
|
||||
- 宫格排序
|
||||
- 单列列表排序
|
||||
- 带删除能力的拖拽
|
||||
- 尾部追加“+”入口
|
||||
- 手柄拖拽排序
|
||||
- 九宫格图片选择和排序
|
||||
- 支付方式排序
|
||||
|
||||
组件通过 v-model 绑定了一个数组,通过插槽渲染绑定数组的内容,通过拖拽调整数组项的顺序。
|
||||
|
||||
组件还提供了show-close属性,显示底部删除条,拖到删除条即可删除。
|
||||
|
||||
组件还提供了show-append属性,显示尾部的添加插槽。该插槽不参与拖拽排序,始终在最后一个。(添加数组项的逻辑需自行实现)
|
||||
|
||||
通过这些能力,本组件具备对数组数据排序、添加、删除的能力。
|
||||
|
||||
但本组件不适合超长虚拟列表。不具备拖动到顶部或底部后自动滚动列表的功能。
|
||||
|
||||
### 设计目标
|
||||
|
||||
这个组件的设计不是“拖一下就立刻改数组”,而是分成两个阶段:
|
||||
|
||||
1. **拖拽预览阶段**
|
||||
- 拖动过程中,通过 `transform` 动态调整视觉顺序
|
||||
- 其他 item 会根据碰撞结果做位移动画
|
||||
- 此时外部 `v-model` 还不会被正式改写
|
||||
|
||||
2. **拖拽提交阶段**
|
||||
- 松手后,才正式修改内部数组顺序
|
||||
- 然后通过 `update:modelValue` 和 `change` 把最终结果抛给外部
|
||||
|
||||
这样做的好处是:
|
||||
- 拖动过程更平滑
|
||||
- 外部响应式更新次数更少
|
||||
- 更容易支持删除条、追加入口、手柄拖拽等扩展能力
|
||||
|
||||
### 交互模式
|
||||
|
||||
组件目前支持两种进入拖拽态的方式:长按拖拽和手柄拖拽。
|
||||
|
||||
#### 1. 长按拖拽
|
||||
|
||||
默认模式。
|
||||
|
||||
- 长按某个 item 后进入拖拽态
|
||||
- 当前长按触发时间为 `350ms`
|
||||
- 进入拖拽态后,item 会跟随手指/鼠标移动,并且会scale放大1.12倍。
|
||||
- 拖动过程中持续做碰撞检测,并实时预览排序结果
|
||||
|
||||
补充:
|
||||
- 如果按下后移动距离超过组件内部阈值,会取消本次长按判定,避免页面滚动时误触发拖拽
|
||||
|
||||
适合:
|
||||
- 标签排序
|
||||
- 宫格排序
|
||||
- 图片排序
|
||||
|
||||
#### 2. 手柄拖拽
|
||||
|
||||
当 `handleMode` 为 `true`,且同时提供 `handle` 插槽时,会启用手柄模式。
|
||||
|
||||
- 不再需要长按
|
||||
- 只有拖动手柄才会开始排序
|
||||
- 内容区本身可以继续点击、滚动、预览
|
||||
|
||||
适合:
|
||||
- 设置项排序
|
||||
- 表单项排序
|
||||
- 内容区本身还有点击行为的列表
|
||||
|
||||
### 插槽
|
||||
|
||||
| 插槽名 | 插槽参数 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `default` | `{ item, index }` | 默认内容插槽,用于渲染每一个可排序项。组件不限制 item 的视觉形式,可以自由渲染标签、卡片、宫格、图片等内容。 |
|
||||
| `append` | - | 列表末尾的固定入口。不参与拖拽排序,不会写入 `v-model`,不会被删除,适合放“新增按钮”“上传入口”“更多入口”等内容。 |
|
||||
| `handle` | `{ item, index }` | 拖拽手柄插槽,仅在 `handleMode` 开启时生效。手柄由组件放在每个 item 的右上角,建议给内容区预留右侧空间,避免手柄遮挡正文。 |
|
||||
|
||||
### 事件
|
||||
|
||||
| 事件名 | 回调参数 | 触发时机 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `change` | 最新数组 | 排序完成后、删除完成后 | 最终结果事件。拖动中的视觉预览不会频繁触发 `change`。 |
|
||||
|
||||
### 使用方式
|
||||
|
||||
#### 1. 基础标签排序
|
||||
|
||||
```vue
|
||||
<uni-drag-cell v-model="tagList">
|
||||
<template #default="{ item, index }">
|
||||
<view class="tag-item" @click="onClickTag(item, index)">
|
||||
<text>{{ item }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</uni-drag-cell>
|
||||
```
|
||||
|
||||
适合:
|
||||
- 标签拖拽
|
||||
- 轻量横向流式布局
|
||||
|
||||
说明:
|
||||
- 默认使用长按触发拖拽
|
||||
- 当前长按触发时间为 `350ms`
|
||||
|
||||
#### 2. 宫格排序
|
||||
|
||||
```vue
|
||||
<uni-drag-cell v-model="gridList" :column="3">
|
||||
<template #default="{ item }">
|
||||
<view class="grid-item">
|
||||
<text>{{ item.name }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</uni-drag-cell>
|
||||
```
|
||||
|
||||
适合:
|
||||
- 应用图标
|
||||
- 功能入口宫格
|
||||
- 九宫格类布局
|
||||
|
||||
#### 3. 带删除条的拖拽
|
||||
|
||||
```vue
|
||||
<uni-drag-cell v-model="list" :show-close="true">
|
||||
<template #default="{ item }">
|
||||
<view class="cell-item">
|
||||
<text>{{ item }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</uni-drag-cell>
|
||||
```
|
||||
|
||||
交互:
|
||||
- 长按进入拖拽
|
||||
- 拖到屏幕底部删除条
|
||||
- 删除条高亮后松手即删除
|
||||
|
||||
#### 4. 尾部追加一个固定入口
|
||||
|
||||
```vue
|
||||
<uni-drag-cell v-model="tagList" :show-append="true">
|
||||
<template #default="{ item }">
|
||||
<view class="tag-item">
|
||||
<text>{{ item }}</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #append>
|
||||
<view class="tag-add" @click="addTag">
|
||||
<text>+</text>
|
||||
</view>
|
||||
</template>
|
||||
</uni-drag-cell>
|
||||
```
|
||||
|
||||
说明:
|
||||
- `append` 是通用设计,不局限于图片场景
|
||||
- 拖拽排序时,`append` 不参与位置计算
|
||||
|
||||
#### 5. 九宫格图片选择
|
||||
|
||||
```vue
|
||||
<uni-drag-cell
|
||||
v-model="imageList"
|
||||
:column="3"
|
||||
:show-close="true"
|
||||
:show-append="imageList.length < 9">
|
||||
<template #default="{ item, index }">
|
||||
<view class="image-item" @click="previewImage(index)">
|
||||
<image :src="item" mode="aspectFill"></image>
|
||||
</view>
|
||||
</template>
|
||||
<template #append>
|
||||
<view class="image-add" @click="chooseImages">
|
||||
<text>+</text>
|
||||
</view>
|
||||
</template>
|
||||
</uni-drag-cell>
|
||||
```
|
||||
|
||||
设计说明:
|
||||
- 图片数组本身就是 `v-model`
|
||||
- `append` 用来放“添加图片”入口
|
||||
- 当达到上限时,把 `showAppend` 设为 `false`
|
||||
|
||||
#### 6. 手柄拖拽排序
|
||||
|
||||
```vue
|
||||
<uni-drag-cell v-model="list" :column="1" :handle-mode="true">
|
||||
<template #default="{ item }">
|
||||
<view class="cell-item">
|
||||
<text>{{ item }}</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #handle>
|
||||
<view class="cell-handle">
|
||||
<text>≡</text>
|
||||
</view>
|
||||
</template>
|
||||
</uni-drag-cell>
|
||||
```
|
||||
|
||||
说明:
|
||||
- 手柄模式下,不需要长按
|
||||
- 当前实现中,手柄模式不会再把整个 item 放大
|
||||
- 这样可以避免单列场景下被父容器裁剪
|
||||
|
||||
### 组件内部设计说明
|
||||
|
||||
#### 1. 排序预览不是直接改数组
|
||||
|
||||
拖动过程中,组件内部维护的是“预览顺序”。
|
||||
|
||||
实现方式:
|
||||
- 记录每个 item 的稳定 id
|
||||
- 根据碰撞结果生成一份预览顺序
|
||||
- 通过 `transform` 让其他项移动到目标位置
|
||||
- 松手后再正式更新数组和 id 顺序
|
||||
|
||||
这样可以避免:
|
||||
- 拖动中频繁改外部数组
|
||||
- 频繁触发父组件重渲染
|
||||
- item 身份丢失导致动画错乱
|
||||
|
||||
#### 2. 删除判定基于拖动内容区域
|
||||
|
||||
删除命中不是简单按手指坐标判断,而是优先按“拖动内容的真实边界”和“删除条边界”是否接触来判断。
|
||||
|
||||
这样做是为了避免:
|
||||
- item 外层有 padding 时提前触发删除
|
||||
- 不同高度 item 在删除区表现不一致
|
||||
- web 与 app 坐标系不一致时出现明显偏移
|
||||
|
||||
#### 3. 追加项与删除条互不干扰
|
||||
|
||||
`append` 项的设计目标是“固定入口”,因此:
|
||||
- 不参与碰撞排序
|
||||
- 不参与删除
|
||||
- 不写入绑定数组
|
||||
|
||||
#### 4. 手柄模式与长按模式共用同一套拖拽链路
|
||||
|
||||
也就是说:
|
||||
- 排序逻辑相同
|
||||
- 删除逻辑相同
|
||||
- 只是“进入拖拽态”的入口不同
|
||||
|
||||
这样可以减少两套逻辑分叉导致的维护成本。
|
||||
|
||||
### 性能注意
|
||||
|
||||
拖拽时耗费性能的点主要有两个:
|
||||
|
||||
#### 1. 拖动中的碰撞检测
|
||||
|
||||
每次 move 都会更新:
|
||||
- 当前拖动偏移
|
||||
- 删除条命中状态
|
||||
- 目标悬停位置
|
||||
|
||||
这是拖拽组件的必要开销,当前实现已经把重测布局的频率控制在较低水平,只在必要时重新测量。
|
||||
|
||||
#### 2. 删除条命中时的实时矩形读取
|
||||
|
||||
当启用 `showClose` 时,拖动中会优先读取当前拖动内容的实时矩形,以保证删除命中准确。
|
||||
|
||||
这部分是“精度优先”的设计,开销可接受,但不建议把它用于超大数量、超复杂 item 的高频拖拽场景。
|
||||
|
||||
### 使用注意事项
|
||||
|
||||
#### 1. 建议 item 尺寸稳定
|
||||
|
||||
尤其是宫格模式下,建议每个 item 的宽高尽量稳定。
|
||||
|
||||
否则可能带来:
|
||||
- 拖动命中感不一致
|
||||
- 排序预览观感不稳定
|
||||
|
||||
#### 2. 手柄模式下给内容区预留右侧空间
|
||||
|
||||
因为手柄是覆盖在 item 右上角的。
|
||||
|
||||
建议:
|
||||
- 内容区右侧留白
|
||||
- 或者把手柄区域设计成视觉上独立的一块
|
||||
|
||||
#### 3. `append` 只是入口,不是数据项
|
||||
|
||||
这点非常重要。
|
||||
|
||||
不要指望:
|
||||
- `append` 参与排序
|
||||
- `append` 进入 `v-model`
|
||||
- `append` 被删除
|
||||
|
||||
如果你需要“真正的数据项”,请直接写进 `modelValue`。
|
||||
|
||||
#### 4. 外部不要在拖拽过程中频繁整体替换列表
|
||||
|
||||
建议:
|
||||
- 由组件在松手后统一回传结果
|
||||
- 父组件收到结果后再更新自己的数据源
|
||||
|
||||
不建议:
|
||||
- 拖动过程中再由父组件持续重建整个数组
|
||||
|
||||
#### 5. 不建议用于超大列表或虚拟列表场景
|
||||
|
||||
这个组件当前更适合:
|
||||
- 小中型标签列表
|
||||
- 宫格
|
||||
- 表单排序
|
||||
- 九宫格图片选择
|
||||
|
||||
如果是超长列表、虚拟滚动列表,应该使用专门为大数据量设计的方案。
|
||||
|
||||
### 平台说明
|
||||
|
||||
#### Web
|
||||
|
||||
- 支持 pointer / touch 交互
|
||||
|
||||
#### Android
|
||||
|
||||
- 支持触摸拖拽
|
||||
@@ -0,0 +1,2 @@
|
||||
## 1.0.0(2026-04-30)
|
||||
- 支持 uni-fab-button 组件
|
||||
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<view style="align-items: center; justify-content: center;" class="fab" @click="handleClick">
|
||||
<view flatten class="plus" :class="plusClass" style="position: absolute;"></view>
|
||||
<view flatten class="plus" :class="plusClass" style="position: absolute;transform: rotate(90deg);"></view>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 悬浮加号按钮
|
||||
* @description 悬浮加号按钮组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
defineOptions({
|
||||
externalClasses: ['plus-class']
|
||||
})
|
||||
const props = defineProps({
|
||||
/**
|
||||
* @description 加号图标的自定义样式类,用于调整加号颜色、尺寸等样式
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
plusClass: { type: String, default: "" }
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* @description 点击事件,参数为事件对象,类型为 UniPointerEvent
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
click: [event: UniPointerEvent]
|
||||
}>()
|
||||
|
||||
const handleClick = (event: UniPointerEvent) => {
|
||||
emit('click', event)
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.fab {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background-color: #007AFF;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 5px 2px rgba(0, 0, 0, .3);
|
||||
/* #ifdef WEB */
|
||||
cursor: pointer;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.plus {
|
||||
background-color: white;
|
||||
border-radius: 2px; /* 给加号边缘一点圆角 */
|
||||
width: 40%;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"id": "uni-fab-button",
|
||||
"displayName": "uni-fab-button",
|
||||
"version": "1.0.0",
|
||||
"description": "uni-fab-button",
|
||||
"isExt": true,
|
||||
"extUrl": "https://ext.dcloud.net.cn/plugin?id=27846",
|
||||
"keywords": [
|
||||
"uni-fab-button",
|
||||
"uni-app-x",
|
||||
"ui",
|
||||
"组件"
|
||||
],
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"engines": {
|
||||
"HBuilderX": "^5.07",
|
||||
"uni-app-x": "^5.07",
|
||||
"uni-app": ""
|
||||
},
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x",
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": "√",
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
悬浮加号按钮
|
||||
|
||||
本组件常用于页面中右下角悬浮的、里面有一个加号的圆形按钮。
|
||||
|
||||
组件的根view默认是一个圆形,蓝色背景(#007AFF),并带有阴影。使用组件时注意组件的父容器要留出阴影的空间,避免被裁剪。
|
||||
|
||||
组件的二级view是2个view交叉组成的加号。其中垂直的view是通过transform变换方向实现的。
|
||||
|
||||
组件的属性为 plus-class,它是一个externalClass,用于外部修改组件内部的、组成加号的2个view的样式。
|
||||
|
||||
组件中加号的默认样式为背景白色、粗细为2px、尺寸为40%。
|
||||
|
||||
### 注意
|
||||
- 鸿蒙平台按下fab-button不缩写的问题,需升级到HBuilderX 5.09+解决
|
||||
@@ -0,0 +1,4 @@
|
||||
## 1.0.0 (2026-10-16)
|
||||
- 新增 uni-app x 和蒸汽模式支持
|
||||
- 新增组合式 API 实现
|
||||
- 调整 支持直接使用 `class` 或 `style` 自定义颜色、字号和字体
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* uni-icons 图标名称与 Unicode 字符映射表
|
||||
* @description 独立映射表,可在其它组件中直接 import 复用
|
||||
*/
|
||||
export const uniIconsMap = {
|
||||
'arrow-down': '\ue6be',
|
||||
'arrow-left': '\ue6bc',
|
||||
'arrow-right': '\ue6bb',
|
||||
'arrow-up': '\ue6bd',
|
||||
'auth': '\ue6ab',
|
||||
'auth-filled': '\ue6cc',
|
||||
'back': '\ue6b9',
|
||||
'bars': '\ue627',
|
||||
'calendar': '\ue6a0',
|
||||
'calendar-filled': '\ue6c0',
|
||||
'camera': '\ue65a',
|
||||
'camera-filled': '\ue658',
|
||||
'cart': '\ue631',
|
||||
'cart-filled': '\ue6d0',
|
||||
'chat': '\ue65d',
|
||||
'chat-filled': '\ue659',
|
||||
'chatboxes': '\ue696',
|
||||
'chatboxes-filled': '\ue692',
|
||||
'chatbubble': '\ue697',
|
||||
'chatbubble-filled': '\ue694',
|
||||
'checkbox': '\ue62b',
|
||||
'checkbox-filled': '\ue62c',
|
||||
'checkmarkempty': '\ue65c',
|
||||
'circle': '\ue65b',
|
||||
'circle-filled': '\ue65e',
|
||||
'clear': '\ue66d',
|
||||
'close': '\ue673',
|
||||
'closeempty': '\ue66c',
|
||||
'cloud-download': '\ue647',
|
||||
'cloud-download-filled': '\ue646',
|
||||
'cloud-upload': '\ue645',
|
||||
'cloud-upload-filled': '\ue648',
|
||||
'color': '\ue6cf',
|
||||
'color-filled': '\ue6c9',
|
||||
'compose': '\ue67f',
|
||||
'contact': '\ue693',
|
||||
'contact-filled': '\ue695',
|
||||
'down': '\ue6b8',
|
||||
'bottom': '\ue6b8',
|
||||
'download': '\ue68d',
|
||||
'download-filled': '\ue681',
|
||||
'email': '\ue69e',
|
||||
'email-filled': '\ue69a',
|
||||
'eye': '\ue651',
|
||||
'eye-filled': '\ue66a',
|
||||
'eye-slash': '\ue6b3',
|
||||
'eye-slash-filled': '\ue6b4',
|
||||
'fire': '\ue6a1',
|
||||
'fire-filled': '\ue6c5',
|
||||
'flag': '\ue65f',
|
||||
'flag-filled': '\ue660',
|
||||
'folder-add': '\ue6a9',
|
||||
'folder-add-filled': '\ue6c8',
|
||||
'font': '\ue6a3',
|
||||
'forward': '\ue6ba',
|
||||
'gear': '\ue664',
|
||||
'gear-filled': '\ue661',
|
||||
'gift': '\ue6a4',
|
||||
'gift-filled': '\ue6c4',
|
||||
'hand-down': '\ue63d',
|
||||
'hand-down-filled': '\ue63c',
|
||||
'hand-up': '\ue63f',
|
||||
'hand-up-filled': '\ue63e',
|
||||
'headphones': '\ue630',
|
||||
'heart': '\ue639',
|
||||
'heart-filled': '\ue641',
|
||||
'help': '\ue679',
|
||||
'help-filled': '\ue674',
|
||||
'home': '\ue662',
|
||||
'home-filled': '\ue663',
|
||||
'image': '\ue670',
|
||||
'image-filled': '\ue678',
|
||||
'images': '\ue650',
|
||||
'images-filled': '\ue64b',
|
||||
'info': '\ue669',
|
||||
'info-filled': '\ue649',
|
||||
'left': '\ue6b7',
|
||||
'link': '\ue6a5',
|
||||
'list': '\ue644',
|
||||
'location': '\ue6ae',
|
||||
'location-filled': '\ue6af',
|
||||
'locked': '\ue66b',
|
||||
'locked-filled': '\ue668',
|
||||
'loop': '\ue633',
|
||||
'mail-open': '\ue643',
|
||||
'mail-open-filled': '\ue63a',
|
||||
'map': '\ue667',
|
||||
'map-filled': '\ue666',
|
||||
'map-pin': '\ue6ad',
|
||||
'map-pin-ellipse': '\ue6ac',
|
||||
'medal': '\ue6a2',
|
||||
'medal-filled': '\ue6c3',
|
||||
'mic': '\ue671',
|
||||
'mic-filled': '\ue677',
|
||||
'micoff': '\ue67e',
|
||||
'micoff-filled': '\ue6b0',
|
||||
'minus': '\ue66f',
|
||||
'minus-filled': '\ue67d',
|
||||
'more': '\ue64d',
|
||||
'more-filled': '\ue64e',
|
||||
'navigate': '\ue66e',
|
||||
'navigate-filled': '\ue67a',
|
||||
'notification': '\ue6a6',
|
||||
'notification-filled': '\ue6c1',
|
||||
'paperclip': '\ue652',
|
||||
'paperplane': '\ue672',
|
||||
'paperplane-filled': '\ue675',
|
||||
'person': '\ue699',
|
||||
'person-filled': '\ue69d',
|
||||
'personadd': '\ue69f',
|
||||
'personadd-filled': '\ue698',
|
||||
'personadd-filled-copy': '\ue6d1',
|
||||
'phone': '\ue69c',
|
||||
'phone-filled': '\ue69b',
|
||||
'plus': '\ue676',
|
||||
'plus-filled': '\ue6c7',
|
||||
'plusempty': '\ue67b',
|
||||
'pulldown': '\ue632',
|
||||
'pyq': '\ue682',
|
||||
'qq': '\ue680',
|
||||
'redo': '\ue64a',
|
||||
'redo-filled': '\ue655',
|
||||
'refresh': '\ue657',
|
||||
'refresh-filled': '\ue656',
|
||||
'refreshempty': '\ue6bf',
|
||||
'reload': '\ue6b2',
|
||||
'right': '\ue6b5',
|
||||
'scan': '\ue62a',
|
||||
'search': '\ue654',
|
||||
'settings': '\ue653',
|
||||
'settings-filled': '\ue6ce',
|
||||
'shop': '\ue62f',
|
||||
'shop-filled': '\ue6cd',
|
||||
'smallcircle': '\ue67c',
|
||||
'smallcircle-filled': '\ue665',
|
||||
'sound': '\ue684',
|
||||
'sound-filled': '\ue686',
|
||||
'spinner-cycle': '\ue68a',
|
||||
'staff': '\ue6a7',
|
||||
'staff-filled': '\ue6cb',
|
||||
'star': '\ue688',
|
||||
'star-filled': '\ue68f',
|
||||
'starhalf': '\ue683',
|
||||
'trash': '\ue687',
|
||||
'trash-filled': '\ue685',
|
||||
'tune': '\ue6aa',
|
||||
'tune-filled': '\ue6ca',
|
||||
'undo': '\ue64f',
|
||||
'undo-filled': '\ue64c',
|
||||
'up': '\ue6b6',
|
||||
'top': '\ue6b6',
|
||||
'upload': '\ue690',
|
||||
'upload-filled': '\ue68e',
|
||||
'videocam': '\ue68c',
|
||||
'videocam-filled': '\ue689',
|
||||
'vip': '\ue6a8',
|
||||
'vip-filled': '\ue6c6',
|
||||
'wallet': '\ue6b1',
|
||||
'wallet-filled': '\ue6c2',
|
||||
'weibo': '\ue68b',
|
||||
'weixin': '\ue691',
|
||||
}
|
||||
|
||||
export type IconsType = keyof typeof uniIconsMap
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<text style="font-family: UniIconsFontFamily;">{{ unicode }}</text>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { uniIconsMap, IconsType } from './icons.uts'
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* @description 图标名称,取值范围见 icons.uts 导出的 IconsType,未传入或取值不存在时不显示图标
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
type? : IconsType
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const unicode = ref('')
|
||||
|
||||
const updateUnicode = () : void => {
|
||||
const type = props.type
|
||||
unicode.value = type != null ? uniIconsMap[type] : ''
|
||||
}
|
||||
|
||||
watch(() : IconsType | undefined => props.type, updateUnicode, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: UniIconsFontFamily;
|
||||
src: url('./uniicons.ttf');
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"id": "uni-icons-x",
|
||||
"displayName": "uni-icons 图标",
|
||||
"version": "1.0.0",
|
||||
"description": "适用于 uni-app x 的字体图标组件,可自定义颜色、大小和字体。",
|
||||
"keywords": [
|
||||
"uni-ui-x",
|
||||
"uni-app-x",
|
||||
"icon",
|
||||
"图标"
|
||||
],
|
||||
"repository": "https://github.com/dcloudio/uni-ui-x",
|
||||
"engines": {
|
||||
"HBuilderX": "^5.0",
|
||||
"uni-app-x": "^5.0",
|
||||
"uni-app": ""
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui-x",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "x",
|
||||
"vue3": "x"
|
||||
},
|
||||
"web": {
|
||||
"safari": "-",
|
||||
"chrome": "-"
|
||||
},
|
||||
"app": {
|
||||
"vue": "x",
|
||||
"nvue": "x",
|
||||
"android": "-",
|
||||
"ios": "-",
|
||||
"harmony": "-"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "-",
|
||||
"alipay": "-",
|
||||
"toutiao": "-",
|
||||
"baidu": "-",
|
||||
"kuaishou": "-",
|
||||
"jd": "-",
|
||||
"harmony": "-",
|
||||
"qq": "-",
|
||||
"lark": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "-",
|
||||
"union": "-"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": "√",
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
### [查看文档](https://doc.dcloud.net.cn/uni-app-x/component/uni-ui-x/uni-icons.html)
|
||||
@@ -0,0 +1,4 @@
|
||||
## 1.0.1(2026-04-30)
|
||||
- 更新文档
|
||||
## 1.0.0(2026-04-30)
|
||||
- 支持 uni-index-bar 组件
|
||||
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<!-- #ifdef MP-ALIPAY -->
|
||||
<view class="index-text" ref="indexTextRef" v-bind="$attrs" @touchstart="onTouchStart" @touchmove.stop.prevent="onTouchMove"
|
||||
@touchend="onTouchEnd">
|
||||
<text v-for="(item, index) in indexList" :key="item" class="index-item">{{ item }}</text>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-ALIPAY -->
|
||||
<text class="index-text" ref="indexTextRef" v-bind="$attrs" @touchstart="onTouchStart" @touchmove.stop.prevent="onTouchMove"
|
||||
@touchend="onTouchEnd">{{ indexs }}</text>
|
||||
<!-- #endif -->
|
||||
<!-- indicator with arrow -->
|
||||
<view v-show="touching" class="indicator" :class="indicatorViewClass" :style="indicatorStyle">
|
||||
<text class="indicator-text" :class="indicatorTextClass">{{ currentIndex }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 索引列表
|
||||
* @description 索引列表组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.08"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
defineOptions({
|
||||
externalClasses: ['indicator-view-class', 'indicator-text-class']
|
||||
})
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* @description 索引字符列表,使用换行符 \n 分隔,每行作为一个索引项
|
||||
* @default "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ"
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.08"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
indexs? : string,
|
||||
/**
|
||||
* @description 指示器容器的自定义样式类,触摸时显示当前索引的气泡
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.08"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
indicatorViewClass? : string.ClassString,
|
||||
/**
|
||||
* @description 指示器内文字的自定义样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.08"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
indicatorTextClass? : string.ClassString
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
indexs: 'A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ',
|
||||
indicatorViewClass: '',
|
||||
indicatorTextClass: ''
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* @description 选择索引时触发,参数为当前选择的索引字符,类型为 string
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.08"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
select: [index: string]
|
||||
}>()
|
||||
|
||||
// 将索引字符串按换行分割为数组
|
||||
const indexList = computed(() : string[] => props.indexs.split('\n'))
|
||||
|
||||
const currentIndex = ref('')
|
||||
const touching = ref(false)
|
||||
const indicatorTop = ref(0)
|
||||
|
||||
const indicatorStyle = computed(() : string => {
|
||||
return `top: ${indicatorTop.value}px;`
|
||||
})
|
||||
|
||||
// 布局缓存变量
|
||||
let textTop = 0
|
||||
let itemHeight = 0
|
||||
let indexCount = 0
|
||||
let cachedIndexList : string[] = []
|
||||
const navBarHeight = uni.getWindowInfo().windowTop
|
||||
|
||||
// #ifndef MP
|
||||
const indexTextRef = ref<UniTextElement | null>(null)
|
||||
// #endif
|
||||
|
||||
// 更新布局信息
|
||||
const updateLayout = () => {
|
||||
// 缓存索引数组和长度,避免在 touch 事件中重复访问 computed
|
||||
cachedIndexList = indexList.value
|
||||
indexCount = cachedIndexList.length
|
||||
|
||||
// #ifndef MP
|
||||
// App/Web 端使用 getBoundingClientRect 同步获取
|
||||
const textEl = indexTextRef.value
|
||||
if (textEl != null) {
|
||||
const rect = textEl.getBoundingClientRect()
|
||||
textTop = rect.top
|
||||
itemHeight = rect.height / indexCount
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
// #ifdef MP
|
||||
// 小程序需要在 setup 阶段保存 instance,因为异步回调中 getCurrentInstance() 返回 null
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
// 小程序端使用 createSelectorQuery 异步获取布局
|
||||
const updateLayoutAsync = () => {
|
||||
cachedIndexList = indexList.value
|
||||
indexCount = cachedIndexList.length
|
||||
|
||||
if (instance == null) return
|
||||
|
||||
uni.createSelectorQuery()
|
||||
.in(instance.proxy)
|
||||
.select('.index-text')
|
||||
.boundingClientRect((rect) => {
|
||||
if (rect != null) {
|
||||
textTop = rect.top as number
|
||||
itemHeight = (rect.height as number) / indexCount
|
||||
}
|
||||
})
|
||||
.exec()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
updateLayoutAsync()
|
||||
})
|
||||
})
|
||||
|
||||
watch(() => props.indexs, () => {
|
||||
nextTick(() => {
|
||||
updateLayoutAsync()
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
|
||||
const getIndexFromY = (y : number) : number => {
|
||||
const relY = y - textTop
|
||||
let idx = Math.floor(relY / itemHeight)
|
||||
if (idx < 0) idx = 0
|
||||
if (idx >= indexCount) idx = indexCount - 1
|
||||
return idx
|
||||
}
|
||||
|
||||
// 更新指示器位置
|
||||
const updateIndicator = (idx : number) => {
|
||||
indicatorTop.value = textTop + idx * itemHeight + itemHeight / 2 - 25 - navBarHeight
|
||||
}
|
||||
|
||||
const onTouchStart = (e : UniTouchEvent) => {
|
||||
updateLayout()
|
||||
touching.value = true
|
||||
const touch = e.touches[0]
|
||||
const idx = getIndexFromY(touch.clientY)
|
||||
const selectedIndex = cachedIndexList[idx]
|
||||
currentIndex.value = selectedIndex
|
||||
updateIndicator(idx)
|
||||
emit('select', selectedIndex)
|
||||
}
|
||||
|
||||
const onTouchMove = (e : UniTouchEvent) => {
|
||||
// #ifdef APP && UNI-APP-X
|
||||
e.preventDefault()
|
||||
// #endif
|
||||
// #ifdef WEB && UNI-APP-X
|
||||
if (e.cancelable) {
|
||||
e.preventDefault()
|
||||
}
|
||||
// #endif
|
||||
const touch = e.touches[0]
|
||||
const idx = getIndexFromY(touch.clientY)
|
||||
const newIndex = cachedIndexList[idx]
|
||||
if (newIndex != currentIndex.value) {
|
||||
currentIndex.value = newIndex
|
||||
updateIndicator(idx)
|
||||
emit('select', newIndex)
|
||||
}
|
||||
}
|
||||
|
||||
const onTouchEnd = (_e : UniTouchEvent) => {
|
||||
touching.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.index-text {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 28px;
|
||||
font-size: 11px;
|
||||
color: #757575;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.index-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
right: 50px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: #c9c9c9;
|
||||
border-radius: 25px 25px 25px 0;
|
||||
transform: rotate(225deg);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.indicator-text {
|
||||
font-size: 22px;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
transform: rotate(-225deg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"id": "uni-index-bar",
|
||||
"displayName": "uni-index-bar 索引栏",
|
||||
"version": "1.0.1",
|
||||
"description": "uni-index-bar 索引栏组件,用于显示索引栏,支持自定义索引栏的样式和内容。",
|
||||
"isExt": true,
|
||||
"extUrl": "https://ext.dcloud.net.cn/plugin?id=27847",
|
||||
"keywords": [
|
||||
"uni-index-bar",
|
||||
"索引栏",
|
||||
"index-bar",
|
||||
"uni-app-x",
|
||||
"组件"
|
||||
],
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"engines": {
|
||||
"HBuilderX": "^5.08",
|
||||
"uni-app-x": "^5.08",
|
||||
"uni-app": ""
|
||||
},
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x",
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": "√",
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√",
|
||||
"alipay": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
### 介绍
|
||||
|
||||
本组件以竖排方式显示字母索引,常悬浮或固定于列表右侧,用户通过点击或滑动索引条上的字母索引,触发 `select` 事件。在该事件中通过代码驱动指定的列表滚动到该字母索引所在的位置。
|
||||
|
||||
本组件为绝对定位,默认垂直居中于父容器右侧。
|
||||
|
||||
本组件不包括 list-view 组件,需要配合 list-view 使用。list-view 中需要有对应索引的 list-item,且这些 list-item 需要有 id 属性,可以通过 `scroll-into-view` 属性或 `scrollTop` 属性跳转。
|
||||
|
||||
本组件包括一个指示器(indicator)子组件。当用户手指触摸在索引条上的某个字母时,在触摸的字母左边悬浮显示一个水滴形气泡,箭头指向当前触摸的字母。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```html
|
||||
<view style="flex: 1; position: relative;">
|
||||
<list-view ref="listRef" style="flex: 1;">
|
||||
<!-- 列表内容 -->
|
||||
</list-view>
|
||||
<uni-index-bar @select="onSelect"></uni-index-bar>
|
||||
</view>
|
||||
```
|
||||
|
||||
```ts
|
||||
const onSelect = (index: string) => {
|
||||
// 处理索引选择,滚动列表到对应位置
|
||||
}
|
||||
```
|
||||
|
||||
### 事件
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
|--------|------|----------|
|
||||
| select | 选择索引时触发 | index: string - 当前选择的索引字符 |
|
||||
|
||||
### 外部样式类
|
||||
|
||||
| 类名 | 说明 |
|
||||
|------|------|
|
||||
| indicator-view-class | 指示器容器样式,可自定义气泡背景色等 |
|
||||
| indicator-text-class | 指示器文字样式,可自定义文字颜色等 |
|
||||
|
||||
### 示例
|
||||
|
||||
#### 自定义索引
|
||||
|
||||
```html
|
||||
<uni-index-bar :indexs="'☆\nA\nB\nC\n#'" @select="onSelect"></uni-index-bar>
|
||||
```
|
||||
|
||||
#### 自定义样式
|
||||
|
||||
```html
|
||||
<uni-index-bar
|
||||
indicator-view-class="custom-indicator"
|
||||
indicator-text-class="custom-indicator-text"
|
||||
style="color: aqua;"
|
||||
@select="onSelect">
|
||||
</uni-index-bar>
|
||||
```
|
||||
|
||||
```css
|
||||
.custom-indicator {
|
||||
background-color: #007aff;
|
||||
}
|
||||
.custom-indicator-text {
|
||||
color: #ffff00;
|
||||
}
|
||||
```
|
||||
|
||||
#### 配合 list-view 使用
|
||||
|
||||
```html
|
||||
<view style="flex: 1; position: relative;">
|
||||
<list-view ref="listRef" style="flex: 1;" :scroll-into-view="indexViewID">
|
||||
<template v-for="group in cityGroups" :key="group.index">
|
||||
<list-item :id="'idx-' + group.index" type="header">
|
||||
<view class="group-header">
|
||||
<text>{{ group.index }}</text>
|
||||
</view>
|
||||
</list-item>
|
||||
<list-item v-for="city in group.cities" :key="city.id" type="city">
|
||||
<view class="city-item">
|
||||
<text>{{ city.name }}</text>
|
||||
</view>
|
||||
</list-item>
|
||||
</template>
|
||||
</list-view>
|
||||
<uni-index-bar :indexs="indexList" @select="onSelect"></uni-index-bar>
|
||||
</view>
|
||||
```
|
||||
|
||||
```ts
|
||||
const indexList = cityGroups.map((g): string => g.index).join('\n')
|
||||
const indexViewID = ref("")
|
||||
|
||||
const onSelect = (index: string) => {
|
||||
indexViewID.value = 'idx-' + index
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
## 1.0.0(2026-05-07)
|
||||
- 支持 uni-link 组件
|
||||
@@ -0,0 +1,362 @@
|
||||
<template>
|
||||
<text style="text-decoration-line: underline" @click="openURL">
|
||||
<slot></slot>
|
||||
</text>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 链接
|
||||
* @description 链接组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
// #ifdef APP
|
||||
import { openSchema } from '@/uni_modules/uts-openSchema'
|
||||
// #endif
|
||||
|
||||
defineOptions({
|
||||
name: 'UniLink',
|
||||
styleIsolation: 'app'
|
||||
})
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* @description 链接地址,点击组件时要打开的 URL
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
href?: string
|
||||
/**
|
||||
* @description 打开方式。"_self" 在当前应用内置 webview 中打开;"_blank" 在外部浏览器中打开(小程序端会复制链接提示用户外部打开)
|
||||
* @default "_self"
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
target?: '_blank' | '_self'
|
||||
/**
|
||||
* @description `target="_blank"`失败时,可以通过本属性控制失败回退策略。"turn-self"使用_self方式打开;"copy-link-modal"复制链接并弹框提醒;"none"不回退,触发失败回调
|
||||
* @default "turn-self"
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
blankFallback: 'turn-self' | 'copy-link-modal' | 'none'
|
||||
}
|
||||
|
||||
type LocaleType = 'zh-Hans' | 'zh-Hant' | 'en' | 'fr' | 'la'
|
||||
|
||||
type LocaleMessages = {
|
||||
emptyHref: string
|
||||
popupBlocked: string
|
||||
internalOpenFailed: string
|
||||
copiedOpenInBrowserPrefix: string
|
||||
copyFailedOpenInBrowser: string
|
||||
}
|
||||
|
||||
const LINK_WEBVIEW_PAGE_PATH = '/uni_modules/uni-link-x/pages/link-webview'
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
href: '',
|
||||
target: '_self',
|
||||
blankFallback: 'turn-self'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
error: [message: string]
|
||||
}>()
|
||||
|
||||
const appLanguage = (uni.getAppBaseInfo().appLanguage ?? 'en').toLowerCase()
|
||||
|
||||
function resolveLocaleType() : LocaleType {
|
||||
if (
|
||||
appLanguage.indexOf('zh-hant') == 0 ||
|
||||
appLanguage.indexOf('zh-tw') == 0 ||
|
||||
appLanguage.indexOf('zh-hk') == 0 ||
|
||||
appLanguage.indexOf('zh-mo') == 0
|
||||
) {
|
||||
return 'zh-Hant'
|
||||
}
|
||||
if (appLanguage.indexOf('zh') == 0) {
|
||||
return 'zh-Hans'
|
||||
}
|
||||
if (appLanguage.indexOf('fr') == 0) {
|
||||
return 'fr'
|
||||
}
|
||||
if (appLanguage.indexOf('la') == 0 || appLanguage.indexOf('lat') == 0) {
|
||||
return 'la'
|
||||
}
|
||||
return 'en'
|
||||
}
|
||||
|
||||
function getLocaleMessages() : LocaleMessages {
|
||||
const localeType = resolveLocaleType()
|
||||
|
||||
if (localeType == 'zh-Hans') {
|
||||
return {
|
||||
emptyHref: '链接地址不能为空',
|
||||
popupBlocked: '浏览器拦截了新窗口',
|
||||
internalOpenFailed: '应用中未找到uni-link组件下的link-webview页面',
|
||||
copiedOpenInBrowserPrefix: '已复制链接,请使用外部浏览器打开:',
|
||||
copyFailedOpenInBrowser: '复制链接失败,请手动复制链接并使用外部浏览器打开'
|
||||
}
|
||||
}
|
||||
|
||||
if (localeType == 'zh-Hant') {
|
||||
return {
|
||||
emptyHref: '連結地址不能為空',
|
||||
popupBlocked: '瀏覽器封鎖了新視窗',
|
||||
internalOpenFailed: '應用中未找到 uni-link 組件下的 link-webview 頁面',
|
||||
copiedOpenInBrowserPrefix: '已複製連結,請使用外部瀏覽器開啟:',
|
||||
copyFailedOpenInBrowser: '複製連結失敗,請手動複製連結並使用外部瀏覽器開啟'
|
||||
}
|
||||
}
|
||||
|
||||
if (localeType == 'fr') {
|
||||
return {
|
||||
emptyHref: 'L’URL ne peut pas être vide',
|
||||
popupBlocked: 'Le navigateur a bloqué la nouvelle fenêtre',
|
||||
internalOpenFailed: 'Échec de l\'ouverture de la page @uni_modules/uni-link-x/pages/link-webview',
|
||||
copiedOpenInBrowserPrefix: 'Le lien a été copié. Veuillez l’ouvrir dans un navigateur externe :',
|
||||
copyFailedOpenInBrowser: 'Échec de la copie du lien. Veuillez le copier manuellement et l’ouvrir dans un navigateur externe.'
|
||||
}
|
||||
}
|
||||
|
||||
if (localeType == 'la') {
|
||||
return {
|
||||
emptyHref: 'Inscriptio nexus vacua esse non potest',
|
||||
popupBlocked: 'Navigator fenestram novam prohibuit',
|
||||
internalOpenFailed: 'Pagina aperiri non potuit @uni_modules/uni-link-x/pages/link-webview',
|
||||
copiedOpenInBrowserPrefix: 'Nexus descriptus est. Quaeso eum in navigatro externo aperi:',
|
||||
copyFailedOpenInBrowser: 'Descriptio nexus defecit. Quaeso nexum manu describe et in navigatro externo aperi.'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
emptyHref: 'The link URL cannot be empty',
|
||||
popupBlocked: 'The browser blocked the new window',
|
||||
internalOpenFailed: 'Failed to open page @uni_modules/uni-link-x/pages/link-webview',
|
||||
copiedOpenInBrowserPrefix: 'The link has been copied. Please open it in an external browser:',
|
||||
copyFailedOpenInBrowser: 'Failed to copy the link. Please copy it manually and open it in an external browser.'
|
||||
}
|
||||
}
|
||||
|
||||
function showPrompt(content : string) {
|
||||
uni.showModal({
|
||||
content,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
function copyLinkAndPrompt(url : string) {
|
||||
const messages = getLocaleMessages()
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
showPrompt(messages.copiedOpenInBrowserPrefix + '\n' + url)
|
||||
},
|
||||
fail: () => {
|
||||
showPrompt(messages.copyFailedOpenInBrowser)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function reportOpenError(message : string) {
|
||||
emit('error', message)
|
||||
}
|
||||
|
||||
function openInBuiltInWebview(url : string) {
|
||||
uni.navigateTo({
|
||||
url: `${LINK_WEBVIEW_PAGE_PATH}?url=${encodeURIComponent(url)}`,
|
||||
success: () => {},
|
||||
fail: () => {
|
||||
// 异常情况下可能pages.json中漏配了本组件下的link-webview.uvue页面
|
||||
reportOpenError(getLocaleMessages().internalOpenFailed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openURL() {
|
||||
const url = props.href.trim()
|
||||
|
||||
if (url.length == 0) {
|
||||
reportOpenError(getLocaleMessages().emptyHref)
|
||||
return
|
||||
}
|
||||
|
||||
if (props.target == '_self') {
|
||||
openInBuiltInWebview(url)
|
||||
}
|
||||
else{
|
||||
// _blank方式
|
||||
// #ifdef WEB
|
||||
const targetWindow = window.open(url, '_blank')
|
||||
if (targetWindow != null) {
|
||||
return
|
||||
}
|
||||
else if (props.blankFallback=="turn-self"){
|
||||
openInBuiltInWebview(url)
|
||||
return
|
||||
}
|
||||
else if(props.blankFallback=="copy-link-modal"){
|
||||
copyLinkAndPrompt(url)
|
||||
return
|
||||
}
|
||||
reportOpenError(getLocaleMessages().popupBlocked)
|
||||
// #endif
|
||||
|
||||
// #ifdef MP
|
||||
if (props.blankFallback=="turn-self"){
|
||||
openInBuiltInWebview(url)
|
||||
}
|
||||
else if(props.blankFallback=="copy-link-modal"){
|
||||
copyLinkAndPrompt(url)
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef APP
|
||||
try {
|
||||
openSchema(url)
|
||||
} catch (error) {
|
||||
// 这里常见的情况是iOS设备在真机运行时缺少xcode环境编译openSchema失败
|
||||
if (props.blankFallback=="turn-self"){
|
||||
openInBuiltInWebview(url)
|
||||
}
|
||||
else if(props.blankFallback=="copy-link-modal"){
|
||||
copyLinkAndPrompt(url)
|
||||
}
|
||||
else{
|
||||
reportOpenError((error as Error).message) // 报错后发送错误事件,这里的错误原因未知,无法自行提前国际化
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"id": "uni-link-x",
|
||||
"displayName": "uni-link 链接",
|
||||
"version": "1.0.0",
|
||||
"description": "Link 链接组件,可自定义链接的打开方式。",
|
||||
"isExt": true,
|
||||
"extUrl": "https://ext.dcloud.net.cn/plugin?id=27898",
|
||||
"keywords": [
|
||||
"uni-ui",
|
||||
"uniui",
|
||||
"link",
|
||||
"链接",
|
||||
"uni-app-x"
|
||||
],
|
||||
"engines": {
|
||||
"HBuilderX": "",
|
||||
"uni-app": "",
|
||||
"uni-app-x": "^5.07"
|
||||
},
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"type": "component-vue",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x",
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"uts-openSchema"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": "√",
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<web-view style="flex: 1;" :src="currentUrl" @error="onWebViewError"></web-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const currentUrl = ref<string>('')
|
||||
|
||||
onLoad((options : UTSJSONObject | null) => {
|
||||
if (options == null) {
|
||||
return
|
||||
}
|
||||
const rawUrl = options['url']
|
||||
if (rawUrl == null) {
|
||||
return
|
||||
}
|
||||
const nextUrl = rawUrl as string
|
||||
currentUrl.value = decodeURIComponent(nextUrl) ?? nextUrl
|
||||
})
|
||||
|
||||
function onWebViewError(event : UniWebViewErrorEvent) {
|
||||
const detail = event.detail
|
||||
const errMsg = detail.errMsg ?? ''
|
||||
uni.showModal({
|
||||
content: `Error: ${errMsg}`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
打开web链接组件。点击后通过内置或外部浏览器打开链接,加载 `href` 属性中配置的 URL。
|
||||
|
||||
- `target="_self"` 时,使用 `uni_modules/uni-link-x/pages/link-webview` 页面内的 `web-view` 打开链接。
|
||||
* 注意小程序需要将域名加入应用的域名白名单。
|
||||
- `target="_blank"` 时,
|
||||
* APP平台调用手机浏览器打开链接
|
||||
* Web调用 `window.open(url, '_blank')`
|
||||
* 小程序平台无法打开手机浏览器,此时会走到失败回退逻辑中
|
||||
|
||||
`target="_blank"`失败时,可以通过组件属性`blank-fallback`来控制失败后的回退处理策略,`blank-fallback`属性有3个值:
|
||||
|
||||
1. turn-self :使用_self方式打开。此值为默认值。也就是 `target="_blank"` 时,小程序下默认转到内置浏览器打开。
|
||||
2. copy-link-modal :复制链接并弹框提醒。会复制URL到剪贴板中,并弹框提示用户在外部浏览器中打开。
|
||||
3. none :不回退。会触发组件的error事件。
|
||||
|
||||
- 开发者也可以通过条件编译组合使用,比如APP平台使用`_black`,小程序平台使用`_self`
|
||||
|
||||
常见失败原因:
|
||||
- 安装本组件时未在项目的pages.json中注册组件的内置页面。(HBuilderX导入本组件到项目下时会弹框提醒注册内置页面)
|
||||
- 小程序平台未将域名加入应用的域名白名单。
|
||||
- iOS运行基座未包括openSchema插件,仅在无xcode时运行会触发。打包其实不会发生此错误。
|
||||
- app平台,有些手机系统会弹框询问用户是否允许app打开浏览器,如果用户点了拒绝,这种情况没有失败回调。但有种判断方式是app的onHide生命周期是否触发,如果没有触发,那么意味着并没有启动系统浏览器。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```html
|
||||
<uni-link href="https://uniapp.dcloud.net.cn">uni-app x 文档
|
||||
</uni-link>
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
| --- | --- | --- |
|
||||
| error | 打开失败时触发 | `message: string` |
|
||||
|
||||
### 依赖
|
||||
|
||||
`APP` 平台依赖 `uts-openSchema`。`uni-link` 已在 `uni_modules/uni-link-x/package.json` 中声明该依赖。
|
||||
|
||||
组件目录下有 `pages_init.json`,会向应用的 pages.json 中注册组件中自带的内置浏览器页面。如在HBuilderX中弹框询问是否将组件中的页面注册到项目pages.json中时,请选择同意。
|
||||
|
||||
### 示例页面
|
||||
|
||||
`/pages/uni-ui/link/link`
|
||||
@@ -0,0 +1,4 @@
|
||||
## 1.0.1(2026-05-20)
|
||||
- 纠正类名拼写错误
|
||||
## 1.0.0(2026-05-07)
|
||||
- 支持 uni-nav-bar 组件
|
||||
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<view style="flex-direction: row;padding-top: var(--status-bar-height);box-sizing: content-box;align-items: center;position: relative;">
|
||||
<view style="height: 44px;align-items: center;justify-content: center;
|
||||
<!-- #ifdef WEB -->
|
||||
cursor: pointer
|
||||
<!-- #endif -->
|
||||
" class="uni-left-class-builtin" :class="leftClass">
|
||||
<!-- 旋转边框的view实现返回箭头。因需要扩大点击区域,外面多套了一层 -->
|
||||
<view v-if="!hideDefaultBack && slots['left']==null" style="width: 44px;height: 44px;justify-content: center;align-items: center;" @click="back">
|
||||
<view style="width: 12px;height: 12px;transform: rotate(45deg);
|
||||
border-left: 2px solid; border-bottom: 2px solid;" :style="backIconStyle"></view>
|
||||
</view>
|
||||
<slot name="left" v-else></slot>
|
||||
</view>
|
||||
|
||||
<view style="position: absolute; height: 44px; flex-direction: row; align-items: center;" class="uni-mid-class-builtin" :class="midClass" flatten>
|
||||
<text v-if="slots['mid']==null" :style="titleStyle" flatten> {{ title }} </text>
|
||||
<slot name="mid" v-else></slot>
|
||||
</view>
|
||||
<view style="position: absolute; height: 44px; flex-direction: row; align-items: center;right: 0;" class="uni-right-class-builtin" :class="rightClass" flatten>
|
||||
<slot name="right"></slot>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 自定义导航栏
|
||||
* @description 自定义导航栏组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
defineOptions({
|
||||
// inheritAttrs: false,
|
||||
externalClasses: ['left-class','mid-class','right-class']
|
||||
})
|
||||
const props = defineProps({
|
||||
/**
|
||||
* @description 是否隐藏默认的返回箭头。为 true 时需要通过 left 插槽自定义返回按钮
|
||||
* @default false
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
hideDefaultBack: { type: Boolean, default: false },
|
||||
/**
|
||||
* @description 导航栏中间显示的标题文字,若使用 mid 插槽则该属性无效
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
title: { type: String, default: "" },
|
||||
// navigationBarTextStyle: { type: "#000000" | "#ffffff" | "#000" | "#fff" | "black" | "white" | "", default: "" },
|
||||
/**
|
||||
* @description 导航栏前景色(文字和返回箭头颜色)。非小程序端未传入时会自动读取 pageStyle 的 navigationBarTextStyle
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
navigationBarTextStyle: { type: String as PropType<"white" | "black" | "">, default: "" },
|
||||
/**
|
||||
* @description 左侧区域的自定义样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
leftClass: { type: String, default: "" },
|
||||
/**
|
||||
* @description 中间区域的自定义样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
midClass: { type: String, default: "" },
|
||||
/**
|
||||
* @description 右侧区域的自定义样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
rightClass: { type: String, default: "" }
|
||||
// stat: { type: Boolean, default: false }
|
||||
})
|
||||
const slots = useSlots();
|
||||
const backIconStyle = computed(() : string => {
|
||||
return props.navigationBarTextStyle == "" ? "" : `border-left-color: ${props.navigationBarTextStyle}; border-bottom-color: ${props.navigationBarTextStyle};`;
|
||||
})
|
||||
const titleStyle = computed(() : string => {
|
||||
return props.navigationBarTextStyle == "" ? "" : `color: ${props.navigationBarTextStyle};`;
|
||||
})
|
||||
|
||||
// #ifndef MP
|
||||
// 获取pageStyle在web和app上都没有兼容theme.json的场景,这段代码注释掉。需要先修改底层bug。本组件使用了其他方案
|
||||
/* const currentPage = getCurrentInstance()?.proxy?.$page
|
||||
console.log("props.navigationBarTextStyle:",props.navigationBarTextStyle);
|
||||
console.log("navigationBarTextStyle:",currentPage?.getPageStyle()["navigationBarTextStyle"]);
|
||||
console.log("backgroundColorContent:",currentPage?.getPageStyle()["backgroundColorContent"]);
|
||||
const foreColor = (props.navigationBarTextStyle=="") ? currentPage?.getPageStyle()["navigationBarTextStyle"] : props.navigationBarTextStyle; //如果外部没有传入前景色,从pageStyle获取
|
||||
console.log("foreColor:",foreColor); */
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
// const foreColor = props.navigationBarTextStyle; //小程序端无法获取pageStyle,需要显式传入前景色
|
||||
// #endif
|
||||
|
||||
/* const foreColor = computed(() : string => {
|
||||
return props.navigationBarTextStyle;
|
||||
}) */
|
||||
|
||||
// let statusBarHeight = ref(0)
|
||||
onMounted(() => {
|
||||
// #ifdef MP-WEIXIN
|
||||
// TODO 部分小程序平台不支持getWindowInfo
|
||||
// statusBarHeight.value = uni.getWindowInfo().statusBarHeight
|
||||
// #endif
|
||||
|
||||
/* if (props.stat && props.title != '') {
|
||||
uni.report({
|
||||
name: 'title',
|
||||
options: props.title
|
||||
})
|
||||
} */
|
||||
})
|
||||
|
||||
const back = () => {
|
||||
uni.navigateBack({});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.uni-left-class-builtin {
|
||||
width: 44px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.uni-mid-class-builtin {
|
||||
justify-content: center;
|
||||
left: 52px; /* padding 6 + 44 = 52*/
|
||||
right: 52px;
|
||||
}
|
||||
.uni-right-class-builtin {
|
||||
width: 44px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"id": "uni-nav-bar-x",
|
||||
"displayName": "uni-nav-bar",
|
||||
"version": "1.0.1",
|
||||
"description": "uni-nav-bar",
|
||||
"isExt": true,
|
||||
"extUrl": "https://ext.dcloud.net.cn/plugin?id=27897",
|
||||
"keywords": [
|
||||
"uni-nav-bar",
|
||||
"uni-app-x",
|
||||
"导航栏",
|
||||
"组件"
|
||||
],
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"engines": {
|
||||
"HBuilderX": "^5.0",
|
||||
"uni-app-x": "^5.07",
|
||||
"uni-app": ""
|
||||
},
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x",
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": "√",
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
自定义导航栏组件
|
||||
|
||||
在页面pages.json中关闭原生导航栏后,即使用`"style": {"navigationStyle": "custom"}`,可以使用本组件实现自定义导航栏。
|
||||
|
||||
同时注意在pages.json中配置页面style增加属性`disableScroll:true`,即禁止页面滚动。此时只有uni-nav-bar组件的下方才可以滚动。否则页面的回弹bounce效果会把自定义导航栏也拉下来。
|
||||
|
||||
本组件自动适配顶部安全区。用padding-top让出顶部状态栏的高度。除去状态栏高度后,本组件的高度为44px。
|
||||
|
||||
本组件左右两边默认各让出了6px的边距。也可以在left-class和right-class中自定义边距。
|
||||
|
||||
本组件分为left、mid、right 3个区域。
|
||||
|
||||
- left区域默认显示一个返回箭头,大小为44*44px。可以通过属性hideDefaultBack来隐藏,也可以传入一个slot name='left'来替代。可以通过left-class来修饰样式。
|
||||
- mid区域默认显示title属性设置的标题。也可以传入一个slot name='mid'来替代。其默认宽度为屏幕宽度-两边默认边距-left区域默认宽度-right区域默认宽度。可以通过mid-class来修饰样式。
|
||||
- right区域默认不显示内容,可以传入一个slot name='right'来显示自定义内容。right区域默认width也是44px,可以通过right-class来修饰样式。
|
||||
|
||||
支持属性:
|
||||
- hideDefaultBack: 隐藏返回箭头
|
||||
- title: 通过属性方便设置标题。如果传入mid slot,则不生效
|
||||
- navigationBarTextStyle: 返回箭头和属性设置的标题,它们的颜色均由该属性控制,可选 white|black
|
||||
|
||||
本组件默认没有背景色,即透明,会透显页面的背景色,开发者可通过组件的class自行设置背景色。
|
||||
|
||||
如果遇到顶部安全区避让失效的问题,可升级到 HBuilderX 5.25 版本。
|
||||
|
||||
在小程序端,如果需要规避右上角胶囊按钮,可以参考下方代码设置 margin-right 让出胶囊按钮的宽度和右侧间距。
|
||||
|
||||
```html
|
||||
<template>
|
||||
<uni-nav-bar title="标题" right-class="nav-right">
|
||||
<template #right>
|
||||
<view :style="{ transform: 'translateX(-' + rightMargin + 'px)' }">
|
||||
<text class="txt-button">right</text>
|
||||
</view>
|
||||
</template>
|
||||
</uni-nav-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const rightMargin = ref(0)
|
||||
onMounted(() => {
|
||||
// #ifdef MP-WEIXIN
|
||||
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
// 预留胶囊按钮宽度和右侧间距,让右侧插槽显示在胶囊按钮左边
|
||||
rightMargin.value = windowInfo.windowWidth - menuButtonInfo.right + menuButtonInfo.width
|
||||
// #endif
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.nav-right {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.txt-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
## 1.0.1(2026-05-07)
|
||||
- 移除无用依赖
|
||||
## 1.0.0(2026-05-07)
|
||||
- 支持 uni-number-box 组件
|
||||
@@ -0,0 +1,802 @@
|
||||
<template>
|
||||
<view class="uni-numbox">
|
||||
<view
|
||||
class="uni-numbox__btn uni-numbox__minus"
|
||||
:class="btnClass"
|
||||
<!-- #ifdef WEB -->
|
||||
@pointerdown="onMinus"
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef WEB -->
|
||||
@click="onMinus"
|
||||
<!-- #endif -->
|
||||
>
|
||||
<!-- Chrome 133版本上有点击响应慢的bug,为规避bug在条件编译中使用 @pointerdown-->
|
||||
<view class="uni-numbox__icon-line" :class="[iconClass, minusDisabled ? 'uni-numbox__icon-line--disabled' : '']"></view>
|
||||
</view>
|
||||
<input
|
||||
class="uni-numbox__input"
|
||||
:class="inputClass"
|
||||
:disabled="disabled"
|
||||
:type="step < 1 ? 'digit' : 'number'"
|
||||
v-model="inputValue"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
<view
|
||||
class="uni-numbox__btn uni-numbox__plus"
|
||||
:class="btnClass"
|
||||
<!-- #ifdef WEB -->
|
||||
@pointerdown="onPlus"
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef WEB -->
|
||||
@click="onPlus"
|
||||
<!-- #endif -->
|
||||
>
|
||||
<view class="uni-numbox__icon-line" :class="[iconClass, plusDisabled ? 'uni-numbox__icon-line--disabled' : '']"></view>
|
||||
<view class="uni-numbox__icon-line uni-numbox__icon-line--vertical" :class="[iconClass, plusDisabled ? 'uni-numbox__icon-line--disabled' : '']"></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* NumberBox 数字输入框
|
||||
* @description 带加减按钮的数字输入框,常见于购物车商品数量选择
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
* @property {Number} value 输入框当前值,默认值 1
|
||||
* @property {Number} modelValue v-model 绑定值,默认值 1
|
||||
* @property {Number} min 最小值,默认值 0
|
||||
* @property {Number} max 最大值,默认值 100
|
||||
* @property {Number} step 步长,每次点击改变的间隔大小,默认值 1,支持小数
|
||||
* @property {Boolean} disabled 是否禁用,默认值 false
|
||||
* @property {String} btnClass 按钮自定义样式类
|
||||
* @property {String} inputClass 输入框自定义样式类
|
||||
* @property {String} iconClass 图标自定义样式类
|
||||
* @event {Function} change 值改变时触发,参数为当前值
|
||||
* @event {Function} focus 输入框聚焦时触发
|
||||
* @event {Function} blur 输入框失焦时触发
|
||||
*/
|
||||
|
||||
defineOptions({
|
||||
externalClasses: ['btn-class', 'input-class', 'icon-class']
|
||||
})
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* @description 输入框当前值,不使用 v-model 时通过该属性传入初始值
|
||||
* @default 1
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
value?: number,
|
||||
/**
|
||||
* @description v-model 绑定值,支持双向绑定
|
||||
* @default 1
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
modelValue?: number,
|
||||
/**
|
||||
* @description 允许输入的最小值
|
||||
* @default 0
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
min?: number,
|
||||
/**
|
||||
* @description 允许输入的最大值
|
||||
* @default 100
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
max?: number,
|
||||
/**
|
||||
* @description 每次点击加/减按钮改变的步长,支持小数
|
||||
* @default 1
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
step?: number,
|
||||
/**
|
||||
* @description 是否禁用组件,禁用时输入框和加减按钮都不可操作
|
||||
* @default false
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
disabled?: boolean,
|
||||
/**
|
||||
* @description 加减按钮的自定义样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
btnClass?: string.ClassString,
|
||||
/**
|
||||
* @description 输入框的自定义样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
inputClass?: string.ClassString,
|
||||
/**
|
||||
* @description 加减按钮内部图标的自定义样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
iconClass?: string.ClassString
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
value: 1,
|
||||
modelValue: 1,
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
disabled: false,
|
||||
btnClass: '',
|
||||
inputClass: '',
|
||||
iconClass: ''
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* @description 值改变时触发,参数为当前值,类型为 number
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
change: [value: number]
|
||||
/**
|
||||
* @description v-model 绑定值变化事件,参数为当前值,类型为 number
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
'update:modelValue': [value: number]
|
||||
/**
|
||||
* @description 输入框失焦时触发,参数为事件对象,类型为 UniInputBlurEvent
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
blur: [event: UniInputBlurEvent]
|
||||
/**
|
||||
* @description 输入框聚焦时触发,参数为事件对象,类型为 UniInputFocusEvent
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
focus: [event: UniInputFocusEvent]
|
||||
}>()
|
||||
|
||||
// 内部输入值
|
||||
const inputValue = ref<string>('')
|
||||
|
||||
// 获取小数精度
|
||||
const getDecimalScale = () : number => {
|
||||
let scale = 1
|
||||
// 浮点型
|
||||
if (Math.floor(props.step) != props.step) {
|
||||
const stepStr = props.step.toString()
|
||||
const dotIndex = stepStr.indexOf('.')
|
||||
if (dotIndex != -1) {
|
||||
const decimalPart = stepStr.substring(dotIndex + 1)
|
||||
scale = Math.pow(10, decimalPart.length) as number
|
||||
}
|
||||
}
|
||||
return scale
|
||||
}
|
||||
|
||||
// 格式化数值
|
||||
const formatValue = (value : number) : string => {
|
||||
const scale = getDecimalScale()
|
||||
const scaleStr = scale.toString()
|
||||
const precision = scaleStr.length - 1
|
||||
return value.toFixed(precision)
|
||||
}
|
||||
|
||||
// 发射变更事件
|
||||
const emitChange = (value : number) : void => {
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
}
|
||||
|
||||
// 初始化值
|
||||
const initValue = () : void => {
|
||||
let val : number = props.value
|
||||
if (props.modelValue != 1) {
|
||||
val = props.modelValue
|
||||
}
|
||||
inputValue.value = formatValue(val)
|
||||
}
|
||||
|
||||
// 判断减号是否禁用
|
||||
const minusDisabled = computed(() : boolean => {
|
||||
const currentVal = parseFloat(inputValue.value)
|
||||
if (isNaN(currentVal)) return true
|
||||
return currentVal <= props.min || props.disabled
|
||||
})
|
||||
|
||||
// 判断加号是否禁用
|
||||
const plusDisabled = computed(() : boolean => {
|
||||
const currentVal = parseFloat(inputValue.value)
|
||||
if (isNaN(currentVal)) return true
|
||||
return currentVal >= props.max || props.disabled
|
||||
})
|
||||
|
||||
// 点击减号
|
||||
const onMinus = () : void => {
|
||||
if (props.disabled) return
|
||||
|
||||
const scale = getDecimalScale()
|
||||
let currentVal = parseFloat(inputValue.value)
|
||||
if (isNaN(currentVal)) {
|
||||
currentVal = props.min
|
||||
}
|
||||
|
||||
let value = currentVal * scale
|
||||
const stepScaled = props.step * scale
|
||||
|
||||
value -= stepScaled
|
||||
|
||||
if (value < props.min * scale) return
|
||||
if (value > props.max * scale) {
|
||||
value = props.max * scale
|
||||
}
|
||||
|
||||
const newValue = value / scale
|
||||
inputValue.value = formatValue(newValue)
|
||||
emitChange(newValue)
|
||||
}
|
||||
|
||||
// 点击加号
|
||||
const onPlus = () : void => {
|
||||
if (props.disabled) return
|
||||
|
||||
const scale = getDecimalScale()
|
||||
let currentVal = parseFloat(inputValue.value)
|
||||
if (isNaN(currentVal)) {
|
||||
currentVal = props.min
|
||||
}
|
||||
|
||||
let value = currentVal * scale
|
||||
const stepScaled = props.step * scale
|
||||
|
||||
value += stepScaled
|
||||
|
||||
if (value > props.max * scale) return
|
||||
if (value < props.min * scale) {
|
||||
value = props.min * scale
|
||||
}
|
||||
|
||||
const newValue = value / scale
|
||||
inputValue.value = formatValue(newValue)
|
||||
emitChange(newValue)
|
||||
}
|
||||
|
||||
// 失焦事件
|
||||
const onBlur = (event : UniInputBlurEvent) : void => {
|
||||
emit('blur', event)
|
||||
|
||||
let value = parseFloat(event.detail.value)
|
||||
if (isNaN(value)) {
|
||||
inputValue.value = formatValue(props.value)
|
||||
return
|
||||
}
|
||||
|
||||
if (value > props.max) {
|
||||
value = props.max
|
||||
} else if (value < props.min) {
|
||||
value = props.min
|
||||
}
|
||||
|
||||
inputValue.value = formatValue(value)
|
||||
emitChange(value)
|
||||
}
|
||||
|
||||
// 聚焦事件
|
||||
const onFocus = (event : UniInputFocusEvent) : void => {
|
||||
emit('focus', event)
|
||||
}
|
||||
|
||||
// 监听 props 变化
|
||||
watch(() : number | null => {
|
||||
return props.modelValue ?? props.value ?? null
|
||||
}, (newValue : number | null) => {
|
||||
if (newValue != null) {
|
||||
inputValue.value = formatValue(newValue)
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化
|
||||
initValue()
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.uni-numbox {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
/* #ifdef WEB */
|
||||
user-select: none;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-numbox__btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background-color: #f5f5f5;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* #ifdef WEB */
|
||||
cursor: pointer;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-numbox__minus {
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
}
|
||||
|
||||
.uni-numbox__plus {
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
.uni-numbox__icon-line {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 1px;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.uni-numbox__icon-line--vertical {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.uni-numbox__icon-line--disabled {
|
||||
background-color: #c0c0c0;
|
||||
}
|
||||
|
||||
.uni-numbox__input {
|
||||
margin: 0 2px;
|
||||
background-color: #f5f5f5;
|
||||
width: 40px;
|
||||
height: 26px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
border-width: 0;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"id": "uni-number-box-x",
|
||||
"displayName": "uni-number-box 数字输入框",
|
||||
"version": "1.0.1",
|
||||
"description": "NumberBox 带加减按钮的数字输入框组件,用户可以控制每次点击增加的数值,支持小数。",
|
||||
"isExt": true,
|
||||
"extUrl": "https://ext.dcloud.net.cn/plugin?id=27896",
|
||||
"keywords": [
|
||||
"uni-ui",
|
||||
"uniui",
|
||||
"数字输入框",
|
||||
"uni-app-x"
|
||||
],
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"engines": {
|
||||
"HBuilderX": "",
|
||||
"uni-app": "",
|
||||
"uni-app-x": "^5.07"
|
||||
},
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"type": "component-vue",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x",
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": "√",
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
带加减按钮的数字输入框,常见于购物车商品数量选择。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```html
|
||||
<uni-number-box v-model="value" />
|
||||
```
|
||||
|
||||
### 设置范围
|
||||
|
||||
```html
|
||||
<uni-number-box v-model="value" :min="5" :max="20" />
|
||||
```
|
||||
|
||||
### 设置步长
|
||||
|
||||
```html
|
||||
<!-- 整数步长 -->
|
||||
<uni-number-box v-model="value" :step="5" />
|
||||
|
||||
<!-- 小数步长 -->
|
||||
<uni-number-box v-model="value" :step="0.1" />
|
||||
```
|
||||
|
||||
### 禁用状态
|
||||
|
||||
```html
|
||||
<uni-number-box :value="5" :disabled="true" />
|
||||
```
|
||||
|
||||
### 自定义样式
|
||||
|
||||
通过 `btn-class`、`input-class`、`icon-class` 自定义组件样式:
|
||||
|
||||
```html
|
||||
<uni-number-box v-model="value" btn-class="custom-btn" input-class="custom-input" icon-class="custom-icon" />
|
||||
```
|
||||
|
||||
```css
|
||||
.custom-btn {
|
||||
background-color: #007aff;
|
||||
}
|
||||
.custom-input {
|
||||
background-color: #e8f4ff;
|
||||
color: #007aff;
|
||||
}
|
||||
.custom-icon {
|
||||
background-color: #fff;
|
||||
}
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
| --- | --- | --- |
|
||||
| change | 值改变时触发 | value: number |
|
||||
| focus | 输入框聚焦时触发 | event: UniInputFocusEvent |
|
||||
| blur | 输入框失焦时触发 | event: UniInputBlurEvent |
|
||||
@@ -0,0 +1,4 @@
|
||||
## 1.0.1(2026-05-11)
|
||||
- 修复 starClass 和 starActiveClass 设置不生效的问题
|
||||
## 1.0.0(2026-05-07)
|
||||
- 支持 uni-rate 组件
|
||||
@@ -0,0 +1,624 @@
|
||||
<template>
|
||||
<!-- TODO Android/iOS平台不支持裁剪显示半个文字,高性能场景包裹一层view不合适,需要等App平台解决-->
|
||||
<text v-if="props.full!=true" class="active-color" :class="props.starActiveClass ?? ''" :style="props.starActiveStyle ?? ''">{{ stars }}</text> <!-- 高性能,单层,只读,不显示灰星,不支持小数。必须给value-->
|
||||
<view v-else
|
||||
<!-- #ifdef MP -->
|
||||
:id="containerId"
|
||||
<!-- #endif -->
|
||||
style="position: relative;align-self: flex-start;" @click="onTap">
|
||||
<text class="stars-pos gray-stars" style="white-space: nowrap;" :class="props.starClass ?? ''" :style="props.starStyle ?? ''">{{ stars }}</text> <!-- 背景灰星-->
|
||||
<!-- Android/iOS平台不支持裁剪显示半个文字,需通过包裹一层view裁剪实现 -->
|
||||
<view class="stars-pos active-init-width" :style="'width:'+calculatedWidth">
|
||||
<text v-if="props.full" class="active-color" :class="props.starActiveClass ?? ''"
|
||||
:style="'white-space: nowrap; align-self: flex-start;' + props.starStyle + ';' + props.starActiveStyle">★★★★★</text>
|
||||
</view>
|
||||
<!-- 前景亮星,宽度根据value变化-->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Rate 评分
|
||||
* @description 评分组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
defineOptions({
|
||||
externalClasses: ['star-class', 'star-active-class']
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* @description 当前评分,取值范围 0~5 的数字
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
value?: number,
|
||||
/**
|
||||
* @description v-model 绑定值,支持双向绑定,优先级高于 value
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
modelValue?: number,
|
||||
/**
|
||||
* @description 是否显示完整 5 颗星。为 true 时用灰星填充未选中位置;为 false 时仅显示亮星,性能更高但不支持点击交互
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
full?: boolean,
|
||||
/**
|
||||
* @description 是否只读,为 true 时不响应点击评分(full 为 false 时始终只读)
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
readonly?: boolean,
|
||||
/**
|
||||
* @description 灰星是否使用镂空样式(☆),为 false 时使用实心灰星(★)
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
plain?: boolean,
|
||||
/**
|
||||
* @description 灰星(未选中)的自定义样式类
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
starClass?: string.ClassString,
|
||||
/**
|
||||
* @description 灰星(未选中)的内联样式
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
starStyle?: string
|
||||
/**
|
||||
* @description 亮星(选中)的自定义样式类
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
starActiveClass?: string.ClassString,
|
||||
/**
|
||||
* @description 亮星(选中)的内联样式
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
starActiveStyle?: string
|
||||
}>()
|
||||
|
||||
// 计算星星显示
|
||||
const stars = computed(() => {
|
||||
if (props.full!=true) {
|
||||
const value = Math.max(0, Math.min(5, props.value ?? 0))
|
||||
return '★'.repeat(Math.round(value)) //返回亮星数量
|
||||
} else {
|
||||
return props.plain == true ? '☆'.repeat(5) : '★'.repeat(5) //返回灰星数量
|
||||
}
|
||||
})
|
||||
|
||||
// 问题:props.full != true 时,下面的Script其实都没有意义,但却需要执行。但vue里又无法用if包裹下面的代码,不灵活
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* @description 评分变化事件,参数为评分值,类型为 number
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
change: [value: number]
|
||||
/**
|
||||
* @description v-model 绑定值变化事件,参数为评分值,类型为 number
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
'update:modelValue': [value: number]
|
||||
}>()
|
||||
|
||||
// #ifdef MP
|
||||
const instance = getCurrentInstance()
|
||||
const containerId = `uni_rate_${instance?.uid || 'default'}`
|
||||
// #endif
|
||||
|
||||
// 响应式数据 - 优先使用modelValue,其次使用value
|
||||
const currentValue = ref<number>(0)
|
||||
currentValue.value = props.modelValue ?? props.value ?? 0
|
||||
// 安全初始化函数
|
||||
/* function safeInitValue() {
|
||||
const initialValue = props.modelValue ?? props.value ?? 0
|
||||
if (typeof initialValue === 'number' && !isNaN(initialValue)) {
|
||||
currentValue.value = initialValue
|
||||
} else {
|
||||
currentValue.value = 0
|
||||
}
|
||||
}
|
||||
// 初始化
|
||||
safeInitValue() */
|
||||
// 监听props变化
|
||||
watch(() : number | null => {
|
||||
return props.modelValue ?? props.value ?? null
|
||||
}, (newValue : number | null) => {
|
||||
if (newValue != null && typeof newValue === 'number' && !isNaN(newValue as number)) {
|
||||
currentValue.value = newValue as number
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 计算点亮层的宽度
|
||||
const calculatedWidth = computed(() => {
|
||||
// 确保在 0 到 5 之间
|
||||
const value = currentValue.value
|
||||
const rate = Math.max(0, Math.min(5, value))
|
||||
// console.log('rate: ',rate);
|
||||
// TODO Android端小数无法正常渲染,不是宽度计算的问题
|
||||
return `${(rate / 5) * 100}%`
|
||||
})
|
||||
|
||||
// #ifndef MP
|
||||
// 处理点击事件
|
||||
function onTap(event: UniPointerEvent) : void {
|
||||
// 如果是禁用模式,不处理点击
|
||||
if (props.readonly == true) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取点击位置 - 优化的事件处理
|
||||
let clickX = event.x
|
||||
|
||||
// 获取容器宽度 - 使用 currentTarget 获取绑定事件的 view 容器,而不是被点击的 text 元素
|
||||
let containerWidth = 0
|
||||
try {
|
||||
if (event.currentTarget?.offsetWidth != null && (event.currentTarget as UniElement).offsetWidth > 0) {
|
||||
containerWidth = (event.currentTarget as UniElement).offsetWidth
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略错误,使用默认值
|
||||
console.log('e: ',e);
|
||||
}
|
||||
// console.log("containerWidth:" + containerWidth);
|
||||
|
||||
// 计算点击的是第几颗星(从0开始)
|
||||
const finalValue = Math.round(clickX / containerWidth * 5)
|
||||
// console.log('finalValue: ',finalValue);
|
||||
|
||||
// 计算每颗星的宽度
|
||||
// const starWidth = containerWidth / 5
|
||||
// 计算点击位置在星内的偏移量
|
||||
// const offsetInStar = (clickX % starWidth) / starWidth
|
||||
/* // 根据偏移量决定是整星还是半星
|
||||
let newValue: number
|
||||
if (offsetInStar <= 0.5) {
|
||||
newValue = starIndex + 0.5 // 半星
|
||||
} else {
|
||||
newValue = starIndex + 1 // 整星
|
||||
}
|
||||
// 确保值在有效范围内 */
|
||||
// const finalValue = Math.max(0.5, Math.min(5, starIndex + 1))
|
||||
|
||||
// 更新值
|
||||
currentValue.value = finalValue
|
||||
|
||||
// 触发change事件
|
||||
emit('change', finalValue)
|
||||
|
||||
// 触发v-model更新
|
||||
emit('update:modelValue', finalValue)
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP
|
||||
async function onTap(event: UniPointerEvent) : Promise<void> {
|
||||
if (props.readonly == true) {
|
||||
return
|
||||
}
|
||||
|
||||
const rect = await uni.getElementById(containerId)?.getBoundingClientRectAsync()
|
||||
if (rect == null || rect.width <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const localX = event.clientX - rect.left
|
||||
if (!isFinite(localX)) {
|
||||
return
|
||||
}
|
||||
|
||||
const clampedX = Math.max(0, Math.min(rect.width, localX))
|
||||
const finalValue = Math.max(0, Math.min(5, Math.round(clampedX / rect.width * 5)))
|
||||
|
||||
currentValue.value = finalValue
|
||||
emit('change', finalValue)
|
||||
emit('update:modelValue', finalValue)
|
||||
}
|
||||
// #endif
|
||||
|
||||
// TODO 接入form表单规范,可以submit
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.stars-pos {
|
||||
/* 确保两个 star 的位置重叠 */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden; /* 关键:裁剪点亮层 */
|
||||
}
|
||||
.gray-stars {
|
||||
color: #ccc; /* 未点亮颜色 */
|
||||
position: relative; /* 确保底层可见 */
|
||||
}
|
||||
.active-init-width {
|
||||
width: 0%; /* full时高亮星的初始宽度 */
|
||||
}
|
||||
.active-color{
|
||||
color: #ffca3e; /* 点亮颜色 */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"id": "uni-rate-x",
|
||||
"displayName": "uni-rate 评分",
|
||||
"version": "1.0.1",
|
||||
"description": "Rate 评分组件,可自定义评分星星图标的大小、间隔、评分数。",
|
||||
"isExt": true,
|
||||
"extUrl": "https://ext.dcloud.net.cn/plugin?id=27900",
|
||||
"keywords": [
|
||||
"uni-ui",
|
||||
"uniui",
|
||||
"评分",
|
||||
"uni-app-x"
|
||||
],
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"engines": {
|
||||
"HBuilderX": "",
|
||||
"uni-app": "",
|
||||
"uni-app-x": "^5.07"
|
||||
},
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"type": "component-vue",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x",
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": "√",
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
评分组件,多用于购买商品后,对商品进行评价等场景
|
||||
|
||||
### 文件路径
|
||||
项目下有一个 /pages/uni-ui/rate/rate.uvue 页面,里面演示了rate组件的各种用法。
|
||||
|
||||
### 设计
|
||||
|
||||
rate组件是投票评分使用的组件。它使用unicode字符实现,通过修改字体的样式,实现各种效果。
|
||||
|
||||
它分2个版本,一个版本是高性能版本,只有一层,不显示灰星,仅显示亮星,亮星不支持小数点,不能点击。适于长列表中显示。
|
||||
另一个版本是普通版本,由背景灰星和前景亮星叠加而成,支持小数点,支持点击。
|
||||
这2个版本通过属性full来切换。
|
||||
|
||||
本组件支持v-model绑定响应式数据。
|
||||
|
||||
rate组件有如下属性:
|
||||
- full
|
||||
是否显示全部5颗星。当黄色不够5星时,是否填充灰星以达到5星的固定长度。不填充灰星性能更高
|
||||
- readonly
|
||||
是否响应点击变化。full属性为false时不生效,即无灰星时一定只读
|
||||
- plain
|
||||
灰星是否镂空
|
||||
- star-class
|
||||
星星的样式
|
||||
- star-style
|
||||
星星的样式
|
||||
- star-active-class
|
||||
选中的星星的样式
|
||||
- star-active-style
|
||||
选中的星星的样式
|
||||
|
||||
rate组件有onChange事件,事件回调中会给出点击触发的新评分值。
|
||||
@@ -0,0 +1,2 @@
|
||||
## 1.0.0(2026-04-30)
|
||||
- 支持 uni-refresh-box 组件
|
||||
@@ -0,0 +1,526 @@
|
||||
<template>
|
||||
<view slot="refresher" class="uni-refresh-box-buildin"> <!-- TODO 鸿蒙需要在这个view上补属性slot="refresher",不认外层传入-->
|
||||
<slot name="loading" :state="currentState">
|
||||
<loading ref="loadingRef" :paused="currentState != 2" class="uni-loading-class-buildin" :class="loadingClass" bold></loading>
|
||||
</slot>
|
||||
<text class="uni-text-class-buildin" :class="textClass">{{ tipText }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 自定义下拉刷新
|
||||
* @description 自定义下拉刷新组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "x"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
defineOptions({
|
||||
externalClasses: ['loading-class', 'text-class']
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* @description 当前下拉距离(px),通常由外部 scroll-view 的 refresher 状态传入
|
||||
* @default 0
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "x"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
pullingDistance: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* @description 是否正在刷新中,外部控制;为 true 时显示 loading 动画
|
||||
* @default false
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "x"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
refreshing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* @description 触发刷新的下拉阈值(px),下拉距离超过该值时进入“松手刷新”状态
|
||||
* @default 45
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "x"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
threshold: {
|
||||
type: Number,
|
||||
default: 45
|
||||
},
|
||||
/**
|
||||
* @description 下拉过程中(未达到阈值)显示的提示文字
|
||||
* @default "下拉刷新"
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "x"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
pullingText: {
|
||||
type: String,
|
||||
default: '下拉刷新'
|
||||
},
|
||||
/**
|
||||
* @description 下拉超过阈值后显示的提示文字
|
||||
* @default "松手刷新"
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "x"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
loosingText: {
|
||||
type: String,
|
||||
default: '松手刷新'
|
||||
},
|
||||
/**
|
||||
* @description 刷新中显示的提示文字
|
||||
* @default "正在刷新"
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "x"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
loadingText: {
|
||||
type: String,
|
||||
default: '正在刷新'
|
||||
},
|
||||
/**
|
||||
* @description 刷新完成瞬间显示的提示文字,为空则不展示
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "x"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
completeText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* @description 提示文字的自定义样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "x"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
textClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* @description loading 图标的自定义样式类
|
||||
* @default ""
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "x"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
loadingClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// Data
|
||||
const loadingRef = ref<UniElement | null>(null)
|
||||
const resetting = ref(false)
|
||||
// 标记是否正在归位中(从刷新结束到完全收起)
|
||||
const restoring = ref(false)
|
||||
|
||||
// 状态:0-下拉中 1-松手可刷新 2-刷新中 3-刷新完成 4-归位中(不显示文字)
|
||||
const currentState = computed((): number => {
|
||||
if (resetting.value) {
|
||||
return 3
|
||||
}
|
||||
if (props.refreshing) {
|
||||
return 2
|
||||
}
|
||||
// 归位中不显示文字
|
||||
if (restoring.value) {
|
||||
return 4
|
||||
}
|
||||
if (props.pullingDistance > props.threshold) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
// 提示文字
|
||||
const tipText = computed((): string => {
|
||||
switch (currentState.value) {
|
||||
case 0:
|
||||
return props.pullingText
|
||||
case 1:
|
||||
return props.loosingText
|
||||
case 2:
|
||||
return props.loadingText
|
||||
case 3:
|
||||
return props.completeText
|
||||
case 4:
|
||||
return '' // 归位中不显示文字
|
||||
default:
|
||||
return props.pullingText
|
||||
}
|
||||
})
|
||||
|
||||
// 监听外部 refreshing 变化
|
||||
watch((): boolean => props.refreshing, (newVal: boolean, oldVal: boolean) => {
|
||||
if (!newVal && oldVal) {
|
||||
// 外部结束刷新,显示完成状态
|
||||
resetting.value = true
|
||||
restoring.value = true
|
||||
setTimeout(() => {
|
||||
resetting.value = false
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 pullingDistance 变化
|
||||
watch((): number => props.pullingDistance, (distance: number, oldDistance: number) => {
|
||||
// 归零时结束归位状态
|
||||
if (distance == 0 && restoring.value) {
|
||||
restoring.value = false
|
||||
}
|
||||
|
||||
// 开始新的下拉时(从0变为大于0),重置归位状态
|
||||
if (distance > 0 && oldDistance == 0 && !props.refreshing) {
|
||||
restoring.value = false
|
||||
}
|
||||
|
||||
// 更新 loading 旋转角度
|
||||
const el = loadingRef.value
|
||||
if (el != null && !props.refreshing) {
|
||||
const maxDistance = 200
|
||||
const maxRotation = 540
|
||||
const rotation = Math.min((distance / maxDistance) * maxRotation, maxRotation)
|
||||
el.style.setProperty('transform', `rotate(${rotation}deg)`)
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露状态给外部
|
||||
defineExpose({
|
||||
currentState
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.uni-refresh-box-buildin {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.uni-loading-class-buildin {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-color: #888;
|
||||
}
|
||||
|
||||
.uni-text-class-buildin {
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"id": "uni-refresh-box",
|
||||
"displayName": "uni-refresh-box",
|
||||
"version": "1.0.0",
|
||||
"description": "uni-refresh-box",
|
||||
"isExt": true,
|
||||
"extUrl": "https://ext.dcloud.net.cn/plugin?id=27848",
|
||||
"keywords": [
|
||||
"uni-refresh-box",
|
||||
"uni-app-x",
|
||||
"uni-app x",
|
||||
"组件"
|
||||
],
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"engines": {
|
||||
"HBuilderX": "^5.07",
|
||||
"uni-app-x": "^5.07",
|
||||
"uni-app": ""
|
||||
},
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x",
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": "√",
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
自定义下拉刷新组件。
|
||||
|
||||
本组件封装了 scroll-view/list-view等滚动容器的下拉刷新功能,使用者只需在滚动容器中加入本组件,设置组件的属性和样式,即可实现自定义下拉刷新功能。
|
||||
|
||||
本组件自带一个全平台通用的下拉刷新样式,即左边一个 loading 圈,右边为下拉刷新相关的文字。也提供了各种自定义方法,包括文字、样式,甚至通过插槽传入完全不同的自定义下拉效果。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<scroll-view
|
||||
style="flex: 1;"
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="refreshing1"
|
||||
refresher-default-style="none"
|
||||
:refresher-threshold="45"
|
||||
refresher-max-drag-distance="200px"
|
||||
@refresherpulling="onRefresherpulling1"
|
||||
@refresherrefresh="onRefresherrefresh1"
|
||||
@refresherrestore="onRefresherrestore1"
|
||||
@refresherabort="onRefresherabort1"
|
||||
>
|
||||
<!-- 列表内容 -->
|
||||
<view v-for="i in listCount1" :key="i" class="content-item">
|
||||
<text class="text">item-{{ i }}</text>
|
||||
</view>
|
||||
|
||||
<!-- refresher 插槽使用 uni-refresh-box 组件 -->
|
||||
<uni-refresh-box
|
||||
slot="refresher"
|
||||
:pulling-distance="pullingDistance1"
|
||||
:refreshing="refreshing1"
|
||||
></uni-refresh-box>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const listCount1 = ref(3)
|
||||
const refreshing1 = ref(false)
|
||||
const pullingDistance1 = ref(0)
|
||||
|
||||
function onRefresherpulling1(e: RefresherEvent) {
|
||||
pullingDistance1.value = e.detail.dy
|
||||
}
|
||||
|
||||
function onRefresherrefresh1() {
|
||||
refreshing1.value = true
|
||||
console.log('列表1 触发刷新')
|
||||
setTimeout(() => {
|
||||
listCount1.value += 5
|
||||
refreshing1.value = false
|
||||
console.log('列表1 刷新完成')
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
function onRefresherrestore1() {
|
||||
pullingDistance1.value = 0
|
||||
}
|
||||
|
||||
function onRefresherabort1() {
|
||||
pullingDistance1.value = 0
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
#### state 状态说明
|
||||
|
||||
| 值 | 说明 |
|
||||
| :-- | :-- |
|
||||
| 0 | 下拉中(未达到阈值) |
|
||||
| 1 | 松手可刷新(已达到阈值) |
|
||||
| 2 | 刷新中 |
|
||||
| 3 | 刷新完成 |
|
||||
| 4 | 归位中(不显示文字) |
|
||||
|
||||
### Slots
|
||||
|
||||
| 名称 | 说明 | 插槽参数 |
|
||||
| :-- | :-- | :-- |
|
||||
| loading | 自定义图标 | - |
|
||||
@@ -0,0 +1,6 @@
|
||||
## 1.0.2(2026-07-16)
|
||||
- 修复微信小程序滚动内容穿透底部 tab-bar
|
||||
## 1.0.1(2026-07-07)
|
||||
- 优化组件的宽度计算方式
|
||||
## 1.0.0(2026-04-30)
|
||||
- 支持 uni-tab-bar 组件
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<view class="uni-tab-list-root" :class="tabBarRootClass">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 底部选项卡栏
|
||||
* @description 底部选项卡栏组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
const tabContentHeightFull = inject<Ref<boolean>>('uniTabContentHeightFull', ref(false))
|
||||
const changeTab = inject<(index: number) => void>('uniTabBarChangeTab', (index: number) => {})
|
||||
|
||||
const itemCount = ref<number>(0)
|
||||
const hasMidButtonItem = ref<boolean>(false)
|
||||
|
||||
const tabBarRootClass = computed(() : string => {
|
||||
return tabContentHeightFull.value ? 'uni-tab-list-root-full' : 'uni-tab-list-root-normal'
|
||||
})
|
||||
|
||||
const validMidButtonLayout = computed(() : boolean => {
|
||||
return hasMidButtonItem.value == false || (itemCount.value > 0 && itemCount.value % 2 == 0)
|
||||
})
|
||||
|
||||
function registerItem() : number {
|
||||
const index = itemCount.value
|
||||
itemCount.value++
|
||||
return index
|
||||
}
|
||||
|
||||
|
||||
function registerMidButton(width : number, height : number) : void {
|
||||
hasMidButtonItem.value = true
|
||||
if (width <= 0) {
|
||||
console.warn('uni-tab-midbutton: width 必须大于 0')
|
||||
}
|
||||
if (height <= 0) {
|
||||
console.warn('uni-tab-midbutton: height 必须大于 0')
|
||||
}
|
||||
}
|
||||
|
||||
function syncOccupiedHeight() : void {
|
||||
if (hasMidButtonItem.value && validMidButtonLayout.value == false) {
|
||||
console.warn('uni-tab-bar: mid-button 需要偶数个普通 uni-tab-item,才能保持居中均分布局')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
syncOccupiedHeight()
|
||||
})
|
||||
|
||||
watch(() : number => itemCount.value, () => {
|
||||
syncOccupiedHeight()
|
||||
})
|
||||
|
||||
watch(() : boolean => hasMidButtonItem.value, () => {
|
||||
syncOccupiedHeight()
|
||||
})
|
||||
|
||||
provide('uniTabListRegisterItem', registerItem)
|
||||
provide('uniTabListRegisterMidButton', registerMidButton)
|
||||
provide('uniTabListChangeTab', changeTab)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.uni-tab-list-root {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: var(--uni-tab-bar-height, 50px);
|
||||
flex-shrink: 0;
|
||||
box-sizing: content-box;
|
||||
padding-bottom: var(--uni-safe-area-inset-bottom);
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
overflow: visible;
|
||||
border-top-width: 0.5px;
|
||||
border-top-color: rgba(0,0,0,0.33);
|
||||
border-top-style: solid;
|
||||
}
|
||||
|
||||
.uni-tab-list-root-normal {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* #ifdef MP-ALIPAY */
|
||||
.uni-tab-list-root-normal {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
.uni-tab-list-root-full {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<view v-if="hasRendered" class="uni-tab-content-root"
|
||||
:style="contentStyle">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 选项卡内容
|
||||
* @description 选项卡内容组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
// 从父组件注入数据和方法
|
||||
const currentIndex = inject<Ref<number>>('uniTabBarCurrentIndex', ref(0))
|
||||
const registerContent = inject<() => number>('uniTabBarRegisterContent', () => 0)
|
||||
const tabContentHeightFull = inject<Ref<boolean>>('uniTabContentHeightFull', ref(false))
|
||||
|
||||
// 当前 content 的索引
|
||||
const contentIndex = ref<number>(-1)
|
||||
|
||||
// 是否已经渲染过(按需加载)
|
||||
const hasRendered = ref<boolean>(false)
|
||||
|
||||
// 组件挂载时分配索引
|
||||
onMounted(() => {
|
||||
contentIndex.value = registerContent()
|
||||
|
||||
// 如果是第一个激活的 tab-content,立即渲染
|
||||
if (contentIndex.value == currentIndex.value) {
|
||||
hasRendered.value = true
|
||||
}
|
||||
})
|
||||
|
||||
// 计算是否激活
|
||||
const isActive = computed((): boolean => {
|
||||
return currentIndex.value == contentIndex.value
|
||||
})
|
||||
|
||||
// 监听激活状态,首次激活时渲染
|
||||
watch((): boolean => {
|
||||
return isActive.value
|
||||
}, (newValue: boolean) => {
|
||||
if (newValue && hasRendered.value == false) {
|
||||
hasRendered.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
const active = isActive.value
|
||||
return {
|
||||
visibility: active ? 'visible' : 'hidden',
|
||||
position: active ? 'relative' : 'absolute',
|
||||
left: active ? 'auto' : '0px',
|
||||
top: active ? 'auto' : '0px',
|
||||
right: active ? 'auto' : '0px',
|
||||
bottom: active ? 'auto' : (tabContentHeightFull.value ? '0px' : 'auto')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.uni-tab-content-root {
|
||||
flex: 1;
|
||||
min-height: 0px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<view class="uni-tab-list-item-root" @click.stop="handleClick">
|
||||
<slot></slot>
|
||||
<text v-if="props.badgeText != null && props.badgeText != '0' && props.badgeText != ''" class="uni-tab-list-item-badge-position uni-tab-list-item-badge-text" :class="props.badgeClass">{{ props.badgeText }}</text>
|
||||
<view v-else-if="props.badgeText != null && props.badgeText != '0'" class="uni-tab-list-item-badge-position uni-tab-list-item-badge-dot" :class="props.badgeClass"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 选项卡项
|
||||
* @description 选项卡项组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
defineOptions({
|
||||
externalClasses: ['badge-class']
|
||||
})
|
||||
|
||||
// 定义组件属性
|
||||
type Props = {
|
||||
/**
|
||||
* @description badge 的内容
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
badgeText?: string,
|
||||
/**
|
||||
* @description badge 的样式类
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
badgeClass?: string.ClassString,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
badgeClass: ''
|
||||
})
|
||||
|
||||
const registerItem = inject<() => number>('uniTabListRegisterItem', () => 0)
|
||||
const changeTab = inject<(index: number) => void>('uniTabListChangeTab', (index: number) => {})
|
||||
const itemIndex = ref<number>(-1)
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
itemIndex.value = registerItem()
|
||||
})
|
||||
|
||||
function handleClick() : void {
|
||||
if (itemIndex.value >= 0) {
|
||||
changeTab(itemIndex.value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.uni-tab-list-item-root {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.uni-tab-list-item-badge-position {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 50%;
|
||||
transform: translate(25%, -50%);
|
||||
}
|
||||
|
||||
.uni-tab-list-item-badge-text {
|
||||
background-color: rgb(225, 80, 80);
|
||||
min-width: 14px;
|
||||
height: 14px;
|
||||
line-height: 14px;
|
||||
border-radius: 7px;
|
||||
font-size: 10px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.uni-tab-list-item-badge-dot {
|
||||
background-color: rgb(225, 80, 80);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<view :style="{ width: props.width + 'px', height: props.height + 'px' }" class="uni-tab-list-item-midbutton-root">
|
||||
<view class="uni-tab-list-item-midbutton-slot">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 选项卡中间按钮
|
||||
* @description 选项卡中间悬浮按钮组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
type Props = {
|
||||
/**
|
||||
* @description 中间按钮宽度,单位 px
|
||||
* @default 90
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
width?: number,
|
||||
/**
|
||||
* @description 中间按钮高度,单位 px
|
||||
* @default 90
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
height?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
width: 90,
|
||||
height: 90
|
||||
})
|
||||
const registerMidButton = inject<(width : number, height : number) => void>('uniTabListRegisterMidButton', (width : number, height : number) => {})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
registerMidButton(props.width, props.height)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.uni-tab-list-item-midbutton-root {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.uni-tab-list-item-midbutton-slot {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<view class="uni-tab-bar-root"> <!-- 如果 tab 是一个非全屏的组件,需要套一层。如果是全屏场景,这一层可以省略 -->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 选项卡
|
||||
* @description 选项卡容器组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
type Props = {
|
||||
/**
|
||||
* @description 当前激活的选项卡索引
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
activeIndex: number,
|
||||
/**
|
||||
* @description tab-content 的高度是否通到tab-bar下面
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
tabContentHeightFull?: boolean
|
||||
}
|
||||
|
||||
// 定义组件属性
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
tabContentHeightFull: false
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* @description 选项卡变化时触发,参数为当前激活的选项卡索引,类型为 number
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
change: [index: number]
|
||||
}>()
|
||||
|
||||
// 当前激活的索引
|
||||
const currentIndex = ref<number>(props.activeIndex)
|
||||
|
||||
const tabContentHeightFull = ref<boolean>(props.tabContentHeightFull)
|
||||
|
||||
// tab-content 的数量计数器
|
||||
const contentCount = ref<number>(0)
|
||||
|
||||
// 监听 activeIndex 变化
|
||||
watch((): number => {
|
||||
return props.activeIndex
|
||||
}, (newIndex: number) => {
|
||||
currentIndex.value = newIndex
|
||||
})
|
||||
|
||||
watch(() : boolean => {
|
||||
return props.tabContentHeightFull
|
||||
}, () => {
|
||||
tabContentHeightFull.value = props.tabContentHeightFull
|
||||
})
|
||||
|
||||
// 切换选项卡
|
||||
function changeTab(index: number): void {
|
||||
if (currentIndex.value != index) {
|
||||
currentIndex.value = index
|
||||
emit('change', index)
|
||||
}
|
||||
}
|
||||
|
||||
// 注册 tab-content
|
||||
function registerContent() : number {
|
||||
const index = contentCount.value
|
||||
contentCount.value++
|
||||
return index
|
||||
}
|
||||
|
||||
// 通过 provide 向子组件提供数据和方法
|
||||
provide('uniTabBarCurrentIndex', currentIndex)
|
||||
provide('uniTabBarChangeTab', changeTab)
|
||||
provide('uniTabBarRegisterContent', registerContent)
|
||||
provide('uniTabContentHeightFull', tabContentHeightFull)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.uni-tab-bar-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0px;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"id": "uni-tab-bar",
|
||||
"displayName": "uni-tab-bar 选项卡",
|
||||
"version": "1.0.2",
|
||||
"description": "底部选项卡组件集",
|
||||
"isExt": true,
|
||||
"extUrl": "https://ext.dcloud.net.cn/plugin?id=27849",
|
||||
"examples": [
|
||||
{
|
||||
"name": "tab-bar-dark",
|
||||
"description": "暗色风格",
|
||||
"url": "https://gitcode.com/dcloud/hello-uni-app-x/blob/prod_alpha/pages/uni-ui/tab-bar/tab-bar-dark.uvue"
|
||||
},
|
||||
{
|
||||
"name": "tab-bar-custom",
|
||||
"description": "自定义样式",
|
||||
"url": "https://gitcode.com/dcloud/hello-uni-app-x/blob/prod_alpha/pages/uni-ui/tab-bar/tab-bar-custom.uvue"
|
||||
},
|
||||
{
|
||||
"name": "tab-bar-blur",
|
||||
"description": "半透明毛玻璃",
|
||||
"url": "https://gitcode.com/dcloud/hello-uni-app-x/blob/prod_alpha/pages/uni-ui/tab-bar/tab-bar-blur.uvue"
|
||||
},
|
||||
{
|
||||
"id": "uni-tab-midbutton",
|
||||
"name": "tab-bar-midbutton",
|
||||
"description": "中间按钮",
|
||||
"url": "https://gitcode.com/dcloud/hello-uni-app-x/blob/prod_alpha/pages/uni-ui/tab-bar/tab-bar-midbutton.uvue"
|
||||
},
|
||||
{
|
||||
"id": "uni-tab-midbutton",
|
||||
"name": "tab-bar-midbutton-notch",
|
||||
"description": "中间按钮镂空",
|
||||
"url": "https://gitcode.com/dcloud/hello-uni-app-x/blob/prod_alpha/pages/uni-ui/tab-bar/tab-bar-midbutton-notch.uvue"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"uni-ui",
|
||||
"uniui",
|
||||
"自定义选项卡",
|
||||
"uni-app-x",
|
||||
"tab-bar"
|
||||
],
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"engines": {
|
||||
"HBuilderX": "^5.07",
|
||||
"uni-app": "",
|
||||
"uni-app-x": "^5.07"
|
||||
},
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"type": "component-vue",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x",
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": "√",
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
自定义tab-bar组件, 包括一组前端组件:
|
||||
|
||||
- `uni-tab`:最外层容器,负责激活索引、内容区高度和切换事件
|
||||
- `uni-tab-bar`:底部 tab 容器,负责底栏布局、安全区和中间按钮占位
|
||||
- `uni-tab-item`:普通 tab 项,负责插槽展示和 badge
|
||||
- `uni-tab-midbutton`:中间悬浮按钮占位容器
|
||||
- `uni-tab-content`:上方内容区域容器,负责内容注册、懒渲染和显示切换
|
||||
|
||||
每个选项卡内容,不再是页面,而变成了uni-tab-content组件的子组件。
|
||||
|
||||
自定义tab-bar组件的使用方式是纯前端的、也是跨平台的。这与pages.json中的tabBar完全不同:
|
||||
1. pages.json中的tabBar相关的api无法应用于自定义tab-bar,比如uni.switchTab、uni.showTabBar、uni.hideTabBar、uni.showTabBarRedDot、uni.hideTabBarRedDot、uni.setTabBarBadge、uni.removeTabBarBadge、uni.setTabBarStyle、uni.setTabBarItem。
|
||||
2. pages.json中的tabBar页面,生命周期仍然是页面生命周期。但自定义tab-bar的uni-tab-content内容不触发页面生命周期,只触发组件生命周期。
|
||||
|
||||
当然,自定义tab-bar组件拥有更强的功能、更灵活的控制方式。比如下凹式midbutton。在uni-app x的非小程序平台中,性能并不会比pages.json中的tabBar差。
|
||||
|
||||
|
||||
### 基础用法
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<uni-tab style="flex: 1" :active-index="activeIndex" @change="handleChange">
|
||||
<uni-tab-content>
|
||||
<scroll-view style="flex: 1">
|
||||
<view>
|
||||
<text>内容一</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</uni-tab-content>
|
||||
<uni-tab-content>
|
||||
<view>
|
||||
<text>内容二</text>
|
||||
</view>
|
||||
</uni-tab-content>
|
||||
|
||||
<uni-tab-bar>
|
||||
<uni-tab-item>
|
||||
<text>首页</text>
|
||||
</uni-tab-item>
|
||||
<uni-tab-item badge-text="8">
|
||||
<text>消息</text>
|
||||
</uni-tab-item>
|
||||
</uni-tab-bar>
|
||||
</uni-tab>
|
||||
</template>
|
||||
```
|
||||
|
||||
底栏主体高度默认为 `50px`。如果需要自定义,请在 `uni-tab` 上设置 `--uni-tab-bar-height` CSS 变量:
|
||||
|
||||
```vue
|
||||
<uni-tab style="flex: 1; --uni-tab-bar-height: 40px" :active-index="activeIndex" @change="handleChange">
|
||||
...
|
||||
</uni-tab>
|
||||
```
|
||||
|
||||
### 组件 API
|
||||
|
||||
#### uni-tab
|
||||
|
||||
##### 事件
|
||||
|
||||
| 事件名 | 返回参数 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `change` | `index: number` | 点击普通 tab 项切换后触发,返回新的激活索引 |
|
||||
|
||||
#### uni-tab-bar
|
||||
|
||||
无专用属性和API,可直接通过 `class` / `style` 自定义底栏外观。
|
||||
|
||||
#### uni-tab-content
|
||||
|
||||
无专用属性和API,可直接通过 `class` / `style` 自定义外观。
|
||||
|
||||
多个选项卡内容揉在一个uni-tab中比较乱,一般推荐uni-tab-content下放一个子组件,子组件作为独立uvue文件。
|
||||
|
||||
各个uni-tab-content的子组件,在显示和隐藏的没有生命周期。
|
||||
如需监听显示/隐藏需要通过provide/inject方式从外层传入响应式变量activeIndex,然后在子组件中watch这个activeIndex。
|
||||
|
||||
### 使用注意事项
|
||||
|
||||
1. `uni-tab` 所在的页面不应该滚动,应该在pages.json中配置页面style为`disableScroll:true`。
|
||||
2. 屏幕变化适配,Android需HBuilderX 5.09+支持。之前版本组件里无法监听页面的onResize。
|
||||
3. `uni-tab-content` 与 `uni-tab-item` 的数量必须一致,顺序也必须一一对应,否则切换后的内容会错位。
|
||||
4. `uni-tab` 默认占满页面的宽度和高度,即 `style="flex: 1"`
|
||||
5. 自定义底栏高度时,请在 `uni-tab` 上设置带长度单位的 `--uni-tab-bar-height` CSS 变量,例如 `--uni-tab-bar-height: 40px`。底栏和需要显式计算高度的内容区会共用该变量。
|
||||
6. 如果底栏存在镂空、下凹或透明边缘,需要让底部内容透出来,请在 `uni-tab` 上开启 `tab-content-height-full`,此时内容区会延伸到 `uni-tab-bar` 下方。
|
||||
7. `uni-tab-content` 采用“首次激活再渲染”的策略;第一次切入前不会创建实例,切走后只隐藏不销毁,适合保留 tab 内部状态。
|
||||
8. `uni-tab-item` 只负责普通 tab 项注册与展示,不提供单独点击事件;请统一监听 `uni-tab` 的 `change`。
|
||||
9. 使用 `uni-tab-midbutton` 时,中间按钮左右两侧的普通 `uni-tab-item` 数量需要相等,也就是普通项总数必须为偶数,才能保持居中均分布局。
|
||||
10. `uni-tab-midbutton` 只负责预留中间按钮位置,不参与激活索引计算;中间按钮点击行为需要在插槽内部自行处理。
|
||||
11. `badge-text` 传空字符串时显示红点,传 `'0'` 时不显示;如果需要自定义 badge 外观,请使用 `badge-class` 覆盖样式。
|
||||
12. `uni-tab-bar`、`uni-tab-item`、`uni-tab-content` 的根节点都支持直接挂 `class` / `style`,推荐把视觉差异放在页面侧处理,不要改组件内部逻辑。
|
||||
13. 微信小程序提供了一种webview方式渲染的底部tabBar方式,来进行tabBar自定义。这种方式仅微信小程序支持,与本组件无关。
|
||||
@@ -0,0 +1,10 @@
|
||||
## 中间凸起
|
||||
非下沉的,中间凸起的midbutton页面,缺少一个平滑的过渡曲线
|
||||
|
||||
## 左右滑
|
||||
Android常见,微信
|
||||
避免和页面内的内容冲突
|
||||
|
||||
## tab-bar高斯模糊
|
||||
等view支持高斯模糊的css后,补一个示例页面即可
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
## 1.0.0(2026-04-30)
|
||||
- 支持 uni-time-format 组件
|
||||
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<text class="time-text">{{ formattedTime }}</text>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 时间格式化
|
||||
* @description 时间格式化展示组件
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
type Props = {
|
||||
/**
|
||||
* @description 需要格式化的时间戳,支持秒级(10 位)或毫秒级(13 位),秒级会自动换算成毫秒
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
timestamp: number
|
||||
/**
|
||||
* @description 输出格式。"datetime" 输出 YYYY-MM-DD HH:mm;"date" 输出 YYYY-MM-DD;"time" 输出 HH:mm:ss;"relative" 输出相对时间(刚刚 / N 分钟前 / N 小时前 / 昨天 HH:mm / 完整日期)
|
||||
* @default "datetime"
|
||||
* @uniPlatform {
|
||||
* "app": {
|
||||
* "android": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.21"
|
||||
* },
|
||||
* "ios": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.11"
|
||||
* },
|
||||
* "harmony": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07",
|
||||
* "unixVaporVer": "5.07"
|
||||
* }
|
||||
* },
|
||||
* "mp": {
|
||||
* "weixin": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* },
|
||||
* "alipay": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.31"
|
||||
* }
|
||||
* },
|
||||
* "web": {
|
||||
* "uniVer": "x",
|
||||
* "unixVer": "5.07"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
format?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
format: 'datetime'
|
||||
})
|
||||
|
||||
const appLanguage = (uni.getAppBaseInfo().appLanguage ?? 'en').toLowerCase()
|
||||
const formattedTime = ref<string>('')
|
||||
|
||||
function padZero(num : number) : string {
|
||||
return num < 10 ? '0' + num.toString() : num.toString()
|
||||
}
|
||||
|
||||
function normalizeTimestamp(timestamp : number) : number {
|
||||
const absTimestamp = Math.abs(timestamp)
|
||||
if (absTimestamp > 0 && absTimestamp < 100000000000) {
|
||||
return timestamp * 1000
|
||||
}
|
||||
return timestamp
|
||||
}
|
||||
|
||||
function isValidDate(date : Date) : boolean {
|
||||
return !isNaN(date.getTime())
|
||||
}
|
||||
|
||||
function getLocaleKey(language : string) : string {
|
||||
if (language.indexOf('zh-hant') == 0 || language.indexOf('zh-tw') == 0 || language.indexOf('zh-hk') == 0 || language.indexOf('zh-mo') == 0) {
|
||||
return 'zh-hant'
|
||||
}
|
||||
if (language.indexOf('zh') == 0) {
|
||||
return 'zh-hans'
|
||||
}
|
||||
if (language.indexOf('fr') == 0) {
|
||||
return 'fr'
|
||||
}
|
||||
if (language.indexOf('la') == 0) {
|
||||
return 'la'
|
||||
}
|
||||
return 'en'
|
||||
}
|
||||
|
||||
const localeKey = getLocaleKey(appLanguage)
|
||||
|
||||
function formatDateTime(date : Date) : string {
|
||||
const year = date.getFullYear()
|
||||
const month = padZero(date.getMonth() + 1)
|
||||
const day = padZero(date.getDate())
|
||||
const hours = padZero(date.getHours())
|
||||
const minutes = padZero(date.getMinutes())
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
function formatTime(date : Date) : string {
|
||||
return `${padZero(date.getHours())}:${padZero(date.getMinutes())}`
|
||||
}
|
||||
|
||||
function getNowText(localeKey : string) : string {
|
||||
if (localeKey == 'zh-hans') return '刚刚'
|
||||
if (localeKey == 'zh-hant') return '刚刚'
|
||||
if (localeKey == 'fr') return 'mnt.'
|
||||
if (localeKey == 'la') return 'nunc'
|
||||
return 'now'
|
||||
}
|
||||
|
||||
function formatMinuteAgo(localeKey : string, minutes : number) : string {
|
||||
if (localeKey == 'zh-hans') return `${minutes}分钟前`
|
||||
if (localeKey == 'zh-hant') return `${minutes}分鐘前`
|
||||
if (localeKey == 'fr') return `-${minutes}min`
|
||||
if (localeKey == 'la') return `-${minutes}m`
|
||||
return `${minutes}m ago`
|
||||
}
|
||||
|
||||
function formatHourAgo(localeKey : string, hours : number) : string {
|
||||
if (localeKey == 'zh-hans') return `${hours}小时前`
|
||||
if (localeKey == 'zh-hant') return `${hours}小時前`
|
||||
if (localeKey == 'fr') return `-${hours}h`
|
||||
if (localeKey == 'la') return `-${hours}h`
|
||||
return `${hours}h ago`
|
||||
}
|
||||
|
||||
function formatYesterday(localeKey : string, timeText : string) : string {
|
||||
if (localeKey == 'zh-hant' || localeKey == 'zh-hans') return `昨天 ${timeText}`
|
||||
if (localeKey == 'fr') return `hier ${timeText}`
|
||||
if (localeKey == 'la') return `heri ${timeText}`
|
||||
return `yest ${timeText}`
|
||||
}
|
||||
|
||||
function getDayStartTimestamp(date : Date) : number {
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()
|
||||
}
|
||||
|
||||
function getRelativeTime(targetDate : Date, currentTimestamp : number) : string {
|
||||
const targetTimestamp = targetDate.getTime()
|
||||
if (targetTimestamp > currentTimestamp) {
|
||||
return formatDateTime(targetDate)
|
||||
}
|
||||
|
||||
const nowDate = new Date(currentTimestamp)
|
||||
const todayStart = getDayStartTimestamp(nowDate)
|
||||
const yesterdayStart = todayStart - 24 * 60 * 60 * 1000
|
||||
|
||||
if (targetTimestamp >= todayStart) {
|
||||
const diffMs = currentTimestamp - targetTimestamp
|
||||
if (diffMs < 60 * 1000) {
|
||||
return getNowText(localeKey)
|
||||
}
|
||||
const diffMinutes = Math.floor(diffMs / (60 * 1000))
|
||||
if (diffMinutes < 60) {
|
||||
return formatMinuteAgo(localeKey, diffMinutes)
|
||||
}
|
||||
const diffHours = Math.floor(diffMs / (60 * 60 * 1000))
|
||||
return formatHourAgo(localeKey, diffHours)
|
||||
}
|
||||
|
||||
if (targetTimestamp >= yesterdayStart) {
|
||||
return formatYesterday(localeKey, formatTime(targetDate))
|
||||
}
|
||||
|
||||
return formatDateTime(targetDate)
|
||||
}
|
||||
|
||||
function refreshFormattedTime() {
|
||||
const normalizedTimestamp = normalizeTimestamp(props.timestamp)
|
||||
const targetDate = new Date(normalizedTimestamp)
|
||||
if (!isValidDate(targetDate)) {
|
||||
formattedTime.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
const formatType = props.format
|
||||
if (formatType == 'relative') {
|
||||
formattedTime.value = getRelativeTime(targetDate, Date.now())
|
||||
return
|
||||
}
|
||||
|
||||
const hours = padZero(targetDate.getHours())
|
||||
const minutes = padZero(targetDate.getMinutes())
|
||||
if (formatType == 'time') {
|
||||
const seconds = padZero(targetDate.getSeconds())
|
||||
formattedTime.value = `${hours}:${minutes}:${seconds}`
|
||||
return
|
||||
}
|
||||
|
||||
const year = targetDate.getFullYear()
|
||||
const month = padZero(targetDate.getMonth() + 1)
|
||||
const day = padZero(targetDate.getDate())
|
||||
if (formatType == 'date') {
|
||||
formattedTime.value = `${year}-${month}-${day}`
|
||||
return
|
||||
}
|
||||
formattedTime.value = `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
watch(() : string => props.format, () => {
|
||||
refreshFormattedTime()
|
||||
})
|
||||
|
||||
watch(() : number => props.timestamp, () => {
|
||||
refreshFormattedTime()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
refreshFormattedTime()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.time-text {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"id": "uni-time-format",
|
||||
"displayName": "uni-time-format 时间格式化",
|
||||
"version": "1.0.0",
|
||||
"description": "TimeFormat 时间格式化组件,可自定义时间格式化。",
|
||||
"isExt": true,
|
||||
"extUrl": "https://ext.dcloud.net.cn/plugin?id=27851",
|
||||
"keywords": [
|
||||
"uni-ui",
|
||||
"uniui",
|
||||
"time-format",
|
||||
"uni-app-x"
|
||||
],
|
||||
"engines": {
|
||||
"HBuilderX": "^5.07",
|
||||
"uni-app-x": "^5.07",
|
||||
"uni-app": ""
|
||||
},
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"type": "component-vue",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x",
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"encrypt": [],
|
||||
"dependencies": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": "√",
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
时间格式化展示组件,支持绝对时间与相对时间。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```html
|
||||
<uni-time-format :timestamp="Date.now()" format="datetime"></uni-time-format>
|
||||
```
|
||||
|
||||
### relative 规则
|
||||
|
||||
`format=relative` 按自然日输出:
|
||||
- 今天内:
|
||||
`< 1 分钟` 输出“刚刚”
|
||||
`< 60 分钟` 输出“n分钟前”
|
||||
`>= 60 分钟` 输出“n小时前”
|
||||
- 昨天内:输出“昨天 HH:mm”
|
||||
- 超过昨天:输出“YYYY-MM-DD HH:mm”
|
||||
- 未来时间:直接输出“YYYY-MM-DD HH:mm”
|
||||
|
||||
### 相对时间国际化
|
||||
|
||||
根据 `uni.getAppBaseInfo().appLanguage` 自动匹配:
|
||||
- 简体中文(`zh`)
|
||||
- 繁体中文(`zh-hant` / `zh-tw` / `zh-hk` / `zh-mo`)
|
||||
- 英文(默认)
|
||||
- 法文(`fr`)
|
||||
- 拉丁文(`la`)
|
||||
|
||||
法文、拉丁文使用短文案,避免长文本占位。
|
||||
|
||||
### 非法时间
|
||||
|
||||
当 `timestamp` 非法时,组件返回空字符串。
|
||||
|
||||
### 示例页面
|
||||
|
||||
`/pages/uni-ui/time-format/time-format`
|
||||
示例页面包含两组相对时间演示:
|
||||
- 输入框动态演示:输入 `yyyy-mm-dd hh:mm:ss`,实时展示对应相对时间
|
||||
- 固定分支演示:展示“刚刚 / 分钟前 / 小时前 / 昨天 HH:mm / 超过昨天”五种分支,且不随输入变化
|
||||
@@ -0,0 +1,19 @@
|
||||
## 1.0.6 (2026-10-08)
|
||||
- uni-combox 修复安卓蒸汽模式会出现内容不显示的问题
|
||||
## 1.0.5 (2026-09-09)
|
||||
- uni-tab-bar 优化组件的宽度计算方式
|
||||
- uni-tab-bar 修复微信小程序滚动内容穿透底部 tab-bar
|
||||
## 1.0.4(2026-05-26)
|
||||
- uni-badge-view 修复类名拼写错误
|
||||
- uni-nav-bar 纠正类名拼写错误
|
||||
## 1.0.3(2026-05-15)
|
||||
- uni-collapse 支持 content-wrap-class 定义内层容器样式
|
||||
## 1.0.2(2026-05-12)
|
||||
- uni-badge-view 更新文档
|
||||
- uni-index-bar 更新文档
|
||||
- uni-number-box 移除无用依赖
|
||||
- uni-rate 修复 starClass 和 starActiveClass 设置不生效的问题
|
||||
## 1.0.1(2026-05-07)
|
||||
- 新增 uni-collapse、uni-link、uni-nav-bar、uni-number-box、uni-rate 组件
|
||||
## 1.0.0(2026-04-30)
|
||||
- 支持 uni-ui-x 组件库
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"id": "uni-ui-x",
|
||||
"displayName": "uni-ui-x",
|
||||
"version": "1.0.6",
|
||||
"description": "uni-ui-x是DCloud为uni-app x提供的组件库",
|
||||
"isExt": true,
|
||||
"keywords": [
|
||||
"uni-ui-x",
|
||||
"UI组件库",
|
||||
"ui框架",
|
||||
"ui库",
|
||||
"uni-app-x"
|
||||
],
|
||||
"engines": {
|
||||
"HBuilderX": "",
|
||||
"uni-app-x": "^5.08",
|
||||
"uni-app": ""
|
||||
},
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"type": "component-vue",
|
||||
"darkmode": "√",
|
||||
"i18n": "x",
|
||||
"widescreen": "√",
|
||||
"npmurl": "",
|
||||
"vapor": "√"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"uni-badge-view",
|
||||
"uni-collapse-x",
|
||||
"uni-drag-cell",
|
||||
"uni-fab-button",
|
||||
"uni-index-bar",
|
||||
"uni-link-x",
|
||||
"uni-nav-bar-x",
|
||||
"uni-number-box-x",
|
||||
"uni-rate-x",
|
||||
"uni-refresh-box",
|
||||
"uni-tab-bar",
|
||||
"uni-time-format",
|
||||
"uni-combox-x"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": {
|
||||
"extVersion": "1.0.3",
|
||||
"minVersion": ""
|
||||
},
|
||||
"chrome": {
|
||||
"extVersion": "1.0.3",
|
||||
"minVersion": ""
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "1.0.3",
|
||||
"minVersion": ""
|
||||
},
|
||||
"ios": {
|
||||
"extVersion": "1.0.3",
|
||||
"minVersion": ""
|
||||
},
|
||||
"harmony": {
|
||||
"extVersion": "1.0.3",
|
||||
"minVersion": ""
|
||||
}
|
||||
},
|
||||
"mp": {
|
||||
"weixin": {
|
||||
"extVersion": "1.0.3",
|
||||
"minVersion": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
## 项目介绍
|
||||
|
||||
uni-ui x,是DCloud为 uni-app x 提供的扩展组件库。[开源库地址](https://gitcode.com/dcloud/uni-ui-x/tree/alpha)
|
||||
|
||||
uni-ui x 是全新设计的,与为uni-app做的uni-ui不同。
|
||||
|
||||
uni-ui x 是伴随着HBuilderX 5.08发版的。5.07版本上有部分组件在部分平台有兼容性问题,详见组件详情的兼容性表格。再往前的HBuilderX版本未测试。
|
||||
|
||||
## 特点
|
||||
|
||||
- **性能优先**
|
||||
|
||||
uni-ui x 非常注重性能,在DOM层级控制、包体积、代码执行时间方面精益求精。
|
||||
|
||||
它了解 uni-app x 蒸汽模式的最佳实践,尽可能利用模板和样式的预编译特性来提升性能。当然它也支持非蒸汽模式。
|
||||
|
||||
- **合理的抽象**
|
||||
|
||||
一个组件的抽象能力非常重要,如何在自定义性、易用性、性能之间尽可能三全其美。
|
||||
|
||||
很多组件为了满足使用者的需求,封装的属性越来越多,但组件使用者仍然在抱怨自定义性不足。
|
||||
|
||||
尤其是组件把样式封装在组件属性上,造成用户需要自定义样式时无限封装属性,并且这些属性的控制无法使用css变量。
|
||||
|
||||
uni-ui x 提供了完全的样式自定义能力,所有的样式定义都可以通过class设置,组件属性只控制功能逻辑。
|
||||
|
||||
* 组件的根节点样式,可以在使用组件时的组件class或style上设置。
|
||||
* 组件的子节点样式,可以通过[externalClass](https://doc.dcloud.net.cn/uni-app-x/css/common/style-isolation.html#external-class)设置样式。子组件开放自己的样式出来,外部可通过externalClass进行子组件的样式自定义。
|
||||
|
||||
组件使用者完全可以通过css自己控制所有样式。无需修改组件源码。加上css变量加持,灵活度远高于其他组件库。
|
||||
|
||||
使用者可以通过css变量完全自定义自己的主题,而不会被组件属性上不能写css变量困扰。
|
||||
|
||||
- **为AI设计**
|
||||
|
||||
uni-ui x 优先代码而不是二进制文件。目前没有引入字体文件。这出于性能考虑,也出于AI友好度考虑。
|
||||
|
||||
以箭头为例,uni-ui x 中使用一个view配置左下边框并transform旋转来实现,这种设计性能比字体更好,且AI可以方便的理解这段代码的UI表现,可以在代码层面调整样式。
|
||||
|
||||
如果是字体图片,AI很难理解、生成、修改。
|
||||
|
||||
再举一个例子,uni-tab下沉midbutton,涉及一个贝塞尔曲线,此时没有使用图片,而是使用svg,同样是为了方便AI理解和修改。
|
||||
|
||||
- **中性**
|
||||
|
||||
uni-ui x的风格是中性的,和uni内置组件、小程序内置组件一样,重视是开发者可以自定义成各种风格,而不是为开发者提供某套风格。
|
||||
|
||||
开发者可以通过自定义css,形成自己的风格,或者交给AI完成类似工作。
|
||||
|
||||
uni-ui x 没有写死的颜色和文字:
|
||||
|
||||
* 没有写死的文字,不影响国际化。
|
||||
* 没有写死的颜色,不影响自定义主题。
|
||||
|
||||
<!-- 组件列表开始 -->
|
||||
### uni-ui x 已支持的组件列表
|
||||
|
||||
| 组件名 | 组件说明 |
|
||||
| --- | --- |
|
||||
| uni-badge-view | [数字角标](https://ext.dcloud.net.cn/plugin?name=uni-badge-view) |
|
||||
| uni-collapse | [折叠面板](https://ext.dcloud.net.cn/plugin?name=uni-collapse-x) |
|
||||
| uni-drag-cell | [可拖拽排序组件](https://ext.dcloud.net.cn/plugin?name=uni-drag-cell) |
|
||||
| uni-fab-button | [uni-fab-button](https://ext.dcloud.net.cn/plugin?name=uni-fab-button) |
|
||||
| uni-index-bar | [uni-index-bar](https://ext.dcloud.net.cn/plugin?name=uni-index-bar) |
|
||||
| uni-link | [链接](https://ext.dcloud.net.cn/plugin?name=uni-link-x) |
|
||||
| uni-nav-bar | [uni-nav-bar](https://ext.dcloud.net.cn/plugin?name=uni-nav-bar-x) |
|
||||
| uni-number-box | [数字输入框](https://ext.dcloud.net.cn/plugin?name=uni-number-box-x) |
|
||||
| uni-rate | [评分](https://ext.dcloud.net.cn/plugin?name=uni-rate-x) |
|
||||
| uni-refresh-box | [uni-refresh-box](https://ext.dcloud.net.cn/plugin?name=uni-refresh-box) |
|
||||
| uni-tab | [选项卡](https://ext.dcloud.net.cn/plugin?name=uni-tab-bar) |
|
||||
| uni-time-format | [时间格式化](https://ext.dcloud.net.cn/plugin?name=uni-time-format) |
|
||||
<!-- 组件列表结束 -->
|
||||
|
||||
## uni-ui的升级建议@uniuiupgrade
|
||||
|
||||
为uni-app提供的uni-ui,升级到uni-app x时无法直接对照升级,因为组件名称、用法都有变化。
|
||||
|
||||
但升级时,老版uni-ui组件在uni-app x下同等功能的新组件是什么,这里说明下功能映射关系:
|
||||
|
||||
|uni-ui组件 |中文名称 |在uni-app x下的方案 |
|
||||
|-- |-- |-- |
|
||||
|uni-badge |数字角标 |改用uni-badge-view |
|
||||
|uni-calendar |日历 |见hello uni-app x里的模板中的日历页面 |
|
||||
|uni-card |卡片 |无 |
|
||||
|uni-collapse |折叠面板 |改用uni-collapse |
|
||||
|uni-combox |组合框 |无 |
|
||||
|uni-countdown |倒计时 |无 |
|
||||
|uni-data-checkbox |数据选择器 |无 |
|
||||
|uni-data-picker |数据驱动的picker选择器 |组件本身支持uni-app x |
|
||||
|uni-dateformat |日期格式化 |改用uni-time-format |
|
||||
|uni-datetime-picker |日期选择器 |内置组件picker支持mode=date |
|
||||
|uni-drawer |抽屉 |改用内置组件page-container |
|
||||
|uni-easyinput |增强输入框 |无 |
|
||||
|uni-fab |悬浮按钮 |改用uni-fab-button |
|
||||
|uni-fav |收藏按钮 |无 |
|
||||
|uni-file-picker |文件选择上传 |无 |
|
||||
|uni-forms |表单 |改用内置组件form |
|
||||
|uni-goods-nav |商品导航 |无 |
|
||||
|uni-grid |宫格 |无 |
|
||||
|uni-group |分组 |无 |
|
||||
|uni-icons |图标 |无 |
|
||||
|uni-indexed-list |索引列表 |改用uni-index-bar |
|
||||
|uni-link |超链接 |改用uni-link |
|
||||
|uni-list |列表 |使用内置组件list |
|
||||
|uni-load-more |加载更多 |使用内置组件loading |
|
||||
|uni-nav-bar |自定义导航栏 |改用uni-nav-bar |
|
||||
|uni-notice-bar |通告栏 |无 |
|
||||
|uni-number-box |数字输入框 |改用uni-number-box |
|
||||
|uni-pagination |分页器 |无 |
|
||||
|uni-popup |弹出层 |改用内置组件page-container |
|
||||
|uni-rate |评分 |改用uni-rate |
|
||||
|uni-row |布局-行 |无 |
|
||||
|uni-search-bar |搜索栏 |无 |
|
||||
|uni-segmented-control|分段器 |无 |
|
||||
|uni-steps |步骤条 |无 |
|
||||
|uni-swipe-action |滑动操作 |参考hello uni-app x模板中可左滑删除长列表示例 |
|
||||
|uni-swiper-dot |轮播图指示点 |改用内置组件swiper,通过externalClass控制指示点样式 |
|
||||
|uni-table |表格 |推荐使用rich-text或web-view中的table。 |
|
||||
|uni-tag |标签 |无 |
|
||||
|uni-title |章节标题 |无 |
|
||||
|uni-transition |过渡动画 |无 |
|
||||
|
||||
标记为`无`的,大多比较简单,对于AI而言直接写相关UI比下载学习一个组件更合适。
|
||||
|
||||
## 交流群
|
||||
|
||||
有关 uni-ui-x 组件的任何意见或建议,欢迎通过 [uni-ui-x im 交流群](https://ext.dcloud.net.cn/publisher/start-session?pluginId=27852) 沟通交流。
|
||||
@@ -0,0 +1,2 @@
|
||||
## 1.0.0(2026-04-30)
|
||||
- 支持 uts-openSchema API
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"id": "uts-openSchema",
|
||||
"displayName": "uts-openSchema",
|
||||
"version": "1.0.0",
|
||||
"description": "在 Android、iOS、HarmonyOS 上打开链接的 UTS API",
|
||||
"keywords": [
|
||||
"uts-openSchema"
|
||||
],
|
||||
"repository": "https://gitcode.com/dcloud/uni-ui-x",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.0",
|
||||
"uni-app-x": "^4.75"
|
||||
},
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "uts",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "√"
|
||||
},
|
||||
"client": {
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "x",
|
||||
"chrome": "x"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "1.0.0",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": {
|
||||
"extVersion": "1.0.0",
|
||||
"minVersion": "12"
|
||||
},
|
||||
"harmony": {
|
||||
"extVersion": "1.1.2",
|
||||
"minVersion": "5.0.0"
|
||||
}
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "x"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
打开链接,支持:
|
||||
|
||||
1. 打开外部 App
|
||||
2. 使用浏览器打开链接
|
||||
3. 打开地图到指定地点
|
||||
4. ...
|
||||
|
||||
## 使用
|
||||
|
||||
1. 安装此插件
|
||||
2. 在要使用的地方 `import` 导入
|
||||
```ts
|
||||
import { openSchema, canOpenURL } from '@/uni_modules/uts-openSchema'
|
||||
```
|
||||
3. 直接调用 `openSchema` 方法:
|
||||
```ts
|
||||
// #ifdef UNI-APP-X
|
||||
// 使用外部浏览器打开指定URL
|
||||
openSchema('https://uniapp.dcloud.io/uni-app-x')
|
||||
|
||||
// #ifdef APP-ANDROID
|
||||
// Android 使用应用商店打开指定App
|
||||
openSchema('market://details?id=com.tencent.mm')
|
||||
|
||||
// Android 打开地图坐标
|
||||
// 可以先用canOpenURL判断是否安装了地图软件
|
||||
if (canOpenURL('androidamap://')) {
|
||||
openSchema('androidamap://viewMap?sourceApplication=Hello%20uni-app&poiname=DCloud&lat=39.9631018208&lon=116.3406135236&dev=0')
|
||||
} else {
|
||||
console.log('未安装高德地图')
|
||||
}
|
||||
// #endif -->
|
||||
|
||||
// #ifdef APP-IOS
|
||||
// 打开 AppStore 到搜索页
|
||||
openSchema('itms-apps://search.itunes.apple.com//WebObjects//MZSearch.woa/wa/search?media=software<erm=')
|
||||
|
||||
// 打开 iOS 地图坐标
|
||||
openSchema('http://maps.apple.com/?q=Mexican+Restaurant&sll=50.894967,4.341626&z=10&t=s')
|
||||
// #endif -->
|
||||
|
||||
// #endif -->
|
||||
```
|
||||
|
||||
### 参数
|
||||
|
||||
- openSchema(url: string) // `url`:要打开的链接 `必填` `不为空字符串`
|
||||
|
||||
## 相关开发文档
|
||||
|
||||
[UTS 语法](https://uniapp.dcloud.net.cn/tutorial/syntax-uts.html)
|
||||
|
||||
[UTS API插件](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html)
|
||||
|
||||
[UTS 组件插件](https://uniapp.dcloud.net.cn/plugin/uts-component.html)
|
||||
|
||||
[Hello UTS](https://gitcode.net/dcloud/hello-uts)
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"minSdkVersion": "21"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import Intent from 'android.content.Intent'
|
||||
import Uri from 'android.net.Uri'
|
||||
import { OpenSchema, CanOpenURL } from '../interface.uts'
|
||||
|
||||
export const openSchema : OpenSchema = function (url : string) {
|
||||
if (canOpenURL(url)) {
|
||||
const context = UTSAndroid.getUniActivity()!
|
||||
const uri = Uri.parse(url)
|
||||
const intent = new Intent(Intent.ACTION_VIEW, uri)
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.setData(uri)
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
console.error('[uts-openSchema] url param Error:', JSON.stringify(url))
|
||||
}
|
||||
}
|
||||
|
||||
export const canOpenURL : CanOpenURL = function (url : string) : boolean {
|
||||
if (typeof url === 'string' && url.length > 0) {
|
||||
const context = UTSAndroid.getUniActivity()!
|
||||
const uri = Uri.parse(url)
|
||||
const intent = new Intent(Intent.ACTION_VIEW, uri)
|
||||
return intent.resolveActivity(context.packageManager) != null ? true : false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { bundleManager, common } from '@kit.AbilityKit'
|
||||
import OpenLinkOptions from '@ohos.app.ability.OpenLinkOptions'
|
||||
import { getAbilityContext } from '@dcloudio/uni-runtime'
|
||||
import { OpenSchema, CanOpenURL } from '../interface.uts'
|
||||
|
||||
export const openSchema : OpenSchema = function (url : string) : void {
|
||||
;(getAbilityContext() as common.UIAbilityContext)?.openLink(url, {
|
||||
appLinkingOnly: false
|
||||
} as OpenLinkOptions)
|
||||
}
|
||||
|
||||
export const canOpenURL : CanOpenURL = function (url : string) : boolean {
|
||||
try {
|
||||
return bundleManager.canOpenLink(url)
|
||||
} catch (error) {
|
||||
console.error('[uts-openSchema] url param Error:', JSON.stringify(url))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"deploymentTarget": "12.0"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { UIApplication } from 'UIKit'
|
||||
import { URL } from 'Foundation'
|
||||
import { OpenSchema, CanOpenURL } from '../interface.uts'
|
||||
|
||||
export const openSchema : OpenSchema = function (url : string) : void {
|
||||
if (canOpenURL(url)) {
|
||||
let uri = new URL(string = url)
|
||||
UIApplication.shared.open(uri!)
|
||||
} else {
|
||||
console.error('[uts-openSchema] url param Error: ', url)
|
||||
}
|
||||
}
|
||||
|
||||
export const canOpenURL : CanOpenURL = function (url : string) : boolean {
|
||||
if (typeof url == 'string' && url.length > 0) {
|
||||
let uri = new URL(string = url)
|
||||
if (uri != null && UIApplication.shared.canOpenURL(uri!)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type OpenSchema = (url : string) => void
|
||||
export type CanOpenURL = (url : string) => boolean
|
||||
@@ -0,0 +1,12 @@
|
||||
import { OpenSchema, CanOpenURL } from '../interface.uts'
|
||||
|
||||
export const openSchema : OpenSchema = function (url : string) : void {
|
||||
location.href = url
|
||||
}
|
||||
|
||||
export const canOpenURL : CanOpenURL = function (url : string) : boolean {
|
||||
if (url != '') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user