Repository: ant-design/ant-design-mobile
Pull request #5650 (2022-09-08 08:44:50)

feat(Calendar): add props `minPage` and `maxPage`
- 想要禁止日历的翻页，比如当前的需求限制用户只能看当前月份，无法实现，试过一些方法，详见：https://github.com/ant-design/ant-design-mobile/discussions/5649
- 新增两个 props：prevFarthestDate，nextFarthestDate。分别表示**最前**能看到的日期，和**最后**能看到的日期，该区间为左闭右闭，也即为 [prevFarthestDate, nextFarthestDate]


Patch:
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index e013cc19c..ddcc78f5a 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -1,5 +1,7 @@
 # 参与 Ant Design Mobile 建设
 
+[//]: # 'TODO: 翻译成英文版'
+
 感谢参与 Ant Design Mobile（下文中使用 antd-mobile 指代）的开源建设，目前我们正在进行 v5 版本的开发，这里是一些相关信息的指引。
 
 ## 在开始之前的准备
@@ -42,13 +44,13 @@
 
 如果你希望贡献一些新功能（例如增加一个新组件），那么**请先提交一条 RFC issue**，这样可以让我们的整个沟通过程更加的高效。或许这看起来增加了额外的工作量和时间成本，但是我们相信通过 RFC 中的讨论和确认，可以规避掉很多方向错误导致的问题和浪费。
 
-## 一些开发过程中的注意事项
+## 开发指南
 
 - 请务必使用 pnpm 作为包管理器
 - 推荐使用英文写 commit message
 - 代码中的注释并不是什么要紧的东西，我们更推荐写出自释性更强的代码
 
-## 常见问题
+## 其他问题
 
 ### 我想参与 v2 版本的建设，该如何做？
 
diff --git a/config/components.ts b/config/components.ts
index 460b2c9c4..4e95e34e5 100644
--- a/config/components.ts
+++ b/config/components.ts
@@ -31,6 +31,7 @@ export const components = {
     '/components/swiper',
     '/components/tag',
     '/components/water-mark',
+    '/components/footer',
   ],
   dataEntry: [
     '/components/cascader',
diff --git a/docs/guide/quick-start.en.md b/docs/guide/quick-start.en.md
index 80af360be..d316597f4 100644
--- a/docs/guide/quick-start.en.md
+++ b/docs/guide/quick-start.en.md
@@ -60,3 +60,7 @@ If you don't want to configure your environment locally, you can also try it dir
 
 - [DingDing](https://gw.alipayobjects.com/mdn/rms_25513e/afts/img/A*hBjlR4nUWjkAAAAAAAAAAAAAARQnAQ)
 - [Discord](https://discord.gg/jmNvw4WFYn)
+
+## Contributing
+
+Everyone are welcomed to join the community of antd-mobile. If you want to contribute, please refer to [this guide](https://github.com/ant-design/ant-design-mobile/blob/master/.github/CONTRIBUTING.md).
diff --git a/docs/guide/quick-start.zh.md b/docs/guide/quick-start.zh.md
index d2c5df993..89b9f7691 100644
--- a/docs/guide/quick-start.zh.md
+++ b/docs/guide/quick-start.zh.md
@@ -65,3 +65,7 @@ import { Button } from 'antd-mobile'
 或者也可以加入我们的 Discord 讨论组：
 
 https://discord.gg/jmNvw4WFYn
+
+## 参与贡献
+
+antd-mobile 社区欢迎每一位社区朋友，如果你想参与建设，可以阅读[这篇指南](https://github.com/ant-design/ant-design-mobile/blob/master/.github/CONTRIBUTING.md)。
diff --git a/package.json b/package.json
index b5bb4611c..26b213b7c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "antd-mobile",
-  "version": "5.22.0",
+  "version": "5.23.0",
   "scripts": {
     "start": "dumi dev",
     "build": "gulp",
diff --git a/src/components/avatar/avatar.tsx b/src/components/avatar/avatar.tsx
index 3352121e5..f08f641f7 100644
--- a/src/components/avatar/avatar.tsx
+++ b/src/components/avatar/avatar.tsx
@@ -1,6 +1,6 @@
 import React, { FC, ReactNode } from 'react'
 import { NativeProps, withNativeProps } from '../../utils/native-props'
-import { Image } from '../../index'
+import Image from '../image'
 import { mergeProps } from '../../utils/with-default-props'
 import { Fallback } from './fallback'
 import { ImageProps } from '../image'
diff --git a/src/components/calendar/calendar.en.md b/src/components/calendar/calendar.en.md
index d698c28a2..9e9b639a2 100644
--- a/src/components/calendar/calendar.en.md
+++ b/src/components/calendar/calendar.en.md
@@ -22,8 +22,10 @@ When the user needs to enter a date, he can select it in the pop-up date panel.
 | ----------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
 | allowClear        | Whether to allow clearing after another click.                                  | `boolean`                                                                                                                            | `true`     |
 | defaultValue      | The default selected date or date range.                                        | Same as `value` prop.                                                                                                                | -          |
-| max               | The maximum value of a selectable range.                                        | `Date`                                                                                                                               | -          |
-| min               | Minimum value of selectable range.                                              | `Date`                                                                                                                               | -          |
+| max               | Maximum value of a selectable range.                                            | `Date`                                                                                                                               | -          |
+| min               | Minimum value of a selectable range.                                            | `Date`                                                                                                                               | -          |     | -   |
+| maxPage           | Maximum visible page of date.                                                   | `Page`                                                                                                                               |
+| minPage           | Minimum visible page of date.                                                   | `Page`                                                                                                                               | -          |
 | nextMonthButton   | Contents of the Next Month button on the navigation pane                        | `React.ReactNode`                                                                                                                    | `>`        |
 | nextYearButton    | Contents of the next Year button on the navigation pane                         | `React.ReactNode`                                                                                                                    | `>>`       |
 | onChange          | Trigger when selected date changes.                                             | `(val: Date \| null) => void` when selection mode is "single". `(val: [Date, Date] \| null) => void` when selection mode is "range". | -          |
diff --git a/src/components/calendar/calendar.tsx b/src/components/calendar/calendar.tsx
index db1aba2f4..5963156e4 100644
--- a/src/components/calendar/calendar.tsx
+++ b/src/components/calendar/calendar.tsx
@@ -15,14 +15,17 @@ import { useConfig } from '../config-provider'
 import isoWeek from 'dayjs/plugin/isoWeek'
 import { useUpdateEffect } from 'ahooks'
 import { usePropsValue } from '../../utils/use-props-value'
-import { convertValueToRange, DateRange } from './convert'
+import {
+  convertValueToRange,
+  convertPageToDayjs,
+  DateRange,
+  Page,
+} from './convert'
 
 dayjs.extend(isoWeek)
 
 const classPrefix = 'adm-calendar'
 
-type Page = { month: number; year: number }
-
 export type CalendarRef = {
   jumpTo: (page: Page | ((page: Page) => Page)) => void
   jumpToToday: () => void
@@ -40,6 +43,8 @@ export type CalendarProps = {
   max?: Date
   min?: Date
   shouldDisableDate?: (date: Date) => boolean
+  minPage?: Page
+  maxPage?: Page
 } & (
   | {
       selectionMode?: undefined
@@ -118,23 +123,40 @@ export const Calendar = forwardRef<CalendarRef, CalendarProps>((p, ref) => {
       } else {
         page = pageOrPageGenerator
       }
-      setCurrent(
-        dayjs()
-          .year(page.year)
-          .month(page.month - 1)
-          .date(1)
-      )
+      setCurrent(convertPageToDayjs(page))
     },
     jumpToToday: () => {
       setCurrent(dayjs().date(1))
     },
   }))
+
+  const handlePageChange = (
+    action: 'subtract' | 'add',
+    num: number,
+    type: 'month' | 'year'
+  ) => {
+    const nxtCurrent = current[action](num, type)
+    if (action === 'subtract' && props.minPage) {
+      const minPage = convertPageToDayjs(props.minPage)
+      if (nxtCurrent.isBefore(minPage, type)) {
+        return
+      }
+    }
+    if (action === 'add' && props.maxPage) {
+      const maxPage = convertPageToDayjs(props.maxPage)
+      if (nxtCurrent.isAfter(maxPage, type)) {
+        return
+      }
+    }
+    setCurrent(current[action](num, type))
+  }
+
   const header = (
     <div className={`${classPrefix}-header`}>
       <a
         className={`${classPrefix}-arrow-button ${classPrefix}-arrow-button-year`}
         onClick={() => {
-          setCurrent(current.subtract(1, 'year'))
+          handlePageChange('subtract', 1, 'year')
         }}
       >
         {props.prevYearButton}
@@ -142,7 +164,7 @@ export const Calendar = forwardRef<CalendarRef, CalendarProps>((p, ref) => {
       <a
         className={`${classPrefix}-arrow-button ${classPrefix}-arrow-button-month`}
         onClick={() => {
-          setCurrent(current.subtract(1, 'month'))
+          handlePageChange('subtract', 1, 'month')
         }}
       >
         {props.prevMonthButton}
@@ -160,7 +182,7 @@ export const Calendar = forwardRef<CalendarRef, CalendarProps>((p, ref) => {
           `${classPrefix}-arrow-button-right-month`
         )}
         onClick={() => {
-          setCurrent(current.add(1, 'month'))
+          handlePageChange('add', 1, 'month')
         }}
       >
         {props.nextMonthButton}
@@ -172,7 +194,7 @@ export const Calendar = forwardRef<CalendarRef, CalendarProps>((p, ref) => {
           `${classPrefix}-arrow-button-right-year`
         )}
         onClick={() => {
-          setCurrent(current.add(1, 'year'))
+          handlePageChange('add', 1, 'year')
         }}
       >
         {props.nextYearButton}
diff --git a/src/components/calendar/calendar.zh.md b/src/components/calendar/calendar.zh.md
index ee31bf0a2..abf74aaa9 100644
--- a/src/components/calendar/calendar.zh.md
+++ b/src/components/calendar/calendar.zh.md
@@ -22,6 +22,8 @@
 | defaultValue      | 默认选择的日期                                 | 同 `value` 属性                                                                                | -          |
 | max               | 可选择范围的最大值                             | `Date`                                                                                         | -          |
 | min               | 可选择范围的最小值                             | `Date`                                                                                         | -          |
+| maxPage           | 可切换到的最晚日期                             | `Page`                                                                                         | -          |
+| minPage           | 可切换到的最早日期                             | `Page`                                                                                         | -          |
 | nextMonthButton   | 导航窗格上的“下一月”按钮的内容                 | `React.ReactNode`                                                                              | `>`        |
 | nextYearButton    | 导航窗格上的“下一年”按钮的内容                 | `React.ReactNode`                                                                              | `>>`       |
 | onChange          | 选择日期变化时触发                             | 单选模式下为 `(val: Date \| null) => void`，多选模式下为 `(val: [Date, Date] \| null) => void` | -          |
diff --git a/src/components/calendar/convert.ts b/src/components/calendar/convert.ts
index 2b8adb751..c69226160 100644
--- a/src/components/calendar/convert.ts
+++ b/src/components/calendar/convert.ts
@@ -1,4 +1,6 @@
+import dayjs from 'dayjs'
 export type DateRange = [Date, Date] | null
+export type Page = { month: number; year: number }
 export function convertValueToRange(
   selectionMode: 'single' | 'range' | undefined,
   value: Date | [Date, Date] | null
@@ -14,3 +16,9 @@ export function convertValueToRange(
   }
   return [value, value]
 }
+export function convertPageToDayjs(page: Page) {
+  return dayjs()
+    .year(page.year)
+    .month(page.month - 1)
+    .date(1)
+}
diff --git a/src/components/calendar/demos/demo3.tsx b/src/components/calendar/demos/demo3.tsx
index 2f907aa64..0a82f6ffd 100644
--- a/src/components/calendar/demos/demo3.tsx
+++ b/src/components/calendar/demos/demo3.tsx
@@ -1,7 +1,8 @@
 import dayjs from 'dayjs'
-import React from 'react'
+import React, { useEffect, useRef } from 'react'
 import { Calendar } from 'antd-mobile'
 import { DemoBlock } from 'demos'
+import { CalendarRef } from 'antd-mobile/es/components/calendar'
 
 const min = new Date()
 min.setDate(5)
@@ -10,6 +11,15 @@ max.setDate(20)
 
 export default () => {
   const today = dayjs()
+  const calendarRef = useRef<CalendarRef>(null)
+  useEffect(() => {
+    if (calendarRef.current) {
+      calendarRef.current.jumpTo({
+        year: 2022,
+        month: 4,
+      })
+    }
+  }, [])
   return (
     <>
       <DemoBlock title='周一作为每周的第一天'>
@@ -28,6 +38,15 @@ export default () => {
       <DemoBlock title='限制日期范围'>
         <Calendar min={min} max={max} selectionMode='range' />
       </DemoBlock>
+      <DemoBlock title='限制可切换的page。 2022-08 至 2022-12'>
+        <Calendar
+          minPage={{ year: 2022, month: 8 }}
+          maxPage={{ year: 2022, month: 12 }}
+        />
+      </DemoBlock>
+      <DemoBlock title='默认展示 4 月'>
+        <Calendar ref={calendarRef} />
+      </DemoBlock>
     </>
   )
 }
diff --git a/src/components/footer/demos/demo1.tsx b/src/components/footer/demos/demo1.tsx
new file mode 100644
index 000000000..152e93aca
--- /dev/null
+++ b/src/components/footer/demos/demo1.tsx
@@ -0,0 +1,99 @@
+import React from 'react'
+import { Footer } from 'antd-mobile'
+import { DemoBlock } from 'demos'
+import { HandPayCircleOutline } from 'antd-mobile-icons'
+import { ChipItem, LinkItem } from '../footer'
+
+export default () => {
+  const links: LinkItem[] = [
+    {
+      text: '阿里云',
+      href: 'https://www.aliyun.com/',
+    },
+    {
+      text: '支付宝',
+      href: 'https://www.antgroup.com/',
+    },
+  ]
+  const chips: ChipItem[] = [
+    {
+      text: '蚂蚁借呗',
+    },
+    {
+      text: '备用金',
+    },
+    {
+      text: '蚂蚁花呗',
+    },
+  ]
+  const chipsLinkData: ChipItem[] = [
+    {
+      text: '蚂蚁借呗',
+      type: 'link',
+    },
+    {
+      text: '备用金',
+      type: 'link',
+    },
+    {
+      text: '蚂蚁花呗',
+      type: 'link',
+    },
+  ]
+
+  const onChipClick = (item: ChipItem, index: number) => {
+    console.log(item, index)
+    alert(`${item?.text}被点击了`)
+  }
+  const onLinkClick = (item: LinkItem, index: number) => {
+    console.log(item, index)
+    alert(`跳转到${item?.href}`)
+  }
+
+  return (
+    <>
+      <DemoBlock title='基础页脚'>
+        <Footer label='没有更多了'></Footer>
+      </DemoBlock>
+      <DemoBlock title='自定义 label'>
+        <Footer
+          label={
+            <div>
+              <HandPayCircleOutline /> 蚂蚁财富
+            </div>
+          }
+        ></Footer>
+      </DemoBlock>
+      <DemoBlock title='带内容的页脚'>
+        <Footer content='@ 2004-2020 Alipay.com All rights reserved'></Footer>
+      </DemoBlock>
+      <DemoBlock title='带链接的页脚'>
+        <Footer
+          links={[
+            {
+              text: '阿里云',
+              href: 'https://www.aliyun.com/',
+            },
+          ]}
+        ></Footer>
+      </DemoBlock>
+      <DemoBlock title='通过点击事件跳转'>
+        <Footer links={links} onLinkClick={onLinkClick}></Footer>
+      </DemoBlock>
+      <DemoBlock title='带标签的页脚'>
+        <Footer chips={chips}></Footer>
+      </DemoBlock>
+      <DemoBlock title='标签可点击'>
+        <Footer chips={chipsLinkData} onChipClick={onChipClick}></Footer>
+      </DemoBlock>
+      <DemoBlock title='组合使用'>
+        <Footer
+          label='没有更多了'
+          content='@ 2004-2020 Alipay.com All rights reserved'
+          links={links}
+          chips={chips}
+        ></Footer>
+      </DemoBlock>
+    </>
+  )
+}
diff --git a/src/components/footer/footer.less b/src/components/footer/footer.less
new file mode 100644
index 000000000..123163285
--- /dev/null
+++ b/src/components/footer/footer.less
@@ -0,0 +1,55 @@
+@class-prefix-footer: ~'adm-footer';
+
+.@{class-prefix-footer} {
+  --background-color: var(--adm-color-background);
+
+  color: var(--adm-color-text);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  background-color: var(--background-color);
+
+  &-label {
+    width: 100%;
+    .adm-divider.adm-divider {
+      color: var(--adm-color-light);
+    }
+  }
+  &-links {
+    margin: 8px 0;
+    color: var(--adm-color-primary);
+    white-space: nowrap;
+    a {
+      text-decoration: none;
+    }
+  }
+  &-content {
+    display: flex;
+    align-items: center;
+    margin: 8px 0;
+    color: var(--adm-color-light);
+  }
+  &-chips {
+    display: flex;
+    align-items: center;
+    white-space: nowrap;
+    margin: 8px 0;
+  }
+  &-chip {
+    white-space: nowrap;
+    margin-right: 20px;
+    font-size: 12px;
+    background-color: var(--adm-color-box);
+    color: var(--adm-color-weak);
+    padding: 4px 12px;
+    border-radius: 100px;
+    &-link {
+      cursor: pointer;
+      background-color: #e7f1ff;
+      color: var(--adm-color-primary);
+    }
+    &:last-child {
+      margin-right: 0;
+    }
+  }
+}
diff --git a/src/components/footer/footer.tsx b/src/components/footer/footer.tsx
new file mode 100644
index 000000000..1aa6f8765
--- /dev/null
+++ b/src/components/footer/footer.tsx
@@ -0,0 +1,101 @@
+import React, { FC, ReactNode } from 'react'
+import classNames from 'classnames'
+import { NativeProps, withNativeProps } from '../../utils/native-props'
+import { mergeProps } from '../../utils/with-default-props'
+import { Divider } from '../divider/divider'
+
+const classPrefix = `adm-footer`
+
+export type LinkItem = {
+  text: string
+  href: string
+}
+
+export type ChipItem = {
+  text: ReactNode
+  type?: 'plain' | 'link'
+}
+
+export type FooterProps = {
+  label?: ReactNode
+  links?: LinkItem[]
+  content?: ReactNode
+  chips?: ChipItem[]
+  onChipClick?: (item: ChipItem, index: number) => void
+  onLinkClick?: (item: LinkItem, index: number) => void
+} & NativeProps<'--background-color'>
+
+const defaultProps: FooterProps = {
+  label: '',
+  links: [],
+  content: '',
+  chips: [],
+}
+
+export const Footer: FC<FooterProps> = p => {
+  const props = mergeProps(defaultProps, p)
+  const { label, links, content, chips, onChipClick, onLinkClick } = props
+
+  const clickChipItem = (item: ChipItem, index: number) => {
+    if (chips?.length && item.type === 'link') {
+      onChipClick?.(item, index)
+    }
+  }
+
+  const clickLinkItem = (
+    item: LinkItem,
+    index: number,
+    e: React.MouseEvent<HTMLAnchorElement, MouseEvent>
+  ) => {
+    if (onLinkClick) {
+      e.preventDefault()
+      onLinkClick(item, index)
+    }
+  }
+  return withNativeProps(
+    props,
+    <div className={classNames(classPrefix)}>
+      {label && (
+        <div className={`${classPrefix}-label`}>
+          <Divider>{label}</Divider>
+        </div>
+      )}
+      {links && links.length > 0 && (
+        <div className={`${classPrefix}-links`}>
+          {links.map((link, index) => {
+            return (
+              <React.Fragment key={index}>
+                <a
+                  href={link.href}
+                  rel='noopener noreferrer'
+                  onClick={event => clickLinkItem(link, index, event)}
+                >
+                  {link.text}
+                </a>
+                {index !== links.length - 1 && <Divider direction='vertical' />}
+              </React.Fragment>
+            )
+          })}
+        </div>
+      )}
+      {content && <div className={`${classPrefix}-content`}>{content}</div>}
+      {chips && chips.length > 0 && (
+        <div className={`${classPrefix}-chips`}>
+          {chips.map((chip, index) => {
+            return (
+              <div
+                key={index}
+                onClick={() => clickChipItem(chip, index)}
+                className={classNames(`${classPrefix}-chip`, {
+                  [`${classPrefix}-chip-link`]: chip.type === 'link',
+                })}
+              >
+                {chip.text}
+              </div>
+            )
+          })}
+        </div>
+      )}
+    </div>
+  )
+}
diff --git a/src/components/footer/index.en.md b/src/components/footer/index.en.md
new file mode 100644
index 000000000..374606574
--- /dev/null
+++ b/src/components/footer/index.en.md
@@ -0,0 +1,42 @@
+# Footer
+
+## When to Use
+
+Applicable to information description at the bottom of the page。
+
+## Demos
+
+<code src="./demos/demo1.tsx"></code>
+
+## Footer
+
+### Props
+
+| Name        | Description                                                                                                                 | Type                                      | Default |
+| ----------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ------- |
+| label       | Top content with a dividing line                                                                                            | `string \| ReactNode`                     | -       |
+| links       | Links to content                                                                                                            | `LinkItem[]`                              | -       |
+| label       | The common content section                                                                                                  | `string \| ReactNode`                     | -       |
+| chips       | Chips, only one line of content is allowed. If the content exceeds the width, the content will be automatically intercepted | `ChipItem[]`                              | -       |
+| onTagClick  | Click the bottom label to trigger, and it will only take effect when the label 'type' is' link '                            | `(item: ChipItem, index: number) => void` | -       |
+| onLinkClick | Clicking the link triggers an event that intercepts the href jump to the A tag                                              | `(item: LinkItem, index: number) => void` | -       |
+
+### ChipItem
+
+| Name | Description                                           | Type                  | Default   |
+| ---- | ----------------------------------------------------- | --------------------- | --------- |
+| text | Tag Text                                              | `string \| ReactNode` | -         |
+| type | Tag Type, `plain`is pure display, `link` is clickable | `'plain' \| 'link'`   | `'plain'` |
+
+### LinkItem
+
+| Name | Description   | Type     | Default |
+| ---- | ------------- | -------- | ------- |
+| text | Text of links | `string` | -       |
+| href | Href of links | `string` | -       |
+
+### CSS Variables
+
+| Name               | Description | Default                       |
+| ------------------ | ----------- | ----------------------------- |
+| --background-color | background  | `var(--adm-color-background)` |
diff --git a/src/components/footer/index.ts b/src/components/footer/index.ts
new file mode 100644
index 000000000..9d9657ce7
--- /dev/null
+++ b/src/components/footer/index.ts
@@ -0,0 +1,6 @@
+import './footer.less'
+import { Footer } from './footer'
+
+export type { FooterProps, ChipItem, LinkItem } from './footer'
+
+export default Footer
diff --git a/src/components/footer/index.zh.md b/src/components/footer/index.zh.md
new file mode 100644
index 000000000..7b392d9a7
--- /dev/null
+++ b/src/components/footer/index.zh.md
@@ -0,0 +1,42 @@
+# Footer 页脚
+
+## 何时使用
+
+适用于页面底部信息描述。
+
+## 示例
+
+<code src="./demos/demo1.tsx"></code>
+
+## Footer
+
+### 属性
+
+| 属性        | 说明                                             | 类型                                      | 默认值 |
+| ----------- | ------------------------------------------------ | ----------------------------------------- | ------ |
+| label       | 带分割线的顶部内容                               | `string \| ReactNode`                     | -      |
+| links       | 链接内容                                         | `LinkItem[]`                              | -      |
+| content     | 普通的内容部分                                   | `string \| ReactNode`                     | -      |
+| chips       | 底部标签                                         | `ChipItem[]`                              | -      |
+| onChipClick | 点击底部标签触发，仅在标签 `type` 为 `link` 生效 | `(item: ChipItem, index: number) => void` | -      |
+| onLinkClick | 点击链接触发事件，拦截 a 标签的 href 跳转        | `(item: LinkItem, index: number) => void` | -      |
+
+### ChipItem
+
+| 属性 | 说明                                        | 类型                  | 默认值    |
+| ---- | ------------------------------------------- | --------------------- | --------- |
+| text | 标签内容                                    | `string \| ReactNode` | -         |
+| type | 标签类型，`plain` 为纯展示，`link` 为可点击 | `'plain' \| 'link'`   | `'plain'` |
+
+### LinkItem
+
+| 属性 | 说明        | 类型     | 默认值 |
+| ---- | ----------- | -------- | ------ |
+| text | 链接的文字  | `string` | -      |
+| href | 链接的 href | `string` | -      |
+
+### CSS 变量
+
+| 属性               | 说明   | 默认值                        |
+| ------------------ | ------ | ----------------------------- |
+| --background-color | 背景色 | `var(--adm-color-background)` |
diff --git a/src/components/image-uploader/image-uploader.tsx b/src/components/image-uploader/image-uploader.tsx
index e1959b117..b3db8d793 100644
--- a/src/components/image-uploader/image-uploader.tsx
+++ b/src/components/image-uploader/image-uploader.tsx
@@ -10,7 +10,7 @@ import { NativeProps, withNativeProps } from '../../utils/native-props'
 import { useConfig } from '../config-provider'
 import type { ImageProps } from '../image'
 
-export type TaskStatus = 'pending' | 'fail'
+export type TaskStatus = 'pending' | 'fail' | 'success'
 
 export interface ImageUploadItem {
   key?: string | number
@@ -26,10 +26,13 @@ type Task = {
   status: TaskStatus
 }
 
+export type UploadTask = Pick<Task, 'id' | 'status'>
+
 export type ImageUploaderProps = {
   defaultValue?: ImageUploadItem[]
   value?: ImageUploadItem[]
   onChange?: (items: ImageUploadItem[]) => void
+  onUploadQueueChange?: (tasks: UploadTask[]) => void
   accept?: string
   multiple?: boolean
   maxCount?: number
@@ -49,6 +52,11 @@ export type ImageUploaderProps = {
   showFailed?: boolean
   imageFit?: ImageProps['fit']
   children?: React.ReactNode
+  renderItem?: (
+    originNode: React.ReactElement,
+    file: ImageUploadItem,
+    fileList: ImageUploadItem[]
+  ) => React.ReactNode
 } & NativeProps<'--cell-size'>
 
 const classPrefix = `adm-image-uploader`
@@ -82,9 +90,15 @@ export const ImageUploader: FC<ImageUploaderProps> = p => {
     )
   }, [value])
 
+  useIsomorphicLayoutEffect(() => {
+    props.onUploadQueueChange?.(
+      tasks.map(item => ({ id: item.id, status: item.status }))
+    )
+  }, [tasks])
+
   const idCountRef = useRef(0)
 
-  const { maxCount, onPreview } = props
+  const { maxCount, onPreview, renderItem } = props
 
   async function processFile(file: File, fileList: File[]) {
     const { beforeUpload } = props
@@ -145,6 +159,7 @@ export const ImageUploader: FC<ImageUploaderProps> = p => {
               if (task.id === currentTask.id) {
                 return {
                   ...task,
+                  status: 'success',
                   url: result.url,
                 }
               }
@@ -193,29 +208,36 @@ export const ImageUploader: FC<ImageUploaderProps> = p => {
     props.showUpload &&
     (maxCount === 0 || value.length + tasks.length < maxCount)
 
+  const renderImages = () => {
+    return value.map((fileItem, index) => {
+      const originNode = (
+        <PreviewItem
+          key={fileItem.key ?? index}
+          url={fileItem.thumbnailUrl ?? fileItem.url}
+          deletable={props.deletable}
+          imageFit={props.imageFit}
+          onClick={() => {
+            if (props.preview) {
+              previewImage(index)
+            }
+            onPreview && onPreview(index, fileItem)
+          }}
+          onDelete={async () => {
+            const canDelete = await props.onDelete?.(fileItem)
+            if (canDelete === false) return
+            setValue(value.filter((x, i) => i !== index))
+          }}
+        />
+      )
+      return renderItem ? renderItem(originNode, fileItem, value) : originNode
+    })
+  }
+
   return withNativeProps(
     props,
     <div className={classPrefix}>
       <Space className={`${classPrefix}-space`} wrap block>
-        {value.map((fileItem, index) => (
-          <PreviewItem
-            key={fileItem.key ?? index}
-            url={fileItem.thumbnailUrl ?? fileItem.url}
-            deletable={props.deletable}
-            imageFit={props.imageFit}
-            onClick={() => {
-              if (props.preview) {
-                previewImage(index)
-              }
-              onPreview && onPreview(index, fileItem)
-            }}
-            onDelete={async () => {
-              const canDelete = await props.onDelete?.(fileItem)
-              if (canDelete === false) return
-              setValue(value.filter((x, i) => i !== index))
-            }}
-          />
-        ))}
+        {renderImages()}
         {tasks.map(task => {
           if (!props.showFailed && task.status === 'fail') {
             return null
diff --git a/src/components/image-uploader/index.en.md b/src/components/image-uploader/index.en.md
index 40602af19..ade729693 100644
--- a/src/components/image-uploader/index.en.md
+++ b/src/components/image-uploader/index.en.md
@@ -10,27 +10,29 @@
 
 ### Props
 
-| Name          | Description                                                                                                                                                              | Type                                                                   | Default   |
-| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | --------- |
-| accept        | The file types, `image/gif` `image/jpeg` `image/jpg` `image/png`                                                                                                         | `string`                                                               | `image/*` |
-| beforeUpload  | Callback function before file reading, return `null` to terminate file reading, support return `Promise`                                                                 | `(file: File, files: File[]) => Promise<File \| null> \| File \| null` | -         |
-| capture       | Picture selection mode, the optional value is `camera` (directly adjust the camera)                                                                                      | `boolean \| string`                                                    | -         |
-| children      | Custom upload button                                                                                                                                                     | `ReactNode`                                                            | -         |
-| defaultValue  | Default list of uploaded files                                                                                                                                           | `ImageUploadItem[]`                                                    | -         |
-| deletable     | Whether to show the delete button                                                                                                                                        | `boolean`                                                              | `true`    |
-| disableUpload | Whether to disable the upload button                                                                                                                                     | `boolean`                                                              | `false`   |
-| imageFit      | The fill mode of the image, same as [Image](/components/image#props)                                                                                                     | `'contain' \| 'cover' \| 'fill' \| 'none' \| 'scale-down'`             | `cover`   |
-| maxCount      | To control how many pictures to be uploaded at most, the upload button would be automatically hidden if the number is exceeded, `0` means no limit                       | `number`                                                               | `0`       |
-| multiple      | Whether to support selecting multiple pictures                                                                                                                           | `boolean`                                                              | `false`   |
-| onChange      | Triggered when the list of uploaded files changing                                                                                                                       | `(items: ImageUploadItem[]) => void`                                   | -         |
-| onCountExceed | Triggered when the number of pictures selected by the user exceeds the maximum limit                                                                                     | `(exceed: number) => void`                                             | -         |
-| onDelete      | Triggered when the successfully uploaded image is deleted, if it returns false, it means that it is prevented from being deleted, and it supports the return of Promise. | `(item: ImageUploadItem) => boolean \| Promise<boolean> \| void`       | -         |
-| onPreview     | Callback when the preview picture is clicked                                                                                                                             | `(index: number, item: ImageUploadItem) => void`                       | -         |
-| preview       | Whether to support preview                                                                                                                                               | `boolean`                                                              | `true`    |
-| showFailed    | Whether to display pictures in the failed uploading state                                                                                                                | `boolean`                                                              | `true`    |
-| showUpload    | Whether to display the upload button                                                                                                                                     | `boolean`                                                              | `true`    |
-| upload        | Upload method, the input parameter is the file object that needs to be uploaded, after asynchronous processing, the upload result is returned                            | `(file: File) => Promise<ImageUploadItem>`                             | -         |
-| value         | List of uploaded files                                                                                                                                                   | `ImageUploadItem[]`                                                    | -         |
+| Name                | Description                                                                                                                                                              | Type                                                                                                       | Default   |
+| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | --------- |
+| accept              | The file types, `image/gif` `image/jpeg` `image/jpg` `image/png`                                                                                                         | `string`                                                                                                   | `image/*` |
+| beforeUpload        | Callback function before file reading, return `null` to terminate file reading, support return `Promise`                                                                 | `(file: File, files: File[]) => Promise<File \| null> \| File \| null`                                     | -         |
+| capture             | Picture selection mode, the optional value is `camera` (directly adjust the camera)                                                                                      | `boolean \| string`                                                                                        | -         |
+| children            | Custom upload button                                                                                                                                                     | `ReactNode`                                                                                                | -         |
+| defaultValue        | Default list of uploaded files                                                                                                                                           | `ImageUploadItem[]`                                                                                        | -         |
+| deletable           | Whether to show the delete button                                                                                                                                        | `boolean`                                                                                                  | `true`    |
+| disableUpload       | Whether to disable the upload button                                                                                                                                     | `boolean`                                                                                                  | `false`   |
+| imageFit            | The fill mode of the image, same as [Image](/components/image#props)                                                                                                     | `'contain' \| 'cover' \| 'fill' \| 'none' \| 'scale-down'`                                                 | `cover`   |
+| maxCount            | To control how many pictures to be uploaded at most, the upload button would be automatically hidden if the number is exceeded, `0` means no limit                       | `number`                                                                                                   | `0`       |
+| multiple            | Whether to support selecting multiple pictures                                                                                                                           | `boolean`                                                                                                  | `false`   |
+| onChange            | Triggered when the list of uploaded files changing                                                                                                                       | `(items: ImageUploadItem[]) => void`                                                                       | -         |
+| onCountExceed       | Triggered when the number of pictures selected by the user exceeds the maximum limit                                                                                     | `(exceed: number) => void`                                                                                 | -         |
+| onDelete            | Triggered when the successfully uploaded image is deleted, if it returns false, it means that it is prevented from being deleted, and it supports the return of Promise. | `(item: ImageUploadItem) => boolean \| Promise<boolean> \| void`                                           | -         |
+| onPreview           | Callback when the preview picture is clicked                                                                                                                             | `(index: number, item: ImageUploadItem) => void`                                                           | -         |
+| preview             | Whether to support preview                                                                                                                                               | `boolean`                                                                                                  | `true`    |
+| showFailed          | Whether to display pictures in the failed uploading state                                                                                                                | `boolean`                                                                                                  | `true`    |
+| showUpload          | Whether to display the upload button                                                                                                                                     | `boolean`                                                                                                  | `true`    |
+| upload              | Upload method, the input parameter is the file object that needs to be uploaded, after asynchronous processing, the upload result is returned                            | `(file: File) => Promise<ImageUploadItem>`                                                                 | -         |
+| value               | List of uploaded files                                                                                                                                                   | `ImageUploadItem[]`                                                                                        | -         |
+| onUploadQueueChange | Triggered when the image upload queue changes                                                                                                                            | `(tasks: UploadTask[]) => void`                                                                            |
+| renderItem          | Custom item of uploadList                                                                                                                                                | `(originNode: React.ReactElement, file: ImageUploadItem, fileList: ImageUploadItem[] ) => React.ReactNode` | -         |
 
 ### ImageUploadItem
 
@@ -41,6 +43,13 @@
 | thumbnailUrl | The resource address of the thumbnail picture | `string`           | `url`           |
 | url          | The resource address of the picture           | `string`           | -               |
 
+### UploadTask
+
+| Name   | Description         | Type                               | Default |
+| ------ | ------------------- | ---------------------------------- | ------- |
+| id     | Image upload id     | `number`                           | -       |
+| status | Image upload status | `'pending' \| 'fail' \| 'success'` | -       |
+
 ### CSS Variables
 
 | Name        | Description                         | Default |
diff --git a/src/components/image-uploader/index.ts b/src/components/image-uploader/index.ts
index 817db206d..6f7eb9059 100644
--- a/src/components/image-uploader/index.ts
+++ b/src/components/image-uploader/index.ts
@@ -1,6 +1,10 @@
 import { ImageUploader } from './image-uploader'
 import './image-uploader.less'
 
-export type { ImageUploadItem, ImageUploaderProps } from './image-uploader'
+export type {
+  ImageUploadItem,
+  ImageUploaderProps,
+  UploadTask,
+} from './image-uploader'
 
 export default ImageUploader
diff --git a/src/components/image-uploader/index.zh.md b/src/components/image-uploader/index.zh.md
index d5443ec80..a0eed56ec 100644
--- a/src/components/image-uploader/index.zh.md
+++ b/src/components/image-uploader/index.zh.md
@@ -10,27 +10,29 @@
 
 ### 属性
 
-| 属性          | 说明                                                                          | 类型                                                                   | 默认值    |
-| ------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------- |
-| accept        | 文件类型，`image/gif` `image/jpeg` `image/jpg` `image/png`                    | `string`                                                               | `image/*` |
-| beforeUpload  | 文件读取前的回调函数，返回 `null` 可终止文件读取，支持返回 `Promise`          | `(file: File, files: File[]) => Promise<File \| null> \| File \| null` | -         |
-| capture       | 图片选取模式，可选值为 `camera`（直接调起摄像头）                             | `boolean \| string`                                                    | -         |
-| children      | 自定义上传按钮                                                                | `ReactNode`                                                            | -         |
-| defaultValue  | 默认已上传的文件列表                                                          | `ImageUploadItem[]`                                                    | -         |
-| deletable     | 是否展示删除按钮                                                              | `boolean`                                                              | `true`    |
-| disableUpload | 是否禁用上传按钮                                                              | `boolean`                                                              | `false`   |
-| imageFit      | 图片填充模式，同 [Image](/zh/components/image#属性)                           | `'contain' \| 'cover' \| 'fill' \| 'none' \| 'scale-down'`             | `cover`   |
-| maxCount      | 最多上传几张图片，超出数量会自动隐藏上传按钮，`0` 表示不做限制                | `number`                                                               | `0`       |
-| multiple      | 是否支持选择多张图片                                                          | `boolean`                                                              | `false`   |
-| onChange      | 已上传的文件列表变化时触发                                                    | `(items: ImageUploadItem[]) => void`                                   | -         |
-| onCountExceed | 用户选择的图片数量超出最大限制时触发                                          | `(exceed: number) => void`                                             | -         |
-| onDelete      | 删除已上传成功的图片时触发，如果返回 `false` 表示阻止删除，支持返回 `Promise` | `(item: ImageUploadItem) => boolean \| Promise<boolean> \| void`       | -         |
-| onPreview     | 点击预览图片                                                                  | `(index: number, item: ImageUploadItem) => void`                       | -         |
-| preview       | 是否支持预览                                                                  | `boolean`                                                              | `true`    |
-| showFailed    | 是否展示上传失败状态下的图片                                                  | `boolean`                                                              | `true`    |
-| showUpload    | 是否展示上传按钮                                                              | `boolean`                                                              | `true`    |
-| upload        | 上传方法，入参是需要被上传的文件对象，经过异步处理之后，返回上传结果          | `(file: File) => Promise<ImageUploadItem>`                             | -         |
-| value         | 已上传的文件列表                                                              | `ImageUploadItem[]`                                                    | -         |
+| 属性                | 说明                                                                          | 类型                                                                                                       | 默认值    |
+| ------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------- |
+| accept              | 文件类型，`image/gif` `image/jpeg` `image/jpg` `image/png`                    | `string`                                                                                                   | `image/*` |
+| beforeUpload        | 文件读取前的回调函数，返回 `null` 可终止文件读取，支持返回 `Promise`          | `(file: File, files: File[]) => Promise<File \| null> \| File \| null`                                     | -         |
+| capture             | 图片选取模式，可选值为 `camera`（直接调起摄像头）                             | `boolean \| string`                                                                                        | -         |
+| children            | 自定义上传按钮                                                                | `ReactNode`                                                                                                | -         |
+| defaultValue        | 默认已上传的文件列表                                                          | `ImageUploadItem[]`                                                                                        | -         |
+| deletable           | 是否展示删除按钮                                                              | `boolean`                                                                                                  | `true`    |
+| disableUpload       | 是否禁用上传按钮                                                              | `boolean`                                                                                                  | `false`   |
+| imageFit            | 图片填充模式，同 [Image](/zh/components/image#属性)                           | `'contain' \| 'cover' \| 'fill' \| 'none' \| 'scale-down'`                                                 | `cover`   |
+| maxCount            | 最多上传几张图片，超出数量会自动隐藏上传按钮，`0` 表示不做限制                | `number`                                                                                                   | `0`       |
+| multiple            | 是否支持选择多张图片                                                          | `boolean`                                                                                                  | `false`   |
+| onChange            | 已上传的文件列表变化时触发                                                    | `(items: ImageUploadItem[]) => void`                                                                       | -         |
+| onCountExceed       | 用户选择的图片数量超出最大限制时触发                                          | `(exceed: number) => void`                                                                                 | -         |
+| onDelete            | 删除已上传成功的图片时触发，如果返回 `false` 表示阻止删除，支持返回 `Promise` | `(item: ImageUploadItem) => boolean \| Promise<boolean> \| void`                                           | -         |
+| onPreview           | 点击预览图片                                                                  | `(index: number, item: ImageUploadItem) => void`                                                           | -         |
+| preview             | 是否支持预览                                                                  | `boolean`                                                                                                  | `true`    |
+| showFailed          | 是否展示上传失败状态下的图片                                                  | `boolean`                                                                                                  | `true`    |
+| showUpload          | 是否展示上传按钮                                                              | `boolean`                                                                                                  | `true`    |
+| upload              | 上传方法，入参是需要被上传的文件对象，经过异步处理之后，返回上传结果          | `(file: File) => Promise<ImageUploadItem>`                                                                 | -         |
+| value               | 已上传的文件列表                                                              | `ImageUploadItem[]`                                                                                        | -         |
+| onUploadQueueChange | 图片上传队列变化时触发                                                        | `(tasks: UploadTask[]) => void`                                                                            | -         |
+| renderItem          | 自定义上传列表项                                                              | `(originNode: React.ReactElement, file: ImageUploadItem, fileList: ImageUploadItem[] ) => React.ReactNode` | -         |
 
 ### ImageUploadItem
 
@@ -41,6 +43,13 @@
 | thumbnailUrl | 缩略图的资源地址                         | `string`           | `url`    |
 | url          | 图片的资源地址                           | `string`           | -        |
 
+### UploadTask
+
+| 属性   | 说明         | 类型                               | 默认值 |
+| ------ | ------------ | ---------------------------------- | ------ |
+| id     | 上传图片 id  | `number`                           | -      |
+| status | 图片上传状态 | `'pending' \| 'fail' \| 'success'` | -      |
+
 ### CSS 变量
 
 | 属性        | 说明                 | 默认值 |
diff --git a/src/components/image-uploader/preview-item.tsx b/src/components/image-uploader/preview-item.tsx
index e286d5cb9..193afb6da 100644
--- a/src/components/image-uploader/preview-item.tsx
+++ b/src/components/image-uploader/preview-item.tsx
@@ -1,4 +1,4 @@
-import React, { FC, useMemo } from 'react'
+import React, { FC, useLayoutEffect, useMemo } from 'react'
 import { CloseOutline } from 'antd-mobile-icons'
 import classNames from 'classnames'
 import { TaskStatus } from './image-uploader'
@@ -32,6 +32,12 @@ const PreviewItem: FC<Props> = props => {
     return ''
   }, [url, file])
 
+  useLayoutEffect(() => {
+    return () => {
+      URL.revokeObjectURL(src)
+    }
+  }, [src])
+
   function renderLoading() {
     return (
       props.status === 'pending' && (
diff --git a/src/components/image-viewer/slides.tsx b/src/components/image-viewer/slides.tsx
index f0b9f61f9..606204a6c 100644
--- a/src/components/image-viewer/slides.tsx
+++ b/src/components/image-viewer/slides.tsx
@@ -91,9 +91,9 @@ export const Slides = forwardRef<SlidesRef, SlidesType>((props, ref) => {
         className={`${classPrefix}-slides-inner`}
         style={{ x: x.to(x => -x) }}
       >
-        {props.images.map(image => (
+        {props.images.map((image, index) => (
           <Slide
-            key={image}
+            key={index}
             image={image}
             onTap={props.onTap}
             maxZoom={props.maxZoom}
diff --git a/src/components/picker-view/picker-view.less b/src/components/picker-view/picker-view.less
index b9d9d8010..2ea84c54b 100644
--- a/src/components/picker-view/picker-view.less
+++ b/src/components/picker-view/picker-view.less
@@ -130,3 +130,12 @@
   justify-content: center;
   align-items: center;
 }
+
+.@{class-prefix-picker-view}-item-height-measure {
+  position: relative;
+  left: 0;
+  top: 0;
+  height: var(--item-height);
+  width: 0;
+  pointer-events: none;
+}
diff --git a/src/components/picker-view/wheel.tsx b/src/components/picker-view/wheel.tsx
index 8705648c8..ce8a0ac0c 100644
--- a/src/components/picker-view/wheel.tsx
+++ b/src/components/picker-view/wheel.tsx
@@ -43,13 +43,15 @@ export const Wheel = memo<Props>(
     const draggingRef = useRef(false)
 
     const rootRef = useRef<HTMLDivElement>(null)
+
+    const itemHeightMeasureRef = useRef<HTMLDivElement>(null)
     const itemHeight = useRef<number>(34)
 
     useIsomorphicLayoutEffect(() => {
-      const root = rootRef.current
-      if (!root) return
+      const itemHeightMeasure = itemHeightMeasureRef.current
+      if (!itemHeightMeasure) return
       itemHeight.current = measureCSSLength(
-        window.getComputedStyle(root).getPropertyValue('--item-height')
+        window.getComputedStyle(itemHeightMeasure).getPropertyValue('height')
       )
     })
 
@@ -200,6 +202,10 @@ export const Wheel = memo<Props>(
 
     return (
       <div className={`${classPrefix}-column`}>
+        <div
+          className={`${classPrefix}-item-height-measure`}
+          ref={itemHeightMeasureRef}
+        />
         <animated.div
           ref={rootRef}
           style={{ translateY: y }}
diff --git a/src/components/slider/index.en.md b/src/components/slider/index.en.md
index da3050f34..9b2481cbd 100644
--- a/src/components/slider/index.en.md
+++ b/src/components/slider/index.en.md
@@ -16,21 +16,22 @@ When the user needs to select within the numerical interval/custom interval, it
 
 ### Props
 
-| Name          | Description                                                                                                                                              | Type                                          | Default              |
-| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | -------------------- |
-| defaultValue  | Default value                                                                                                                                            | `number \| [number, number]`                  | `range ? [0, 0] : 0` |
-| disabled      | Whether disabled                                                                                                                                         | `boolean`                                     | `false`              |
-| icon          | The icon of slider                                                                                                                                       | `ReactNode`                                   | -                    |
-| marks         | Tick marks                                                                                                                                               | `{ [key: number]: React.ReactNode }`          | -                    |
-| max           | Max value                                                                                                                                                | `number`                                      | `100`                |
-| min           | Min value                                                                                                                                                | `number`                                      | `0`                  |
-| onAfterChange | Consistent with the trigger timing of `touchend`, pass the current value as a parameter                                                                  | `(value: number \| [number, number]) => void` | -                    |
-| onChange      | Triggered when the slider is dragged, and the current dragged value is passed in as a parameter                                                          | `(value: number \| [number, number]) => void` | -                    |
-| popover       | Whether to display the popover when dragging. Support passing in function to customize the rendering content.                                            | `boolean \| ((value: number) => ReactNode)`   | `false`              |
-| range         | Whether it is a double sliders                                                                                                                           | `boolean`                                     | `false`              |
-| step          | Step distance, the value must be greater than `0`, and can be divisible by `(max-min)`. When `marks` is not null, the configuration of `step` is invalid | `number`                                      | `1`                  |
-| ticks         | Whether to display the scale                                                                                                                             | `boolean`                                     | `false`              |
-| value         | Current value                                                                                                                                            | `number \| [number, number]`                  | -                    |
+| Name            | Description                                                                                                                                              | Type                                          | Default              |
+| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | -------------------- |
+| defaultValue    | Default value                                                                                                                                            | `number \| [number, number]`                  | `range ? [0, 0] : 0` |
+| disabled        | Whether disabled                                                                                                                                         | `boolean`                                     | `false`              |
+| icon            | The icon of slider                                                                                                                                       | `ReactNode`                                   | -                    |
+| marks           | Tick marks                                                                                                                                               | `{ [key: number]: React.ReactNode }`          | -                    |
+| max             | Max value                                                                                                                                                | `number`                                      | `100`                |
+| min             | Min value                                                                                                                                                | `number`                                      | `0`                  |
+| onAfterChange   | Consistent with the trigger timing of `touchend`, pass the current value as a parameter                                                                  | `(value: number \| [number, number]) => void` | -                    |
+| onChange        | Triggered when the slider is dragged, and the current dragged value is passed in as a parameter                                                          | `(value: number \| [number, number]) => void` | -                    |
+| popover         | Whether to display the popover when dragging. Support passing in function to customize the rendering content.                                            | `boolean \| ((value: number) => ReactNode)`   | `false`              |
+| residentPopover | Whether the `popover` is permanently displayed , this attribute takes effect when `popover` exists                                                       | `boolean `                                    | `false`              |
+| range           | Whether it is a double sliders                                                                                                                           | `boolean`                                     | `false`              |
+| step            | Step distance, the value must be greater than `0`, and can be divisible by `(max-min)`. When `marks` is not null, the configuration of `step` is invalid | `number`                                      | `1`                  |
+| ticks           | Whether to display the scale                                                                                                                             | `boolean`                                     | `false`              |
+| value           | Current value                                                                                                                                            | `number \| [number, number]`                  | -                    |
 
 ### CSS Variables
 
diff --git a/src/components/slider/index.zh.md b/src/components/slider/index.zh.md
index 2067b0586..f689d1f4a 100644
--- a/src/components/slider/index.zh.md
+++ b/src/components/slider/index.zh.md
@@ -16,21 +16,22 @@
 
 ### 属性
 
-| 属性          | 说明                                                                                            | 类型                                          | 默认值               |
-| ------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------- | -------------------- |
-| defaultValue  | 默认值                                                                                          | `number \| [number, number]`                  | `range ? [0, 0] : 0` |
-| disabled      | 是否禁用                                                                                        | `boolean`                                     | `false`              |
-| icon          | 滑块的图标                                                                                      | `ReactNode`                                   | -                    |
-| marks         | 刻度标记                                                                                        | `{ [key: number]: React.ReactNode }`          | -                    |
-| max           | 最大值                                                                                          | `number`                                      | `100`                |
-| min           | 最小值                                                                                          | `number`                                      | `0`                  |
-| onAfterChange | 与 `touchend` 触发时机一致，把当前值作为参数传入                                                | `(value: number \| [number, number]) => void` | -                    |
-| onChange      | 拖拽滑块时触发，并把当前拖拽的值作为参数传入                                                    | `(value: number \| [number, number]) => void` | -                    |
-| popover       | 是否在拖动时显示悬浮提示，支持传入函数自定义渲染内容                                            | `boolean \| ((value: number) => ReactNode)`   | `false`              |
-| range         | 是否为双滑块                                                                                    | `boolean`                                     | `false`              |
-| step          | 步距，取值必须大于 `0`，并且可被 `(max - min)` 整除。当 `marks` 不为空对象时，`step` 的配置失效 | `number`                                      | `1`                  |
-| ticks         | 是否显示刻度                                                                                    | `boolean`                                     | `false`              |
-| value         | 当前值                                                                                          | `number \| [number, number]`                  | -                    |
+| 属性            | 说明                                                                                            | 类型                                          | 默认值               |
+| --------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------- | -------------------- |
+| defaultValue    | 默认值                                                                                          | `number \| [number, number]`                  | `range ? [0, 0] : 0` |
+| disabled        | 是否禁用                                                                                        | `boolean`                                     | `false`              |
+| icon            | 滑块的图标                                                                                      | `ReactNode`                                   | -                    |
+| marks           | 刻度标记                                                                                        | `{ [key: number]: React.ReactNode }`          | -                    |
+| max             | 最大值                                                                                          | `number`                                      | `100`                |
+| min             | 最小值                                                                                          | `number`                                      | `0`                  |
+| onAfterChange   | 与 `touchend` 触发时机一致，把当前值作为参数传入                                                | `(value: number \| [number, number]) => void` | -                    |
+| onChange        | 拖拽滑块时触发，并把当前拖拽的值作为参数传入                                                    | `(value: number \| [number, number]) => void` | -                    |
+| popover         | 是否在拖动时显示悬浮提示，支持传入函数自定义渲染内容                                            | `boolean \| ((value: number) => ReactNode)`   | `false`              |
+| residentPopover | `popover` 是否常驻显示，`popover` 存在时生效                                                    | `boolean `                                    | `false`              |
+| range           | 是否为双滑块                                                                                    | `boolean`                                     | `false`              |
+| step            | 步距，取值必须大于 `0`，并且可被 `(max - min)` 整除。当 `marks` 不为空对象时，`step` 的配置失效 | `number`                                      | `1`                  |
+| ticks           | 是否显示刻度                                                                                    | `boolean`                                     | `false`              |
+| value           | 当前值                                                                                          | `number \| [number, number]`                  | -                    |
 
 ### CSS 变量
 
diff --git a/src/components/slider/slider.tsx b/src/components/slider/slider.tsx
index f46dc1bc0..9271b82ee 100644
--- a/src/components/slider/slider.tsx
+++ b/src/components/slider/slider.tsx
@@ -25,6 +25,7 @@ export type SliderProps = {
   range?: boolean
   icon?: ReactNode
   popover?: boolean | ((value: number) => ReactNode)
+  residentPopover?: boolean
   onChange?: (value: SliderValue) => void
   onAfterChange?: (value: SliderValue) => void
 } & NativeProps<'--fill-color'>
@@ -37,6 +38,7 @@ const defaultProps = {
   range: false,
   disabled: false,
   popover: false,
+  residentPopover: false,
 }
 
 export const Slider: FC<SliderProps> = p => {
@@ -161,6 +163,7 @@ export const Slider: FC<SliderProps> = p => {
         trackRef={trackRef}
         icon={icon}
         popover={props.popover}
+        residentPopover={props.residentPopover}
         onDrag={(position, first, last) => {
           if (first) {
             dragLockRef.current += 1
diff --git a/src/components/slider/thumb.tsx b/src/components/slider/thumb.tsx
index 9c48b625d..28064bb95 100644
--- a/src/components/slider/thumb.tsx
+++ b/src/components/slider/thumb.tsx
@@ -16,10 +16,11 @@ type ThumbProps = {
   trackRef: RefObject<HTMLDivElement>
   icon?: React.ReactNode
   popover: boolean | ((value: number) => ReactNode)
+  residentPopover: boolean
 } & NativeProps
 
 const Thumb: FC<ThumbProps> = props => {
-  const { value, min, max, disabled, onDrag, icon } = props
+  const { value, min, max, disabled, icon, residentPopover, onDrag } = props
   const prevValue = useRef(value)
   const { locale } = useConfig()
 
@@ -80,7 +81,7 @@ const Thumb: FC<ThumbProps> = props => {
         <Popover
           content={renderPopoverContent(value)}
           placement='top'
-          visible={dragging}
+          visible={residentPopover || dragging}
           getContainer={null}
           mode='dark'
         >
diff --git a/src/index.ts b/src/index.ts
index 5858a072d..c51eb939c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -81,7 +81,7 @@ export { default as Toast } from './components/toast'
 export { default as TreeSelect } from './components/tree-select'
 export { default as VirtualInput } from './components/virtual-input'
 export { default as WaterMark } from './components/water-mark'
-
+export { default as Footer } from './components/footer'
 export { createErrorBlock } from './components/error-block'
 
 export { reduceMotion, restoreMotion } from './utils/reduce-and-restore-motion'
diff --git a/src/utils/measure-css-length.ts b/src/utils/measure-css-length.ts
index d4c2fe406..913a3eb96 100644
--- a/src/utils/measure-css-length.ts
+++ b/src/utils/measure-css-length.ts
@@ -6,7 +6,7 @@ export function measureCSSLength(raw: string | undefined | null): number {
     if (isDev) {
       devError(
         'Global',
-        'Seems like the you will encounter a style messed problem. Please check the browser environment to make sure it supports CSS variables.'
+        'Something went wrong when calculating CSS length. Please report an issue at https://github.com/ant-design/ant-design-mobile/issues/new/choose'
       )
     }
     return 0


Test patch:
diff --git a/src/components/config-provider/tests/config-provider.test.tsx b/src/components/config-provider/tests/config-provider.test.tsx
index a8bc72470..4070556a7 100644
--- a/src/components/config-provider/tests/config-provider.test.tsx
+++ b/src/components/config-provider/tests/config-provider.test.tsx
@@ -23,6 +23,8 @@ import kkKZ from '../../../locales/kk-KZ'
 const locales = [zhCN, zhTW, zhHK, enUS, faIR, esES, koKR, frFR, kkKZ, idID]
 
 describe('ConfigProvider', () => {
+  URL.revokeObjectURL = jest.fn(() => '')
+
   beforeAll(() => {
     MockDate.set(new Date('2022-03-22'))
   })
diff --git a/src/components/image-uploader/tests/image-uploader.test.tsx b/src/components/image-uploader/tests/image-uploader.test.tsx
index 32c217190..3ef405c24 100644
--- a/src/components/image-uploader/tests/image-uploader.test.tsx
+++ b/src/components/image-uploader/tests/image-uploader.test.tsx
@@ -45,6 +45,7 @@ async function mockInputFile(file: File | File[] = mockImg) {
 describe('ImageUploader', () => {
   // jsdom does not support createObjectURL
   URL.createObjectURL = jest.fn(() => '')
+  URL.revokeObjectURL = jest.fn(() => '')
 
   const App = (props: any) => {
     const [fileList, setFileList] = useState<ImageUploadItem[]>([
@@ -206,4 +207,23 @@ describe('ImageUploader', () => {
 
     expect(container).toMatchSnapshot()
   })
+
+  test('`renderItem` prop', async () => {
+    const customClassName = 'custom-wrapper'
+    render(
+      <App
+        renderItem={(originNode: React.ReactElement, file: ImageUploadItem) => {
+          return (
+            <div key={file.url} className={customClassName}>
+              {originNode}
+            </div>
+          )
+        }}
+      />
+    )
+
+    await waitFor(() => {
+      expect($$(`.${customClassName}`)[0]).toBeVisible()
+    })
+  })
 })
diff --git a/src/tests/mock-global-styles.ts b/src/tests/mock-global-styles.ts
index 533e55f01..230cb8a0d 100644
--- a/src/tests/mock-global-styles.ts
+++ b/src/tests/mock-global-styles.ts
@@ -11,6 +11,10 @@ const mockStyleHtml = `
   .adm-picker-view-column-wheel {
     --item-height: 34px;
   }
+
+  .@{class-prefix-picker-view}-item-height-measure {
+    height: 34px;
+  }
 </style>
 `