提交 ac1a3695 编写于 作者: V vben

perf(form): improve the form function

上级 4ff1c408
......@@ -8,6 +8,9 @@
- 新增主框架外页面示例
- `route.meta` 新增`currentActiveMenu`,`hideTab`,`hideMenu`参数 用于控制详情页面包屑级菜单显示隐藏。
- 新增面包屑导航示例
- form: 新增`suffix`属性,用于配置后缀内容
- form: 新增远程下拉`ApiSelect`及示例
- form: 新增`autoFocusFirstItem`配置。用于配置是否聚焦表单第一个输入框
### 🐛 Bug Fixes
......
import { MockMethod } from 'vite-plugin-mock';
import { resultSuccess } from '../_util';
const demoList = (() => {
const result: any[] = [];
for (let index = 0; index < 20; index++) {
result.push({
label: `选项${index}`,
value: `${index}`,
});
}
return result;
})();
export default [
{
url: '/api/select/getDemoOptions',
timeout: 4000,
method: 'get',
response: ({ query }) => {
return resultSuccess(demoList);
},
},
] as MockMethod[];
import { BasicFetchResult } from '/@/api/model/baseModel';
export interface DemoOptionsItem {
label: string;
value: string;
}
/**
* @description: Request list return value
*/
export type DemoOptionsGetResultModel = BasicFetchResult<DemoOptionsItem[]>;
import { defHttp } from '/@/utils/http/axios';
import { DemoOptionsGetResultModel } from './model/optionsModel';
enum Api {
OPTIONS_LIST = '/select/getDemoOptions',
}
/**
* @description: Get sample options value
*/
export function optionsListApi() {
return defHttp.request<DemoOptionsGetResultModel>({
url: Api.OPTIONS_LIST,
method: 'GET',
});
}
<template>
<Form v-bind="{ ...$attrs, ...$props }" ref="formElRef" :model="formModel">
<Row :class="getProps.compact ? 'compact-form-row' : ''" :style="getRowWrapStyle">
<Form v-bind="{ ...$attrs, ...$props }" :class="getFormClass" ref="formElRef" :model="formModel">
<Row :style="getRowWrapStyle">
<slot name="formHeader" />
<template v-for="schema in getSchema" :key="schema.field">
<FormItem
......@@ -18,7 +18,6 @@
</FormItem>
</template>
<!-- -->
<FormAction
v-bind="{ ...getProps, ...advanceState }"
@toggle-advanced="handleToggleAdvanced"
......@@ -46,8 +45,10 @@
import useAdvanced from './hooks/useAdvanced';
import { useFormEvents } from './hooks/useFormEvents';
import { createFormContext } from './hooks/useFormContext';
import { useAutoFocus } from './hooks/useAutoFocus';
import { basicProps } from './props';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({
name: 'BasicForm',
......@@ -71,6 +72,8 @@
const schemaRef = ref<Nullable<FormSchema[]>>(null);
const formElRef = ref<Nullable<FormActionType>>(null);
const { prefixCls } = useDesign('basic-form');
// Get the basic configuration of the form
const getProps = computed(
(): FormProps => {
......@@ -78,6 +81,15 @@
}
);
const getFormClass = computed(() => {
return [
prefixCls,
{
[`${prefixCls}--compact`]: unref(getProps).compact,
},
];
});
// Get uniform row style
const getRowWrapStyle = computed(
(): CSSProperties => {
......@@ -115,7 +127,7 @@
defaultValueRef,
});
const { transformDateFunc, fieldMapToTime } = toRefs(props);
const { transformDateFunc, fieldMapToTime, autoFocusFirstItem } = toRefs(props);
const { handleFormValues, initDefault } = useFormValues({
transformDateFuncRef: transformDateFunc,
......@@ -125,6 +137,13 @@
formModel,
});
useAutoFocus({
getSchema,
autoFocusFirstItem,
isInitedDefault: isInitedDefaultRef,
formElRef: formElRef as Ref<FormActionType>,
});
const {
handleSubmit,
setFieldsValue,
......@@ -217,8 +236,51 @@
getSchema,
formActionType,
setFormModel,
prefixCls,
getFormClass,
...formActionType,
};
},
});
</script>
<style lang="less">
@import (reference) '../../../design/index.less';
@prefix-cls: ~'@{namespace}-basic-form';
.@{prefix-cls} {
.ant-form-item {
&-label label::after {
margin: 0 6px 0 2px;
}
&-with-help {
margin-bottom: 0;
}
&:not(.ant-form-item-with-help) {
margin-bottom: 20px;
}
&.suffix-item {
.ant-form-item-children {
display: flex;
}
.suffix {
display: inline-block;
padding-left: 6px;
}
}
}
.ant-form-explain {
font-size: 14px;
}
&--compact {
.ant-form-item {
margin-bottom: 8px;
}
}
}
</style>
......@@ -19,6 +19,7 @@ import {
} from 'ant-design-vue';
import RadioButtonGroup from './components/RadioButtonGroup.vue';
import ApiSelect from './components/ApiSelect.vue';
import { BasicUpload } from '/@/components/Upload';
const componentMap = new Map<ComponentType, Component>();
......@@ -32,6 +33,7 @@ componentMap.set('InputNumber', InputNumber);
componentMap.set('AutoComplete', AutoComplete);
componentMap.set('Select', Select);
componentMap.set('ApiSelect', ApiSelect);
// componentMap.set('SelectOptGroup', Select.OptGroup);
// componentMap.set('SelectOption', Select.Option);
componentMap.set('TreeSelect', TreeSelect);
......
<template>
<Select v-bind="attrs" :options="options" v-model:value="state">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data" />
</template>
<template #suffixIcon v-if="loading">
<LoadingOutlined spin />
</template>
<template #notFoundContent v-if="loading">
<span>
<LoadingOutlined spin class="mr-1" />
{{ t('component.form.apiSelectNotFound') }}
</span>
</template>
</Select>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watchEffect } from 'vue';
import { Select } from 'ant-design-vue';
import { isFunction } from '/@/utils/is';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { get } from 'lodash-es';
import { LoadingOutlined } from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n';
type OptionsItem = { label: string; value: string; disabled?: boolean };
export default defineComponent({
name: 'RadioButtonGroup',
components: {
Select,
LoadingOutlined,
},
props: {
value: {
type: String as PropType<string>,
},
api: {
type: Function as PropType<(arg: Recordable) => Promise<OptionsItem[]>>,
default: null,
},
params: {
type: Object as PropType<Recordable>,
default: () => {},
},
resultField: {
type: String as PropType<string>,
default: '',
},
},
setup(props) {
const options = ref<OptionsItem[]>([]);
const loading = ref(false);
const attrs = useAttrs();
const { t } = useI18n();
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props);
watchEffect(() => {
fetch();
});
async function fetch() {
const api = props.api;
if (!api || !isFunction(api)) return;
try {
loading.value = true;
const res = await api(props.params);
if (Array.isArray(res)) {
options.value = res;
return;
}
if (props.resultField) {
options.value = get(res, props.resultField) || [];
}
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
}
return { state, attrs, options, loading, t };
},
});
</script>
......@@ -3,7 +3,6 @@ import type { FormActionType, FormProps } from '../types/form';
import type { FormSchema } from '../types/form';
import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
import type { TableActionType } from '/@/components/Table';
import type { ComponentType } from '../types';
import { defineComponent, computed, unref, toRefs } from 'vue';
import { Form, Col } from 'ant-design-vue';
......@@ -16,7 +15,6 @@ import { createPlaceholderMessage, setComponentRuleType } from '../helper';
import { upperFirst, cloneDeep } from 'lodash-es';
import { useItemLabelWidth } from '../hooks/useLabelWidth';
import { isNumber } from '/@/utils/is';
import { useI18n } from '/@/hooks/web/useI18n';
export default defineComponent({
......@@ -81,7 +79,7 @@ export default defineComponent({
if (!isFunction(componentProps)) {
return componentProps;
}
return componentProps({ schema, tableAction, formModel, formActionType }) || {};
return componentProps({ schema, tableAction, formModel, formActionType }) ?? {};
});
const getDisable = computed(() => {
......@@ -99,7 +97,7 @@ export default defineComponent({
return disabled;
});
function getShow() {
const getShow = computed(() => {
const { show, ifShow } = props.schema;
const { showAdvancedButton } = props.formProps;
const itemIsAdvanced = showAdvancedButton
......@@ -124,7 +122,7 @@ export default defineComponent({
}
isShow = isShow && itemIsAdvanced;
return { isShow, isIfShow };
}
});
function handleRules(): ValidationRule[] {
const {
......@@ -171,7 +169,7 @@ export default defineComponent({
}
}
// 最大输入长度规则校验
// Maximum input length rule check
const characterInx = rules.findIndex((val) => val.max);
if (characterInx !== -1 && !rules[characterInx].validator) {
rules[characterInx].message =
......@@ -180,20 +178,6 @@ export default defineComponent({
return rules;
}
function handleValue(component: ComponentType, field: string) {
const val = props.formModel[field];
if (['Input', 'InputPassword', 'InputSearch', 'InputTextArea'].includes(component)) {
if (val && isNumber(val)) {
props.setFormModel(field, `${val}`);
// props.formModel[field] = `${val}`;
return `${val}`;
}
return val;
}
return val;
}
function renderComponent() {
const {
renderComponentContent,
......@@ -217,7 +201,6 @@ export default defineComponent({
const value = target ? (isCheck ? target.checked : target.value) : e;
props.setFormModel(field, value);
// props.formModel[field] = value;
},
};
const Comp = componentMap.get(component) as typeof defineComponent;
......@@ -233,7 +216,7 @@ export default defineComponent({
const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder;
let placeholder;
// RangePicker place为数组
// RangePicker place is an array
if (isCreatePlaceholder && component !== 'RangePicker' && component) {
placeholder = unref(getComponentsProps)?.placeholder || createPlaceholderMessage(component);
}
......@@ -242,7 +225,7 @@ export default defineComponent({
propsData.formValues = unref(getValues);
const bindValue: Recordable = {
[valueField || (isCheck ? 'checked' : 'value')]: handleValue(component, field),
[valueField || (isCheck ? 'checked' : 'value')]: props.formModel[field],
};
const compAttr: Recordable = {
......@@ -284,7 +267,7 @@ export default defineComponent({
}
function renderItem() {
const { itemProps, slot, render, field } = props.schema;
const { itemProps, slot, render, field, suffix } = props.schema;
const { labelCol, wrapperCol } = unref(itemLabelWidthProp);
const { colon } = props.formProps;
......@@ -296,17 +279,27 @@ export default defineComponent({
: renderComponent();
};
const showSuffix = !!suffix;
const getSuffix = isFunction(suffix) ? suffix(unref(getValues)) : suffix;
return (
<Form.Item
name={field}
colon={colon}
class={{ 'suffix-item': showSuffix }}
{...(itemProps as Recordable)}
label={renderLabelHelpMessage()}
rules={handleRules()}
labelCol={labelCol}
wrapperCol={wrapperCol}
>
{() => getContent()}
{() => (
<>
{getContent()}
{showSuffix && <span class="suffix">{getSuffix}</span>}
</>
)}
</Form.Item>
);
}
......@@ -317,7 +310,7 @@ export default defineComponent({
const { baseColProps = {} } = props.formProps;
const realColProps = { ...baseColProps, ...colProps };
const { isIfShow, isShow } = getShow();
const { isIfShow, isShow } = unref(getShow);
const getContent = () => {
return colSlot
......
import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
import type { ComponentType } from './types/index';
import { useI18n } from '/@/hooks/web/useI18n';
import { isNumber } from '/@/utils/is';
const { t } = useI18n();
......@@ -41,6 +42,14 @@ export function setComponentRuleType(rule: ValidationRule, component: ComponentT
}
}
export function handleInputNumberValue(component?: ComponentType, val: any) {
if (!component) return val;
if (['Input', 'InputPassword', 'InputSearch', 'InputTextArea'].includes(component)) {
return val && isNumber(val) ? `${val}` : val;
}
return val;
}
/**
* 时间字段
*/
......
import type { ColEx } from '../types';
import type { AdvanceState } from '../types/hooks';
import { ComputedRef, Ref } from 'vue';
import type { ComputedRef, Ref } from 'vue';
import type { FormProps, FormSchema } from '../types/form';
import { computed, unref, watch } from 'vue';
......
import type { ComputedRef, Ref } from 'vue';
import type { FormSchema, FormActionType } from '../types/form';
import { unref, nextTick, watchEffect } from 'vue';
interface UseAutoFocusContext {
getSchema: ComputedRef<FormSchema[]>;
autoFocusFirstItem: Ref<boolean>;
isInitedDefault: Ref<boolean>;
formElRef: Ref<FormActionType>;
}
export async function useAutoFocus({
getSchema,
autoFocusFirstItem,
formElRef,
isInitedDefault,
}: UseAutoFocusContext) {
watchEffect(async () => {
if (unref(isInitedDefault) || !unref(autoFocusFirstItem)) return;
await nextTick();
const schemas = unref(getSchema);
const formEl = unref(formElRef);
const el = (formEl as any)?.$el as HTMLElement;
if (!formEl || !el || !schemas || schemas.length === 0) return;
const firstItem = schemas[0];
// Only open when the first form item is input type
if (!firstItem.component.includes('Input')) return;
const inputEl = el.querySelector('.ant-row:first-child input') as Nullable<HTMLInputElement>;
if (!inputEl) return;
inputEl?.focus();
});
}
......@@ -6,7 +6,7 @@ import { unref, toRaw } from 'vue';
import { isArray, isFunction, isObject, isString } from '/@/utils/is';
import { deepMerge, unique } from '/@/utils';
import { dateItemType } from '../helper';
import { dateItemType, handleInputNumberValue } from '../helper';
import moment from 'moment';
import { cloneDeep } from 'lodash-es';
import { error } from '/@/utils/log';
......@@ -49,29 +49,32 @@ export function useFormEvents({
/**
* @description: Set form value
*/
async function setFieldsValue(values: any): Promise<void> {
async function setFieldsValue(values: Recordable): Promise<void> {
const fields = unref(getSchema)
.map((item) => item.field)
.filter(Boolean);
const validKeys: string[] = [];
Object.keys(values).forEach((key) => {
const element = values[key];
const schema = unref(getSchema).find((item) => item.field === key);
let value = values[key];
value = handleInputNumberValue(schema?.component, value);
// 0| '' is allow
if (element !== undefined && element !== null && fields.includes(key)) {
if (value !== undefined && value !== null && fields.includes(key)) {
// time type
if (itemIsDateType(key)) {
if (Array.isArray(element)) {
const arr: any[] = [];
for (const ele of element) {
if (Array.isArray(value)) {
const arr: moment.Moment[] = [];
for (const ele of value) {
arr.push(moment(ele));
}
formModel[key] = arr;
} else {
formModel[key] = moment(element);
formModel[key] = moment(value);
}
} else {
formModel[key] = element;
formModel[key] = value;
}
validKeys.push(key);
}
......
......@@ -65,6 +65,8 @@ export const basicProps = {
actionColOptions: Object as PropType<Partial<ColEx>>,
// 显示重置按钮
showResetButton: propTypes.bool.def(true),
// 是否聚焦第一个输入框,只在第一个表单项为input的时候作用
autoFocusFirstItem: propTypes.bool,
// 重置按钮配置
resetButtonOptions: Object as PropType<Partial<ButtonProps>>,
......
......@@ -82,6 +82,8 @@ export interface FormProps {
rulesMessageJoinLabel?: boolean;
// Whether to show collapse and expand buttons
showAdvancedButton?: boolean;
// Whether to focus on the first input box, only works when the first form item is input
autoFocusFirstItem?: boolean;
// Automatically collapse over the specified number of rows
autoAdvancedLine?: number;
// Whether to show the operation button
......@@ -139,6 +141,8 @@ export interface FormSchema {
// Required
required?: boolean;
suffix?: string | number | ((values: RenderCallbackParams) => string | number);
// Validation rules
rules?: Rule[];
// Check whether the information is added to the label
......
......@@ -89,6 +89,7 @@ export type ComponentType =
| 'InputNumber'
| 'InputCountDown'
| 'Select'
| 'ApiSelect'
| 'SelectOptGroup'
| 'SelectOption'
| 'TreeSelect'
......
......@@ -48,37 +48,6 @@
color: @primary-color !important;
}
// =================================
// ==============form===============
// =================================
.ant-form-item.deltag .ant-form-item-required::before {
content: '';
}
.ant-form-item-with-help {
margin-bottom: 0;
}
.ant-form-item {
&-label label::after {
margin: 0 6px 0 2px;
}
}
.ant-form-item:not(.ant-form-item-with-help) {
margin-bottom: 20px;
}
.ant-form-explain {
font-size: 14px;
}
.compact-form-row {
.ant-form-item {
margin-bottom: 8px;
}
}
// =================================
// ==============empty==============
// =================================
......
......@@ -8,4 +8,6 @@ export default {
choose: 'Please Choose ',
maxTip: 'The number of characters should be less than {0}',
apiSelectNotFound: 'Wait for data loading to complete...',
};
......@@ -8,4 +8,6 @@ export default {
choose: '请选择',
maxTip: '字符数应小于{0}位',
apiSelectNotFound: '请等待数据加载完成...',
};
......@@ -105,28 +105,29 @@ const transform: AxiosTransform = {
if (apiUrl && isString(apiUrl)) {
config.url = `${apiUrl}${config.url}`;
}
const params = config.params || {};
if (config.method?.toUpperCase() === RequestEnum.GET) {
if (!isString(config.params)) {
if (!isString(params)) {
config.data = {
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
params: Object.assign(config.params || {}, createNow(joinTime, false)),
params: Object.assign(params || {}, createNow(joinTime, false)),
};
} else {
// 兼容restful风格
config.url = config.url + config.params + `${createNow(joinTime, true)}`;
config.url = config.url + params + `${createNow(joinTime, true)}`;
config.params = undefined;
}
} else {
if (!isString(config.params)) {
formatDate && formatRequestDate(config.params);
config.data = config.params;
if (!isString(params)) {
formatDate && formatRequestDate(params);
config.data = params;
config.params = undefined;
if (joinParamsToUrl) {
config.url = setObjToUrlParams(config.url as string, config.data);
}
} else {
// 兼容restful风格
config.url = config.url + config.params;
config.url = config.url + params;
config.params = undefined;
}
}
......
......@@ -170,7 +170,7 @@
}
function setFormValues() {
setFieldsValue({
field1: '1111',
field1: 1111,
field5: ['1'],
field7: '1',
});
......
......@@ -2,6 +2,7 @@
<div class="m-4">
<CollapseContainer title="基础示例">
<BasicForm
autoFocusFirstItem
:labelWidth="100"
:schemas="schemas"
:actionColOptions="{ span: 24 }"
......@@ -16,11 +17,13 @@
import { CollapseContainer } from '/@/components/Container/index';
import { useMessage } from '/@/hooks/web/useMessage';
import { optionsListApi } from '/@/api/demo/select';
const schemas: FormSchema[] = [
{
field: 'field1',
component: 'Input',
label: '字段1',
colProps: {
span: 8,
},
......@@ -46,7 +49,7 @@
{
field: 'field2',
component: 'Input',
label: '字段2',
label: '带后缀',
defaultValue: '111',
colProps: {
span: 8,
......@@ -56,6 +59,7 @@
console.log(e);
},
},
suffix: '',
},
{
field: 'field3',
......@@ -208,6 +212,19 @@
],
},
},
{
field: 'field30',
component: 'ApiSelect',
label: '远程下拉',
required: true,
componentProps: {
api: optionsListApi,
},
colProps: {
span: 8,
},
},
{
field: 'field20',
component: 'InputNumber',
......
......@@ -1076,10 +1076,10 @@
resolved "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.0.0-rc.4.tgz#46098fb544a4eb3af724219e4955c9022801835e"
integrity sha512-YCSECbeXKFJEIVkKgKMjUzJ439ysufmL/a31B1j7dCvnHaBWsX9J4XehhJgg/aTy3yvhHaVhI6xt1kSMZP799A==
"@iconify/json@^1.1.276":
version "1.1.276"
resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.276.tgz#c8d51751abc84cc73a466f55bc2f352686451786"
integrity sha512-Ra/mGT+n38vhi/i1cjsPYOmSR2d6rNIXZ+OsrIWp9J35zAPQ93sSTQMpTyxZdLu3QxU0vYwtcaC7h/Y1/3H3wg==
"@iconify/json@^1.1.277":
version "1.1.277"
resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.277.tgz#e11e01833b05845ce1afc5ad61759804f6ed2eb2"
integrity sha512-66n4lsv57iRwtcb2Q8ax8iasVLzFz9VWcqtgobHVrvyfsVqf8hSldJELnTl/gtqayqa35pT4mHEpdfsqt1mnLA==
"@intlify/core-base@9.0.0-beta.14":
version "9.0.0-beta.14"
......@@ -1831,18 +1831,18 @@
vscode-languageserver-textdocument "^1.0.1"
vscode-uri "^2.1.2"
"@vueuse/core@^4.0.0":
version "4.0.0"
resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.0.tgz#5bea3eaa848e3b3e00427f5053fb98e7e4834b0f"
integrity sha512-BBkqriC2j9SH/LuHCggS2MP7VSwBfGkTB9qQh1lzadodk2TnM1JHwM76f3G0hCGqqhEF7ab8Xs+1M1PlvuEQYA==
"@vueuse/core@^4.0.1":
version "4.0.1"
resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.1.tgz#be90fd09de0264dbe61c571b5967334ca94d8cb2"
integrity sha512-bC6H/ES9aFnzp6rT3W3d5j/CqB8mN1UrvBj1RO639QMwxPbJ5/JDjDD4HHtOdIZfA82d6p2Ijbv4Y04mXmkHng==
dependencies:
"@vueuse/shared" "4.0.0"
"@vueuse/shared" "4.0.1"
vue-demi latest
"@vueuse/shared@4.0.0":
version "4.0.0"
resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.0.tgz#d495b8fd2f28a453ef0fccae175ca848a4a84bb0"
integrity sha512-8tn1BpnaMJU2LqFyFzzN6Dvmc1uDsSlb3Neli5bwwb9f+rcASpuOS3nAWAY6/rIODZP1iwXDNCL4rNFR3YxYtQ==
"@vueuse/shared@4.0.1":
version "4.0.1"
resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.1.tgz#28750d34400cd0cabf2576342c5ee7471b0e27bd"
integrity sha512-7SQ1OqUPiuOSe5OFGIn5NvawZ7mfID5V4AwsHwpMAQn22Ex73az6TFE1N/6fL4rZBx6wLrkPfVO9v7vSsOkvlg==
dependencies:
vue-demi latest
......@@ -8039,10 +8039,10 @@ vary@^1.1.2:
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
vditor@^3.7.3:
version "3.7.3"
resolved "https://registry.npmjs.org/vditor/-/vditor-3.7.3.tgz#6f7bdee7dca758985b29be1533ed952178f0aac4"
integrity sha512-2EHwAc9l+HOo6dcScSJDPmVTsVuEqHK2ucZwAHgvctpua3pMz/CAGMHgPoyB5X1Pju7yrLfsESHZh8V6Ndh6rg==
vditor@^3.7.4:
version "3.7.4"
resolved "https://registry.npmjs.org/vditor/-/vditor-3.7.4.tgz#e2ec46f009e99d4ef1804d4ef355d44be7efb9a3"
integrity sha512-NfpXCoiVEeaORwGPNaxVDQGHs6Sib2RlI+slSFc5eXV8pFfYM639O6iOLjG2Ks+lN7nM9SsmpcGXwnQ0/S90xA==
dependencies:
diff-match-patch "^1.0.5"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册