Merge branch 'dev' into next

This commit is contained in:
chenjiahan 2022-05-22 19:59:35 +08:00
commit 582582e480
41 changed files with 3278 additions and 2992 deletions

View File

@ -21,7 +21,7 @@
"@vant/eslint-config": "workspace:*",
"@vant/stylelint-config": "workspace:*",
"eslint": "^8.2.0",
"husky": "^7.0.4",
"husky": "^8.0.1",
"lint-staged": "^12.1.2",
"prettier": "^2.5.0",
"rimraf": "^3.0.2",

View File

@ -1,5 +1,11 @@
# 更新日志
## v4.0.2
`2022-05-14`
- 修复编译 script setup 错误的问题
## v4.0.1
`2022-03-03`

View File

@ -1,6 +1,6 @@
{
"name": "@vant/cli",
"version": "4.0.1",
"version": "4.0.2",
"type": "module",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
@ -70,7 +70,7 @@
"gh-pages": "^3.2.3",
"hash-sum": "^2.0.0",
"highlight.js": "^11.3.1",
"husky": "^7.0.4",
"husky": "^8.0.1",
"jest": "^27.3.1",
"jest-canvas-mock": "^2.3.1",
"jest-serializer-html": "^7.1.0",

View File

@ -2,7 +2,7 @@ import fse from 'fs-extra';
import babel from '@babel/core';
import esbuild, { type Format } from 'esbuild';
import { sep } from 'path';
import { isJsx, replaceExt } from '../common/index.js';
import { isJsx, replaceExt, getVantConfig } from '../common/index.js';
import { replaceCSSImportExt } from '../common/css.js';
import { replaceScriptImportExt } from './get-deps.js';
@ -50,7 +50,9 @@ export async function compileScript(
({ code } = esbuildResult);
const jsFilePath = replaceExt(filePath, '.js');
const extensionMap = getVantConfig().build?.extensions;
const extension = extensionMap?.[format] || '.js';
const jsFilePath = replaceExt(filePath, extension);
removeSync(filePath);
outputFileSync(jsFilePath, code);
}

View File

@ -1,7 +1,12 @@
import fse from 'fs-extra';
import path from 'path';
import hash from 'hash-sum';
import { parse, SFCBlock, compileTemplate } from 'vue/compiler-sfc';
import {
parse,
SFCBlock,
compileTemplate,
compileScript,
} from 'vue/compiler-sfc';
import { replaceExt } from '../common/index.js';
const { remove, readFileSync, outputFile } = fse;
@ -73,8 +78,9 @@ export async function compileSfc(filePath: string): Promise<any> {
const scopeId = hasScoped ? `data-v-${hash(source)}` : '';
// compile js part
if (descriptor.script) {
const lang = descriptor.script.lang || 'js';
if (descriptor.script || descriptor.scriptSetup) {
const lang =
descriptor.script?.lang || descriptor.scriptSetup?.lang || 'js';
const scriptFilePath = replaceExt(filePath, `.${lang}`);
tasks.push(
@ -86,7 +92,14 @@ export async function compileSfc(filePath: string): Promise<any> {
script += '// @ts-nocheck\n';
}
script += descriptor.script!.content;
if (descriptor.scriptSetup) {
script += compileScript(descriptor, {
id: scopeId,
}).content;
} else {
script += descriptor.script!.content;
}
script = injectStyle(script, styles, filePath);
script = script.replace(EXPORT, `const ${VUEIDS} =`);

View File

@ -14,7 +14,9 @@ export function getViteConfigForPackage({
}): InlineConfig {
setBuildTarget('package');
const { name } = getVantConfig();
const { name, build } = getVantConfig();
const entryExtension = build?.extensions?.esm || '.js';
const entry = join(ES_DIR, `index${entryExtension}`);
return {
root: CWD,
@ -24,7 +26,7 @@ export function getViteConfigForPackage({
build: {
lib: {
name,
entry: join(ES_DIR, 'index.js'),
entry,
formats,
fileName: (format: string) => {
const suffix = format === 'umd' ? '' : `.${format}`;

View File

@ -51,6 +51,7 @@ module.exports = {
'vue/require-v-for-key': 'off',
'vue/require-default-prop': 'off',
'vue/no-unused-components': 'off',
'vue/multi-word-component-names': 'off',
'vue/return-in-computed-property': 'off',
// typescript-eslint
'@typescript-eslint/camelcase': 'off',

View File

@ -6,9 +6,9 @@ exports[`should render content slot correctly 1`] = `
</div>
`;
exports[`should render nothing when content is empty string 1`] = ``;
exports[`should render nothing when content is empty string 1`] = `""`;
exports[`should render nothing when content is undefined 1`] = ``;
exports[`should render nothing when content is undefined 1`] = `""`;
exports[`should render nothing when content is zero 1`] = `
<div class="van-badge van-badge--top-right">

View File

@ -59,6 +59,7 @@ import type {
FieldFormatTrigger,
FieldValidateError,
FieldAutosizeConfig,
FieldValidationStatus,
FieldValidateTrigger,
FieldFormSharedProps,
} from './types';
@ -135,8 +136,8 @@ export default defineComponent({
setup(props, { emit, slots }) {
const id = useId();
const state = reactive({
status: 'unvalidated' as FieldValidationStatus,
focused: false,
validateFailed: false,
validateMessage: '',
});
@ -181,7 +182,7 @@ export default defineComponent({
rules.reduce(
(promise, rule) =>
promise.then(() => {
if (state.validateFailed) {
if (state.status === 'failed') {
return;
}
@ -192,7 +193,7 @@ export default defineComponent({
}
if (!runSyncRule(value, rule)) {
state.validateFailed = true;
state.status = 'failed';
state.validateMessage = getRuleMessage(value, rule);
return;
}
@ -200,10 +201,10 @@ export default defineComponent({
if (rule.validator) {
return runRuleValidator(value, rule).then((result) => {
if (result && typeof result === 'string') {
state.validateFailed = true;
state.status = 'failed';
state.validateMessage = result;
} else if (result === false) {
state.validateFailed = true;
state.status = 'failed';
state.validateMessage = getRuleMessage(value, rule);
}
});
@ -213,10 +214,8 @@ export default defineComponent({
);
const resetValidation = () => {
if (state.validateFailed) {
state.validateFailed = false;
state.validateMessage = '';
}
state.status = 'unvalidated';
state.validateMessage = '';
};
const validate = (rules = props.rules) =>
@ -224,12 +223,13 @@ export default defineComponent({
resetValidation();
if (rules) {
runRules(rules).then(() => {
if (state.validateFailed) {
if (state.status === 'failed') {
resolve({
name: props.name,
message: state.validateMessage,
});
} else {
state.status = 'passed';
resolve();
}
});
@ -351,7 +351,7 @@ export default defineComponent({
if (typeof props.error === 'boolean') {
return props.error;
}
if (form && form.props.showError && state.validateFailed) {
if (form && form.props.showError && state.status === 'failed') {
return true;
}
});
@ -383,6 +383,8 @@ export default defineComponent({
const getInputId = () => props.id || `${id}-input`;
const getValidationStatus = () => state.status;
const renderInput = () => {
const controlClass = bem('control', [
getProp('inputAlign'),
@ -407,7 +409,6 @@ export default defineComponent({
name: props.name,
rows: props.rows !== undefined ? +props.rows : undefined,
class: controlClass,
value: props.modelValue,
disabled: getProp('disabled'),
readonly: getProp('readonly'),
autofocus: props.autofocus,
@ -531,6 +532,7 @@ export default defineComponent({
validate,
formValue,
resetValidation,
getValidationStatus,
});
provide(CUSTOM_FIELD_INJECTION_KEY, {

View File

@ -327,6 +327,7 @@ import type {
FieldValidateError,
FieldAutosizeConfig,
FieldValidateTrigger,
FieldValidationStatus,
} from 'vant';
```

View File

@ -346,6 +346,7 @@ import type {
FieldValidateError,
FieldAutosizeConfig,
FieldValidateTrigger,
FieldValidationStatus,
} from 'vant';
```

View File

@ -17,6 +17,7 @@ export type {
FieldValidateError,
FieldAutosizeConfig,
FieldValidateTrigger,
FieldValidationStatus,
} from './types';
declare module 'vue' {

View File

@ -66,6 +66,8 @@ export type FieldRule = {
formatter?: FiledRuleFormatter;
};
export type FieldValidationStatus = 'passed' | 'failed' | 'unvalidated';
// Shared props of Field and Form
export type FieldFormSharedProps =
| 'colon'
@ -83,6 +85,7 @@ export type FieldExpose = {
rules?: FieldRule[] | undefined
) => Promise<void | FieldValidateError>;
resetValidation: () => void;
getValidationStatus: () => FieldValidationStatus;
/** @private */
formValue: ComputedRef<unknown>;
};

View File

@ -18,6 +18,7 @@ import type {
FieldTextAlign,
FieldValidateError,
FieldValidateTrigger,
FieldValidationStatus,
} from '../field/types';
import type { FormExpose } from './types';
@ -141,6 +142,12 @@ export default defineComponent({
});
};
const getValidationStatus = () =>
children.reduce<Record<string, FieldValidationStatus>>((form, field) => {
form[field.name] = field.getValidationStatus();
return form;
}, {});
const scrollToField = (
name: string,
options?: boolean | ScrollIntoViewOptions
@ -186,6 +193,7 @@ export default defineComponent({
getValues,
scrollToField,
resetValidation,
getValidationStatus,
});
return () => (

View File

@ -538,9 +538,10 @@ Use [ref](https://v3.vuejs.org/guide/component-template-refs.html) to get Form i
| Name | Description | Attribute | Return value |
| --- | --- | --- | --- |
| submit | Submit form | - | - |
| validate | Validate form | _name?: string \| string[]_ | _Promise_ |
| getValues `v3.4.8` | Get current form values | - | _Record<string, unknown>_ |
| validate | Validate form | _name?: string \| string[]_ | _Promise\<void\>_ |
| resetValidation | Reset validation | _name?: string \| string[]_ | - |
| getValidationStatus `v3.5.0` | Get validation status of all fieldsstatus can be `passed``failed``unvalidated` | - | _Record\<string, FieldValidationStatus\>_ |
| scrollToField | Scroll to field | _name: string, alignToTop: boolean_ | - |
### Types

View File

@ -576,9 +576,10 @@ export default {
| 方法名 | 说明 | 参数 | 返回值 |
| --- | --- | --- | --- |
| submit | 提交表单,与点击提交按钮的效果等价 | - | - |
| validate | 验证表单,支持传入 `name` 来验证单个或部分表单项 | _name?: string \| string[]_ | _Promise_ |
| getValues `v3.4.8` | 获取所有表单项当前的值 | - | _Record<string, unknown>_ |
| resetValidation | 重置表单项的验证提示,支持传入 `name` 来重置单个或部分表单项 | _name?: string \| string[]_ | - |
| validate | 验证表单,支持传入一个或多个 `name` 来验证单个或部分表单项,不传入 `name` 时,会验证所有表单项 | _name?: string \| string[]_ | _Promise\<void\>_ |
| resetValidation | 重置表单项的验证提示,支持传入一个或多个 `name` 来重置单个或部分表单项,不传入 `name` 时,会重置所有表单项 | _name?: string \| string[]_ | - |
| getValidationStatus `v3.5.0` | 获取所有表单项的校验状态,状态包括 `passed``failed``unvalidated` | - | _Record\<string, FieldValidationStatus\>_ |
| scrollToField | 滚动到对应表单项的位置,默认滚动到顶部,第二个参数传 false 可滚动至底部 | _name: string, alignToTop: boolean_ | - |
### 类型定义

View File

@ -459,7 +459,7 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>

View File

@ -153,3 +153,37 @@ test('getValues method should return all current values', () => {
expect(formRef.value?.getValues()).toEqual({ A: '123', B: '456' });
});
test('getValidationStatus method should the status of all fields', async () => {
const formRef = ref<FormInstance>();
const rules = getSimpleRules();
mount({
render() {
return (
<Form ref={formRef}>
<Field name="A" rules={rules.rulesA} modelValue="123" />
<Field name="B" rules={rules.rulesB} modelValue="456" />
</Form>
);
},
});
expect(formRef.value?.getValidationStatus()).toEqual({
A: 'unvalidated',
B: 'unvalidated',
});
await formRef.value?.validate();
expect(formRef.value?.getValidationStatus()).toEqual({
A: 'passed',
B: 'passed',
});
formRef.value?.resetValidation();
expect(formRef.value?.getValidationStatus()).toEqual({
A: 'unvalidated',
B: 'unvalidated',
});
});

View File

@ -1,5 +1,6 @@
import type { ComponentPublicInstance } from 'vue';
import type { FormProps } from './Form';
import type { FieldValidationStatus } from '../field';
export type FormExpose = {
submit: () => void;
@ -10,6 +11,7 @@ export type FormExpose = {
options?: boolean | ScrollIntoViewOptions | undefined
) => void;
resetValidation: (name?: string | string[] | undefined) => void;
getValidationStatus: () => Record<string, FieldValidationStatus>;
};
export type FormProvide = {

View File

@ -41,13 +41,16 @@ Current supported languages:
| Language | Filename | Version |
| ------------------------ | ------------ | -------- |
| Bulgarian | bg-BG | `v3.5.0` |
| Bangla (Bangladesh) | bn-BD | `v3.4.5` |
| Danish | da-DK | `v3.4.8` |
| German | de-DE | - |
| German (formal) | de-DE-formal | - |
| Greek | el-GR | `v3.5.0` |
| English | en-US | - |
| Spanish (Spain) | es-ES | - |
| French | fr-FR | - |
| Hebrew | he-IL | `v3.5.0` |
| Hindi | hi-IN | `v3.4.3` |
| Indonesian | id-ID | `v3.4.5` |
| Icelandic | is-IS | `v3.4.7` |

View File

@ -42,13 +42,16 @@ Locale.add(messages);
| 语言 | 文件名 | 版本 |
| -------------------- | ------------ | -------- |
| 保加利亚语 | bg-BG | `v3.5.0` |
| 孟加拉语(孟加拉国) | bn-BD | `v3.4.5` |
| 丹麦语 | da-DK | `v3.4.8` |
| 德语 | de-DE | - |
| 德语(正式) | de-DE-formal | - |
| 希腊语 | el-GR | `v3.5.0` |
| 英语 | en-US | - |
| 西班牙语 | es-ES | - |
| 法语 | fr-FR | - |
| 希伯来语 | he-IL | `v3.5.0` |
| 印地语 | hi-IN | `v3.4.3` |
| 印度尼西亚语 | id-ID | `v3.4.5` |
| 冰岛语 | is-IS | `v3.4.7` |

View File

@ -0,0 +1,71 @@
export default {
name: 'Име',
tel: 'Телефон',
save: 'Запазване',
confirm: 'Потвърди',
cancel: 'Отказ',
delete: 'Изтриване',
loading: 'Зареждане...',
noCoupon: 'Без купони',
nameEmpty: 'Моля, попълнете името',
addContact: 'Добавяне на контакт',
telInvalid: 'Неправилно формиран телефонен номер',
vanCalendar: {
end: 'Край',
start: 'Старт',
title: 'Календар',
weekdays: [
'неделя',
'понеделник',
'вторник',
'сряда',
'четвъртък',
'петък',
'събота',
],
monthTitle: (year: number, month: number) => `${year}/${month}`,
rangePrompt: (maxRange: number) => `Изберете не повече от ${maxRange} дни`,
},
vanCascader: {
select: 'Избор',
},
vanPagination: {
prev: 'Предишна',
next: 'Напред',
},
vanPullRefresh: {
pulling: 'Издърпайте за опресняване...',
loosing: 'Разхлабен за опресняване...',
},
vanSubmitBar: {
label: 'Общо:',
},
vanCoupon: {
unlimited: 'Неограничен',
discount: (discount: number) => `${discount * 10}% отстъпка`,
condition: (condition: number) => `Поне ${condition}`,
},
vanCouponCell: {
title: 'Купон',
count: (count: number) => `Имате ${count} купони`,
},
vanCouponList: {
exchange: 'Размяна',
close: 'Затвори',
enable: 'Налично',
disabled: 'Недостъпно',
placeholder: 'Код на купон',
},
vanAddressEdit: {
area: 'Площ',
postal: 'Пощенски',
areaEmpty: 'Моля, изберете зона за получаване',
addressEmpty: 'Адресът не може да бъде празен',
postalEmpty: 'Грешен пощенски код',
addressDetail: 'Адрес',
defaultAddress: 'Задаване като адрес по подразбиране',
},
vanAddressList: {
add: 'Добавяне на нов адрес',
},
};

View File

@ -0,0 +1,72 @@
export default {
name: 'Όνομα',
tel: 'Τηλέφωνο',
save: 'Αποθήκευση',
confirm: 'Επιβεβαίωση',
cancel: 'Ακύρωση',
delete: 'Διαγραφή',
loading: 'Φόρτωση...',
noCoupon: 'Χωρίς κουπόνια',
nameEmpty: 'Παρακαλώ συμπληρώστε το όνομα',
addContact: 'Προσθήκη επαφής',
telInvalid: 'Αριθμός τηλεφώνου με εσφαλμένη μορφή',
vanCalendar: {
end: 'Τέλος',
start: 'Έναρξη',
title: 'Ημερολόγιο',
weekdays: [
'Κυριακή',
'Δευτέρα',
'Τρίτη',
'Τετάρτη',
'Πέμπτη',
'Παρασκευή',
'Σάββατο',
],
monthTitle: (year: number, month: number) => `${year}/${month}`,
rangePrompt: (maxRange: number) =>
`Επιλέξτε όχι περισσότερες από ${maxRange} ημέρες`,
},
vanCascader: {
select: 'Επιλογή',
},
vanPagination: {
prev: 'Προηγούμενο',
next: 'Επόμενο',
},
vanPullRefresh: {
pulling: 'Τραβήξτε για ανανέωση...',
loosing: 'Χαλαρά για ανανέωση...',
},
vanSubmitBar: {
label: 'Σύνολο:',
},
vanCoupon: {
unlimited: 'Απεριόριστο',
discount: (discount: number) => `${discount * 10}% έκπτωση`,
condition: (condition: number) => `Τουλάχιστον ${condition}`,
},
vanCouponCell: {
title: 'Κουπόνι',
count: (count: number) => `Έχετε ${count} κουπόνια`,
},
vanCouponList: {
exchange: 'Ανταλλαγή',
close: 'Κλείσιμο',
enable: 'Διαθέσιμο',
disabled: 'Μη διαθέσιμο',
placeholder: 'Κωδικός κουπονιού',
},
vanAddressEdit: {
area: 'Περιοχή',
postal: 'Ταχυδρομείο',
areaEmpty: 'Παρακαλώ επιλέξτε μια περιοχή λήψης',
addressEmpty: 'Η διεύθυνση δεν μπορεί να είναι κενή',
postalEmpty: 'Λάθος ταχυδρομικός κώδικας',
addressDetail: 'Διεύθυνση',
defaultAddress: 'Ορισμός ως προεπιλεγμένη διεύθυνση',
},
vanAddressList: {
add: 'Προσθήκη νέας διεύθυνσης',
},
};

View File

@ -0,0 +1,63 @@
export default {
name: 'שם',
tel: 'טלפון',
save: 'שמור',
confirm: 'אישור',
cancel: 'ביטול',
delete: 'מחיקה',
loading: 'טוען...',
noCoupon: 'אין קופונים',
nameEmpty: 'אנא מלא את השדה',
addContact: 'הוסף איש-קשר',
telInvalid: 'מספר טלפון שגוי',
vanCalendar: {
end: 'סוף',
start: 'התחלה',
title: 'לוח שנה',
weekdays: ['ראשון', 'שני', 'שלישי', 'רביעי', 'חמישי', 'שישי', 'שבת'],
monthTitle: (year: number, month: number) => `${year}/${month}`,
rangePrompt: (maxRange: number) => ` בחר לא יותר מ ${maxRange} ימים `,
},
vanCascader: {
select: 'בחר',
},
vanPagination: {
prev: 'הקודם',
next: 'הבא',
},
vanPullRefresh: {
pulling: 'גרור כדי לרענן',
loosing: 'שחרר כדי לרענן',
},
vanSubmitBar: {
label: 'סך הכל:',
},
vanCoupon: {
unlimited: 'ללא הגבלה',
discount: (discount: number) => `${discount * 10}% הנחה`,
condition: (condition: number) => ` לפחות ${condition}`,
},
vanCouponCell: {
title: 'קופון',
count: (count: number) => ` יש לך ${count} קופונים `,
},
vanCouponList: {
exchange: 'החלפה',
close: 'סגירה',
enable: 'זמינים',
disabled: 'לא זמינים',
placeholder: 'קוד קופון',
},
vanAddressEdit: {
area: 'איזור',
postal: 'מיקוד',
areaEmpty: 'אנא בחר איזור קבלה',
addressEmpty: 'יש למלא כתובת',
postalEmpty: 'טעות במיקוד',
addressDetail: 'כתובת',
defaultAddress: 'הגדר ככתובת ברירת מחדש',
},
vanAddressList: {
add: 'הוספת כתובת חדשה',
},
};

View File

@ -238,3 +238,28 @@ test('should not render mask and frame when options is empty', async () => {
expect(wrapper.find('.van-picker__mask').exists()).toBeTruthy();
expect(wrapper.find('.van-picker__frame').exists()).toBeTruthy();
});
test('columns-field-names responsiveness', async () => {
const columnsOne = [
{ type: 1, name: 'Ios' },
{ type: 2, name: 'Android' },
];
const columnsTwo = [
{ type: 1, serverName: 'server1' },
{ type: 2, serverName: 'server2' },
];
const wrapper = mount(Picker, {
props: {
columns: columnsOne,
columnsFieldNames: {
text: 'name',
},
},
});
expect(wrapper.findAll('.van-ellipsis')[0].text()).toEqual('Ios');
await wrapper.setProps({
columns: columnsTwo,
columnsFieldNames: { text: 'serverName' },
});
expect(wrapper.findAll('.van-ellipsis')[0].text()).toEqual('server1');
});

View File

@ -143,8 +143,8 @@ export default {
| show-plus | Whether to show plus button | _boolean_ | `true` |
| show-minus | Whether to show minus button | _boolean_ | `true` |
| show-input | Whether to show input | _boolean_ | `true` |
| long-press | Whether to allow long press | _boolean_ | `true` |
| allow-empty | Whether to allow the input to be empty | _boolean_ | `false` |
| long-press | Whether to enable the long press gesture, when enabled you can long press the increase and decrease buttons | _boolean_ | `true` |
| allow-empty | Whether to allow the input value to be empty, set to `true` to allow an empty string to be passed in | _boolean_ | `false` |
### Events

View File

@ -163,8 +163,8 @@ export default {
| show-plus | 是否显示增加按钮 | _boolean_ | `true` |
| show-minus | 是否显示减少按钮 | _boolean_ | `true` |
| show-input | 是否显示输入框 | _boolean_ | `true` |
| long-press | 是否开启长按手势 | _boolean_ | `true` |
| allow-empty | 是否允许输入的值为空 | _boolean_ | `false` |
| long-press | 是否开启长按手势,开启后可以长按增加和减少按钮 | _boolean_ | `true` |
| allow-empty | 是否允许输入的值为空,设置为 `true` 后允许传入空字符串 | _boolean_ | `false` |
### Events

View File

@ -59,6 +59,37 @@ export default {
<van-switch v-model="checked" active-color="#ee0a24" inactive-color="#dcdee0" />
```
### Custom Node
Using `node` slot to custom the content of the node.
```html
<van-switch v-model="checked">
<div class="icon-wrapper">
<van-icon :name="checked ? 'success' : 'cross'" />
</div>
</van-switch>
<style>
.icon-wrapper {
display: flex;
width: 100%;
justify-content: center;
font-size: 18px;
}
.icon-wrapper .van-icon-success {
line-height: 32px;
color: var(--van-blue);
}
.icon-wrapper .van-icon-cross {
line-height: 32px;
color: var(--van-gray-5);
}
</style>
```
### Async Control
```html
@ -121,6 +152,12 @@ export default {
| change | Emitted when check status changed | _value: any_ |
| click | Emitted when component is clicked | _event: MouseEvent_ |
### Slots
| Name | Description | SlotProps |
| ------------- | -------------------------- | --------- |
| node `v3.5.0` | Custom the content of node | - |
### Types
The component exports the following type definitions:

View File

@ -69,6 +69,37 @@ export default {
<van-switch v-model="checked" active-color="#ee0a24" inactive-color="#dcdee0" />
```
### 自定义按钮
通过 `node` 插槽自定义按钮的内容。
```html
<van-switch v-model="checked">
<div class="icon-wrapper">
<van-icon :name="checked ? 'success' : 'cross'" />
</div>
</van-switch>
<style>
.icon-wrapper {
display: flex;
width: 100%;
justify-content: center;
font-size: 18px;
}
.icon-wrapper .van-icon-success {
line-height: 32px;
color: var(--van-blue);
}
.icon-wrapper .van-icon-cross {
line-height: 32px;
color: var(--van-gray-5);
}
</style>
```
### 异步控制
需要异步控制开关时,可以使用 `modelValue` 属性和 `update:model-value` 事件代替 `v-model`,并在事件回调函数中手动处理开关状态。
@ -133,6 +164,12 @@ export default {
| change | 开关状态切换时触发 | _value: any_ |
| click | 点击时触发 | _event: MouseEvent_ |
### Slots
| 名称 | 说明 | 参数 |
| ------------- | ---------------- | ---- |
| node `v3.5.0` | 自定义按钮的内容 | - |
### 类型定义
组件导出以下类型定义:

View File

@ -31,7 +31,7 @@ export default defineComponent({
emits: ['change', 'update:modelValue'],
setup(props, { emit }) {
setup(props, { emit, slots }) {
const isChecked = () => props.modelValue === props.activeValue;
const onClick = () => {
@ -47,6 +47,9 @@ export default defineComponent({
const color = isChecked() ? props.activeColor : props.inactiveColor;
return <Loading class={bem('loading')} color={color} />;
}
if (slots.node) {
return slots.node();
}
};
useCustomFieldValue(() => props.modelValue);

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import VanSwitch from '..';
import VanCell from '../../cell';
import VanIcon from '../../icon';
import { ref } from 'vue';
import { useTranslate } from '../../../docs/site';
import { Dialog } from '../../dialog';
@ -12,6 +13,7 @@ const t = useTranslate({
message: '是否切换开关?',
withCell: '搭配单元格使用',
customSize: '自定义大小',
customNode: '自定义按钮',
customColor: '自定义颜色',
asyncControl: '异步控制',
},
@ -21,6 +23,7 @@ const t = useTranslate({
message: 'Are you sure to toggle switch?',
withCell: 'Inside a Cell',
customSize: 'Custom Size',
customNode: 'Custom Node',
customColor: 'Custom Color',
asyncControl: 'Async Control',
},
@ -67,6 +70,16 @@ const onUpdateValue = (checked: boolean) => {
/>
</demo-block>
<demo-block :title="t('customNode')">
<van-switch v-model="checked3">
<template #node>
<div class="icon-wrapper">
<van-icon :name="checked3 ? 'success' : 'cross'" />
</div>
</template>
</van-switch>
</demo-block>
<demo-block :title="t('asyncControl')">
<van-switch :model-value="checked4" @update:model-value="onUpdateValue" />
</demo-block>
@ -85,5 +98,24 @@ const onUpdateValue = (checked: boolean) => {
.van-switch {
margin-left: var(--van-padding-md);
}
.icon-wrapper {
display: flex;
width: 100%;
justify-content: center;
font-size: 18px;
.van-icon {
line-height: 32px;
}
.van-icon-success {
color: var(--van-blue);
}
.van-icon-cross {
color: var(--van-gray-5);
}
}
}
</style>

View File

@ -69,6 +69,20 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
</div>
<div>
<div role="switch"
class="van-switch van-switch--on"
tabindex="0"
aria-checked="true"
>
<div class="van-switch__node">
<div class="icon-wrapper">
<i class="van-badge__wrapper van-icon van-icon-success">
</i>
</div>
</div>
</div>
</div>
<div>
<div role="switch"
class="van-switch van-switch--on"

View File

@ -344,7 +344,8 @@ export default {
| Name | Description | SlotProps |
| --- | --- | --- |
| default | Custom icon | - |
| default | Custom upload area | - |
| preview-delete `v.3.5.0` | Custom delete icon | `item: FileListItem` |
| preview-cover | Custom content that covers the image preview | `item: FileListItem` |
### Parameters of before-read、after-read、before-delete

View File

@ -363,9 +363,10 @@ export default {
### Slots
| 名称 | 说明 | 参数 |
| ------------- | ------------------------------ | -------------------- |
| default | 自定义上传区域 | - |
| 名称 | 说明 | 参数 |
| --- | --- | --- |
| default | 自定义上传区域 | - |
| preview-delete `v3.5.0` | 自定义删除按钮 | - |
| preview-cover | 自定义覆盖在预览区域上方的内容 | _item: FileListItem_ |
### 回调参数

View File

@ -280,7 +280,7 @@ export default defineComponent({
return (
<UploaderPreviewItem
v-slots={{ 'preview-cover': slots['preview-cover'] }}
v-slots={pick(slots, ['preview-cover', 'preview-delete'])}
item={item}
index={index}
onClick={() => emit('clickPreview', item, getDetail(index))}

View File

@ -73,15 +73,20 @@ export default defineComponent({
const renderDeleteIcon = () => {
if (props.deletable && props.item.status !== 'uploading') {
const slot = slots['preview-delete'];
return (
<div
role="button"
class={bem('preview-delete')}
class={bem('preview-delete', { shadow: !slot })}
tabindex={0}
aria-label={t('delete')}
onClick={onDelete}
>
<Icon name="cross" class={bem('preview-delete-icon')} />
{slot ? (
slot()
) : (
<Icon name="cross" class={bem('preview-delete-icon')} />
)}
</div>
);
}

View File

@ -106,10 +106,13 @@ body {
position: absolute;
top: 0;
right: 0;
width: var(--van-uploader-delete-icon-size);
height: var(--van-uploader-delete-icon-size);
background: var(--van-uploader-delete-background);
border-radius: 0 0 0 12px;
&--shadow {
width: var(--van-uploader-delete-icon-size);
height: var(--van-uploader-delete-icon-size);
background: var(--van-uploader-delete-background);
border-radius: 0 0 0 12px;
}
&-icon {
position: absolute;

View File

@ -30,7 +30,7 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
@ -50,7 +50,7 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
@ -126,7 +126,7 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
@ -160,7 +160,7 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
@ -195,7 +195,7 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
@ -258,7 +258,7 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
@ -294,7 +294,7 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
@ -359,7 +359,7 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>

View File

@ -17,7 +17,7 @@ exports[`delete preview image 1`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
@ -66,7 +66,7 @@ exports[`disable preview image 2`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
@ -116,68 +116,7 @@ exports[`image-fit prop 1`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
tabindex="0"
aria-label="Delete"
>
<i class="van-badge__wrapper van-icon van-icon-cross van-uploader__preview-delete-icon">
</i>
</div>
</div>
<div class="van-uploader__upload">
<i class="van-badge__wrapper van-icon van-icon-photograph van-uploader__upload-icon">
</i>
<input type="file"
class="van-uploader__input"
accept="image/*"
>
</div>
</div>
</div>
`;
exports[`preview-cover slot 1`] = `
<div class="van-uploader">
<div class="van-uploader__wrapper">
<div class="van-uploader__preview">
<div class="van-image van-uploader__preview-image">
<img src="https://cdn.jsdelivr.net/npm/@vant/assets/cat.jpeg"
class="van-image__img"
style="object-fit: cover;"
>
<div class="van-image__loading">
<i class="van-badge__wrapper van-icon van-icon-photo van-image__loading-icon">
</i>
</div>
<div class="van-uploader__preview-cover">
Custom Preview Cover
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
tabindex="0"
aria-label="Delete"
>
<i class="van-badge__wrapper van-icon van-icon-cross van-uploader__preview-delete-icon">
</i>
</div>
</div>
<div class="van-uploader__preview">
<div class="van-image van-uploader__preview-image">
<img src="https://cdn.jsdelivr.net/npm/@vant/assets/cat.jpeg"
class="van-image__img"
style="object-fit: cover;"
>
<div class="van-image__loading">
<i class="van-badge__wrapper van-icon van-icon-photo van-image__loading-icon">
</i>
</div>
<div class="van-uploader__preview-cover">
Custom Preview Cover
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
@ -212,7 +151,7 @@ exports[`render preview image 1`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
@ -229,7 +168,7 @@ exports[`render preview image 1`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
@ -246,7 +185,7 @@ exports[`render preview image 1`] = `
</div>
</div>
<div role="button"
class="van-uploader__preview-delete"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
@ -277,6 +216,22 @@ exports[`should not render upload input when using readonly prop 1`] = `
</div>
`;
exports[`should render preview-cover slot correctly 1`] = `
<div class="van-uploader__preview-cover">
Custom Preview Cover
</div>
`;
exports[`should render preview-delete slot correctly 1`] = `
<div role="button"
class="van-uploader__preview-delete"
tabindex="0"
aria-label="Delete"
>
Custom Preview Delete
</div>
`;
exports[`upload-icon prop 1`] = `
<div class="van-uploader">
<div class="van-uploader__wrapper">

View File

@ -565,7 +565,7 @@ test('closePreview event', async () => {
});
await later();
wrapper.find('.van-image').trigger('click');
await wrapper.find('.van-image').trigger('click');
const preview = document.querySelector<HTMLDivElement>('.van-image-preview');
const swipe = preview?.querySelector<HTMLDivElement>(
@ -627,17 +627,32 @@ test('multiFile upload filter max-size file', async () => {
expect(wrapper.emitted<[File]>('oversize')![0]).toBeTruthy();
});
test('preview-cover slot', async () => {
test('should render preview-cover slot correctly', async () => {
const wrapper = mount(Uploader, {
props: {
modelValue: [{ url: IMAGE }, { url: IMAGE }],
modelValue: [{ url: IMAGE }],
},
slots: {
'preview-cover': 'Custom Preview Cover',
},
});
expect(wrapper.html()).toMatchSnapshot();
expect(wrapper.find('.van-uploader__preview-cover').html()).toMatchSnapshot();
});
test('should render preview-delete slot correctly', async () => {
const wrapper = mount(Uploader, {
props: {
modelValue: [{ url: IMAGE }],
},
slots: {
'preview-delete': 'Custom Preview Delete',
},
});
expect(
wrapper.find('.van-uploader__preview-delete').html()
).toMatchSnapshot();
});
test('should not render upload input when using readonly prop', async () => {

5604
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff