= {};
export const EmptyFunctionalProps: IEmptyProps = {
title: '',
message: '',
image: '',
imageMode: 'scaleToFill',
buttonInfo: {
list: [],
layout: 'horizontal',
},
size: 'normal',
onClickButton: () => {},
};
================================================
FILE: src/Empty/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
// text 颜色
@empty-text-color: var(--empty-text-color, @COLOR_TEXT_PRIMARY);
// asisst-text 颜色
@empty-asisst-text-color: var(--empty-asisst-text-color, @COLOR_TEXT_ASSIST);
@empty-normal-width: 510 * @rpx;
@empty-small-width: 416 * @rpx;
@empty-normal-height: 282 * @rpx;
@empty-small-height: 230 * @rpx;
@empty-font-size-common: 24 * @rpx;
@empty-font-size-large: 36 * @rpx;
@empty-margin-x-small: 12 * @rpx;
@empty-margin-small: 24 * @rpx;
@empty-margin-middle: 32 * @rpx;
@empty-margin-large: 24 * @rpx;
@empty-vertical-button-width: 368 * @rpx;
@empty-vertical-button-gap: 32 * @rpx;
@empty-horizontal-button-gap: 24 * @rpx;
@empty-sub-line-height: 37 * @rpx;
@empty-text-line-height: 50 * @rpx;
@empty-image-url: 'https://mdn.alipayobjects.com/huamei_mnxlps/afts/img/A*J9z7RqVm1soAAAAAAAAAAAAADkqGAQ/original';
================================================
FILE: src/Feedback/index.axml
================================================
{{ title }}
{{ item.text }}
{{ title }}
{{ item.text }}
================================================
FILE: src/Feedback/index.en.md
================================================
---
nav:
path: /components
group:
title: Feedback
order: 15
toc: content
supportPlatform: ['alipay', 'wechat']
---
# Feedback
Feedback mostly acts on distribution scenarios.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-feedback": "antd-mini/es/Feedback/index"
#endif
#if WECHAT
"ant-feedback": "antd-mini/Feedback/index"
#endif
}
```
## Code Sample
### Basic use
```xml
Here is the popover content
```
```js
Page({
data: {
visible: true,
},
onVisibleChange(value) {
#if ALIPAY
this.setData({ visible: value });
#endif
#if WECHAT
this.setData({ visible: value.detail });
#endif
},
});
```
### Point Guide Feedback
```xml
```
```js
Page({
data: {
visible: true,
feedList: [
{ icon: 'HeartOutline', text: '喜欢推荐内容', id: '1', },
{ icon: 'FrownOutline', text: '我不感兴趣', id: '2', },
{ image: 'https://gw.alipayobjects.com/mdn/rms_ce4c6f/afts/img/A*XMCgSYx3f50AAAAAAAAAAABkARQnAQ', text: '看过类似内容', id: '3', },
],
},
onVisibleChange(value) {
#if ALIPAY
this.setData({ visible: value });
#endif
#if WECHAT
this.setData({ visible: value.detail });
#endif
},
onTapFeedItem(feedItem) {
#if ALIPAY
my.showToast({ content: `点击了反馈项${feedItem.text}`, });
#endif
#if WECHAT
wx.showToast({ title: `点击了反馈项${feedItem.text}`, });
#endif
},
});
```
### Feedback Card
```xml
Click to show feedback card
```
```js
Page({
data: {
visible: true,
feedList: [
{ icon: 'HeartOutline', text: '喜欢推荐内容', id: '1', },
{ icon: 'FrownOutline', text: '我不感兴趣', id: '2', },
{ image: 'https://gw.alipayobjects.com/mdn/rms_ce4c6f/afts/img/A*XMCgSYx3f50AAAAAAAAAAABkARQnAQ', text: '看过类似内容', id: '3', },
],
},
onVisibleChange(value) {
#if ALIPAY
this.setData({ visible: value });
#endif
#if WECHAT
this.setData({ visible: value.detail });
#endif
},
onTapFeedItem(feedItem) {
#if ALIPAY
my.showToast({ content: `点击了反馈项${feedItem.text}`, });
#endif
#if WECHAT
wx.showToast({ title: `点击了反馈项${feedItem.text}`, });
#endif
},
onShowFeedback() {
this.setData({
visible: true,
});
},
});
```
### Demo Code
## API
| Property | Description | Type | Default Value |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------ |
| visible | Whether visible | boolean | false |
| type | Type of feedback | 'popover' \| 'card' \| 'float' | card |
| title | Title of the feedback | string | - |
| placement | Bubble box position, optional `top`、`top-right`、`top-left`、`bottom`、`bottom-left`、`bottom-right`、`left`、`left-top`、`left-bottom`、`right`、`right-top` or `right-bottom` | string | bottom-right |
| list | List of Feedback | [FeedItem](#feeditem)[] | - |
| className | outermost style name | string | - |
| style | outermost style string | string | - |
| popoverType | Bootstrap style for bubble feedback | 'circle' \| 'default' | - |
| autoAdjustOverflow | Whether bubble position is adaptive | boolean | true |
| #if ALIPAY onVisibleChange | Hook showing state change | (visible: boolean) => void | - |
| #if ALIPAY onTapFeedItem | Feedback Item Click Callback | (item: any) => void | - |
| #if WECHAT bindvisiblechange | Hook showing state change | (visible: boolean) => void | - |
| #if WECHAT bindtapfeeditem | Feedback Item Click Callback | ([FeedItem](#feeditem): any) => void | - |
### FeedItem
| Property | Description | Type | Default Value |
| ----- | ---------------------------------------- | ------ | ------ |
| icon | Icons for feedback items | string | - |
| text | Copy of the feedback item | string | - |
| image | The image and the icon of the feedback item are mutually exclusive, and the icon is displayed first. | string | - |
| id | Unique identification of the feedback item | string | - |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| --feedback-text-color | #333333
| #c5cad1
| Feedback Text Color |
| --feedback-background-color | rgba(216, 216, 216, 0.42)
| rgba(216, 216, 216, 0.42)
| Feedback Background Color |
| --feedback-mask-color | rgba(0, 0, 0, 0.25)
| rgba(0, 0, 0, 0.25)
| Feedback Mask Color |
| --feedback-content-background-color | #ffffff
| #1a1a1a
| Feedback Content Background Color |
| --feedback-list-background-color | #f5f5f5
| #121212
| Feedback List Background Color |
| --feedback-list-text-color | #333333
| #c5cad1
| Feedback List Text Color |
================================================
FILE: src/Feedback/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-mask": "../Mask/index",
"ant-icon": "../Icon/index",
"popover": "../Popover/index"
}
}
================================================
FILE: src/Feedback/index.less
================================================
@import (reference) './variable.less';
@avatarPrefix: ant-feedback;
.@{avatarPrefix} {
.ant-popover-inner {
max-width: 100%;
color: @feedback-text-color;
background: @feedback-content-background-color;
}
.ant-popover-arrow {
border-bottom-color: @feedback-content-background-color;
}
&-popoverType-default {
.ant-popover-content {
width: 568 * @rpx;
}
}
&-popoverType-circle {
.ant-popover-content {
width: max-content;
transform: translate(-50%, 60 * @rpx);
left: 50% !important;
top: 50% !important;
bottom: 50% !important;
right: 50% !important;
}
}
&-show {
z-index: 9;
position: relative;
}
&-mask {
z-index: 2;
background: @feedback-mask-color;
}
&-trigger {
position: relative;
}
&-guide {
position: absolute;
background: @feedback-background-color;
border-radius: 50vh;
width: 48 * @rpx;
height: 48 * @rpx;
z-index: 999;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
align-items: center;
&-content {
background: @feedback-content-background-color;
border-radius: 50vh;
width: 24 * @rpx;
height: 24 * @rpx;
}
}
&-content {
background: @feedback-content-background-color;
border-radius: 24 * @rpx;
&-card {
display: flex;
flex-direction: column;
padding: 24 * @rpx;
background: @feedback-content-background-color;
.ant-feedback-content-list {
flex-direction: column;
gap: 16 * @rpx;
}
.ant-feedback-content-list-item {
background: @feedback-list-background-color;
border-radius: 50vh;
padding: 18 * @rpx 24 * @rpx;
}
}
&-popover {
padding: 24 * @rpx;
.ant-feedback-content-list {
flex-direction: column;
gap: 16 * @rpx;
}
.ant-feedback-content-list-item {
background: @feedback-list-background-color;
border-radius: 50vh;
padding: 18 * @rpx 24 * @rpx;
}
}
&-title {
font-weight: 500;
font-size: 28 * @rpx;
color: @feedback-list-text-color;
letter-spacing: 0;
padding-bottom: 20 * @rpx;
padding-top: 4 * @rpx;
}
&-list {
display: flex;
flex-direction: column;
&-item {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
.ant-icon,
&-image {
font-size: 36 * @rpx;
width: 36 * @rpx;
height: 36 * @rpx;
margin-right: 16 * @rpx;
}
&-text {
font-weight: 400;
font-size: 26 * @rpx;
color: @feedback-list-text-color;
line-height: 37 * @rpx;
}
}
}
}
}
================================================
FILE: src/Feedback/index.md
================================================
---
nav:
path: /components
group:
title: 反馈引导
order: 15
toc: content
supportPlatform: ['alipay', 'wechat']
---
# Feedback 信息反馈
反馈多作用于分发场景。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-feedback": "antd-mini/es/Feedback/index"
#endif
#if WECHAT
"ant-feedback": "antd-mini/Feedback/index"
#endif
}
```
## 代码示例
### 基本使用
```xml
这里是 popover 内容
```
```js
Page({
data: {
visible: true,
},
onVisibleChange(value) {
#if ALIPAY
this.setData({ visible: value });
#endif
#if WECHAT
this.setData({ visible: value.detail });
#endif
},
});
```
### 点状引导反馈
```xml
```
```js
Page({
data: {
visible: true,
feedList: [
{ icon: 'HeartOutline', text: '喜欢推荐内容', id: '1', },
{ icon: 'FrownOutline', text: '我不感兴趣', id: '2', },
{ image: 'https://gw.alipayobjects.com/mdn/rms_ce4c6f/afts/img/A*XMCgSYx3f50AAAAAAAAAAABkARQnAQ', text: '看过类似内容', id: '3', },
],
},
onVisibleChange(value) {
#if ALIPAY
this.setData({ visible: value });
#endif
#if WECHAT
this.setData({ visible: value.detail });
#endif
},
onTapFeedItem(feedItem) {
#if ALIPAY
my.showToast({ content: `点击了反馈项${feedItem.text}`, });
#endif
#if WECHAT
wx.showToast({ title: `点击了反馈项${feedItem.text}`, });
#endif
},
});
```
### 反馈卡片
```xml
点击 展示反馈卡片
```
```js
Page({
data: {
visible: true,
feedList: [
{ icon: 'HeartOutline', text: '喜欢推荐内容', id: '1', },
{ icon: 'FrownOutline', text: '我不感兴趣', id: '2', },
{ image: 'https://gw.alipayobjects.com/mdn/rms_ce4c6f/afts/img/A*XMCgSYx3f50AAAAAAAAAAABkARQnAQ', text: '看过类似内容', id: '3', },
],
},
onVisibleChange(value) {
#if ALIPAY
this.setData({ visible: value });
#endif
#if WECHAT
this.setData({ visible: value.detail });
#endif
},
onTapFeedItem(feedItem) {
#if ALIPAY
my.showToast({ content: `点击了反馈项${feedItem.text}`, });
#endif
#if WECHAT
wx.showToast({ title: `点击了反馈项${feedItem.text}`, });
#endif
},
onShowFeedback() {
this.setData({
visible: true,
});
},
});
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------ |
| visible | 是否可见 | boolean | false |
| type | 反馈类型 | 'popover' \| 'card' \| 'float' | card |
| title | 反馈的标题 | string | - |
| placement | 气泡框位置,可选 `top`、`top-right`、`top-left`、`bottom`、`bottom-left`、`bottom-right`、`left`、`left-top`、`left-bottom`、`right`、`right-top` 或 `right-bottom` | string | bottom-right |
| list | 反馈的列表 | [FeedItem](#feeditem)[] | - |
| className | 最外层的样式名 | string | - |
| style | 最外层的样式字符串 | string | - |
| popoverType | 气泡反馈的引导样式 | 'circle' \| 'default' | - |
| autoAdjustOverflow | 气泡位置是否自适应 | boolean | true |
| #if ALIPAY onVisibleChange | 展示状态改变的勾子 | (visible: boolean) => void | - |
| #if ALIPAY onTapFeedItem | 反馈项点击回调 | (item: any) => void | - |
| #if WECHAT bindvisiblechange | 展示状态改变的勾子 | (visible: boolean) => void | - |
| #if WECHAT bindtapfeeditem | 反馈项点击回调 | ([FeedItem](#feeditem): any) => void | - |
### FeedItem
| 属性 | 说明 | 类型 | 默认值 |
| ----- | ---------------------------------------- | ------ | ------ |
| icon | 反馈项的图标 | string | - |
| text | 反馈项的文案 | string | - |
| image | 反馈项的图片 和 icon 互斥,优先展示 icon | string | - |
| id | 反馈项的唯一标识 | string | - |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| --feedback-text-color | #333333
| #c5cad1
| 反馈文本颜色 |
| --feedback-background-color | rgba(216, 216, 216, 0.42)
| rgba(216, 216, 216, 0.42)
| 反馈背景颜色 |
| --feedback-mask-color | rgba(0, 0, 0, 0.25)
| rgba(0, 0, 0, 0.25)
| 反馈遮罩颜色 |
| --feedback-content-background-color | #ffffff
| #1a1a1a
| 反馈内容背景颜色 |
| --feedback-list-background-color | #f5f5f5
| #121212
| 反馈列表背景颜色 |
| --feedback-list-text-color | #333333
| #c5cad1
| 反馈列表文本颜色 |
================================================
FILE: src/Feedback/index.ts
================================================
import { resolveEventValue } from '../_util/platform';
import { Component, triggerEvent } from '../_util/simply';
import { assertAilpayNativeNotSupport } from '../_util/support';
import { FeedbackDefaultProps } from './props';
assertAilpayNativeNotSupport('Feedback');
Component({
props: FeedbackDefaultProps,
methods: {
handleVisibleChange(visible, e) {
triggerEvent(this, 'visibleChange', resolveEventValue(visible), e);
},
onTapFeedItem(e) {
const { item } = e.currentTarget.dataset;
triggerEvent(this, 'tapFeedItem', item, e);
},
maskClick(e) {
triggerEvent(this, 'visibleChange', true, e);
},
},
});
================================================
FILE: src/Feedback/props.ts
================================================
import { IBaseProps } from '../_util/base';
/**
* @description 反馈,通过气泡和卡片的形式展示反馈的内容
*/
export interface FeedItem {
/**
* @description 反馈项的图标
*/
icon?: string;
/**
* @description 反馈项的文案
*/
text: string;
/**
* @description 反馈项的图片 和icon互斥 优先展示icon
*/
image?: string;
/**
* @description 反馈项的唯一标识
*/
id?: string;
}
export interface IFeedbackProps extends IBaseProps {
/**
* @description 是否可见
* @default false
*/
visible: boolean;
/**
* @description 反馈类型
* @default card
*/
type: 'popover' | 'card' | 'float';
/**
* @description 反馈的标题
*/
title?: string;
/**
* @description 反馈气泡的位置
* @default bottom-right
*/
placement: string;
/**
* @description 反馈的列表
*/
list?: FeedItem[];
/**
* @description 最外层的样式名
*/
className: string;
/**
* @description 最外层的样式字符串
*/
style: string;
/**
* @description 气泡反馈的引导样式
*/
popoverType: 'circle' | 'default';
/**
* @description 气泡位置是否自适应
* @default true
*/
autoAdjustOverflow;
/**
* 展示状态改变的勾子
* @param visible 反馈内容展示状态
*/
onVisibleChange: (visible) => void;
/**
* 反馈项点击回调
* @param item 点击的反馈项内容
*/
onTapFeedItem: (item) => void;
}
export const FeedbackDefaultProps: Partial = {
className: '',
title: '',
list: [],
type: 'card',
placement: 'bottom-right',
visible: null,
style: '',
popoverType: 'default',
autoAdjustOverflow: true,
onVisibleChange: () => {},
onTapFeedItem: () => {},
};
================================================
FILE: src/Feedback/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
// 字体颜色
@feedback-text-color: var(--feedback-text-color, @COLOR_TEXT_PRIMARY);
// 背景颜色
@feedback-background-color: var(
--feedback-background-color,
rgba(216, 216, 216, 0.42)
);
// mask颜色
@feedback-mask-color: var(--feedback-mask-color, rgba(0, 0, 0, 0.25));
// content颜色
@feedback-content-background-color: var(
--feedback-content-background-color,
@COLOR_CARD_DEFAULT
);
// list背景色
@feedback-list-background-color: var(
--feedback-list-background-color,
@COLOR_BACKGROUND_DEFAULT
);
// list字体颜色
@feedback-list-text-color: var(--feedback-list-text-color @COLOR_TEXT_PRIMARY);
================================================
FILE: src/Footer/index.axml
================================================
================================================
FILE: src/Footer/index.en.md
================================================
---
nav:
path: /components
group:
title: Navigation
order: 6
toc: 'content'
---
# Footer
appears at the bottom of the page and provides the user with additional instructions or assistance beyond the content of the page
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-footer": "antd-mini/es/Footer/index"
#endif
#if WECHAT
"ant-footer": "antd-mini/Footer/index"
#endif
}
```
## Code Sample
### Basic use
> If no split line is needed, pass in `noLabelDivider` property.
```xml
#if ALIPAY
Ant Wealth
#endif
```
### Footer with Content
```xml
#if ALIPAY
©Ant Technology Group Co., Ltd.
#endif
```
### with link footer
```xml
```
```js
Page({
data: {
links: [
{ key: 'alicloud', text: '阿里云' },
{ key: 'alipay', text: '支付宝' }
],
},
handleLinkTap(item) {
#if ALIPAY
if (item.disabled) return;
my.showToast({ content: item.key });
#endif
#if WECHAT
if (item.detail.disabled) return;
wx.showToast({
title: item.detail.key,
});
#endif
},
});
```
### Tagged Footer
```xml
```
```js
Page({
data: {
chips: [
{ key: 'jiebei', text: '蚂蚁借呗' },
{ key: 'beiyongjin', text: '备用金' },
{ key: 'huabei', text: '花呗收钱', disabled: true }
],
},
handleChipTap(item) {
#if ALIPAY
if (item.disabled) return;
my.showToast({ content: item.key });
#endif
#if WECHAT
if (item.detail.disabled) return;
wx.showToast({
title: item.detail.key,
});
#endif
},
});
```
### Demo Code
## API
| Property | Description | Type | Default Value |
| ----------------------- | -------------------- | ---------------------------------------------------- | ------- |
| label | Top content with dividing line | `string` | - |
| content | Common Content Section | `string` | - |
| links | Link | `Array<`[ActionItem](#actionitem)`>` | - |
| chips | Bottom Label | `Array<`[ActionItem](#actionitem)`>` | - |
| noLabelDivider | No dividing line with label | `boolean` | `false` |
| #if ALIPAY onLinkTap | Callback after link click | `(actionItem?: `[ActionItem](#actionitem)`) => void` | - |
| #if ALIPAY onChipTap | Callback after bottom tab click | `(actionItem?: `[ActionItem](#actionitem)`) => void` | - |
| #if WECHAT bindlinktap | Callback after link click | `(actionItem?: `[ActionItem](#actionitem)`) => void` | - |
| #if WECHAT bindchiptap | Callback after bottom tab click | `(actionItem?: `[ActionItem](#actionitem)`) => void` | - |
### ActionItem
| Property | Description | Type | Default Value |
| ---- | -------- | ------ | ------ |
| text | Show copy | string | - |
| key | Unique identification | string | - |
================================================
FILE: src/Footer/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-divider": "../Divider"
}
}
================================================
FILE: src/Footer/index.less
================================================
@import (reference) './variable.less';
.ant-footer {
display: flex;
flex-direction: column;
align-items: center;
.hover {
opacity: 0.4;
}
&-label {
width: 100%;
margin: 20 * @rpx 0;
padding: 0;
&-text {
margin: 0 24 * @rpx;
font-size: 28 * @rpx;
line-height: 40 * @rpx;
color: @footer-color;
}
.ant-divider-horizontal-left,
.ant-divider-horizontal-right {
flex: 1 !important;
border-color: @footer-divider-color;
}
}
&-links {
display: flex;
align-items: center;
margin: 8 * @rpx 0;
&-item {
display: flex;
align-items: center;
font-size: 26 * @rpx;
line-height: 37 * @rpx;
color: @footer-link-color;
.ant-divider-vertical {
height: 24 * @rpx;
}
}
}
&-content {
font-size: 26 * @rpx;
line-height: 37 * @rpx;
color: @footer-color;
margin: 8 * @rpx 0;
}
&-chips {
display: flex;
align-items: center;
margin: 12 * @rpx 0;
&-item {
box-sizing: border-box;
height: 49 * @rpx;
padding: 8 * @rpx 24 * @rpx;
border-radius: 25 * @rpx;
font-size: 24 * @rpx;
line-height: 33 * @rpx;
background: @footer-chip-background-color;
color: @footer-link-color;
margin-left: 40 * @rpx;
&.first {
margin-left: 0;
}
&.disabled {
background: @footer-chip-disabled-background-color;
color: @footer-chip-disabled-color;
}
}
}
}
================================================
FILE: src/Footer/index.md
================================================
---
nav:
path: /components
group:
title: 导航
order: 6
toc: 'content'
---
# Footer 页脚
出现在页面底部,为用户提供页面内容外的额外说明或辅助操作
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-footer": "antd-mini/es/Footer/index"
#endif
#if WECHAT
"ant-footer": "antd-mini/Footer/index"
#endif
}
```
## 代码示例
### 基本使用
> 如果不需要分割线,传入 `noLabelDivider` 属性。
```xml
#if ALIPAY
蚂蚁财富
#endif
```
### 带内容页脚
```xml
#if ALIPAY
© 蚂蚁科技集团股份有限公司
#endif
```
### 带链接页脚
```xml
```
```js
Page({
data: {
links: [
{ key: 'alicloud', text: '阿里云' },
{ key: 'alipay', text: '支付宝' }
],
},
handleLinkTap(item) {
#if ALIPAY
if (item.disabled) return;
my.showToast({ content: item.key });
#endif
#if WECHAT
if (item.detail.disabled) return;
wx.showToast({
title: item.detail.key,
});
#endif
},
});
```
### 带标签页脚
```xml
```
```js
Page({
data: {
chips: [
{ key: 'jiebei', text: '蚂蚁借呗' },
{ key: 'beiyongjin', text: '备用金' },
{ key: 'huabei', text: '花呗收钱', disabled: true }
],
},
handleChipTap(item) {
#if ALIPAY
if (item.disabled) return;
my.showToast({ content: item.key });
#endif
#if WECHAT
if (item.detail.disabled) return;
wx.showToast({
title: item.detail.key,
});
#endif
},
});
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| ----------------------- | -------------------- | ---------------------------------------------------- | ------- |
| label | 带分割线的顶部内容 | `string` | - |
| content | 普通的内容部分 | `string` | - |
| links | 链接 | `Array<`[ActionItem](#actionitem)`>` | - |
| chips | 底部标签 | `Array<`[ActionItem](#actionitem)`>` | - |
| noLabelDivider | 没有 label 的分割线 | `boolean` | `false` |
| #if ALIPAY onLinkTap | 链接点击后的回调 | `(actionItem?: `[ActionItem](#actionitem)`) => void` | - |
| #if ALIPAY onChipTap | 底部标签点击后的回调 | `(actionItem?: `[ActionItem](#actionitem)`) => void` | - |
| #if WECHAT bindlinktap | 链接点击后的回调 | `(actionItem?: `[ActionItem](#actionitem)`) => void` | - |
| #if WECHAT bindchiptap | 底部标签点击后的回调 | `(actionItem?: `[ActionItem](#actionitem)`) => void` | - |
### ActionItem
| 属性 | 说明 | 类型 | 默认值 |
| ---- | -------- | ------ | ------ |
| text | 展示文案 | string | - |
| key | 唯一标识 | string | - |
================================================
FILE: src/Footer/index.ts
================================================
import { Component, triggerEvent } from '../_util/simply';
import { DefaultProps } from './props';
Component({
props: DefaultProps,
methods: {
onTapLink(e) {
const { item } = e.currentTarget.dataset;
triggerEvent(this, 'linkTap', item, e);
},
onTapChip(e) {
const { item } = e.currentTarget.dataset;
triggerEvent(this, 'chipTap', item, e);
},
},
});
================================================
FILE: src/Footer/props.ts
================================================
import { IBaseProps } from '../_util/base';
export type ActionItem = {
key?: string;
text: string;
};
export interface IProps extends IBaseProps {
/**
* @description 带分割线的顶部内容
* @default ''
*/
label?: string;
/**
* @description 普通的内容部分
* @default ''
*/
content?: string;
/**
* @description 链接
* @default undefined
*/
links?: Array;
/**
* @description 底部标签
* @default undefined
*/
chips?: Array;
/**
* @description 没有label的分割线
* @default false
*/
noLabelDivider?: boolean;
/**
* @description 链接点击后的回调
*/
onLinkTap?: (actionItem?: ActionItem) => void;
/**
* @description 底部标签点击后的回调
*/
onChipTap?: (actionItem?: ActionItem) => void;
}
export const DefaultProps: Partial = {
className: null,
label: null,
content: null,
links: null,
chips: null,
noLabelDivider: false,
onLinkTap() {},
onChipTap() {},
};
================================================
FILE: src/Footer/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
@footer-color: var(--footer-color, #cccccc);
@footer-divider-color: var(--footer-divider-color, #eeeeee);
@footer-link-color: var(--footer-link-color, #1677ff);
@footer-chip-background-color: var(--footer-chip-background-color, #f0f3f7);
@footer-chip-disabled-background-color: var(
--footer-chip-disabled-background-color,
#f5f5f5
);
@footer-chip-disabled-color: var(--footer-chip-disabled-color, #999999);
================================================
FILE: src/Form/FormCascaderPicker/index.axml
================================================
-
{{ extra }}
================================================
FILE: src/Form/FormCascaderPicker/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"form-item": "../FormItem/index",
"ant-cascader-picker": "../../Picker/CascaderPicker/index",
"ant-icon": "../../Icon/index"
}
}
================================================
FILE: src/Form/FormCascaderPicker/index.less
================================================
@import (reference) '../variable.less';
.ant-form-cascader-picker {
&-arrow {
margin-left: 8 * @rpx;
color: @form-text-color;
font-size: 36 * @rpx;
}
}
================================================
FILE: src/Form/FormCascaderPicker/index.ts
================================================
import { effect } from '@preact/signals-core';
import { resolveEventValue, resolveEventValues } from '../../_util/platform';
import {
ComponentWithSignalStoreImpl,
getValueFromProps,
triggerEvent,
triggerEventOnly,
triggerEventValues,
} from '../../_util/simply';
import i18nController from '../../_util/store';
import { createForm } from '../form';
import { FormCascaderPickerDefaultProps } from './props';
ComponentWithSignalStoreImpl({
storeOptions: {
store: () => i18nController,
updateHook: effect,
mapState: {
locale: ({ store }) => store.currentLocale.value,
},
},
props: FormCascaderPickerDefaultProps,
methods: {
onOk(value, option, e) {
const v = resolveEventValues(value, option);
this.emit('onChange', v[0]);
triggerEventValues(this, 'ok', v, e);
},
onPickerChange(value, option, e) {
triggerEventValues(
this,
'pickerChange',
resolveEventValues(value, option),
e
);
},
onVisibleChange(visible, e) {
triggerEvent(this, 'visibleChange', resolveEventValue(visible), e);
},
onDismissPicker(e) {
triggerEventOnly(this, 'cancel', e);
},
onChange(value, options, e) {
triggerEventValues(this, 'change', resolveEventValues(value, options), e);
},
handleFormat(value, option) {
const onFormat = getValueFromProps(this, 'onFormat');
if (onFormat) {
return onFormat(value, option);
}
},
},
mixins: [createForm()],
/// #if WECHAT
attached() {
this.setData({
handleFormat: this.handleFormat.bind(this),
});
},
/// #endif
});
================================================
FILE: src/Form/FormCascaderPicker/props.ts
================================================
import {
CascaderDefaultProps,
ICascaderProps,
} from '../../Picker/CascaderPicker/props';
import { FormItemDefaultProps, FormItemProps } from '../FormItem/props';
export interface FormCascaderPickerProps extends ICascaderProps, FormItemProps {
/**
* @description 箭头方向,不传表示没有箭头
*/
arrow: boolean | 'right' | 'up' | 'down';
}
export const FormCascaderPickerDefaultProps: Partial =
{
...FormItemDefaultProps,
...CascaderDefaultProps,
arrow: false,
};
================================================
FILE: src/Form/FormCheckboxGroup/index.axml
================================================
{{formData.value}}
-
{{ extra }}
================================================
FILE: src/Form/FormCheckboxGroup/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"form-item": "../FormItem/index",
"checkbox-group": "../../Checkbox/CheckboxGroup/index"
}
}
================================================
FILE: src/Form/FormCheckboxGroup/index.ts
================================================
import { resolveEventValue } from '../../_util/platform';
import { Component, triggerEvent } from '../../_util/simply';
import { createForm } from '../form';
import { FormCheckboxGroupDefaultProps } from './props';
Component({
props: FormCheckboxGroupDefaultProps,
methods: {
onChange(value, e) {
this.emit('onChange', resolveEventValue(value));
triggerEvent(this, 'change', resolveEventValue(value), e);
},
},
mixins: [createForm()],
});
================================================
FILE: src/Form/FormCheckboxGroup/props.ts
================================================
import { FormItemDefaultProps, FormItemProps } from '../FormItem/props';
import {
CheckboxGroupDefaultProps,
ICheckboxGroupProps,
} from '../../Checkbox/CheckboxGroup/props';
export interface FormCheckboxGroupProps
extends Omit,
FormItemProps {
checkboxPosition?: ICheckboxGroupProps['position'];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { position, ...rest } = CheckboxGroupDefaultProps;
export const FormCheckboxGroupDefaultProps: Partial = {
...FormItemDefaultProps,
...rest,
checkboxPosition: 'horizontal',
};
================================================
FILE: src/Form/FormDatePicker/index.axml
================================================
-
{{ extra }}
================================================
FILE: src/Form/FormDatePicker/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"form-item": "../FormItem/index",
"ant-date-picker": "../../DatePicker/index",
"ant-icon": "../../Icon/index"
}
}
================================================
FILE: src/Form/FormDatePicker/index.less
================================================
@import (reference) '../variable.less';
.ant-form-date-picker {
&-arrow {
margin-left: 8 * @rpx;
color: @form-text-color;
font-size: 36 * @rpx;
}
}
================================================
FILE: src/Form/FormDatePicker/index.ts
================================================
import { effect } from '@preact/signals-core';
import { resolveEventValue, resolveEventValues } from '../../_util/platform';
import {
ComponentWithSignalStoreImpl,
getValueFromProps,
triggerEvent,
triggerEventOnly,
triggerEventValues,
} from '../../_util/simply';
import i18nController from '../../_util/store';
import { createForm } from '../form';
import { FormDatePickerDefaultProps } from './props';
ComponentWithSignalStoreImpl({
storeOptions: {
store: () => i18nController,
updateHook: effect,
mapState: {
locale: ({ store }) => store.currentLocale.value,
},
},
props: FormDatePickerDefaultProps,
methods: {
onOk(date, dateStr, e) {
const v = resolveEventValues(date, dateStr);
/// #if ALIPAY
this.emit('onChange', v[0]);
/// #endif
/// #if WECHAT
this.emit('onChange', v[1]);
/// #endif
triggerEventValues(this, 'ok', v, e);
},
onPickerChange(date, dateStr, e) {
triggerEventValues(
this,
'pickerChange',
resolveEventValues(date, dateStr),
e
);
},
onVisibleChange(visible, e) {
triggerEvent(this, 'visibleChange', resolveEventValue(visible), e);
},
onDismissPicker(e) {
triggerEventOnly(this, 'dismissPicker', e);
},
handleFormat(date, dateStr) {
const onFormat = getValueFromProps(this, 'onFormat');
if (onFormat) {
return onFormat(date, dateStr);
}
},
handleFormatLabel(type, value) {
const onFormatLabel = getValueFromProps(this, 'onFormatLabel');
if (onFormatLabel) {
return onFormatLabel(type, value);
}
},
},
mixins: [createForm()],
/// #if WECHAT
attached() {
this.setData({
handleFormat: this.handleFormat.bind(this),
handleFormatLabel: this.handleFormatLabel.bind(this),
});
},
/// #endif
});
================================================
FILE: src/Form/FormDatePicker/props.ts
================================================
import {
DatePickerDefaultProps,
IDatePickerProps,
} from '../../DatePicker/props';
import { FormItemDefaultProps, FormItemProps } from '../FormItem/props';
export interface FormDatePickerProps extends IDatePickerProps, FormItemProps {
/**
* @description 箭头方向,不传表示没有箭头
*/
arrow: boolean | 'right' | 'up' | 'down';
}
export const FormDatePickerDefaultProps: Partial = {
...FormItemDefaultProps,
...DatePickerDefaultProps,
arrow: false,
};
================================================
FILE: src/Form/FormImageUpload/index.axml
================================================
-
{{ extra }}
================================================
FILE: src/Form/FormImageUpload/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"form-item": "../FormItem/index",
"image-upload": "../../ImageUpload/index"
}
}
================================================
FILE: src/Form/FormImageUpload/index.less
================================================
================================================
FILE: src/Form/FormImageUpload/index.ts
================================================
import { resolveEventValue } from '../../_util/platform';
import { Component, getValueFromProps, triggerEvent } from '../../_util/simply';
import { createForm } from '../form';
import { FormImageUploadDefaultProps } from './props';
Component({
props: FormImageUploadDefaultProps,
methods: {
handleRef(imageUpload) {
/// #if ALIPAY
this.imageUpload = imageUpload;
/// #endif
/// #if WECHAT
this.imageUpload = imageUpload.detail;
/// #endif
},
onChange(value) {
this.emit('onChange', resolveEventValue(value));
triggerEvent(this, 'change', resolveEventValue(value));
},
onPreview(file) {
triggerEvent(this, 'preview', resolveEventValue(file));
},
onChooseImageError(err) {
triggerEvent(this, 'chooseImageError', resolveEventValue(err));
},
handleUpload(localFile) {
const onUpload = getValueFromProps(this, 'onUpload');
if (!onUpload) {
throw new Error('need props onUpload');
}
return onUpload(localFile);
},
handleRemove(file) {
const onRemove = getValueFromProps(this, 'onRemove');
if (onRemove) {
return onRemove(file);
}
},
handleBeforeUpload(localFileList) {
const onBeforeUpload = getValueFromProps(this, 'onBeforeUpload');
if (onBeforeUpload) {
return onBeforeUpload(localFileList);
}
},
},
mixins: [
createForm({
methods: {
setFormData(this: any, values) {
this.setData({
...this.data,
formData: {
...this.data.formData,
...values,
},
});
this.imageUpload && this.imageUpload.update(this.data.formData.value);
},
},
}),
],
/// #if WECHAT
attached() {
this.setData({
handleUpload: this.handleUpload.bind(this),
handleRemove: this.handleRemove.bind(this),
handleBeforeUpload: this.handleBeforeUpload.bind(this),
});
},
/// #endif
});
================================================
FILE: src/Form/FormImageUpload/props.ts
================================================
import { FormItemDefaultProps, FormItemProps } from '../FormItem/props';
import {
IUploaderProps,
UploaderDefaultProps,
} from '../../ImageUpload/props';
export interface FormImageUploadProps
extends Omit,
FormItemProps {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { defaultFileList, ...rest } = UploaderDefaultProps;
export const FormImageUploadDefaultProps: Partial = {
...FormItemDefaultProps,
...rest,
};
================================================
FILE: src/Form/FormInput/index.axml
================================================
{{formData.value}}
-
{{ extra }}
{{ extra }}
================================================
FILE: src/Form/FormInput/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"form-item": "../FormItem/index",
"ant-input": "../../Input/index"
}
}
================================================
FILE: src/Form/FormInput/index.less
================================================
================================================
FILE: src/Form/FormInput/index.ts
================================================
import { resolveEventValue } from '../../_util/platform';
import { Component, triggerEvent } from '../../_util/simply';
import { createForm } from '../form';
import { FormInputDefaultProps } from './props';
Component({
props: FormInputDefaultProps,
methods: {
handleRef(input) {
/// #if ALIPAY
this.input = input;
/// #endif
/// #if WECHAT
this.input = input.detail;
/// #endif
},
onChange(value, e) {
this.emit('onChange', resolveEventValue(value));
triggerEvent(this, 'change', resolveEventValue(value), e);
},
onBlur(value, e) {
this.emit('onChange', resolveEventValue(value));
triggerEvent(this, 'blur', resolveEventValue(value), e);
},
onFocus(value, e) {
triggerEvent(this, 'focus', resolveEventValue(value), e);
},
onConfirm(value, e) {
triggerEvent(this, 'confirm', resolveEventValue(value), e);
},
},
mixins: [
createForm({
methods: {
setFormData(this: any, values) {
this.setData({
...this.data,
formData: {
...this.data.formData,
...values,
},
});
this.input && this.input.update(this.data.formData.value);
},
},
}),
],
});
================================================
FILE: src/Form/FormInput/props.ts
================================================
import { FormItemProps, FormItemDefaultProps } from '../FormItem/props';
import { InputProps, InputDefaultProps } from '../../Input/props';
export interface FormInputProps
extends Omit,
FormItemProps {
inputClassName: string;
inputClassStyle: string;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { value, defaultValue, name, ...rest } = InputDefaultProps;
export const FormInputDefaultProps: FormInputProps = {
...FormItemDefaultProps,
...rest,
inputClassName: '',
inputClassStyle: ' ',
};
================================================
FILE: src/Form/FormInput/variable.less
================================================
@import (reference) '../../style/themes/index.less';
// input 字体颜色
@input-item-color: @COLOR_TEXT_PRIMARY;
// input 字体大小
@input-item-size: @font-size-content;
// input placeholder 颜色
@input-item-placeholder-color: @COLOR_TEXT_WEAK;
// input 清除 颜色
@input-item-clear-color: @COLOR_TEXT_ASSIST;
================================================
FILE: src/Form/FormItem/index.axml
================================================
{{ label }}
{{ tooltip }}
{{ help || errors[0] }}
================================================
FILE: src/Form/FormItem/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"popover": "../../Popover/index",
"ant-icon": "../../Icon/index"
}
}
================================================
FILE: src/Form/FormItem/index.less
================================================
@import (reference) '../variable.less';
@import '../../style/mixins/hairline.less';
.formItemLabelHorizontal() {
width: 170 * @rpx;
margin-right: 24 * @rpx;
color: @form-item-color;
font-size: 34 * @rpx;
line-height: 48 * @rpx;
}
.formItemLabelVertical() {
font-size: 34 * @rpx;
line-height: 42 * @rpx;
color: @form-item-color;
margin-bottom: 8 * @rpx;
}
.formItemContentHorizontal() {
display: flex;
align-items: center;
}
.formItemFieldHorizontal() {
text-align: right;
}
@formItemPrefix: ant-form-item;
.@{formItemPrefix} {
background-color: @form-item-bg;
padding: 24 * @rpx 24 * @rpx 24 * @rpx 32 * @rpx;
position: relative;
.hairline('bottom');
&-line {
display: flex;
flex: 1;
align-items: center;
align-self: stretch;
max-width: 100%;
position: relative;
}
&-content {
flex: 1;
&-horizontal {
.formItemContentHorizontal();
}
}
&-error-info {
font-size: 13px;
color: @form-error-color;
line-height: 37 * @rpx;
text-align: left;
margin-top: 8 * @rpx;
}
&-help-info {
font-size: 13px;
color: @form-item-color;
line-height: 37 * @rpx;
text-align: left;
margin-top: 8 * @rpx;
}
&-label {
display: flex;
align-items: center;
font-size: 34 * @rpx;
line-height: 48 * @rpx;
overflow: visible;
position: relative;
&-text {
display: flex;
align-items: center;
position: relative;
}
&-help-content {
font-size: 30 * @rpx;
text-align: center;
white-space: nowrap;
}
&-horizontal {
.formItemLabelHorizontal();
}
&-vertical {
.formItemLabelVertical();
}
}
&-field {
flex: 1;
&-horizontal {
.formItemFieldHorizontal();
.ant-picker {
width: 100%;
justify-content: flex-end;
}
}
&-vertical {
.ant-picker {
width: 100%;
justify-content: space-between;
}
}
}
// &-other {
// display: flex;
// align-items: center;
// }
&-extra {
display: flex;
align-items: center;
font-size: 32 * @rpx;
color: @form-extra-color;
}
&-asterisk {
.@{formItemPrefix}-label-required .@{formItemPrefix}-label-text {
&::before {
content: '*';
position: absolute;
left: -18 * @rpx;
font-size: 30 * @rpx;
color: @form-asterisk-color;
}
}
}
&-text-required {
.@{formItemPrefix}-label-required .@{formItemPrefix}-label-text {
&::after {
padding-left: 4 * @rpx;
content: '(必填)';
display: block;
left: -18 * @rpx;
font-size: 30 * @rpx;
color: @form-text-color;
}
}
}
&-text-optional {
.@{formItemPrefix}-label .@{formItemPrefix}-label-text {
&::after {
padding-left: 4 * @rpx;
content: '(选填)';
display: block;
left: -18 * @rpx;
font-size: 30 * @rpx;
color: @form-text-color;
}
}
.@{formItemPrefix}-label-required .@{formItemPrefix}-label-text {
&::after {
content: none;
}
}
}
}
================================================
FILE: src/Form/FormItem/index.ts
================================================
import { Component } from '../../_util/simply';
import { FormItemDefaultProps } from './props';
Component({
props: FormItemDefaultProps,
/// #if WECHAT
attached() {
this.triggerEvent('ref', this);
},
/// #endif
});
================================================
FILE: src/Form/FormItem/props.ts
================================================
import { IBaseProps } from '../../_util/base';
import { ValidateStatus } from '../form';
export interface FormItemProps extends IBaseProps {
name: string;
label: string;
labelWidth: number;
position: 'horizontal' | 'vertical';
validateStatus: ValidateStatus;
help: string;
requiredMark: 'asterisk' | 'text-required' | 'text-optional';
status: 'default' | 'success' | 'error' | 'validating';
errors: string[];
tooltip?: string;
required?: boolean;
disabled?: boolean;
extra?: string;
message?: string;
dependencies?: string[];
readonly?: boolean;
}
export const FormItemDefaultProps: FormItemProps = {
name: null,
label: null,
labelWidth: null,
position: 'horizontal',
validateStatus: null,
help: null,
requiredMark: 'asterisk',
status: null,
errors: null,
tooltip: '',
required: false,
disabled: false,
readonly: false,
extra: '',
message: null,
dependencies: null,
};
================================================
FILE: src/Form/FormPicker/index.axml
================================================
-
{{ extra }}
================================================
FILE: src/Form/FormPicker/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"form-item": "../FormItem/index",
"ant-picker": "../../Picker/index",
"ant-icon": "../../Icon/index"
}
}
================================================
FILE: src/Form/FormPicker/index.less
================================================
@import (reference) '../variable.less';
.ant-form-picker {
&-arrow {
margin-left: 8 * @rpx;
color: @form-text-color;
font-size: 36 * @rpx;
}
}
================================================
FILE: src/Form/FormPicker/index.ts
================================================
import { effect } from '@preact/signals-core';
import { resolveEventValue, resolveEventValues } from '../../_util/platform';
import {
ComponentWithSignalStoreImpl,
getValueFromProps,
triggerEvent,
triggerEventOnly,
triggerEventValues,
} from '../../_util/simply';
import i18nController from '../../_util/store';
import { createForm } from '../form';
import { FormPickerDefaultProps } from './props';
ComponentWithSignalStoreImpl({
storeOptions: {
store: () => i18nController,
updateHook: effect,
mapState: {
locale: ({ store }) => store.currentLocale.value,
},
},
props: FormPickerDefaultProps,
methods: {
onOk(value, column, e) {
const v = resolveEventValues(value, column);
this.emit('onChange', v[0]);
triggerEventValues(this, 'ok', v, e);
},
onChange(value, column, e) {
triggerEventValues(this, 'change', resolveEventValues(value, column), e);
},
onVisibleChange(visible, e) {
triggerEvent(this, 'visibleChange', resolveEventValue(visible), e);
},
onDismissPicker(e) {
triggerEventOnly(this, 'cancel', e);
},
handleFormat(value, column) {
const onFormat = getValueFromProps(this, 'onFormat');
if (onFormat) {
return onFormat(value, column);
}
},
},
mixins: [createForm()],
/// #if WECHAT
attached() {
this.setData({
handleFormat: this.handleFormat.bind(this),
});
},
/// #endif
});
================================================
FILE: src/Form/FormPicker/props.ts
================================================
import { IPickerProps, PickerDefaultProps } from '../../Picker/props';
import { FormItemDefaultProps, FormItemProps } from '../FormItem/props';
export interface FormPickerProps extends IPickerProps, FormItemProps {
/**
* @description 箭头方向,不传表示没有箭头
*/
arrow: boolean | 'right' | 'up' | 'down';
}
export const FormPickerDefaultProps: Partial = {
...FormItemDefaultProps,
...PickerDefaultProps,
arrow: false,
};
================================================
FILE: src/Form/FormRadioGroup/index.axml
================================================
{{formData.value}}
-
{{ extra }}
================================================
FILE: src/Form/FormRadioGroup/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"form-item": "../FormItem/index",
"ant-radio-group": "../../Radio/RadioGroup/index"
}
}
================================================
FILE: src/Form/FormRadioGroup/index.ts
================================================
import { resolveEventValue } from '../../_util/platform';
import { Component, triggerEvent } from '../../_util/simply';
import { createForm } from '../form';
import { FormRadioGroupDefaultProps } from './props';
Component({
props: FormRadioGroupDefaultProps,
methods: {
onChange(value, e) {
this.emit('onChange', resolveEventValue(value));
triggerEvent(this, 'change', resolveEventValue(value), e);
},
},
mixins: [createForm()],
});
================================================
FILE: src/Form/FormRadioGroup/props.ts
================================================
import { FormItemDefaultProps, FormItemProps } from '../FormItem/props';
import {
IRadioGroupProps,
RadioGroupDefaultProps,
} from '../../Radio/RadioGroup/props';
export interface FormRadioGroupProps
extends Omit<
IRadioGroupProps,
'value' | 'defaultValue' | 'position' | 'name'
>,
FormItemProps {
radioPosition: IRadioGroupProps['position'];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { value, defaultValue, position, name, ...rest } =
RadioGroupDefaultProps;
export const FormRadioGroupDefaultProps: Partial = {
...FormItemDefaultProps,
...rest,
radioPosition: 'horizontal',
};
================================================
FILE: src/Form/FormRangePicker/index.axml
================================================
-
{{ extra }}
================================================
FILE: src/Form/FormRangePicker/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"form-item": "../FormItem/index",
"ant-range-picker": "../../DatePicker/RangePicker/index",
"ant-icon": "../../Icon/index"
}
}
================================================
FILE: src/Form/FormRangePicker/index.less
================================================
@import (reference) '../variable.less';
.ant-form-range-picker {
&-arrow {
margin-left: 8 * @rpx;
color: @form-text-color;
font-size: 36 * @rpx;
}
}
================================================
FILE: src/Form/FormRangePicker/index.ts
================================================
import { effect } from '@preact/signals-core';
import { resolveEventValue, resolveEventValues } from '../../_util/platform';
import {
ComponentWithSignalStoreImpl,
getValueFromProps,
triggerEvent,
triggerEventOnly,
triggerEventValues,
} from '../../_util/simply';
import i18nController from '../../_util/store';
import { createForm } from '../form';
import { FormRangePickerDefaultProps } from './props';
ComponentWithSignalStoreImpl({
storeOptions: {
store: () => i18nController,
updateHook: effect,
mapState: {
locale: ({ store }) => store.currentLocale.value,
},
},
props: FormRangePickerDefaultProps,
methods: {
onOk(date, dateStr, e) {
const v = resolveEventValues(date, dateStr);
/// #if ALIPAY
this.emit('onChange', v[0]);
/// #endif
/// #if WECHAT
this.emit('onChange', v[1]);
/// #endif
triggerEventValues(this, 'ok', v, e);
},
onPickerChange(type, date, dateStr, e) {
triggerEventValues(
this,
'pickerChange',
resolveEventValues(type, date, dateStr),
e
);
},
onVisibleChange(visible, e) {
triggerEvent(this, 'visibleChange', resolveEventValue(visible), e);
},
onDismissPicker(e) {
triggerEventOnly(this, 'dismissPicker', e);
},
handleFormat(date, dateStr) {
const onFormat = getValueFromProps(this, 'onFormat');
if (onFormat) {
return onFormat(date, dateStr);
}
},
handleFormatLabel(type, value) {
const onFormatLabel = getValueFromProps(this, 'onFormatLabel');
if (onFormatLabel) {
return onFormatLabel(type, value);
}
},
},
mixins: [createForm()],
/// #if WECHAT
attached() {
this.setData({
handleFormat: this.handleFormat.bind(this),
handleFormatLabel: this.handleFormatLabel.bind(this),
});
},
/// #endif
});
================================================
FILE: src/Form/FormRangePicker/props.ts
================================================
import { FormItemDefaultProps, FormItemProps } from '../FormItem/props';
import { IDateRangePickerProps } from '../../DatePicker/RangePicker/props';
import { DateRangePickerDefaultProps } from '../../DatePicker/RangePicker/props';
export interface FormRangePickerProps
extends IDateRangePickerProps,
FormItemProps {
/**
* @description 箭头方向,不传表示没有箭头
*/
arrow: boolean | 'right' | 'up' | 'down';
}
export const FormRangePickerDefaultProps: Partial = {
...FormItemDefaultProps,
...DateRangePickerDefaultProps,
arrow: false,
};
================================================
FILE: src/Form/FormRate/index.axml
================================================
-
{{ extra }}
================================================
FILE: src/Form/FormRate/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"form-item": "../FormItem/index",
"ant-icon": "../../Icon/index",
"rate": "../../Rate/index"
}
}
================================================
FILE: src/Form/FormRate/index.less
================================================
================================================
FILE: src/Form/FormRate/index.ts
================================================
import { resolveEventValue } from '../../_util/platform';
import { Component, triggerEvent } from '../../_util/simply';
import { createForm } from '../form';
import { FormRateDefaultProps } from './props';
Component({
props: FormRateDefaultProps,
methods: {
onChange(value, e) {
this.emit('onChange', resolveEventValue(value));
triggerEvent(this, 'change', resolveEventValue(value), e);
},
},
mixins: [createForm()],
});
================================================
FILE: src/Form/FormRate/props.ts
================================================
import { IRateProps, RateDefaultProps } from '../../Rate/props';
import { FormItemDefaultProps, FormItemProps } from '../FormItem/props';
export interface FormRateProps
extends Omit,
Omit {
rateClassName?: string;
rateStyle?: string;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { defaultValue, ...rest } = RateDefaultProps;
export const FormRateDefaultProps: Partial = {
...FormItemDefaultProps,
...rest,
rateClassName: '',
rateStyle: '',
};
================================================
FILE: src/Form/FormSelector/index.axml
================================================
{{formData.value}}
-
{{ extra }}
================================================
FILE: src/Form/FormSelector/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"form-item": "../FormItem/index",
"selector": "../../Selector/index"
}
}
================================================
FILE: src/Form/FormSelector/index.ts
================================================
import { resolveEventValues } from '../../_util/platform';
import { Component, triggerEventValues } from '../../_util/simply';
import { createForm } from '../form';
import { FormSelectorDefaultProps } from './props';
Component({
props: FormSelectorDefaultProps,
methods: {
onChange(value, item, e) {
const v = resolveEventValues(value, item);
this.emit('onChange', v[0]);
triggerEventValues(this, 'change', v, e);
},
onSelectMax(value, item, e) {
triggerEventValues(this, 'selectMax', resolveEventValues(value, item), e);
},
onSelectMin(value, item, e) {
triggerEventValues(this, 'selectMin', resolveEventValues(value, item), e);
},
},
mixins: [createForm()],
});
================================================
FILE: src/Form/FormSelector/props.ts
================================================
import { FormItemDefaultProps, FormItemProps } from '../FormItem/props';
import { ISelectorProps, SelectorDefaultProps } from '../../Selector/props';
export interface FormSelectorProps extends ISelectorProps, FormItemProps {}
export const FormSelectorDefaultProps: Partial = {
...FormItemDefaultProps,
...SelectorDefaultProps,
};
================================================
FILE: src/Form/FormSlider/index.axml
================================================
{{formData.value}}
-
{{ extra }}
================================================
FILE: src/Form/FormSlider/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"form-item": "../FormItem/index",
"slider": "../../Slider/index"
}
}
================================================
FILE: src/Form/FormSlider/index.less
================================================
================================================
FILE: src/Form/FormSlider/index.ts
================================================
import { resolveEventValue } from '../../_util/platform';
import { Component, triggerEvent } from '../../_util/simply';
import { createForm } from '../form';
import { FormSliderDefaultProps } from './props';
Component({
props: FormSliderDefaultProps,
methods: {
onChange(value, e) {
this.emit('onChange', resolveEventValue(value));
triggerEvent(this, 'change', resolveEventValue(value), e);
},
onAfterChange(value, e) {
triggerEvent(this, 'afterChange', resolveEventValue(value), e);
},
},
mixins: [createForm()],
});
================================================
FILE: src/Form/FormSlider/props.ts
================================================
import { FormItemDefaultProps, FormItemProps } from '../FormItem/props';
import { ISliderProps, sliderDefaultProps } from '../../Slider/props';
export interface FormSliderProps
extends Omit,
FormItemProps {
sliderClassName: string;
sliderStyle: string;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { value, defaultValue, ...rest } = sliderDefaultProps;
export const FormSliderDefaultProps: Partial = {
...FormItemDefaultProps,
...rest,
sliderClassName: '',
sliderStyle: '',
};
================================================
FILE: src/Form/FormSlider/variable.less
================================================
================================================
FILE: src/Form/FormStepper/index.axml
================================================
{{formData.value}}
-
{{ extra }}
================================================
FILE: src/Form/FormStepper/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"form-item": "../FormItem/index",
"stepper": "../../Stepper/index"
}
}
================================================
FILE: src/Form/FormStepper/index.less
================================================
================================================
FILE: src/Form/FormStepper/index.ts
================================================
import { resolveEventValue } from '../../_util/platform';
import { Component, triggerEvent, triggerEventOnly } from '../../_util/simply';
import { createForm } from '../form';
import { FormStepperDefaultProps } from './props';
Component({
props: FormStepperDefaultProps,
methods: {
onChange(value, e) {
this.emit('onChange', resolveEventValue(value));
triggerEvent(this, 'change', resolveEventValue(value), e);
},
onBlur(e) {
triggerEventOnly(this, 'blur', e);
},
onFocus(e) {
triggerEventOnly(this, 'focus', e);
},
onConfirm(value, e) {
triggerEvent(this, 'confirm', resolveEventValue(value), e);
},
},
mixins: [createForm()],
});
================================================
FILE: src/Form/FormStepper/props.ts
================================================
import { FormItemDefaultProps, FormItemProps } from '../FormItem/props';
import { IStepperProps, StepperDefaultProps } from '../../Stepper/props';
export interface FormStepperProps
extends Omit,
FormItemProps {
stepperClassName: string;
stepperStyle: string;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { value, defaultValue, ...rest } = StepperDefaultProps;
export const FormStepperDefaultProps: Partial = {
...FormItemDefaultProps,
...rest,
stepperClassName: '',
stepperStyle: '',
};
================================================
FILE: src/Form/FormStepper/variable.less
================================================
================================================
FILE: src/Form/FormSwitch/index.axml
================================================
-
{{ extra }}
================================================
FILE: src/Form/FormSwitch/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"form-item": "../FormItem/index",
"switch": "../../Switch/index"
}
}
================================================
FILE: src/Form/FormSwitch/index.less
================================================
================================================
FILE: src/Form/FormSwitch/index.ts
================================================
import { resolveEventValue } from '../../_util/platform';
import { Component, triggerEvent } from '../../_util/simply';
import { createForm } from '../form';
import { FormSwitchDefaultProps } from './props';
Component({
props: FormSwitchDefaultProps,
methods: {
onChange(value, e) {
this.emit('onChange', resolveEventValue(value));
triggerEvent(this, 'change', resolveEventValue(value), e);
},
},
mixins: [createForm()],
});
================================================
FILE: src/Form/FormSwitch/props.ts
================================================
import { ISwitchProps, SwitchDefaultProps } from '../../Switch/props';
import { FormItemDefaultProps, FormItemProps } from '../FormItem/props';
export interface FormSwitchProps
extends Omit,
Omit {
switchClassName: string;
switchStyle: string;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { checked, defaultChecked, ...rest } = SwitchDefaultProps;
export const FormSwitchDefaultProps: Partial = {
...FormItemDefaultProps,
...rest,
switchClassName: '',
switchStyle: '',
};
================================================
FILE: src/Form/FormSwitch/variable.less
================================================
@import (reference) '../../style/themes/index.less';
// input 字体颜色
@input-item-color: @COLOR_TEXT_PRIMARY;
// input 字体大小
@input-item-size: @font-size-content;
// input placeholder 颜色
@input-item-placeholder-color: @COLOR_TEXT_WEAK;
// input 清除 颜色
@input-item-clear-color: @COLOR_TEXT_ASSIST;
================================================
FILE: src/Form/FormTextarea/index.axml
================================================
{{formData.value}}
-
{{ extra }}
================================================
FILE: src/Form/FormTextarea/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"form-item": "../FormItem/index",
"ant-textarea": "../../Input/Textarea/index"
}
}
================================================
FILE: src/Form/FormTextarea/index.less
================================================
================================================
FILE: src/Form/FormTextarea/index.ts
================================================
import { effect } from '@preact/signals-core';
import { resolveEventValue } from '../../_util/platform';
import { ComponentWithSignalStoreImpl, triggerEvent } from '../../_util/simply';
import i18nController from '../../_util/store';
import { createForm } from '../form';
import { FormTextareaDefaultProps } from './props';
ComponentWithSignalStoreImpl({
storeOptions: {
store: () => i18nController,
updateHook: effect,
mapState: {
locale: ({ store }) => store.currentLocale.value,
},
},
props: FormTextareaDefaultProps,
methods: {
handleRef(input) {
/// #if ALIPAY
this.input = input;
/// #endif
/// #if WECHAT
this.input = input.detail;
/// #endif
},
onChange(value, e) {
this.emit('onChange', resolveEventValue(value));
triggerEvent(this, 'change', resolveEventValue(value), e);
},
onBlur(value, e) {
triggerEvent(this, 'blur', resolveEventValue(value), e);
},
onFocus(value, e) {
triggerEvent(this, 'focus', resolveEventValue(value), e);
},
onConfirm(value, e) {
triggerEvent(this, 'confirm', resolveEventValue(value), e);
},
onClear(value, e) {
this.emit('onChange', '');
triggerEvent(this, 'change', resolveEventValue(value), e);
},
},
mixins: [
createForm({
methods: {
setFormData(this: any, values) {
this.setData({
...this.data,
formData: {
...this.data.formData,
...values,
},
});
this.input && this.input.update(this.data.formData.value);
},
},
}),
],
});
================================================
FILE: src/Form/FormTextarea/props.ts
================================================
import { FormItemDefaultProps } from './../FormItem/props';
import { FormItemProps } from '../FormItem/props';
import {
TextareaDefaultProps,
TextareaProps,
} from '../../Input/Textarea/props';
export interface FormTextareaProps
extends Omit,
FormItemProps {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { value, defaultValue, name, ...rest } = TextareaDefaultProps;
export const FormTextareaDefaultProps: Partial = {
...FormItemDefaultProps,
...rest,
};
================================================
FILE: src/Form/form.ts
================================================
import AsyncValidator, {
InternalRuleItem,
Rule as RawRule,
RuleItem,
Rules as RawRules,
ValidateError,
ValidateMessages,
Value,
Values,
} from 'async-validator';
import { IMixin4Legacy } from '@mini-types/alipay';
import flattenObject from '../_util/flattenObject';
import get from '../_util/get';
import set from '../_util/set';
import { getValueFromProps } from '../_util/simply';
export { Value, Values };
export type Validator = (
rule: Omit,
value: Value
) => void | Promise;
export type ValidatorRender = (form: Form) => {
validator: Validator;
};
export type FormRuleItem = Omit | ValidatorRender;
export type Rule = FormRuleItem | FormRuleItem[];
export type Rules = Record;
export type FormConfig = {
rules?: Rules;
initialValues?: Values;
validateMessages?: ValidateMessages;
};
export type ValidateStatus = 'default' | 'success' | 'error' | 'validating';
export interface ValidatorStatus {
status: ValidateStatus;
errors: string[];
}
export interface FromItemRef {
setFormData: (values: Values) => void;
getFormData: () => Values;
getProps: () => Record;
on: (callback: (trigger: EventTrigger, value?: Value) => void) => void;
}
export type ValidateTrigger = 'onChange' | 'onBlur' | 'onFocus';
export type EventTrigger =
| ValidateTrigger
| 'didUnmount'
| 'deriveDataFromProps';
class EventEmitter {
public listenders: Record void)[]> = {};
on(event: string, listener: (...args: any) => void) {
this.listenders[event] = this.listenders[event] || [];
this.listenders[event].push(listener);
return this;
}
emit(event: string, ...args: any) {
const arr = this.listenders[event];
if (arr) {
arr.forEach((listener) => listener(...args));
}
}
}
class Field extends EventEmitter {
/**
* Field ref对象
*/
private ref: FromItemRef;
/**
* Field 名称
*/
public name: string;
/**
* 校验器
*/
private validator: AsyncValidator;
/**
* 是否被用户操作过
*/
private touched: boolean;
/**
* required
*/
private required: boolean;
/**
* 触发验证的时机,submit不需要设置也会触发校验器
*/
private validateTrigger: ValidateTrigger[];
private formRules: RawRule;
/**
* Field构建
* @param ref field ref对象
* @param initialValue 初始值
*/
constructor(
ref: FromItemRef,
name: string,
initialValues: Values,
rules: RawRules,
validateMessages: ValidateMessages,
required: boolean,
label: string,
message: string,
validateTrigger: ValidateTrigger
) {
super();
this.ref = ref;
this.formRules = rules;
this.create(
name,
get(initialValues, name),
rules[name],
validateMessages,
required,
label,
message,
validateTrigger
);
this.ref.on(async (trigger, value, extraInfo?: any) => {
if (trigger === 'onChange') {
this.setValue(value);
this.touched = true;
// 触发校验,需要在 onValueChange 之前执行
await Promise.all(
this.validateTrigger.map((item) => {
if (item === trigger) {
return this.validate();
}
})
);
this.emit('valueChange', value);
return;
} else if (trigger === 'didUnmount') {
this.emit('didUnmount');
} else if (trigger === 'deriveDataFromProps') {
const props = extraInfo ? extraInfo : this.ref.getProps();
if (
(value.name && value.name !== props.name) ||
value.required !== props.required ||
value.label !== props.label ||
value.message !== props.message ||
value.validateTrigger !== props.validateTrigger
) {
this.create(
value.name,
get(initialValues, value.name),
this.formRules[value.name],
validateMessages,
value.required,
value.message,
value.label,
value.validateTrigger,
true
);
}
if (value.name !== props.name) {
this.emit('replaceName', value.name);
}
}
this.validateTrigger.forEach((item) => {
if (item === trigger) {
this.validate();
}
});
});
}
create(
name: string,
initialValue: Value,
rule: RawRule,
validateMessages: ValidateMessages,
required: boolean,
label: string,
message: string,
validateTrigger: ValidateTrigger,
update?: boolean
) {
this.name = name;
this.required = this.transformValidatorRules(
name,
rule,
required,
label,
message,
validateMessages
);
if (!update) {
this.reset(initialValue);
} else {
this.ref.setFormData({
required: this.required,
});
}
let validateTriggerList: ValidateTrigger[] | ValidateTrigger =
validateTrigger || 'onChange';
if (!Array.isArray(validateTriggerList)) {
validateTriggerList = [validateTriggerList];
}
this.validateTrigger = validateTriggerList;
}
updateFieldRules(rules: RawRule, validateMessages: ValidateMessages) {
const props = this.ref.getProps();
this.formRules = rules;
this.create(
props.name,
null,
rules[props.name],
validateMessages,
props.required,
props.label,
props.message,
props.validateTrigger,
true
);
}
/**
*
* @param rule 修改 Validator
* @param name
* @param required
* @param message
* @param validateMessages
* @returns
*/
private transformValidatorRules(
name: string,
rule: RawRule,
required: boolean,
label: string,
message: string,
validateMessages: ValidateMessages
) {
let requiredRule = false;
let validator: AsyncValidator;
if (rule) {
const ruleList = Array.isArray(rule) ? rule : [rule];
const result = ruleList.find((item) => item.required);
if (result) {
if (message) {
result.message = message;
}
requiredRule = true;
} else if (required) {
ruleList.push({
required,
// message 不允许为 null
message: message ?? undefined,
});
requiredRule = true;
}
validator = new AsyncValidator({
[name]: ruleList,
});
} else if (required) {
validator = new AsyncValidator({
[name]: {
required,
// message 不允许为 null
message: message ?? undefined,
},
});
requiredRule = true;
}
if (validator) {
const obj = {
label,
};
Object.keys(validator.rules).forEach((name) => {
validator.rules[name].forEach((item) => {
if (typeof item.len !== 'undefined') {
obj['len'] = item.len;
}
if (typeof item.min !== 'undefined') {
obj['min'] = item.min;
}
if (typeof item.max !== 'undefined') {
obj['max'] = item.max;
}
if (typeof item.pattern !== 'undefined') {
obj['pattern'] = item.pattern;
}
});
});
validator.messages(this.transformValidateMessages(validateMessages, obj));
}
this.validator = validator;
return requiredRule;
}
private transformValidateMessages(
validateMessages: ValidateMessages,
obj: Partial<{
label: string;
len: number;
min: number;
max: number;
pattern: RegExp | string;
}>
) {
if (!validateMessages) {
return;
}
function replaceLabel(
validateMessages: ValidateMessages,
target: ValidateMessages
) {
Object.keys(validateMessages).forEach((item) => {
if (typeof validateMessages[item] === 'string') {
target[item] = validateMessages[item].replace(
'${label}',
obj.label || ''
);
if (typeof obj.len !== 'undefined') {
target[item] = target[item].replace('${len}', obj.len);
}
if (typeof obj.min !== 'undefined') {
target[item] = target[item].replace('${min}', obj.min);
}
if (typeof obj.max !== 'undefined') {
target[item] = target[item].replace('${max}', obj.max);
}
if (typeof obj.pattern !== 'undefined') {
target[item] = target[item].replace('${pattern}', obj.pattern);
}
return;
}
if (typeof validateMessages[item] === 'object') {
const val = (target[item] = {});
replaceLabel(validateMessages[item], val);
return;
}
target[item] = validateMessages[item];
});
}
const messages: ValidateMessages = {};
replaceLabel(validateMessages, messages);
return messages;
}
/**
* 设置 Field 值
* @param value Field 值
*/
setValue(value: Value) {
this.ref.setFormData({
value,
});
}
/**
* 得到 Field 值
*/
getValue() {
const value = this.ref.getFormData().value;
return value;
}
/**
* 设置 Field 校验器状态
* @param validatorStatue
*/
setValidatorStatus(validatorStatue: ValidatorStatus) {
this.ref.setFormData(validatorStatue);
}
/**
* 得到 Field 校验器状态
* @returns
*/
getValidatorStatus(): ValidatorStatus {
const { status, errors } = this.ref.getFormData();
return {
status,
errors,
};
}
/**
* 校验 Field
*/
async validate() {
const validatorStatusSuccess: ValidatorStatus = {
status: 'success',
errors: [] as string[],
};
const value = this.getValue();
if (!this.validator) {
this.setValidatorStatus(validatorStatusSuccess);
return {
validatorStatus: validatorStatusSuccess,
value,
};
}
const validator = this.validator;
try {
let needUpdateStatus = true;
Promise.resolve().then(() => {
Promise.resolve().then(() => {
if (needUpdateStatus) {
this.setValidatorStatus({
status: 'validating',
errors: [],
});
}
});
});
await this.validator.validate(
{
[this.name]: value,
},
() => {
needUpdateStatus = false;
}
);
if (validator !== this.validator) {
return;
}
this.setValidatorStatus(validatorStatusSuccess);
return {
validatorStatus: validatorStatusSuccess,
value,
};
} catch (err) {
if (validator !== this.validator) {
return;
}
const errors: ValidateError[] = err.errors;
const validatorStatus: ValidatorStatus = {
status: 'error',
errors: errors.map(({ message = '' }) => message),
};
this.setValidatorStatus(validatorStatus);
return {
validatorStatus,
value,
};
}
}
/**
* 重置 Field
* @param initialValue
*/
reset(initialValue: Value) {
this.touched = false;
this.ref.setFormData({
value: initialValue,
required: this.required,
status: 'default',
errors: [],
});
}
/**
* Field 是否被操作
*/
isTouched() {
return this.touched;
}
}
export class Form {
/**
* 表单初始值
*/
private initialValues: Values;
/**
* 原始规则,用户传入的跟之前的规则区别在于validator
*/
private rules: RawRules;
/**
* 表单ref组件对象
*/
private fields: Record = {};
/**
* 验证提示模板
*/
private validateMessages: ValidateMessages;
/**
* 表单字段 change侦听
*/
private changeListeners: ((
changedValues: Values,
allValues: Values
) => void)[] = [];
/**
* 依赖表
*/
private dependenciesMap: Record = {};
/**
* Form构建
* @param formConfig 表单配置项
*/
constructor(formConfig: FormConfig = {}) {
this.setInitialValues(formConfig.initialValues || {});
this.setRules(formConfig.rules || {});
this.validateMessages = formConfig.validateMessages;
}
/**
* 用户传入的rules转换成async-validator rules
* @param rules 校验规则
*/
private transformRules(rules: Rules) {
const result: RawRules = {};
Object.keys(rules).forEach((name) => {
const rule = rules[name];
const list = (result[name] = []);
const arr = Array.isArray(rule) ? rule : [rule];
arr.forEach((item) => {
if (typeof item === 'function') {
list.push(item(this).validator);
} else {
list.push({
...item,
});
}
});
});
return result;
}
/**
* 遍历表单field对象
* @param callback
*/
private eachField(callback: (field: Field, name: string) => void) {
const fields = this.fields;
Object.keys(fields).forEach((name) => {
const field = fields[name];
callback(field, name);
});
}
/**
* 更新 rules
* @param rules
*/
updateRules(rules: Rules) {
const rawRules = this.transformRules(rules);
this.rules = rawRules;
Object.keys(this.fields).forEach((name) => {
this.fields[name].updateFieldRules(rawRules, this.validateMessages);
});
}
/**
* 设置 rules
* @param rules
*/
private setRules(rules: Rules) {
this.rules = this.transformRules(rules);
}
/**
* 添加表单对象
* @param ref 表单ref对象
*/
addItem(ref: FromItemRef, customName?: string) {
const props = ref.getProps();
let name = customName || props.name;
if (!name) {
ref.on((trigger, value) => {
if (trigger === 'deriveDataFromProps') {
if (value.name) {
this.addItem(ref, value.name);
}
}
});
return;
}
if (this.fields[name]) {
throw new Error(`Form "addItem" same name: "${name}"`);
}
const field = new Field(
ref,
name,
this.initialValues,
this.rules,
this.validateMessages,
props.required,
props.label,
props.message,
props.validateTrigger
);
if (props.dependencies) {
props.dependencies.forEach((item) => {
this.dependenciesMap[item] = this.dependenciesMap[item] || [];
if (this.dependenciesMap[item].indexOf(name) < 0) {
this.dependenciesMap[item].push(name);
}
});
}
field
.on('valueChange', (value) => {
if (name) {
const arr = this.dependenciesMap[name];
if (arr) {
arr.forEach((item) => {
if (this.fields[item]) {
this.fields[item].validate();
}
});
}
this.changeListeners.forEach((item) =>
item(set({}, name, value), this.getFieldsValue())
);
}
})
.on('didUnmount', () => {
delete this.fields[name];
})
.on('replaceName', (newName) => {
if (!newName) {
delete this.fields[name];
return;
}
if (this.fields[newName]) {
throw new Error(`Form "addItem" same name: "${newName}"`);
}
this.fields[newName] = field;
delete this.fields[name];
name = newName;
});
if (name) {
this.fields[name] = field;
}
}
/**
* 设置表单值
* @param name 表单名称
* @param value 表单初始值
*/
setFieldValue(name: string, value: Value) {
const field = this.fields[name];
if (field) {
field.setValue(value);
field.setValidatorStatus({
status: 'success',
errors: [],
});
}
}
/**
* 设置表单值
* @param name 表单名称
* @param value 表单初始值
*/
setFieldsValue(values: Values) {
Object.keys(flattenObject(values)).forEach((name) => {
this.setFieldValue(name, get(values, name));
});
}
/**
* 设置 initialValues,这个操作不会对页面进行修改,要是需要重置表单可跟上 reset 方法;
* 这样是对于表单已经在编辑,但是需要重新initialValues的场景
*
* eg:
* this.setInitialValues(initialValues);
* this.reset();
*
* @param initialValues
*/
setInitialValues(initialValues: Values) {
this.initialValues = initialValues;
}
/**
* 获取对应字段名的值
* @param name
* @returns
*/
getFieldValue(name: string) {
const field = this.fields[name];
if (!field) {
return;
}
return field.getValue();
}
/**
* 获取一组字段名对应的值
* @param nameList
* @returns
*/
getFieldsValue(nameList?: string[]) {
const fieldsValue: Values = {};
nameList = nameList || Object.keys(this.fields);
nameList.forEach((name) => {
set(fieldsValue, name, this.getFieldValue(name));
});
return fieldsValue;
}
/**
* 获取对应字段名的校验器状态
* @param name
* @returns
*/
getFieldValidatorStatus(name: string) {
const field = this.fields[name];
if (!field) {
return;
}
return field.getValidatorStatus();
}
/**
* 获取一组字段名的校验器状态
* @param nameList
* @returns
*/
getFieldsValidatorStatus(nameList?: string[]) {
const fieldsValidatorStatus: Record = {};
nameList = nameList || Object.keys(this.fields);
nameList.forEach((name) => {
fieldsValidatorStatus[name] = this.getFieldValidatorStatus(name);
});
return fieldsValidatorStatus;
}
/**
* 设置对应字段名的校验器状态
* @param name 表单名称
* @param validatorStatus 校验状态
* @returns
*/
setFieldValidatorStatus(name: string, validatorStatus: ValidatorStatus) {
const field = this.fields[name];
if (!field) {
return;
}
return field.setValidatorStatus(validatorStatus);
}
/**
* 设置一组字段名的校验器状态
* @param fieldsValidatorStatus 表单校验状态
* @returns
*/
setFieldsValidatorStatus(
fieldsValidatorStatus: Record
) {
Object.keys(fieldsValidatorStatus).forEach((name) => {
this.setFieldValidatorStatus(name, fieldsValidatorStatus[name]);
});
}
/**
* 检查对应字段是否被用户操作过
* @param name 字段名称
* @returns
*/
isFieldTouched(name: string) {
const field = this.fields[name];
if (!field) {
return false;
}
return field.isTouched();
}
/**
* 指定表单字段值更新时触发回调方法
* @param name 表单字段名称
* @param callback 回调方法
*/
onValueChange(
name: string,
callback: (value: Value, allValues: Values) => void
) {
this.changeListeners.push((changedValues: Values, allValues: Values) => {
if (get(changedValues, name)) {
callback(get(changedValues, name), allValues);
}
});
}
/**
* 表单字段值更新时触发回调方法
* @param name 表单字段名称
* @param callback 回调方法
*/
onValuesChange(callback: (changedValues: Values, allValues: Values) => void) {
this.changeListeners.push((changedValues: Values, allValues: Values) => {
callback(changedValues, allValues);
});
}
/**
* 表单提交
*/
async submit() {
const values: Values = {};
const arr: Promise<{
validatorStatus: ValidatorStatus;
value: Value;
name: string;
}>[] = [];
this.eachField((field, name) => {
arr.push(
(async () => {
return {
...(await field.validate()),
name,
};
})()
);
});
const result = await Promise.all(arr);
const errorFields: {
name: string;
errors: string[];
}[] = [];
result.forEach((obj) => {
if (!obj) {
return;
}
const { name, value, validatorStatus } = obj;
if (validatorStatus.status === 'error') {
errorFields.push({
name,
errors: validatorStatus.errors,
});
}
set(values, name, value);
});
if (errorFields.length > 0) {
throw {
values,
errorFields,
};
}
return values;
}
/**
* 表单重置
*/
reset() {
this.eachField((field, name) => {
const initialValue = get(this.initialValues, name);
field.reset(initialValue);
});
}
}
export function createForm({ methods = {} } = {}) {
let mixin = {
data: {
formData: {
value: undefined,
status: 'default',
errors: [],
},
},
/// #if ALIPAY
didUnmount() {
this.emit('didUnmount');
},
deriveDataFromProps(nextProps) {
this.emit('deriveDataFromProps', nextProps);
},
/// #endif
/// #if WECHAT
attached() {
this.triggerEvent('ref', this);
},
detached() {
this.emit('didUnmount');
},
observers: {
'**': function (nextProps) {
this.emit('deriveDataFromProps', nextProps);
},
},
/// #endif
methods: {
emit(trigger: EventTrigger, value?: Value) {},
setFormData(values: Values) {
this.setData({
...this.data,
formData: {
...this.data.formData,
...values,
},
});
},
getFormData() {
return this.data.formData;
},
on(callback: (trigger: EventTrigger, value?: Value) => void) {
this.emit = callback;
},
getProps() {
return getValueFromProps(this);
},
...methods,
},
} as IMixin4Legacy<
{
formData: {
value: Value;
} & ValidatorStatus;
},
Record,
{
emit(trigger: EventTrigger, value?: Value): void;
setFormData(values: Values): void;
getFormData(): {
value: Value;
} & ValidatorStatus;
on(callback: (trigger: EventTrigger, value?: Value) => void): void;
getProps: Record;
}
>;
/// #if WECHAT
// @ts-ignore
mixin = Behavior(mixin);
/// #endif
return mixin;
}
================================================
FILE: src/Form/index.en.md
================================================
---
nav:
path: /components
group:
title: Information Entry
order: 12
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# Form
The Form form contains data entry, validation, and corresponding styles. The Form component requires [component2](https://opendocs.alipay.com/mini/framework/custom-component-overview) Support.
- Used to create entities or collect information.
- When the input data type needs to be verified.
## Introduction
> Take the input box as an example
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"form-input": "antd-mini/es/Form/FormInput/index"
#endif
#if WECHAT
"form-input": "antd-mini/Form/FormInput/index"
#endif
}
```
The logic layer registers the input box component ref into the Form
```xml
```
```js
#if ALIPAY
import { Form } from 'antd-mini/es/Form/form';
#endif
#if WECHAT
import { Form } from 'antd-mini/Form/form';
#endif
Page({
handleRef(ref) {
#if ALIPAY
this.form.addItem(ref);
#endif
#if WECHAT
if (!this.formRefList) {
this.formRefList = [];
}
this.formRefList.push(ref.detail);
#endif
},
});
```
## Code Sample
### Basic use
### Layout
### Initial value
### Initial value asynchronous loading
### Form listening
### Calibration
### Add Delete Form Item
### Form Item Linkage
### Custom Form Item Validation
### Custom Form Validation Message
### Multiple Forms
### Form Item Read Only
### Picture verification
### JSON Generate Form
### Custom Error Styles
Use `validateStatus: success` and `footer slot` Customize the error style.
### Custom Form Item
by using [FormItem](#formitem)、[createForm](#createform) Customizable form items. In the example `form-checklist`、`form-location` For custom form item components.
### Demo Code
## API
### FormItem
Properties Included in All Form Components
| Property | Description | Type | Default Value |
| -----|-----|-----|-----|
| dependencies | Set Dependent Fields, View[Detailed Description](#dependencies) | string[] | - |
| footer | Bottom slot, receiving errors, status | slot | - |
| name | Name | string | - |
| label | Text | string | - |
| labelWidth | Text Width | string | - |
| position | layout, optional `horizontal` `vertical` | string | horizontal |
| validateStatus | The verification status. If it is not set, it will be automatically generated according to the verification rules. Optional `default` `success` `error` `validating` | string | - |
| help | Prompt information, if not set, will be automatically generated according to the verification rules | string | - |
| header | Top slot, receiving errors, status | slot | - |
| tooltip | Form Item Prompt Information | string\|slot | - |
| required | Required style settings. If it is not set, it will be automatically generated according to the verification rules. | boolean | false |
| message | Verify the error message. If it is not set, it will be automatically generated according to the verification rules. | string | false |
| requiredMark | Required optional tag style, optional `asterisk` `text-required` `text-optional` | string | asterisk |
### Form
| Property | Description | Type |
| ---------------- | -------------- | ----------------------------------------- |
| rules | Optional, Validation Rules | View[rules](#rules) |
| initialValues | Optional, initial value | Record |
| validateMessages | Optional, Verify Message | View[validateMessages](#validatemessages) |
### Form instance method
| Property | Description | Type |
| ------------------------ | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| addItem | Add Form Item | (formItem: Ref) => void |
| updateRules | To update the verification rules of the form, you need to pass in the full number of rules each time. | (rules: Rules) => void, the type of Rules can be viewed[rules](#rules) |
| getFieldValue | Get the value of a form item | (name: string) => any |
| getFieldsValue | Gets the value for a set of field names. If no nameList is passed, all fields pairs are returned. | (nameList?: string[]) => Record |
| getFieldValidatorStatus | Get form check status | (name: string) => [ValidatorStatus](#validatorstatus) |
| getFieldsValidatorStatus | Get a set of form validation states. If no nameList is passed, all fields pairs are returned. | (nameList?: string[]) => Record |
| reset | Reset form to initial value | () => void |
| isFieldTouched | Determine whether a form item has been modified | () => boolean |
| onValueChange | Listen for the value modification of the specified form item, view[Detailed Description](#onvaluechangeonvalueschange) | (name: string, (changedValue: any, allValues: Record) => void) => void |
| onValuesChange | Listen for the value modification of a form item, view[Detailed Description](#onvaluechangeonvalueschange) | ((changedValues: Record, allValues: Record) => void) => void |
| setFieldValue | Set the value of a form item | (name: string, value: any) => void; |
| setFieldsValue | Set the value of a form item | (values: Record) => void; |
| setFieldValidatorStatus | Set Form Verification Status | (name: string, validatorStatus: [ValidatorStatus](#validatorstatus)) => void |
| setFieldsValidatorStatus | Set a set of form validation states | (fieldsValidatorStatus: Record) => void |
| setInitialValues | Set Form Initial Values | (initialValues: Record) => void |
| submit | Submit the form, return the promise form value, and the verification error will be thrown. | () => Promise> |
### dependencies
Used when there is a dependency between fields. For example, the "Password" and "Confirm Password" fields of the registered user form, where the "Confirm Password" verification depends on the "Password" field. Setup `dependencies` After that, the "Password" field update will automatically trigger the "Confirm Password" verification.
### rules
Example:
```javascript
{
account: [
{
required: true,
message: '需要输入用户名'
},
],
password: [
{
required: true,
},
],
confirm: [
{
required: true,
message: '需要输入确认密码'
},
(form) => ({
async validator(_, value) {
if (!value || form.getFieldValue('password') === value) {
return;
}
throw new Error('两次密码需一致');
},
}),
]
}
```
`rules` can be in `new Form` can also be set in `FormItem` By `required` or `message` Property settings.
```html
```
### validateMessages
can refer [Asynchronous validator](https://github.com/yiminghe/async-validator/blob/master/src/messages.ts#L4-L55) message,antd-mini added on this basis. `${label}`,`${len}`,`${min}`,`${max}`,`${pattern}`。
Example:
```javascript
{
required: '需要输入${label}',
string: {
min: '${label}最少${min}个字符',
},
pattern: {
mismatch: '${label}需要满足${pattern}模式',
},
}
```
### onValueChange and onValuesChange
`setFieldValue` and `setFieldsValue` Will not trigger `onValueChange` and `onValuesChange`。`onValueChange` and `onValuesChange` It is only triggered when a user action is taken. If in `setFieldValue` or `setFieldsValue` then want to trigger `onValueChange` or `onValuesChange`you need to call these methods manually.
**Example**:
```js
const onValuesChangeCallback = (changedValues) => {
console.log(changedValues);
};
this.form.onValuesChange(onValuesChangeCallback);
this.form.setFieldValue(name, value);
onValuesChangeCallback({
[name]: value,
});
```
### ValidatorStatus
```js
type ValidatorStatus = {
status: 'default' | 'success' | 'error' | 'validating',
errors: string[],
};
```
### submit checksum throws error
```js
{
values: Record,
errorFields: {
name: string;
errors: string[];
}[]
}
```
### FormInput
with `Input` Same.
### FormTextarea
with `Textarea` Same.
### FormSwitch
with `Switch` Same.
### FormStepper
with `Stepper` Same, but with the following added attributes:
| Property | Description | Type | Default Value |
| ---------------- | --------------------------------- | ------ | ------ |
| stepperClassName | Corresponding `Stepper` Components `className` | string | - |
| stepperStyle | Corresponding `Stepper` Components `style` | string | - |
### FormCheckGroup
with `CheckGroup` Same, but with the following added attributes:
| Property | Description | Type | Default Value |
| ---------------- | ----------------------------------- | ------ | ------ |
| checkboxLabel | Corresponding `CheckGroup` Components `label` | slot | - |
| checkboxPosition | Corresponding `CheckGroup` Components `position` | string | - |
### FormRadioGroup
with `RadioGroup` Same, but with the following added attributes:
| Property | Description | Type | Default Value |
| ------------- | ----------------------------------- | ------ | ------ |
| radioLabel | Corresponding `RadioGroup` Components `label` | slot | - |
| radioPosition | Corresponding `RadioGroup` Components `position` | string | - |
### FormPicker
with `Picker` Same, but with the following added attributes:
| Property | Description | Type | Default Value |
| ----- | -------------------------------------------------------- | ----------------- | ------ |
| arrow | right arrow, optional `right`、`up`、`down`, pass true `right` | string \| boolean | - |
### FormDatePicker
with `DatePicker` Same, but with the following added attributes:
| Property | Description | Type | Default Value |
| ----- | -------------------------------------------------------- | ----------------- | ------ |
| arrow | right arrow, optional `right`、`up`、`down`, pass true `right` | string \| boolean | - |
### FormRangePicker
with `RangePicker` Same, but with the following added attributes:
| Property | Description | Type | Default Value |
| ----- | -------------------------------------------------------- | ----------------- | ------ |
| arrow | right arrow, optional `right`、`up`、`down`, pass true `right` | string \| boolean | - |
### FormCascaderPicker
with `CascaderPicker` Same, but with the following added attributes:
| Property | Description | Type | Default Value |
| ----- | -------------------------------------------------------- | ----------------- | ------ |
| arrow | right arrow, optional `right`、`up`、`down`, pass true `right` | string \| boolean | - |
### FormSlider
with `Slider` Same.
### FormSelector
with `Selector` Same.
### FormImageUpload
with `ImageUpload` Same.
### createForm
`createForm` is a `mixin`for custom form items.
```js
import { createForm } from 'antd-mini/es/Form/form';
Component({
mixins: [createForm()],
methods: {
onChange(value) {
this.emit('onChange', value);
},
},
});
```
`createForm` The following is added to the component:
- `data`
```js
{
formData: {
value: undefined, // 表单项的值
status: 'default', // 表单项的Calibration状态,包括 `default`、`success`、`error`、`validating`
errors: [], // 错误信息
},
}
```
- `methods`
```js
// 修改表单项时,需调用 `emit` 方法。Custom Form Item组件In值改变时,应该调用此方法。
function emit(trigger: 'onChange' | 'onBlur' | 'onFocus', value: any): void;
```
For more methods, please refer `createForm` Method-related documentation. Use `formData` and `emit` The implementation of the custom form item is complete.
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| --------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------- |
| --form-text-color | #cccccc
| #474747
| Form text color |
| --form-item-color | #666666
| #808080
| Form Item Color |
| --form-item-bg | #ffffff
| #1a1a1a
| Form Item Background Color |
| --form-error-color | #ff3141
| #ff4a58
| Form Error Color |
| --form-extra-color | #999999
| #616161
| Form extra information color |
| --form-asterisk-color | #ff3b30
| #ff3b30
| Form asterisk color |
================================================
FILE: src/Form/index.md
================================================
---
nav:
path: /components
group:
title: 数据录入
order: 12
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# Form 表单
Form 表单包含数据录入、校验以及相应的样式。Form 组件需要 [component2](https://opendocs.alipay.com/mini/framework/custom-component-overview) 支持。
- 用于创建实体或收集信息。
- 需要对输入的数据类型进行校验时。
## 引入
> 以输入框为例
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"form-input": "antd-mini/es/Form/FormInput/index"
#endif
#if WECHAT
"form-input": "antd-mini/Form/FormInput/index"
#endif
}
```
逻辑层将输入框组件 ref 注册到 Form 中
```xml
```
```js
#if ALIPAY
import { Form } from 'antd-mini/es/Form/form';
#endif
#if WECHAT
import { Form } from 'antd-mini/Form/form';
#endif
Page({
handleRef(ref) {
#if ALIPAY
this.form.addItem(ref);
#endif
#if WECHAT
if (!this.formRefList) {
this.formRefList = [];
}
this.formRefList.push(ref.detail);
#endif
},
});
```
## 代码示例
### 基本使用
### 布局
### 初始值
### 初始值异步加载
### 表单侦听
### 校验
### 增加删除表单项
### 表单项联动
### 自定义表单项校验
### 自定义表单校验消息
### 多个表单
### 表单项只读
### 图片校验
### JSON 生成表单
### 自定义错误样式
使用 `validateStatus: success` 及 `footer slot` 来自定义错误样式。
### 自定义表单项
通过使用 [FormItem](#formitem)、[createForm](#createform) 可自定义表单项。示例里 `form-checklist`、`form-location` 为自定义表单项组件。
### Demo 代码
## API
### FormItem
所有 Form 组件都包括的属性
| 属性 | 说明 | 类型 | 默认值 |
| -----|-----|-----|-----|
| dependencies | 设置依赖字段,查看[详细说明](#dependencies) | string[] | - |
| footer | 底部 slot,接收 errors、status | slot | - |
| name | 名称 | string | - |
| label | 文本 | string | - |
| labelWidth | 文本宽度 | string | - |
| position | 布局,可选 `horizontal` `vertical` | string | horizontal |
| validateStatus | 校验状态,如不设置,则会根据校验规则自动生成,可选 `default` `success` `error` `validating` | string | - |
| help | 提示信息,如不设置,则会根据校验规则自动生成 | string | - |
| header | 顶部 slot,接收 errors、status | slot | - |
| tooltip | 表单项提示信息 | string\|slot | - |
| required | 必填样式设置。如不设置,则会根据校验规则自动生成 | boolean | false |
| message | 校验错误信息。如不设置,则会根据校验规则自动生成 | string | false |
| requiredMark | 必填选填的标记样式,可选 `asterisk` `text-required` `text-optional` | string | asterisk |
### Form
| 属性 | 说明 | 类型 |
| ---------------- | -------------- | ----------------------------------------- |
| rules | 可选,校验规则 | 查看[rules](#rules) |
| initialValues | 可选,初始值 | Record |
| validateMessages | 可选,校验消息 | 查看[validateMessages](#validatemessages) |
### Form 实例方法
| 属性 | 说明 | 类型 |
| ------------------------ | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| addItem | 添加表单项 | (formItem: Ref) => void |
| updateRules | 更新 form 的校验规则,每次都需要传入全量的 rules | (rules: Rules) => void, Rules 的类型可以查看[rules](#rules) |
| getFieldValue | 得到表单项的值 | (name: string) => any |
| getFieldsValue | 获取一组字段名对应的值。不传 nameList 则返回全部 fields 对 | (nameList?: string[]) => Record |
| getFieldValidatorStatus | 得到表单校验状态 | (name: string) => [ValidatorStatus](#validatorstatus) |
| getFieldsValidatorStatus | 得到一组表单校验状态。不传 nameList 则返回全部 fields 对 | (nameList?: string[]) => Record |
| reset | 重置表单为初始值 | () => void |
| isFieldTouched | 判断表单项是否被修改过 | () => boolean |
| onValueChange | 侦听指定表单项的值修改,查看[详细说明](#onvaluechangeonvalueschange) | (name: string, (changedValue: any, allValues: Record) => void) => void |
| onValuesChange | 侦听表单项的值修改,查看[详细说明](#onvaluechangeonvalueschange) | ((changedValues: Record, allValues: Record) => void) => void |
| setFieldValue | 设置表单项的值 | (name: string, value: any) => void; |
| setFieldsValue | 设置表单项的值 | (values: Record) => void; |
| setFieldValidatorStatus | 设置表单校验状态 | (name: string, validatorStatus: [ValidatorStatus](#validatorstatus)) => void |
| setFieldsValidatorStatus | 设置一组表单校验状态 | (fieldsValidatorStatus: Record) => void |
| setInitialValues | 设置表单初始值 | (initialValues: Record) => void |
| submit | 提交表单,返回 promise 表单值,校验错误会抛出 | () => Promise> |
### dependencies
当字段间存在依赖关系时使用。例如注册用户表单的“密码”与“确认密码”字段,其中“确认密码”校验依赖于“密码”字段。设置 `dependencies` 后,“密码”字段更新将自动触发“确认密码”的校验。
### rules
示例:
```javascript
{
account: [
{
required: true,
message: '需要输入用户名'
},
],
password: [
{
required: true,
},
],
confirm: [
{
required: true,
message: '需要输入确认密码'
},
(form) => ({
async validator(_, value) {
if (!value || form.getFieldValue('password') === value) {
return;
}
throw new Error('两次密码需一致');
},
}),
]
}
```
`rules` 可以在 `new Form` 中设置,也可以在 `FormItem` 通过 `required` 或 `message` 属性设置。
```html
```
### validateMessages
可以参考 [Asynchronous validator](https://github.com/yiminghe/async-validator/blob/master/src/messages.ts#L4-L55) 的 message,antd-mini 在此基础上加了 `${label}`,`${len}`,`${min}`,`${max}`,`${pattern}`。
示例:
```javascript
{
required: '需要输入${label}',
string: {
min: '${label}最少${min}个字符',
},
pattern: {
mismatch: '${label}需要满足${pattern}模式',
},
}
```
### onValueChange 和 onValuesChange
`setFieldValue` 和 `setFieldsValue` 不会触发 `onValueChange` 和 `onValuesChange`。`onValueChange` 和 `onValuesChange` 只有在用户操作时才会被触发。如果在 `setFieldValue` 或 `setFieldsValue` 之后想要触发 `onValueChange` 或 `onValuesChange`,你需要手动调用这些方法。
**示例**:
```js
const onValuesChangeCallback = (changedValues) => {
console.log(changedValues);
};
this.form.onValuesChange(onValuesChangeCallback);
this.form.setFieldValue(name, value);
onValuesChangeCallback({
[name]: value,
});
```
### ValidatorStatus
```js
type ValidatorStatus = {
status: 'default' | 'success' | 'error' | 'validating',
errors: string[],
};
```
### submit 校验和抛出错误
```js
{
values: Record,
errorFields: {
name: string;
errors: string[];
}[]
}
```
### FormInput
与 `Input` 相同。
### FormTextarea
与 `Textarea` 相同。
### FormSwitch
与 `Switch` 相同。
### FormStepper
与 `Stepper` 相同,但添加了如下属性:
| 属性 | 说明 | 类型 | 默认值 |
| ---------------- | --------------------------------- | ------ | ------ |
| stepperClassName | 对应 `Stepper` 组件的 `className` | string | - |
| stepperStyle | 对应 `Stepper` 组件的 `style` | string | - |
### FormCheckGroup
与 `CheckGroup` 相同,但添加了如下属性:
| 属性 | 说明 | 类型 | 默认值 |
| ---------------- | ----------------------------------- | ------ | ------ |
| checkboxLabel | 对应 `CheckGroup` 组件的 `label` | slot | - |
| checkboxPosition | 对应 `CheckGroup` 组件的 `position` | string | - |
### FormRadioGroup
与 `RadioGroup` 相同,但添加了如下属性:
| 属性 | 说明 | 类型 | 默认值 |
| ------------- | ----------------------------------- | ------ | ------ |
| radioLabel | 对应 `RadioGroup` 组件的 `label` | slot | - |
| radioPosition | 对应 `RadioGroup` 组件的 `position` | string | - |
### FormPicker
与 `Picker` 相同,但添加了如下属性:
| 属性 | 说明 | 类型 | 默认值 |
| ----- | -------------------------------------------------------- | ----------------- | ------ |
| arrow | 右侧箭头,可选 `right`、`up`、`down`,传 true 为 `right` | string \| boolean | - |
### FormDatePicker
与 `DatePicker` 相同,但添加了如下属性:
| 属性 | 说明 | 类型 | 默认值 |
| ----- | -------------------------------------------------------- | ----------------- | ------ |
| arrow | 右侧箭头,可选 `right`、`up`、`down`,传 true 为 `right` | string \| boolean | - |
### FormRangePicker
与 `RangePicker` 相同,但添加了如下属性:
| 属性 | 说明 | 类型 | 默认值 |
| ----- | -------------------------------------------------------- | ----------------- | ------ |
| arrow | 右侧箭头,可选 `right`、`up`、`down`,传 true 为 `right` | string \| boolean | - |
### FormCascaderPicker
与 `CascaderPicker` 相同,但添加了如下属性:
| 属性 | 说明 | 类型 | 默认值 |
| ----- | -------------------------------------------------------- | ----------------- | ------ |
| arrow | 右侧箭头,可选 `right`、`up`、`down`,传 true 为 `right` | string \| boolean | - |
### FormSlider
与 `Slider` 相同。
### FormSelector
与 `Selector` 相同。
### FormImageUpload
与 `ImageUpload` 相同。
### createForm
`createForm` 是一个 `mixin`,用于自定义表单项。
```js
import { createForm } from 'antd-mini/es/Form/form';
Component({
mixins: [createForm()],
methods: {
onChange(value) {
this.emit('onChange', value);
},
},
});
```
`createForm` 会为组件增加以下内容:
- `data`
```js
{
formData: {
value: undefined, // 表单项的值
status: 'default', // 表单项的校验状态,包括 `default`、`success`、`error`、`validating`
errors: [], // 错误信息
},
}
```
- `methods`
```js
// 修改表单项时,需调用 `emit` 方法。自定义表单项组件在值改变时,应该调用此方法。
function emit(trigger: 'onChange' | 'onBlur' | 'onFocus', value: any): void;
```
想要了解更多方法,请参考 `createForm` 方法相关文档。使用 `formData` 和 `emit` 即可完成自定义表单项的实现。
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| --------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------- |
| --form-text-color | #cccccc
| #474747
| 表单文本颜色 |
| --form-item-color | #666666
| #808080
| 表单项颜色 |
| --form-item-bg | #ffffff
| #1a1a1a
| 表单项背景颜色 |
| --form-error-color | #ff3141
| #ff4a58
| 表单错误颜色 |
| --form-extra-color | #999999
| #616161
| 表单额外信息颜色 |
| --form-asterisk-color | #ff3b30
| #ff3b30
| 表单星号颜色 |
================================================
FILE: src/Form/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
// 字体颜色
@form-text-color: var(--form-text-color, @COLOR_TEXT_WEAK);
@form-item-color: var(--form-item-color, @COLOR_TEXT_SECONDARY);
@form-item-bg: var(--form-item-bg, @COLOR_CARD);
@form-error-color: var(--color-red, @COLOR_RED);
@form-extra-color: var(--form-extra-color, @COLOR_TEXT_ASSIST);
@form-asterisk-color: var(--form-asterisk-color, @COLOR_TEXT_WARNING);
================================================
FILE: src/Grid/index.axml
================================================
{{ item.title }}
{{ item.description }}
{{ item.title }}
{{ item.description }}
================================================
FILE: src/Grid/index.en.md
================================================
---
nav:
path: /components
group:
title: Layout
order: 1
toc: 'content'
---
# Grid
The palace is used for navigation of multiple sub-functions in the business and has a higher screen effect than the form of a list.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-grid": "antd-mini/es/Grid/index"
#endif
#if WECHAT
"ant-grid": "antd-mini/Grid/index"
#endif
}
```
## Code Sample
### Basic use
```xml
```
```js
Page({
data: {
items: [
{
title: 'title text',
icon: 'https://gw.alipayobjects.com/mdn/rms_3a7189/afts/img/A*L8FjQ7lSdq4AAAAAAAAAAAAAARQnAQ',
},
{
title: 'title text',
icon: 'https://gw.alipayobjects.com/mdn/rms_3a7189/afts/img/A*L8FjQ7lSdq4AAAAAAAAAAAAAARQnAQ',
},
{
title: 'title text',
icon: 'https://gw.alipayobjects.com/mdn/rms_3a7189/afts/img/A*L8FjQ7lSdq4AAAAAAAAAAAAAARQnAQ',
},
],
},
});
```
### 3 Columns-With Description
```xml
```
```js
Page({
data: {
items: [
{
title: 'title text',
icon: 'https://gw.alipayobjects.com/mdn/rms_3a7189/afts/img/A*L8FjQ7lSdq4AAAAAAAAAAAAAARQnAQ',
description: 'description',
},
{
title: 'title text',
icon: 'https://gw.alipayobjects.com/mdn/rms_3a7189/afts/img/A*L8FjQ7lSdq4AAAAAAAAAAAAAARQnAQ',
description: 'description',
},
{
title: 'title text',
icon: 'https://gw.alipayobjects.com/mdn/rms_3a7189/afts/img/A*L8FjQ7lSdq4AAAAAAAAAAAAAARQnAQ',
description: 'description',
},
],
},
});
```
### element horizontal layout
```xml
```
### Custom
```xml
#if ALIPAY
Item {{props.index 1}}
Description {{props.index 1}}
#endif
#if WECHAT
由于微信小程序平台的限制, Grid In微信暂时不支持 Slot
#endif
```
### Demo Code
## API
| Property | Description | Type | Default Value |
| --------------------------- | ------------------------------------------------------ | ------------------------------------- | ---------- |
| className | Class Name | string | - |
| columns | The number of elements displayed per row,`default` Mode effective | number | 5 |
| description | Description slot, receiving value, index | slot | - |
| gridItemLayout | item layout, optional `vertical`(Vertical)`horizontal`(Horizontal) | string | `vertical` |
| icon | Icon slot, receiving value, index | slot | - |
| iconSize | Icon size in px | number | - |
| iconStyle | Icon style type, optional `normal` `circle` | string | `normal` |
| items | Content Text | [GridItem](#griditem)[] | - |
| mode | Style type, optional `default`(Tile)`scroll`(Sliding) | string | `default` |
| paginationFillColor | Page break background color,`scroll` Mode effective | string | - |
| paginationFrontColor | Page break color,`scroll` Mode effective | string | - |
| showDivider | Show split line | boolean | - |
| style | Style | string | - |
| title | Header slot, receiving value, index | slot | - |
| #if ALIPAY onTap | Click on each element to trigger | (item: [GridItem](#griditem)) => void | |
| #if ALIPAY onFirstAppear | Triggered when the first visible area of the current element reaches 50% | (item: [GridItem](#griditem)) => void | |
| #if WECHAT bindtap | Click on each element to trigger | (item: [GridItem](#griditem)) => void | - |
| #if WECHAT bindfirstappear | Triggered when the first visible area of the current element reaches 50% | (item: [GridItem](#griditem)) => void | - |
#### GridItem
| Parameters | Description | Type | Default Value |
| ----------- | ----------------------------------------------------- | ------ | ------ |
| description | Description | string | - |
| icon | icons, support images and [Icon](./Icon) | string | - |
| iconStyle | Icon style type, with priority higher than grid, optional `normal` `circle` | string | - |
| title | Title | string | - |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| ---------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------ |
| --ant-grid-title-color | #333333
| #c5cad1
| Grid Title Color |
| --ant-grid-description-color | #999999
| #616161
| Grid Description Color |
| --ant-grid-border-color | #eeeeee
| #2b2b2b
| Grid Border Color |
================================================
FILE: src/Grid/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"image-icon": "../ImageIcon/index",
"ant-pagination": "../Pagination/index"
}
}
================================================
FILE: src/Grid/index.less
================================================
@import (reference) './variable.less';
.@{gridPrefix} {
padding: 24 * @rpx;
box-sizing: border-box;
row-gap: 24 * @rpx;
&-columns-3 {
padding: 24 * @rpx 0;
}
&-default {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
}
&-scroll {
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
}
&-item {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
flex-shrink: 0;
position: relative;
&-line {
.hairline('right',@border-color);
}
&-vertical-space {
margin-bottom: @vertical-space;
}
&-columns-5 {
width: 20%;
}
&-columns-4 {
width: 25%;
}
&-columns-3 {
width: 33.3%;
}
&-columns-2 {
width: 50%;
}
&-horizontal {
justify-content: center;
text-align: left;
position: relative;
min-height: @icon-size;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: row;
.@{gridPrefix}-item-info {
padding-left: 16 * @rpx;
display: flex;
justify-content: center;
align-items: flex-start;
flex-direction: column;
}
.@{gridPrefix}-item-img,
.@{gridPrefix}-item-icon {
display: flex;
justify-content: center;
align-items: center;
}
.@{gridPrefix}-item-title {
margin-top: 0;
}
}
&-vertical {
.@{gridPrefix}-item-description {
padding-top: 4 * @rpx;
}
}
&-columns-scroll {
width: @columnsscroll-width;
}
&-icon {
&-circle {
border-radius: 50%;
overflow: hidden;
}
&-icon {
font-size: @icon-size;
}
&-image {
width: @icon-size;
height: @icon-size;
}
}
&-title {
font-size: 30 * @rpx;
color: @title-color;
line-height: 42 * @rpx;
width: 100%;
margin-top: 16 * @rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&-description {
font-size: @description-size;
color: @description-color;
line-height: 33 * @rpx;
width: 100%;
padding-top: 4 * @rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:empty {
display: none;
}
}
}
}
================================================
FILE: src/Grid/index.md
================================================
---
nav:
path: /components
group:
title: 布局
order: 1
toc: 'content'
---
# Grid 宫格
宫格用于业务中多个子功能的导航,相比于列表的形式,具有更高的屏效。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-grid": "antd-mini/es/Grid/index"
#endif
#if WECHAT
"ant-grid": "antd-mini/Grid/index"
#endif
}
```
## 代码示例
### 基本使用
```xml
```
```js
Page({
data: {
items: [
{
title: 'title text',
icon: 'https://gw.alipayobjects.com/mdn/rms_3a7189/afts/img/A*L8FjQ7lSdq4AAAAAAAAAAAAAARQnAQ',
},
{
title: 'title text',
icon: 'https://gw.alipayobjects.com/mdn/rms_3a7189/afts/img/A*L8FjQ7lSdq4AAAAAAAAAAAAAARQnAQ',
},
{
title: 'title text',
icon: 'https://gw.alipayobjects.com/mdn/rms_3a7189/afts/img/A*L8FjQ7lSdq4AAAAAAAAAAAAAARQnAQ',
},
],
},
});
```
### 3 列-带描述
```xml
```
```js
Page({
data: {
items: [
{
title: 'title text',
icon: 'https://gw.alipayobjects.com/mdn/rms_3a7189/afts/img/A*L8FjQ7lSdq4AAAAAAAAAAAAAARQnAQ',
description: 'description',
},
{
title: 'title text',
icon: 'https://gw.alipayobjects.com/mdn/rms_3a7189/afts/img/A*L8FjQ7lSdq4AAAAAAAAAAAAAARQnAQ',
description: 'description',
},
{
title: 'title text',
icon: 'https://gw.alipayobjects.com/mdn/rms_3a7189/afts/img/A*L8FjQ7lSdq4AAAAAAAAAAAAAARQnAQ',
description: 'description',
},
],
},
});
```
### 元素横向布局
```xml
```
### 自定义
```xml
#if ALIPAY
第{{props.index + 1}}项
描述{{props.index + 1}}
#endif
#if WECHAT
由于微信小程序平台的限制, Grid 在微信暂时不支持 Slot
#endif
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| --------------------------- | ------------------------------------------------------ | ------------------------------------- | ---------- |
| className | 类名 | string | - |
| columns | 每行展示的元素个数,`default` 模式生效 | number | 5 |
| description | 描述插槽,接收 value、index | slot | - |
| gridItemLayout | item 布局,可选 `vertical`(垂直)`horizontal`(水平) | string | `vertical` |
| icon | 图标插槽,接收 value、index | slot | - |
| iconSize | 图标尺寸,单位 px | number | - |
| iconStyle | 图标样式类型,可选 `normal` `circle` | string | `normal` |
| items | 内容文字 | [GridItem](#griditem)[] | - |
| mode | 样式类型,可选 `default`(平铺)`scroll`(滑动) | string | `default` |
| paginationFillColor | 分页符背景色,`scroll` 模式生效 | string | - |
| paginationFrontColor | 分页符颜色,`scroll` 模式生效 | string | - |
| showDivider | 是否展示分割线 | boolean | - |
| style | 样式 | string | - |
| title | 标题插槽,接收 value、index | slot | - |
| #if ALIPAY onTap | 点击每个元素触发 | (item: [GridItem](#griditem)) => void | |
| #if ALIPAY onFirstAppear | 当前元素首次可见面积达到 50% 时触发 | (item: [GridItem](#griditem)) => void | |
| #if WECHAT bindtap | 点击每个元素触发 | (item: [GridItem](#griditem)) => void | - |
| #if WECHAT bindfirstappear | 当前元素首次可见面积达到 50% 时触发 | (item: [GridItem](#griditem)) => void | - |
#### GridItem
| 参数 | 说明 | 类型 | 默认值 |
| ----------- | ----------------------------------------------------- | ------ | ------ |
| description | 描述 | string | - |
| icon | 图标,支持图片和 [Icon](./Icon) | string | - |
| iconStyle | 图标样式类型,优先级高于 grid,可选 `normal` `circle` | string | - |
| title | 标题 | string | - |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| ---------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------ |
| --ant-grid-title-color | #333333
| #c5cad1
| 网格标题颜色 |
| --ant-grid-description-color | #999999
| #616161
| 网格描述颜色 |
| --ant-grid-border-color | #eeeeee
| #2b2b2b
| 网格边框颜色 |
================================================
FILE: src/Grid/index.sjs.ts
================================================
function checkNeedVerticalSpace(count, index, columns) {
if (count % columns === 0) {
return index < count - columns;
} else {
return index < columns * Math.floor(count / columns);
}
}
function checkShowSplitLine(index, count, columns, mode, showDivider) {
if (!showDivider) {
return false;
}
if (index === count - 1) {
return false;
}
if (mode === 'default') {
if ((index + 1) % columns === 0) {
return false;
}
}
return true;
}
export default {
checkNeedVerticalSpace,
checkShowSplitLine,
};
================================================
FILE: src/Grid/index.ts
================================================
import { Component, IPlatformEvent, triggerEvent } from '../_util/simply';
import { GridFunctionalProps } from './props';
Component({
props: GridFunctionalProps,
methods: {
onTap(e: IPlatformEvent) {
const { item } = e.currentTarget.dataset;
triggerEvent(this, 'tap', item);
},
onFirstAppear(e: IPlatformEvent) {
const { item } = e.currentTarget.dataset;
triggerEvent(this, 'firstAppear', item);
},
},
});
================================================
FILE: src/Grid/props.ts
================================================
import { IBaseProps } from '../_util/base';
export interface IGridItem {
/**
* @description 主文案
*/
title: string;
/**
* @description 副文案
*/
description?: string;
/**
* @description 图标
*/
icon: string;
/**
* @desscription 图标样式
* @default 'normal'
*/
iconStyle: 'normal' | 'circle';
}
/**
* @description 宫格
*/
export interface IGridProps extends IBaseProps {
/**
* @desscription 图标样式
* @default 'normal'
*/
iconStyle: 'normal' | 'circle';
/**
* @description 图标尺寸,单位px
*/
iconSize: number;
/**
* @description item布局。垂直/水平,水平仅columns=2生效
* @default 'vertical'
*/
gridItemLayout: 'vertical' | 'horizontal';
/**
* @description 组合形式
* @default 'default'
*/
mode: 'default' | 'scroll';
/**
* @description 每行展示的元素个数
* @default 5
*/
columns: number;
/**
* @description 元素列表
*/
items: IGridItem[];
/**
* @description 是否展示分割线
*/
showDivider: boolean;
/**
* @description 分页符背景色
* @default '#ddd'
*/
paginationFillColor: string;
/**
* @description 分页符颜色
* @default '#1677ff'
*/
paginationFrontColor: string;
/**
* @description 点击事件
* @param item
*/
onTap?(item: IGridItem): void;
/**
* @description 当前元素首次可见面积达到50%时触发
* @param item
*/
onFirstAppear?(item: IGridItem): void;
}
export const GridDefaultProps: Partial = {
iconStyle: 'normal',
mode: 'default',
columns: 5,
gridItemLayout: 'vertical',
showDivider: false,
};
export const GridFunctionalProps: IGridProps = {
iconStyle: 'normal',
iconSize: null,
gridItemLayout: 'vertical',
mode: 'default',
columns: 5,
items: [],
showDivider: false,
paginationFrontColor: '',
paginationFillColor: '',
};
================================================
FILE: src/Grid/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
@import (reference) '../style/mixins/hairline.less';
@gridPrefix: ant-grid;
@icon-size: 56 * @rpx;
@title-color: var(--ant-grid-title-color, @COLOR_TEXT_PRIMARY);
@description-size: 24 * @rpx;
@description-color: var(--ant-grid-description-color, @COLOR_TEXT_ASSIST);
@vertical-space: 40 * @rpx;
@columnsscroll-width: 130 * @rpx;
@border-color: var(--ant-grid-border-color, @COLOR_BORDER);
================================================
FILE: src/GuideTour/index.axml
================================================
{{ u.isPropsEmpty(jumpText) ? locale.guideTour.jumpText : jumpText}}
{{ u.isPropsEmpty(prevStepText) ? locale.guideTour.prevStepText : prevStepText}}
{{ u.isPropsEmpty(nextStepText) ? locale.guideTour.nextStepText : nextStepText}}
{{ u.isPropsEmpty(gotItText) ? locale.guideTour.gotItText : gotItText}}
================================================
FILE: src/GuideTour/index.en.md
================================================
---
nav:
path: /components
group:
title: Navigation
order: 6
toc: 'content'
---
# GuideTour
蒙层与自定义卡片的引导组件。适用于页面上关键功能的介绍。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-guide-tour": "antd-mini/es/GuideTour/index"
#endif
#if WECHAT
"ant-guide-tour": "antd-mini/GuideTour/index"
#endif
}
```
## 代码示例
### 基本使用
```xml
```
```js
Page({
data: {
visible: true,
items: [
{ left: 20, top: 80, imageUrl: 'https://gw.alipayobjects.com/zos/antfincdn/IV3MGP1qL/bianzu%25252013.png', imageMode: 'widthFix' },
{ left: 20, top: 160, imageUrl: 'https://gw.alipayobjects.com/zos/antfincdn/%26B6d3lBJn/bianzu%25252020.png' },
{ left: 20, top: 220, imageUrl: 'https://gw.alipayobjects.com/zos/antfincdn/lwVOkCcwb/bianzu%25252021.png' },
],
},
onChange(index) {
#if ALIPAY
console.log('index', index);
#endif
},
closeTour() {
this.setData({
visible: false,
});
},
});
```
### 受控模式
```xml
```
```js
Page({
data: {
visible: false,
current: 0,
items: [
{ left: 20, top: 80, imageUrl: 'https://gw.alipayobjects.com/zos/antfincdn/IV3MGP1qL/bianzu%25252013.png', imageMode: 'widthFix' },
{ left: 20, top: 160, imageUrl: 'https://gw.alipayobjects.com/zos/antfincdn/%26B6d3lBJn/bianzu%25252020.png' },
{ left: 20, top: 220, imageUrl: 'https://gw.alipayobjects.com/zos/antfincdn/lwVOkCcwb/bianzu%25252021.png' },
],
},
onChangeControlled(value) {
#if ALIPAY
this.setData({ current: value });
#endif
#if WECHAT
this.setData({ current: value.detail });
#endif
},
closeTour() {
this.setData({
visible: false
});
},
});
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| ---------------------- | ---------------- | ----------------------------------- | -------- |
| className | 类名 | `string` | - |
| current | 当前步骤 | `number` | - |
| defaultCurrent | 默认当前步骤 | `number` | 0 |
| items | 步骤信息 | [`GuideTourItem`](#guidetourttem)[] | - |
| maskClassName | 蒙层的类名 | `string` | - |
| maskStyle | 蒙层的样式 | `string` | - |
| style | 样式 | `string` | - |
| swiperable | 是否开启滑动模式 | `boolean` | `false` |
| visible | 是否显示 | `boolean` | `false` |
| jumpText | 跳过按钮的文案 | `string` | '跳过' |
| prevStepText | 上一步按钮的文案 | `string` | '上一步' |
| nextStepText | 下一步按钮的文案 | `string` | '下一步' |
| gotItText | 知道了按钮的文案 | `string` | '知道了' |
| #if ALIPAY onCancel | 关闭回调 | `() => void` | - |
| #if ALIPAY onChange | 步骤改变回调 | `(index: number) => void` | - |
| #if WECHAT bindcancel | 关闭回调 | `() => void` | - |
| #if WECHAT bindchange | 步骤改变回调 | `(index: number) => void` | - |
### GuideTourItem
| 参数 | 说明 | 类型 | 默认值 |
| ---------- | -------------------------- | -------- | ------ |
| left | 距离左边距离,单位 `px` | `number` | - |
| imageMode | 图片模式,同 image 的 mode | `string` | - |
| imageStyle | 图片内联样式 | `string` | - |
| imageUrl | 图片地址 | `string` | - |
| top | 距离顶部距离,单位 `px` | `number` | - |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| ------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------- |
| --guide-tour-text-color | #ffffff
| #ffffff
| 引导教程文本颜色 |
| --guide-tour-clear-color | #999999
| #616161
| 引导教程清除按钮颜色 |
| --guide-tour-dot-color | #999999
| #616161
| 引导教程步骤点颜色 |
| --guide-tour-border-color | #eeeeee
| #2b2b2b
| 引导教程边框颜色 |
| --guide-tour-btn-color | #333333
| #c5cad1
| 引导教程按钮颜色 |
================================================
FILE: src/GuideTour/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-mask": "../Mask/index",
"ant-button": "../Button/index",
"ant-icon": "../Icon/index",
"ant-auto-resize": "../AutoResize/index"
}
}
================================================
FILE: src/GuideTour/index.less
================================================
@import (reference) './variable.less';
.@{guideTourPrefix} {
&-button {
width: 100%;
position: fixed;
left: 0;
bottom: @guide-tour-button-bottom;
z-index: @guide-tour-z-index-3;
display: flex;
justify-content: center;
.ant-button:nth-of-type(1) {
color: @guide-tour-text-color;
background-color: transparent;
box-shadow: inset 0 0 0 @border-width-standard @guide-tour-border-color;
}
.ant-button:nth-of-type(2) {
color: @guide-tour-btn-color;
background-color: @guide-tour-text-color;
box-shadow: none;
}
.ant-button {
margin: 0 12 * @rpx;
width: 152 * @rpx;
}
}
&-indicator {
width: 100%;
position: absolute;
bottom: @guide-tour-dot-bottom;
left: 50%;
transform: translateX(-50%);
z-index: @guide-tour-z-index-2;
display: flex;
justify-content: center;
&-dot {
margin: 0 3 * @rpx;
background-color: @guide-tour-dot-color;
width: @guide-tour-dot-size;
height: @guide-tour-dot-size;
border-radius: @guide-tour-dot-border-radius;
&-active {
width: @guide-tour-dot-active-width;
background-color: @guide-tour-text-color;
}
}
}
&-clear {
position: fixed;
top: 80 * @rpx;
right: 40 * @rpx;
z-index: @guide-tour-z-index-3;
font-size: 56 * @rpx;
color: @guide-tour-clear-color;
}
&-item {
z-index: @guide-tour-z-index-2;
position: fixed;
top: 0;
left: 0;
}
&-swiper {
z-index: @guide-tour-z-index-2;
position: fixed;
top: 0;
left: 0;
/// #if WECHAT
width: 100vw;
/// #endif
}
}
================================================
FILE: src/GuideTour/index.md
================================================
---
nav:
path: /components
group:
title: 导航
order: 6
toc: 'content'
---
# GuideTour 新手引导
蒙层与自定义卡片的引导组件。适用于页面上关键功能的介绍。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-guide-tour": "antd-mini/es/GuideTour/index"
#endif
#if WECHAT
"ant-guide-tour": "antd-mini/GuideTour/index"
#endif
}
```
## 代码示例
### 基本使用
```xml
```
```js
Page({
data: {
visible: true,
items: [
{ left: 20, top: 80, imageUrl: 'https://gw.alipayobjects.com/zos/antfincdn/IV3MGP1qL/bianzu%25252013.png', imageMode: 'widthFix' },
{ left: 20, top: 160, imageUrl: 'https://gw.alipayobjects.com/zos/antfincdn/%26B6d3lBJn/bianzu%25252020.png' },
{ left: 20, top: 220, imageUrl: 'https://gw.alipayobjects.com/zos/antfincdn/lwVOkCcwb/bianzu%25252021.png' },
],
},
onChange(index) {
#if ALIPAY
console.log('index', index);
#endif
},
closeTour() {
this.setData({
visible: false,
});
},
});
```
### 受控模式
```xml
```
```js
Page({
data: {
visible: false,
current: 0,
items: [
{ left: 20, top: 80, imageUrl: 'https://gw.alipayobjects.com/zos/antfincdn/IV3MGP1qL/bianzu%25252013.png', imageMode: 'widthFix' },
{ left: 20, top: 160, imageUrl: 'https://gw.alipayobjects.com/zos/antfincdn/%26B6d3lBJn/bianzu%25252020.png' },
{ left: 20, top: 220, imageUrl: 'https://gw.alipayobjects.com/zos/antfincdn/lwVOkCcwb/bianzu%25252021.png' },
],
},
onChangeControlled(value) {
#if ALIPAY
this.setData({ current: value });
#endif
#if WECHAT
this.setData({ current: value.detail });
#endif
},
closeTour() {
this.setData({
visible: false
});
},
});
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| ---------------------- | ---------------- | ----------------------------------- | -------- |
| className | 类名 | `string` | - |
| current | 当前步骤 | `number` | - |
| defaultCurrent | 默认当前步骤 | `number` | 0 |
| items | 步骤信息 | [`GuideTourItem`](#guidetourttem)[] | - |
| maskClassName | 蒙层的类名 | `string` | - |
| maskStyle | 蒙层的样式 | `string` | - |
| style | 样式 | `string` | - |
| swiperable | 是否开启滑动模式 | `boolean` | `false` |
| visible | 是否显示 | `boolean` | `false` |
| jumpText | 跳过按钮的文案 | `string` | '跳过' |
| prevStepText | 上一步按钮的文案 | `string` | '上一步' |
| nextStepText | 下一步按钮的文案 | `string` | '下一步' |
| gotItText | 知道了按钮的文案 | `string` | '知道了' |
| #if ALIPAY onCancel | 关闭回调 | `() => void` | - |
| #if ALIPAY onChange | 步骤改变回调 | `(index: number) => void` | - |
| #if WECHAT bindcancel | 关闭回调 | `() => void` | - |
| #if WECHAT bindchange | 步骤改变回调 | `(index: number) => void` | - |
### GuideTourItem
| 参数 | 说明 | 类型 | 默认值 |
| ---------- | -------------------------- | -------- | ------ |
| left | 距离左边距离,单位 `px` | `number` | - |
| imageMode | 图片模式,同 image 的 mode | `string` | - |
| imageStyle | 图片内联样式 | `string` | - |
| imageUrl | 图片地址 | `string` | - |
| top | 距离顶部距离,单位 `px` | `number` | - |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| ------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------- |
| --guide-tour-text-color | #ffffff
| #ffffff
| 引导教程文本颜色 |
| --guide-tour-clear-color | #999999
| #616161
| 引导教程清除按钮颜色 |
| --guide-tour-dot-color | #999999
| #616161
| 引导教程步骤点颜色 |
| --guide-tour-border-color | #eeeeee
| #2b2b2b
| 引导教程边框颜色 |
| --guide-tour-btn-color | #333333
| #c5cad1
| 引导教程按钮颜色 |
================================================
FILE: src/GuideTour/index.sjs.ts
================================================
function checkShowNext(current, items) {
return current < items.length - 1;
}
function checkShowPrev(current, items) {
return current > 0;
}
function checkShowJump(current, items) {
return current === 0 && items.length > 1;
}
function checkShowKnow(current, items) {
return current === items.length - 1;
}
export default {
checkShowNext,
checkShowPrev,
checkShowJump,
checkShowKnow,
};
================================================
FILE: src/GuideTour/index.ts
================================================
import { effect } from '@preact/signals-core';
import mixinValue from '../mixins/value';
import {
ComponentWithSignalStoreImpl,
triggerEvent,
triggerEventOnly,
} from '../_util/simply';
import i18nController from '../_util/store';
import { GuideTourDefaultProps } from './props';
ComponentWithSignalStoreImpl({
storeOptions: {
store: () => i18nController,
updateHook: effect,
mapState: {
locale: ({ store }) => store.currentLocale.value,
},
},
props: GuideTourDefaultProps,
methods: {
async onNext() {
const currentValue = this.getValue();
const newCurrent = currentValue + 1;
if (!this.isControlled()) {
this.update(newCurrent);
}
triggerEvent(this, 'change', newCurrent);
},
async onPrev() {
const currentValue = this.getValue();
const newCurrent = currentValue - 1;
if (!this.isControlled()) {
this.update(newCurrent);
}
triggerEvent(this, 'change', newCurrent);
},
onCancel() {
triggerEventOnly(this, 'cancel');
},
async onSwiperChange(e) {
const { current } = e.detail;
if (!this.isControlled()) {
this.update(current);
}
triggerEvent(this, 'change', current);
},
},
mixins: [
mixinValue({
valueKey: 'current',
defaultValueKey: 'defaultCurrent',
}),
],
});
================================================
FILE: src/GuideTour/props.ts
================================================
import { IBaseProps } from '../_util/base';
interface IStep {
/**
* @description 图片地址
*/
imageUrl: string;
/**
* @description 图片模式
*/
imageMode: string;
/**
* @description 图片内联样式
*/
imageStyle: string;
/**
* @description 距离顶部
*/
top: string;
/**
* @description 距离左边
*/
left: string;
/**
* @description className
*/
className?: string;
}
export interface IGuideTour extends IBaseProps {
/**
* @description 蒙层样式
*/
maskStyle: string;
/**
* @description 蒙层 className
*/
maskClassName?: string;
/**
* @description 步骤详情
*/
items: IStep[];
/**
* @description 当前步骤
*/
current: number;
/**
* @description 初始step
*/
defaultCurrent: number;
/**
* @description 是否开启滑动模式
*/
swiperable: boolean;
/**
* @description 引导是否可见, 受控
* @default true
*/
visible: boolean;
/**
* @description 关闭回调
*/
onCancel: () => void;
/**
* @description 步骤改变回调
*/
onChange: (index: number) => boolean;
/**
* @description 上一步按钮文案
* @default "上一步"
*/
prevStepText?: string;
/**
* @description 下一步按钮文案
* @default "下一步"
*/
nextStepText?: string;
/**
* @description 知道了按钮文案
* @default "知道了"
*/
gotItText?: string;
/**
* @description 跳过按钮文案
* @default "跳过"
*/
jumpText?: string;
}
export const GuideTourDefaultProps: Partial = {
visible: false,
swiperable: false,
items: [],
current: null,
defaultCurrent: 0,
gotItText: null,
nextStepText: null,
prevStepText: null,
jumpText: null,
maskStyle: '',
maskClassName: '',
};
================================================
FILE: src/GuideTour/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
@guideTourPrefix: ant-guide-tour;
// 层级-1
@guide-tour-z-index-1: 9999;
// 层级-2
@guide-tour-z-index-2: 10000;
// 层级-3
@guide-tour-z-index-3: 10001;
// 主要文字颜色
@guide-tour-text-color: var(--guide-tour-text-color, @COLOR_WHITE);
// 关闭按钮颜色
@guide-tour-clear-color: var(--guide-tour-clear-color, @COLOR_TEXT_ASSIST);
// 按钮距离底部
@guide-tour-button-bottom: calc(100 * @rpx + env(safe-area-inset-bottom));
// 按钮高度
@guide-tour-button-height:52 * @rpx;
// 步骤条点的长宽
@guide-tour-dot-size: 6 * @rpx;
// 步骤条点的颜色
@guide-tour-dot-color: var(--guide-tour-dot-color, @COLOR_TEXT_ASSIST);
// 激活步骤条点的长度
@guide-tour-dot-active-width:26 * @rpx;
// 步骤条点的弧度
@guide-tour-dot-border-radius:2 * @rpx;
// 步骤条距离底部
@guide-tour-dot-bottom: calc(
@guide-tour-button-bottom + 40 * @rpx + @guide-tour-button-height
);
@guide-tour-border-color: var(--guide-tour-border-color, @COLOR_BORDER);
@guide-tour-btn-color: var(--guide-tour-btn-color, @COLOR_TEXT_PRIMARY);
@guide-tour-right-btn-color: var(--guide-tour-right-btn-color, @COLOR_TEXT_PRIMARY);
================================================
FILE: src/Icon/index.axml
================================================
================================================
FILE: src/Icon/index.en.md
================================================
---
nav:
path: /components
group:
title: General
order: 2
toc: 'content'
---
# Icon
Semantic vector graphics. Icon icons can be used when graphics are needed to metaphorically present basic operating functions and give users correct, friendly and clear operating guidelines.
## Introduction
Introducing components in index.json
```json
"usingComponents": {
#if ALIPAY
"ant-icon": "antd-mini/es/Icon/index"
#endif
#if WECHAT
"ant-icon": "antd-mini/Icon/index"
#endif
}
```
## Icon List
Search and view the icon, click the icon to copy.
## Code Sample
### Basic use
```xml
```
### Custom size and color
```xml
```
### Demo Code
## API
| Property | Description | Type | Default Value |
| :------------------- | :----------------------- | :----------------- | :----- |
| className | Class Name | string | - |
| style | Style | string | - |
| type | Types of icons | string | - |
| #if ALIPAY catchTap | Callback function triggered when icon is clicked | (e: Event) => void | - |
| #if ALIPAY onTap | Callback function triggered when icon is clicked | (e: Event) => void | - |
| #if WECHAT catchtap | Callback function triggered when icon is clicked | (e: Event) => void | - |
| #if WECHAT bindtap | Callback function triggered when icon is clicked | (e: Event) => void | - |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| ------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | -------- |
| --icon-color | #333333
| #c5cad1
| Icon Color |
================================================
FILE: src/Icon/index.json
================================================
{
"styleIsolation": "shared",
"component": true
}
================================================
FILE: src/Icon/index.less
================================================
@import (reference) './variable.less';
@font-face {
font-family: antdmini-icon;
src: url('https://gw.alipayobjects.com/os/bmw-prod/578c472b-19a7-44ab-a92d-adc367ee2fe2.ttf?v=20210115')
format('truetype');
}
@iconPrefix: ant-icon;
.@{iconPrefix} {
/* stylelint-disable font-family-no-missing-generic-family-keyword */
font-family: antdmini-icon !important;
font-style: normal;
-webkit-font-smoothing: antialiased;
/// #if ALIPAY
line-height: @line-height-base;
/// #endif
/// #if WECHAT
font-size: @size-4;
/// #endif
&-MinusOutline:before {
content: '\e66f';
}
&-AlipayCircleFill:before {
content: '\e670';
}
&-CheckCircleFill:before {
content: '\e671';
}
&-FireFill:before {
content: '\e672';
}
&-FaceRecognitionOutline:before {
content: '\e673';
}
&-StarFill:before {
content: '\e674';
}
&-EyeInvisibleFill:before {
content: '\e675';
}
&-SmileFill:before {
content: '\e676';
}
&-FrownFill:before {
content: '\e677';
}
&-BankcardOutline:before {
content: '\e678';
}
&-HeartOutline:before {
content: '\e679';
}
&-EyeFill:before {
content: '\e67a';
}
&-HeartFill:before {
content: '\e67b';
}
&-DownFill:before {
content: '\e67c';
}
&-CloseCircleFill:before {
content: '\e67d';
}
&-VideoOutline:before {
content: '\e67e';
}
&-CouponOutline:before {
content: '\e67f';
}
&-ReceiptOutline:before {
content: '\e680';
}
&-AntOutline:before {
content: '\e681';
}
&-UserCircleOutline:before {
content: '\e682';
}
&-PayCircleOutline:before {
content: '\e683';
}
&-BillOutline:before {
content: '\e684';
}
&-PlayOutline:before {
content: '\e685';
}
&-PayOutline:before {
content: '\e686';
}
&-MoreOutline:before {
content: '\e687';
}
&-ShrinkOutline:before {
content: '\e688';
}
&-ArrowsAltOutline:before {
content: '\e689';
}
&-StarOutline:before {
content: '\e68a';
}
&-CheckOutline:before {
content: '\e68b';
}
&-DeleteOutline:before {
content: '\e68c';
}
&-LinkOutline:before {
content: '\e68d';
}
&-InformationCircleOutline:before {
content: '\e68e';
}
&-GlobalOutline:before {
content: '\e68f';
}
&-InformationCircleFill:before {
content: '\e690';
}
&-ExclamationCircleFill:before {
content: '\e691';
}
&-CheckCircleOutline:before {
content: '\e692';
}
&-CloseCircleOutline:before {
content: '\e693';
}
&-SetOutline:before {
content: '\e694';
}
&-QuestionCircleFill:before {
content: '\e695';
}
&-QuestionCircleOutline:before {
content: '\e696';
}
&-UpCircleOutline:before {
content: '\e697';
}
&-FrownOutline:before {
content: '\e698';
}
&-DownCircleOutline:before {
content: '\e699';
}
&-ExclamationCircleOutline:before {
content: '\e69a';
}
&-MinusCircleOutline:before {
content: '\e69b';
}
&-RedoOutline:before {
content: '\e69c';
}
&-UndoOutline:before {
content: '\e69d';
}
&-EyeInvisibleOutline:before {
content: '\e69e';
}
&-ForbidFill:before {
content: '\e69f';
}
&-PicturesOutline:before {
content: '\e6a0';
}
&-PictureOutline:before {
content: '\e6a1';
}
&-PictureWrongOutline:before {
content: '\e6a2';
}
&-EyeOutline:before {
content: '\e6a3';
}
&-AddCircleOutline:before {
content: '\e6a4';
}
&-ClockCircleFill:before {
content: '\e6a5';
}
&-ClockCircleOutline:before {
content: '\e6a6';
}
&-BellMuteOutline:before {
content: '\e6a7';
}
&-KeyOutline:before {
content: '\e6a8';
}
&-BellOutline:before {
content: '\e6a9';
}
&-SearchOutline:before {
content: '\e6aa';
}
&-CollectMoneyOutline:before {
content: '\e6ab';
}
&-UnorderedListOutline:before {
content: '\e6ac';
}
&-AppstoreOutline:before {
content: '\e6ad';
}
&-ExclamationTriangleOutline:before {
content: '\e6ae';
}
&-AddOutline:before {
content: '\e6af';
}
&-ScanningOutline:before {
content: '\e6b0';
}
&-ScanCodeOutline:before {
content: '\e6b1';
}
&-ExclamationOutline:before {
content: '\e6b2';
}
&-CloseOutline:before {
content: '\e6b3';
}
&-ScanningFaceOutline:before {
content: '\e6b4';
}
&-LeftOutline:before {
content: '\e6b5';
}
&-DownOutline:before {
content: '\e6b6';
}
&-UpOutline:before {
content: '\e6b7';
}
&-RightOutline:before {
content: '\e6b8';
}
&-KoubeiOutline:before {
content: '\e6b9';
}
&-KoubeiFill:before {
content: '\e6ba';
}
&-AAOutline:before {
content: '\e6bb';
}
&-ArrowDownCircleOutline:before {
content: '\e6bc';
}
&-MovieOutline:before {
content: '\e6bd';
}
&-CompassOutline:before {
content: '\e6be';
}
&-LoopOutline:before {
content: '\e6bf';
}
&-TextOutline:before {
content: '\e6c0';
}
&-TagOutline:before {
content: '\e6c1';
}
&-FlagOutline:before {
content: '\e6c2';
}
&-EnvironmentOutline:before {
content: '\e6c3';
}
&-CalendarOutline:before {
content: '\e6c4';
}
&-LocationFill:before {
content: '\e6c5';
}
&-PhoneFill:before {
content: '\e6c6';
}
&-PhonebookOutline:before {
content: '\e6c7';
}
&-SmileOutline:before {
content: '\e6c8';
}
&-UserAddOutline:before {
content: '\e6c9';
}
&-FileWrongOutline:before {
content: '\e6ca';
}
&-SoundMuteFill:before {
content: '\e6cb';
}
&-SoundMuteOutline:before {
content: '\e6cc';
}
&-LockOutline:before {
content: '\e6cd';
}
&-UnlockOutline:before {
content: '\e6ce';
}
&-EditSOutline:before {
content: '\e6cf';
}
&-UploadOutline:before {
content: '\e6d0';
}
&-SoundOutline:before {
content: '\e6d1';
}
&-DownlandOutline:before {
content: '\e6d2';
}
&-SendOutline:before {
content: '\e6d3';
}
&-FillinOutline:before {
content: '\e6d4';
}
&-AudioMutedOutline:before {
content: '\e6d5';
}
&-AudioOutline:before {
content: '\e6d6';
}
&-UserOutline:before {
content: '\e6d7';
}
&-UserContactOutline:before {
content: '\e6d8';
}
&-TeamOutline:before {
content: '\e6d9';
}
&-UserSetOutline:before {
content: '\e6da';
}
&-FileOutline:before {
content: '\e6db';
}
&-MailOutline:before {
content: '\e6dc';
}
&-TruckOutline:before {
content: '\e6dd';
}
&-MailOpenOutline:before {
content: '\e6de';
}
&-ChatCheckOutline:before {
content: '\e6df';
}
&-ChatAddOutline:before {
content: '\e6e0';
}
&-ChatWrongOutline:before {
content: '\e6e1';
}
&-PhonebookFill:before {
content: '\e6e2';
}
&-AddressBookFill:before {
content: '\e6e3';
}
&-CalculatorOutline:before {
content: '\e6e4';
}
&-PieOutline:before {
content: '\e6e5';
}
&-HandPayCircleOutline:before {
content: '\e6e6';
}
&-GiftOutline:before {
content: '\e6e7';
}
&-TransportQRcodeOutline:before {
content: '\e6e8';
}
&-FolderOutline:before {
content: '\e6e9';
}
&-AlipaySquareFill:before {
content: '\e6ea';
}
&-TravelOutline:before {
content: '\e6eb';
}
&-AppOutline:before {
content: '\e6ec';
}
&-HistogramOutline:before {
content: '\e6ed';
}
&-MailFill:before {
content: '\e6ee';
}
&-CameraOutline:before {
content: '\e6ef';
}
&-EditFill:before {
content: '\e6f0';
}
&-SystemQRcodeOutline:before {
content: '\e6f1';
}
&-LockFill:before {
content: '\e6f2';
}
&-AudioFill:before {
content: '\e6f3';
}
&-TeamFill:before {
content: '\e6f4';
}
&-FilterOutline:before {
content: '\e6f5';
}
&-EditSFill:before {
content: '\e6f6';
}
&-LikeOutline:before {
content: '\e6f7';
}
&-TextDeletionOutline:before {
content: '\e6f8';
}
&-StopOutline:before {
content: '\e6f9';
}
&-FingerdownOutline:before {
content: '\e6fa';
}
&-MessageFill:before {
content: '\e6fb';
}
&-LocationOutline:before {
content: '\e6fc';
}
&-ContentOutline:before {
content: '\e6fd';
}
&-ExclamationShieldFill:before {
content: '\e6fe';
}
&-ReceivePaymentOutline:before {
content: '\e6ff';
}
&-ExclamationShieldOutline:before {
content: '\e700';
}
&-AddSquareOutline:before {
content: '\e701';
}
&-CloseShieldOutline:before {
content: '\e702';
}
&-CheckShieldOutline:before {
content: '\e703';
}
&-CheckShieldFill:before {
content: '\e704';
}
&-ShopbagOutline:before {
content: '\e705';
}
&-MessageOutline:before {
content: '\e706';
}
}
================================================
FILE: src/Icon/index.md
================================================
---
nav:
path: /components
group:
title: 通用
order: 2
toc: 'content'
---
# Icon 图标
语义化的矢量图形。当需要使用图形来对基础操作功能进行隐喻呈现,并给予用户正确、友好且清晰的操作指引时,可以使用 Icon 图标。
## 引入
在 index.json 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-icon": "antd-mini/es/Icon/index"
#endif
#if WECHAT
"ant-icon": "antd-mini/Icon/index"
#endif
}
```
## 图标列表
搜索并查看图标,点击图标可进行复制。
## 代码示例
### 基本使用
```xml
```
### 自定义大小及颜色
```xml
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| :------------------- | :----------------------- | :----------------- | :----- |
| className | 类名 | string | - |
| style | 样式 | string | - |
| type | 图标的类型 | string | - |
| #if ALIPAY catchTap | 点击图标时触发的回调函数 | (e: Event) => void | - |
| #if ALIPAY onTap | 点击图标时触发的回调函数 | (e: Event) => void | - |
| #if WECHAT catchtap | 点击图标时触发的回调函数 | (e: Event) => void | - |
| #if WECHAT bindtap | 点击图标时触发的回调函数 | (e: Event) => void | - |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| ------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | -------- |
| --icon-color | #333333
| #c5cad1
| 图标颜色 |
================================================
FILE: src/Icon/index.ts
================================================
import { IconDefaultProps } from './props';
import fmtEvent from '../_util/fmtEvent';
import '../_util/assert-component2';
Component({
/// #if WECHAT
properties: {
type: {
value: '',
type: String,
},
color: {
type: String,
},
style: {
type: String,
},
className: {
type: String,
},
},
options: {
//@ts-ignore
styleIsolation: 'shared',
},
/// #endif
/// #if ALIPAY
props: IconDefaultProps,
methods: {
onTap(e) {
if (this.props.onTap) {
this.props.onTap(fmtEvent(this.props, e));
}
},
catchTap(e) {
if (this.props.catchTap) {
this.props.catchTap(fmtEvent(this.props, e));
}
},
},
/// #endif
});
================================================
FILE: src/Icon/props.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { IBaseProps } from '../_util/base';
/**
* @description 图标,内置丰富的图标可以选择。
*/
export interface IconProps extends IBaseProps {
/**
* @description icon 图标的类型
* @default ""
*/
type?: string;
/**
* 点击图标
*/
onTap?: (e: any) => void;
/**
* 点击图标
*/
catchTap?: (e: any) => void;
}
export const IconDefaultProps: Partial = {
type: '',
};
================================================
FILE: src/Icon/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
@icon-color: var(--icon-color, @COLOR_TEXT_PRIMARY);
================================================
FILE: src/ImageIcon/index.axml
================================================
================================================
FILE: src/ImageIcon/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-icon": "../Icon/index"
}
}
================================================
FILE: src/ImageIcon/index.sjs.ts
================================================
const iconTypes = [
'MinusOutline',
'AlipayCircleFill',
'CheckCircleFill',
'FireFill',
'FaceRecognitionOutline',
'StarFill',
'EyeInvisibleFill',
'SmileFill',
'FrownFill',
'BankcardOutline',
'HeartOutline',
'EyeFill',
'HeartFill',
'DownFill',
'CloseCircleFill',
'VideoOutline',
'CouponOutline',
'ReceiptOutline',
'AntOutline',
'UserCircleOutline',
'PayCircleOutline',
'BillOutline',
'PlayOutline',
'PayOutline',
'MoreOutline',
'ShrinkOutline',
'ArrowsAltOutline',
'StarOutline',
'CheckOutline',
'DeleteOutline',
'LinkOutline',
'InformationCircleOutline',
'GlobalOutline',
'InformationCircleFill',
'ExclamationCircleFill',
'CheckCircleOutline',
'CloseCircleOutline',
'SetOutline',
'QuestionCircleFill',
'QuestionCircleOutline',
'UpCircleOutline',
'FrownOutline',
'DownCircleOutline',
'ExclamationCircleOutline',
'MinusCircleOutline',
'RedoOutline',
'UndoOutline',
'EyeInvisibleOutline',
'ForbidFill',
'PicturesOutline',
'PictureOutline',
'PictureWrongOutline',
'EyeOutline',
'AddCircleOutline',
'ClockCircleFill',
'ClockCircleOutline',
'BellMuteOutline',
'KeyOutline',
'BellOutline',
'SearchOutline',
'CollectMoneyOutline',
'UnorderedListOutline',
'AppstoreOutline',
'ExclamationTriangleOutline',
'AddOutline',
'ScanningOutline',
'ScanCodeOutline',
'ExclamationOutline',
'CloseOutline',
'ScanningFaceOutline',
'LeftOutline',
'DownOutline',
'UpOutline',
'RightOutline',
'KoubeiOutline',
'KoubeiFill',
'AAOutline',
'ArrowDownCircleOutline',
'MovieOutline',
'CompassOutline',
'LoopOutline',
'TextOutline',
'TagOutline',
'FlagOutline',
'EnvironmentOutline',
'CalendarOutline',
'LocationFill',
'PhoneFill',
'PhonebookOutline',
'SmileOutline',
'UserAddOutline',
'FileWrongOutline',
'SoundMuteFill',
'SoundMuteOutline',
'LockOutline',
'UnlockOutline',
'EditSOutline',
'UploadOutline',
'SoundOutline',
'DownlandOutline',
'SendOutline',
'FillinOutline',
'AudioMutedOutline',
'AudioOutline',
'UserOutline',
'UserContactOutline',
'TeamOutline',
'UserSetOutline',
'FileOutline',
'MailOutline',
'TruckOutline',
'MailOpenOutline',
'ChatCheckOutline',
'ChatAddOutline',
'ChatWrongOutline',
'PhonebookFill',
'AddressBookFill',
'CalculatorOutline',
'PieOutline',
'HandPayCircleOutline',
'GiftOutline',
'TransportQRcodeOutline',
'FolderOutline',
'AlipaySquareFill',
'TravelOutline',
'AppOutline',
'HistogramOutline',
'MailFill',
'CameraOutline',
'EditFill',
'SystemQRcodeOutline',
'LockFill',
'AudioFill',
'TeamFill',
'FilterOutline',
'EditSFill',
'LikeOutline',
'TextDeletionOutline',
'StopOutline',
'FingerdownOutline',
'MessageFill',
'LocationOutline',
'ContentOutline',
'ExclamationShieldFill',
'ReceivePaymentOutline',
'ExclamationShieldOutline',
'AddSquareOutline',
'CloseShieldOutline',
'CheckShieldOutline',
'CheckShieldFill',
'ShopbagOutline',
'MessageOutline',
];
const isIcon = (src) => {
return iconTypes.indexOf(src) > -1;
};
export default { isIcon };
================================================
FILE: src/ImageIcon/index.ts
================================================
import { Component } from '../_util/simply';
import { ImageIconProps } from './props';
Component({
props: {
image: '',
},
});
================================================
FILE: src/ImageIcon/props.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { IBaseProps } from '../_util/base';
export interface ImageIconProps extends IBaseProps {
image: string;
}
export const ImageIconDefaultProps: Partial = {
image: '',
};
================================================
FILE: src/ImageUpload/index.axml
================================================
{{ u.isPropsEmpty(uploadingText) ? locale.imageUpload.uploadingText : uploadingText }}
{{ u.isPropsEmpty(uploadfailedText) ? locale.imageUpload.uploadfailedText : uploadfailedText }}
================================================
FILE: src/ImageUpload/index.en.md
================================================
---
nav:
path: /components
group:
title: Information Entry
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# ImageUpload
It is used to upload pictures to the server, and briefly display the information of the uploaded files and provide preview function. Use when you need to upload local pictures or photos to the server.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-uploader": "antd-mini/es/ImageUpload/index"
#endif
#if WECHAT
"ant-uploader": "antd-mini/ImageUpload/index"
#endif
}
```
## Code Sample
### Basic use
> `onBeforeUpload`、`onUpload` Function Receive `LocalFile`, you can customize the upload-related business logic.`preview` The event is triggered by clicking on the uploaded picture and can be coordinated [my.previewimage](https://opendocs.alipay.com/mini/api/media/image/my.previewimage) Realize the picture preview.`change` event will be triggered when the list of uploaded files changes.
```xml
```
```js
Page({
data: {
defaultFileList: [{ url: 'https://gw.alipayobjects.com/mdn/rms_226d75/afts/img/A*znK_ToIL8rQAAAAAAAAAAAAAARQnAQ', status: 'done' }],
#if WECHAT
onUpload(file) {
return new Promise((resolve) => {
console.log('上传的图片为:', file);
setTimeout(() => {
resolve(
'https://gw.alipayobjects.com/mdn/rms_226d75/afts/img/A*5m0ZQYhxhjEAAAAAAAAAAAAAARQnAQ'
);
}, 2000);
});
},
onBeforeUpload(localFileList) {
console.log('即将上传的图片列表为:', localFileList);
localFileList = localFileList.filter((item) => item.size < 10000);
console.log('修改上传的图片列表为:', localFileList);
return localFileList;
},
#endif
},
onChange(fileList) {
console.log('图片列表:', fileList);
},
onPreview(file) {
console.log('preview', file);
},
#if ALIPAY
onUpload(file) {
return new Promise((resolve) => {
console.log('上传的图片为:', file);
setTimeout(() => {
resolve(
'https://gw.alipayobjects.com/mdn/rms_226d75/afts/img/A*5m0ZQYhxhjEAAAAAAAAAAAAAARQnAQ'
);
}, 2000);
});
},
onBeforeUpload(localFileList) {
console.log('即将上传的图片列表为:', localFileList);
localFileList = localFileList.filter((item) => item.size < 10000);
console.log('修改上传的图片列表为:', localFileList);
return localFileList;
},
#endif
});
```
### Controlled Mode
> `fileList`and `change` Events cooperate to achieve controlled mode. If you want the component to trigger the upload logic externally, pass `ref` way to get the component instance and call the component. `chooseImage` method can be.
```xml
Component External Trigger Upload Logic
```
```js
Page({
data: {
fileList: [{ url: 'https://gw.alipayobjects.com/mdn/rms_226d75/afts/img/A*znK_ToIL8rQAAAAAAAAAAAAAARQnAQ', status: 'done' }],
#if WECHAT
onUpload(file) {
return new Promise((resolve) => {
console.log('上传的图片为:', file);
setTimeout(() => {
resolve(
'https://gw.alipayobjects.com/mdn/rms_226d75/afts/img/A*5m0ZQYhxhjEAAAAAAAAAAAAAARQnAQ'
);
}, 2000);
});
},
#endif
},
#if ALIPAY
onUpload(file) {
return new Promise((resolve) => {
console.log('上传的图片为:', file);
setTimeout(() => {
resolve(
'https://gw.alipayobjects.com/mdn/rms_226d75/afts/img/A*5m0ZQYhxhjEAAAAAAAAAAAAAARQnAQ'
);
}, 2000);
});
},
#endif
handleControlledChange(fileList) {
#if WECHAT
this.setData({
fileList: fileList.detail,
});
#endif
#if ALIPAY
this.setData({
fileList,
});
#endif
},
handleUploaderRef(ref) {
console.log('handleUploaderRef', ref);
#if WECHAT
this.handleUploaderRef = ref.detail;
#endif
#if ALIPAY
this.handleUploaderRef = ref;
#endif
},
upload() {
this.handleUploaderRef.chooseImage();
},
});
```
### Demo Code
## API
| Property | Description | Type | Default Value |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------- |
| className | Class Name | string | - |
| maxCount | Maximum number of uploaded images. See details in [maxCount](#maxcount) | number | - |
| defaultFileList | File List Initial Value | [File](#file)[] | [] |
| sourceType | Upload the selected source. Optional `['album']`、`['camera']` or `['album', 'camera']` | string[] | ['album', 'camera'] |
| style | Style | string | - |
| uploadingText | Prompt copy in upload | string | 'Uploading......' |
| uploadfailedText | Prompt copy for upload failure | string | 'Upload failed' |
| fileList | File List (Controlled) | [File](#file)[] | - |
| imageMode | Picture zoom mode and crop mode. See details in [Image Mode Description](https://opendocs.alipay.com/mini/component/image#mode) | string | scaleToFill |
| onBeforeUpload | The hook before uploading the file. The parameter is the list of uploaded files. If false is returned, the upload is stopped. You can return a Promise object. When you Promise an object to reject, the upload is stopped and the upload is started when it is resolve (resolve the modified localFileList is passed in). | (localFileList: [localFile](#localfile)[]) => boolean \| Promise<[localFile](#localfile)[]> | - |
| onChooseImageError | Select picture failure callback. See details in [onChooseImageError](###onChooseImageError) | (err) => void | - |
| onUpload | Image upload method. See details in [onUpload](#onupload) | (localFile: [LocalFile](#localfile)) => Promise\ | - |
| onRemove | Click the callback when removing files. Do not remove if the return value is false. Support to return a Promise object, Promise object resolve(false) or reject does not remove | (file: [File](#file)) => boolean \| Promise\ | - |
| #if ALIPAY onChange | Triggered when the list of uploaded files changes | (fileList: [File](#file)[]) => void | - |
| #if ALIPAY onPreview | Triggered when an image is clicked | (file: [File](#file)[]) => void | - |
| #if WECHAT bindchange | Triggered when the list of uploaded files changes | (fileList: [File](#file)[]) => void | - |
| #if WECHAT bindpreview | Triggered when an image is clicked | (file: [File](#file)[]) => void | - |
### File
| Parameters | Description | Type | Default Value |
| ------ | ------------------------------------------------------------------------------ | ------ | ------ |
| url | image url | string | - |
| status | Upload status. Optional `uploading`(Uploading),`done`(upload complete) or `error`(Upload failed) | string | - |
| uid | The unique identifier. Automatically generated when not set | string | - |
| path | Local Image Path | string | - |
| size | Local image size. Some models may not return this attribute | number | - |
### LocalFile
| Parameters | Description | Type | Default Value |
| ---- | ------------------------------------------ | ------ | ------ |
| path | Local Image Path | string | - |
| size | Local image size. Some models may not return this attribute | number | - |
### onUpload
`onUpload` Method Receive `LocalFile`Return in the Promise. `File`. The following is the call [my.uploadFile](https://opendocs.alipay.com/mini/api/kmq4hc) Sample code for uploading:
```js
onUpload(localFile) {
return new Promise((resolve, reject) => {
my.uploadFile({
url: 'https://...', // 请替换成有效的服务端 url
fileType: 'image',
name: 'userfile', // 根据后台服务需求替换
filePath: localFile.path, // 传入 localFile.path
formData: { extra: '其他信息' }, // 根据后台服务需求替换
success: res => {
// 根据后台返回,得到上传成功的image url
const { url } = JSON.parse(res.data);
resolve(url); // 调用 resolve 传入image url
},
fail: err => {
reject(); // 上传错误调用 reject
},
});
});
}
```
### onChooseImageError
Selecting a picture may fail,`onChooseImageError` can get errors, you can view [my.chooseImage Error Code](https://opendocs.alipay.com/mini/api/media/image/my.chooseimage#%E9%94%99%E8%AF%AF%E7%A0%81)。
```js
onChooseImageError(err) {
console.log(err);
}
```
### maxCount
1. `maxCount` Indicates the maximum number of pictures that can be uploaded, and does not transmit means unlimited. Since the small program selection picture interface has an upper limit of one-time selection of pictures, if `maxCount` More than this limit, multiple uploads are required. For example,`maxCount="{{10}}"`, 10 pictures are allowed to be uploaded. Users need to upload 9 pictures first and then 1 picture.
2. `maxCount` No restrictions `defaultFileList` and `fileList`. For example,`maxCount` 1, and `defaultFileList` Contains 2 images, will not reduce `defaultFileList` in the picture. The upload button is hidden.
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| --image-upload-cover-background | rgba(0, 0, 0, 0.4)
| rgba(0, 0, 0, 0.4)
| Image upload cover background color (overlay) |
| --image-upload-wrapper-background | #f5f5f5
| #121212
| Picture upload package background color |
| --image-upload-text-color | #ffffff
| #ffffff
| Image upload text color |
| --image-upload-background-color | rgba(0, 0, 0, 0.4)
| #1a1a1a
| Image upload background color |
================================================
FILE: src/ImageUpload/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-icon": "../Icon/index",
"loading": "../Loading/index"
}
}
================================================
FILE: src/ImageUpload/index.less
================================================
@import (reference) './variable.less';
@imageUploadPrefix: ant-image-upload;
.@{imageUploadPrefix} {
display: flex;
flex-wrap: wrap;
background: @image-upload-background-color;
&-show {
position: relative;
}
&-image {
width: 160 * @rpx;
height: 160 * @rpx;
margin: @image-upload-common-margin;
border-radius: @corner-radius-md;
}
&-close {
width: @image-upload-close-tip-width;
height: @image-upload-close-tip-height;
position: absolute;
z-index: 99;
top: @image-upload-size-base * -1;
right: 0;
background: url(@image-upload-close-tip-url) no-repeat;
background-size: cover;
}
&-add-image-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 160 * @rpx;
height: 160 * @rpx;
margin: @image-upload-add-image-container-margin;
background-color: @image-upload-wrapper-background;
border-radius: @corner-radius-md;
}
&-add-image-icon {
color: @COLOR_TEXT_ASSIST;
}
&-cover {
position: absolute;
width: 160 * @rpx;
height: 160 * @rpx;
margin: @image-upload-common-margin;
border-radius: @corner-radius-md;
background-color: @image-upload-cover-background;
display: flex;
justify-content: center;
align-items: center;
&-loading {
text-align: center;
&-icon {
width: 48 * @rpx;
height: 48 * @rpx;
}
&-text {
color: @image-upload-text-color;
font-size: 24 * @rpx;
margin-top: 8 * @rpx;
}
}
&-error {
text-align: center;
&-icon {
font-size: 48rpx;
color: @image-upload-text-color;
}
&-text {
color: @image-upload-text-color;
font-size: 24rpx;
margin-top: 8rpx;
}
}
}
}
================================================
FILE: src/ImageUpload/index.md
================================================
---
nav:
path: /components
group:
title: 数据录入
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# ImageUpload 图片上传
用于将图片上传到服务器,并简略展示上传文件的信息及提供预览功能。需要将本地图片或拍照后上传到服务器时使用。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-uploader": "antd-mini/es/ImageUpload/index"
#endif
#if WECHAT
"ant-uploader": "antd-mini/ImageUpload/index"
#endif
}
```
## 代码示例
### 基本使用
> `onBeforeUpload`、`onUpload` 函数接收 `LocalFile`,可以定制上传相关业务逻辑。`preview` 事件是点击已上传图片触发,可以配合 [my.previewimage](https://opendocs.alipay.com/mini/api/media/image/my.previewimage) 实现图片预览。`change` 事件则将在已上传的文件列表变化时触发。
```xml
```
```js
Page({
data: {
defaultFileList: [{ url: 'https://gw.alipayobjects.com/mdn/rms_226d75/afts/img/A*znK_ToIL8rQAAAAAAAAAAAAAARQnAQ', status: 'done' }],
#if WECHAT
onUpload(file) {
return new Promise((resolve) => {
console.log('上传的图片为:', file);
setTimeout(() => {
resolve(
'https://gw.alipayobjects.com/mdn/rms_226d75/afts/img/A*5m0ZQYhxhjEAAAAAAAAAAAAAARQnAQ'
);
}, 2000);
});
},
onBeforeUpload(localFileList) {
console.log('即将上传的图片列表为:', localFileList);
localFileList = localFileList.filter((item) => item.size < 10000);
console.log('修改上传的图片列表为:', localFileList);
return localFileList;
},
#endif
},
onChange(fileList) {
console.log('图片列表:', fileList);
},
onPreview(file) {
console.log('preview', file);
},
#if ALIPAY
onUpload(file) {
return new Promise((resolve) => {
console.log('上传的图片为:', file);
setTimeout(() => {
resolve(
'https://gw.alipayobjects.com/mdn/rms_226d75/afts/img/A*5m0ZQYhxhjEAAAAAAAAAAAAAARQnAQ'
);
}, 2000);
});
},
onBeforeUpload(localFileList) {
console.log('即将上传的图片列表为:', localFileList);
localFileList = localFileList.filter((item) => item.size < 10000);
console.log('修改上传的图片列表为:', localFileList);
return localFileList;
},
#endif
});
```
### 受控模式
> `fileList`和 `change` 事件配合实现受控模式。想要组件外部触发上传逻辑,通过 `ref` 的方式拿到组件实例,并调用组件中的 `chooseImage` 方法即可。
```xml
组件外部触发上传逻辑
```
```js
Page({
data: {
fileList: [{ url: 'https://gw.alipayobjects.com/mdn/rms_226d75/afts/img/A*znK_ToIL8rQAAAAAAAAAAAAAARQnAQ', status: 'done' }],
#if WECHAT
onUpload(file) {
return new Promise((resolve) => {
console.log('上传的图片为:', file);
setTimeout(() => {
resolve(
'https://gw.alipayobjects.com/mdn/rms_226d75/afts/img/A*5m0ZQYhxhjEAAAAAAAAAAAAAARQnAQ'
);
}, 2000);
});
},
#endif
},
#if ALIPAY
onUpload(file) {
return new Promise((resolve) => {
console.log('上传的图片为:', file);
setTimeout(() => {
resolve(
'https://gw.alipayobjects.com/mdn/rms_226d75/afts/img/A*5m0ZQYhxhjEAAAAAAAAAAAAAARQnAQ'
);
}, 2000);
});
},
#endif
handleControlledChange(fileList) {
#if WECHAT
this.setData({
fileList: fileList.detail,
});
#endif
#if ALIPAY
this.setData({
fileList,
});
#endif
},
handleUploaderRef(ref) {
console.log('handleUploaderRef', ref);
#if WECHAT
this.handleUploaderRef = ref.detail;
#endif
#if ALIPAY
this.handleUploaderRef = ref;
#endif
},
upload() {
this.handleUploaderRef.chooseImage();
},
});
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------- |
| className | 类名 | string | - |
| maxCount | 上传图片限制的最大数量。详见 [maxCount](#maxcount) | number | - |
| defaultFileList | 文件列表初始值 | [File](#file)[] | [] |
| sourceType | 上传选择的来源。可选 `['album']`、`['camera']` 或 `['album', 'camera']` | string[] | ['album', 'camera'] |
| style | 样式 | string | - |
| uploadingText | 上传中提示文案 | string | '上传中……' |
| uploadfailedText | 上传失败提示文案 | string | '上传失败' |
| fileList | 文件列表(受控) | [File](#file)[] | - |
| imageMode | 图片缩放模式和裁剪模式。详见 [图片 mode 说明](https://opendocs.alipay.com/mini/component/image#mode) | string | scaleToFill |
| onBeforeUpload | 上传文件之前的钩子。参数为上传的文件列表,若返回 false 则停止上传。支持返回一个 Promise 对象,Promise 对象 reject 时则停止上传,resolve 时开始上传(resolve 传入修改后的 localFileList) | (localFileList: [localFile](#localfile)[]) => boolean \| Promise<[localFile](#localfile)[]> | - |
| onChooseImageError | 选择图片失败回调。详见 [onChooseImageError](###onChooseImageError) | (err) => void | - |
| onUpload | 图片上传方法。详见 [onUpload](#onupload) | (localFile: [LocalFile](#localfile)) => Promise\ | - |
| onRemove | 点击移除文件时的回调。返回值为 false 时不移除。支持返回一个 Promise 对象,Promise 对象 resolve(false) 或 reject 时不移除 | (file: [File](#file)) => boolean \| Promise\ | - |
| #if ALIPAY onChange | 已上传的文件列表变化时触发 | (fileList: [File](#file)[]) => void | - |
| #if ALIPAY onPreview | 点击图片时触发 | (file: [File](#file)[]) => void | - |
| #if WECHAT bindchange | 已上传的文件列表变化时触发 | (fileList: [File](#file)[]) => void | - |
| #if WECHAT bindpreview | 点击图片时触发 | (file: [File](#file)[]) => void | - |
### File
| 参数 | 说明 | 类型 | 默认值 |
| ------ | ------------------------------------------------------------------------------ | ------ | ------ |
| url | 图片 url | string | - |
| status | 上传状态。可选 `uploading`(上传中)、`done`(上传完成)或 `error`(上传失败) | string | - |
| uid | 唯一标识符。不设置时会自动生成 | string | - |
| path | 本地图片路径 | string | - |
| size | 本地图片大小。有的机型可能没有返回这个属性 | number | - |
### LocalFile
| 参数 | 说明 | 类型 | 默认值 |
| ---- | ------------------------------------------ | ------ | ------ |
| path | 本地图片路径 | string | - |
| size | 本地图片大小。有的机型可能没有返回这个属性 | number | - |
### onUpload
`onUpload` 方法接收 `LocalFile`,在 Promise 里返回 `File`。以下是调用 [my.uploadFile](https://opendocs.alipay.com/mini/api/kmq4hc) 进行上传的示例代码:
```js
onUpload(localFile) {
return new Promise((resolve, reject) => {
my.uploadFile({
url: 'https://...', // 请替换成有效的服务端 url
fileType: 'image',
name: 'userfile', // 根据后台服务需求替换
filePath: localFile.path, // 传入 localFile.path
formData: { extra: '其他信息' }, // 根据后台服务需求替换
success: res => {
// 根据后台返回,得到上传成功的图片 url
const { url } = JSON.parse(res.data);
resolve(url); // 调用 resolve 传入图片 url
},
fail: err => {
reject(); // 上传错误调用 reject
},
});
});
}
```
### onChooseImageError
选择图片可能会失败,`onChooseImageError` 可获取错误,可以查看 [my.chooseImage 错误码](https://opendocs.alipay.com/mini/api/media/image/my.chooseimage#%E9%94%99%E8%AF%AF%E7%A0%81)。
```js
onChooseImageError(err) {
console.log(err);
}
```
### maxCount
1. `maxCount` 表示最多可以上传的图片数量,不传表示无限制。由于小程序选择图片界面有一次性选择图片的上限,如果 `maxCount` 大于此上限,则需要多次上传。例如,`maxCount="{{10}}"`,允许上传 10 张图片,用户需先上传 9 张,之后再上传 1 张。
2. `maxCount` 不限制 `defaultFileList` 和 `fileList`。例如,`maxCount` 为 1,而 `defaultFileList` 包含 2 张图片,不会减少 `defaultFileList` 里的图片。上传按钮则会隐藏。
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| --image-upload-cover-background | rgba(0, 0, 0, 0.4)
| rgba(0, 0, 0, 0.4)
| 图片上传封面背景颜色(覆盖) |
| --image-upload-wrapper-background | #f5f5f5
| #121212
| 图片上传包裹背景颜色 |
| --image-upload-text-color | #ffffff
| #ffffff
| 图片上传文本颜色 |
| --image-upload-background-color | rgba(0, 0, 0, 0.4)
| #1a1a1a
| 图片上传背景颜色 |
================================================
FILE: src/ImageUpload/index.ts
================================================
import { effect } from '@preact/signals-core';
import createValue from '../mixins/value';
import { chooseImage } from '../_util/jsapi/choose-image';
import {
ComponentWithSignalStoreImpl,
getValueFromProps,
triggerEvent,
} from '../_util/simply';
import { assertAilpayNativeNotSupport } from '../_util/support';
import i18nController from '../_util/store';
import { File, LocalFile, UploaderDefaultProps } from './props';
assertAilpayNativeNotSupport('ImageUpload');
ComponentWithSignalStoreImpl({
storeOptions: {
store: () => i18nController,
updateHook: effect,
mapState: {
locale: ({ store }) => store.currentLocale.value,
},
},
props: UploaderDefaultProps,
methods: {
async chooseImage() {
const [onBeforeUpload, onUpload, onChooseImageError] = getValueFromProps(
this,
['onBeforeUpload', 'onUpload', 'onChooseImageError']
);
if (!onUpload) {
throw new Error('need props onUpload');
}
const fileList = this.getValue();
const [maxCount, sourceType] = getValueFromProps(this, [
'maxCount',
'sourceType',
]);
let localFileList: LocalFile[];
try {
const chooseImageRes = await chooseImage({
count:
typeof maxCount === 'number' && !isNaN(maxCount)
? maxCount - fileList.length
: 9999,
sourceType,
});
localFileList = (
chooseImageRes.tempFiles ||
chooseImageRes.tempFilePaths ||
chooseImageRes.apFilePaths ||
chooseImageRes.filePaths ||
[]
)
.map((item) => {
if (typeof item === 'string') {
return {
path: item,
};
}
if (item.path) {
return {
path: item.path,
size: item.size,
};
}
})
.filter((item) => !!item);
} catch (err) {
onChooseImageError(err);
return;
}
if (onBeforeUpload) {
try {
const beforeUploadRes = await onBeforeUpload(localFileList);
if (beforeUploadRes === false) {
return;
}
if (Array.isArray(beforeUploadRes)) {
localFileList = beforeUploadRes;
}
} catch (err) {
return;
}
}
const tasks = localFileList.map((file) => this.uploadFile(file));
await Promise.all(tasks);
},
async uploadFile(localFile: LocalFile) {
const onUpload = getValueFromProps(this, 'onUpload');
const uid = this.getCount();
const tempFileList = [
...this.getValue(),
{
path: localFile.path,
size: localFile.size,
uid,
status: 'uploading',
},
];
if (!this.isControlled()) {
this.update(tempFileList);
}
triggerEvent(this, 'change', tempFileList);
try {
const url = await onUpload(localFile);
if (typeof url !== 'string' || !url) {
this.updateFile(uid, {
status: 'error',
});
return;
}
this.updateFile(uid, {
status: 'done',
url,
});
} catch (err) {
this.updateFile(uid, {
status: 'error',
});
}
},
updateFile(uid: string, file: Partial) {
const fileList = this.getValue();
const tempFileList = fileList.map((item) => {
if (item.uid === uid) {
return {
...item,
...file,
};
}
return item;
});
if (!this.isControlled()) {
this.update(tempFileList);
}
triggerEvent(this, 'change', tempFileList);
},
async onRemove(e) {
const fileList = this.getValue();
const onRemove = getValueFromProps(this, 'onRemove');
const { uid } = e.currentTarget.dataset;
const file = fileList.find((item) => item.uid === uid);
if (onRemove) {
const result = await onRemove(file);
if (result === false) {
return;
}
}
const tempFileList = fileList.filter((item) => item.uid !== uid);
if (!this.isControlled()) {
this.update(tempFileList);
}
triggerEvent(this, 'change', tempFileList);
},
onPreview(e) {
const { uid } = e.currentTarget.dataset;
const fileList = this.getValue();
const file = fileList.find((item) => item.uid === uid);
triggerEvent(this, 'preview', file);
},
updateShowUploadButton() {
const maxCount = getValueFromProps(this, 'maxCount');
this.setData({
showUploadButton: !maxCount || this.getValue().length < maxCount,
});
},
count: 0,
getCount() {
// 使用 Date.now() 与 useId 作为前缀,防止每次前缀都相同
this.count = (this.count || 0) + 1;
// 使用 Date.now() 与 useId 作为前缀,防止每次前缀都相同
let id = this.id;
/// #if ALIPAY
id = this.$id;
/// #endif
const prefix = id + '-' + Date.now();
return `${prefix}-${this.count}`;
},
},
mixins: [
createValue({
defaultValueKey: 'defaultFileList',
valueKey: 'fileList',
transformValue(fileList = []) {
return {
needUpdate: true,
value: (fileList || []).map((item) => {
const file = {
...item,
};
if (typeof item.url === 'undefined') {
file.url = '';
}
if (typeof item.uid === 'undefined') {
file.uid = this.getCount();
}
if (typeof item.status === 'undefined') {
file.status = 'done';
}
return file;
}),
};
},
}),
],
/// #if ALIPAY
didMount() {
this.updateShowUploadButton();
},
didUpdate(prevProps, prevData) {
if (!this.isEqualValue(prevData)) {
this.updateShowUploadButton();
}
},
/// #endif
/// #if WECHAT
attached() {
this.triggerEvent('ref', this);
this.updateShowUploadButton();
this._prevData = this.data;
},
observers: {
'**': function (data) {
const prevData = this._prevData || this.data;
this._prevData = { ...data };
if (!this.isEqualValue(prevData)) {
this.updateShowUploadButton();
}
},
},
/// #endif
});
================================================
FILE: src/ImageUpload/props.ts
================================================
import { IBaseProps } from '../_util/base';
export type Status = 'uploading' | 'done' | 'error';
export interface File {
/**
* @description 唯一标识符,不设置时会自动生成
*/
uid?: string;
/**
* @description 图片的资源地址
*/
url: string;
/**
* @description 上传状态
*/
status?: Status;
}
export interface LocalFile {
path: string;
size?: number;
}
export interface IUploaderProps extends IBaseProps {
/**
* @description 默认已经上传的文件列表
* @default []
*/
defaultFileList: File[];
/**
* @description 已经上传的文件列表(受控)
*/
fileList: File[];
/**
* @description 上传图片的最大数量
*/
maxCount: number;
/**
* @description 图片缩放模式和裁剪模式
* @default 'scaleToFill'
*/
imageMode:
| 'scaleToFill'
| 'aspectFit'
| 'aspectFill'
| 'widthFix'
| 'heightFix'
| 'top'
| 'bottom'
| 'center'
| 'left'
| 'right'
| 'top left'
| 'top right'
| 'bottom left'
| 'bottom right';
/**
* @description 视频选择的来源
* @default ['album', 'camera']
*/
sourceType: ['album'] | ['camera'] | ['camera', 'album'];
/**
* @description 图片上传前的回调函数,返回 false 可终止图片上传,支持返回 Promise
*/
onBeforeUpload?: (
localFileList: LocalFile[]
) => boolean | Promise;
/**
* @description 选择图片失败回调
*/
onChooseImageError?: (err: any) => void;
/**
* @description 已上传的文件列表变化时触发
*/
onChange?: (v: Array) => void;
/**
* @description 删除当前列表中的图片时触发,包括上传成功和上传失败的图片,如果返回 false 表示阻止删除,支持返回 Promise
*/
onRemove?: (v: File) => boolean | Promise;
/**
* @description 点击图片进行预览时触发,会覆盖默认的预览功能
*/
onPreview?: (v: Array) => void;
/**
* @description 自定义上传方式,只在不存在action字段时生效
*/
onUpload?: (localFile: LocalFile) => Promise;
/**
* @description 上传中文案
* @default "上传中……"
*/
uploadingText?: string;
/**
* @description 上传失败文案
* @default "上传失败"
*/
uploadfailedText?: string;
}
export const UploaderDefaultProps: Partial = {
defaultFileList: [],
fileList: null,
maxCount: null,
imageMode: 'scaleToFill',
sourceType: ['camera', 'album'],
onUpload: null,
onBeforeUpload: null,
onRemove: null,
uploadingText: null,
uploadfailedText: null,
};
================================================
FILE: src/ImageUpload/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
@image-upload-size-base: 4 * @rpx;
@image-upload-margin-size-base: 4 * @rpx;
@image-upload-margin-size-1: @image-upload-margin-size-base * 1;
@image-upload-margin-size-2: @image-upload-margin-size-base * 2;
@image-upload-margin-size-3: @image-upload-margin-size-base * 3;
@image-upload-common-margin: @image-upload-margin-size-1
@image-upload-margin-size-2;
@image-upload-add-image-container-margin: @image-upload-margin-size-1
@image-upload-margin-size-2 @image-upload-margin-size-3;
@image-upload-close-tip-width: @image-upload-size-base * 10;
@image-upload-close-tip-height: @image-upload-size-base * 10;
@image-upload-close-tip-url: 'https://gw.alipayobjects.com/mdn/rms_226d75/afts/img/A*_Az1QavR4OsAAAAAAAAAAAAAARQnAQ';
@image-upload-background-color: var(
--image-upload-background-color,
@COLOR_CARD
);
@image-upload-wrapper-background: var(
--image-upload-wrapper-background,
@COLOR_GREY_CARD
);
@image-upload-cover-background: var(
--image-upload-cover-background,
rgba(0, 0, 0, 0.4)
);
@image-upload-text-color: var(--image-upload-text-color, @COLOR_WHITE);
================================================
FILE: src/IndexBar/index.axml
================================================
{{item.label}}
{{item.label}}
================================================
FILE: src/IndexBar/index.en.md
================================================
---
nav:
path: /components
group:
title: Information Display
order: 8
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# IndexBar
Side index component. Used to quickly locate a list index.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
"ant-index-bar": "antd-mini/es/IndexBar/index"
}
```
## Code Sample
### Basic use
```xml
```
```css
.base {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.indexbar {
width: calc(100% - 20px);
display: flex;
align-items: center;
justify-content: flex-end;
}
```
```js
Page({
data: {
items: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map((u) => {
return { label: u };
}),
},
});
```
### Use in conjunction with a list
```xml
#if ALIPAY
{{props.value.label}}-{{itemX}}
#endif
#if WECHAT
因微信不支持作用域插槽,暂不支持此用法
#endif
```
```css
#if ALIPAY .base {
width: 100%;
height: 100vh;
}
.indexbar {
position: fixed;
right: 10px;
top: 20vh;
}
# endif #if WECHAT Because WeChat does not support scope slots, this usage is temporarily not supported# endif;
```
```js
#if ALIPAY
Page({
data: {
items: [],
},
onLoad() {
this.setData({
items: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map((u) => {
return { label: u };
}),
});
},
});
#endif
#if WECHAT
因微信不支持作用域插槽,暂不支持此用法
#endif
```
### Controlled Mode
> Reference below [Demo Code](#demo-代码) implementation in.
### Demo Code
## API
| Property | Description | Type | Default Value |
| ---------------------- | --------------------------------- | --------------------------------------------- | ------ |
| activeClassName | Style when index is active | string | - |
| className | Class Name | string | - |
| current | Index value | string | - |
| defaultCurrent | Default Index | string | - |
| labelPreview | Index preview content, receiving value and index | slot | - |
| items | Index Array | [Item](#item) | [] |
| style | Style | string | - |
| size | Dimensions of the index (width and height in px) | number | 16 |
| vibrate | Whether it vibrates when the index changes | boolean | true |
| #if ALIPAY onChange | Callback when index changes | (value: [Item](#item), index: number) => void |
| #if WECHAT bindchange | Callback when index changes | (value: [Item](#item), index: number) => void |
#### Item
| Property | Description | Type | Default Value |
| -------------- | ------------------------ | ------- | ------ |
| label | Index ID | string | - |
| disablePreview | Disable preview effect when index is triggered | boolean | - |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For more information, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| -------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------ |
| --index-bar-tip-background-color | #cccccc
| #474747
| Index bar prompt background color |
| --index-bar-text-color | #ffffff
| #ffffff
| Index column text color |
| --index-bar-assist-color | #999999
| #616161
| Index Bar Secondary Text Color |
| --index-bar-active-color | #1677ff
| #3086ff
| Index Bar Activation Color |
================================================
FILE: src/IndexBar/index.json
================================================
{
"styleIsolation": "shared",
"component": true
}
================================================
FILE: src/IndexBar/index.less
================================================
@import (reference) './variable.less';
.ant-indexbar-side {
z-index: 9;
&-mask {
position: fixed;
width: 100vw;
height: 100vh;
left: 0;
top: 0;
z-index: 9;
}
&-content {
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
text-align: center;
}
&-item {
position: relative;
padding-left: 20 * @rpx;
&-default {
font-size: 22 * @rpx;
font-weight: bold;
color: @index-bar-assist-color;
display: flex;
align-items: center;
justify-content: center;
}
&-tip {
position: absolute;
left: -158 * @rpx;
top: 50%;
transform: translateY(-50%);
width: 94 * @rpx;
height: 94 * @rpx;
border-radius: 94 * @rpx;
line-height: 94 * @rpx;
text-align: center;
background: @index-bar-tip-background-color;
color: @index-bar-text-color;
font-size: 50 * @rpx;
}
}
&-active {
border-radius: 16 * @rpx;
background-color: @index-bar-active-color;
color: @index-bar-text-color;
}
&-scroll {
width: 100%;
height: 100%;
overflow: scroll;
scrollbar-width: none;
}
&-notSee {
position: fixed;
left: -200%;
top: 0;
z-index: -1;
}
}
================================================
FILE: src/IndexBar/index.md
================================================
---
nav:
path: /components
group:
title: 数据展示
order: 8
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# IndexBar 索引
侧边索引组件。用于快速定位列表索引。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
"ant-index-bar": "antd-mini/es/IndexBar/index"
}
```
## 代码示例
### 基本使用
```xml
```
```css
.base {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.indexbar {
width: calc(100% - 20px);
display: flex;
align-items: center;
justify-content: flex-end;
}
```
```js
Page({
data: {
items: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map((u) => {
return { label: u };
}),
},
});
```
### 结合列表使用
```xml
#if ALIPAY
{{props.value.label}}-{{itemX}}
#endif
#if WECHAT
因微信不支持作用域插槽,暂不支持此用法
#endif
```
```css
#if ALIPAY .base {
width: 100%;
height: 100vh;
}
.indexbar {
position: fixed;
right: 10px;
top: 20vh;
}
#endif #if WECHAT 因微信不支持作用域插槽,暂不支持此用法 #endif;
```
```js
#if ALIPAY
Page({
data: {
items: [],
},
onLoad() {
this.setData({
items: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map((u) => {
return { label: u };
}),
});
},
});
#endif
#if WECHAT
因微信不支持作用域插槽,暂不支持此用法
#endif
```
### 受控模式
> 参考下面 [Demo 代码](#demo-代码) 中的实现。
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| ---------------------- | --------------------------------- | --------------------------------------------- | ------ |
| activeClassName | 索引激活时的样式 | string | - |
| className | 类名 | string | - |
| current | 索引值 | string | - |
| defaultCurrent | 默认索引 | string | - |
| labelPreview | 索引预览内容,接收 value 和 index | slot | - |
| items | 索引数组 | [Item](#item) | [] |
| style | 样式 | string | - |
| size | 索引的尺寸(宽高,单位 px) | number | 16 |
| vibrate | 索引改变时是否震动 | boolean | true |
| #if ALIPAY onChange | 索引改变时的回调 | (value: [Item](#item), index: number) => void |
| #if WECHAT bindchange | 索引改变时的回调 | (value: [Item](#item), index: number) => void |
#### Item
| 属性 | 说明 | 类型 | 默认值 |
| -------------- | ------------------------ | ------- | ------ |
| label | 索引标识 | string | - |
| disablePreview | 禁用索引触发时的预览效果 | boolean | - |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| -------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------ |
| --index-bar-tip-background-color | #cccccc
| #474747
| 索引栏提示背景颜色 |
| --index-bar-text-color | #ffffff
| #ffffff
| 索引栏文本颜色 |
| --index-bar-assist-color | #999999
| #616161
| 索引栏辅助文本颜色 |
| --index-bar-active-color | #1677ff
| #3086ff
| 索引栏激活颜色 |
================================================
FILE: src/IndexBar/index.ts
================================================
import equal from 'fast-deep-equal';
import {
getAllInstanceBoundingClientRect,
getInstanceBoundingClientRect,
} from '../_util/jsapi/get-instance-bounding-client-rect';
import {
Component,
getValueFromProps,
triggerEventValues,
} from '../_util/simply';
import { assertAilpayNativeNotSupport } from '../_util/support';
import { IndexBarDefaultProps } from './props';
assertAilpayNativeNotSupport('IndexBar');
Component({
props: IndexBarDefaultProps,
data: {
touchClientY: 0, // 按下触摸点所处页面的高度
touchKeyIndex: -1, // 选中的字母
touchKey: '', // 触发的key
itemHeight: 16, // 每个字母的高度
moving: false, // 滑动进行时
showMask: false, // 打开遮罩,防止和页面的滑动重叠了
currentKey: 0,
topRange: [],
hasDefaultSlot: true,
},
methods: {
getInstance() {
if (this.$id) {
return my;
}
return this;
},
init(nextProps) {
const { defaultCurrent, current, items } = nextProps;
this.initItemHeight();
/// #if ALIPAY
this.initTopRange();
/// #endif
const initCurrent = this.isControlled(nextProps)
? current
: defaultCurrent;
const _index = items?.findIndex((u) => initCurrent === u.label);
this.setData(
{
currentKey: _index,
touchKeyIndex: _index,
touchKey: initCurrent,
},
() => {
this.setData({
touchKeyIndex: -1,
touchKey: '',
});
}
);
},
isControlled(nextProps, valueKey = 'current') {
if ('controlled' in nextProps) {
return nextProps.controlled;
}
return valueKey in nextProps;
},
// 初始化每个块的高度,用已计算滑动距离
async initItemHeight() {
const ret = await getInstanceBoundingClientRect(
this.getInstance(),
`#ant-alphabet-0`
);
if (ret === null) return;
const { height } = ret;
this.setData({ itemHeight: height });
},
onTouchStart(e: any) {
const { moving } = this.data;
const items = getValueFromProps(this, 'items');
if (moving) return;
const { item, index } = e.currentTarget.dataset.item;
const point = (e && e.touches && e.touches[0]) || {};
const { clientY } = point;
this.setData({
touchClientY: clientY,
touchKeyIndex: index,
touchKey: items[index].label,
moving: true,
showMask: true,
currentKey: index,
});
this.onAlphabetClick(item, index); // 触摸开始
},
async onAlphabetClick(item, index) {
const vibrate = getValueFromProps(this, 'vibrate');
/// #if ALIPAY
vibrate && (await my.vibrateShort());
/// #endif
/// #if WECHAT
// @ts-ignore
vibrate && (await wx.vibrateShort());
/// #endif
triggerEventValues(this, 'change', [item, index]);
},
onTouchEnd() {
// 没进入moving状态就不处理
if (!this.data.moving) return;
this.setData({
touchKeyIndex: -1,
touchKey: '',
showMask: false,
moving: false,
});
},
onTouchMove(e: any) {
const { touchClientY, touchKeyIndex, itemHeight, touchKey } = this.data;
const items = getValueFromProps(this, 'items');
const point = e.changedTouches[0];
const movePageY = point.clientY;
// 滑动距离
const movingHeight = Math.abs(movePageY - touchClientY);
// 滑动几个itemHeight的距离即等于滑动了几格,不那么精准,但是几乎可以忽略不计
const movingNum = parseInt(`${movingHeight / itemHeight}`, 10);
// 上 or 下
const isUp = movePageY < touchClientY;
// 新的触发的索引应该在哪个index
const newIndex = isUp
? touchKeyIndex - movingNum
: touchKeyIndex + movingNum;
// 超出索引列表范围 or 索引没变化return
if (!items[newIndex] || touchKey === items[newIndex].label) return;
// 结算
this.setData({ touchKey: items[newIndex].label, currentKey: newIndex });
this.onAlphabetClick(items[newIndex], newIndex);
},
onScroll(e) {
const { topRange, currentKey, moving } = this.data;
const items = getValueFromProps(this, 'items');
const { scrollTop } = e.detail;
let newIndex = 0;
if (scrollTop + 1 > topRange[topRange.length - 1]) {
newIndex = topRange.length;
} else {
newIndex = topRange?.findIndex((h) => scrollTop + 1 < h);
}
if (currentKey !== newIndex - 1 && newIndex - 1 >= 0 && !moving) {
this.setData({
currentKey: newIndex - 1,
});
this.onAlphabetClick(items[newIndex - 1], newIndex - 1);
}
},
async initTopRange() {
const ret = await getAllInstanceBoundingClientRect(
this.getInstance(),
'.ant-indexbar-side-list'
);
if (ret.length === 0) return;
const arr = [];
ret.forEach((u) => {
arr.push(u.top - ret[0].top);
});
this.setData({ topRange: arr, hasDefaultSlot: !!ret[0].height });
},
},
/// #if ALIPAY
didMount() {
this.init(getValueFromProps(this));
},
deriveDataFromProps(nextProps) {
const _prop = getValueFromProps(this);
const { current, items } = nextProps;
if (!equal(_prop.items, nextProps.items)) {
this.init(nextProps);
}
if (_prop.current !== current) {
const _index = items?.findIndex((u) => current === u.label);
this.setData({
currentKey: _index,
});
if (!this.isControlled(nextProps)) {
this.setData({
touchKeyIndex: _index,
touchKey: current,
});
}
}
},
/// #endif
/// #if WECHAT
attached() {
this.init(getValueFromProps(this));
},
observers: {
'**': function (data) {
const prevData = this._prevData || this.data;
this._prevData = { ...data };
if (!equal(prevData.items, data.items)) {
this.init(data.items);
}
if (!equal(prevData.current, data.current)) {
const _index = data.items.findIndex((u) => data.current === u.label);
this.setData({
currentKey: _index,
});
if (!this.isControlled(data)) {
this.setData({
touchKeyIndex: _index,
touchKey: data.current,
});
}
}
},
},
/// #endif
});
================================================
FILE: src/IndexBar/props.ts
================================================
import { IBaseProps } from '../_util/base';
interface ItemObj {
label: string;
disablePreview?: boolean;
}
export interface IndexBarProps extends IBaseProps {
/**
* @description 触发的索引样式
*/
activeClassName: string;
/**
* @description 默认触发的索引
*/
defaultCurrent: string;
/**
* @description 触发的索引
*/
current: string;
/**
* @description 索引触发时是否震动
*/
vibrate: boolean;
/**
* @description 索引列表
*/
items: ItemObj[];
/**
* @description 索引的尺寸
*/
size: number;
/**
* @description 触发索引时的回调
*/
onChange: (value: ItemObj, index: number) => void;
}
export const IndexBarDefaultProps: Partial = {
className: '',
activeClassName: '',
defaultCurrent: null,
vibrate: true,
items: [],
size: 16,
style: '',
onChange: (value, index) => {},
};
================================================
FILE: src/IndexBar/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
@index-bar-tip-background-color: var(
--index-bar-tip-background-color,
@COLOR_TEXT_WEAK
);
@index-bar-text-color: var(--index-bar-text-color, @COLOR_WHITE);
@index-bar-assist-color: var(--index-bar-assist-color, @COLOR_TEXT_ASSIST);
@index-bar-active-color: var(--index-bar-active-color, @COLOR_BRAND1);
================================================
FILE: src/Input/InputBlur/index.axml
================================================
================================================
FILE: src/Input/InputBlur/index.json
================================================
{
"styleIsolation": "shared",
"component": true
}
================================================
FILE: src/Input/InputBlur/index.ts
================================================
import mixinValue from '../../mixins/value';
import { Component, triggerEvent } from '../../_util/simply';
import { InputBlurDefaultProps } from './props';
Component({
props: InputBlurDefaultProps,
focus: false,
methods: {
onChange(e) {
const value = e.detail.value;
if (this.isControlled()) {
this.update(value, {}, true);
}
triggerEvent(this, 'change', value, e);
},
onFocus(e) {
const value = e.detail.value;
this.focus = true;
triggerEvent(this, 'focus', value, e);
},
onBlur(e) {
const value = e.detail.value;
this.focus = false;
if (this.isControlled()) {
this.update(this.props.value);
}
triggerEvent(this, 'blur', value, e);
},
onConfirm(e) {
const value = e.detail.value;
triggerEvent(this, 'confirm', value, e);
},
},
mixins: [
mixinValue({
scopeKey: 'state',
transformValue(value, extra, updateWithoutFocusCheck) {
if (value === null || (!updateWithoutFocusCheck && this.focus)) {
return {
needUpdate: false,
};
}
return {
needUpdate: true,
value,
};
},
}),
],
});
================================================
FILE: src/Input/InputBlur/props.ts
================================================
import { IBaseProps } from '../../_util/base';
export type InputType =
| 'text'
| 'number'
| 'idcard'
| 'digit'
| 'numberpad'
| 'digitpad'
| 'idcardpad';
/**
* @description 输入框。
*/
export interface InputBlurProps extends IBaseProps {
value: string;
defaultValue: string;
placeholder: string;
placeholderClassName: string;
placeholderStyle: string;
enableNative: boolean;
confirmType: string;
confirmHold: string;
alwaysSystem: boolean;
selectionStart: number;
selectionEnd: number;
cursor: number;
controlled: boolean;
maxLength?: number;
inputClassName: string;
inputStyle: string;
focus?: boolean;
password: boolean;
disabled?: boolean;
/**
* @description 组件名字,用于表单提交获取数据。
*/
name?: string;
type?: InputType;
/**
* @description 当 type 为 number, digit, idcard 数字键盘是否随机排列。
* @default false
*/
randomNumber?: boolean;
onChange?: (value: string, e: any) => void;
onBlur?: (value: string, e: any) => void;
onFocus?: (value: string, e: any) => void;
onConfirm?: (value: string, e: any) => void;
}
export const InputBlurDefaultProps: Partial = {
value: null,
defaultValue: null,
placeholder: null,
placeholderClassName: '',
placeholderStyle: '',
enableNative: null,
confirmType: null,
confirmHold: null,
alwaysSystem: null,
selectionStart: null,
selectionEnd: null,
cursor: null,
controlled: null,
inputClassName: null,
inputStyle: null,
focus: null,
password: null,
disabled: null,
name: null,
type: null,
randomNumber: null,
/// #if WECHAT
// 微信小程序的 maxLength 需要为 -1 才能不限制输入长度。 不能传 null
maxLength: -1,
/// #endif
};
================================================
FILE: src/Input/Textarea/index.axml
================================================
================================================
FILE: src/Input/Textarea/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-icon": "../../Icon/index"
}
}
================================================
FILE: src/Input/Textarea/index.less
================================================
@import (reference) '../variable.less';
@inputItemPrefix: ant-textarea;
.@{inputItemPrefix} {
display: flex;
align-items: center;
background: @input-background-color;
&-disabled {
opacity: 0.4;
}
&-line {
position: relative;
flex: 1;
display: flex;
align-items: center;
overflow: hidden;
}
&-content {
width: 100%;
align-self: center;
padding: 0;
font-size: 34 * @rpx;
text-align: left;
color: @input-item-color;
background: @input-background-color;
&-clear {
padding-right: 34 * @rpx;
}
}
&-clear {
position: absolute;
top: 0;
right: 0;
z-index: 2;
display: flex;
justify-content: center;
align-items: center;
border-radius: 16 * @rpx;
margin-left: 8 * @rpx;
&-icon {
color: @input-item-placeholder-color;
font-size: 34 * @rpx;
}
}
&-clear-show {
display: flex;
}
&-clear-hidden {
display: none;
pointer-events: none;
}
&-placeholder {
font-size: 34 * @rpx;
color: @input-item-placeholder-color;
margin-left: -6 * @rpx;
}
}
================================================
FILE: src/Input/Textarea/index.ts
================================================
import { effect } from '@preact/signals-core';
import mixinValue from '../../mixins/value';
import { ComponentWithSignalStoreImpl, triggerEvent } from '../../_util/simply';
import i18nController from '../../_util/store';
import { TextareaDefaultProps } from './props';
ComponentWithSignalStoreImpl({
storeOptions: {
store: () => i18nController,
updateHook: effect,
mapState: {
locale: ({ store }) => store.currentLocale.value,
},
},
props: TextareaDefaultProps,
data: {
selfFocus: false,
},
methods: {
onChange(e) {
const value = e.detail.value;
if (!this.isControlled()) {
this.update(value);
}
triggerEvent(this, 'change', value, e);
},
onFocus(e) {
const value = e.detail.value;
this.setData({
selfFocus: true,
});
triggerEvent(this, 'focus', value, e);
},
onBlur(e) {
const value = e.detail.value;
this.setData({
selfFocus: false,
});
triggerEvent(this, 'blur', value, e);
},
onConfirm(e) {
const value = e.detail.value;
triggerEvent(this, 'confirm', value, e);
},
onClear(e) {
if (!this.isControlled()) {
this.update('');
}
triggerEvent(this, 'change', '', e);
},
},
mixins: [mixinValue({ scopeKey: 'state' })],
/// #if WECHAT
attached() {
this.triggerEvent('ref', this);
},
/// #endif
});
================================================
FILE: src/Input/Textarea/props.ts
================================================
import { IBaseProps } from '../../_util/base';
/**
* 有效值:return(显示“换行”)、done(显示“完成”)、go(显示“前往”)、next(显示“下一个”)、search(显示“搜索”)、send(显示“发送”)。
*/
export type ConfirmType = 'return' | 'done' | 'go' | 'next' | 'search' | 'send';
/**
* @description 输入框。
*/
export interface TextareaProps extends IBaseProps {
value?: string;
defaultValue: string;
placeholder: string;
placeholderClassName: string;
placeholderStyle: string;
autoHeight: boolean;
showCount: boolean;
allowClear: boolean;
controlled: boolean;
enableNative?: boolean;
maxLength?: number;
inputClassName: string;
disabled?: boolean;
inputStyle: string;
focusStyle?: string;
name?: string;
confirmType?: ConfirmType;
focus?: boolean;
confirmHold?: string;
/// #if WECHAT
focusClassName?: boolean;
/**
* @default true
* @description 是否显示键盘上方带有”完成“按钮那一栏
*/
showConfirmBar?: boolean;
/**
* @default false
* @description focus时,点击页面的时候不收起键盘
*/
holdKeyboard?: boolean;
/**
* @default false
* @description 是否去掉 iOS 下的默认内边距
*/
disableDefaultPadding?: boolean;
/**
* @description 键盘对齐位置
*/
adjustKeyboardTo?: 'cursor' | 'bottom';
/// #endif
onChange?: (value: string, e: any) => void;
onBlur?: (value: string, e: any) => void;
onFocus?: (value: string, e: any) => void;
onConfirm?: (value: string, e: any) => void;
}
export const TextareaDefaultProps: TextareaProps = {
value: null,
defaultValue: null,
placeholder: null,
placeholderClassName: null,
placeholderStyle: null,
autoHeight: null,
showCount: null,
allowClear: null,
controlled: null,
enableNative: false,
inputClassName: null,
disabled: null,
inputStyle: null,
focusStyle: null,
name: null,
confirmType: null,
focus: null,
confirmHold: null,
/// #if WECHAT
focusClassName: null,
maxLength: -1,
showConfirmBar: true,
holdKeyboard: false,
disableDefaultPadding: false,
adjustKeyboardTo: 'cursor',
/// #endif
};
================================================
FILE: src/Input/index.axml
================================================
{{ prefix }}
{{ suffix }}
================================================
FILE: src/Input/index.en.md
================================================
---
nav:
path: /components
group:
title: Information Entry
order: 10
toc: content
supportPlatform: ['alipay', 'wechat']
---
# Input
Enter content through the keyboard, is the most basic form field packaging. Generally used in the form page for information collection.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-input": "antd-mini/es/Input/index",
"ant-textarea": "antd-mini/es/Input/Textarea/index"
#endif
#if WECHAT
"ant-input": "antd-mini/Input/index",
"ant-textarea": "antd-mini/Input/Textarea/index"
#endif
}
```
## Code Sample
### Input basic usage
```xml
```
### With Prefix and Suffix
```xml
¥
RMB
```
### Controlled Mode
```xml
```
### Enter Amount
```xml
¥
RMB
```
### SearchBar search box
```xml
Cancel
```
```css
.search-line {
display: flex;
align-items: center;
padding: 16px;
}
.search-bar {
padding: 4px 0 4px 0;
border-radius: 4px;
border: 1px solid transparent;
transition: all 0.4s;
flex: 1;
&-focus {
border-color: #1677ff;
}
}
.cancel {
color: #fff;
margin-left: 8px;
}
```
### Textarea
#### Basic Usage
```xml
```
#### Controlled Mode
```xml
```
### Custom
#### With border
```xml
```
```css
.custom {
border: 1px solid #ccc;
padding: 4px;
border-radius: 4px;
margin-bottom: 12px;
}
```
#### Custom Background Color
```xml
```
```css
.custom-color {
margin-bottom: 12px;
input,
textarea {
padding: 4px;
background: #f5f5f5;
border-radius: 4px;
}
}
```
### Demo Code
## API
### Input, Textarea the same property
| Property | Description | Type | Default Value |
| ---------------------- | -------------------------------------------------------------------------------- | ------------------------------------- | ------ |
| className | Class Name | string | - |
| controlled | There are keyboard input problems, you can use `controlled="{{false}}"` | boolean | - |
| defaultValue | Initial value | string | - |
| disabled | Disable | boolean | false |
| maxLength | Maximum length | number | 140 |
| placeholder | Placeholder | string | - |
| style | Style | string | - |
| max | The maximum value, which takes effect only when type is number, digit, numberpad, or digitpad, and a valid number is entered. | number | - |
| min | The minimum value, which takes effect only when type is number, digit, numberpad, or digitpad, and a valid number is entered. | number | - |
| precision | Calculation accuracy, keep a few decimal places, enter a valid number to take effect | number | -1 |
| value | The value of the input box. Controlled mode. | string | - |
| #if ALIPAY onConfirm | This callback is triggered when the keyboard is clicked to complete | (value: string, event: Event) => void | - |
| #if ALIPAY onFocus | Trigger this callback when focused | (value: string, event: Event) => void | - |
| #if ALIPAY onBlur | Trigger this callback when out of focus | (value: string, event: Event) => void | - |
| #if ALIPAY onChange | This callback is triggered when input | (value: string, event: Event) => void | - |
| #if WECHAT bindconfirm | This callback is triggered when the keyboard is clicked to complete | (value: string, event: Event) => void | - |
| #if WECHAT bindfocus | Trigger this callback when focused | (value: string, event: Event) => void | - |
| #if WECHAT bindblur | Trigger this callback when out of focus | (value: string, event: Event) => void | - |
| #if WECHAT bindchange | This callback is triggered when input | (value: string, event: Event) => void | - |
### Input property
| Property | Description | Type | Default Value |
| ------------ | ---------------------------------- | ------- | ------ |
| allowClear | You can click the clear icon to delete content | boolean | false |
| enableNative | Whether to use native | boolean | - |
| focus | Focus, View[Detailed Description](#input-focus) | boolean | - |
| password | Whether it is password type | boolean | false |
| prefix | input prefix | slot | - |
| suffix | input suffix | slot | - |
### Textarea Properties
| Property | Description | Type | Default Value |
| ------------ | ---------------- | ------- | ------ |
| autoHeight | Whether to increase automatically. | boolean | false |
| enableNative | Whether to use native | boolean | - |
| showCount | Show word count | boolean | true |
### Input, Textarea More Same Properties
- confirm-type
- name
- placeholder-class
- placeholder-style
- type
[Input](https://opendocs.alipay.com/mini/component/input)
[Textarea](https://opendocs.alipay.com/mini/component/textarea)
### Input focus
To use the focus attribute, you need to add it to the app.json window. `"enableInPageRenderInput": "YES"`Otherwise, the keyboard cannot be opened in iOS.
### Input, Textarea instance methods
Instance methods require small programs `component2` Can be used.
| Property | Description | Type |
| -------- | ------ | ----------------------- |
| update | Update Value | (value: string) => void |
| getValue | get value | () => string |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| ------------------------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------ |
| --input-item-color | #333333
| #c5cad1
| Input Text Color |
| --input-item-placeholder-color | #cccccc
| #474747
| Input item placeholder color |
| --input-item-clear-color | #999999
| #616161
| Input Clear Button Color |
| --input-background-color | #ffffff
| #1a1a1a
| Input Background Color |
## FAQ
### Input focus does not open the keyboard
You need to add it to the app.json window. `"enableInPageRenderInput": "YES"`。
### Cursor problem with Input
Can be viewed [This document](https://opendocs.alipay.com/mini/component/input#FAQ) Use `enableNative` property to resolve.
### Keyboard problems using value controlled mode
This is due [A known issue with input boxes](https://opendocs.alipay.com/mini/component/input#Bug%20%26%20Tip)。
The solution is to call the input update method in ref mode without using value controlled mode.
```xml
```
```js
Page({
handleRef(input) {
#if ALIPAY
this.input = input;
#endif
#if WECHAT
this.input = input.detail;
#endif
},
clear() {
this.input.update('');
},
});
```
Starting with v2.15.0, updating values via the update method is not supported in controlled mode.
### Instance method not available
need to use `component2`, for details see[ref Get Component Instance](https://opendocs.alipay.com/mini/framework/component-ref)。
### The input font style is overwritten by css and cannot take effect on ios.
Need to pass in `always-system="{{true}}"`
================================================
FILE: src/Input/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-icon": "../Icon/index"
}
}
================================================
FILE: src/Input/index.less
================================================
@import (reference) './variable.less';
@inputItemPrefix: ant-input;
.@{inputItemPrefix} {
display: flex;
align-items: center;
background: @input-background-color;
&-disabled {
opacity: 0.4;
}
&-line {
position: relative;
flex: 1;
display: flex;
align-items: center;
overflow: hidden;
}
&-prefix {
color: @input-item-placeholder-color;
font-size: 34 * @rpx;
margin: 0 8 * @rpx 0 8 * @rpx;
&:empty {
display: none;
}
}
&-suffix {
color: @input-item-placeholder-color;
font-size: 34 * @rpx;
margin: 0 8 * @rpx 0 8 * @rpx;
&:empty {
display: none;
}
}
&-content {
width: 100%;
align-self: center;
padding: 0;
font-size: 34 * @rpx;
text-align: left;
color: @input-item-color;
background: @input-background-color;
}
&-clear {
display: flex;
justify-content: center;
align-items: center;
border-radius: 16 * @rpx;
&-icon {
color: @input-item-placeholder-color;
font-size: 34 * @rpx;
padding: 8 * @rpx 0 8 * @rpx 16 * @rpx;
}
}
&-clear-show {
opacity: 1;
}
&-clear-hidden {
opacity: 0;
pointer-events: none;
}
&-placeholder {
font-size: 34 * @rpx;
color: @input-item-placeholder-color;
margin-left: -6 * @rpx;
}
}
================================================
FILE: src/Input/index.md
================================================
---
nav:
path: /components
group:
title: 数据录入
order: 10
toc: content
supportPlatform: ['alipay', 'wechat']
---
# Input 输入框
通过键盘输入内容,是最基础的表单域包装。一般用在表单页进行信息的收集。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-input": "antd-mini/es/Input/index",
"ant-textarea": "antd-mini/es/Input/Textarea/index"
#endif
#if WECHAT
"ant-input": "antd-mini/Input/index",
"ant-textarea": "antd-mini/Input/Textarea/index"
#endif
}
```
## 代码示例
### Input 基本使用
```xml
```
### 带前缀后缀
```xml
¥
RMB
```
### 受控模式
```xml
```
### 输入金额
```xml
¥
RMB
```
### SearchBar 搜索框
```xml
取消
```
```css
.search-line {
display: flex;
align-items: center;
padding: 16px;
}
.search-bar {
padding: 4px 0 4px 0;
border-radius: 4px;
border: 1px solid transparent;
transition: all 0.4s;
flex: 1;
&-focus {
border-color: #1677ff;
}
}
.cancel {
color: #fff;
margin-left: 8px;
}
```
### Textarea
#### 基础用法
```xml
```
#### 受控模式
```xml
```
### 自定义
#### 带有边框
```xml
```
```css
.custom {
border: 1px solid #ccc;
padding: 4px;
border-radius: 4px;
margin-bottom: 12px;
}
```
#### 自定义背景色
```xml
```
```css
.custom-color {
margin-bottom: 12px;
input,
textarea {
padding: 4px;
background: #f5f5f5;
border-radius: 4px;
}
}
```
### Demo 代码
## API
### Input、Textarea 相同的属性
| 属性 | 说明 | 类型 | 默认值 |
| ---------------------- | -------------------------------------------------------------------------------- | ------------------------------------- | ------ |
| className | 类名 | string | - |
| controlled | 有键盘输入问题,可使用 `controlled="{{false}}"` | boolean | - |
| defaultValue | 初始值 | string | - |
| disabled | 是否禁用 | boolean | false |
| maxLength | 最大长度 | number | 140 |
| placeholder | 占位符 | string | - |
| style | 样式 | string | - |
| max | 最大值,仅在 type 为 number、digit、numberpad、digitpad 时,且输入有效数字后生效 | number | - |
| min | 最小值,仅在 type 为 number、digit、numberpad、digitpad 时,且输入有效数字后生效 | number | - |
| precision | 计算精度,保留几位小数,输入有效数字后生效 | number | -1 |
| value | 输入框的值。受控模式。 | string | - |
| #if ALIPAY onConfirm | 点击键盘完成时触发此回调 | (value: string, event: Event) => void | - |
| #if ALIPAY onFocus | 聚焦时触发此回调 | (value: string, event: Event) => void | - |
| #if ALIPAY onBlur | 失焦时触发此回调 | (value: string, event: Event) => void | - |
| #if ALIPAY onChange | 输入时触发此回调 | (value: string, event: Event) => void | - |
| #if WECHAT bindconfirm | 点击键盘完成时触发此回调 | (value: string, event: Event) => void | - |
| #if WECHAT bindfocus | 聚焦时触发此回调 | (value: string, event: Event) => void | - |
| #if WECHAT bindblur | 失焦时触发此回调 | (value: string, event: Event) => void | - |
| #if WECHAT bindchange | 输入时触发此回调 | (value: string, event: Event) => void | - |
### Input 属性
| 属性 | 说明 | 类型 | 默认值 |
| ------------ | ---------------------------------- | ------- | ------ |
| allowClear | 可以点击清除图标删除内容 | boolean | false |
| enableNative | 是否使用 native | boolean | - |
| focus | 聚焦,查看[详细说明](#input-focus) | boolean | - |
| password | 是否是密码类型 | boolean | false |
| prefix | input 前缀 | slot | - |
| suffix | input 后缀 | slot | - |
### Textarea 属性
| 属性 | 说明 | 类型 | 默认值 |
| ------------ | ---------------- | ------- | ------ |
| autoHeight | 是否自动增高。 | boolean | false |
| enableNative | 是否使用 native | boolean | - |
| showCount | 是否显示字数统计 | boolean | true |
### Input、Textarea 更多相同属性
- confirm-type
- name
- placeholder-class
- placeholder-style
- type
[Input](https://opendocs.alipay.com/mini/component/input)
[Textarea](https://opendocs.alipay.com/mini/component/textarea)
### Input focus
使用 focus 属性需要在 app.json window 里加上 `"enableInPageRenderInput": "YES"`,否则无法在 iOS 里打开键盘。
### Input、Textarea 实例方法
实例方法需要小程序 `component2` 可使用。
| 属性 | 说明 | 类型 |
| -------- | ------ | ----------------------- |
| update | 更新值 | (value: string) => void |
| getValue | 得到值 | () => string |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| ------------------------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------ |
| --input-item-color | #333333
| #c5cad1
| 输入项文本颜色 |
| --input-item-placeholder-color | #cccccc
| #474747
| 输入项占位符颜色 |
| --input-item-clear-color | #999999
| #616161
| 输入项清除按钮颜色 |
| --input-background-color | #ffffff
| #1a1a1a
| 输入项背景颜色 |
## FAQ
### Input focus 没有打开键盘
需要在 app.json window 里加上 `"enableInPageRenderInput": "YES"`。
### Input 出现光标问题
可查看 [此文档](https://opendocs.alipay.com/mini/component/input#FAQ) 使用 `enableNative` 属性解决。
### 使用 value 受控模式出现键盘问题
这个是由于 [输入框的一个已知问题](https://opendocs.alipay.com/mini/component/input#Bug%20%26%20Tip)。
解决方式为:不使用 value 受控模式,采用 ref 方式调用 input 更新方法。
```xml
```
```js
Page({
handleRef(input) {
#if ALIPAY
this.input = input;
#endif
#if WECHAT
this.input = input.detail;
#endif
},
clear() {
this.input.update('');
},
});
```
从 v2.15.0 开始,受控模式下,不支持通过 update 方法更新值。
### 实例方法不可用
需要使用 `component2`,详情参见[ref 获取组件实例](https://opendocs.alipay.com/mini/framework/component-ref)。
### Input 字体样式通过 css 覆盖,ios 端无法生效
需要传入 `always-system="{{true}}"`
================================================
FILE: src/Input/index.ts
================================================
import { effect } from '@preact/signals-core';
import mixinValue from '../mixins/value';
import {
ComponentWithSignalStoreImpl,
getValueFromProps,
triggerEvent,
} from '../_util/simply';
import i18nController from '../_util/store';
import { assertAilpayNativeNotSupport } from '../_util/support';
import { InputDefaultProps } from './props';
import { formatNumberWithLimits, isNumber } from './utils';
assertAilpayNativeNotSupport('Input');
ComponentWithSignalStoreImpl({
storeOptions: {
store: () => i18nController,
updateHook: effect,
mapState: {
locale: ({ store }) => store.currentLocale.value,
},
},
props: InputDefaultProps,
data: {
selfFocus: false,
},
methods: {
onChange(e) {
const value = e.detail.value;
if (!this.isControlled()) {
this.update(value);
}
triggerEvent(this, 'change', value, e);
},
onFocus(e) {
const value = e.detail.value;
this.setData({
selfFocus: true,
});
triggerEvent(this, 'focus', value, e);
},
onBlur(e) {
let value = e.detail.value;
this.setData({
selfFocus: false,
});
const val = this.checkNumberValue(value);
if (val !== null) {
if (val !== value && !this.isControlled()) {
this.update(val);
}
value = val;
}
triggerEvent(this, 'blur', value, e);
},
onConfirm(e) {
const value = e.detail.value;
triggerEvent(this, 'confirm', value, e);
},
onClear(e) {
if (!this.isControlled()) {
this.update('');
}
triggerEvent(this, 'change', '', e);
},
checkNumberValue(value) {
const [type, max, min, precision] = getValueFromProps(this, [
'type',
'max',
'min',
'precision',
]);
const NUMBER_KEYBOARD = ['number', 'digit', 'numberpad', 'digitpad'];
if (
NUMBER_KEYBOARD.indexOf(type) !== -1 &&
isNumber(value) &&
isNumber(max) &&
isNumber(min)
) {
return formatNumberWithLimits(value, max, min, precision);
}
return null;
},
},
mixins: [mixinValue({ scopeKey: 'state' })],
/// #if WECHAT
attached() {
this.triggerEvent('ref', this);
},
/// #endif
});
================================================
FILE: src/Input/props.ts
================================================
import { IBaseProps } from '../_util/base';
export type InputType =
| 'text'
| 'number'
| 'idcard'
| 'digit'
/**
* 支付宝
*/
| 'numberpad'
| 'digitpad'
| 'idcardpad'
/**
* 只支持微信
*/
| 'safe-password'
| 'nickname';
/**
* @description 输入框。
*/
export interface InputProps extends IBaseProps {
type?: InputType;
value?: string;
defaultValue: string;
placeholder: string;
placeholderClassName: string;
placeholderStyle: string;
allowClear: boolean;
enableNative: boolean;
confirmType: string;
confirmHold: string;
controlled: boolean;
alwaysSystem: boolean;
selectionStart: number;
selectionEnd: number;
cursor: number;
maxLength?: number;
inputClassName?: string;
inputStyle: string;
password?: boolean;
prefix?: string;
disabled?: boolean;
focusClassName?: string;
suffix?: string;
focus?: boolean;
/**
* @description 组件名字,用于表单提交获取数据。
*/
name?: string;
focusStyle?: string;
/**
* @description 当 type 为 number, digit, idcard 数字键盘是否随机排列。
* @default false
*/
randomNumber?: boolean;
/**
* @description 最大值,仅在 type 为 number、digit、numberpad、digitpad 时,且输入有效数字后生效
*/
max?: number;
/**
* @description 最小值,仅在 type 为 number、digit、numberpad、digitpad 时,且输入有效数字后生效
*/
min?: number;
/**
* @description 计算精度,保留几位小数,输入有效数字后生效
* @default -1
*/
precision?: number;
/// #if WECHAT
/**
* @default 指定光标与键盘的距离,取 input 距离底部的距离和 cursor-spacing 指定的距离的最小值作为光标与键盘的距离
*/
cursorSpacing?: number;
/**
* @default 强制 input 处于同层状态,默认 focus 时 input 会切到非同层状态 (仅在 iOS 下生效)
*/
alwaysEmbed?: boolean;
/**
* @description 光标颜色。iOS 下的格式为十六进制颜色值 #000000,安卓下的只支持 default 和 green,Skyline 下无限制
*/
cursorColor?: number;
/**
* @default true
* @description 键盘弹起时,是否自动上推页面
*/
adjustPosition?: boolean;
/**
* @default false
* @description focus时,点击页面的时候不收起键盘
*/
holdKeyboard?: boolean;
/// #endif
onChange?: (value: string, e: any) => void;
onBlur?: (value: string, e: any) => void;
onFocus?: (value: string, e: any) => void;
onConfirm?: (value: string, e: any) => void;
}
export const InputDefaultProps: InputProps = {
type: null,
value: null,
defaultValue: null,
placeholder: null,
placeholderClassName: null,
placeholderStyle: null,
allowClear: null,
enableNative: null,
confirmType: null,
confirmHold: null,
controlled: null,
alwaysSystem: null,
selectionStart: null,
selectionEnd: null,
cursor: null,
/// #if WECHAT
maxLength: -1,
alwaysEmbed: false,
cursorSpacing: 0,
cursorColor: null,
adjustPosition: true,
holdKeyboard: false,
/// #endif
inputClassName: null,
inputStyle: null,
password: null,
prefix: null,
disabled: null,
focusClassName: null,
suffix: null,
focus: null,
name: null,
focusStyle: null,
randomNumber: null,
min: null,
max: null,
precision: -1,
};
================================================
FILE: src/Input/utils.ts
================================================
/**
* 检查给定值是否是一个数字或可以被解析为一个数字
* @param value - 要检查的任意值,类型为 unknown
* @returns 一个布尔值,指示值是否是数字或者可以被解析为数字
*/
export function isNumber(value: unknown): boolean {
if (typeof value === 'number') {
return !isNaN(value);
}
if (typeof value === 'string' && value.trim() !== '') {
return !isNaN(Number(value));
}
return false;
}
/**
* 格式化数字,同时限制其范围和小数精度。
*
* @param value - 要格式化的数值。
* @param max - 数值允许的最大值。若 `value > max`,则将其限制为 `max`。
* @param min - 数值允许的最小值。若 `value < min`,则将其限制为 `min`。
* @param precision - 要保留的小数位数。如果为负值,则返回完整的原始数值(不限制精度)。
* @returns 格式化后的数值字符串。
*/
export function formatNumberWithLimits(
value: number,
max: number,
min: number,
precision: number
): string {
// 确保值在[min, max]范围内
let formattedValue = Math.max(min, Math.min(max, value));
if (precision < 0) {
// 不限制精度,直接返回数值
return String(formattedValue);
} else {
// 限制精度,保留指定的位数并补足零
formattedValue = parseFloat(formattedValue.toFixed(precision));
return formattedValue.toFixed(precision);
}
}
================================================
FILE: src/Input/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
// input 字体颜色
@input-item-color: var(--input-item-color, @COLOR_TEXT_PRIMARY);
// input 字体大小
@input-item-size: @font-size-content;
// input placeholder 颜色
@input-item-placeholder-color: var(
--input-item-placeholder-color,
@COLOR_TEXT_WEAK
);
// input 清除 颜色
@input-item-clear-color: var(--input-item-clear-color, @COLOR_TEXT_ASSIST);
@input-background-color: var(--input-background-color, @COLOR_CARD);
================================================
FILE: src/List/ListItem/index.axml
================================================
{{ title }}
{{ title }}
{{ brief }}
{{ brief }}
================================================
FILE: src/List/ListItem/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-icon": "../../Icon/index",
"image-icon": "../../ImageIcon/index"
}
}
================================================
FILE: src/List/ListItem/index.less
================================================
@import (reference) '../variable.less';
@import '../../style/mixins/hairline.less';
@listItemPrefix: ant-list-item;
.@{listItemPrefix} {
align-items: center;
background-color: @list-background-color;
color: @list-content-color;
line-height: 1.4;
&-hover {
background-color: @list-item-border-color;
}
&-line {
flex: 1;
display: flex;
padding: 0 24 * @rpx;
align-items: center;
position: relative;
&-divider {
.hairline('bottom');
}
&-disabled {
opacity: 0.4;
}
}
&-image {
border-radius: 50vh;
&-large-icon {
font-size: @list-item-image-large-size;
}
&-large-image {
width: @list-item-image-large-size;
height: @list-item-image-large-size;
}
&-normal-icon {
font-size: @list-item-image-normal-size;
}
&-normal-image {
width: @list-item-image-normal-size;
height: @list-item-image-normal-size;
}
&-container {
margin-right: 24 * @rpx;
&:empty {
display: none;
}
}
}
&-content {
&-container {
display: flex;
flex: 1;
flex-direction: column;
padding: 24 * @rpx 0;
&-small-text {
font-size: 24 * @rpx;
line-height: 33 * @rpx;
}
&-normal-text {
font-size: 28 * @rpx;
line-height: 40 * @rpx;
}
}
&-main {
font-size: 32 * @rpx;
line-height: 45 * @rpx;
}
&-title {
&-container {
color: @list-extra-brief-color;
}
}
&-brief {
&-container {
color: @list-extra-brief-color;
}
}
}
&-extra {
font-size: 32 * @rpx;
line-height: 45 * @rpx;
display: flex;
flex-direction: row-reverse;
&:empty {
display: none;
}
&-container {
display: flex;
flex-direction: column;
}
&-brief {
display: flex;
flex-direction: row-reverse;
font-size: 28 * @rpx;
line-height: 40 * @rpx;
color: @list-extra-brief-color;
}
}
&-arrow {
margin-left: 8 * @rpx;
color: @list-item-text-color;
display: flex;
align-items: center;
.ant-icon {
font-size: 36 * @rpx;
}
&-large-margin {
margin-left: 24 * @rpx;
}
}
.line {
height: 1 * @rpx;
background-color: @list-item-border-color;
width: calc(100% - 24 * @rpx);
position: absolute;
right: 0;
}
}
================================================
FILE: src/List/ListItem/index.ts
================================================
import {
Component,
triggerCatchEvent,
triggerEventOnly,
} from '../../_util/simply';
import { IListItemProps } from './props';
Component({
props: {
image: '',
title: '',
brief: '',
arrow: false,
extra: '',
extraBrief: '',
disabled: false,
showDivider: true,
},
methods: {
/// #if ALIPAY
onTap(e) {
if (this.props.disabled) {
return;
}
triggerEventOnly(this, 'tap', e);
},
catchTap(e) {
if (this.props.disabled) {
return;
}
triggerCatchEvent(this, 'catchTap', e);
},
/// #endif
},
});
================================================
FILE: src/List/ListItem/props.ts
================================================
import { IBaseProps } from '../../_util/base';
export interface IListItemProps extends IBaseProps {
/**
* @description 左侧图片
*/
image?: string;
/**
* @description 标题信息
*/
title?: string;
/**
* @description 描述信息
*/
brief?: string;
/**
* @description 箭头方向,不传表示没有箭头
*/
arrow?: boolean | 'right' | 'up' | 'down';
/**
* @description 右侧额外内容,即,辅助信息
*/
extra?: string;
/**
* @description 右侧额外内容,即,次要辅助信息
*/
extraBrief?: string;
/**
* @description 右侧额外内容,即,次要辅助信息
* @default false
*/
disabled?: boolean;
/**
* @description 是否显示下划线
* @default true
*/
showDivider?: boolean;
/**
* @description 点击整行回调
*/
onTap?: (event?: any) => void;
/**
* @description 点击整行回调
*/
catchTap?: (event?: any) => void;
}
export const ListItemDefaultProps: Partial = {
disabled: false,
showDivider: true,
};
================================================
FILE: src/List/index.axml
================================================
================================================
FILE: src/List/index.en.md
================================================
---
nav:
path: /components
group:
title: Information Display
order: 8
toc: 'content'
---
# List
Generic lists can be clean and efficient to carry text, lists, pictures, paragraphs and other elements.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-list": "antd-mini/es/List/index"
#endif
#if WECHAT
"ant-list": "antd-mini/List/index"
#endif
}
```
## Code Sample
### Basic use
```xml
1
2
3
```
### List Item Configuration
```xml
Total assets
Setup
```
### List item clickable
```xml
Total assets
Setup
```
### Demo Code
## API
### List
| Property | Description | Type | Default Value |
| --------- | ---------- | -------------- | ------ |
| className | Class Name | string | - |
| footer | Bottom Description | string \| slot | - |
| header | Head Description | string \| slot | - |
| radius | Whether with rounded corners | boolean | false |
| style | Style | string | - |
### ListItem
| Property | Description | Type | Default Value |
| ------------------------ | -------------------------------------------------------- | ------------------ | ------ |
| arrow | right arrow, optional `right`、`up`、`down`, pass true `right` | string \| boolean | - |
| brief | Second line of information | string \| slot | - |
| className | Class Name | string | - |
| disabled | Disable | boolean | false |
| extra | Extra right | string \| slot | - |
| extraBrief | Auxiliary information on the right side | string \| slot | - |
| image | Picture on the left | string | - |
| radius | Whether with rounded corners | boolean | false |
| showDivider | Show underline or not | boolean | true |
| style | Style | string | - |
| title | Header Information | string \| slot | - |
| #if ALIPAY catchTap | Callback triggered when clicked | (e: Event) => void |
| #if ALIPAY onTap | Callback triggered when clicked | (e: Event) => void |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| ------------------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------- |
| --list-header-color | #999999
| #616161
| List head color |
| --list-footer-color | #999999
| #616161
| List bottom color |
| --list-background-color | #ffffff
| #1a1a1a
| List background color |
| --list-content-color | #333333
| #c5cad1
| List Content Text Color |
| --list-extra-brief-color | #999999
| #616161
| List Extra Brief Color |
| --list-item-border-color | #eeeeee
| #2b2b2b
| List Item Border Color |
| --list-item-text-color | #cccccc
| #474747
| List Item Text Color |
================================================
FILE: src/List/index.json
================================================
{
"styleIsolation": "shared",
"component": true
}
================================================
FILE: src/List/index.less
================================================
@import (reference) './variable.less';
@listPrefix: ant-list;
.@{listPrefix} {
&-radius {
margin: 0 @v-spacing-large;
& .@{listPrefix}-body {
border-radius: @list-radius;
}
.@{listPrefix}-header,
.@{listPrefix}-footer {
padding: @v-spacing-standard 0;
}
}
&-header,
&-footer {
display: flex;
align-items: center;
padding: @v-spacing-standard @v-spacing-large;
line-height: 1.4;
font-size: 30 * @rpx;
color: @list-header-color;
&:empty {
display: none;
}
}
&-body {
position: relative;
overflow: hidden;
// 隐藏最后一根分割线
> .ant-list-item:last-child {
.ant-list-item-line::after {
display: none;
}
}
}
}
================================================
FILE: src/List/index.md
================================================
---
nav:
path: /components
group:
title: 数据展示
order: 8
toc: 'content'
---
# List 列表
通用列表可以干净高效地承载文字、列表、图片、段落等元素。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-list": "antd-mini/es/List/index"
#endif
#if WECHAT
"ant-list": "antd-mini/List/index"
#endif
}
```
## 代码示例
### 基本使用
```xml
1
2
3
```
### 列表项配置
```xml
总资产
设置
```
### 列表项可点击
```xml
总资产
设置
```
### Demo 代码
## API
### List
| 属性 | 说明 | 类型 | 默认值 |
| --------- | ---------- | -------------- | ------ |
| className | 类名 | string | - |
| footer | 底部说明 | string \| slot | - |
| header | 头部说明 | string \| slot | - |
| radius | 是否带圆角 | boolean | false |
| style | 样式 | string | - |
### ListItem
| 属性 | 说明 | 类型 | 默认值 |
| ------------------------ | -------------------------------------------------------- | ------------------ | ------ |
| arrow | 右侧箭头,可选 `right`、`up`、`down`,传 true 为 `right` | string \| boolean | - |
| brief | 第二行信息 | string \| slot | - |
| className | 类名 | string | - |
| disabled | 是否禁用 | boolean | false |
| extra | 右侧额外内容 | string \| slot | - |
| extraBrief | 右侧辅助信息 | string \| slot | - |
| image | 左侧图片 | string | - |
| radius | 是否带圆角 | boolean | false |
| showDivider | 是否显示下划线 | boolean | true |
| style | 样式 | string | - |
| title | 标题信息 | string \| slot | - |
| #if ALIPAY catchTap | 点击时触发的回调 | (e: Event) => void |
| #if ALIPAY onTap | 点击时触发的回调 | (e: Event) => void |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| ------------------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------- |
| --list-header-color | #999999
| #616161
| 列表头部颜色 |
| --list-footer-color | #999999
| #616161
| 列表底部颜色 |
| --list-background-color | #ffffff
| #1a1a1a
| 列表背景颜色 |
| --list-content-color | #333333
| #c5cad1
| 列表内容文本颜色 |
| --list-extra-brief-color | #999999
| #616161
| 列表额外简述颜色 |
| --list-item-border-color | #eeeeee
| #2b2b2b
| 列表项边框颜色 |
| --list-item-text-color | #cccccc
| #474747
| 列表项文本颜色 |
================================================
FILE: src/List/index.ts
================================================
import { Component } from '../_util/simply';
import { IListProps } from './props';
Component({
props: {
radius: false,
header: '',
footer: '',
},
});
================================================
FILE: src/List/props.ts
================================================
import { IBaseProps } from '../_util/base';
/**
* @description 列表,内部配合 ListItem 使用。
*/
export interface IListProps extends IBaseProps {
/**
* @description 是否带圆角
* @default false
*/
radius?: boolean;
/**
* @description 头部说明
*/
header?: string;
/**
* @description 底部说明
*/
footer?: string;
}
export const ListDefaultProps: Partial = {
radius: false,
};
================================================
FILE: src/List/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
// header 颜色
@list-header-color: var(--list-header-color, @COLOR_TEXT_ASSIST);
// header 字体大小
@list-header-size: 28 * @rpx;
// footer 颜色
@list-footer-color: var(--list-footer-color, @COLOR_TEXT_ASSIST);
// footer 字体大小
@list-footer-size: 28 * @rpx;
// 圆角
@list-radius: 24 * @rpx;
// 背景
@list-background-color: var(--list-background-color, @COLOR_CARD);
// 主要文字大小
@list-content-size: @font-size-list;
// 额外文字大小
@list-extra-size: 32 * @rpx;
// 第二行文字大小
@list-brief-size: 26 * @rpx;
// 主要文字颜色
@list-content-color: var(--list-content-color, @COLOR_TEXT_PRIMARY);
// 辅助文字颜色
@list-extra-brief-color: var(--list-extra-brief-color, @COLOR_TEXT_ASSIST);
// 图片尺寸
@list-icon-size: 52 * @rpx;
// 多行,图片尺寸
@list-multi-icon-size: 72 * @rpx;
// 图片和icon的大小
@list-item-image-normal-size: 56 * @rpx;
// 图片和icon的大小
@list-item-image-large-size: 80 * @rpx;
@list-item-border-color: var(--list-item-border-color, @COLOR_BORDER);
@list-item-text-color: var(--list-item-text-color, @COLOR_TEXT_WEAK);
================================================
FILE: src/Loading/index.axml
================================================
.
.
.
================================================
FILE: src/Loading/index.en.md
================================================
---
nav:
path: /components
group:
title: Feedback
toc: 'content'
---
# Loading
Used to prompt that a part or page is loading.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-loading": "antd-mini/es/Loading/index"
#endif
#if WECHAT
"ant-loading": "antd-mini/Loading/index"
#endif
}
```
## Code Sample
### Basic use
```xml
```
### Spin size
```xml
```
### Custom Colors
```xml
```
### Custom Size
```xml
```
### Demo Code
## API
| Property | Description | Type | Default Value |
| --------- | ---------------------------------------------------------------------------------- | ------ | -------- |
| className | Root node class name | string | - |
| color | The color when loading, when type is `spin` Only hexadecimal color codes are supported. | string | `#fff` |
| size | Load icon size, when type is `spin` effective. Optional `small`、`medium`、`large`、`x-large` | string | `medium` |
| style | root node style | string | - |
| type | Load style type, optional `spin`、`mini` | string | `spin` |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For more information, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| --loading-text-color | #d8d8d8
| #454955
| Load Text Color (Iron Black) |
| --loading-icon-light-color | rgba(255, 255, 255, 0.6)
| rgba(255, 255, 255, 0.6)
| Load icon light |
================================================
FILE: src/Loading/index.json
================================================
{
"styleIsolation": "shared",
"component": true
}
================================================
FILE: src/Loading/index.less
================================================
@import (reference) './variable.less';
@loadingPrefix: ant-loading;
.@{loadingPrefix} {
&-small {
width: @loading-icon-size-small;
height: @loading-icon-size-small;
}
&-medium {
width: @loading-icon-size-medium;
height: @loading-icon-size-medium;
}
&-large {
width: @loading-icon-size-large;
height: @loading-icon-size-large;
}
&-x-large {
width: @loading-icon-size-x-large;
height: @loading-icon-size-x-large;
}
&-spin {
display: inline-flex;
justify-content: center;
align-items: center;
&-icon {
width: 100%;
height: 100%;
background-position: center;
background-repeat: no-repeat;
}
}
&-mini {
display: inline-flex;
justify-content: center;
align-items: center;
min-width: 66 * @rpx;
min-height: 66 * @rpx;
&-item {
flex: 0 0 8 * @rpx;
min-width: 8 * @rpx;
min-height: 8 * @rpx;
max-width: 8 * @rpx;
max-height: 8 * @rpx;
overflow: hidden;
margin-right: @h-spacing-standard;
font-size: 0;
border-radius: @corner-radius-sm / 2;
background-color: @COLOR_TEXT_ASSIST;
animation: ant-loading-animation 1s 0s infinite linear;
&__1 {
animation-delay: 0s;
}
&__2 {
animation-delay: 150ms;
}
&__3 {
margin-right: 0;
animation-delay: 300ms;
}
}
}
}
@keyframes ant-loading-animation {
0% {
transform: translate3d(0, 0, 0);
}
12% {
transform: translate3d(0, -150%, 0);
}
40% {
transform: translate3d(0, 110%, 0);
}
55% {
transform: translate3d(0, -10%, 0);
}
60% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
}
================================================
FILE: src/Loading/index.md
================================================
---
nav:
path: /components
group:
title: 反馈引导
toc: 'content'
---
# Loading 加载
用于提示局部或页面在加载中。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-loading": "antd-mini/es/Loading/index"
#endif
#if WECHAT
"ant-loading": "antd-mini/Loading/index"
#endif
}
```
## 代码示例
### 基本使用
```xml
```
### Spin 大小
```xml
```
### 自定义颜色
```xml
```
### 自定义大小
```xml
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| --------- | ---------------------------------------------------------------------------------- | ------ | -------- |
| className | 根节点类名 | string | - |
| color | 加载时的颜色,当 type 为 `spin` 时,只支持十六进制颜色码 | string | `#fff` |
| size | 加载图标尺寸,当 type 为 `spin` 时生效。可选 `small`、`medium`、`large`、`x-large` | string | `medium` |
| style | 根节点 style | string | - |
| type | 加载样式类型,可选 `spin`、`mini` | string | `spin` |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| --loading-text-color | #d8d8d8
| #454955
| 加载文本颜色(铁黑) |
| --loading-icon-light-color | rgba(255, 255, 255, 0.6)
| rgba(255, 255, 255, 0.6)
| 加载图标浅色 |
================================================
FILE: src/Loading/index.sjs.ts
================================================
function getLoadingColor(color) {
if (typeof color === 'string' && color[0] === '#') {
return color.slice(1);
}
}
function getClass(size) {
const list = ['small', 'medium', 'large', 'x-large'];
if (list.indexOf(size) >= 0) {
return `ant-loading-${size}`;
}
return 'ant-loading-medium';
}
export default {
getLoadingColor: getLoadingColor,
getClass: getClass,
};
================================================
FILE: src/Loading/index.ts
================================================
import { LoadingDefaultProps } from './props';
import '../_util/assert-component2';
Component({
/// #if WECHAT
properties: {
type: {
value: 'spin',
type: String,
},
color: {
type: String,
},
size: {
type: String,
},
style: {
type: String,
},
className: {
type: String,
},
},
options: {
styleIsolation: 'shared',
} as unknown,
/// #endif
props: LoadingDefaultProps,
});
================================================
FILE: src/Loading/props.ts
================================================
import { IBaseProps } from '../_util/base';
/**
* @description 加载,用于提示局部或页面在加载中。
*/
export interface ILoadingProps extends IBaseProps {
/**
* @description 加载时的颜色,当 type 为 'spin' 时,只支持十六进制颜色码,如'#fff'
* @default '#fff'
*/
color?: string;
/**
* @description 加载图标尺寸,当 type 为 'spin' 时生效
* @default medium
*/
size?: 'small' | 'medium' | 'large' | 'x-large';
type?: 'spin' | 'mini';
}
export const LoadingDefaultProps: Partial = {
size: 'medium',
color: '#fff',
type: 'spin',
};
================================================
FILE: src/Loading/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
// loading 尺寸
@loading-icon-size-x-large: 140 * @rpx;
@loading-icon-size-large: 140 * 0.75 * @rpx;
@loading-icon-size-medium: 140 * 0.5 * @rpx;
@loading-icon-size-small: 140 * 0.25 * @rpx;
// 辅助文字尺寸
@loading-text-size: @font-size-weak;
// 辅助文字颜色
@loading-text-color: var(--loading-text-color, @color-ironblack-1);
// 浅色颜色
@loading-icon-light-color: var(
--loading-icon-light-color,
rgba(255, 255, 255, 0.6)
);
================================================
FILE: src/Mask/index.axml
================================================
================================================
FILE: src/Mask/index.json
================================================
{
"styleIsolation": "shared",
"component": true
}
================================================
FILE: src/Mask/index.less
================================================
@import (reference) './variable.less';
.ant-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
background-color: @mask-background-color;
width: 300vw;
height: 300vh;
transform: translate3d(-100vw, -100vh, 0);
}
================================================
FILE: src/Mask/index.ts
================================================
import { Component, IPlatformEvent, triggerEventOnly } from '../_util/simply';
Component({
props: {
show: true,
},
methods: {
onMaskTap(e: IPlatformEvent) {
triggerEventOnly(this, 'maskTap', e);
},
},
});
================================================
FILE: src/Mask/props.ts
================================================
import { IBaseProps } from '../_util/base';
/**
* @description 加载,用于提示局部或页面在加载中。
*/
export interface IMaskProps extends IBaseProps {
show: boolean;
onMaskTap?: (v: Record) => void;
}
export const MaskDefaultProps: Partial = {
show: true,
};
================================================
FILE: src/Mask/variable.less
================================================
@import (reference) '../style/variables.less';
@mask-background-color: var(--mask-background-color, rgba(0, 0, 0, 0.55));
================================================
FILE: src/Modal/index.axml
================================================
{{ title }}
{{ title }}
{{ content }}
{{ content }}
{{ primaryButtonText }}
{{ secondaryButtonText }}
{{ cancelButtonText }}
{{ cancelButtonText }}
{{ secondaryButtonText }}
{{ primaryButtonText }}
{{ primaryButtonText }}
{{ secondaryButtonText }}
{{ cancelButtonText }}
================================================
FILE: src/Modal/index.en.md
================================================
---
nav:
path: /components
group:
title: To Discard
order: 99
toc: 'content'
---
# Modal
Not recommended, this component will be obsolete. recommend use [Dialog](/components/dialog) Components, with the latest design specifications.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-modal": "antd-mini/es/Modal/index"
#endif
#if WECHAT
"ant-modal": "antd-mini/Modal/index"
#endif
}
```
### Demo Code
## API
| Property | Description | Type | Default Value |
| ---------------------------------- | ------------------------------------------------- | ------------ | --------- |
| bodyClassName | body class name | string | - |
| bodyStyle | body style | string | - |
| cancelButtonStyle | Cancel Button Style | string | - |
| cancelButtonText | Cancel Button Text | string | - |
| className | Class Name | string | - |
| closable | Whether to display the close button in the upper right corner. Only valid when type is focus | boolean | - |
| content | Content | string\|slot | - |
| destroyOnClose | Whether to unload content when invisible | boolean | false |
| footer | Custom Button Area | slot | - |
| maskClosable | Click whether the layer triggers the onClose. | boolean | true |
| maskClassName | Class name of the layer | string | - |
| maskStyle | Mask Style | string | - |
| primaryButtonStyle | Main Button Style | string | - |
| primaryButtonText | Main button text | string | - |
| secondaryButtonStyle | Auxiliary Button Style | string | - |
| secondaryButtonText | Secondary Button Text | string | - |
| style | Style | string | - |
| title | Title | string\|slot | - |
| type | type, optional `default`(default),`focus`(emphasis mode) | string | `default` |
| visible | Whether to display | boolean | false |
| #if ALIPAY onCancelButtonTap | Cancel button click event | ()=>void | - |
| #if ALIPAY onClose | Click the close button in the upper right corner in the layer or emphasis mode to trigger the callback | ()=>void | - |
| #if ALIPAY onPrimaryButtonTap | Main button click event | ()=>void | - |
| #if ALIPAY onSecondaryButtonTap | Secondary button click event | ()=>void | - |
| #if WECHAT bindcancelbuttontap | Cancel button click event | ()=>void | - |
| #if WECHAT bindclose | Click the close button in the upper right corner in the layer or emphasis mode to trigger the callback | ()=>void | - |
| #if WECHAT bindprimarybuttontap | Main button click event | ()=>void | - |
| #if WECHAT bindsecondarybuttontap | Secondary button click event | ()=>void | - |
================================================
FILE: src/Modal/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-button": "../Button/index",
"ant-icon": "../Icon/index",
"ant-popup": "../Popup/index"
}
}
================================================
FILE: src/Modal/index.less
================================================
@import (reference) './variable.less';
@import (reference) '../style/mixins/hairline.less';
@modalPrefix: ant-modal;
.@{modalPrefix} {
&-body {
width: 560 * @rpx;
background-color: @modal-background-color;
border-radius: @modal-radius;
padding-top: 40 * @rpx;
display: flex;
flex-direction: column;
&-title {
font-size: @font-size-title;
line-height: 50 * @rpx;
text-align: center;
color: @modal-title-color;
margin: 0 24 * @rpx 16 * @rpx;
}
&-content {
line-height: 42 * @rpx;
font-size: @modal-content-size;
color: @modal-content-color;
text-align: center;
margin: 0 24 * @rpx 40 * @rpx;
}
&-footer {
&-focus {
padding: 0 24 * @rpx 24 * @rpx;
&-secondary,
&-cancel {
color: @COLOR_BRAND1;
text-align: center;
font-size: 36 * @rpx;
margin-top: 16 * @rpx;
padding-top: 24 * @rpx;
}
}
&-vertical {
padding: 0 0 24 * @rpx;
&-primary,
&-secondary,
&-cancel {
color: @COLOR_BRAND1;
text-align: center;
font-size: 36 * @rpx;
margin-top: 16 * @rpx;
padding-top: 24 * @rpx;
position: relative;
.hairline('top');
}
}
&-horizontal {
display: flex;
flex-direction: row;
position: relative;
.hairline('top');
&-primary,
&-secondary,
&-cancel {
position: relative;
flex: 1;
color: @COLOR_BRAND1;
text-align: center;
font-size: 36 * @rpx;
padding: 24 * @rpx 0;
&:nth-child(1) {
.hairline('right');
}
}
}
}
}
&-close {
position: absolute;
right: 16 * @rpx;
top: 13 * @rpx;
z-index: 3;
width: 52 * @rpx;
height: 58 * @rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 36 * @rpx;
color: @modal-close-text-color;
}
.ant-popup-content.ant-popup-center {
min-height: 0;
padding: 0;
}
}
================================================
FILE: src/Modal/index.md
================================================
---
nav:
path: /components
group:
title: 待废弃
order: 99
toc: 'content'
---
# Modal 弹窗
不建议使用,此组件即将废弃。推荐使用 [Dialog](/components/dialog) 组件,拥有最新设计规范。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-modal": "antd-mini/es/Modal/index"
#endif
#if WECHAT
"ant-modal": "antd-mini/Modal/index"
#endif
}
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| ---------------------------------- | ------------------------------------------------- | ------------ | --------- |
| bodyClassName | body 类名 | string | - |
| bodyStyle | body 样式 | string | - |
| cancelButtonStyle | 取消按钮样式 | string | - |
| cancelButtonText | 取消按钮文本 | string | - |
| className | 类名 | string | - |
| closable | 是否显示右上角的关闭按钮。仅在 type 为 focus 生效 | boolean | - |
| content | 内容 | string\|slot | - |
| destroyOnClose | 不可见时是否卸载内容 | boolean | false |
| footer | 自定义按钮区 | slot | - |
| maskClosable | 点击蒙层是否触发 onClose | boolean | true |
| maskClassName | 蒙层的类名 | string | - |
| maskStyle | 蒙层的样式 | string | - |
| primaryButtonStyle | 主按钮样式 | string | - |
| primaryButtonText | 主按钮文本 | string | - |
| secondaryButtonStyle | 辅助按钮样式 | string | - |
| secondaryButtonText | 辅助按钮文本 | string | - |
| style | 样式 | string | - |
| title | 标题 | string\|slot | - |
| type | 类型,可选 `default`(默认)、`focus`(强调模式) | string | `default` |
| visible | 是否显示 | boolean | false |
| #if ALIPAY onCancelButtonTap | 取消按钮点击事件 | ()=>void | - |
| #if ALIPAY onClose | 点击蒙层或强调模式下点击右上角关闭按钮,触发回调 | ()=>void | - |
| #if ALIPAY onPrimaryButtonTap | 主按钮点击事件 | ()=>void | - |
| #if ALIPAY onSecondaryButtonTap | 辅助按钮点击事件 | ()=>void | - |
| #if WECHAT bindcancelbuttontap | 取消按钮点击事件 | ()=>void | - |
| #if WECHAT bindclose | 点击蒙层或强调模式下点击右上角关闭按钮,触发回调 | ()=>void | - |
| #if WECHAT bindprimarybuttontap | 主按钮点击事件 | ()=>void | - |
| #if WECHAT bindsecondarybuttontap | 辅助按钮点击事件 | ()=>void | - |
================================================
FILE: src/Modal/index.sjs.ts
================================================
function getDirection(
primaryButtonText,
secondaryButtonText,
cancelButtonText,
type
) {
// 不显示按钮区域
if (!primaryButtonText && !secondaryButtonText && !cancelButtonText) {
return '';
}
// 1个按钮,当纵向处理
if (
[primaryButtonText, secondaryButtonText, cancelButtonText].filter(
(v) => !!v
).length === 1
) {
return 'vertical';
}
// 三个按钮,必是纵向
if (primaryButtonText && secondaryButtonText && cancelButtonText) {
return 'vertical';
}
// 两个按钮,在 “标准模式” 是横向;在“强调模式”是纵向
return type === 'focus' ? 'vertical' : 'horizontal';
}
export default { getDirection };
================================================
FILE: src/Modal/index.ts
================================================
import { Component, triggerEventOnly } from '../_util/simply';
import { ModalFunctionalProps } from './props';
Component({
props: ModalFunctionalProps,
methods: {
onClose() {
triggerEventOnly(this, 'close');
},
onMaskClose() {
/// #if WECHAT
if (this.properties.maskClosable) {
triggerEventOnly(this, 'close');
}
/// #endif
/// #if ALIPAY
if (this.props.maskClosable) {
triggerEventOnly(this, 'close');
}
/// #endif
},
onPrimaryButtonTap() {
triggerEventOnly(this, 'primaryButtonTap');
},
onSecondaryButtonTap() {
triggerEventOnly(this, 'secondaryButtonTap');
},
onCancelButtonTap() {
triggerEventOnly(this, 'cancelButtonTap');
},
},
});
================================================
FILE: src/Modal/props.ts
================================================
import { IBaseProps } from '../_util/base';
/**
* @description 对话框,当应用中需要比较明显的对用户当前的操作行为进行警示或提醒时,可以使用对话框。用户需要针对对话框进行操作后方可结束。
*/
export interface IModalProps extends IBaseProps {
/**
* @description Modal body类名
*/
bodyClassName: string;
/**
* @description Modal body样式
*/
bodyStyle: string;
/**
* @description 遮罩层类名
*/
maskClassName: string;
/**
* @description 遮罩层样式
*/
maskStyle: string;
/**
* @description 是否可点击蒙层关闭
* @default true
*/
maskClosable: boolean;
/**
* @description 类型
*/
type: 'default' | 'focus';
/**
* @description 是否显示右上角的关闭按钮。只有在 type 为 focus 生效
*/
closable: boolean;
/**
* @description 过渡动画时长,单位毫秒
*/
duration: number;
/**
* @description 是否开启过渡动画
*/
animation: boolean;
/**
* @description 弹窗层级
*/
zIndex: number;
/**
* @description 标题
*/
title: string;
/**
* @description 内容
*/
content: string;
/**
* @description 是否可见,受控模式
* @default false
*/
visible: boolean;
/**
* @description 是否关闭后销毁内部元素
* @default false
*/
destroyOnClose?: boolean;
/**
* @description 主按钮文本
*/
primaryButtonText: string;
/**
* @description 辅助按钮文本
*/
secondaryButtonText: string;
/**
* @description 取消按钮文案
*/
cancelButtonText: string;
/**
* @description 主按钮样式
*/
primaryButtonStyle: string;
/**
* @description 辅助按钮样式
*/
secondaryButtonStyle: string;
/**
* @description 取消按钮样式
*/
cancelButtonStyle: string;
/**
* @description 触发关闭时回调
*/
onClose: () => void;
/**
* @description 主按钮点击事件
*/
onPrimaryButtonTap: () => void;
/**
* @description 次要按钮点击事件
*/
onSecondaryButtonTap: () => void;
/**
* @description 取消按钮点击事件
*/
onCancelButtonTap: () => void;
}
export const ModalDefaultProps: Partial = {
visible: false,
maskClosable: true,
closable: true,
type: 'default',
duration: 200,
animation: true,
zIndex: 998,
};
export const ModalFunctionalProps: Partial = {
bodyClassName: '',
bodyStyle: '',
maskClassName: '',
maskStyle: '',
maskClosable: true,
type: 'default',
closable: true,
duration: 200,
animation: true,
zIndex: 998,
title: '',
content: '',
visible: false,
destroyOnClose: false,
primaryButtonText: '',
secondaryButtonText: '',
cancelButtonText: '',
primaryButtonStyle: '',
secondaryButtonStyle: '',
cancelButtonStyle: '',
};
================================================
FILE: src/Modal/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
// modal 背景色
@modal-background-color: var(--modal-background-color, @COLOR_CARD);
// modal 圆角
@modal-radius: @corner-radius-lg;
// 标题颜色
@modal-title-color: var(--modal-title-color, @COLOR_TEXT_PRIMARY);
// 内容大小
@modal-content-size: @font-size-subtitle;
// 内容颜色
@modal-content-color: var(--modal-content-color, @COLOR_TEXT_PRIMARY);
// 关闭图标尺寸
@modal-close-icon-size: 36 * @rpx;
@modal-close-text-color: var(--modal-close-text-color, @COLOR_TEXT_ASSIST);
================================================
FILE: src/NoticeBar/index.axml
================================================
================================================
FILE: src/NoticeBar/index.en.md
================================================
---
nav:
path: /components
group:
title: Feedback
order: 12
toc: content
---
# NoticeBar
Shows a set of message notifications. Usually used for the notification of information in the current page, is a more eye-catching in-page notification.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-notice": "antd-mini/es/NoticeBar/index"
#endif
#if WECHAT
"ant-notice": "antd-mini/NoticeBar/index"
#endif
}
```
## Code Sample
### Basic use
```xml
default
info
error
```
### Can close the notice board
```xml
This notification can be turned off
```
### Multi-line display notice board
```xml
Turn on line feed when text overflows. Not enough text to continue adding text to make up. Not enough text to continue adding text to make up.
```
### Scrollable notice board
```xml
Turn on circular scrolling when text overflows. Not enough text to continue adding text to make up.
```
### Custom
```xml
Customize Left Icon
Customize the left icon image
Customize right button
```
### Demo Code
## API
| Property | Description | Type | Default Value |
| ------------- | ---------------------------------------------------------------------------------------------- | ------------ | ------- |
| className | Class Name | string | - |
| enableMarquee | Whether to turn on scroll animation | boolean | false |
| ellipsisRow | The copy multi-line shows the maximum number of rows, and`enableMarquee`Cannot be configured simultaneously | number\|boolean | false |
| extra | Customize right content | slot | - |
| icon | The icon on the left supports all built-in iconType and custom links, as well as custom slots (WeChat version requires slotIcon settings) | string | - |
| loop | Whether to cycle scrolling,`enableMarquee` Valid when true | boolean | false |
| mode | the type of announcement,`link` Indicates connection, the whole line can be dotted;`closeable` Indicates that clicking x can be closed; If you do not fill in, there is no icon on the right. | string | - |
| style | Style | string | - |
| title | Title | string\|slot | - |
| type | type, optional `default`, `error`, `primary`, `info` | string | default |
| slotIcon | WeChat version needs to enable the slot of icon position through this field | boolean | - |
| onTap | Click the icon (arrow or cross) on the right side of the notification bar to trigger the callback | () => void | - |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| --notice-background-color | #fff9ed
| #fff9ed
| Notification background color |
| --notice-border-color | #fff3d9
| #fff3d9
| Notification border color |
| --notice-color | #ff6010
| #ff6010
| Notification Color |
| --notice-error-border-color | #fff3d9
| #fff3d9
| Error notification border color |
| --notice-error-color | #ffffff
| #ffffff
| Error notification color |
| --notice-error-background-color | #ff3141
| #ff4a58
| Error notification background color |
| --notice-primary-border-color | rgba(22, 119, 255, 0.72)
| rgba(22, 119, 255, 0.72)
| Primary notification border color |
| --notice-primary-color | #1677ff
| #3086ff
| Primary notification color |
| --notice-primary-background-color | rgba(208, 228, 255, 1)
| rgba(208, 228, 255, 1)
| Primary notification background color |
| --notice-info-text-color | #ffffff
| #ffffff
| Information notification text color |
| --notice-info-background-color | #666666
| #808080
| Information notification background color |
================================================
FILE: src/NoticeBar/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-icon": "../Icon/index",
"image-icon": "../ImageIcon/index"
}
}
================================================
FILE: src/NoticeBar/index.less
================================================
@import (reference) './variable.less';
@noticeBarPrefix: ant-notice-bar;
.@{noticeBarPrefix} {
position: relative;
display: flex;
height: 75 * @rpx;
align-items: center;
overflow: hidden;
padding: @v-spacing-standard @h-spacing-large;
font-size: @notice-font-size;
color: @notice-color;
background-color: @notice-background-color;
box-sizing: border-box;
&.@{noticeBarPrefix}-no-height {
height: unset;
align-items: flex-start;
}
&-error {
color: @notice-error-color;
background-color: @notice-error-background-color;
&-scroll-left,
&-scroll-right {
background: @notice-error-background-color;
}
}
&-primary {
color: @notice-primary-color;
background-color: @notice-primary-background-color;
&-scroll-left,
&-scroll-right {
background: @notice-primary-background-color;
}
}
&-info {
color: @notice-info-text-color;
background: @notice-info-background-color;
&-scroll-left,
&-scroll-right {
background: @notice-info-background-color;
}
}
&-icon {
margin-right: @h-spacing-standard;
font-size: 36 * @rpx;
&-image-image {
width: 36 * @rpx;
height: 36 * @rpx;
}
}
&-content {
position: relative;
z-index: 2;
flex: 1 100%;
overflow: hidden;
vertical-align: middle;
line-height: @default-line-height;
}
&-marquee {
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
&-ellipsis {
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
}
&-operation {
display: flex;
align-items: center;
&-icon {
margin-left: 24 * @rpx;
}
}
}
.ant-icon-size-x-small {
font-size: 18px;
}
================================================
FILE: src/NoticeBar/index.md
================================================
---
nav:
path: /components
group:
title: 反馈引导
order: 12
toc: content
---
# NoticeBar 通告栏
展示一组消息通知。通常用于当前页面内信息的通知,是一种较醒目的页面内通知方式。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-notice": "antd-mini/es/NoticeBar/index"
#endif
#if WECHAT
"ant-notice": "antd-mini/NoticeBar/index"
#endif
}
```
## 代码示例
### 基本使用
```xml
default
info
error
```
### 可关闭通告栏
```xml
这条通知可以关闭
```
### 多行展示通告栏
```xml
文本溢出时,开启换行。文字不够继续添加文字凑数。文字不够继续添加文字凑数。
```
### 可滚动通告栏
```xml
文本溢出时,开启循环滚动。文字不够继续添加文字凑数。
```
### 自定义
```xml
自定义左侧图标
自定义左侧图标图片
自定义右侧按钮
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| ------------- | ---------------------------------------------------------------------------------------------- | ------------ | ------- |
| className | 类名 | string | - |
| enableMarquee | 是否开启滚动动画 | boolean | false |
| ellipsisRow | 文案多行展示最大行数,和`enableMarquee`不能同时配置 | number\|boolean | false |
| extra | 自定义右侧内容 | slot | - |
| icon | 左侧 icon,支持所有内置 iconType 和自定义链接,也支持自定义 slot (微信版本需要设置 slotIcon) | string | - |
| loop | 是否循环滚动,`enableMarquee` 为 true 时有效 | boolean | false |
| mode | 通告类型,`link` 表示连接,整行可点;`closeable` 表示点击 x 可以关闭;不填时表示右侧没有图标 | string | - |
| style | 样式 | string | - |
| title | 标题 | string\|slot | - |
| type | 类型,可选 `default`, `error`, `primary`, `info` | string | default |
| slotIcon | 微信版本需要通过此字段启用 icon 位置的插槽 | boolean | - |
| onTap | 点击通知栏右侧的图标(箭头或者叉),触发回调 | () => void | - |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| --notice-background-color | #fff9ed
| #fff9ed
| 通知背景颜色 |
| --notice-border-color | #fff3d9
| #fff3d9
| 通知边框颜色 |
| --notice-color | #ff6010
| #ff6010
| 通知颜色 |
| --notice-error-border-color | #fff3d9
| #fff3d9
| 错误通知边框颜色 |
| --notice-error-color | #ffffff
| #ffffff
| 错误通知颜色 |
| --notice-error-background-color | #ff3141
| #ff4a58
| 错误通知背景颜色 |
| --notice-primary-border-color | rgba(22, 119, 255, 0.72)
| rgba(22, 119, 255, 0.72)
| 主要通知边框颜色 |
| --notice-primary-color | #1677ff
| #3086ff
| 主要通知颜色 |
| --notice-primary-background-color | rgba(208, 228, 255, 1)
| rgba(208, 228, 255, 1)
| 主要通知背景颜色 |
| --notice-info-text-color | #ffffff
| #ffffff
| 信息通知文字颜色 |
| --notice-info-background-color | #666666
| #808080
| 信息通知背景颜色 |
================================================
FILE: src/NoticeBar/index.ts
================================================
import { IBoundingClientRect } from '../_util/base';
import { getInstanceBoundingClientRect } from '../_util/jsapi/get-instance-bounding-client-rect';
import {
Component,
getValueFromProps,
triggerEventOnly,
} from '../_util/simply';
import { NoticeBarDefaultProps } from './props';
Component({
props: NoticeBarDefaultProps,
data: {
show: true,
marqueeStyle: '',
},
methods: {
getInstance() {
if (this.$id) {
return my;
}
return this;
},
async getBoundingClientRectWithId(prefix: string) {
return await getInstanceBoundingClientRect(
this.getInstance(),
`${prefix}${this.$id ? `-${this.$id}` : ''}`
);
},
onTap() {
const mode = getValueFromProps(this, 'mode');
if (mode === 'link') {
triggerEventOnly(this, 'tap');
}
if (mode === 'closeable') {
/// #if ALIPAY
if (typeof this.props.onTap !== 'function') {
return;
}
/// #endif
this.setData({
show: false,
});
triggerEventOnly(this, 'tap');
}
},
startMarquee(state) {
const loop = getValueFromProps(this, 'loop');
const leading = 500;
const { duration, overflowWidth, viewWidth } = state;
let marqueeScrollWidth = overflowWidth;
if (loop) {
marqueeScrollWidth = overflowWidth + viewWidth;
/// #if WECHAT
// 微信的view标签不支持onTransitionEnd,需要这里实现循环
const trailing = 200;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.measureText((state) => {
this.resetMarquee.call(this, state);
this.measureText((state) => {
this.startMarquee.call(this, state);
});
});
clearTimeout(this.timer);
}, trailing + duration * 1000);
/// #endif
}
const newMarqueeStyle = `transform: translate3d(${-marqueeScrollWidth}px, 0, 0); transition: ${duration}s all linear ${
typeof leading === 'number' ? `${leading / 1000}s` : '0s'
};`;
this.setData({
marqueeStyle: newMarqueeStyle,
});
return newMarqueeStyle;
},
measureText(callback) {
const fps = 40;
const loop = getValueFromProps(this, 'loop');
// 计算文本所占据的宽度,计算需要滚动的宽度
setTimeout(async () => {
const marqueeSize: IBoundingClientRect | null =
await this.getBoundingClientRectWithId('.ant-notice-bar-marquee');
const contentSize: IBoundingClientRect | null =
await this.getBoundingClientRectWithId('.ant-notice-bar-content');
const overflowWidth =
(marqueeSize &&
contentSize &&
marqueeSize.width - contentSize.width) ||
0;
const viewWidth = contentSize?.width || 0;
let marqueeScrollWidth = overflowWidth;
if (loop) {
marqueeScrollWidth = overflowWidth + viewWidth;
}
if (overflowWidth > 0) {
callback({
overflowWidth,
viewWidth,
duration: marqueeScrollWidth / fps,
});
}
}, 0);
},
// 文本滚动的计算
resetMarquee(state) {
const loop = getValueFromProps(this, 'loop');
const { viewWidth } = state;
let showMarqueeWidth = '0px';
if (loop) {
showMarqueeWidth = `${viewWidth}px`;
}
const marqueeStyle = `transform: translate3d(${showMarqueeWidth}, 0, 0); transition: 0s all linear;`;
this.setData({
marqueeStyle,
});
},
/// #if ALIPAY
onTransitionEnd() {
const loop = getValueFromProps(this, 'loop');
const trailing = 200;
if (loop) {
const timer = setTimeout(() => {
this.measureText((state) => {
this.resetMarquee.call(this, state);
});
clearTimeout(timer);
}, trailing);
}
},
/// #endif
},
/// #if ALIPAY
didMount() {
const { enableMarquee } = this.props;
if (enableMarquee) {
this.measureText((state) => {
this.startMarquee.call(this, state);
});
}
},
didUpdate() {
const { enableMarquee } = this.props;
// 这里更新处理的原因是防止notice内容在动画过程中发生改变。
if (enableMarquee) {
this.measureText((state) => {
this.startMarquee.call(this, state);
});
}
},
pageEvents: {
onShow() {
if (this.props.enableMarquee) {
this.setData({ marqueeStyle: '' });
this.resetMarquee({
overflowWidth: 0,
duration: 0,
viewWidth: 0,
});
this.measureText((state) => {
this.startMarquee.call(this, state);
});
}
},
},
/// #endif
/// #if WECHAT
attached() {
const { enableMarquee } = this.properties;
if (enableMarquee) {
this.measureText((state) => {
this.startMarquee.call(this, state);
});
}
},
observers: {
'enableMarquee': function (enableMarquee) {
// 这里更新处理的原因是防止notice内容在动画过程中发生改变。
if (enableMarquee) {
this.measureText((state) => {
this.startMarquee.call(this, state);
});
}
},
},
pageLifetimes: {
show: function () {
if (this.properties.enableMarquee) {
this.setData({ marqueeStyle: '' });
this.resetMarquee({
overflowWidth: 0,
duration: 0,
viewWidth: 0,
});
this.measureText((state) => this.startMarquee.call(this, state));
}
},
},
/// #endif
});
================================================
FILE: src/NoticeBar/props.ts
================================================
import { IBaseProps } from '../_util/base';
/**
* @description 通告栏,
* 当应用有重要公告或者由于用户的刷新操作产生提示反馈时可以使用通告栏系统。
* 通告栏不会对用户浏览当前页面内容产生影响,但又能明显的引起用户的注意。公告内容不超过一行。
*/
export interface INoticeBarProps extends IBaseProps {
/**
* @description 消息的展示
*/
icon?: string;
/**
* @description 提示类型
* default 橙色,error 红色,primary 蓝色,info 灰色
* @default "default"
*/
type?: 'default' | 'error' | 'primary' | 'info';
/**
* @description 通告类型,link 表示连接,整行可点;closeable 表示点击 x 可以关闭;不填时表示你右侧没有图标
*/
mode?: 'link' | 'closeable';
/**
* @description 是否开启滚动动画
* @default false
*/
enableMarquee: boolean;
/**
* @description 是否循环滚动,enableMarquee 为 true 时有效
* @default false
*/
loop: boolean;
/**
* @description 微信版本需要通过此字段启用 icon 位置的插槽
* @default false
*/
slotIcon?: boolean;
/**
* @description 多行省略展示的行数配置
* @default false
*/
ellipsisRow: number | boolean;
/**
* @description 点击图标(箭头或者叉,由mode属性决定)的事件回调
*/
onTap: () => void;
/**
* @description 行动点点击回调
* @param 当前点击的行动点序号
*/
onActionTap: (index: number) => void;
}
export const NoticeBarDefaultProps: Partial = {
icon: '',
type: 'default',
mode: null,
enableMarquee: false,
ellipsisRow: false,
loop: false,
slotIcon: false,
};
================================================
FILE: src/NoticeBar/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
// 标准背景
@notice-background-color: var(--notice-background-color, @COLOR_GOLDEN_3);
// 标准边框颜色
@notice-border-color: var(--notice-border-color, @COLOR_GOLDEN_2);
// 标准字体颜色
@notice-color: var(--notice-color, @COLOR_TANGERINE_1);
// 标准字体大小
@notice-font-size: @font-size-subtitle;
// error 边框颜色
@notice-error-border-color: var(--notice-error-border-color, @COLOR_GOLDEN_2);
// error 字体颜色
@notice-error-color: var(--notice-error-color, @COLOR_WHITE);
// error 背景
@notice-error-background-color: var(
--notice-error-background-color,
@COLOR_RED
);
// primary 边框颜色
@notice-primary-border-color: var(
--notice-primary-border-color,
tint(@COLOR_BRAND1, 72%)
);
// primary 字体颜色
@notice-primary-color: var(--notice-primary-color, @COLOR_BRAND1);
// primary 背景
@notice-primary-background-color: var(
--notice-primary-background-color,
tint(@COLOR_BRAND1, 80%)
);
@notice-info-text-color: var(--notice-info-text-color, @COLOR_WHITE);
@notice-info-background-color: var(
--notice-info-background-color,
@COLOR_TEXT_SECONDARY
);
================================================
FILE: src/NumberInput/index.axml
================================================
{{unit}}
{{prefix}}
================================================
FILE: src/NumberInput/index.en.md
================================================
---
nav:
path: /components
group:
title: Bizness Components
order: 15
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# NumberInput
Use when you need to enter an amount and provide a quick amount selection. Supports automatic conversion of units, maximum limit and other functions.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-number-input": "antd-mini/es/NumberInput/index"
#endif
#if WECHAT
"ant-number-input": "antd-mini/NumberInput/index"
#endif
}
```
## Code Sample
### Basic use
## API
### Property
| Property | Description | Type | Default Value |
| ---------------------- | -------------------- | ----------------------- | ------------ |
| className | Class Name | string | - |
| style | Style | string | - |
| value | Value of the input box | string | - |
| title | Title | string | - |
| linkText | Top right link copy | string | - |
| quickAmounts | Quick Amount Options | number[] | - |
| placeholder | Placeholder text | string | 'Please enter amount' |
| prefix | Amount prefix | string | '¥' |
| maxValue | Maximum amount that can be entered | number | - |
| #if ALIPAY onChange | Callback when content changes | (value: string) => void | - |
| #if ALIPAY onLinkTap | Click on the callback link in the upper right corner | () => void | - |
| #if WECHAT bindchange | Callback when content changes | (value: string) => void | - |
| #if WECHAT bindlinktap | Click on the callback link in the upper right corner | () => void | - |
### Slot
Currently only one footer slot is provided for inserting custom content below the input box. More content may be opened later on demand.
Example of use:
```xml
This is a piece of custom content
```
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Light Mode Default | Dark Mode Default | Remarks |
| --------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------ |
| --number-input-background-color | #ffffff
| #1a1a1a
| Background Color |
| --number-input-title-color | #333333
| #c5cad1
| Title Color |
| --number-input-link-color | #4b6b99
| #3f5980
| Top Right Link Color |
| --number-input-unit-color | #999999
| #616161
| Unit color above input box |
| --number-input-border-color | #eeeeee
| #2b2b2b
| Split line color |
| --number-input-prefix-color | #333333
| #c5cad1
| Claw Color |
| --number-input-caret-color | #1677ff
| #3086ff
| Cursor Color |
| --number-input-quick-text-color | #1677ff
| #3086ff
| Quick input text color |
| --number-input-quick-border-color | #1677ff
| #3086ff
| Quick input border color |
================================================
FILE: src/NumberInput/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-input": "../Input/index"
}
}
================================================
FILE: src/NumberInput/index.less
================================================
@import (reference) './variable.less';
@import (reference) '../style/mixins/hairline.less';
@prefixCls: ant-number-input;
.@{prefixCls} {
background: @number-input-background-color;
padding: @v-spacing-standard;
border-radius: @corner-radius-lg;
&-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: @v-spacing-standard;
}
&-title {
font-size: @font-size-subtitle;
color: @number-input-title-color;
}
&-link {
font-size: @font-size-subtitle;
color: @number-input-link-color;
}
&-unit {
height: 28rpx;
margin-top: 30rpx;
margin-left: 50rpx;
&-text {
position: relative;
font-size: @sizeFont5;
color: @number-input-unit-color;
text-align: left;
line-height: 28rpx;
text-indent: 0.5em;
.hairline('left', @number-input-border-color);
}
}
&-content {
position: relative;
display: flex;
align-items: baseline;
margin-bottom: @v-spacing-standard;
.hairline('bottom', @number-input-border-color);
}
&-prefix {
font-size: @sizeFont12;
color: @number-input-prefix-color;
margin-right: @h-spacing-standard;
}
&-field {
flex: 1;
.ant-input-content {
height: 125rpx;
font-size: @sizeFont14;
line-height: 74rpx;
caret-color: @number-input-caret-color;
}
}
&-placeholder {
font-size: @sizeFont5;
}
&-quick {
display: flex;
flex-wrap: wrap;
gap: @h-spacing-standard;
margin-bottom: @v-spacing-standard;
&-item {
font-size: @sizeFont5;
line-height: @default-line-height;
color: @number-input-quick-text-color;
padding: @size-1 @size-3;
border: 1rpx solid @number-input-quick-border-color;
border-radius: @corner-radius-circle;
&:active {
opacity: 0.7;
}
}
}
}
================================================
FILE: src/NumberInput/index.md
================================================
---
nav:
path: /components
group:
title: 业务组件
order: 15
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# NumberInput 金额输入
需要输入金额并提供快捷金额选择时使用。支持单位自动转换、最大值限制等功能。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-number-input": "antd-mini/es/NumberInput/index"
#endif
#if WECHAT
"ant-number-input": "antd-mini/NumberInput/index"
#endif
}
```
## 代码示例
### 基本使用
## API
### 属性
| 属性 | 说明 | 类型 | 默认值 |
| ---------------------- | -------------------- | ----------------------- | ------------ |
| className | 类名 | string | - |
| style | 样式 | string | - |
| value | 输入框的值 | string | - |
| title | 标题 | string | - |
| linkText | 右上角链接文案 | string | - |
| quickAmounts | 快捷金额选项 | number[] | - |
| placeholder | 占位符文本 | string | '请输入金额' |
| prefix | 金额前缀 | string | '¥' |
| maxValue | 最大可输入金额 | number | - |
| #if ALIPAY onChange | 内容变化时的回调 | (value: string) => void | - |
| #if ALIPAY onLinkTap | 点击右上角链接的回调 | () => void | - |
| #if WECHAT bindchange | 内容变化时的回调 | (value: string) => void | - |
| #if WECHAT bindlinktap | 点击右上角链接的回调 | () => void | - |
### 插槽
目前只提供一个 footer 插槽,用于在输入框下方插入自定义内容。后续根据需求可能会开放更多内容。
使用示例:
```xml
这是一条自定义内容
```
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 浅色模式默认值 | 深色模式默认值 | 备注 |
| --------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------ |
| --number-input-background-color | #ffffff
| #1a1a1a
| 背景颜色 |
| --number-input-title-color | #333333
| #c5cad1
| 标题颜色 |
| --number-input-link-color | #4b6b99
| #3f5980
| 右上角链接颜色 |
| --number-input-unit-color | #999999
| #616161
| 输入框上方单位颜色 |
| --number-input-border-color | #eeeeee
| #2b2b2b
| 分割线颜色 |
| --number-input-prefix-color | #333333
| #c5cad1
| 羊角符颜色 |
| --number-input-caret-color | #1677ff
| #3086ff
| 光标颜色 |
| --number-input-quick-text-color | #1677ff
| #3086ff
| 快捷输入文本颜色 |
| --number-input-quick-border-color | #1677ff
| #3086ff
| 快捷输入边框颜色 |
================================================
FILE: src/NumberInput/index.ts
================================================
import {
Component,
getValueFromProps,
triggerEvent,
triggerEventOnly,
} from '../_util/simply';
import { assertAilpayNativeNotSupport } from '../_util/support';
import { NumberInputProps } from './props';
assertAilpayNativeNotSupport('NumberInput');
const UNIT_LIST = [
'百',
'千',
'万',
'十万',
'百万',
'千万',
'亿',
'十亿',
'百亿',
'千亿',
];
Component({
props: NumberInputProps,
data: {
unit: '',
},
methods: {
// 统一处理输入值
update(value: string) {
this.setUnit(value);
triggerEvent(this, 'change', value);
},
handleInput(val) {
let value = val;
/// #if WECHAT
value = val.detail;
/// #endif
// 处理金额输入格式
const formattedValue = this.formatAmount(value);
const checkedValue = this.checkMaxValue(formattedValue);
this.update(checkedValue);
},
handleQuickInput(e) {
const { value } = e.currentTarget.dataset;
this.update(value);
},
formatAmount(value: string) {
// 移除非数字和小数点
value = value.replace(/[^\d.]/g, '');
// 保留两位小数
const parts = value.split('.');
if (parts.length > 2) {
// 移除多余的小数点
value = parts[0] + '.' + parts[1];
}
if (parts[1]?.length > 2) {
// 保留两位小数
value = parts[0] + '.' + parts[1].slice(0, 2);
}
return value;
},
checkMaxValue(value: string) {
const maxValue = getValueFromProps(this, 'maxValue');
if (Number(value) > maxValue) {
return maxValue;
}
return value;
},
handleLinkTap() {
triggerEventOnly(this, 'linkTap');
},
setUnit(value: string) {
const intValue = parseInt(value);
let unit = '';
if (intValue) {
unit = UNIT_LIST[intValue.toString().length - 3] || '';
}
this.setData({ unit });
},
},
/// #if WECHAT
attached() {
this.triggerEvent('ref', this);
},
/// #endif
});
================================================
FILE: src/NumberInput/props.ts
================================================
import { IBaseProps } from '../_util/base';
interface NumberInputProps extends IBaseProps {
/**
* @description 输入框的值
*/
value: string,
/**
* @description 标题
*/
title: string,
/**
* @description 快捷金额选项
*/
quickAmounts: string[],
/**
* @description 占位符文本
*/
placeholder: string,
/**
* @description 最大值
*/
maxValue: number,
/**
* @description 前缀
*/
prefix: string,
/**
* @description 链接文本
*/
linkText: string,
/**
* @description 内容变化时的回调
*/
onChange: (value: string) => void,
/**
* @description 链接点击时的回调
*/
onLinkTap: () => void,
}
export const NumberInputProps: NumberInputProps = {
value: '',
title: '输入金额',
quickAmounts: [],
placeholder: '',
maxValue: Infinity,
prefix: '¥',
linkText: '',
onChange: () => {},
onLinkTap: () => {},
}
================================================
FILE: src/NumberInput/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
// 背景色
@number-input-background-color: var(--number-input-background-color, @COLOR_WHITE);
// 标题文字颜色
@number-input-title-color: var(--number-input-title-color, @COLOR_TEXT_PRIMARY);
// 链接文字颜色
@number-input-link-color: var(--number-input-link-color, @COLOR_LINK);
// 单位文字颜色
@number-input-unit-color: var(--number-input-unit-color, @COLOR_TEXT_ASSIST);
// 分割线
@number-input-border-color: var(--number-input-border-color, @COLOR_BORDER);
// 前缀文字颜色
@number-input-prefix-color: var(--number-input-prefix-color, @COLOR_TEXT_PRIMARY);
// 输入框光标颜色
@number-input-caret-color: var(--number-input-caret-color, @COLOR_BRAND1);
// 快捷金额按钮文字颜色
@number-input-quick-text-color: var(--number-input-quick-text-color, @COLOR_BRAND1);
// 快捷金额按钮边框颜色
@number-input-quick-border-color: var(--number-input-quick-border-color, @COLOR_BRAND1);
================================================
FILE: src/NumberKeyboard/index.axml
================================================
{{ itemKey }}
.
0
0
.
{{ confirmText }}
================================================
FILE: src/NumberKeyboard/index.en.md
================================================
---
nav:
path: /components
group:
title: Information Entry
order: 10
toc: 'content'
---
# NumberKeyboard
Customize the numeric keypad.
## Precautions
1. The numeric keyboard has not yet solved the problem that the keyboard blocks the input box. Developers need to set up screen scrolling to solve such problems.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-number-keyboard": "antd-mini/es/NumberKeyboard/index"
#endif
#if WECHAT
"ant-number-keyboard": "antd-mini/NumberKeyboard/index"
#endif
}
```
## Code Sample
### Basic use
#### Default Keyboard
```xml
```
#### No decimal point
```xml
```
#### With confirmation button
```xml
```
#### With closed arrow
```xml
```
#### scrambled keyboard
```xml
```
#### Custom Button
```xml
```
### Digital input box
### Verification code input box
### Demo Code
## API
| Property | Description | Type | Default Value |
| ----------------------- | ---------------- | --------------------- | ------ |
| className | Class Name | string | - |
| style | Style | string | - |
| value | Enter value | string | - |
| visible | Show | boolean | false |
| vibrate | Vibration | boolean | true |
| closeable | Close Arrow | boolean | false |
| point | decimal point | boolean | true |
| random | out of order | boolean | false |
| confirmDisabled | Disable confirmation button | boolean | false |
| safeArea | Safety distance | boolean | true |
| confirmText | Confirm the text of the button | string | - |
| header | Override the title bar of the keyboard | slot | - |
| confirm | Override keyboard confirmation button | slot | - |
| #if ALIPAY onChange | Callback when number changes | (val: string) => void | - |
| #if ALIPAY onClose | Callback when hiding keyboard | () => void | - |
| #if ALIPAY onConfirm | Callback when clicking confirmation | () => void | - |
| #if WECHAT bindchange | Callback when number changes | (val: string) => void | - |
| #if WECHAT bindclose | Callback when hiding keyboard | () => void | - |
| #if WECHAT bindconfirm | Callback when clicking confirmation | () => void | - |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------- |
| --number-key-board-active-background-color | #d3d3d3
| #d3d3d3
| Numeric keypad to activate background color |
| --number-key-board-text-color | #333333
| #c5cad1
| Number pad text color |
| --number-key-board-none-text-color | #999999
| #616161
| No text color on numeric keypad |
| --number-key-board-iphonex-safe-background-color | #ffffff
| #1a1a1a
| Digital Keyboard iPhone X Secure Area Background Color |
| --number-key-board-background-color | #f5f5f5
| #121212
| Numeric keypad background color |
| --number-key-board-border-color | #eeeeee
| #2b2b2b
| Numeric keypad border color |
| --number-key-board-transfer-bg | #ffffff
| #1677ff
| Numeric keypad button background |
| --number-key-board-transfer-color | #ffffff
| #3086ff
| Numeric keypad button color |
================================================
FILE: src/NumberKeyboard/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-icon": "../Icon/index"
}
}
================================================
FILE: src/NumberKeyboard/index.less
================================================
@import (reference) './variable.less';
@keyframes number-input-cursor {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.ant-number-keyboard {
color: @number-key-board-text-color;
width: 0;
height: 0;
position: relative;
overflow: hidden;
&-modal {
z-index: 998;
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: transparent;
}
&-kb {
background: @number-key-board-background-color;
max-height: 600px;
min-width: 100vw;
overflow: hidden;
position: fixed;
bottom: 0;
left: 0;
transition: all 200ms linear;
z-index: 999;
&_hide {
transform: translateY(100%);
max-height: 0;
}
&_bd {
display: flex;
}
&_row {
display: flex;
}
&_keys {
flex: 3;
}
&_function {
flex: 1;
display: flex;
flex-direction: column;
}
&_none {
height: 80 * @rpx;
width: calc(100% - @number-key-board-none-padding * 2);
padding: 0 @number-key-board-none-padding;
font-size: 28 * @rpx;
background-color: @number-key-board-iphonex-safe-background-color;
display: flex;
position: relative;
flex-direction: row;
align-items: center;
justify-content: center;
border-bottom: @border-width-standard solid
@number-key-board-background-color;
color: @number-key-board-none-text-color;
}
&_number {
flex: 1;
height: calc(100vh / 14);
text-align: center;
font-size: 50 * @rpx;
color: @number-key-board-text-color;
background: @number-key-board-iphonex-safe-background-color;
border-right: @border-width-standard solid
@number-key-board-background-color;
border-bottom: @border-width-standard solid
@number-key-board-background-color;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
&:active {
background-color: @number-key-board-active-background-color;
opacity: 0.5;
color: @number-key-board-text-color;
}
}
&_disable {
&:active {
opacity: 1;
}
}
&_zero {
flex: 2;
}
&_point {
flex: 1;
}
&_transfer {
background-color: @number-key-board-transfer-bg;
color: @number-key-board-transfer-color;
flex: 1;
height: calc(100vh / 14);
text-align: center;
font-size: 34 * @rpx;
border-right: @border-width-standard solid
@number-key-board-background-color;
border-bottom: @border-width-standard solid
@number-key-board-background-color;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
position: relative;
overflow: hidden;
&-btn {
font-size: 32 * @rpx;
font-weight: bold;
}
&:active {
opacity: 0.9;
}
}
&_del {
position: relative;
flex: none;
border-right: none;
&::before {
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: '';
}
}
&_iphonex-safe {
height: 48 * @rpx;
width: 100%;
background-color: @number-key-board-iphonex-safe-background-color;
}
&_flex {
flex: 1;
}
&-e {
background-color: @number-key-board-border-color;
}
}
&-down {
font-size: 40 * @rpx;
}
&-del {
font-size: 50 * @rpx;
}
}
================================================
FILE: src/NumberKeyboard/index.md
================================================
---
nav:
path: /components
group:
title: 数据录入
order: 10
toc: 'content'
---
# NumberKeyboard 数字键盘
自定义数字键盘。
## 注意事项
1. 数字键盘暂未解决键盘遮挡输入框的问题,需要开发者自行设置屏幕滚动来解决此类问题。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-number-keyboard": "antd-mini/es/NumberKeyboard/index"
#endif
#if WECHAT
"ant-number-keyboard": "antd-mini/NumberKeyboard/index"
#endif
}
```
## 代码示例
### 基本使用
#### 默认键盘
```xml
```
#### 没有小数点
```xml
```
#### 带确认按钮
```xml
```
#### 带关闭箭头
```xml
```
#### 乱序键盘
```xml
```
#### 自定义按钮
```xml
```
### 数字输入框
### 验证码输入框
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| ----------------------- | ---------------- | --------------------- | ------ |
| className | 类名 | string | - |
| style | 样式 | string | - |
| value | 输入值 | string | - |
| visible | 展示 | boolean | false |
| vibrate | 震动 | boolean | true |
| closeable | 关闭箭头 | boolean | false |
| point | 小数点 | boolean | true |
| random | 乱序 | boolean | false |
| confirmDisabled | 禁用确认按钮 | boolean | false |
| safeArea | 安全距离 | boolean | true |
| confirmText | 确认按钮的文字 | string | - |
| header | 覆盖键盘的标题栏 | slot | - |
| confirm | 覆盖键盘确认按钮 | slot | - |
| #if ALIPAY onChange | 数字变化时的回调 | (val: string) => void | - |
| #if ALIPAY onClose | 隐藏键盘时的回调 | () => void | - |
| #if ALIPAY onConfirm | 点击确认时的回调 | () => void | - |
| #if WECHAT bindchange | 数字变化时的回调 | (val: string) => void | - |
| #if WECHAT bindclose | 隐藏键盘时的回调 | () => void | - |
| #if WECHAT bindconfirm | 点击确认时的回调 | () => void | - |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------- |
| --number-key-board-active-background-color | #d3d3d3
| #d3d3d3
| 数字键盘激活背景颜色 |
| --number-key-board-text-color | #333333
| #c5cad1
| 数字键盘文本颜色 |
| --number-key-board-none-text-color | #999999
| #616161
| 数字键盘无文本颜色 |
| --number-key-board-iphonex-safe-background-color | #ffffff
| #1a1a1a
| 数字键盘 iPhone X 安全区域背景颜色 |
| --number-key-board-background-color | #f5f5f5
| #121212
| 数字键盘背景颜色 |
| --number-key-board-border-color | #eeeeee
| #2b2b2b
| 数字键盘边框颜色 |
| --number-key-board-transfer-bg | #ffffff
| #1677ff
| 数字键盘按钮背景 |
| --number-key-board-transfer-color | #ffffff
| #3086ff
| 数字键盘按钮颜色 |
================================================
FILE: src/NumberKeyboard/index.ts
================================================
import {
Component,
getValueFromProps,
triggerEvent,
triggerEventOnly,
} from '../_util/simply';
import { NumberKeyboardDefaultProps } from './props';
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
Component({
props: NumberKeyboardDefaultProps,
data: {
numArr: [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
],
randomArr: [],
},
methods: {
catchAppearModal() {
this.setRandom();
},
setRandom() {
if (getValueFromProps(this, 'random')) {
const newArr = arr.sort(function () {
return Math.random() - 0.5;
});
this.setData({
randomArr: [
newArr.slice(0, 3),
newArr.slice(3, 6),
newArr.slice(6, 9),
],
});
}
},
tapButton(e) {
const [value, confirmDisabled] = getValueFromProps(this, [
'value',
'confirmDisabled',
]);
this.vibrate();
const _key = e.currentTarget.dataset.key;
const _val = `${value}`;
// 回退
if (_key === 'del') {
triggerEvent(this, 'change', `${_val.substr(0, _val.length - 1)}`, e);
return;
}
if (_key !== 'del' && _key !== 'enter') {
triggerEvent(this, 'change', `${_val}${_key}`, e);
}
if (_key === 'enter' && !confirmDisabled) {
this.onClickEnter();
triggerEventOnly(this, 'close');
}
},
// 隐藏键盘,失去焦点
handleHide() {
triggerEventOnly(this, 'close');
},
onClickEnter() {
const confirmDisabled = getValueFromProps(this, 'confirmDisabled');
if (confirmDisabled) return;
this.handleHide();
triggerEventOnly(this, 'confirm');
},
// 振动反馈
vibrate() {
if (getValueFromProps(this, 'vibrate') && typeof my !== 'undefined') {
my.canIUse('vibrateShort') && my.vibrateShort();
}
},
},
/// #if ALIPAY
didMount() {
this.setRandom();
},
/// #endif
/// #if WECHAT
attached() {
this.setRandom();
},
/// #endif
});
================================================
FILE: src/NumberKeyboard/props.ts
================================================
import { IBaseProps } from '../_util/base';
export interface INumberKeyboardProps extends IBaseProps {
/**
* @description 关闭箭头
* @default false
*/
closeable: boolean;
/**
* @description 展示小数点
* @default true
*/
point: boolean;
/**
* @description 确认按钮文字
* @default ''
*/
confirmText: string;
/**
* @description 是否震动
* @default true
*/
vibrate: boolean;
/**
* @description 是否展示
* @default false
*/
visible: boolean;
/**
* @description 值
* @default ''
*/
value: string;
/**
* @description 安全距离
* @default true
*/
safeArea: boolean;
/**
* @description 乱序
* @default fasle
*/
random: boolean;
/**
* @description 禁用
* @default false
*/
confirmDisabled: boolean;
/**
* @description 输入值变化时的回调
*/
onChange?: (val: string) => void;
/**
* @description 点击确认时的回调
*/
onConfirm?: () => void;
/**
* @description 隐藏键盘时的回调
*/
onClose?: () => void;
}
export const NumberKeyboardDefaultProps: Partial = {
closeable: false, // 关闭箭头
point: true, // 展示小数点
confirmText: '', // 确认按钮文字
vibrate: false, // 震动反馈
visible: false, // 是否展示
value: '', // 值
safeArea: true, // 安全区域
random: false, // 乱序
confirmDisabled: false, // 禁用确认按钮
onChange: () => {},
onConfirm: () => {},
onClose: () => {},
};
================================================
FILE: src/NumberKeyboard/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
@number-key-board-none-padding: 48 * @rpx;
// 标准字体颜色
@number-key-board-text-color: var(
--number-key-board-text-color,
@COLOR_TEXT_PRIMARY
);
// nont-字体颜色
@number-key-board-none-text-color: var(
--number-key-board-none-text-color,
@COLOR_TEXT_ASSIST
);
@number-key-board-iphonex-safe-background-color: var(
--number-key-board-iphonex-safe-background-color,
@COLOR_CARD
);
@number-key-board-background-color: var(
--number-key-board-background-color,
@COLOR_BACKGROUND
);
@number-key-board-border-color: var(
--number-key-board-border-color,
@COLOR_BORDER
);
@number-key-board-transfer-bg: var(
--number-key-board-transfer-bg,
@COLOR_BRAND1
);
@number-key-board-active-background-color: var(
--number-key-board-active-background-color,
#d3d3d3
);
@number-key-board-transfer-color: var(
--number-key-board-transfer-color,
@COLOR_WHITE
);
================================================
FILE: src/PageContainer/index.axml
================================================
{{actionText}}
{{secondaryActionText}}
================================================
FILE: src/PageContainer/index.en.md
================================================
---
nav:
path: /components
group:
title: Information Display
order: 8
toc: 'content'
---
# PageContainer
The page-level container component provides common capabilities such as loading status, page exception handling, top/bottom safe margins, and so on, out of the box.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
"ant-page": "antd-mini/es/PageContainer/index"
}
```
## Code Sample
### Basic use
```xml
minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
```
```js
Page({
data: {
loading: true,
status: 'failed',
safeArea: 'both',
},
onLoad() {
setTimeout(() => {
this.setData({
loading: false,
});
}, 1000);
},
handleRefresh() {
my.reLaunch({
url: 'index',
fail(e) {
console.log(e);
},
});
},
handleSwitchToDisconnected() {
this.setData({
status: 'disconnected',
title: undefined,
message: undefined,
image: '',
});
},
});
```
### Demo Code
## API
| Property | Description | Type | Default Value |
| -------------------- | ------------------------------------------------------- | ---------------- | -------- |
| className | Class Name | string | - |
| style | Style | string | - |
| safeArea | Location of margin inside the safety zone,`top`、`bottom`、`both` | string | `both` |
| loading | Loading | boolean \| slot | false |
| loadingType | Load style type,`spin`、`mini` | string | `spin` |
| loadingSize | Load Style Size,`small`、`medium`、`large`、`x-large` | string | `medium` |
| loadingColor | Load Style Color | string | '#ccc' |
| status | Page exception status,`failed`、`busy`、`disconnected`、`empty` | string \| slot | - |
| image | Page Exception Status Custom Image | string | - |
| title | Page Exception Status Custom Title | string | - |
| message | Page Exception Status Custom Description | string | - |
| actionText | Page Exception Status Button Copy | string | - |
| secondaryActionText | Page Exception Secondary Button Copy | string | - |
| extra | Page Exception Custom Button | slot | - |
| onActionTap | Page abnormal state button click event | (e: any) => void | - |
| onSecondaryActionTap | Page Exception Status Secondary Button Click Event | (e: any) => void | - |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| --------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------- |
| --page-container-background-color | #f5f5f5
| #121212
| Page Container Background Color |
================================================
FILE: src/PageContainer/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-button": "../Button/index",
"ant-loading": "../Loading/index",
"ant-empty": "../Empty/index"
}
}
================================================
FILE: src/PageContainer/index.less
================================================
@import (reference) './variable.less';
@import '../style/mixins/hairline.less';
@prefix: ant-page-container;
.@{prefix} {
overflow: auto;
color: @COLOR_TEXT_PRIMARY;
&-safe-top {
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
}
&-safe-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
&-loading-wrap {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100vh;
background: @page-container-background-color;
opacity: 0.4;
z-index: 3;
}
&-loading {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
&-status {
margin-top: 420 * @rpx;
text-align: center;
}
}
================================================
FILE: src/PageContainer/index.md
================================================
---
nav:
path: /components
group:
title: 数据展示
order: 8
toc: 'content'
---
# PageContainer 页面容器
页面级容器组件,提供加载状态、页面异常处理、顶部/底部安全边距等常用能力,开箱即用。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
"ant-page": "antd-mini/es/PageContainer/index"
}
```
## 代码示例
### 基本使用
```xml
minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
```
```js
Page({
data: {
loading: true,
status: 'failed',
safeArea: 'both',
},
onLoad() {
setTimeout(() => {
this.setData({
loading: false,
});
}, 1000);
},
handleRefresh() {
my.reLaunch({
url: 'index',
fail(e) {
console.log(e);
},
});
},
handleSwitchToDisconnected() {
this.setData({
status: 'disconnected',
title: undefined,
message: undefined,
image: '',
});
},
});
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| -------------------- | ------------------------------------------------------- | ---------------- | -------- |
| className | 类名 | string | - |
| style | 样式 | string | - |
| safeArea | 安全区内边距位置,`top`、`bottom`、`both` | string | `both` |
| loading | 加载中 | boolean \| slot | false |
| loadingType | 加载样式类型,`spin`、`mini` | string | `spin` |
| loadingSize | 加载样式大小,`small`、`medium`、`large`、`x-large` | string | `medium` |
| loadingColor | 加载样式颜色 | string | '#ccc' |
| status | 页面异常状态,`failed`、`busy`、`disconnected`、`empty` | string \| slot | - |
| image | 页面异常状态自定义图片 | string | - |
| title | 页面异常状态自定义标题 | string | - |
| message | 页面异常状态自定义描述 | string | - |
| actionText | 页面异常状态按钮文案 | string | - |
| secondaryActionText | 页面异常状态次要按钮文案 | string | - |
| extra | 页面异常状态自定义按钮 | slot | - |
| onActionTap | 页面异常状态按钮点击事件 | (e: any) => void | - |
| onSecondaryActionTap | 页面异常状态次要按钮点击事件 | (e: any) => void | - |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| --------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------- |
| --page-container-background-color | #f5f5f5
| #121212
| 页面容器背景颜色 |
================================================
FILE: src/PageContainer/index.sjs.ts
================================================
================================================
FILE: src/PageContainer/index.ts
================================================
import { effect } from '@preact/signals-core';
import equal from 'fast-deep-equal';
import {
ComponentWithSignalStoreImpl,
getValueFromProps,
triggerEventOnly,
} from '../_util/simply';
import i18nController from '../_util/store';
import { BuiltinStatus, PageDefaultProps } from './props';
ComponentWithSignalStoreImpl({
storeOptions: {
store: () => i18nController,
updateHook: effect,
mapState: {
locale: ({ store }) => store.currentLocale.value,
},
},
props: PageDefaultProps,
methods: {
handleActionTap(e) {
triggerEventOnly(this, 'actionTap', e);
},
handleSecondaryActionTap(e) {
triggerEventOnly(this, 'secondaryActionTap', e);
},
updatePageStatus(prevProps: any, nextProps: any) {
if (!equal(prevProps, nextProps)) {
const [status, image, title, message] = getValueFromProps(this, [
'status',
'image',
'title',
'message',
]);
const updateData = {
...nextProps,
// 自定义内容优先 status
image: image || BuiltinStatus[status]?.image || '',
title:
title ||
BuiltinStatus[status]?.title ||
this.data.locale.pageContainer[status]?.title ||
'',
message:
message ||
BuiltinStatus[status]?.message ||
this.data.locale.pageContainer[status]?.message ||
'',
};
this.setData(updateData);
}
},
},
/// #if ALIPAY
didMount() {
const props = getValueFromProps(this);
this.updatePageStatus({}, props);
},
didUpdate(prevProps) {
const props = getValueFromProps(this);
this.updatePageStatus(prevProps, props);
},
/// #endif
/// #if WECHAT
attached() {
const props = getValueFromProps(this);
this.updatePageStatus({}, props);
},
observers: {
'**': function (data) {
const prevData = this._prevData || this.data;
this._prevData = { ...data };
this.updatePageStatus(prevData, data);
},
},
/// #endif
});
================================================
FILE: src/PageContainer/props.ts
================================================
import { IBaseProps } from '../_util/base';
/**
* @description 页面容器,提供开箱即用的页面状态展示和基础能力。
*/
export interface IPageProps extends IBaseProps {
/**
* @description 安全区内边距位置 top=顶部 bottom=底部 both=顶部和底部
*/
safeArea?: 'top' | 'bottom' | 'both';
/**
* @description 加载中
*/
loading?: boolean;
/**
* @description 加载样式类型,参考 Loading 组件
*/
loadingType?: string;
/**
* @description 加载样式大小,参考 Loading 组件
*/
loadingSize?: string;
/**
* @description 加载样式颜色,参考 Loading 组件
*/
loadingColor?: string;
/**
* @description 页面异常状态
*/
status?: 'failed' | 'busy' | 'disconnected' | 'empty';
/**
* @description 页面异常状态-标题
*/
title?: string;
/**
* @description 页面异常状态-图片
*/
image?: string;
/**
* @description 页面异常状态-描述
*/
message?: string;
/**
* @description 页面异常状态-按钮文案
*/
actionText?: string;
/**
* @description 页面异常状态-次要按钮文案
*/
secondaryActionText?: string;
/**
* @description 页面异常状态-按钮点击事件
*/
onActionTap?: (e: any) => void;
/**
* @description 页面异常状态-次要按钮点击事件
*/
onSecondaryActionTap?: (e: any) => void;
}
export const PageDefaultProps: IPageProps = {
loading: false,
safeArea: 'both',
loadingColor: '#ccc',
loadingSize: 'medium',
loadingType: 'spin',
status: null,
title: '',
image: '',
message: '',
actionText: '',
secondaryActionText: '',
onActionTap: null,
onSecondaryActionTap: null,
};
// 内置异常配置
export const BuiltinStatus = {
failed: {
image:
'https://gw.alipayobjects.com/mdn/rms_7cc883/afts/img/A*PG7NQoXbN38AAAAAAAAAAAAAARQnAQ',
title: '',
message: '',
},
disconnected: {
image:
'https://mdn.alipayobjects.com/huamei_yqdpol/afts/img/A*uqB5TY4urA4AAAAAAAAAAAAADj16AQ/original',
title: '',
message: '',
},
empty: {
title: '',
message: '',
image:
'https://gw.alipayobjects.com/mdn/rms_226d75/afts/img/A*0AaRRrYlVDkAAAAAAAAAAAAAARQnAQ',
},
busy: {
image:
'https://mdn.alipayobjects.com/huamei_yqdpol/afts/img/A*avTGQIyeHk0AAAAAAAAAAAAADj16AQ/original',
title: '',
message: '',
},
};
================================================
FILE: src/PageContainer/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
@page-container-background-color: var(
--page-container-background-color,
@COLOR_BACKGROUND
);
================================================
FILE: src/Pagination/index.axml
================================================
================================================
FILE: src/Pagination/index.json
================================================
{
"styleIsolation": "shared",
"component": true
}
================================================
FILE: src/Pagination/index.less
================================================
@import (reference) './variable.less';
@pageInfinite: ant-page-infinite;
.@{pageInfinite} {
position: relative;
width: 100%;
box-sizing: border-box;
&-content {
width: 100%;
&::-webkit-scrollbar {
display: none;
}
}
&-wrap {
position: absolute;
bottom: 2 * @rpx;
left: 0;
right: 0;
width: 100%;
height: 6 * @rpx;
z-index: 2;
pointer-events: none;
}
&-area {
position: absolute;
left: 50%;
width: 52 * @rpx;
height: 6 * @rpx;
overflow: hidden;
border-radius: @corner-radius-sm / 2;
transform: translateX(-50%);
background-color: @pagination-area-background-color;
}
&-move {
position: absolute;
bottom: 0;
left: 0;
width: 50%;
height: 100%;
transition: all 100ms linear;
border-radius: @corner-radius-sm / 2;
background-color: @pagination-move-background-color;
}
}
================================================
FILE: src/Pagination/index.sjs.ts
================================================
function changeScollDistance(event, ownerComponent) {
const scrollLeft = event.detail.scrollLeft;
const scrollWidth = event.detail.scrollWidth;
const viewWidth = ownerComponent
.selectComponent('.ant-page-infinite-content')
.getBoundingClientRect().width;
const moveDom = ownerComponent.selectComponent('.ant-page-infinite-move');
const pageDeg = Math.ceil((scrollLeft / (scrollWidth - viewWidth)) * 100);
moveDom.setStyle(`transform: translateX(${pageDeg}%);`);
}
export default { changeScollDistance };
================================================
FILE: src/Pagination/index.ts
================================================
import { isAilpayNative } from '../_util/support';
import { getInstanceBoundingClientRect } from '../_util/jsapi/get-instance-bounding-client-rect';
import { Component } from '../_util/simply';
import { PaginationDefaultProps } from './props';
Component({
props: PaginationDefaultProps,
data: {
pageDeg: 0,
supportSjs: true,
},
wrapWidth: 0,
methods: {
async clacWidth() {
const rect = await getInstanceBoundingClientRect(
this,
`#ant-pageInfinite${this.$id ? `-${this.$id}` : ''}`
);
if (rect) {
return rect.width;
}
return 0;
},
async onScroll(e) {
const { scrollLeft, scrollWidth } = e.detail;
const viewWidth = await this.clacWidth();
if (viewWidth) {
this.setData({
pageDeg: Math.ceil((scrollLeft / (scrollWidth - viewWidth)) * 100),
});
}
},
},
onInit() {
/// #if ALIPAY
let supportSjs;
if (typeof my === 'undefined') {
supportSjs = true;
}
supportSjs = my.canIUse('sjs.event');
if (isAilpayNative()) {
supportSjs = false;
}
this.setData({ supportSjs });
/// #endif
},
});
================================================
FILE: src/Pagination/props.ts
================================================
import { IBaseProps } from '../_util/base';
export interface IPaginationProps extends IBaseProps {
/**
* @description 分页符背景色
* @default '#ddd'
*/
fillColor: string;
/**
* @description 分页符颜色
* @default '#1677ff'
*/
frontColor: string;
}
export const PaginationDefaultProps: Partial = {
fillColor: '',
frontColor: '',
};
================================================
FILE: src/Pagination/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
@pagination-area-background-color: var(
--pagination-area-background-color,
@COLOR_GREY_CARD
);
@pagination-move-background-color: var(
--pagination-move-background-color,
@COLOR_BRAND1
);
================================================
FILE: src/Picker/CascaderPicker/index.axml
================================================
================================================
FILE: src/Picker/CascaderPicker/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-picker": "../index"
}
}
================================================
FILE: src/Picker/CascaderPicker/index.less
================================================
================================================
FILE: src/Picker/CascaderPicker/index.ts
================================================
import equal from 'fast-deep-equal';
import mixinValue from '../../mixins/value';
import { resolveEventValue, resolveEventValues } from '../../_util/platform';
import {
Component,
getValueFromProps,
triggerEvent,
triggerEventOnly,
triggerEventValues,
} from '../../_util/simply';
import { CascaderDefaultProps } from './props';
Component({
props: CascaderDefaultProps,
data: {
currentValue: [], // 当前picker选中值,didmount、弹窗打开、picker变化时更新
columns: [], // 可选项,didmound、弹窗打开、picker变化时更新
formattedValueText: '',
visible: false,
},
methods: {
// visible受控判断
isVisibleControlled() {
/// #if ALIPAY
return 'visible' in getValueFromProps(this);
/// #endif
/// #if WECHAT
return getValueFromProps(this, 'visible') !== null;
/// #endif
},
initColumns() {
const [options, visible, defaultVisible, value, defaultValue] =
getValueFromProps(this, [
'options',
'visible',
'defaultVisible',
'value',
'defaultValue',
]);
const realValue = value || defaultValue || [];
const columns = this.getterColumns(realValue, options);
// 首次无需校验value有效性,onOk时会校验
this.setData({
columns,
visible: this.isVisibleControlled() ? visible : defaultVisible,
currentValue: realValue,
formattedValueText: this.onFormat(),
});
},
getterColumns(value, options) {
const getColumns = (options, value, columns = []) => {
columns.push(options.map((v) => ({ value: v.value, label: v.label })));
const currentOption =
options.find((v) => v.value === value?.[columns.length - 1]) ||
options[0];
if (currentOption?.children?.length > 0) {
return getColumns(currentOption.children, value, columns);
}
return columns;
};
return getColumns(options, value);
},
// 获取有效value,若从x项开始在columns里找不到,则从此项开始都选第一条
getValidValue(value, columns) {
const result = [];
for (let i = 0; i < columns.length; i++) {
if (!columns[i].some((v) => v?.value === value?.[i])) {
result.push(...columns.slice(i).map((v) => v?.[0]?.value));
break;
} else {
result[i] = value[i];
}
}
return result;
},
getOptionByValue(value) {
const options = getValueFromProps(this, 'options');
if (!(value?.length > 0)) return null;
const result = [];
let item = options.find((v) => v.value === value[0]);
for (let i = 0; i < value.length; i++) {
if (!item) {
return null;
}
result.push({
value: item.value,
label: item.label,
});
item = item.children?.find((v) => v.value === value[i + 1]);
}
return result;
},
onChange(selectedVal) {
let [selectedValue] = resolveEventValues(selectedVal);
const options = getValueFromProps(this, 'options');
const { columns } = this.data;
const newColumns = this.getterColumns(selectedValue, options);
// columns没变化说明selectedValue在范围内,无需重置
const newData: any = {};
if (!equal(columns, newColumns)) {
selectedValue = this.getValidValue(selectedValue, newColumns);
newData.columns = newColumns;
}
newData.currentValue = selectedValue;
this.setData(newData);
triggerEventValues(this, 'change', [
selectedValue,
this.getOptionByValue(selectedValue),
]);
},
async onOk() {
const { currentValue } = this.data;
const options = getValueFromProps(this, 'options');
const newColumns = this.getterColumns(currentValue, options);
const validValue = this.getValidValue(currentValue, newColumns);
if (!this.isControlled()) {
this.update(validValue);
}
triggerEventValues(this, 'ok', [
validValue,
this.getOptionByValue(validValue),
]);
},
onVisibleChange(visible) {
const options = getValueFromProps(this, 'options');
const { columns } = this.data;
const realValue = this.getValue();
if (!this.isVisibleControlled() && visible) {
const newColumns = this.getterColumns(realValue, options);
if (!equal(columns, newColumns)) {
this.setData({ columns: newColumns }, () => {
this.setData({
currentValue: this.getValidValue(realValue, newColumns),
formattedValueText: this.onFormat(),
});
});
}
}
triggerEvent(this, 'visibleChange', resolveEventValue(visible));
},
defaultFormat(value, options) {
if (options) {
return options.map((v) => v.label).join('');
}
return '';
},
onFormat() {
const realValue = this.getValue();
const onFormat = getValueFromProps(this, 'onFormat');
const formatValueByProps =
onFormat && onFormat(realValue, this.getOptionByValue(realValue));
if (formatValueByProps !== undefined && formatValueByProps !== null) {
return formatValueByProps;
}
return this.defaultFormat(realValue, this.getOptionByValue(realValue));
},
onCancel(e) {
triggerEventOnly(this, 'cancel', e);
},
},
mixins: [mixinValue()],
/// #if ALIPAY
onInit() {
this.initColumns();
},
didUpdate(prevProps, prevData) {
const options = getValueFromProps(this, 'options');
if (!equal(options, prevProps.options)) {
const { currentValue } = this.data;
const newColumns = this.getterColumns(currentValue, options);
this.setData({
columns: newColumns,
});
}
if (!this.isEqualValue(prevData)) {
const realValue = this.getValue();
const newColumns = this.getterColumns(realValue, options);
const currentValue = this.getValidValue(realValue, newColumns);
this.setData({ currentValue, formattedValueText: this.onFormat() });
}
const visible = getValueFromProps(this, 'visible');
if (this.isVisibleControlled() && !equal(prevProps.visible, visible)) {
this.setData({ visible });
}
},
/// #endif
/// #if WECHAT
created() {
this.initColumns();
},
observers: {
'**': function (data) {
const prevData = this._prevData || this.data;
this._prevData = { ...data };
const options = getValueFromProps(this, 'options');
if (!equal(options, prevData.options)) {
const { currentValue } = this.data;
const newColumns = this.getterColumns(currentValue, options);
this.setData({
columns: newColumns,
});
}
if (!this.isEqualValue(prevData)) {
const realValue = this.getValue();
const newColumns = this.getterColumns(realValue, options);
const currentValue = this.getValidValue(realValue, newColumns);
this.setData({ currentValue, formattedValueText: this.onFormat() });
}
},
'visible': function (data) {
const prevVisible = this._prevVisible;
this._prevVisible = data;
const visible = getValueFromProps(this, 'visible');
if (this.isVisibleControlled() && !equal(prevVisible, visible)) {
this.setData({ visible });
}
},
},
/// #endif
});
================================================
FILE: src/Picker/CascaderPicker/props.ts
================================================
import { IBaseProps } from '../../_util/base';
export interface ICascaderOption {
label: string;
value: any;
children?: ICascaderOption[];
}
/**
* @description 级联组件,基于Picker封装
*/
export interface ICascaderProps extends IBaseProps {
visible?: boolean;
defaultVisible?: boolean;
/**
* @desciption 动画类型
* @default "transform"
*/
animationType: 'transform' | 'position';
/**
* @description 当前数据
*/
value: any[];
/**
* @description 默认值
*/
defaultValue: any[];
/**
* @description 可选项数据
*/
options: ICascaderOption[];
/**
* @description 提示文案
* @default '请选择'
*/
placeholder: string;
/**
* @description 取消文案
* @default "取消"
*/
cancelText: string;
/**
* @description 是否禁用
*/
disabled?: boolean;
/**
* @description 是否只读
*/
readonly?: boolean;
/**
* @description 标题
*/
title: string;
/**
* @description 确定按钮文案
* @default "确定"
*/
okText: string;
/**
*@description 选中框样式
* 版本要求: 支付宝小程序基础库 1.10.0 及以上
*/
indicatorStyle?: string;
/**
*@description 选中框类名
* 版本要求: 支付宝小程序基础库 1.10.0 及以上
*/
indicatorClassName?: string;
/**
* @description 蒙层的样式。
* 版本要求: 支付宝小程序基础库 1.10.0 及以上
*/
maskStyle?: string;
/**
* @description 蒙层的类名。
* 版本要求: 支付宝小程序基础库 1.10.0 及以上
*/
maskClassName?: string;
/**
* @description 点击确认回调
*/
onOk?: (
value: any[],
selectedOptions: ICascaderOption[],
e: Record
) => void;
/**
* @description 点击取消回调
*/
onCancel?: (e: Record) => void;
/**
* @description 选中值的文本显示格式
*/
onFormat?: (value: any[], selectedOptions: ICascaderOption[]) => string;
/**
* @description 切换显示隐藏
*/
onVisibleChange?: (visible: boolean, e: Record) => void;
/**
* @description 发生滚动即触发, 与 onChange 点击 ok 后触发不同
*/
onChange?: (
value: any[],
selectedOptions: ICascaderOption[],
e: Record
) => void;
/**
* @description 点击蒙层是否可以关闭
* @default false
*/
maskClosable: boolean;
/**
* @description 弹出框类名
*/
popClassName: string;
/**
* @description 弹出框样式
*/
popStyle: string;
}
export const CascaderDefaultProps: Partial = {
visible: null,
defaultVisible: null,
animationType: 'transform',
value: null,
defaultValue: null,
options: [],
placeholder: '请选择',
cancelText: '取消',
disabled: false,
readonly: false,
title: '',
okText: '确定',
maskClosable: true,
popClassName: '',
popStyle: '',
onFormat: null,
};
================================================
FILE: src/Picker/CascaderPicker/utils.ts
================================================
export function defaultFormat(value, options) {
if (options) {
return options.map((v) => v.label).join('');
}
return '';
}
export function getterColumns(value, options = []) {
const getColumns = (options, value, columns = []) => {
columns.push(options.map((v) => ({ value: v.value, label: v.label })));
const currentOption =
options.find((v) => v.value === value?.[columns.length - 1]) ||
options[0];
if (currentOption?.children?.length > 0) {
return getColumns(currentOption.children, value, columns);
}
return columns;
};
return getColumns(options, value);
}
export function getValidValue(value, columns) {
const result = [];
for (let i = 0; i < columns.length; i++) {
if (!columns[i].some((v) => v.value === value?.[i])) {
result.push(...columns.slice(i).map((v) => v[0]?.value));
break;
} else {
result[i] = value[i];
}
}
return result;
}
================================================
FILE: src/Picker/index.axml
================================================
{{ formatValue }}
{{ u.isPropsEmpty(placeholder) ? locale.global.placeholder : placeholder }}
{{ _sjs.getPickerViewLabel(item) }}
{{ u.isPropsEmpty(emptyText) ? locale.global.emptyText : emptyText}}
================================================
FILE: src/Picker/index.en.md
================================================
---
nav:
path: /components
group:
title: Information Entry
order: 10
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# Picker
The Picker selector displays a scrollable list of one or more collections of options, providing consistency between the iOS and Android-side experiences compared to native pickers.
- One or more sets of association options are provided for selection by the user.
- When there are less than 5 options, it is recommended to tile the options directly. Using Radio is a better choice.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-picker": "antd-mini/es/Picker/index",
"ant-cascader-picker": "antd-mini/es/Picker/CascaderPicker/index"
#endif
#if WECHAT
"ant-picker": "antd-mini/Picker/index",
"ant-cascader-picker": "antd-mini/Picker/CascaderPicker/index"
#endif
}
```
## Code Sample
### Basic use
```xml
```
```js
Page({
data: {
defaultValue: '上海',
list: ['北京', '上海', '深圳', '广州', '南京', '武汉', '无锡', '苏州'],
},
handleDismiss(e) {
console.log('onCancel', e);
},
handleTriggerPicker(visible, e) {
console.log('onVisibleChange', visible, e);
},
handleOk(value, column, e) {
console.log('onOk value', value, 'onOk column', column, e);
},
handleChange(value, column, e) {
console.log('onChange value', value, 'onChange column', column, e);
},
});
```
### Format selected text
> `onFormat` The function return value is the value of the selected area to be rendered.
```xml
```
```js
Page({
data: {
#if WECHAT
onFormat: (value) => {
return `已选择:${value}`;
},
#endif
},
#if ALIPAY
onFormat: (value) => {
return `已选择:${value}`;
},
#endif
})
```
### multi-column selection
> When complex options appear, but do not need to link between options, you can achieve multi-column selection in the following ways:
```xml
```
```js
Page({
data: {
columns: [
[
{
label: '周一',
value: 'Mon',
},
{
label: '周二',
value: 'Tues',
},
{
label: '周三',
value: 'Wed',
},
{
label: '周四',
value: 'Thur',
},
{
label: '周五',
value: 'Fri',
},
],
[
{
label: '上午',
value: 'am',
},
{
label: '下午',
value: 'pm',
},
],
],
#if WECHAT
formatTime: (value) => {
return column.map((c) => c && c.label).join(',');
},
#endif
},
#if ALIPAY
formatTime: (value) => {
return column.map((c) => c && c.label).join(',');
}
#endif
})
```
### Controlled Mode
> `value` property and `ok` Events, with the implementation of controlled mode.
```xml
Select Shenzhen
Empty
```
```js
Page({
data:{
value: '上海',
list: ['北京', '上海', '深圳', '广州', '南京', '武汉', '无锡', '苏州'],
},
handleControlledOk(value) {
#if ALIPAY
this.setData({
value,
});
#endif
#if WECHAT
this.setData({
value: value.detail[0],
});
#endif
},
handleClearControlled() {
this.setData({
value: '',
});
},
handleChangeControlled() {
this.setData({
value: '深圳',
});
},
})
```
### Cascading Picker
> When more complex options appear, the need for linkage between options requires the introduction `ant-cascader-picker` Cascade selection components to achieve. The component implements the cascading function by passing in the following tree structure:
```xml
```
```js
Page({
data: {
cityList: [
{
label: '北京',
value: '11',
children: [
{
label: '北京',
value: '110',
},
],
},
{
label: '河北',
value: '18',
children: [
{
label: '石家庄',
value: '188',
},
{
label: '唐山',
value: '181',
},
],
},
],
},
});
```
### Demo Code
## API
### Picker
| Property | Description | Type | Default Value |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| animationType | Animation type, optional`transform` `position`, the default is used`transform`Animation performance is better. Due to a bug in the basic library of small programs, the picker-view shadow style in the pop-up window may have style problems under iOS and can be temporarily switched`position`Solve | string | `transform` |
| className | Class Name | string | - |
| defaultValue | Default Value | string \| number \| Array\ | - |
| disabled | Disable | boolean | false |
| readonly | Read-only | boolean | false |
| cancelText | Cancel Copy | string | 'Cancel' |
| content | Custom Content Slots | slot | - |
| indicatorStyle | Check Box Style | string | - |
| indicatorClassName | Class name of the check box | string | - |
| maskClassName | Class name of the layer | string | - |
| maskClosable | Click whether the layer can be closed | boolean | true |
| maskStyle | Mask Style | string | - |
| okText | Confirm Button Copy | string | 'OK' |
| emptyText | Empty status button copy | string | 'No data' |
| options | picker data, configure options for each column | [PickerColumnItem](#pickercolumnitem)[] | [] |
| placeholder | Prompt Copy | string | 'Please Select' |
| popClassName | Popup box class name | string | - |
| popStyle | Pop-up Style | string | - |
| prefix | Prefix | slot | - |
| style | Style | string | - |
| suffix | Suffix | slot | - |
| title | Pop-up Title | string \| slot | - |
| value | Selected value | string \| number \| Array\ | - |
| visible | Display | boolean | false |
| defaultVisible | Display by default | boolean | false |
| onFormat | Text display format of the selected value | (value: [PickerColumnItem](#pickercolumnitem), column: [PickerColumnItem](#pickercolumnitem)) => string | - |
| #if ALIPAY onOk | Click the OK button to trigger the callback | (value: [PickerColumnItem](#pickercolumnitem), column: [PickerColumnItem](#pickercolumnitem), event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onCancel | Click the cancel button/layer to trigger the callback | (event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onChange | The selected item changes, triggering a callback | (value: [PickerColumnItem](#pickercolumnitem), column: [PickerColumnItem](#pickercolumnitem), event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onVisibleChange | Pop-up display/hide status change trigger | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindok | Click the OK button to trigger the callback | (value: [PickerColumnItem](#pickercolumnitem), column: [PickerColumnItem](#pickercolumnitem), event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindcancel | Click the cancel button/layer to trigger the callback | (event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindchange | The selected item changes, triggering a callback | (value: [PickerColumnItem](#pickercolumnitem), column: [PickerColumnItem](#pickercolumnitem), event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindvisiblechange | Pop-up display/hide status change trigger | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
### CascaderPicker
| Property | Description | Type | Default Value |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| animationType | Animation type, optional`transform` `position`, the default is used`transform`Animation performance is better. Due to a bug in the basic library of small programs, the picker-view shadow style in the pop-up window may have style problems under iOS and can be temporarily switched`position`Solve | string | `transform` |
| className | Class Name | string | - |
| defaultValue | Default selected value | string[] | - |
| disabled | Disable | boolean | false |
| readonly | Read-only | boolean | false |
| cancelText | Cancel Copy | string | 'Cancel' |
| content | Custom Content Slots | slot | - |
| format | Time format display, the format is the same[dayjs](https://day.js.org/docs/zh-CN/display/format) | string | 'YYYY/MM/DD' |
| indicatorStyle | Check Box Style | string | - |
| indicatorClassName | Class name of the check box | string | - |
| maskClassName | Class name of the layer | string | - |
| maskClosable | Click whether the layer can be closed | boolean | true |
| maskStyle | Mask Style | string | - |
| okText | Confirm Button Copy | string | 'OK' |
| options | Optional data | [CascaderOption](#cascaderoption)[] | [] |
| placeholder | Prompt Copy | string | 'Please Select' |
| popClassName | Popup box class name | string | - |
| popStyle | Pop-up Style | string | - |
| prefix | Prefix | slot | - |
| style | Style | string | - |
| suffix | Suffix | slot | - |
| title | Pop-up Title | string \| slot | - |
| value | Selected value | string[] | - |
| visible | Display | boolean | false |
| defaultVisible | Display by default | boolean | false |
| onFormat | The text display format of the selected value. The default display is labels.join(''). | (value: string[], selectedOptions: [CascaderOption](#cascaderoption)[]) => string | - |
| #if ALIPAY onOk | Click the OK button to trigger the callback | (value: string[], selectedOptions: [CascaderOption](#cascaderoption)[], event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onCancel | Click the cancel button/layer to trigger the callback | (event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onChange | The selected item changes, triggering a callback | (value: string[], selectedOptions: [CascaderOption](#cascaderoption)[], event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onVisibleChange | Pop-up display/hide status change trigger | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindok | Click the OK button to trigger the callback | (value: string[], selectedOptions: [CascaderOption](#cascaderoption)[], event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindcancel | Click the cancel button/layer to trigger the callback | (event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindchange | The selected item changes, triggering a callback | (value: string[], selectedOptions: [CascaderOption](#cascaderoption)[], event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindvisibleChange | Pop-up display/hide status change trigger | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
### PickerColumnItem
| Parameters | Description | Type | Default Value |
| ----- | ---- | ---------------- | ------ |
| label | Text | string | - |
| value | Value | string \| number | - |
### CascaderOption
| Parameters | Description | Type | Default Value |
| -------- | ---- | ---------------- | ------ |
| label | Text | string | - |
| value | Value | string \| number | - |
| children | Sub Level | CascaderOption[] | - |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Light Mode Default | Dark Mode Default | Remarks |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| --picker-item-color | #333333
| #c5cad1
| Selector item color |
| --picker-header-action-color | #1677ff
| #3086ff
| Selector Head Operation Color |
| --picker-placeholder-color | #cccccc
| #474747
| Selector Placeholder Color |
| --picker-header-color | #eeeeee
| #2b2b2b
| Selector head color |
| --picker-content-background-color | #ffffff
| #1a1a1a
| Selector Content Background Color |
| --picker-mask-bg-faded-95 | rgba(255, 255, 255, 0.05)
| rgba(255, 255, 255, 0.02)
| Selector mask background color (95% transparency) |
| --picker-mask-bg-faded-60 | rgba(255, 255, 255, 0.4)
| rgba(255, 255, 255, 0.01)
| Selector mask background color (60% transparency) |
## FAQ
### Exhibit exception when dynamically changing the number of picker columns
Due to the limitation of native picker-view, dynamic change of the number of columns is not supported for the time being.
================================================
FILE: src/Picker/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-popup": "../Popup/index"
}
}
================================================
FILE: src/Picker/index.less
================================================
@import (reference) './variable.less';
@import '../style/mixins/hairline.less';
@pickerPrefix: ant-picker;
.@{pickerPrefix} {
display: inline-flex;
align-items: center;
color: @picker-item-color;
&-disabled {
color: @picker-placeholder-color;
}
&-header {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: @picker-header-padding;
box-sizing: border-box;
.hairline('bottom', @COLOR_BORDER);
&-item {
display: flex;
align-items: center;
height: 100%;
white-space: nowrap;
font-size: @picker-header-action-size;
color: @picker-header-action-color;
box-sizing: border-box;
}
&-title {
display: flex;
justify-content: center;
overflow: hidden;
-webkit-line-clamp: 1;
box-orient: vertical;
white-space: normal;
color: @picker-item-color;
width: @picker-header-title-width;
}
}
/// #if WECHAT
&-picker-view {
width: 100%;
height: @picker-view-height;
}
&-picker-view-column {
text-align: center;
}
/// #endif
&-content {
background: @picker-content-background-color;
.a-picker-view-picker-item {
color: @picker-item-color;
}
.a-picker-view-picker-indicator {
&::before,
&::after {
border-color: @picker-header-color;
}
}
.a-picker-view-picker-mask {
background-image: linear-gradient(
to bottom,
@picker-mask-bg-faded-95,
@picker-mask-bg-faded-60
),
linear-gradient(
to top,
@picker-mask-bg-faded-95,
@picker-mask-bg-faded-60
);
will-change: transform;
}
}
&-value {
&-placeholder,
&-text {
&:not(:nth-child(1)) {
display: none;
}
}
&-placeholder {
color: @picker-placeholder-color;
}
}
}
================================================
FILE: src/Picker/index.md
================================================
---
nav:
path: /components
group:
title: 数据录入
order: 10
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# Picker 选择器
Picker 选择器显示一个或多个选项集合的可滚动列表,相比于原生 picker,实现了 iOS 与 Android 端体验的一致性。
- 提供一组或多组关联选项供用户选择。
- 当少于 5 个选项时,建议直接将选项平铺,使用 Radio 是更好的选择。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-picker": "antd-mini/es/Picker/index",
"ant-cascader-picker": "antd-mini/es/Picker/CascaderPicker/index"
#endif
#if WECHAT
"ant-picker": "antd-mini/Picker/index",
"ant-cascader-picker": "antd-mini/Picker/CascaderPicker/index"
#endif
}
```
## 代码示例
### 基本使用
```xml
```
```js
Page({
data: {
defaultValue: '上海',
list: ['北京', '上海', '深圳', '广州', '南京', '武汉', '无锡', '苏州'],
},
handleDismiss(e) {
console.log('onCancel', e);
},
handleTriggerPicker(visible, e) {
console.log('onVisibleChange', visible, e);
},
handleOk(value, column, e) {
console.log('onOk value', value, 'onOk column', column, e);
},
handleChange(value, column, e) {
console.log('onChange value', value, 'onChange column', column, e);
},
});
```
### 格式化已选文本
> `onFormat` 函数返回值则是已选区域需要渲染的值。
```xml
```
```js
Page({
data: {
#if WECHAT
onFormat: (value) => {
return `已选择:${value}`;
},
#endif
},
#if ALIPAY
onFormat: (value) => {
return `已选择:${value}`;
},
#endif
})
```
### 多列选择
> 当出现复杂选项时,但又不需要选项之间联动,可以通过一下方式实现多列选择:
```xml
```
```js
Page({
data: {
columns: [
[
{
label: '周一',
value: 'Mon',
},
{
label: '周二',
value: 'Tues',
},
{
label: '周三',
value: 'Wed',
},
{
label: '周四',
value: 'Thur',
},
{
label: '周五',
value: 'Fri',
},
],
[
{
label: '上午',
value: 'am',
},
{
label: '下午',
value: 'pm',
},
],
],
#if WECHAT
formatTime: (value) => {
return column.map((c) => c && c.label).join(',');
},
#endif
},
#if ALIPAY
formatTime: (value) => {
return column.map((c) => c && c.label).join(',');
}
#endif
})
```
### 受控模式
> `value` 属性和 `ok` 事件,配合实现受控模式。
```xml
选择深圳
清空
```
```js
Page({
data:{
value: '上海',
list: ['北京', '上海', '深圳', '广州', '南京', '武汉', '无锡', '苏州'],
},
handleControlledOk(value) {
#if ALIPAY
this.setData({
value,
});
#endif
#if WECHAT
this.setData({
value: value.detail[0],
});
#endif
},
handleClearControlled() {
this.setData({
value: '',
});
},
handleChangeControlled() {
this.setData({
value: '深圳',
});
},
})
```
### 级联 Picker
> 当出现更复杂选项时,需要选项之间联动,就需要引入 `ant-cascader-picker` 级联选择组件来实现。该组件通过传入以下的树状结构来实现级联功能:
```xml
```
```js
Page({
data: {
cityList: [
{
label: '北京',
value: '11',
children: [
{
label: '北京',
value: '110',
},
],
},
{
label: '河北',
value: '18',
children: [
{
label: '石家庄',
value: '188',
},
{
label: '唐山',
value: '181',
},
],
},
],
},
});
```
### Demo 代码
## API
### Picker
| 属性 | 说明 | 类型 | 默认值 |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| animationType | 动画类型,可选`transform` `position`,默认使用`transform`动画性能更好。由于小程序基础库 bug,弹窗内 picker-view 阴影样式在 iOS 下可能存在样式问题,可暂切换为`position`解决 | string | `transform` |
| className | 类名 | string | - |
| defaultValue | 默认值 | string \| number \| Array\ | - |
| disabled | 是否禁用 | boolean | false |
| readonly | 是否只读 | boolean | false |
| cancelText | 取消文案 | string | '取消' |
| content | 自定义内容插槽 | slot | - |
| indicatorStyle | 选中框样式 | string | - |
| indicatorClassName | 选中框的类名 | string | - |
| maskClassName | 蒙层的类名 | string | - |
| maskClosable | 点击蒙层是否可以关闭 | boolean | true |
| maskStyle | 蒙层的样式 | string | - |
| okText | 确认按钮文案 | string | '确定' |
| emptyText | 空状态按钮文案 | string | '暂无数据' |
| options | picker 数据,配置每一列的选项 | [PickerColumnItem](#pickercolumnitem)[] | [] |
| placeholder | 提示文案 | string | '请选择' |
| popClassName | 弹出框类名 | string | - |
| popStyle | 弹出框样式 | string | - |
| prefix | 前缀 | slot | - |
| style | 样式 | string | - |
| suffix | 后缀 | slot | - |
| title | 弹出框标题 | string \| slot | - |
| value | 选中的值 | string \| number \| Array\ | - |
| visible | 是否显示 | boolean | false |
| defaultVisible | 默认是否显示 | boolean | false |
| onFormat | 选中值的文本显示格式 | (value: [PickerColumnItem](#pickercolumnitem), column: [PickerColumnItem](#pickercolumnitem)) => string | - |
| #if ALIPAY onOk | 点击确定按钮,触发回调 | (value: [PickerColumnItem](#pickercolumnitem), column: [PickerColumnItem](#pickercolumnitem), event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onCancel | 点击取消按钮/蒙层,触发回调 | (event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onChange | 选中项发生变化,触发回调 | (value: [PickerColumnItem](#pickercolumnitem), column: [PickerColumnItem](#pickercolumnitem), event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onVisibleChange | 弹出框显示/隐藏状态变化触发 | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindok | 点击确定按钮,触发回调 | (value: [PickerColumnItem](#pickercolumnitem), column: [PickerColumnItem](#pickercolumnitem), event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindcancel | 点击取消按钮/蒙层,触发回调 | (event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindchange | 选中项发生变化,触发回调 | (value: [PickerColumnItem](#pickercolumnitem), column: [PickerColumnItem](#pickercolumnitem), event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindvisiblechange | 弹出框显示/隐藏状态变化触发 | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
### CascaderPicker
| 属性 | 说明 | 类型 | 默认值 |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| animationType | 动画类型,可选`transform` `position`,默认使用`transform`动画性能更好。由于小程序基础库 bug,弹窗内 picker-view 阴影样式在 iOS 下可能存在样式问题,可暂切换为`position`解决 | string | `transform` |
| className | 类名 | string | - |
| defaultValue | 默认选中的值 | string[] | - |
| disabled | 是否禁用 | boolean | false |
| readonly | 是否只读 | boolean | false |
| cancelText | 取消文案 | string | '取消' |
| content | 自定义内容插槽 | slot | - |
| format | 时间格式化显示,格式同[dayjs](https://day.js.org/docs/zh-CN/display/format) | string | 'YYYY/MM/DD' |
| indicatorStyle | 选中框样式 | string | - |
| indicatorClassName | 选中框的类名 | string | - |
| maskClassName | 蒙层的类名 | string | - |
| maskClosable | 点击蒙层是否可以关闭 | boolean | true |
| maskStyle | 蒙层的样式 | string | - |
| okText | 确认按钮文案 | string | '确定' |
| options | 可选数据 | [CascaderOption](#cascaderoption)[] | [] |
| placeholder | 提示文案 | string | '请选择' |
| popClassName | 弹出框类名 | string | - |
| popStyle | 弹出框样式 | string | - |
| prefix | 前缀 | slot | - |
| style | 样式 | string | - |
| suffix | 后缀 | slot | - |
| title | 弹出框标题 | string \| slot | - |
| value | 选中的值 | string[] | - |
| visible | 是否显示 | boolean | false |
| defaultVisible | 默认是否显示 | boolean | false |
| onFormat | 选中值的文本显示格式,默认展示 labels.join('') | (value: string[], selectedOptions: [CascaderOption](#cascaderoption)[]) => string | - |
| #if ALIPAY onOk | 点击确定按钮,触发回调 | (value: string[], selectedOptions: [CascaderOption](#cascaderoption)[], event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onCancel | 点击取消按钮/蒙层,触发回调 | (event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onChange | 选中项发生变化,触发回调 | (value: string[], selectedOptions: [CascaderOption](#cascaderoption)[], event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onVisibleChange | 弹出框显示/隐藏状态变化触发 | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindok | 点击确定按钮,触发回调 | (value: string[], selectedOptions: [CascaderOption](#cascaderoption)[], event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindcancel | 点击取消按钮/蒙层,触发回调 | (event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindchange | 选中项发生变化,触发回调 | (value: string[], selectedOptions: [CascaderOption](#cascaderoption)[], event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindvisibleChange | 弹出框显示/隐藏状态变化触发 | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
### PickerColumnItem
| 参数 | 说明 | 类型 | 默认值 |
| ----- | ---- | ---------------- | ------ |
| label | 文字 | string | - |
| value | 值 | string \| number | - |
### CascaderOption
| 参数 | 说明 | 类型 | 默认值 |
| -------- | ---- | ---------------- | ------ |
| label | 文字 | string | - |
| value | 值 | string \| number | - |
| children | 子级 | CascaderOption[] | - |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 浅色模式默认值 | 深色模式默认值 | 备注 |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| --picker-item-color | #333333
| #c5cad1
| 选择器项颜色 |
| --picker-header-action-color | #1677ff
| #3086ff
| 选择器头部操作颜色 |
| --picker-placeholder-color | #cccccc
| #474747
| 选择器占位符颜色 |
| --picker-header-color | #eeeeee
| #2b2b2b
| 选择器头部颜色 |
| --picker-content-background-color | #ffffff
| #1a1a1a
| 选择器内容背景颜色 |
| --picker-mask-bg-faded-95 | rgba(255, 255, 255, 0.05)
| rgba(255, 255, 255, 0.02)
| 选择器遮罩背景色(95%透明度) |
| --picker-mask-bg-faded-60 | rgba(255, 255, 255, 0.4)
| rgba(255, 255, 255, 0.01)
| 选择器遮罩背景色(60%透明度) |
## FAQ
### 动态改变 picker 列数时展示异常
由于原生 picker-view 的限制,暂不支持动态改变列数
================================================
FILE: src/Picker/index.sjs.ts
================================================
function getPickerViewLabel(item) {
if (typeof item === 'object' && typeof item.label === 'string') {
return item.label || '';
}
return item;
}
export default {
getPickerViewLabel,
};
================================================
FILE: src/Picker/index.ts
================================================
import { effect } from '@preact/signals-core';
import equal from 'fast-deep-equal';
import mixinValue from '../mixins/value';
import {
ComponentWithSignalStoreImpl,
getValueFromProps,
triggerEvent,
triggerEventOnly,
triggerEventValues,
} from '../_util/simply';
import i18nController from '../_util/store';
import { assertAilpayNativeNotSupport } from '../_util/support';
import { PickerDefaultProps } from './props';
import {
getMatchedItemByIndex,
getMatchedItemByValue,
getStrictMatchedItemByValue,
} from './utils';
assertAilpayNativeNotSupport('Picker');
ComponentWithSignalStoreImpl({
storeOptions: {
store: () => i18nController,
updateHook: effect,
mapState: {
locale: ({ store }) => store.currentLocale.value,
},
},
props: PickerDefaultProps,
data: {
formatValue: '',
columns: [],
visible: false,
selectedIndex: [],
locale: {
locale: '23123',
global: {},
},
},
tempSelectedIndex: null,
single: false,
isChangingPickerView: false,
methods: {
// visible受控判断
isVisibleControlled() {
/// #if ALIPAY
return 'visible' in getValueFromProps(this);
/// #endif
/// #if WECHAT
return getValueFromProps(this, 'visible') !== null;
/// #endif
},
initData() {
const [options, visible, defaultVisible] = getValueFromProps(this, [
'options',
'visible',
'defaultVisible',
]);
const columns = this.getterColumns(options);
this.setData(
{
columns,
},
() => {
const formatValue = this.getterFormatText();
const selectedIndex = this.getterSelectedIndex();
this.setData({
formatValue,
selectedIndex,
visible: this.isVisibleControlled() ? visible : defaultVisible,
});
}
);
},
getterColumns(options) {
let columns = [];
if (options.length > 0) {
if (options.every((item) => Array.isArray(item))) {
this.single = false;
columns = options.slice();
} else {
this.single = true;
columns = [options];
}
}
return columns;
},
defaultFormat(value, column) {
if (Array.isArray(column)) {
return column
.filter((c) => c !== undefined)
.map(function (c) {
if (typeof c === 'object') {
return c.label;
}
return c;
})
.join('-');
}
return (column && column.label) || column || '';
},
getterFormatText() {
const [onFormat, formattedValueText] = getValueFromProps(this, [
'onFormat',
'formattedValueText',
]);
if (typeof formattedValueText === 'string') {
return formattedValueText;
}
const { columns } = this.data;
const realValue = this.getValue();
const { matchedColumn } = getStrictMatchedItemByValue(
columns,
realValue,
this.single
);
const formatValueByProps = onFormat && onFormat(realValue, matchedColumn);
if (formatValueByProps !== undefined && formatValueByProps !== null) {
return formatValueByProps;
}
return this.defaultFormat(realValue, matchedColumn);
},
getterSelectedIndex() {
const selectedIndex = [];
const columns = this.data.columns;
const realValue = this.getValue();
let value = realValue || [];
if (this.single) {
value = [realValue];
}
for (let i = 0; i < columns.length; i++) {
const column = columns[i];
const compareValue = value[i];
if (compareValue === undefined || compareValue === null) {
selectedIndex[i] = 0;
}
let index = column.findIndex((c) => {
return c === compareValue || c.value === compareValue;
});
if (index === -1) {
index = 0;
}
selectedIndex[i] = index;
}
return selectedIndex;
},
onOpen() {
const [disabled, readonly] = getValueFromProps(this, [
'disabled',
'readonly',
]);
if (!disabled && !readonly) {
this.tempSelectedIndex = null;
const selectedIndex = this.getterSelectedIndex();
this.setData({
selectedIndex,
});
this.triggerPicker(true);
}
},
triggerPicker(visible) {
this.setData({
visible,
});
triggerEvent(this, 'visibleChange', visible);
},
onMaskDismiss() {
const maskClosable = getValueFromProps(this, 'maskClosable');
if (!maskClosable) {
return;
}
this.triggerPicker(false);
triggerEventOnly(this, 'cancel', { detail: { type: 'mask' } });
},
onCancel() {
this.triggerPicker(false);
triggerEventOnly(this, 'cancel', { detail: { type: 'cancel' } });
},
onChange(e) {
const { value: selectedIndex } = e.detail;
this.tempSelectedIndex = selectedIndex;
this.isChangingPickerView = true;
const { matchedColumn, matchedValues } = getMatchedItemByIndex(
this.data.columns,
this.tempSelectedIndex,
this.single
);
this.setData({
selectedIndex,
});
triggerEventValues(this, 'change', [matchedValues, matchedColumn], e);
},
async onOk() {
let result;
if (this.tempSelectedIndex) {
result = getMatchedItemByIndex(
this.data.columns,
this.tempSelectedIndex,
this.single
);
} else {
result = getMatchedItemByValue(
this.data.columns,
this.getValue(),
this.single
);
}
const { matchedColumn, matchedValues } = result;
this.triggerPicker(false);
if (!this.isControlled()) {
this.update(matchedValues);
}
triggerEventValues(this, 'ok', [matchedValues, matchedColumn]);
},
},
mixins: [
mixinValue({
transformValue(value) {
return {
needUpdate: true,
value: value === undefined ? [] : value,
};
},
}),
],
/// #if ALIPAY
onInit() {
this.initData();
},
didUpdate(prevProps) {
const options = getValueFromProps(this, 'options');
if (!equal(options, prevProps.options)) {
const newColums = this.getterColumns(options);
this.setData(
{
columns: newColums,
},
() => {
// 如果是在滚动过程中columns发生变化,以onChange里抛出的selectedIndex为准
if (!this.isChangingPickerView) {
this.tempSelectedIndex = null;
const selectedIndex = this.getterSelectedIndex();
this.setData({
selectedIndex,
});
}
}
);
}
const value = getValueFromProps(this, 'value');
if (!equal(prevProps.value, value)) {
const selectedIndex = this.getterSelectedIndex();
this.tempSelectedIndex = null;
this.setData({
selectedIndex,
});
}
const visible = getValueFromProps(this, 'visible');
if (!equal(prevProps.visible, visible)) {
this.setData({ visible });
}
const formatValue = this.getterFormatText();
const formattedValueText = getValueFromProps(this, 'formattedValueText');
if (
formatValue !== this.data.formatValue ||
prevProps.formattedValueText !== formattedValueText
) {
this.setData({
formatValue,
});
}
this.isChangingPickerView = false;
},
/// #endif
/// #if WECHAT
created() {
this.initData();
},
observers: {
'options': function () {
const options = getValueFromProps(this, 'options');
const newColums = this.getterColumns(options);
this.setData(
{
columns: newColums,
},
() => {
// 如果是在滚动过程中columns发生变化,以onChange里抛出的selectedIndex为准
if (!this.isChangingPickerView) {
this.tempSelectedIndex = null;
const selectedIndex = this.getterSelectedIndex();
this.setData({
selectedIndex,
});
}
this.isChangingPickerView = false;
}
);
},
'value': function () {
const selectedIndex = this.getterSelectedIndex();
this.tempSelectedIndex = null;
this.setData({
selectedIndex,
});
},
'visible': function () {
const visible = getValueFromProps(this, 'visible');
if (this.data.visible !== visible) {
this.setData({
visible,
});
}
},
'formattedValueText': function () {
const formattedValueText = getValueFromProps(this, 'formattedValueText');
this.setData({
formatValue: formattedValueText,
});
},
'**': function () {
const formatValue = this.getterFormatText();
if (formatValue !== this.data.formatValue) {
this.setData({
formatValue,
});
}
},
},
/// #endif
});
================================================
FILE: src/Picker/props.ts
================================================
import { IBaseProps } from '../_util/base';
export interface PickerData {
value: PickerValue;
label: string;
}
export declare type PickerValue =
| string
| number
| PickerData
| (string | number | PickerData)[];
/**
* @description 选择器,包括一个或多个不同值的可滚动列表,每个值可以在视图的中心以较暗的文本形式显示。当用户激活 **Picker** 后,将会从底部弹出。
*/
export interface IPickerProps extends IBaseProps {
visible?: boolean;
defaultVisible?: boolean;
/**
* @desciption 动画类型
* @default "transform"
*/
animationType: 'transform' | 'position';
/**
* @description picker 数据
*/
value: PickerValue;
/**
* @description 格式化后的 value 文本, 优先级大于 onFormat
*/
formattedValueText?: string;
/**
* @description 默认picker 数据
*/
defaultValue: PickerValue;
/**
* @description 是否禁用
*/
disabled?: boolean;
/**
* @description 是否只读
*/
readonly?: boolean;
/**
* @description 标题
*/
title: string;
/**
* @description 确定按钮文案
* @default "确定"
*/
okText: string;
/**
* @description 取消文案
* @default "取消"
*/
cancelText: string;
/**
* @description 提示文案
* @default '请选择'
*/
placeholder: string;
/**
* @description 空状态提示文案
* @default '暂无数据'
*/
emptyText?: string;
/**
* @description picker 数据
*/
options: PickerValue[];
/**
* @description 点击蒙层是否可以关闭
* @default false
*/
maskClosable: boolean;
/**
* @description 弹出框类名
*/
popClassName: string;
/**
* @description 弹出框样式
*/
popStyle: string;
/**
*@description 选中框样式
* 版本要求: 支付宝小程序基础库 1.10.0 及以上
*/
indicatorStyle?: string;
/**
*@description 选中框类名
* 版本要求: 支付宝小程序基础库 1.10.0 及以上
*/
indicatorClassName?: string;
/**
* @description 蒙层的样式。
* 版本要求: 支付宝小程序基础库 1.10.0 及以上
*/
maskStyle?: string;
/**
* @description 蒙层的类名。
* 版本要求: 支付宝小程序基础库 1.10.0 及以上
*/
maskClassName?: string;
/**
* @description 点击确认回调
*/
onOk?: (
value: PickerValue,
column: PickerData,
e: Record
) => void;
/**
* @description 点击取消回调
*/
onCancel?: (e: Record) => void;
/**
* @description 发生滚动即触发, 与 onChange 点击 ok 后触发不同
*/
onChange?: (
value: PickerValue,
column: PickerData,
e: Record
) => void;
/**
* @description 选中值的文本显示格式
*/
onFormat?: (value: PickerValue, column: PickerValue) => string;
/**
* @description 切换显示隐藏
*/
onVisibleChange?: (visible: boolean, e: Record) => void;
}
export const PickerDefaultProps: Partial = {
formattedValueText: null,
visible: null,
defaultVisible: null,
animationType: 'transform',
value: null,
defaultValue: null,
disabled: false,
readonly: false,
title: '',
okText: null,
cancelText: null,
placeholder: null,
options: [],
popClassName: '',
popStyle: '',
maskClosable: true,
onFormat: null,
emptyText: null,
};
================================================
FILE: src/Picker/utils.ts
================================================
function getColumnValue(columnItem) {
if (typeof columnItem === 'object') return columnItem.value;
return columnItem;
}
export function getStrictMatchedItemByValue(columns, value, single) {
if (single) {
value = [value];
}
const matchedValues = [];
const matchedColumn = [];
let index = null;
for (let i = 0; i < columns.length; i++) {
const column = columns[i];
const compareValue = (value || [])[i];
index = column.findIndex((c) => {
const columnValue = getColumnValue(c);
return columnValue === compareValue;
});
matchedColumn[i] = column[index];
matchedValues[i] = getColumnValue(column[index]);
}
return {
matchedColumn: single ? matchedColumn?.[0] : matchedColumn,
matchedValues: single ? matchedValues?.[0] : matchedValues,
};
}
// 如果找不到value对应的item项目,返回第一项
export function getMatchedItemByValue(columns, value, single) {
if (single) {
value = [value];
}
const matchedValues = [];
const matchedColumn = [];
let index = null;
for (let i = 0; i < columns.length; i++) {
const column = columns[i];
const compareValue = (value || [])[i];
if (compareValue === undefined || compareValue === null) {
index = 0;
} else {
index = column.findIndex((c) => {
const columnValue = getColumnValue(c);
return columnValue === compareValue;
});
if (index === -1) {
index = 0;
} // 没有找到, 默认选择第一个
}
matchedColumn[i] = column[index];
matchedValues[i] = getColumnValue(column[index]);
}
return {
matchedColumn: single ? matchedColumn[0] : matchedColumn,
matchedValues: single ? matchedValues[0] : matchedValues,
};
}
export function getMatchedItemByIndex(columns, selectedIndex, single) {
const matchedValues = [];
const matchedColumn = [];
let index = null;
for (let i = 0; i < columns.length; i++) {
const column = columns[i];
const compareValue = selectedIndex[i];
index = null;
if (compareValue === undefined || compareValue === null) {
index = 0;
} else {
index = compareValue;
// 当column变化时, picker-view onChange 里抛出来的selectedIndex有可能不正确
if (columns?.[i]?.[compareValue] === undefined) {
index = 0;
}
if (index === -1) {
index = 0;
} // 没有找到, 默认选择第一个
}
matchedColumn[i] = column[index];
matchedValues[i] = getColumnValue(column[index]);
}
return {
matchedColumn: single ? matchedColumn[0] : matchedColumn,
matchedValues: single ? matchedValues[0] : matchedValues,
};
}
================================================
FILE: src/Picker/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
// picker 选项的高度
@picker-item-height: 68 * @rpx;
// picker 选项字体颜色
@picker-item-color: var(--picker-item-color, @COLOR_TEXT_PRIMARY);
// picker 选项字体大小
@picker-item-size: 42 * @rpx;
// picker 选项选中字体大小
@picker-item-active-size: @font-size-list;
// 取消 和 确定认按钮颜色
@picker-header-action-color: var(--picker-header-action-color, @COLOR_BRAND1);
// 取消 和 确定按钮大小
@picker-header-action-size: 30 * @rpx;
// header 区域padding
@picker-header-padding: 30 * @rpx 24 * @rpx 24 * @rpx;
// header 区域 title 宽度
@picker-header-title-width: 518 * @rpx;
@picker-placeholder-color: var(--picker-placeholder-color, @COLOR_TEXT_WEAK);
@picker-header-color: var(--picker-header-color, @COLOR_BORDER);
@picker-content-background-color: var(
--picker-content-background-color,
@COLOR_CARD
);
// 微信小程序 picker-view 高度
@picker-view-height: 460 * @rpx;
@picker-mask-bg-faded-95: var(--picker-mask-bg-faded-95, @COLOR_CARD_FADED_95);
@picker-mask-bg-faded-60: var(--picker-mask-bg-faded-60, @COLOR_CARD_FADED_60);
================================================
FILE: src/Popover/index.axml
================================================
{{ content }}
{{ content }}
{{ actionText }}
================================================
FILE: src/Popover/index.en.md
================================================
---
nav:
path: /components
group:
title: Feedback
order: 12
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# Popover
Click on the element to pop up the bubble menu. The bubble menu for navigation functions is evoked, usually used to accommodate functions used at low frequencies. This function can only be activated via the icon on the navigation bar.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-popover": "antd-mini/es/Popover/index"
#endif
#if WECHAT
"ant-popover": "antd-mini/Popover/index"
#endif
}
```
## Code Sample
### Basic use
```xml
Point me
```
### Support for pictures and action point buttons
```xml
Point me
```
### Custom
#### Color
```xml
Custom Colors
```
#### Slot
```xml
Slot
```
#### Location
Optional `top`、`top-right`、`top-left`、`bottom`、`bottom-left`、`bottom-right`、`left`、`left-top`、`left-bottom`、`right`、`right-top`、`right-bottom`
```xml
Point me
```
### Controlled
```xml
Click the button to modify the visible
Change visible
```
```js
Page({
data: { visible: false },
handleVisibleChange() {
this.setData({
visible: !this.data.visible,
});
},
});
```
### Demo Code
## API
| Property | Description | Type | Default Value |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ------ |
| autoAdjustOverflow | Automatically adjust the position when the bubble is blocked | boolean | true |
| className | Class Name | string | - |
| color | Background Color | string | - |
| contentClassName | content Class Name | string | - |
| contentStyle | content Style | string | - |
| content | Content | string \| slot | - |
| defaultVisible | Display by default | boolean | false |
| destroyOnClose | Whether to unload content when invisible | boolean | false |
| maskClassName | Class name of the layer | string | - |
| maskStyle | The style of the layer | string | - |
| placement | Bubble box position, optional `top`、`top-right`、`top-left`、`bottom`、`bottom-left`、`bottom-right`、`left`、`left-top`、`left-bottom`、`right`、`right-top` or `right-bottom` | string | top |
| showMask | Whether to display the mask layer. If true, click the blank to close the Popover. | boolean | true |
| style | Style | string | - |
| visible | Whether to display | boolean | - |
| #if ALIPAY onVisibleChange | Callback at the time of visible change | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onTapAction | Operation button click callback | () => void | - |
| #if WECHAT bindvisiblechange | Callback at the time of visible change | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindtapaction | Operation button click callback | () => void | - |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| -------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------- |
| --popover-color-background | #404040
| #404040
| Popover default background color |
| --popover-bg | #ffffff
| #1a1a1a
| Popover background color |
| --popover-inner-color | #ffffff
| #ffffff
| Popover internal text color |
| --popover-text-color | #000000
| #ffffff
| Popover operation button text color |
================================================
FILE: src/Popover/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"image-icon": "../ImageIcon/index",
"ant-mask": "../Mask/index"
}
}
================================================
FILE: src/Popover/index.less
================================================
@import (reference) './variable.less';
@import '../style/mixins/hairline.less';
@popoverPrefix: ant-popover;
.@{popoverPrefix} {
position: relative;
&-children {
z-index: 999;
}
&-close {
margin-left: 24 * @rpx;
width: 38 * @rpx;
height: 38 * @rpx;
.ant-popover-close-icon {
position: absolute;
top: 20 * @rpx;
right: 20 * @rpx;
/// #if ALIPAY
font-size: 36 * @rpx;
/// #endif
.ant-icon {
color: @popover-inner-color;
}
}
}
&-image {
padding-right: 24 * @rpx;
border-radius: 16 * @rpx;
}
&-action {
background: @popover-bg;
font-weight: 400;
font-size: 24 * @rpx;
color: @popover-text-color;
padding: 10 * @rpx 20 * @rpx;
line-height: 33 * @rpx;
border-radius: 50vh;
margin-left: 24 * @rpx;
}
&-mask {
z-index: 998;
background: none;
}
&-content {
position: absolute;
min-width: 64 * @rpx;
max-width: calc(100vw - 48 * @rpx);
z-index: 999;
}
&-inner {
position: relative;
border-radius: 24 * @rpx;
overflow: hidden;
font-size: 28 * @rpx;
background-color: @popover-inner-bg;
color: @popover-inner-color;
padding: 20 * @rpx;
width: max-content;
.ant-icon {
color: @popover-inner-color;
}
display: flex;
justify-content: flex-start;
align-items: center;
&-text {
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2; /* 这里是超出几行省略 */
overflow: hidden;
max-width: 400 * @rpx;
line-height: 40 * @rpx;
}
.ant-popover-image-content-image {
width: 80 * @rpx;
height: 80 * @rpx;
}
}
&-arrow {
position: absolute;
width: 0;
height: 0;
border-left: 18 * @rpx solid transparent;
border-right: 18 * @rpx solid transparent;
border-bottom: 18 * @rpx solid @popover-inner-bg;
}
&-top {
transform-origin: center bottom;
.popover-position(0.6, -50%, -100%);
&-opening {
opacity: 0;
.popover-position(0.6, -50%, -100%);
.popover-scale-animation('top');
}
&-closing {
opacity: 1;
.popover-scale-animation-close('top');
}
&-arrow {
bottom: 2 * @rpx;
transform: translate(-50%, 100%) rotate(180deg);
left: 50%;
}
}
&-bottom {
transform-origin: center top;
.popover-position(0.6, -50%, 100%);
&-opening {
opacity: 0;
.popover-position(0.6, -50%, 100%);
.popover-scale-animation('bottom');
}
&-closing {
opacity: 1;
.popover-scale-animation-close('bottom');
}
&-arrow {
top: 2 * @rpx;
transform: translate(-50%, -100%) rotate(0deg);
left: 50%;
}
}
&-left {
transform-origin: right center;
.popover-position(0.6, -100%, -50%);
&-opening {
opacity: 0;
.popover-position(0.6, -100%, -50%);
.popover-scale-animation('left');
}
&-closing {
opacity: 1;
.popover-scale-animation-close('left');
}
&-arrow {
right: 2 * @rpx;
transform: translate(75%, -50%) rotate(90deg);
top: 50%;
}
}
&-right {
transform-origin: left center;
.popover-position(0.6, 100%, -50%);
&-opening {
opacity: 0;
.popover-position(0.6, 100%, -50%);
.popover-scale-animation('right');
}
&-closing {
opacity: 1;
.popover-scale-animation-close('right');
}
&-arrow {
left: 2 * @rpx;
transform: translate(-75%, -50%) rotate(-90deg);
top: 50%;
}
}
&-top-left {
transform-origin: left bottom;
.popover-position(0.6, 0, -100%);
&-opening {
opacity: 0;
.popover-position(0.6, 0, -100%);
.popover-scale-animation('top-left');
}
&-closing {
opacity: 1;
.popover-scale-animation-close('top-left');
}
&-arrow {
bottom: 2 * @rpx;
transform: translate(0, 100%) rotate(180deg);
left: 24 * @rpx;
}
}
&-top-right {
transform-origin: right bottom;
.popover-position(0.6, 0, -100%);
&-opening {
opacity: 0;
.popover-position(0.6, 0, -100%);
.popover-scale-animation('top-right');
}
&-closing {
opacity: 1;
.popover-scale-animation-close('top-right');
}
&-arrow {
bottom: 2 * @rpx;
transform: translate(0, 100%) rotate(180deg);
right: 24 * @rpx;
}
}
&-bottom-left {
transform-origin: left top;
.popover-position(0.6, 0, 100%);
&-opening {
opacity: 0;
.popover-position(0.6, 0, 100%);
.popover-scale-animation('bottom-left');
}
&-closing {
opacity: 1;
.popover-scale-animation-close('bottom-left');
}
&-arrow {
top: 2 * @rpx;
transform: translate(0, -100%) rotate(0deg);
left: 24 * @rpx;
}
}
&-bottom-right {
transform-origin: right top;
.popover-position(0.6, 0, 100%);
&-opening {
opacity: 0;
.popover-position(0.6, 0, 100%);
.popover-scale-animation('bottom-right');
}
&-closing {
opacity: 1;
.popover-scale-animation-close('bottom-right');
}
&-arrow {
top: 2 * @rpx;
transform: translate(0, -100%) rotate(0deg);
right: 24 * @rpx;
}
}
&-left-top {
transform-origin: right top;
.popover-position(0.6, -100%, 0);
&-opening {
opacity: 0;
.popover-position(0.6, -100%, 0);
.popover-scale-animation('left-top');
}
&-closing {
opacity: 1;
.popover-scale-animation-close('left-top');
}
&-arrow {
right: 2 * @rpx;
transform: translate(75%, 0) rotate(90deg);
top: 24 * @rpx;
}
}
&-left-bottom {
transform-origin: right bottom;
.popover-position(0.6, -100%, 0);
&-opening {
opacity: 0;
.popover-position(0.6, -100%, 0);
.popover-scale-animation('left-bottom');
}
&-closing {
opacity: 1;
.popover-scale-animation-close('left-bottom');
}
&-arrow {
right: 2 * @rpx;
transform: translate(75%, 0) rotate(90deg);
bottom: 24 * @rpx;
}
}
&-right-top {
transform-origin: left top;
.popover-position(0.6, 100%, 0);
&-opening {
opacity: 0;
.popover-position(0.6, 100%, 0);
.popover-scale-animation('right-top');
}
&-closing {
opacity: 1;
.popover-scale-animation-close('right-top');
}
&-arrow {
left: 2 * @rpx;
transform: translate(-75%, 0) rotate(-90deg);
top: 24 * @rpx;
}
}
&-right-bottom {
transform-origin: left bottom;
.popover-position(0.6, 100%, 0);
&-opening {
opacity: 0;
.popover-position(0.6, 100%, 0);
.popover-scale-animation('right-bottom');
}
&-closing {
opacity: 1;
.popover-scale-animation-close('right-bottom');
}
&-arrow {
left: 2 * @rpx;
transform: translate(-75%, 0) rotate(-90deg);
bottom: 24 * @rpx;
}
}
}
.ant-popover-animation('top', -50%, -100%);
.ant-popover-animation('bottom', -50%, 100%);
.ant-popover-animation('left', -100%, -50%);
.ant-popover-animation('right', 100%, -50%);
.ant-popover-animation('top-left', 0, -100%);
.ant-popover-animation('top-right', 0, -100%);
.ant-popover-animation('bottom-left', 0, 100%);
.ant-popover-animation('bottom-right', 0, 100%);
.ant-popover-animation('left-top', -100%, 0);
.ant-popover-animation('left-bottom', -100%, 0);
.ant-popover-animation('right-top', 100%, 0);
.ant-popover-animation('right-bottom', 100%, 0);
================================================
FILE: src/Popover/index.md
================================================
---
nav:
path: /components
group:
title: 反馈引导
order: 12
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# Popover 气泡
点击元素,弹出气泡式的菜单。用于导航功能的气泡菜单唤起,通常用于收纳低频使用的功能。该功能只能通过导航栏上的图标激活。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-popover": "antd-mini/es/Popover/index"
#endif
#if WECHAT
"ant-popover": "antd-mini/Popover/index"
#endif
}
```
## 代码示例
### 基本使用
```xml
点我
```
### 支持图片和行动点按钮
```xml
点我
```
### 自定义
#### 颜色
```xml
自定义颜色
```
#### 插槽
```xml
插槽
```
#### 位置
可选 `top`、`top-right`、`top-left`、`bottom`、`bottom-left`、`bottom-right`、`left`、`left-top`、`left-bottom`、`right`、`right-top`、`right-bottom`
```xml
点我
```
### 受控
```xml
点击按钮修改visible
更改visible
```
```js
Page({
data: { visible: false },
handleVisibleChange() {
this.setData({
visible: !this.data.visible,
});
},
});
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ------ |
| autoAdjustOverflow | 气泡被遮挡时的自动调整位置 | boolean | true |
| className | 类名 | string | - |
| color | 背景颜色 | string | - |
| contentClassName | content 类名 | string | - |
| contentStyle | content 样式 | string | - |
| content | 内容 | string \| slot | - |
| defaultVisible | 默认是否显示 | boolean | false |
| destroyOnClose | 不可见时是否卸载内容 | boolean | false |
| maskClassName | 蒙层的类名 | string | - |
| maskStyle | 蒙层的样式 | string | - |
| placement | 气泡框位置,可选 `top`、`top-right`、`top-left`、`bottom`、`bottom-left`、`bottom-right`、`left`、`left-top`、`left-bottom`、`right`、`right-top` 或 `right-bottom` | string | top |
| showMask | 是否展示蒙层,为 true 时点击空白处可关闭 Popover | boolean | true |
| style | 样式 | string | - |
| visible | 是否显示 | boolean | - |
| #if ALIPAY onVisibleChange | visible 变更时的回调 | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onTapAction | 操作按钮点击回调 | () => void | - |
| #if WECHAT bindvisiblechange | visible 变更时的回调 | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindtapaction | 操作按钮点击回调 | () => void | - |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| -------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------- |
| --popover-color-background | #404040
| #404040
| Popover 默认底色 |
| --popover-bg | #ffffff
| #1a1a1a
| Popover 背景颜色 |
| --popover-inner-color | #ffffff
| #ffffff
| Popover 内部文字颜色 |
| --popover-text-color | #000000
| #ffffff
| Popover 操作按钮文字颜色 |
================================================
FILE: src/Popover/index.ts
================================================
import mixinValue from '../mixins/value';
import { getInstanceBoundingClientRect } from '../_util/jsapi/get-instance-bounding-client-rect';
import { getSystemInfo } from '../_util/jsapi/get-system-info';
import {
Component,
getValueFromProps,
triggerEvent,
triggerEventOnly,
} from '../_util/simply';
import { assertAilpayNativeNotSupport } from '../_util/support';
import { PopoverDefaultProps } from './props';
import { getPopoverStyle } from './utils';
assertAilpayNativeNotSupport('Popover');
Component({
props: PopoverDefaultProps,
data: {
adjustedPlacement: '',
popoverContentStyle: '',
closing: false,
},
methods: {
getInstance() {
if (this.$id) {
return my;
}
return this;
},
async updatePopover() {
const [placement, autoAdjustOverflow] = getValueFromProps(this, [
'placement',
'autoAdjustOverflow',
]);
const [containerRect, childrenRect, contentRect, systemInfo] =
await Promise.all([
getInstanceBoundingClientRect(
this.getInstance(),
`#ant-popover-children${this.$id ? `-${this.$id}` : ''}`
),
getInstanceBoundingClientRect(
this.getInstance(),
this.$id
? `#ant-popover-children-${this.$id} > *`
: `#ant-popover-children-container`
),
getInstanceBoundingClientRect(
this.getInstance(),
this.$id
? `#ant-popover-content-${this.$id}`
: '#ant-popover-content'
),
getSystemInfo(),
]);
const { popoverContentStyle, adjustedPlacement } = getPopoverStyle(
placement,
autoAdjustOverflow,
{
containerRect,
childrenRect,
contentRect,
systemInfo,
}
);
this.setData({
popoverContentStyle,
adjustedPlacement,
});
},
onVisibleChange(e) {
/// #if ALIPAY
if (
!this.getValue() &&
e.target.id &&
e.target.id.indexOf('ant-popover-') === 0
) {
return;
}
/// #endif
const value = !this.getValue();
if (!this.isControlled()) {
this.update(value);
}
triggerEvent(this, 'visibleChange', value, e);
if (!value) {
this.setData({ closing: true });
}
},
onAnimationEnd() {
if (this.data.closing) {
this.setData({ closing: false });
}
},
onTapAction() {
this.onVisibleChange();
triggerEventOnly(this, 'tapAction');
},
},
mixins: [
mixinValue({
valueKey: 'visible',
defaultValueKey: 'defaultVisible',
transformValue(value) {
if (value) {
this.updatePopover();
}
return {
needUpdate: true,
value,
};
},
}),
],
/// #if ALIPAY
didUpdate(prevProps) {
const [placement, autoAdjustOverflow] = getValueFromProps(this, [
'placement',
'autoAdjustOverflow',
]);
if (
(prevProps.placement !== placement ||
prevProps.autoAdjustOverflow !== autoAdjustOverflow) &&
this.getValue()
) {
this.updatePopover();
}
},
/// #endif
/// #if WECHAT
observers: {
'placement, autoAdjustOverflow, mixin': function () {
if (this.getValue()) {
this.updatePopover();
}
},
},
/// #endif
});
================================================
FILE: src/Popover/props.ts
================================================
import { IBaseProps } from '../_util/base';
/**
* @description 气泡,内部配合 PopoverItem 使用。
*/
export interface IPopoverProps extends IBaseProps {
/**
* @description 是否可见
* @default false
*/
visible: boolean;
/**
* @description 默认是否可见
* @default false
*/
defaultVisible: boolean;
/**
* @description 是否关闭后销毁内部元素
* @default false
*/
destroyOnClose: boolean;
/**
* @description 背景颜色
*/
color: string;
/**
* @description content区样式
*/
contentStyle: string;
/**
* @description content类名
*/
contentClassName: string;
/**
* @description content 内容
*/
content?: string;
/**
* @description 蒙层类名
*/
maskClassName: string;
/**
* @description 蒙层样式
*/
maskStyle: string;
/**
* @description visible 变更时回调
*/
onVisibleChange?: (visible: boolean, e: Record) => void;
/**
* @description 点击行动点
*/
onTapAction?: () => void;
/**
* @description 气泡框位置
*/
placement:
| 'top'
| 'top-right'
| 'top-left'
| 'bottom'
| 'bottom-left'
| 'bottom-right'
| 'left'
| 'left-top'
| 'left-bottom'
| 'right'
| 'right-top'
| 'right-bottom';
/**
* @description 是否展示蒙层
* @default false
*/
showMask: boolean;
/**
* @description 气泡被遮挡时自动调整位置
* @default true
*/
autoAdjustOverflow: boolean;
/**
* @description 是否关闭的icon
* @default false
*/
showCloseIcon: boolean;
/**
* @description 气泡左侧的图片链接
* @default ''
*/
imageUrl: string;
/**
* @description 右侧操作按钮文案
* @default ''
*/
actionText: string;
}
export const PopoverDefaultProps: Partial = {
visible: null,
imageUrl: '',
defaultVisible: false,
destroyOnClose: false,
showCloseIcon: false,
contentClassName: '',
actionText: '',
color: '',
contentStyle: '',
showMask: true,
placement: 'top',
autoAdjustOverflow: true,
maskClassName: '',
maskStyle: '',
content: '',
onVisibleChange() {},
onTapAction() {},
};
================================================
FILE: src/Popover/utils.ts
================================================
interface Rect {
left: number;
top: number;
right: number;
bottom: number;
width: number;
height: number;
}
interface SystemInfo {
windowHeight: number;
windowWidth: number;
}
export function getPopoverStyle(
placement: string,
autoAdjustOverflow: boolean,
size: {
containerRect: Rect;
childrenRect: Rect;
contentRect: Rect;
systemInfo: SystemInfo;
}
) {
const { containerRect, childrenRect, contentRect, systemInfo } = size;
const left = childrenRect.left - containerRect.left;
const top = childrenRect.top - containerRect.top;
const bottom = containerRect.bottom - childrenRect.bottom;
const right = containerRect.right - childrenRect.right;
let adjustedPlacement = placement as string;
const arrowMargin = 12;
const contentRectWidth = contentRect.width + arrowMargin;
const contentRectHeight = contentRect.height + arrowMargin;
if (autoAdjustOverflow) {
if (adjustedPlacement === 'top') {
if (childrenRect.top - contentRectHeight < 0) {
adjustedPlacement = 'bottom';
}
} else if (adjustedPlacement === 'bottom') {
if (childrenRect.bottom + contentRectHeight > systemInfo.windowHeight) {
adjustedPlacement = 'top';
}
} else if (adjustedPlacement === 'left') {
if (childrenRect.left - contentRectWidth < 0) {
adjustedPlacement = 'right';
}
} else if (adjustedPlacement === 'right') {
if (childrenRect.right + contentRectWidth > systemInfo.windowWidth) {
adjustedPlacement = 'left';
}
} else if (adjustedPlacement === 'top-left') {
if (childrenRect.top - contentRectHeight < 0) {
adjustedPlacement = adjustedPlacement.replace('top', 'bottom');
}
if (childrenRect.left + contentRectWidth > systemInfo.windowWidth) {
adjustedPlacement = adjustedPlacement.replace('left', 'right');
}
} else if (adjustedPlacement === 'top-right') {
if (childrenRect.top - contentRectHeight < 0) {
adjustedPlacement = adjustedPlacement.replace('top', 'bottom');
}
if (childrenRect.right - contentRectWidth < 0) {
adjustedPlacement = adjustedPlacement.replace('right', 'left');
}
} else if (adjustedPlacement === 'bottom-left') {
if (childrenRect.bottom + contentRectHeight > systemInfo.windowHeight) {
adjustedPlacement = adjustedPlacement.replace('bottom', 'top');
}
if (childrenRect.left + contentRectWidth > systemInfo.windowWidth) {
adjustedPlacement = adjustedPlacement.replace('left', 'right');
}
} else if (adjustedPlacement === 'bottom-right') {
if (childrenRect.bottom + contentRectHeight > systemInfo.windowHeight) {
adjustedPlacement = adjustedPlacement.replace('bottom', 'top');
}
if (childrenRect.right - contentRectWidth < 0) {
adjustedPlacement = adjustedPlacement.replace('right', 'left');
}
} else if (adjustedPlacement === 'left-top') {
if (childrenRect.left - contentRectWidth < 0) {
adjustedPlacement = adjustedPlacement.replace('left', 'right');
}
if (childrenRect.top + contentRectHeight > systemInfo.windowHeight) {
adjustedPlacement = adjustedPlacement.replace('top', 'bottom');
}
} else if (adjustedPlacement === 'left-bottom') {
if (childrenRect.left - contentRectWidth < 0) {
adjustedPlacement = adjustedPlacement.replace('left', 'right');
}
if (childrenRect.bottom - contentRectHeight < 0) {
adjustedPlacement = adjustedPlacement.replace('bottom', 'top');
}
} else if (adjustedPlacement === 'right-top') {
if (childrenRect.right + contentRectWidth > systemInfo.windowWidth) {
adjustedPlacement = adjustedPlacement.replace('right', 'left');
}
if (childrenRect.top + contentRectHeight > systemInfo.windowHeight) {
adjustedPlacement = adjustedPlacement.replace('top', 'bottom');
}
} else if (adjustedPlacement === 'right-bottom') {
if (childrenRect.right + contentRectWidth > systemInfo.windowWidth) {
adjustedPlacement = adjustedPlacement.replace('right', 'left');
}
if (childrenRect.bottom - contentRectHeight < 0) {
adjustedPlacement = adjustedPlacement.replace('bottom', 'top');
}
}
}
let popoverContentStyle;
if (adjustedPlacement === 'top') {
popoverContentStyle = getStyle({
left: left + childrenRect.width / 2,
top: top - arrowMargin,
});
} else if (adjustedPlacement === 'bottom') {
popoverContentStyle = getStyle({
left: left + childrenRect.width / 2,
bottom: bottom - arrowMargin,
});
} else if (adjustedPlacement === 'left') {
popoverContentStyle = getStyle({
left: left - arrowMargin,
top: top + childrenRect.height / 2,
});
} else if (adjustedPlacement === 'right') {
popoverContentStyle = getStyle({
right: right - arrowMargin,
top: top + childrenRect.height / 2,
});
} else if (adjustedPlacement === 'top-left') {
popoverContentStyle = getStyle({
left,
top: top - arrowMargin,
});
} else if (adjustedPlacement === 'top-right') {
popoverContentStyle = getStyle({
right,
top: top - arrowMargin,
});
} else if (adjustedPlacement === 'bottom-left') {
popoverContentStyle = getStyle({
left,
bottom: bottom - arrowMargin,
});
} else if (adjustedPlacement === 'bottom-right') {
popoverContentStyle = getStyle({
right,
bottom: bottom - arrowMargin,
});
} else if (adjustedPlacement === 'left-top') {
popoverContentStyle = getStyle({
left: left - arrowMargin,
top,
});
} else if (adjustedPlacement === 'left-bottom') {
popoverContentStyle = getStyle({
left: left - arrowMargin,
bottom,
});
} else if (adjustedPlacement === 'right-top') {
popoverContentStyle = getStyle({
right: right - arrowMargin,
top,
});
} else if (adjustedPlacement === 'right-bottom') {
popoverContentStyle = getStyle({
right: right - arrowMargin,
bottom,
});
}
return {
popoverContentStyle,
adjustedPlacement,
};
}
function getStyle(obj) {
return Object.keys(obj)
.map((item) => `${item}: ${obj[item]}px`)
.join(';');
}
================================================
FILE: src/Popover/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
// 蒙层
@popover-mask: @color-product-mask;
// 圆角
@popover-radius: @corner-radius-md;
@popover-bg: var(--popover-bg, @COLOR_CARD);
@popover-text-color: var(--popover-text-color, @COLOR_BLACK);
@popover-inner-bg: var(--popover-color-background, @COLOR_BACKGROUND_POPOVER);
@popover-inner-color: var(--popover-inner-color, @COLOR_WHITE_DEFAULT);
.popover-position(@translate-scale, @translate-x, @translate-y) {
transform: translate(@translate-x, @translate-y) scale(@translate-scale);
}
.popover-scale-animation(@position) {
@open-scale-name: ~'ant-popover-open-scale-@{position}';
@open-opacity-name: ~'ant-popover-open-opacity-@{position}';
animation-name: @open-scale-name, @open-opacity-name;
animation-duration: 200ms, 100ms;
animation-timing-function: cubic-bezier(0.57, -0.22, 0, 1.2),
cubic-bezier(0.35, 0, 0.65, 1);
animation-fill-mode: forwards;
}
.popover-scale-animation-close(@position) {
@close-name: ~'ant-popover-close-@{position}';
animation-name: @close-name;
animation-duration: 200ms;
animation-timing-function: cubic-bezier(0.6, 0, 1, 0.6);
animation-fill-mode: forwards;
}
.ant-popover-animation(@position, @translate-x, @translate-y) {
@open-scale-name: ~'ant-popover-open-scale-@{position}';
@open-opacity-name: ~'ant-popover-open-opacity-@{position}';
@close-name: ~'ant-popover-close-@{position}';
@keyframes @open-scale-name {
0% {
.popover-position(0.6, @translate-x, @translate-y);
}
100% {
.popover-position(1, @translate-x, @translate-y);
}
}
@keyframes @open-opacity-name {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes @close-name {
0% {
.popover-position(1, @translate-x, @translate-y);
opacity: 1;
}
100% {
.popover-position(0.6, @translate-x, @translate-y);
opacity: 0;
}
}
}
================================================
FILE: src/PopoverList/index.axml
================================================
{{ item.text }}
================================================
FILE: src/PopoverList/index.en.md
================================================
---
nav:
path: /components
group:
title: Navigation
order: 6
toc: 'content'
---
# PopoverList
Click on the element to pop up the bubble menu. The bubble menu for navigation functions is evoked, usually used to accommodate functions used at low frequencies. This function can only be activated via the icon on the navigation bar.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-popover-list": "antd-mini/es/PopoverList/index"
#endif
#if WECHAT
"ant-popover-list": "antd-mini/PopoverList/index"
#endif
}
```
## Code Sample
### Basic use
```xml
Point me
```
```js
Page({
data: {
visible: true,
list: [
{ text: '扫一扫', icon: 'ScanningOutline', showBadge: true, id: 'scan', },
{ text: '付钱/收钱', icon: 'ReceivePaymentOutline', showBadge: false, id: 'pay', },
{ text: '乘车码', icon: 'TransportQRcodeOutline', showBadge: false, id: 'code', },
{ text: '图片', iconImage: 'https://gw.alipayobjects.com/mdn/rms_ce4c6f/afts/img/A*XMCgSYx3f50AAAAAAAAAAABkARQnAQ', showBadge: false, id: 'image', },
],
},
handleVisibleChange(visible, e) {
console.log(visible, e);
#if ALIPAY
this.setData({ visible });
#endif
#if WECHAT
this.setData({ visible: visible.detail });
#endif
},
handleTapItem(e, item) {
console.log(e, item);
this.setData({ visible: false });
#if ALIPAY
my.showToast({ content: '点击列表', duration: 1000 });
#endif
#if WECHAT
wx.showToast({ title: '点击列表' });
#endif
},
});
```
### bubble position adaptive
> `autoAdjustOverflow` Property to automatically adjust the position of the bubble when it is occluded
```xml
bubble position adaptive
```
### Demo Code
## API
| Property | Description | Type | Default Value |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------ |
| autoAdjustOverflow | Automatically adjust the position when the bubble is blocked | boolean | true |
| className | Class Name | string | - |
| color | Background Color | string | - |
| contentClassName | content Class Name | string | - |
| contentStyle | content Style | string | - |
| content | Content | string \| slot | - |
| defaultVisible | Display by default | boolean | false |
| destroyOnClose | Whether to unload content when invisible | boolean | false |
| maskClassName | Class name of the layer | string | - |
| maskStyle | The style of the layer | string | - |
| placement | Bubble box position, optional `top`、`top-right`、`top-left`、`bottom`、`bottom-left`、`bottom-right`、`left`、`left-top`、`left-bottom`、`right`、`right-top` or `right-bottom` | string | top |
| showMask | Whether to show the layer, if true, click the blank to close the Popover. | boolean | true |
| style | Style | string | - |
| visible | Display | boolean | - |
| #if ALIPAY onVisibleChange | Callback at the time of visible change | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onTapItem | Callback at the time of visible change | (item: [PopoverListItem](#popoverlistitem), event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindvisiblechange | Callback at the time of visible change | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindtapitem | Callback at the time of visible change | (item: [PopoverListItem](#popoverlistitem), event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
### PopoverListItem
| Property | Description | Type | Default Value |
| --------- | ------------------------------------------------------- | ------- | ------ |
| icon | The icon of the menu | string | - |
| iconImage | The icon picture of the menu is taken first iconImage the icon field is not taken again. | string | - |
| text | Class Name | string | - |
| showBadge | Disable | boolean | false |
| id | Extra right | string | - |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For more information, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| --popover-list-content-bg | rgba(0, 0, 0, 0.93)
| rgba(0, 0, 0, 0.93)
| Popover List Content Background Color |
| --popover-list-content-color | #ffffff
| #c5cad1
| Popover List Content Color |
| --popover-list-badge-color | #ff411c
| #ff411c
| Popover List Badge Color |
| --popover-list-item-bg | #e5e5e5
| #444444
| Popover list content text color |
================================================
FILE: src/PopoverList/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-icon": "../Icon/index",
"ant-mask": "../Mask/index"
}
}
================================================
FILE: src/PopoverList/index.less
================================================
@import (reference) './variable.less';
@import '../style/mixins/hairline.less';
@popoverPrefix: ant-popover-list;
.@{popoverPrefix} {
position: relative;
&-children {
z-index: 999;
}
&-mask {
z-index: 998;
background: none;
}
&-content {
position: absolute;
// min-width: 64 * @rpx;
max-width: calc(100vw - 48 * @rpx);
z-index: 999;
border-radius: 24 * @rpx;
background: @popover-content-bg;
box-shadow: 0 4 * @rpx 10 * @rpx 0 @popover-content-bg;
color: @popover-content-color;
&-arrow {
position: absolute;
width: 0;
height: 0;
border-left: 18 * @rpx solid transparent;
border-right: 18 * @rpx solid transparent;
border-bottom: 18 * @rpx solid @popover-content-bg;
}
.ant-popover-list-item::before {
background: @popover-list-item-bg;
}
}
&-item {
padding: 26 * @rpx 24 * @rpx 26 * @rpx 32 * @rpx;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: row;
position: relative;
z-index: 1;
&-0 {
&::before {
display: none;
}
}
&::before {
content: '';
position: absolute;
top: 0;
left: 16 * @rpx;
width: calc(200% - @popover-content-width);
height: 2 * @rpx;
transform: scale(0.5);
background: @popover-content-color;
z-index: 2;
}
&-left {
padding-right: 16 * @rpx;
width: 48 * @rpx;
height: 48 * @rpx;
overflow: hidden;
.ant-icon {
font-size: 48 * @rpx;
width: 48 * @rpx;
height: 48 * @rpx;
color: @popover-content-color;
}
}
&-image {
width: 48 * @rpx;
height: 48 * @rpx;
}
&-text {
font-weight: 400;
font-size: 30 * @rpx;
letter-spacing: 0;
white-space: nowrap;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: row;
&-badge {
background: @popover-badge-color;
width: 20 * @rpx;
border-radius: 50vh;
height: 20 * @rpx;
margin-left: 8 * @rpx;
}
}
}
&-top {
transform-origin: center bottom;
.popover-list-position(0.4, -50%, -100%);
&-opening {
opacity: 0;
.popover-list-position(0.4, -50%, -100%);
.popover-list-scale-animation('top');
}
&-closing {
opacity: 1;
.popover-list-scale-animation-close('top');
}
&-arrow {
bottom: 2 * @rpx;
transform: translate(-50%, 100%) rotate(180deg);
left: 50%;
}
}
&-bottom {
transform-origin: center top;
.popover-list-position(0.4, -50%, 100%);
&-opening {
opacity: 0;
.popover-list-position(0.4, -50%, 100%);
.popover-list-scale-animation('bottom');
}
&-closing {
opacity: 1;
.popover-list-scale-animation-close('bottom');
}
&-arrow {
top: 2 * @rpx;
transform: translate(-50%, -100%) rotate(0deg);
left: 50%;
}
}
&-left {
transform-origin: right center;
.popover-list-position(0.4, -100%, -50%);
&-opening {
opacity: 0;
.popover-list-position(0.4, -100%, -50%);
.popover-list-scale-animation('left');
}
&-closing {
opacity: 1;
.popover-list-scale-animation-close('left');
}
&-arrow {
right: 2 * @rpx;
transform: translate(75%, -50%) rotate(90deg);
top: 50%;
}
}
&-right {
transform-origin: left center;
.popover-list-position(0.4, 100%, -50%);
&-opening {
opacity: 0;
.popover-list-position(0.4, 100%, -50%);
.popover-list-scale-animation('right');
}
&-closing {
opacity: 1;
.popover-list-scale-animation-close('right');
}
&-arrow {
left: 2 * @rpx;
transform: translate(-75%, -50%) rotate(-90deg);
top: 50%;
}
}
&-top-left {
transform-origin: left bottom;
.popover-list-position(0.4, 0, -100%);
&-opening {
opacity: 0;
.popover-list-position(0.4, 0, -100%);
.popover-list-scale-animation('top-left');
}
&-closing {
opacity: 1;
.popover-list-scale-animation-close('top-left');
}
&-arrow {
bottom: 2 * @rpx;
transform: translate(0, 100%) rotate(180deg);
left: 24 * @rpx;
}
}
&-top-right {
transform-origin: right bottom;
.popover-list-position(0.4, 0, -100%);
&-opening {
opacity: 0;
.popover-list-position(0.4, 0, -100%);
.popover-list-scale-animation('top-right');
}
&-closing {
opacity: 1;
.popover-list-scale-animation-close('top-right');
}
&-arrow {
bottom: 2 * @rpx;
transform: translate(0, 100%) rotate(180deg);
right: 24 * @rpx;
}
}
&-bottom-left {
transform-origin: left top;
.popover-list-position(0.4, 0, 100%);
&-opening {
opacity: 0;
.popover-list-position(0.4, 0, 100%);
.popover-list-scale-animation('bottom-left');
}
&-closing {
opacity: 1;
.popover-list-scale-animation-close('bottom-left');
}
&-arrow {
top: 2 * @rpx;
transform: translate(0, -100%) rotate(0deg);
left: 24 * @rpx;
}
}
&-bottom-right {
transform-origin: right top;
.popover-list-position(0.4, 0, 100%);
&-opening {
opacity: 0;
.popover-list-position(0.4, 0, 100%);
.popover-list-scale-animation('bottom-right');
}
&-closing {
opacity: 1;
.popover-list-scale-animation-close('bottom-right');
}
&-arrow {
top: 2 * @rpx;
transform: translate(0, -100%) rotate(0deg);
right: 24 * @rpx;
}
}
&-left-top {
transform-origin: right top;
.popover-list-position(0.4, -100%, 0);
&-opening {
opacity: 0;
.popover-list-position(0.4, -100%, 0);
.popover-list-scale-animation('left-top');
}
&-closing {
opacity: 1;
.popover-list-scale-animation-close('left-top');
}
&-arrow {
right: 2 * @rpx;
transform: translate(75%, 0) rotate(90deg);
top: 24 * @rpx;
}
}
&-left-bottom {
transform-origin: right bottom;
.popover-list-position(0.4, -100%, 0);
&-opening {
opacity: 0;
.popover-list-position(0.4, -100%, 0);
.popover-list-scale-animation('left-bottom');
}
&-closing {
opacity: 1;
.popover-list-scale-animation-close('left-bottom');
}
&-arrow {
right: 2 * @rpx;
transform: translate(75%, 0) rotate(90deg);
bottom: 24 * @rpx;
}
}
&-right-top {
transform-origin: left top;
.popover-list-position(0.4, 100%, 0);
&-opening {
opacity: 0;
.popover-list-position(0.4, 100%, 0);
.popover-list-scale-animation('right-top');
}
&-closing {
opacity: 1;
.popover-list-scale-animation-close('right-top');
}
&-arrow {
left: 2 * @rpx;
transform: translate(-75%, 0) rotate(-90deg);
top: 24 * @rpx;
}
}
&-right-bottom {
transform-origin: left bottom;
.popover-list-position(0.4, 100%, 0);
&-opening {
opacity: 0;
.popover-list-position(0.4, 100%, 0);
.popover-list-scale-animation('right-bottom');
}
&-closing {
opacity: 1;
.popover-list-scale-animation-close('right-bottom');
}
&-arrow {
left: 2 * @rpx;
transform: translate(-75%, 0) rotate(-90deg);
bottom: 24 * @rpx;
}
}
}
.ant-popover-list-animation('top', -50%, -100%);
.ant-popover-list-animation('bottom', -50%, 100%);
.ant-popover-list-animation('left', -100%, -50%);
.ant-popover-list-animation('right', 100%, -50%);
.ant-popover-list-animation('top-left', 0, -100%);
.ant-popover-list-animation('top-right', 0, -100%);
.ant-popover-list-animation('bottom-left', 0, 100%);
.ant-popover-list-animation('bottom-right', 0, 100%);
.ant-popover-list-animation('left-top', -100%, 0);
.ant-popover-list-animation('left-bottom', -100%, 0);
.ant-popover-list-animation('right-top', 100%, 0);
.ant-popover-list-animation('right-bottom', 100%, 0);
================================================
FILE: src/PopoverList/index.md
================================================
---
nav:
path: /components
group:
title: 导航
order: 6
toc: 'content'
---
# PopoverList 气泡菜单
点击元素,弹出气泡式的菜单。用于导航功能的气泡菜单唤起,通常用于收纳低频使用的功能。该功能只能通过导航栏上的图标激活。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-popover-list": "antd-mini/es/PopoverList/index"
#endif
#if WECHAT
"ant-popover-list": "antd-mini/PopoverList/index"
#endif
}
```
## 代码示例
### 基本使用
```xml
点我
```
```js
Page({
data: {
visible: true,
list: [
{ text: '扫一扫', icon: 'ScanningOutline', showBadge: true, id: 'scan', },
{ text: '付钱/收钱', icon: 'ReceivePaymentOutline', showBadge: false, id: 'pay', },
{ text: '乘车码', icon: 'TransportQRcodeOutline', showBadge: false, id: 'code', },
{ text: '图片', iconImage: 'https://gw.alipayobjects.com/mdn/rms_ce4c6f/afts/img/A*XMCgSYx3f50AAAAAAAAAAABkARQnAQ', showBadge: false, id: 'image', },
],
},
handleVisibleChange(visible, e) {
console.log(visible, e);
#if ALIPAY
this.setData({ visible });
#endif
#if WECHAT
this.setData({ visible: visible.detail });
#endif
},
handleTapItem(e, item) {
console.log(e, item);
this.setData({ visible: false });
#if ALIPAY
my.showToast({ content: '点击列表', duration: 1000 });
#endif
#if WECHAT
wx.showToast({ title: '点击列表' });
#endif
},
});
```
### 气泡位置自适应
> `autoAdjustOverflow` 属性,可以让气泡被遮挡时自动调整位置
```xml
气泡位置自适应
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------ |
| autoAdjustOverflow | 气泡被遮挡时的自动调整位置 | boolean | true |
| className | 类名 | string | - |
| color | 背景颜色 | string | - |
| contentClassName | content 类名 | string | - |
| contentStyle | content 样式 | string | - |
| content | 内容 | string \| slot | - |
| defaultVisible | 默认是否显示 | boolean | false |
| destroyOnClose | 不可见时是否卸载内容 | boolean | false |
| maskClassName | 蒙层的类名 | string | - |
| maskStyle | 蒙层的样式 | string | - |
| placement | 气泡框位置,可选 `top`、`top-right`、`top-left`、`bottom`、`bottom-left`、`bottom-right`、`left`、`left-top`、`left-bottom`、`right`、`right-top` 或 `right-bottom` | string | top |
| showMask | 是否展示蒙层,为 true 时点击空白处可关闭 Popover | boolean | true |
| style | 样式 | string | - |
| visible | 是否显示 | boolean | - |
| #if ALIPAY onVisibleChange | visible 变更时的回调 | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if ALIPAY onTapItem | visible 变更时的回调 | (item: [PopoverListItem](#popoverlistitem), event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindvisiblechange | visible 变更时的回调 | (visible: boolean, event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
| #if WECHAT bindtapitem | visible 变更时的回调 | (item: [PopoverListItem](#popoverlistitem), event: [Event](https://opendocs.alipay.com/mini/framework/event-object)) => void | - |
### PopoverListItem
| 属性 | 说明 | 类型 | 默认值 |
| --------- | ------------------------------------------------------- | ------- | ------ |
| icon | 菜单的 icon | string | - |
| iconImage | 菜单的 icon 的图片, 优先取 iconImage 没有再取 icon 字段 | string | - |
| text | 类名 | string | - |
| showBadge | 是否禁用 | boolean | false |
| id | 右侧额外内容 | string | - |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| --popover-list-content-bg | rgba(0, 0, 0, 0.93)
| rgba(0, 0, 0, 0.93)
| Popover 列表内容背景颜色 |
| --popover-list-content-color | #ffffff
| #c5cad1
| Popover 列表内容颜色 |
| --popover-list-badge-color | #ff411c
| #ff411c
| Popover 列表徽章颜色 |
| --popover-list-item-bg | #e5e5e5
| #444444
| Popover 列表内容文字颜色 |
================================================
FILE: src/PopoverList/index.ts
================================================
import mixinValue from '../mixins/value';
import { getInstanceBoundingClientRect } from '../_util/jsapi/get-instance-bounding-client-rect';
import { getSystemInfo } from '../_util/jsapi/get-system-info';
import { Component, getValueFromProps, triggerEvent } from '../_util/simply';
import { assertAilpayNativeNotSupport } from '../_util/support';
import { PopoverDefaultProps } from './props';
import { getPopoverStyle } from './utils';
assertAilpayNativeNotSupport('PopoverList');
Component({
props: PopoverDefaultProps,
data: {
adjustedPlacement: '',
popoverContentStyle: '',
closing: false,
},
methods: {
getInstance() {
if (this.$id) {
return my;
}
return this;
},
onTapItem(e) {
const { item } = e.currentTarget.dataset;
triggerEvent(this, 'tapItem', item, e);
},
async updatePopover() {
const [placement, autoAdjustOverflow] = getValueFromProps(this, [
'placement',
'autoAdjustOverflow',
]);
const [containerRect, childrenRect, contentRect, systemInfo] =
await Promise.all([
getInstanceBoundingClientRect(
this.getInstance(),
`#ant-popover-list-children${this.$id ? `-${this.$id}` : ''}`
),
getInstanceBoundingClientRect(
this.getInstance(),
this.$id
? `#ant-popover-list-children-${this.$id} > *`
: `#ant-popover-list-children-container`
),
getInstanceBoundingClientRect(
this.getInstance(),
this.$id
? `#ant-popover-list-content-${this.$id}`
: '#ant-popover-list-content'
),
getSystemInfo(),
]);
const { popoverContentStyle, adjustedPlacement } = getPopoverStyle(
placement,
autoAdjustOverflow,
{
containerRect,
childrenRect,
contentRect,
systemInfo,
}
);
this.setData({
popoverContentStyle,
adjustedPlacement,
});
},
onVisibleChange(e) {
/// #if ALIPAY
if (
!this.getValue() &&
e.target.id &&
e.target.id.indexOf('ant-popover-list-') === 0
) {
return;
}
/// #endif
const value = !this.getValue();
if (!this.isControlled()) {
this.update(value);
}
triggerEvent(this, 'visibleChange', value, e);
if (!value) {
this.setData({ closing: true });
}
},
onAnimationEnd() {
if (this.data.closing) {
this.setData({ closing: false });
}
},
},
mixins: [
mixinValue({
valueKey: 'visible',
defaultValueKey: 'defaultVisible',
transformValue(value) {
if (value) {
this.updatePopover();
}
return {
needUpdate: true,
value,
};
},
}),
],
/// #if ALIPAY
didUpdate(prevProps) {
const [placement, autoAdjustOverflow] = getValueFromProps(this, [
'placement',
'autoAdjustOverflow',
]);
if (
(prevProps.placement !== placement ||
prevProps.autoAdjustOverflow !== autoAdjustOverflow) &&
this.getValue()
) {
this.updatePopover();
}
},
/// #endif
/// #if WECHAT
observers: {
'placement, autoAdjustOverflow, mixin': function () {
if (this.getValue()) {
this.updatePopover();
}
},
},
/// #endif
});
================================================
FILE: src/PopoverList/props.ts
================================================
import { IBaseProps } from '../_util/base';
export interface PopoverListItem {
/**
* @description 菜单的icon
*/
icon: string;
/**
* @description 菜单的icon的图片, 优先取iconImage 没有再取icon字段
* @default ''
*/
iconImage: string;
/**
* @description 菜单项的文案,如果没有则不展示该菜单
* @default ''
*/
text: string;
/**
* @description 菜单项是否展示红点徽标
* @default false
*/
showBadge: boolean;
/**
* @description 菜单项的唯一标识
* @default ''
*/
id: string;
}
/**
* @description 气泡,内部配合 PopoverItem 使用。
*/
export interface IPopoverListProps extends IBaseProps {
/**
* @description 是否可见
* @default false
*/
visible: boolean;
/**
* @description 默认是否可见
* @default false
*/
defaultVisible: boolean;
/**
* @description 是否关闭后销毁内部元素
* @default false
*/
destroyOnClose: boolean;
/**
* @description content区样式
*/
contentStyle: string;
/**
* @description content类名
*/
contentClassName: string;
/**
* @description content 内容
*/
content?: string;
/**
* @description 蒙层类名
*/
maskClassName: string;
/**
* @description 蒙层样式
*/
maskStyle: string;
/**
* @description visible 变更时回调
*/
onVisibleChange?: (visible: boolean, e: Record) => void;
/**
* @description 点击列表项
*/
onTapItem?: (item: PopoverListItem, e: Record) => void;
/**
* @description 气泡框位置
*/
placement:
| 'top'
| 'top-right'
| 'top-left'
| 'bottom'
| 'bottom-left'
| 'bottom-right'
| 'left'
| 'left-top'
| 'left-bottom'
| 'right'
| 'right-top'
| 'right-bottom';
/**
* @description 是否展示蒙层
* @default false
*/
showMask: boolean;
/**
* @description 气泡被遮挡时自动调整位置
* @default true
*/
autoAdjustOverflow: boolean;
/**
* @description 菜单列表数据
* @default []
*/
list: PopoverListItem[];
}
export const PopoverDefaultProps: Partial = {
visible: null,
defaultVisible: false,
destroyOnClose: false,
contentClassName: '',
contentStyle: '',
showMask: true,
placement: 'top',
autoAdjustOverflow: true,
maskClassName: '',
maskStyle: '',
content: '',
list: [],
onVisibleChange() {},
onTapItem() {},
};
================================================
FILE: src/PopoverList/utils.ts
================================================
interface Rect {
left: number;
top: number;
right: number;
bottom: number;
width: number;
height: number;
}
interface SystemInfo {
windowHeight: number;
windowWidth: number;
}
export function getPopoverStyle(
placement: string,
autoAdjustOverflow: boolean,
size: {
containerRect: Rect;
childrenRect: Rect;
contentRect: Rect;
systemInfo: SystemInfo;
}
) {
const { containerRect, childrenRect, contentRect, systemInfo } = size;
const left = childrenRect.left - containerRect.left;
const top = childrenRect.top - containerRect.top;
const bottom = containerRect.bottom - childrenRect.bottom;
const right = containerRect.right - childrenRect.right;
let adjustedPlacement = placement as string;
const arrowMargin = 12;
const contentRectWidth = contentRect.width + arrowMargin;
const contentRectHeight = contentRect.height + arrowMargin;
if (autoAdjustOverflow) {
if (adjustedPlacement === 'top') {
if (childrenRect.top - contentRectHeight < 0) {
adjustedPlacement = 'bottom';
}
} else if (adjustedPlacement === 'bottom') {
if (childrenRect.bottom + contentRectHeight > systemInfo.windowHeight) {
adjustedPlacement = 'top';
}
} else if (adjustedPlacement === 'left') {
if (childrenRect.left - contentRectWidth < 0) {
adjustedPlacement = 'right';
}
} else if (adjustedPlacement === 'right') {
if (childrenRect.right + contentRectWidth > systemInfo.windowWidth) {
adjustedPlacement = 'left';
}
} else if (adjustedPlacement === 'top-left') {
if (childrenRect.top - contentRectHeight < 0) {
adjustedPlacement = adjustedPlacement.replace('top', 'bottom');
}
if (childrenRect.left + contentRectWidth > systemInfo.windowWidth) {
adjustedPlacement = adjustedPlacement.replace('left', 'right');
}
} else if (adjustedPlacement === 'top-right') {
if (childrenRect.top - contentRectHeight < 0) {
adjustedPlacement = adjustedPlacement.replace('top', 'bottom');
}
if (childrenRect.right - contentRectWidth < 0) {
adjustedPlacement = adjustedPlacement.replace('right', 'left');
}
} else if (adjustedPlacement === 'bottom-left') {
if (childrenRect.bottom + contentRectHeight > systemInfo.windowHeight) {
adjustedPlacement = adjustedPlacement.replace('bottom', 'top');
}
if (childrenRect.left + contentRectWidth > systemInfo.windowWidth) {
adjustedPlacement = adjustedPlacement.replace('left', 'right');
}
} else if (adjustedPlacement === 'bottom-right') {
if (childrenRect.bottom + contentRectHeight > systemInfo.windowHeight) {
adjustedPlacement = adjustedPlacement.replace('bottom', 'top');
}
if (childrenRect.right - contentRectWidth < 0) {
adjustedPlacement = adjustedPlacement.replace('right', 'left');
}
} else if (adjustedPlacement === 'left-top') {
if (childrenRect.left - contentRectWidth < 0) {
adjustedPlacement = adjustedPlacement.replace('left', 'right');
}
if (childrenRect.top + contentRectHeight > systemInfo.windowHeight) {
adjustedPlacement = adjustedPlacement.replace('top', 'bottom');
}
} else if (adjustedPlacement === 'left-bottom') {
if (childrenRect.left - contentRectWidth < 0) {
adjustedPlacement = adjustedPlacement.replace('left', 'right');
}
if (childrenRect.bottom - contentRectHeight < 0) {
adjustedPlacement = adjustedPlacement.replace('bottom', 'top');
}
} else if (adjustedPlacement === 'right-top') {
if (childrenRect.right + contentRectWidth > systemInfo.windowWidth) {
adjustedPlacement = adjustedPlacement.replace('right', 'left');
}
if (childrenRect.top + contentRectHeight > systemInfo.windowHeight) {
adjustedPlacement = adjustedPlacement.replace('top', 'bottom');
}
} else if (adjustedPlacement === 'right-bottom') {
if (childrenRect.right + contentRectWidth > systemInfo.windowWidth) {
adjustedPlacement = adjustedPlacement.replace('right', 'left');
}
if (childrenRect.bottom - contentRectHeight < 0) {
adjustedPlacement = adjustedPlacement.replace('bottom', 'top');
}
}
}
let popoverContentStyle;
if (adjustedPlacement === 'top') {
popoverContentStyle = getStyle({
left: left + childrenRect.width / 2,
top: top - arrowMargin,
});
} else if (adjustedPlacement === 'bottom') {
popoverContentStyle = getStyle({
left: left + childrenRect.width / 2,
bottom: bottom - arrowMargin,
});
} else if (adjustedPlacement === 'left') {
popoverContentStyle = getStyle({
left: left - arrowMargin,
top: top + childrenRect.height / 2,
});
} else if (adjustedPlacement === 'right') {
popoverContentStyle = getStyle({
right: right - arrowMargin,
top: top + childrenRect.height / 2,
});
} else if (adjustedPlacement === 'top-left') {
popoverContentStyle = getStyle({
left,
top: top - arrowMargin,
});
} else if (adjustedPlacement === 'top-right') {
popoverContentStyle = getStyle({
right,
top: top - arrowMargin,
});
} else if (adjustedPlacement === 'bottom-left') {
popoverContentStyle = getStyle({
left,
bottom: bottom - arrowMargin,
});
} else if (adjustedPlacement === 'bottom-right') {
popoverContentStyle = getStyle({
right,
bottom: bottom - arrowMargin,
});
} else if (adjustedPlacement === 'left-top') {
popoverContentStyle = getStyle({
left: left - arrowMargin,
top,
});
} else if (adjustedPlacement === 'left-bottom') {
popoverContentStyle = getStyle({
left: left - arrowMargin,
bottom,
});
} else if (adjustedPlacement === 'right-top') {
popoverContentStyle = getStyle({
right: right - arrowMargin,
top,
});
} else if (adjustedPlacement === 'right-bottom') {
popoverContentStyle = getStyle({
right: right - arrowMargin,
bottom,
});
}
return {
popoverContentStyle,
adjustedPlacement,
};
}
function getStyle(obj) {
return Object.keys(obj)
.map((item) => `${item}: ${obj[item]}px`)
.join(';');
}
================================================
FILE: src/PopoverList/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
// 蒙层
@popover-mask: @color-product-mask;
// 圆角
@popover-radius: @corner-radius-md;
@popover-content-width: 192 * @rpx;
//content
@popover-content-bg: var(--popover-list-content-bg, rgba(0, 0, 0, 0.93));
@popover-content-color: var(--popover-list-content-color, @COLOR_TEXT_PRIMARY);
@popover-list-item-bg: var(--popover-list-item-bg, @COLOR_BORDER);
@popover-badge-color: var(--popover-list-badge-color, #ff411c);
.popover-list-position(@translate-scale, @translate-x, @translate-y) {
transform: translate(@translate-x, @translate-y) scale(@translate-scale);
}
.popover-list-scale-animation(@position) {
@open-name: ~'ant-popover-list-open-@{position}';
animation-name: @open-name;
animation-duration: 300ms;
animation-timing-function: cubic-bezier(0.57, -0.22, 0, 1.2);
animation-fill-mode: forwards;
}
.popover-list-scale-animation-close(@position) {
@close-name: ~'ant-popover-list-close-@{position}';
animation-name: @close-name;
animation-duration: 200ms;
animation-timing-function: cubic-bezier(0.6, 0, 1, 0.6);
animation-fill-mode: forwards;
}
.ant-popover-list-animation(@position, @translate-x, @translate-y) {
@open-name: ~'ant-popover-list-open-@{position}';
@close-name: ~'ant-popover-list-close-@{position}';
@keyframes @open-name {
0% {
.popover-list-position(0.4, @translate-x, @translate-y);
opacity: 0;
}
100% {
.popover-list-position(1, @translate-x, @translate-y);
opacity: 1;
}
}
@keyframes @close-name {
0% {
.popover-list-position(1, @translate-x, @translate-y);
opacity: 1;
}
100% {
.popover-list-position(0.4, @translate-x, @translate-y);
opacity: 0;
}
}
}
================================================
FILE: src/Popup/index.axml
================================================
================================================
FILE: src/Popup/index.en.md
================================================
---
nav:
path: /components
group:
title: Feedback
order: 12
toc: 'content'
---
# Popup
Slides or pops up a custom content area from the screen. It is used to display pop-up windows, information prompts, selection input, switching, and other content. It supports multiple pop-up layers for overlay display.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-popup": "antd-mini/es/Popup/index"
#endif
#if WECHAT
"ant-popup": "antd-mini/Popup/index"
#endif
}
```
## Code Sample
### Basic use
> - `ant-popup` Components must pass `visible` Property controls the show/hide state.`position` Specifies the direction of occurrence, optional `top` `bottom` `left` `right`. The contents of the pop-up window are filled in the form of slots.
> - `close` The event will be triggered when the icon is closed by clicking the upper right corner or when the layer is closed by clicking the layer.
> - `clickBack` event is triggered when the back button in the upper left corner is clicked.
```xml
A custom content area slides out or pops up from the screen to display pop-up windows, information prompts, selection input, switching, etc.
```
```js
Page({
data: {
visible: true,
},
onClickBack() {
#if ALIPAY
my.showToast({ content: 'clicked icon', duration: 1000 });
#endif
#if WECHAT
wx.showToast({ content: 'clicked icon', duration: 1000 });
#endif
},
handlePopupClose() {
this.setData({
visible: false,
});
},
});
```
### Set background map
> `backgroundImage` property, you can set the background for the entire bullet layer.
```xml
Ea consectetur ipsum consequat exercitation laboris excepteur pariatur
excepteur labore dolor cillum tempor esse. Ad adipisicing nostrud fugiat eu
mollit. Proident nisi voluptate sunt ea laboris Lorem ullamco deserunt aute
incididunt cillum tempor duis est. Elit voluptate laboris laborum anim
```
### Extra Long Content Scrolling
> If the content in the pop-up window is too long and needs to support scrolling, please use [scroll-view](https://opendocs.alipay.com/mini/component/scroll-view?pathHash=8292073d) component and add the following properties:
```xml
Ea consectetur ipsum consequat exercitation laboris excepteur pariatur
excepteur labore dolor cillum tempor esse. Ad adipisicing nostrud fugiat eu
mollit. Proident nisi voluptate sunt ea laboris Lorem ullamco deserunt aute
incididunt cillum tempor duis est. Elit voluptate laboris laborum anim
exercitation consequat laboris ad. Quis ad enim fugiat.
```
### Demo Code
## API
| Property | Description | Type | Default Value |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ----------- |
| animation | Whether to turn on transition animation | boolean | true |
| animationType | Animation type, optional`transform` `position`, the default is used`transform`Animation performance is better. However, when there is a fixed positioning or picker-view element inside the pop-up window, there may be a style problem, which can be switched`position`Solve | string | `transform` |
| className | Class Name | string | - |
| title | Title | string | '' |
| destroyOnClose | Unload content when invisible | boolean | false |
| duration | Transition animation duration, in milliseconds | number | 300 |
| height | height, in position `top` or `bottom` When used, the unit is px. Optional, when not transmitted, it is highly adaptive according to the content area. | number | - |
| maskClassName | Class name of the layer | string | - |
| maskStyle | The style of the layer | string | - |
| position | Pop-up window layout, optional`top` `bottom` `left` `right` | string | `bottom` |
| showMask | Whether to show the layer | boolean | true |
| style | Style | string | - |
| visible | Whether to display | boolean | false |
| width | The width, in position, is `left` or `right` unit px | number | - |
| zIndex | Pop-up Level | number | 998 |
| backgroundImage | Background map of the bullet box | string | - |
| showClose | Show icon with bullet box closed | boolean | false |
| showBack | Show the icon returned by the bullet box | boolean | false |
| #if ALIPAY onClose | Click the mask to close and trigger the callback. | () => void | - |
| #if ALIPAY onClickBack | Click the back button to trigger the callback | () => void | - |
| #if ALIPAY onAfterShow | Trigger after full display | () => void | - |
| #if ALIPAY onAfterClose | Trigger after full shutdown | () => void | - |
| #if WECHAT bindclose | Click the mask to close and trigger the callback. | () => void | - |
| #if WECHAT bindclickbackicon | Click the back button to trigger the callback | () => void | - |
| #if WECHAT bindaftershow | Trigger after full display | () => void | - |
| #if WECHAT bindafterclose | Trigger after full shutdown | () => void | - |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Default Value | Dark Mode Default | Remarks |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| --popover-list-content-bg | rgba(0, 0, 0, 0.93)
| rgba(0, 0, 0, 0.93)
| Popover List Content Background Color |
| --popover-list-content-color | #ffffff
| #c5cad1
| Popover List Content Color |
| --popover-list-badge-color | #ff411c
| #ff411c
| Popover List Badge Color |
| --popover-list-content-color | #333333
| #c5cad1
| Popover list content text color |
## FAQ
### After Popup is opened, what if the page behind the layer can scroll?
Preventing page scrolling after the layer is currently not effective in IDE and emulator, please debug on real machine.
### Popup internal elements need to support scrolling how to deal?
If you need to scroll in the pop-up window, use the scroll-view component and add the following attributes:
```html
...你的内容...
```
### How to solve the abnormal display of picker-view inside Popup?
Popup is passed by default. `display:none` Hidden, and picker-view cannot be placed in `display:none` in the components. There are two solutions:
1. Add attribute on picker-view `a:if="{{popupVisible}}"`, the picker-view is displayed when the Popup is displayed.
2. Set on Popup `destroyOnClose="{{true}}"`to unload content when the Popup is not visible.
================================================
FILE: src/Popup/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-icon": "../Icon/index"
}
}
================================================
FILE: src/Popup/index.less
================================================
@import (reference) './variable.less';
@popupPrefix: ant-popup;
.@{popupPrefix} {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 998;
touch-action: none;
&-top-image {
width: 100%;
}
&-mask {
width: 100%;
height: 100%;
background-color: @popup-mask-product;
.mask-appear();
&.center {
.mask-appear(300ms);
}
&-closing {
.mask-close();
&.center {
.mask-close(300ms);
}
}
}
&-content {
overflow: hidden;
position: fixed;
background: @popup-background;
color: @popup-color;
&-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background-size: cover;
background-repeat: no-repeat;
}
&-header {
display: flex;
justify-content: center;
align-items: center;
height: 114rpx;
position: relative;
&-title {
font-family: 500;
line-height: 50 * @rpx;
font-size: 36 * @rpx;
color: @popup-color;
text-align: center;
}
&-icon {
width: 48 * @rpx;
height: 48 * @rpx;
font-size: 42 * @rpx;
position: absolute;
top: 33 * @rpx;
color: @popup-assit-color;
display: flex;
justify-content: center;
align-items: center;
&-back {
left: 24 * @rpx;
}
&-close {
right: 24 * @rpx;
}
}
}
}
&-transform {
&-top {
top: 0;
left: 0;
right: 0;
transform: translateY(-100%);
.animation-transform(ant-popup-transform-top);
border-radius: 0 0 @popup-radius @popup-radius;
&-close {
transform: translateY(0);
.animation-transform-close(ant-popup-transform-top-close);
}
}
&-right {
top: 0;
right: 0;
bottom: 0;
transform: translateX(100%);
.animation-transform(ant-popup-transform-right);
&-close {
transform: translateX(0);
.animation-transform-close(ant-popup-transform-right-close);
}
}
&-bottom {
left: 0;
right: 0;
bottom: 0;
transform: translateY(100%);
.animation-transform(ant-popup-transform-bottom);
border-radius: @popup-radius @popup-radius 0 0;
&-close {
transform: translateY(0);
.animation-transform(ant-popup-transform-bottom-close);
}
}
&-left {
top: 0;
left: 0;
bottom: 0;
transform: translateX(-100%);
.animation-transform(ant-popup-transform-left);
&-close {
transform: translateX(0);
.animation-transform(ant-popup-transform-left-close);
}
}
&-center {
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0) scale(0.6);
opacity: 0;
.animation-center();
border-radius: @popup-radius;
background: none;
&-close {
transform: translate3d(-50%, -50%, 0) scale(1);
opacity: 1;
.animation-center-close();
}
}
}
&-position {
&-top {
left: 0;
right: 0;
top: -100%;
.animation-transform(ant-popup-position-top);
border-radius: 0 0 @popup-radius @popup-radius;
&-close {
top: 0;
.animation-transform(ant-popup-position-top-close);
}
}
&-right {
top: 0;
bottom: 0;
right: -100%;
.animation-transform(ant-popup-position-right);
&-close {
right: 0;
.animation-transform(ant-popup-position-right-close);
}
}
&-bottom {
left: 0;
right: 0;
bottom: -100%;
.animation-transform(ant-popup-position-bottom);
border-radius: @popup-radius @popup-radius 0 0;
&-close {
bottom: 0;
.animation-transform(ant-popup-position-bottom-close);
}
}
&-left {
top: 0;
bottom: 0;
left: -100%;
.animation-transform(ant-popup-position-left);
&-close {
left: 0;
.animation-transform(ant-popup-position-left-close);
}
}
&-center {
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0) scale(0.6);
opacity: 0;
.animation-position-center();
border-radius: @popup-radius;
background: none;
&-close {
transform: translate3d(-50%, -50%, 0) scale(1);
opacity: 1;
.animation-position-center-close();
}
}
}
}
@keyframes ant-popup-transform-top {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
@keyframes ant-popup-transform-bottom {
0% {
transform: translateY(100%);
}
100% {
transform: translateY(0);
}
}
@keyframes ant-popup-transform-left {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(0);
}
}
@keyframes ant-popup-transform-right {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(0);
}
}
@keyframes ant-popup-transform-center-scale {
0% {
transform: translate3d(-50%, -50%, 0) scale(0.6);
}
100% {
transform: translate3d(-50%, -50%, 0) scale(1);
}
}
@keyframes ant-popup-transform-center-opacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes ant-popup-transform-top-close {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-100%);
}
}
@keyframes ant-popup-transform-bottom-close {
0% {
transform: translateY(0);
}
100% {
transform: translateY(100%);
}
}
@keyframes ant-popup-transform-left-close {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-100%);
}
}
@keyframes ant-popup-transform-right-close {
0% {
transform: translateX(0);
}
100% {
transform: translateX(100%);
}
}
@keyframes ant-popup-transform-center-close-scale {
0% {
transform: translate3d(-50%, -50%, 0) scale(1);
}
100% {
transform: translate3d(-50%, -50%, 0) scale(0.6);
}
}
@keyframes ant-popup-transform-center-close-opacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes ant-popup-position-top {
0% {
top: -100%;
}
100% {
top: 0;
}
}
@keyframes ant-popup-position-bottom {
0% {
bottom: -100%;
}
100% {
bottom: 0;
}
}
@keyframes ant-popup-position-left {
0% {
left: -100%;
}
100% {
left: 0;
}
}
@keyframes ant-popup-position-right {
0% {
right: -100%;
}
100% {
right: 0;
}
}
@keyframes ant-popup-position-center-scale {
0% {
transform: translate3d(-50%, -50%, 0) scale(0.6);
}
100% {
transform: translate3d(-50%, -50%, 0) scale(1);
}
}
@keyframes ant-popup-position-center-opacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes ant-popup-position-top-close {
0% {
top: 0;
}
100% {
top: -100%;
}
}
@keyframes ant-popup-position-bottom-close {
0% {
bottom: 0;
}
100% {
bottom: -100%;
}
}
@keyframes ant-popup-position-left-close {
0% {
left: 0;
}
100% {
left: -100%;
}
}
@keyframes ant-popup-position-right-close {
0% {
right: 0;
}
100% {
right: -100%;
}
}
@keyframes ant-popup-position-center-close-scale {
0% {
transform: translate3d(-50%, -50%, 0) scale(1);
}
100% {
transform: translate3d(-50%, -50%, 0) scale(0.6);
}
}
@keyframes ant-popup-position-center-close-opacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes ant-popup-mask-appear {
0% {
background-color: @popup-mask-close-bg;
}
100% {
background-color: @popup-mask-product;
}
}
@keyframes ant-popup-mask-close {
0% {
background-color: @popup-mask-product;
}
100% {
background-color: @popup-mask-close-bg;
}
}
================================================
FILE: src/Popup/index.md
================================================
---
nav:
path: /components
group:
title: 反馈引导
order: 12
toc: 'content'
---
# Popup 弹出层
从屏幕滑出或弹出一块自定义内容区。用于展示弹窗、信息提示、选择输入、切换等内容,支持多个弹出层叠加展示。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-popup": "antd-mini/es/Popup/index"
#endif
#if WECHAT
"ant-popup": "antd-mini/Popup/index"
#endif
}
```
## 代码示例
### 基本使用
> - `ant-popup` 组件必须通过 `visible` 属性控制显示/隐藏状态。`position` 指定出现方向,可选 `top` `bottom` `left` `right`。弹窗中的内容通过插槽的形式填入。
> - `close` 事件会在点击右上角关闭 icon 或点击蒙层关闭时触发。
> - `clickBack` 事件会在点击左上角返回按钮时触发。
```xml
从屏幕滑出或弹出一块自定义内容区,用于展示弹窗、信息提示、选择输入、切换等内容。
```
```js
Page({
data: {
visible: true,
},
onClickBack() {
#if ALIPAY
my.showToast({ content: 'clicked icon', duration: 1000 });
#endif
#if WECHAT
wx.showToast({ content: 'clicked icon', duration: 1000 });
#endif
},
handlePopupClose() {
this.setData({
visible: false,
});
},
});
```
### 设置背景图
> `backgroundImage` 属性,可以为整个弹层设置背景。
```xml
Ea consectetur ipsum consequat exercitation laboris excepteur pariatur
excepteur labore dolor cillum tempor esse. Ad adipisicing nostrud fugiat eu
mollit. Proident nisi voluptate sunt ea laboris Lorem ullamco deserunt aute
incididunt cillum tempor duis est. Elit voluptate laboris laborum anim
```
### 超长内容滚动
> 如果弹窗内内容过长需要支持滚动,请使用 [scroll-view](https://opendocs.alipay.com/mini/component/scroll-view?pathHash=8292073d) 组件,并添加以下属性:
```xml
Ea consectetur ipsum consequat exercitation laboris excepteur pariatur
excepteur labore dolor cillum tempor esse. Ad adipisicing nostrud fugiat eu
mollit. Proident nisi voluptate sunt ea laboris Lorem ullamco deserunt aute
incididunt cillum tempor duis est. Elit voluptate laboris laborum anim
exercitation consequat laboris ad. Quis ad enim fugiat.
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ----------- |
| animation | 是否开启过渡动画 | boolean | true |
| animationType | 动画类型,可选`transform` `position`,默认使用`transform`动画性能更好。但当弹窗内部存在 fixed 定位或者 picker-view 元素时可能存在样式问题,可切换为`position`解决 | string | `transform` |
| className | 类名 | string | - |
| title | 标题 | string | '' |
| destroyOnClose | 不可见时卸载内容 | boolean | false |
| duration | 过渡动画时长,单位毫秒 | number | - |
| height | 高度,在 position 为 `top` 或 `bottom` 时使用,单位 px。可选,不传时根据内容区高度自适应。 | number | - |
| maskClassName | 蒙层的类名 | string | - |
| maskStyle | 蒙层的样式 | string | - |
| position | 弹窗布局,可选`top` `bottom` `left` `right` | string | `bottom` |
| showMask | 是否展示蒙层 | boolean | true |
| style | 样式 | string | - |
| visible | 是否显示 | boolean | false |
| width | 宽度, 在 position 为 `left` 或 `right` 时使用,单位 px | number | - |
| zIndex | 弹窗层级 | number | 998 |
| backgroundImage | 弹框的背景图 | string | - |
| showClose | 展示弹框关闭的 icon | boolean | false |
| showBack | 展示弹框返回的 icon | boolean | false |
| #if ALIPAY onClose | 点击蒙层关闭,触发回调 | () => void | - |
| #if ALIPAY onClickBack | 点击返回按钮,触发回调 | () => void | - |
| #if ALIPAY onAfterShow | 完全展示后触发 | () => void | - |
| #if ALIPAY onAfterClose | 完全关闭后触发 | () => void | - |
| #if WECHAT bindclose | 点击蒙层关闭,触发回调 | () => void | - |
| #if WECHAT bindclickbackicon | 点击返回按钮,触发回调 | () => void | - |
| #if WECHAT bindaftershow | 完全展示后触发 | () => void | - |
| #if WECHAT bindafterclose | 完全关闭后触发 | () => void | - |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 默认值 | 深色模式默认值 | 备注 |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| --popover-list-content-bg | rgba(0, 0, 0, 0.93)
| rgba(0, 0, 0, 0.93)
| Popover 列表内容背景颜色 |
| --popover-list-content-color | #ffffff
| #c5cad1
| Popover 列表内容颜色 |
| --popover-list-badge-color | #ff411c
| #ff411c
| Popover 列表徽章颜色 |
| --popover-list-content-color | #333333
| #c5cad1
| Popover 列表内容文字颜色 |
## FAQ
### Popup 打开后,蒙层后面的页面能滚动怎么办?
阻止蒙层后页面滚动目前在 IDE 和模拟器不生效,请在真机上调试。
### Popup 内部元素需要支持滚动怎么处理?
如果弹窗内需要滚动,请使用 scroll-view 组件,并添加以下属性:
```html
...你的内容...
```
### Popup 内部的 picker-view 显示异常怎么解决?
Popup 默认是通过 `display:none` 隐藏的,而 picker-view 不能放到 `display:none` 的组件里。有以下两种解决方式:
1. 在 picker-view 上添加属性 `a:if="{{popupVisible}}"`,在 Popup 显示时再显示 picker-view。
2. 在 Popup 上设置 `destroyOnClose="{{true}}"`,在 Popup 不可见时卸载内容。
================================================
FILE: src/Popup/index.sjs.ts
================================================
function getContentStyle(position, animation, duration, width, height) {
let style = '';
if (animation) {
if (duration) {
style += `-webkit-animation-duration:${duration}ms; animation-duration:${duration}ms;`;
}
} else {
style += `-webkit-animation-duration:0ms; animation-duration:0ms; animation-delay:0ms;`;
}
if (position === 'top' || position === 'bottom') {
if (typeof height !== 'undefined' && height !== null) {
style += `height:${height}px`;
}
}
if (position === 'left' || position === 'right') {
if (typeof width !== 'undefined' && width !== null) {
style += `width:${width}px`;
}
}
return style;
}
function getCloseStyle(animation, duration, maskStyle) {
let style = '';
if (animation) {
if (duration) {
style += `-webkit-animation-duration:${duration}ms; animation-duration:${duration}ms;`;
}
} else {
style += `-webkit-animation-duration:0ms; animation-duration:0ms; animation-delay:0ms;`;
}
if (maskStyle) {
style += maskStyle;
}
return style;
}
export default { getContentStyle, getCloseStyle };
================================================
FILE: src/Popup/index.ts
================================================
import { isOldSDKVersion } from '../_util/platform';
import {
Component,
getValueFromProps,
triggerEventOnly,
} from '../_util/simply';
import { PopupDefaultProps } from './props';
const isOldVersion = isOldSDKVersion();
Component({
props: PopupDefaultProps,
data: {
closing: false,
isOldVersion,
},
methods: {
onClickCloseIcon() {
const { closing } = this.data;
if (closing) {
return;
}
triggerEventOnly(this, 'close');
},
onClickBack() {
triggerEventOnly(this, 'clickBack');
},
onTapMask() {
const { closing } = this.data;
if (closing) {
return;
}
triggerEventOnly(this, 'close');
},
onAnimationEnd() {
const { closing } = this.data;
if (closing) {
this.setData({ closing: false });
}
const [visible, duration, animation] = getValueFromProps(this, [
'visible',
'duration',
'animation',
]);
const enableAnimation =
animation && (duration > 0 || typeof duration !== 'number');
if (enableAnimation) {
triggerEventOnly(this, visible ? 'afterShow' : 'afterClose');
}
},
},
/// #if ALIPAY
async deriveDataFromProps(nextProps) {
const [visible, duration, animation] = getValueFromProps(this, [
'visible',
'duration',
'animation',
]);
const enableAnimation =
animation && (duration > 0 || typeof duration !== 'number');
if (
Boolean(nextProps.visible) !== Boolean(visible) &&
enableAnimation &&
!nextProps.visible &&
!this.data.closing
) {
this.setData({ closing: true });
}
},
didUpdate(prevProps) {
const [visible, duration, animation] = getValueFromProps(this, [
'visible',
'duration',
'animation',
]);
const enableAnimation =
animation && (duration > 0 || typeof duration !== 'number');
if (prevProps.visible !== visible && !enableAnimation) {
triggerEventOnly(this, visible ? 'afterShow' : 'afterClose');
}
},
/// #endif
/// #if WECHAT
observers: {
'**': function (data) {
const prevData = this._prevData || this.data;
this._prevData = { ...data };
const { visible, duration, animation, closing } = data;
const enableAnimation =
animation && (duration > 0 || typeof duration !== 'number');
if (
enableAnimation &&
prevData.visible !== data.visible &&
!visible &&
!closing
) {
this.setData({ closing: true });
}
if (prevData.visible !== data.visible && !enableAnimation) {
triggerEventOnly(this, visible ? 'afterShow' : 'afterClose');
}
},
},
/// #endif
});
================================================
FILE: src/Popup/props.ts
================================================
import { IBaseProps } from '../_util/base';
/**
* @description 弹窗,可在其中加入具体内容,展示更多信息供用户使用。
*/
export interface IPopupProps extends IBaseProps {
/**
* @description 是否显示
* @default false
*/
visible: boolean;
/**
* @description 标题
* @default ''
*/
title?: string;
/**
* @description 是否关闭后销毁内部元素
* @default false
*/
destroyOnClose: boolean;
/**
* @description 是否展示蒙层
* @default true
*/
showMask: boolean;
/**
* @description 弹窗布局
* @default "bottom"
*/
position: 'center' | 'top' | 'bottom' | 'left' | 'right';
/**
* @description 是否开启过渡动画
*/
animation: boolean;
/**
* @description 动画类型
* @default "transform"
*/
animationType: 'transform' | 'position';
/**
* @description 过渡动画时长,单位毫秒
*/
duration?: number;
/**
* @description 内容区高度,单位px
*/
height: number;
/**
* @description 内容区宽度,单位px
*/
width: number;
/**
* @description 遮罩层类名
*/
maskClassName: string;
/**
* @description 遮罩层样式
*/
maskStyle: string;
zIndex: number;
/**
* @description 展示弹框关闭的icon
* @default false
*/
showClose: boolean;
/**
* @description 展示弹框返回的icon
* @default false
*/
showBack: boolean;
/**
* @description 弹框的背景图
*/
backgroundImage: string;
/**
* @description 关闭时回调
*/
onClose?: () => void;
/**
* @description 完全打开时回调
*/
onAfterShow?: () => void;
/**
* @description 完全关闭时回调
*/
onAfterClose?: () => void;
/**
* @description 点击关闭icon时回调
*/
onClickBack?: () => void;
}
export const PopupDefaultProps: Partial = {
visible: false,
title: '',
destroyOnClose: false,
backgroundImage: '',
showMask: true,
showClose: false,
showBack: false,
position: 'bottom',
// 是否开启动画
animation: true,
animationType: 'transform',
// 动画持续时间
duration: null,
height: null,
width: null,
maskClassName: '',
maskStyle: '',
// 弹窗层级
zIndex: 998,
};
================================================
FILE: src/Popup/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
// popup 背景色
@popup-background: var(--popup-background, @COLOR_CARD);
// 产品蒙层
@popup-mask-product: @color-product-mask;
// popup 圆角
@popup-radius: 48 * @rpx;
@popup-color: var(--popup-color, @COLOR_TEXT_PRIMARY);
@popup-assit-color: var(--popup-assit-color, @COLOR_TEXT_ASSIST);
@popup-mask-close-bg: var(--popup-mask-close-bg, @COLOR_BLACK_FADED);
.mask-appear(@duration: 400ms) {
animation-fill-mode: forwards;
animation-duration: @duration;
animation-timing-function: cubic-bezier(0.35, 0, 0.65, 1);
animation-name: ant-popup-mask-appear;
}
.mask-close(@duration: 400ms) {
animation-delay: 100ms;
animation-fill-mode: forwards;
animation-duration: @duration;
animation-timing-function: cubic-bezier(0.35, 0, 0.65, 1);
animation-name: ant-popup-mask-close;
}
.animation-transform(@name, @duration: 300ms) {
animation-fill-mode: forwards;
animation-name: @name;
animation-delay: 100ms;
animation-duration: @duration;
animation-timing-function: cubic-bezier(0, 0.4, 0, 1);
}
.animation-transform-close(@name, @duration: 300ms) {
animation-fill-mode: forwards;
animation-name: @name;
animation-duration: @duration;
animation-timing-function: cubic-bezier(0.6, 0, 1, 0.6);
}
.animation-center() {
animation-fill-mode: forwards;
animation-name: ant-popup-transform-center-scale,
ant-popup-transform-center-opacity;
animation-delay: 100ms, 100ms;
animation-duration: 300ms, 200ms;
animation-timing-function: cubic-bezier(0.57, -0.22, 0, 1.2),
cubic-bezier(0.35, 0, 0.65, 1);
}
.animation-center-close() {
animation-fill-mode: forwards;
animation-name: ant-popup-transform-center-close-scale,
ant-popup-transform-center-close-opacity;
animation-duration: 300ms, 200ms;
animation-timing-function: cubic-bezier(0.6, 0, 1, 0.6),
cubic-bezier(0.35, 0, 0.65, 1);
}
.animation-position-center() {
animation-fill-mode: forwards;
animation-name: ant-popup-position-center-scale,
ant-popup-position-center-opacity;
animation-delay: 100ms, 100ms;
animation-duration: 300ms, 200ms;
animation-timing-function: cubic-bezier(0.57, -0.22, 0, 1.2),
cubic-bezier(0.35, 0, 0.65, 1);
}
.animation-position-center-close() {
animation-fill-mode: forwards;
animation-name: ant-popup-position-center-close-scale,
ant-popup-position-center-close-opacity;
animation-duration: 300ms, 200ms;
animation-timing-function: cubic-bezier(0.6, 0, 1, 0.6),
cubic-bezier(0.35, 0, 0.65, 1);
}
================================================
FILE: src/Postscript/index.axml
================================================
{{title}}
{{item}}
================================================
FILE: src/Postscript/index.en.md
================================================
---
nav:
path: /components
group:
title: Bizness Components
order: 15
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# Postscript
Use when additional notes need to be added below the form or content.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-postscript": "antd-mini/es/Postscript/index"
#endif
#if WECHAT
"ant-postscript": "antd-mini/Postscript/index"
#endif
}
```
## Code Sample
### Basic Usage
## API
### Property
| Property | Description | Type | Default Value |
| --------------------- | ---------------- | ----------------------- | ----------------- |
| className | Class Name | string | - |
| style | Style | string | - |
| title | Title | string | - |
| maxLength | Maximum input length | number | Infinity |
| quickInputs | Quick Input Options | string[] | [] |
| placeholder | Placeholder text | string | "Bring a word to TA ~' |
| placeholderClassName | Placeholder class name | string | - |
| combineSymbol | Quick Input Connection Symbol | string | - |
| #if ALIPAY onChange | Callback when content changes | (value: string) => void | - |
| #if WECHAT bindchange | Callback when content changes | (value: string) => void | - |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Light Mode Default | Dark Mode Default | Remarks |
| ------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------ |
| --postscript-background-color | #ffffff
| #1a1a1a
| Background Color |
| --postscript-title-color | #333
| #c5cad1
| Title Color |
| --postscript-input-color | #333
| #c5cad1
| Enter text color |
| --postscript-caret-color | #1677ff
| #3086ff
| caret color |
| --postscript-placeholder-color | #cccccc
| #474747
| Placeholder color |
| --postscript-quick-text-color | #666
| #808080
| Shortcut Text Color |
| --postscript-quick-border-color | #eeeeee
| #2b2b2b
| Quick border color |
## FAQ
### How do I use the shortcut input stitching function?
Set the combineSymbol attribute to the required connection symbol (such as "" or space), and click the shortcut input option to automatically splice with the existing content.
If you need the shortcut input content to overwrite the existing content, remove the combineSymbol attribute.
================================================
FILE: src/Postscript/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-input": "../Input/index"
}
}
================================================
FILE: src/Postscript/index.less
================================================
@import (reference) './variable.less';
@import (reference) '../style/mixins/hairline.less';
@prefixCls: ant-postscript;
.@{prefixCls} {
background: @postscript-background-color;
padding: @v-spacing-standard;
&-title {
font-size: @font-size-subtitle;
color: @postscript-title-color;
margin-bottom: @v-spacing-standard;
}
&-input {
position: relative;
margin-bottom: @v-spacing-standard;
padding: @v-spacing-standard 0;
.hairline('bottom', @postscript-quick-border-color);
.ant-input-content {
font-size: @font-size-subtitle;
color: @postscript-input-color;
caret-color: @postscript-caret-color;
}
&-placeholder {
font-size: @font-size-content;
color: @postscript-placeholder-color;
}
}
&-quick {
display: flex;
.ant-postscript-quick-item:last-child {
margin-right: 0;
}
&-item {
margin-right: @h-spacing-mini;
flex-shrink: 0;
padding: @h-spacing-small @h-spacing-standard;
border-radius: @corner-radius-circle;
font-size: @font-size-content;
color: @postscript-quick-text-color;
/// #if WECHAT
height: fit-content;
/// #endif
.hairline-radius(@postscript-quick-border-color, @corner-radius-circle);
&:active {
opacity: 0.7;
}
}
}
}
================================================
FILE: src/Postscript/index.md
================================================
---
nav:
path: /components
group:
title: 业务组件
order: 15
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# Postscript 资金附言组件
需要在表单或内容下方添加补充说明时使用。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-postscript": "antd-mini/es/Postscript/index"
#endif
#if WECHAT
"ant-postscript": "antd-mini/Postscript/index"
#endif
}
```
## 代码示例
### 基础用法
## API
### 属性
| 属性 | 说明 | 类型 | 默认值 |
| --------------------- | ---------------- | ----------------------- | ----------------- |
| className | 类名 | string | - |
| style | 样式 | string | - |
| title | 标题 | string | - |
| maxLength | 最大输入长度 | number | Infinity |
| quickInputs | 快捷输入选项 | string[] | [] |
| placeholder | 占位符文本 | string | '给 TA 带句话吧~' |
| placeholderClassName | 占位符类名 | string | - |
| combineSymbol | 快捷输入连接符号 | string | - |
| #if ALIPAY onChange | 内容变化时的回调 | (value: string) => void | - |
| #if WECHAT bindchange | 内容变化时的回调 | (value: string) => void | - |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 浅色模式默认值 | 深色模式默认值 | 备注 |
| ------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------ |
| --postscript-background-color | #ffffff
| #1a1a1a
| 背景颜色 |
| --postscript-title-color | #333
| #c5cad1
| 标题颜色 |
| --postscript-input-color | #333
| #c5cad1
| 输入文字颜色 |
| --postscript-caret-color | #1677ff
| #3086ff
| 插入符颜色 |
| --postscript-placeholder-color | #cccccc
| #474747
| 占位符颜色 |
| --postscript-quick-text-color | #666
| #808080
| 快捷文本颜色 |
| --postscript-quick-border-color | #eeeeee
| #2b2b2b
| 快捷边框颜色 |
## FAQ
### 如何使用快捷输入拼接功能?
设置 combineSymbol 属性为需要的连接符号(如 "+" 或空格),点击快捷输入选项时会自动与已有内容拼接。
如果需要快捷输入内容覆盖已有内容,请移除 combineSymbol 属性。
================================================
FILE: src/Postscript/index.ts
================================================
import { Component, getValueFromProps, triggerEvent } from '../_util/simply';
import { assertAilpayNativeNotSupport } from '../_util/support';
import { PostscriptProps } from './props';
assertAilpayNativeNotSupport('Postscript');
Component({
props: PostscriptProps,
data: {
content: '',
},
methods: {
checkMaxLength(value) {
const maxLength = Number(getValueFromProps(this, 'maxLength') || -1);
if (maxLength !== -1 && value.length > maxLength) {
return value.slice(0, maxLength);
}
return value;
},
handleInput(val) {
let value = val;
/// #if WECHAT
value = val.detail;
/// #endif
this.setData({ content: value });
triggerEvent(this, 'change', value);
},
handleQuickInput(e) {
const { value } = e.currentTarget.dataset;
const combineSymbol = getValueFromProps(this, 'combineSymbol');
const result = this.checkMaxLength(
combineSymbol
? `${
this.data.content ? `${this.data.content}${combineSymbol}` : ''
}${value}`
: value
);
this.setData({ content: result });
triggerEvent(this, 'change', result);
},
},
});
================================================
FILE: src/Postscript/props.ts
================================================
import { IBaseProps } from '../_util/base';
interface PostscriptProps extends IBaseProps {
/**
* @description 标题
*/
title: string,
/**
* @description 最大输入长度
*/
maxLength: number,
/**
* @description 快捷输入选项
*/
quickInputs: string[],
/**
* @description 占位符文本
*/
placeholder: string,
/**
* @description 占位符类名
*/
placeholderClassName: string,
/**
* @description 组合符号,存在时快捷输入会通过组合符号拼接
*/
combineSymbol: string,
/**
* @description 内容变化时的回调
*/
onChange: (value: string) => void,
}
export const PostscriptProps: PostscriptProps = {
title: '',
maxLength: -1,
quickInputs: [],
placeholder: '给TA带句话吧~',
placeholderClassName: '',
combineSymbol: '',
onChange: () => {},
}
================================================
FILE: src/Postscript/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
// 背景色
@postscript-background-color: var(--postscript-background-color, @COLOR_CARD);
// 标题文字颜色
@postscript-title-color: var(--postscript-title-color, @COLOR_TEXT_PRIMARY);
// 输入框文字颜色
@postscript-input-color: var(--postscript-input-color, @COLOR_TEXT_PRIMARY);
// 输入框光标颜色
@postscript-caret-color: var(--postscript-caret-color, @COLOR_BRAND1);
// 占位符文字颜色
@postscript-placeholder-color: var(--postscript-placeholder-color, @COLOR_TEXT_WEAK);
// 快捷输入项文字颜色
@postscript-quick-text-color: var(--postscript-quick-text-color, @COLOR_TEXT_SECONDARY);
// 快捷输入项边框颜色
@postscript-quick-border-color: var(--postscript-quick-border-color, @COLOR_BORDER);
================================================
FILE: src/Progress/index.axml
================================================
{{ percent }}%
================================================
FILE: src/Progress/index.en.md
================================================
---
nav:
path: /components
group:
title: Feedback
order: 12
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# Progress
Displays the progress of user operations and tasks.
## Introduction
In `index.json` Introducing Components in
```json
"usingComponents": {
#if ALIPAY
"ant-progress": "antd-mini/es/Progress/index"
#endif
#if WECHAT
"ant-progress": "antd-mini/Progress/index"
#endif
}
```
## Code Sample
### Basic use
> The progress bar defaults to a blue bar,`percent` Property to set the current progress. Use `type`Property to set the shape, currently supports bar and ring two forms.
```xml
```
### Semantic progress bar
> In line mode, semantic status bars for success and failure states are supported by setting `status`Property implementation.
```xml
```
### Style Customization
> `strokeWidth` You can set the thickness of the progress bar,`strokeColor` You can set the color of the progress bar,`trailColor` You can set the track color,`style` The style can be highly customized.
```xml
```
### Demo Code
## API
| Property | Description | Type | Default Value |
| ----------- | ---------------------------------------------------- | ------- | ------ |
| className | Class Name | string | - |
| percent | Percentage | number | 0 |
| status | status, available in line mode only, optional `success` `exception` | string | - |
| strokeColor | Progress bar color | string | - |
| strokeWidth | Width of progress bar, in px | number | 8 |
| style | Style | string | - |
| trailColor | Track Color | string | - |
| type | type, optional `line` `circle` | string | `line` |
| width | Circular progress bar canvas width, in px | number | 100 |
| animation | Whether to turn on transition animation | boolean | true |
### Theme customization
#### Style Variables
Component provides the following CSS variables, which can be used to customize styles. For details, see ConfigProvider Components.
| Variable name | Light Mode Default | Dark Mode Default | Remarks |
| -------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------- |
| --progress-stroke-color | #1677ff
| #3086ff
| Progress bar main color |
| --progress-trail-color | #f5f5f5
| #121212
| Progress bar track color |
| --progress-success-color | #22b35e
| #34b368
| Progress bar success color |
| --progress-indicator-color | #333333
| #c5cad1
| Progress bar indicator color |
| --progress-exception-color | #ff3141
| #ff4a58
| Progress bar exception color |
| --progress-assist-color | #999999
| #616161
| Progress bar auxiliary color |
================================================
FILE: src/Progress/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"ant-icon": "../Icon/index"
}
}
================================================
FILE: src/Progress/index.less
================================================
@import (reference) './variable.less';
.ant-progress {
&-line {
display: flex;
flex-direction: row;
align-items: center;
.ant-progress-outer {
background-color: @progress-trail-color;
border-radius: @corner-radius-circle;
height: 16 * @rpx;
overflow: hidden;
flex: 1;
}
.ant-progress-inner {
border-radius: @corner-radius-circle;
transition: width 0.3s;
height: 100%;
position: relative;
background-color: @progress-stroke-color;
&-success {
background-color: @progress-success-color;
}
&-exception {
background-color: @progress-exception-color;
}
}
.ant-progress-indicator {
margin-left: 16 * @rpx;
color: @progress-assist-color;
font-size: 26 * @rpx;
height: 37 * @rpx;
display: flex;
align-items: center;
}
.ant-progress-status-icon {
&-success {
color: @progress-success-color;
}
&-exception {
color: @progress-exception-color;
}
}
}
&-circle {
position: relative;
> .ant-progress-canvas {
width: 100%;
height: 100%;
}
.ant-progress-indicator {
font-size: 40 * @rpx;
color: @progress-indicator-color;
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
z-index: 10;
}
}
}
================================================
FILE: src/Progress/index.md
================================================
---
nav:
path: /components
group:
title: 反馈引导
order: 12
toc: 'content'
supportPlatform: ['alipay', 'wechat']
---
# Progress 进度条
用于展示用户操作、任务的进度。
## 引入
在 `index.json` 中引入组件
```json
"usingComponents": {
#if ALIPAY
"ant-progress": "antd-mini/es/Progress/index"
#endif
#if WECHAT
"ant-progress": "antd-mini/Progress/index"
#endif
}
```
## 代码示例
### 基本使用
> 进度条默认为蓝色条形,`percent` 属性来设置当前进度。使用 `type`属性来设置形状,目前支持条形和环形两种形式。
```xml
```
### 语义化进度条
> line 模式下,支持成功、失败状态的语义化状态条,通过设置 `status`属性实现。
```xml
```
### 样式定制
> `strokeWidth` 可以设置进度条的粗细,`strokeColor` 可以设置进度条的颜色,`trailColor` 可以设置轨道颜色,`style` 可以高度定制样式。
```xml
```
### Demo 代码
## API
| 属性 | 说明 | 类型 | 默认值 |
| ----------- | ---------------------------------------------------- | ------- | ------ |
| className | 类名 | string | - |
| percent | 百分比 | number | 0 |
| status | 状态,仅限 line 模式可用,可选 `success` `exception` | string | - |
| strokeColor | 进度条颜色 | string | - |
| strokeWidth | 进度条宽度,单位 px | number | 8 |
| style | 样式 | string | - |
| trailColor | 轨道颜色 | string | - |
| type | 类型,可选 `line` `circle` | string | `line` |
| width | 圆形进度条画布宽度,单位 px | number | 100 |
| animation | 是否开启过渡动画 | boolean | true |
### 主题定制
#### 样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 变量名 | 浅色模式默认值 | 深色模式默认值 | 备注 |
| -------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------- |
| --progress-stroke-color | #1677ff
| #3086ff
| 进度条主颜色 |
| --progress-trail-color | #f5f5f5
| #121212
| 进度条轨道颜色 |
| --progress-success-color | #22b35e
| #34b368
| 进度条成功颜色 |
| --progress-indicator-color | #333333
| #c5cad1
| 进度条指示器颜色 |
| --progress-exception-color | #ff3141
| #ff4a58
| 进度条异常颜色 |
| --progress-assist-color | #999999
| #616161
| 进度条辅助颜色 |
================================================
FILE: src/Progress/index.ts
================================================
import deepEqual from 'fast-deep-equal';
import { createCanvasContext } from '../_util/jsapi/create-canvas-context';
import { getSystemInfo } from '../_util/jsapi/get-system-info';
import { Component, getValueFromProps } from '../_util/simply';
import { assertAilpayNativeNotSupport } from '../_util/support';
import { ProgressBarDefaultProps } from './props';
assertAilpayNativeNotSupport('Progress');
const animationFrameDuration = 16;
Component({
props: ProgressBarDefaultProps,
data: {
curProgress: 0,
canvasWidth: 100,
},
methods: {
requestAnimationFrame(fn, duration = animationFrameDuration) {
setTimeout(fn, duration);
},
getDrawColor() {
const [strokeColor, trailColor] = getValueFromProps(this, [
'strokeColor',
'trailColor',
]);
const drawColor = {
strokeColor: strokeColor || '#1677ff',
trailColor: trailColor || '#F5F5F5',
};
return drawColor;
},
async getCanvasContext() {
if (this.ctx) return;
const systemInfo = await getSystemInfo();
let { pixelRatio } = systemInfo;
const width = getValueFromProps(this, 'width');
this.ctx = await createCanvasContext([this.canvasId, this]);
this.ctx.imageSmoothingEnabled = true;
this.ctx.imageSmoothingQuality = 'high';
/// #if WECHAT
// 微信小程序不支持 CanvasWidth 参数,此时 pixelRatio 默认为 1
pixelRatio = 1;
/// #endif
this.setData({
canvasWidth: width * pixelRatio,
});
},
clearCanvas() {
const ctx = this.ctx;
const { canvasWidth } = this.data;
ctx.clearRect(0, 0, canvasWidth, canvasWidth);
},
async updateCanvasProgress(prev) {
const drawColor = this.getDrawColor();
await this.getCanvasContext();
let curRad = Math.floor((prev / 100) * 360);
const targetRad = Math.floor((this.data.curProgress / 100) * 360);
const direction = curRad < targetRad ? 1 : -1;
const draw = () => {
if (curRad == targetRad) return;
const [speed, animation] = getValueFromProps(this, [
'speed',
'animation',
]);
curRad = direction * speed + curRad;
if (direction == -1) {
curRad = Math.max(curRad, targetRad);
} else {
curRad = Math.min(curRad, targetRad);
}
this.clearCanvas();
this.drawOrbit(drawColor.trailColor);
this.drawProgress(drawColor.strokeColor, curRad);
this.ctx.draw(true);
this.requestAnimationFrame(
draw,
animation ? animationFrameDuration : 0
);
};
draw();
},
drawProgress(color, rad) {
const ctx = this.ctx;
const { canvasWidth } = this.data;
const strokeWidth = Number(getValueFromProps(this, 'strokeWidth'));
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = strokeWidth;
ctx.setLineCap('round');
ctx.arc(
canvasWidth / 2,
canvasWidth / 2,
canvasWidth / 2 - strokeWidth,
-Math.PI / 2,
-Math.PI / 2 + (rad / 360) * 2 * Math.PI,
false
);
ctx.stroke();
},
drawOrbit(color) {
const ctx = this.ctx;
const { canvasWidth } = this.data;
const strokeWidth = Number(getValueFromProps(this, 'strokeWidth'));
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = strokeWidth;
ctx.arc(
canvasWidth / 2,
canvasWidth / 2,
canvasWidth / 2 - strokeWidth,
0,
Math.PI * 2,
false
);
ctx.stroke();
},
calProgress() {
const [p, type] = getValueFromProps(this, ['percent', 'type']);
const percent = +p;
if (isNaN(percent)) {
return this.setData({ curProgress: 0 });
}
const prevProgress = this.data.curProgress;
if (percent === prevProgress) {
return;
}
this.setData({
curProgress: percent > 100 ? 100 : percent < 0 ? 0 : percent,
});
if (type === 'circle') {
this.setCanvasId();
this.updateCanvasProgress(prevProgress);
}
},
setCanvasId() {
this.canvasId = this.$id
? `ant-progress-canvas-${this.$id}`
: `ant-progress-canvas`;
},
},
ctx: null as any,
drawColor: null as any,
canvas: null,
/// #if ALIPAY
didMount() {
this.calProgress();
},
didUpdate(prevProps) {
if (deepEqual(this.props, prevProps)) return;
this.calProgress();
},
/// #endif
/// #if WECHAT
attached() {
this.calProgress();
},
observers: {
'**': function () {
this.calProgress();
},
},
/// #endif
});
================================================
FILE: src/Progress/props.ts
================================================
import { IBaseProps } from '../_util/base';
/**
* @description 弹窗,可在其中加入具体内容,展示更多信息供用户使用。
*/
export interface IProgressBarProps extends IBaseProps {
/**
* @description 当前进度,范围 0-100
* @default 0
*/
percent: number | string;
/**
* @description 模式
* @default 'line'
*/
type: 'line' | 'circle';
/**
* @description 圆形进度条画布宽度,单位 px
*/
width: number;
/**
* @description 进度条宽度,单位px
*/
strokeWidth: number | string;
/**
* @description 状态,仅限 line
*/
status: 'success' | 'exception';
/**
* @description 进度条颜色
*/
strokeColor: string;
/**
* @description 轨道颜色
*/
trailColor: string;
/**
* @description 每次绘制进度条推进的角度,默认6deg
* @default 6
*/
speed: number;
/**
* @description 是否开启过渡动画
* @default true
*/
animation: boolean;
}
export const ProgressBarDefaultProps: Partial = {
percent: 0,
type: 'line',
width: 100,
strokeWidth: 8,
status: null,
strokeColor: '',
trailColor: '',
speed: 6,
animation: true,
};
================================================
FILE: src/Progress/variable.less
================================================
@import (reference) '../style/variables.less';
@import (reference) '../style/themes/index.less';
@progress-stroke-color: var(--progress-stroke-color, @COLOR_BRAND1);
@progress-trail-color: var(--progress-trail-color, @COLOR_GREY_CARD);
@progress-success-color: var(--progress-success-color, @COLOR_GREEN);
@progress-indicator-color: var(--progress-indicator-color, @COLOR_TEXT_PRIMARY);
@progress-exception-color: var(--progress-exception-color, @COLOR_RED);
@progress-assist-color: var(--progress-assist-color, @COLOR_TEXT_ASSIST);
================================================
FILE: src/Radio/RadioGroup/index.axml
================================================
{{ item.label }}
{{ item.label }}
================================================
FILE: src/Radio/RadioGroup/index.json
================================================
{
"component": true,
"styleIsolation": "shared",
"usingComponents": {
"list": "../../List/index",
"list-item": "../../List/ListItem/index",
"radio": "../index"
}
}
================================================
FILE: src/Radio/RadioGroup/index.less
================================================
@import (reference) '../variable.less';
@radioGroupPrefix: ant-radio-group;
.@{radioGroupPrefix} {
&-horizontal {
.@{radioGroupPrefix}-body {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
.ant-list-item-line {
padding-right: 0;
}
.ant-radio-item {
flex-flow: 0;
}
.ant-list-item-line::after {
display: none;
}
}
}
&-header:empty,
&-footer:empty {
display: none;
}
&-header,
&-footer {
display: flex;
align-items: center;
padding: @v-spacing-standard @v-spacing-large;
line-height: 1.4;
font-size: 30 * @rpx;
color: @radio-header-color;
}
&-body {
position: relative;
overflow: hidden;
.ant-radio-item-content {
.ant-radio-group-item-label-default:not(:nth-child(1)) {
display: none;
}
}
}
}
================================================
FILE: src/Radio/RadioGroup/index.ts
================================================
import mixinValue from '../../mixins/value';
import { Component, getValueFromProps, triggerEvent } from '../../_util/simply';
import { RadioGroupDefaultProps } from './props';
Component({
props: RadioGroupDefaultProps,
methods: {
onChange(_, e) {
let event;
/// #if ALIPAY
event = e;
/// #endif
/// #if WECHAT
event = _;
/// #endif
const index = event.currentTarget.dataset.index;
const options = getValueFromProps(this, 'options');
const value = options[index].value;
if (!this.isControlled()) {
this.update(value);
}
triggerEvent(this, 'change', value, event);
},
},
mixins: [mixinValue()],
});
================================================
FILE: src/Radio/RadioGroup/props.ts
================================================
import { IBaseProps } from '../../_util/base';
export interface IRadioGroupProps extends IBaseProps {
value: string;
defaultValue: string;
name: string;
disabled?: boolean;
color: string;
position: 'horizontal' | 'vertical';
onChange?: (value: string, e: any) => void;
options: {
label?: string;
value?: string;
disabled?: boolean;
color?: string;
}[];
}
export const RadioGroupDefaultProps: Partial = {
value: null,
defaultValue: null,
name: '',
disabled: false,
color: '',
position: 'vertical',
options: [],
};
================================================
FILE: src/Radio/index.axml
================================================