Initial commit

This commit is contained in:
tanxin
2026-10-29 10:08:39 +08:00
commit 4c6243a274
124 changed files with 17359 additions and 0 deletions
+6
View File
@@ -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>
+74
View File
@@ -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"
}
}
}
}
+34
View File
@@ -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
+4
View File
@@ -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>
+74
View File
@@ -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": "√"
}
}
}
}
}
}
+96
View File
@@ -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上会消失
+6
View File
@@ -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>
+91
View File
@@ -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": ""
}
}
}
}
}
}
}
+122
View File
@@ -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() | 关闭候选浮层 |
+2
View File
@@ -0,0 +1,2 @@
## 1.0.0(2026-04-30)
- 支持 uni-drag-cell 组件
File diff suppressed because it is too large Load Diff
+73
View File
@@ -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": "√"
}
}
}
}
}
}
+331
View File
@@ -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
- 支持触摸拖拽
+2
View File
@@ -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>
+73
View File
@@ -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": "√"
}
}
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
悬浮加号按钮
本组件常用于页面中右下角悬浮的、里面有一个加号的圆形按钮。
组件的根view默认是一个圆形,蓝色背景(#007AFF),并带有阴影。使用组件时注意组件的父容器要留出阴影的空间,避免被裁剪。
组件的二级view是2个view交叉组成的加号。其中垂直的view是通过transform变换方向实现的。
组件的属性为 plus-class,它是一个externalClass,用于外部修改组件内部的、组成加号的2个view的样式。
组件中加号的默认样式为背景白色、粗细为2px、尺寸为40%。
### 注意
- 鸿蒙平台按下fab-button不缩写的问题,需升级到HBuilderX 5.09+解决
+4
View File
@@ -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>
+100
View File
@@ -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": "√"
}
}
}
}
}
}
+1
View File
@@ -0,0 +1 @@
### [查看文档](https://doc.dcloud.net.cn/uni-app-x/component/uni-ui-x/uni-icons.html)
+4
View File
@@ -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>
+75
View File
@@ -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": "√"
}
}
}
}
}
}
+98
View File
@@ -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
}
```
+2
View File
@@ -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>
+76
View File
@@ -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>
+45
View File
@@ -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`
+4
View File
@@ -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>
+73
View File
@@ -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": "√"
}
}
}
}
}
}
+63
View File
@@ -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>
+74
View File
@@ -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": "√"
}
}
}
}
}
}
+58
View File
@@ -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 |
+4
View File
@@ -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>
+73
View File
@@ -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": "√"
}
}
}
}
}
}
+32
View File
@@ -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事件,事件回调中会给出点击触发的新评分值。
+2
View File
@@ -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>
+73
View File
@@ -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": "√"
}
}
}
}
}
}
+80
View File
@@ -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 | 自定义图标 | - |
+6
View File
@@ -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>
+103
View File
@@ -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": "√"
}
}
}
}
}
}
+93
View File
@@ -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自定义。这种方式仅微信小程序支持,与本组件无关。
+10
View File
@@ -0,0 +1,10 @@
## 中间凸起
非下沉的,中间凸起的midbutton页面,缺少一个平滑的过渡曲线
## 左右滑
Android常见,微信
避免和页面内的内容冲突
## tab-bar高斯模糊
等view支持高斯模糊的css后,补一个示例页面即可
+2
View File
@@ -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>
+73
View File
@@ -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": "√"
}
}
}
}
}
}
+40
View File
@@ -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 / 超过昨天”五种分支,且不随输入变化
+19
View File
@@ -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 组件库
+106
View File
@@ -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": ""
}
}
}
}
}
}
}
+127
View File
@@ -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) 沟通交流。
+2
View File
@@ -0,0 +1,2 @@
## 1.0.0(2026-04-30)
- 支持 uts-openSchema API
+75
View File
@@ -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"
}
}
}
}
}
}
+57
View File
@@ -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&lterm=')
// 打开 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
}