feat(json-schema): ms-json-schema组件-细节交互&预览
This commit is contained in:
parent
cc26849807
commit
ce391aac3a
|
@ -603,7 +603,7 @@
|
|||
showDrag: true,
|
||||
columnSelectorDisabled: true,
|
||||
addLineDisabled: true,
|
||||
typeOptions: [
|
||||
options: [
|
||||
{ label: 'object', value: 'object' },
|
||||
{ label: 'array', value: 'array' },
|
||||
{ label: 'string', value: 'string' },
|
||||
|
|
|
@ -828,7 +828,7 @@
|
|||
* @param fullJson 脑图导出的完整数据
|
||||
* @param callback 保存成功回调
|
||||
*/
|
||||
async function handleMinderSave(fullJson: MinderJson, callback: () => void) {
|
||||
async function handleMinderSave(fullJson: MinderJson, callback: (refersh: boolean) => void) {
|
||||
try {
|
||||
loading.value = true;
|
||||
await saveCaseMinder(makeMinderParams(fullJson));
|
||||
|
@ -836,7 +836,7 @@
|
|||
Message.success(t('common.saveSuccess'));
|
||||
resetMinderParams();
|
||||
emit('save');
|
||||
callback();
|
||||
callback(true);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-popover position="tl" :disabled="!modelValue || modelValue.trim() === ''" class="ms-params-input-popover">
|
||||
<template #content>
|
||||
<div v-if="props.title" class="ms-params-popover-title">
|
||||
{{ props.title }}
|
||||
</div>
|
||||
<div class="ms-params-popover-value">
|
||||
{{ modelValue }}
|
||||
{{ props.popoverTitle || modelValue }}
|
||||
</div>
|
||||
</template>
|
||||
<a-input
|
||||
|
@ -54,8 +55,9 @@
|
|||
:placeholder="props.placeholder"
|
||||
:auto-size="{ minRows: 2 }"
|
||||
:max-length="1000"
|
||||
></a-textarea>
|
||||
/>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -67,6 +69,7 @@
|
|||
defineProps<{
|
||||
type?: 'input' | 'textarea';
|
||||
title?: string;
|
||||
popoverTitle?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
size?: 'small' | 'large' | 'medium' | 'mini';
|
||||
|
@ -85,7 +88,6 @@
|
|||
const emit = defineEmits<{
|
||||
(e: 'input', val: string): void;
|
||||
(e: 'change', val: string): void;
|
||||
(e: 'dblclick'): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
|
|
@ -305,6 +305,7 @@
|
|||
theme: currentTheme.value,
|
||||
lineNumbersMinChars: 3,
|
||||
lineDecorationsWidth: 0,
|
||||
tabSize: 2,
|
||||
});
|
||||
|
||||
editor.getModel()?.setEOL(monaco.editor.EndOfLineSequence.LF); // 设置换行符
|
||||
|
|
|
@ -59,8 +59,9 @@
|
|||
<div>*</div>
|
||||
</MsButton>
|
||||
</a-tooltip>
|
||||
<div v-if="item.isNull && item.isNull(record)" class="ms-form-table-td-text">-</div>
|
||||
<a-input
|
||||
v-if="item.inputType === 'input'"
|
||||
v-else-if="item.inputType === 'input'"
|
||||
v-model:model-value="record[item.dataIndex as string]"
|
||||
:placeholder="t(item.locale)"
|
||||
class="ms-form-table-input"
|
||||
|
@ -71,7 +72,7 @@
|
|||
<a-select
|
||||
v-else-if="item.inputType === 'select'"
|
||||
v-model:model-value="record[item.dataIndex as string]"
|
||||
:options="item.typeOptions || []"
|
||||
:options="item.options || []"
|
||||
class="ms-form-table-input w-full"
|
||||
:size="item.size || 'medium'"
|
||||
@change="() => handleFormChange(record, rowIndex, item)"
|
||||
|
@ -249,6 +250,7 @@
|
|||
step?: number;
|
||||
precision?: number;
|
||||
valueFormat?: (record: Record<string, any>) => string; // 展示值格式化,仅在inputType为text时生效
|
||||
isNull?: (record: Record<string, any>) => boolean; // 需要判断是否为空,为空展示‘-’,不展示表单或文本
|
||||
[key: string]: any; // 扩展属性
|
||||
}
|
||||
|
||||
|
@ -512,6 +514,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.ms-form-table-td-text {
|
||||
padding: 0 8px;
|
||||
}
|
||||
.arco-table-col-fixed-right {
|
||||
.arco-table-cell {
|
||||
padding: 0 8px !important;
|
||||
|
|
|
@ -16,15 +16,19 @@
|
|||
:scroll="{ x: 'max-content' }"
|
||||
show-setting
|
||||
class="ms-json-schema"
|
||||
@select="handleSelect"
|
||||
@select="
|
||||
(rowKeys: (string | number)[], rowKey: string | number, record: Record<string, any>) =>
|
||||
handleSelect(rowKeys, rowKey, record as JsonSchemaTableItem)
|
||||
"
|
||||
>
|
||||
<template #batchAddTitle>
|
||||
<MsButton type="text" size="mini" class="!mr-0" @click="batchAdd">
|
||||
{{ t('apiTestDebug.batchAdd') }}
|
||||
</MsButton>
|
||||
</template>
|
||||
<template #title="{ record, columnConfig }">
|
||||
<span v-if="record.title === 'root'" class="px-[8px]">root</span>
|
||||
<template #title="{ record, columnConfig, rowIndex }">
|
||||
<span v-if="record.id === 'root'" class="px-[8px]">root</span>
|
||||
<span v-else-if="record.parent?.type === 'array'" class="px-[8px]">{{ rowIndex }}</span>
|
||||
<a-popover
|
||||
v-else
|
||||
position="tl"
|
||||
|
@ -73,8 +77,9 @@
|
|||
@change="handleTypeChange(record)"
|
||||
/>
|
||||
</template>
|
||||
<template #value="{ record }">
|
||||
<MsParamsInput v-model:value="record.value" size="medium" @dblclick="() => quickInputParams(record)" />
|
||||
<template #example="{ record }">
|
||||
<div v-if="['object', 'array', 'null'].includes(record.type)" class="ms-form-table-td-text">-</div>
|
||||
<MsParamsInput v-else v-model:value="record.example" size="medium" @dblclick="() => quickInputParams(record)" />
|
||||
</template>
|
||||
<template #minLength="{ record }">
|
||||
<a-input-number
|
||||
|
@ -85,7 +90,7 @@
|
|||
:precision="0"
|
||||
size="medium"
|
||||
/>
|
||||
<div v-else class="ms-json-schema-td-text">-</div>
|
||||
<div v-else class="ms-form-table-td-text">-</div>
|
||||
</template>
|
||||
<template #maxLength="{ record }">
|
||||
<a-input-number
|
||||
|
@ -96,7 +101,7 @@
|
|||
:precision="0"
|
||||
size="medium"
|
||||
/>
|
||||
<div v-else class="ms-json-schema-td-text">-</div>
|
||||
<div v-else class="ms-form-table-td-text">-</div>
|
||||
</template>
|
||||
<template #minimum="{ record }">
|
||||
<a-input-number
|
||||
|
@ -113,7 +118,7 @@
|
|||
:step="1"
|
||||
:precision="0"
|
||||
/>
|
||||
<div v-else class="ms-json-schema-td-text">-</div>
|
||||
<div v-else class="ms-form-table-td-text">-</div>
|
||||
</template>
|
||||
<template #maximum="{ record }">
|
||||
<a-input-number
|
||||
|
@ -130,7 +135,31 @@
|
|||
:step="1"
|
||||
:precision="0"
|
||||
/>
|
||||
<div v-else class="ms-json-schema-td-text">-</div>
|
||||
<div v-else class="ms-form-table-td-text">-</div>
|
||||
</template>
|
||||
<template #minItems="{ record }">
|
||||
<a-input-number
|
||||
v-if="record.type === 'array'"
|
||||
v-model:model-value="record.minItems"
|
||||
class="ms-form-table-input-number"
|
||||
size="medium"
|
||||
:min="0"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
/>
|
||||
<div v-else class="ms-form-table-td-text">-</div>
|
||||
</template>
|
||||
<template #maxItems="{ record }">
|
||||
<a-input-number
|
||||
v-if="record.type === 'array'"
|
||||
v-model:model-value="record.maxItems"
|
||||
class="ms-form-table-input-number"
|
||||
size="medium"
|
||||
:min="0"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
/>
|
||||
<div v-else class="ms-form-table-td-text">-</div>
|
||||
</template>
|
||||
<template #defaultValue="{ record }">
|
||||
<a-input-number
|
||||
|
@ -163,7 +192,7 @@
|
|||
},
|
||||
]"
|
||||
/>
|
||||
<div v-else-if="['object', 'array', 'null'].includes(record.type)" class="ms-json-schema-td-text"> - </div>
|
||||
<div v-else-if="['object', 'array', 'null'].includes(record.type)" class="ms-form-table-td-text"> - </div>
|
||||
<a-input
|
||||
v-else
|
||||
v-model:model-value="record.defaultValue"
|
||||
|
@ -171,6 +200,18 @@
|
|||
class="ms-form-table-input"
|
||||
/>
|
||||
</template>
|
||||
<template #enumValues="{ record }">
|
||||
<div v-if="['object', 'array', 'null', 'boolean'].includes(record.type)" class="ms-form-table-td-text">-</div>
|
||||
<MsQuickInput
|
||||
v-else
|
||||
v-model:model-value="record.enumValues"
|
||||
:title="t('ms.json.schema.enum')"
|
||||
:popover-title="JSON.stringify(record.enumValues.split('\n'))"
|
||||
class="ms-form-table-input"
|
||||
type="textarea"
|
||||
>
|
||||
</MsQuickInput>
|
||||
</template>
|
||||
<template #action="{ record, rowIndex }">
|
||||
<div class="flex w-full items-center gap-[8px]">
|
||||
<a-tooltip :content="t('common.advancedSettings')">
|
||||
|
@ -233,14 +274,19 @@
|
|||
asterisk-position="end"
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="activeRecord.name"
|
||||
v-model:model-value="activeRecord.title"
|
||||
:max-length="255"
|
||||
:placeholder="t('common.pleaseInput')"
|
||||
:disabled="activeRecord.id === 'root'"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('common.desc')">
|
||||
<a-textarea v-model:model-value="activeRecord.description" :placeholder="t('common.pleaseInput')" />
|
||||
<a-textarea
|
||||
v-model:model-value="activeRecord.description"
|
||||
:placeholder="t('common.pleaseInput')"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<template v-if="!['object', 'array', 'null'].includes(activeRecord.type)">
|
||||
<div class="flex items-center justify-between gap-[24px]">
|
||||
|
@ -252,6 +298,7 @@
|
|||
:min="0"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('ms.json.schema.maxLength')" class="w-[144px]">
|
||||
|
@ -261,6 +308,7 @@
|
|||
:min="0"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
@ -272,8 +320,14 @@
|
|||
mode="button"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
<a-input-number
|
||||
v-else
|
||||
v-model:model-value="activeRecord.minimum"
|
||||
mode="button"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
<a-input-number v-else v-model:model-value="activeRecord.minimum" mode="button" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('ms.json.schema.maximum')" class="w-[144px]">
|
||||
<a-input-number
|
||||
|
@ -282,8 +336,14 @@
|
|||
mode="button"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
<a-input-number
|
||||
v-else
|
||||
v-model:model-value="activeRecord.maximum"
|
||||
mode="button"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
<a-input-number v-else v-model:model-value="activeRecord.maximum" mode="button" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item :label="t('ms.json.schema.default')" class="flex-1">
|
||||
|
@ -292,6 +352,7 @@
|
|||
v-model:model-value="activeRecord.defaultValue"
|
||||
mode="button"
|
||||
:placeholder="t('common.pleaseInput')"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
<a-input-number
|
||||
v-else-if="activeRecord.type === 'integer'"
|
||||
|
@ -300,33 +361,71 @@
|
|||
:placeholder="t('common.pleaseInput')"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
<a-input
|
||||
v-else
|
||||
v-model:model-value="activeRecord.defaultValue"
|
||||
:placeholder="t('common.pleaseInput')"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
<a-input v-else v-model:model-value="activeRecord.defaultValue" :placeholder="t('common.pleaseInput')" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
<template v-if="activeRecord.type !== 'boolean'">
|
||||
<a-form-item :label="t('ms.json.schema.enum')">
|
||||
<a-textarea v-model:model-value="activeRecord.enum" :placeholder="t('ms.json.schema.enumPlaceholder')" />
|
||||
<a-textarea
|
||||
v-model:model-value="activeRecord.enumValues"
|
||||
:placeholder="t('ms.json.schema.enumPlaceholder')"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('ms.json.schema.regex')">
|
||||
<a-input v-model:model-value="activeRecord.regex" :placeholder="t('ms.json.schema.regexPlaceholder')" />
|
||||
<a-input
|
||||
v-model:model-value="activeRecord.regex"
|
||||
:placeholder="t('ms.json.schema.regexPlaceholder', { reg: '/<title(.*?)</title>' })"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('ms.json.schema.format')">
|
||||
<a-select
|
||||
v-model:model-value="activeRecord.format"
|
||||
:placeholder="t('common.pleaseSelect')"
|
||||
:options="formatOptions"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="activeRecord.type === 'array'" class="flex items-center gap-[24px]">
|
||||
<a-form-item :label="t('ms.json.schema.minItems')" class="w-[144px]">
|
||||
<a-input-number
|
||||
v-model:model-value="activeRecord.minItems"
|
||||
mode="button"
|
||||
:min="0"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('ms.json.schema.maxItems')" class="w-[144px]">
|
||||
<a-input-number
|
||||
v-model:model-value="activeRecord.maxItems"
|
||||
mode="button"
|
||||
:min="0"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
@change="handleSettingFormChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-[8px]">{{ t('ms.json.schema.preview') }}</div>
|
||||
<MsCodeEditor
|
||||
v-model:model-value="activePreviewValue"
|
||||
theme="vs"
|
||||
height="300px"
|
||||
height="500px"
|
||||
:show-full-screen="false"
|
||||
:language="LanguageEnum.JSON"
|
||||
read-only
|
||||
>
|
||||
</MsCodeEditor>
|
||||
|
@ -362,6 +461,16 @@
|
|||
</template>
|
||||
</MsCodeEditor>
|
||||
</MsDrawer>
|
||||
<MsDrawer v-model:visible="previewDrawerVisible" :width="600" :title="t('common.preview')" :footer="false">
|
||||
<MsCodeEditor
|
||||
v-model:model-value="activePreviewValue"
|
||||
theme="vs"
|
||||
height="100%"
|
||||
:language="LanguageEnum.JSON"
|
||||
:show-full-screen="false"
|
||||
read-only
|
||||
/>
|
||||
</MsDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -374,21 +483,23 @@
|
|||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
import MsFormTable, { FormTableColumn } from '@/components/pure/ms-form-table/index.vue';
|
||||
import MsParamsInput from '@/components/business/ms-params-input/index.vue';
|
||||
import MsQuickInput from '@/components/business/ms-quick-input/index.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { getGenerateId, traverseTree } from '@/utils';
|
||||
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
||||
import { JsonSchemaItem } from './types';
|
||||
import { JsonSchemaTableItem } from './types';
|
||||
import { convertToJsonSchema } from './utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const defaultItem: JsonSchemaItem = {
|
||||
const defaultItem: JsonSchemaTableItem = {
|
||||
id: '',
|
||||
type: 'string',
|
||||
title: '',
|
||||
value: '',
|
||||
example: '',
|
||||
description: '',
|
||||
enable: true,
|
||||
defaultValue: '',
|
||||
|
@ -401,13 +512,13 @@
|
|||
format: undefined,
|
||||
children: undefined,
|
||||
};
|
||||
const data = defineModel<JsonSchemaItem[]>('data', {
|
||||
const data = defineModel<JsonSchemaTableItem[]>('data', {
|
||||
default: () => [
|
||||
{
|
||||
id: 'root',
|
||||
title: 'root',
|
||||
type: 'object',
|
||||
value: '',
|
||||
example: '',
|
||||
description: '',
|
||||
enable: true,
|
||||
defaultValue: '',
|
||||
|
@ -508,8 +619,8 @@
|
|||
},
|
||||
{
|
||||
title: t('ms.json.schema.value'),
|
||||
dataIndex: 'value',
|
||||
slotName: 'value',
|
||||
dataIndex: 'example',
|
||||
slotName: 'example',
|
||||
addLineDisabled: true,
|
||||
columnSelectorDisabled: true,
|
||||
},
|
||||
|
@ -565,6 +676,30 @@
|
|||
showInTable: false,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: t('ms.json.schema.minItems'),
|
||||
dataIndex: 'minItems',
|
||||
slotName: 'minItems',
|
||||
inputType: 'inputNumber',
|
||||
size: 'medium',
|
||||
min: 0,
|
||||
precision: 0,
|
||||
addLineDisabled: true,
|
||||
showInTable: false,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: t('ms.json.schema.maxItems'),
|
||||
dataIndex: 'maxItems',
|
||||
slotName: 'maxItems',
|
||||
inputType: 'inputNumber',
|
||||
size: 'medium',
|
||||
min: 0,
|
||||
precision: 0,
|
||||
addLineDisabled: true,
|
||||
showInTable: false,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: t('ms.json.schema.default'),
|
||||
dataIndex: 'defaultValue',
|
||||
|
@ -576,8 +711,8 @@
|
|||
},
|
||||
{
|
||||
title: t('ms.json.schema.enum'),
|
||||
dataIndex: 'enum',
|
||||
slotName: 'enum',
|
||||
dataIndex: 'enumValues',
|
||||
slotName: 'enumValues',
|
||||
inputType: 'textarea',
|
||||
size: 'medium',
|
||||
addLineDisabled: true,
|
||||
|
@ -591,6 +726,7 @@
|
|||
size: 'medium',
|
||||
addLineDisabled: true,
|
||||
showInTable: false,
|
||||
isNull: (record) => ['object', 'array', 'null', 'boolean'].includes(record.type),
|
||||
},
|
||||
{
|
||||
title: t('ms.json.schema.format'),
|
||||
|
@ -601,6 +737,7 @@
|
|||
options: formatOptions,
|
||||
addLineDisabled: true,
|
||||
showInTable: false,
|
||||
isNull: (record) => ['object', 'array', 'null', 'boolean'].includes(record.type),
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
|
@ -617,8 +754,8 @@
|
|||
/**
|
||||
* 获取类型选项,根节点只能是 object 或 array
|
||||
*/
|
||||
function getTypeOptions(record: Record<string, any>) {
|
||||
if (record.name === 'root') {
|
||||
function getTypeOptions(record: JsonSchemaTableItem) {
|
||||
if (record.id === 'root') {
|
||||
return typeOptions.filter((item) => ['object', 'array'].includes(item.value as string));
|
||||
}
|
||||
return typeOptions;
|
||||
|
@ -627,7 +764,7 @@
|
|||
/**
|
||||
* 处理类型变化
|
||||
*/
|
||||
function handleTypeChange(record: Record<string, any>) {
|
||||
function handleTypeChange(record: JsonSchemaTableItem) {
|
||||
if (record.type === 'object' || record.type === 'array') {
|
||||
if (!record.children) {
|
||||
// 没有子节点,初始化
|
||||
|
@ -641,7 +778,7 @@
|
|||
/**
|
||||
* 添加子节点
|
||||
*/
|
||||
function addChild(record: Record<string, any>) {
|
||||
function addChild(record: JsonSchemaTableItem) {
|
||||
if (!record.children) {
|
||||
record.children = [];
|
||||
}
|
||||
|
@ -664,8 +801,8 @@
|
|||
/**
|
||||
* 删除行
|
||||
*/
|
||||
function deleteLine(record: Record<string, any>, rowIndex: number) {
|
||||
if (record.parent) {
|
||||
function deleteLine(record: JsonSchemaTableItem, rowIndex: number) {
|
||||
if (record.parent?.children) {
|
||||
record.parent.children.splice(rowIndex, 1);
|
||||
} else {
|
||||
data.value.splice(rowIndex, 1);
|
||||
|
@ -675,11 +812,11 @@
|
|||
/**
|
||||
* 行选择处理
|
||||
*/
|
||||
function handleSelect(rowKeys: (string | number)[], rowKey: string | number, record: Record<string, any>) {
|
||||
function handleSelect(rowKeys: (string | number)[], rowKey: string | number, record: JsonSchemaTableItem) {
|
||||
nextTick(() => {
|
||||
if (record.enable && record.children && record.children.length > 0) {
|
||||
// 选中父节点时,选中子孙节点
|
||||
traverseTree(record.children, (item: Record<string, any>) => {
|
||||
traverseTree<JsonSchemaTableItem>(record.children, (item) => {
|
||||
item.enable = true;
|
||||
if (!selectedKeys.value.includes(item.id)) {
|
||||
selectedKeys.value.push(item.id);
|
||||
|
@ -704,25 +841,39 @@
|
|||
const activeRecord = ref<any>({});
|
||||
const activePreviewValue = ref('');
|
||||
|
||||
function openSetting(record: Record<string, any>) {
|
||||
function openSetting(record: JsonSchemaTableItem) {
|
||||
// 浅拷贝,以保留 parent 和 children 的引用
|
||||
activeRecord.value = {
|
||||
...record,
|
||||
};
|
||||
try {
|
||||
activePreviewValue.value = JSON.stringify(convertToJsonSchema(record, record.id === 'root'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
activePreviewValue.value = t('ms.json.schema.convertFailed');
|
||||
} finally {
|
||||
settingDrawerVisible.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSettingFormChange() {
|
||||
activePreviewValue.value = JSON.stringify(
|
||||
convertToJsonSchema(activeRecord.value, activeRecord.value.id === 'root')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用设置
|
||||
*/
|
||||
function applySetting() {
|
||||
if (activeRecord.value.id === 'root') {
|
||||
data.value[0] = activeRecord.value;
|
||||
data.value = [{ ...activeRecord.value }];
|
||||
} else {
|
||||
const brothers = activeRecord.value.parent?.children || [];
|
||||
const index = brothers.findIndex((item: any) => item.id === activeRecord.value.id);
|
||||
if (index > -1) {
|
||||
brothers.splice(index, 1, activeRecord.value);
|
||||
brothers.splice(index, 1, { ...activeRecord.value });
|
||||
}
|
||||
}
|
||||
settingDrawerVisible.value = false;
|
||||
|
@ -748,13 +899,20 @@
|
|||
showQuickInputParam.value = false;
|
||||
clearQuickInputParam();
|
||||
}
|
||||
|
||||
const previewDrawerVisible = ref(false);
|
||||
function previewSchema() {
|
||||
previewDrawerVisible.value = true;
|
||||
activePreviewValue.value = JSON.stringify(convertToJsonSchema(data.value[0]));
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
previewSchema,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.ms-json-schema {
|
||||
.ms-json-schema-td-text {
|
||||
padding: 0 8px;
|
||||
}
|
||||
.ms-json-schema-icon-button {
|
||||
@apply !mr-0;
|
||||
&:hover {
|
||||
|
|
|
@ -13,9 +13,12 @@ export default {
|
|||
'ms.json.schema.enum': '枚举值',
|
||||
'ms.json.schema.enumPlaceholder': '1 行 1 个枚举值',
|
||||
'ms.json.schema.regex': '正则表达式',
|
||||
'ms.json.schema.regexPlaceholder': '如 /<title(.*?)</title>',
|
||||
'ms.json.schema.regexPlaceholder': '如 {reg}',
|
||||
'ms.json.schema.format': '格式化',
|
||||
'ms.json.schema.preview': '预览',
|
||||
'ms.json.schema.batchAdd': '批量添加',
|
||||
'ms.json.schema.batchAddTip': '书写格式:"键":"值",如"nama":"natural"',
|
||||
'ms.json.schema.convertFailed': '数据转换失败,请重试',
|
||||
'ms.json.schema.minItems': '最小元素数量',
|
||||
'ms.json.schema.maxItems': '最大元素数量',
|
||||
};
|
||||
|
|
|
@ -1,22 +1,38 @@
|
|||
export interface JsonSchemaItem {
|
||||
export interface JsonSchemaCommon {
|
||||
id: string;
|
||||
title: string; // 参数名称
|
||||
type: string; // 参数类型
|
||||
description: string; // 参数描述
|
||||
enable: boolean; // 是否启用
|
||||
value: string; // 参数值
|
||||
example: string; // 参数值
|
||||
defaultValue: string | number | boolean; // 默认值
|
||||
example?: Record<string, any>;
|
||||
items?: string; // 子级,当 type 为array 时,使用该值
|
||||
properties?: Record<string, any>; // 子级,当 type 为object 时,使用该值
|
||||
required?: string[]; // 必填参数 这里的值是参数的title
|
||||
pattern?: string; // 正则表达式
|
||||
maxLength?: number;
|
||||
minLength?: number;
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
minItems?: number;
|
||||
maxItems?: number;
|
||||
format?: string; // 格式化
|
||||
enumValues?: string; // 参数值的枚举
|
||||
// 前端渲染字段
|
||||
children?: JsonSchemaItem[];
|
||||
}
|
||||
// json-schema 表格组件的表格项
|
||||
export interface JsonSchemaTableItem extends JsonSchemaCommon {
|
||||
required?: string[]; // 是否必填
|
||||
children?: JsonSchemaTableItem[];
|
||||
parent?: JsonSchemaTableItem; // 父级
|
||||
enumValues?: string; // 参数值的枚举
|
||||
}
|
||||
// json-schema 规范的结构子项(表格组件转换后的结构)
|
||||
export interface JsonSchemaItem extends JsonSchemaCommon {
|
||||
items?: JsonSchemaItem[]; // 子级,当 type 为array 时,使用该值
|
||||
properties?: Record<string, JsonSchemaItem>; // 子级,当 type 为object 时,使用该值
|
||||
required?: string[]; // 必填的字段名
|
||||
enumValues?: string[]; // 参数值的枚举
|
||||
}
|
||||
// json-schema 规范的结构(表格组件转换后的结构)
|
||||
export interface JsonSchema {
|
||||
type: string;
|
||||
properties?: Record<string, JsonSchemaItem>;
|
||||
items?: JsonSchemaItem[];
|
||||
required?: string[];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import type { JsonSchema, JsonSchemaItem, JsonSchemaTableItem } from './types';
|
||||
|
||||
/**
|
||||
* 将 json-schema 表格组件的表格项转换为 json-schema 规范的结构
|
||||
* @param schemaItem 表格组件项
|
||||
* @param isRoot 是否为根节点
|
||||
*/
|
||||
export function convertToJsonSchema(
|
||||
schemaItem: JsonSchemaTableItem,
|
||||
isRoot: boolean = true
|
||||
): JsonSchema | JsonSchemaItem {
|
||||
let schema: JsonSchema | JsonSchemaItem = { type: schemaItem.type };
|
||||
|
||||
// 对于 null 类型,只设置 type 和 enable 属性
|
||||
if (schemaItem.type === 'null') {
|
||||
return {
|
||||
type: 'null',
|
||||
enable: schemaItem.enable,
|
||||
};
|
||||
}
|
||||
|
||||
if (!isRoot) {
|
||||
// 使用解构赋值和剩余参数来拷贝对象,同时排除 children、required、parent、id、title 属性
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { children, required, parent, id, title, ...copiedObject } = schemaItem;
|
||||
// 使用\n分割enumValues字符串,得到枚举值数组
|
||||
const enumArray = (copiedObject.enumValues?.split('\n') || []).filter((value) => value.trim() !== '');
|
||||
schema = {
|
||||
...schema,
|
||||
...copiedObject,
|
||||
enumValues: enumArray.length > 0 ? enumArray : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (schemaItem.children && schemaItem.children.length > 0) {
|
||||
if (schemaItem.type === 'object') {
|
||||
schema = {
|
||||
type: 'object',
|
||||
enable: schemaItem.enable,
|
||||
properties: {},
|
||||
required: [],
|
||||
};
|
||||
schemaItem.children.forEach((child) => {
|
||||
const childSchema = convertToJsonSchema(child, false);
|
||||
schema.properties![child.title] = childSchema as JsonSchemaItem;
|
||||
if (child.required) {
|
||||
schema.required!.push(child.title);
|
||||
}
|
||||
});
|
||||
if (schema.required!.length === 0) {
|
||||
delete schema.required;
|
||||
}
|
||||
} else if (schemaItem.type === 'array') {
|
||||
schema = {
|
||||
type: 'array',
|
||||
enable: schemaItem.enable,
|
||||
items: schemaItem.children.map((child) => convertToJsonSchema(child, false) as JsonSchemaItem),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
export default {};
|
|
@ -203,7 +203,7 @@
|
|||
data = cloneDeep(fullJson);
|
||||
importJson.value = fullJson;
|
||||
}
|
||||
emit('save', data, () => {
|
||||
emit('save', data, (refresh = false) => {
|
||||
importJson.value.root.children = mapTree<MinderJsonNode>(
|
||||
importJson.value.root.children || [],
|
||||
(node, path, level) => ({
|
||||
|
@ -216,12 +216,14 @@
|
|||
},
|
||||
})
|
||||
);
|
||||
// if (innerImportJson.value.treePath?.length > 1) {
|
||||
// switchNode(innerImportJson.value.root.data);
|
||||
// } else {
|
||||
// innerImportJson.value = importJson.value;
|
||||
// window.minder.importJson(importJson.value);
|
||||
// }
|
||||
if (refresh) {
|
||||
if (innerImportJson.value.treePath?.length > 1) {
|
||||
switchNode(innerImportJson.value.root.data);
|
||||
} else {
|
||||
innerImportJson.value = importJson.value;
|
||||
window.minder.importJson(importJson.value);
|
||||
}
|
||||
}
|
||||
minderStore.setMinderUnsaved(false);
|
||||
floatMenuVisible.value = false;
|
||||
});
|
||||
|
|
|
@ -7,6 +7,60 @@ import { Recordable } from '#/global';
|
|||
export default function useLocalForage() {
|
||||
const appStore = useAppStore();
|
||||
|
||||
/**
|
||||
* 检测并序列化函数
|
||||
* @param val 要存储的值
|
||||
*/
|
||||
const serializeValue = (val: any): any => {
|
||||
if (typeof val === 'function') {
|
||||
return `function:${val.toString()}`;
|
||||
}
|
||||
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
||||
const newVal = { ...val };
|
||||
Object.keys(newVal).forEach((key) => {
|
||||
newVal[key] = serializeValue(newVal[key]);
|
||||
});
|
||||
return newVal;
|
||||
}
|
||||
if (Array.isArray(val)) {
|
||||
return val.map((item) => serializeValue(item));
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
const deserializeFunction = (funcStr: string) => {
|
||||
try {
|
||||
// eslint-disable-next-line no-eval
|
||||
const func = eval(`${funcStr}`);
|
||||
return func;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 反序列化值,将特殊格式字符串转换回函数
|
||||
* @param val 从存储中读取的值
|
||||
*/
|
||||
const deserializeValue = <T>(val: any): T | null => {
|
||||
if (typeof val === 'string' && val.startsWith('function:')) {
|
||||
return deserializeFunction(val.slice(9)) as T;
|
||||
}
|
||||
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
||||
const newVal = { ...val };
|
||||
Object.keys(newVal).forEach((key) => {
|
||||
newVal[key] = deserializeValue(newVal[key]);
|
||||
});
|
||||
return newVal as T;
|
||||
}
|
||||
if (Array.isArray(val)) {
|
||||
return val.map((item) => deserializeValue(item) as T) as T;
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
/**
|
||||
* 读取本地存储的数据
|
||||
* @param key 唯一 key
|
||||
|
@ -19,7 +73,7 @@ export default function useLocalForage() {
|
|||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
return res;
|
||||
return deserializeValue<T>(res);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
|
@ -36,7 +90,7 @@ export default function useLocalForage() {
|
|||
const setItem = async (key: string, val: string | number | boolean | Recordable, notIsolatedByProject = false) => {
|
||||
try {
|
||||
const itemKey = notIsolatedByProject ? key : `${appStore.currentProjectId}-${key}`;
|
||||
await localforage.setItem(itemKey, val);
|
||||
await localforage.setItem(itemKey, serializeValue(val));
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
|
|
|
@ -835,7 +835,7 @@ if (!result){
|
|||
title: 'apiTestDebug.paramType',
|
||||
dataIndex: 'variableType',
|
||||
slotName: 'variableType',
|
||||
typeOptions: extractTypeOptions.map((item) => {
|
||||
options: extractTypeOptions.map((item) => {
|
||||
return {
|
||||
label: t(item.label),
|
||||
value: item.value,
|
||||
|
@ -847,7 +847,7 @@ if (!result){
|
|||
title: 'apiTestDebug.mode',
|
||||
dataIndex: 'extractType',
|
||||
slotName: 'extractType',
|
||||
typeOptions: [
|
||||
options: [
|
||||
{
|
||||
label: 'JSONPath',
|
||||
value: RequestExtractExpressionEnum.JSON_PATH,
|
||||
|
@ -867,7 +867,7 @@ if (!result){
|
|||
title: 'apiTestDebug.range',
|
||||
dataIndex: 'extractScope',
|
||||
slotName: 'extractScope',
|
||||
typeOptions: [
|
||||
options: [
|
||||
{
|
||||
label: 'Body',
|
||||
value: RequestExtractScope.BODY,
|
||||
|
|
|
@ -167,7 +167,7 @@
|
|||
<a-select
|
||||
v-model:model-value="record.paramType"
|
||||
:disabled="props.disabledExceptParam"
|
||||
:options="columnConfig.typeOptions || []"
|
||||
:options="columnConfig.options || []"
|
||||
class="ms-form-table-input w-full"
|
||||
@change="(val) => handleTypeChange(val, record, rowIndex, columnConfig.addLineDisabled)"
|
||||
/>
|
||||
|
@ -177,7 +177,7 @@
|
|||
<a-select
|
||||
v-model:model-value="record.extractType"
|
||||
:disabled="props.disabledExceptParam"
|
||||
:options="columnConfig.typeOptions || []"
|
||||
:options="columnConfig.options || []"
|
||||
class="ms-form-table-input w-[110px]"
|
||||
@change="() => addTableLine(rowIndex)"
|
||||
/>
|
||||
|
@ -187,7 +187,7 @@
|
|||
<a-select
|
||||
v-model:model-value="record.variableType"
|
||||
:disabled="props.disabledExceptParam"
|
||||
:options="columnConfig.typeOptions || []"
|
||||
:options="columnConfig.options || []"
|
||||
class="ms-form-table-input w-[110px]"
|
||||
@change="() => addTableLine(rowIndex)"
|
||||
/>
|
||||
|
@ -197,7 +197,7 @@
|
|||
<a-select
|
||||
v-model:model-value="record.extractScope"
|
||||
:disabled="props.disabledExceptParam || record.extractType !== RequestExtractExpressionEnum.REGEX"
|
||||
:options="columnConfig.typeOptions || []"
|
||||
:options="columnConfig.options || []"
|
||||
class="ms-form-table-input w-[180px]"
|
||||
@change="() => addTableLine(rowIndex)"
|
||||
/>
|
||||
|
@ -211,7 +211,7 @@
|
|||
<a-select
|
||||
v-model:model-value="record.scope"
|
||||
:disabled="props.disabledExceptParam"
|
||||
:options="columnConfig.typeOptions || []"
|
||||
:options="columnConfig.options || []"
|
||||
class="ms-form-table-input w-[180px]"
|
||||
@change="(val) => handleScopeChange(val, record, rowIndex, columnConfig.addLineDisabled)"
|
||||
/>
|
||||
|
@ -625,7 +625,7 @@
|
|||
isAutoComplete?: boolean; // 用于 key 列区分是否是请求/响应头联想输入
|
||||
isNormal?: boolean; // 用于 value 列区分是普通输入框还是 MsParamsInput
|
||||
hasRequired?: boolean; // 用于 type 列区分是否有 required 星号
|
||||
typeOptions?: { label: string; value: string }[]; // 用于 type 列选择器选项
|
||||
options?: { label: string; value: string }[]; // 用于 type 列选择器选项
|
||||
typeTitleTooltip?: string | string[]; // 用于 type 表头列展示的 tooltip
|
||||
hasDisable?: boolean; // 用于 operation 列区分是否有 enable 开关
|
||||
moreAction?: ActionsItem[]; // 用于 operation 列更多操作按钮配置
|
||||
|
@ -847,7 +847,7 @@
|
|||
// 最后一行的更改才会触发添加新一行
|
||||
const id = getGenerateId();
|
||||
const lastLineData = paramsData.value[rowIndex]; // 上一行数据
|
||||
const selectColumnKeys = props.columns.filter((e) => e.typeOptions).map((e) => e.dataIndex); // 找到下拉框选项的列
|
||||
const selectColumnKeys = props.columns.filter((e) => e.options).map((e) => e.dataIndex); // 找到下拉框选项的列
|
||||
const nextLine = {
|
||||
id,
|
||||
enable: true, // 是否勾选
|
||||
|
|
|
@ -86,7 +86,8 @@
|
|||
</div> -->
|
||||
</div>
|
||||
<div v-else class="h-[calc(100%-34px)]">
|
||||
<div class="mb-[8px] flex items-center gap-[8px]">
|
||||
<div class="mb-[8px] flex items-center justify-between">
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<MsButton
|
||||
type="text"
|
||||
class="!mr-0"
|
||||
|
@ -103,6 +104,19 @@
|
|||
>Json</MsButton
|
||||
>
|
||||
</div>
|
||||
<a-button
|
||||
v-show="jsonType === 'Schema'"
|
||||
type="outline"
|
||||
class="arco-btn-outline--secondary px-[8px]"
|
||||
size="small"
|
||||
@click="previewJsonSchema"
|
||||
>
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<icon-eye />
|
||||
{{ t('common.preview') }}
|
||||
</div>
|
||||
</a-button>
|
||||
</div>
|
||||
<MsCodeEditor
|
||||
v-if="jsonType === 'Json'"
|
||||
v-model:model-value="currentBodyCode"
|
||||
|
@ -116,7 +130,7 @@
|
|||
is-adaptive
|
||||
>
|
||||
</MsCodeEditor>
|
||||
<MsJsonSchema v-else />
|
||||
<MsJsonSchema v-else ref="jsonSchemaRef" />
|
||||
</div>
|
||||
<batchAddKeyVal
|
||||
v-if="showParamTable"
|
||||
|
@ -222,7 +236,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
const typeOptions = computed(() => {
|
||||
const options = computed(() => {
|
||||
const fullOptions = Object.values(RequestParamsType).map((val) => ({
|
||||
label: val,
|
||||
value: val,
|
||||
|
@ -246,7 +260,7 @@
|
|||
dataIndex: 'paramType',
|
||||
slotName: 'paramType',
|
||||
hasRequired: true,
|
||||
typeOptions: typeOptions.value,
|
||||
options: options.value,
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
|
@ -307,6 +321,14 @@
|
|||
});
|
||||
|
||||
const jsonType = ref<'Schema' | 'Json'>('Schema');
|
||||
const jsonSchemaRef = ref<InstanceType<typeof MsJsonSchema>>();
|
||||
|
||||
function previewJsonSchema() {
|
||||
if (jsonType.value === 'Schema') {
|
||||
jsonSchemaRef.value?.previewSchema();
|
||||
}
|
||||
}
|
||||
|
||||
// 当前显示的代码
|
||||
const currentBodyCode = computed({
|
||||
get() {
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
dataIndex: 'paramType',
|
||||
slotName: 'paramType',
|
||||
hasRequired: true,
|
||||
typeOptions: Object.values(RequestParamsType)
|
||||
options: Object.values(RequestParamsType)
|
||||
.filter((val) => ![RequestParamsType.JSON, RequestParamsType.FILE].includes(val))
|
||||
.map((val) => ({
|
||||
label: val,
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
dataIndex: 'paramType',
|
||||
slotName: 'paramType',
|
||||
hasRequired: true,
|
||||
typeOptions: Object.values(RequestParamsType)
|
||||
options: Object.values(RequestParamsType)
|
||||
.filter((val) => ![RequestParamsType.JSON, RequestParamsType.FILE].includes(val as RequestParamsType))
|
||||
.map((val) => ({
|
||||
label: val,
|
||||
|
|
|
@ -146,7 +146,7 @@
|
|||
title: 'apiScenario.params.csvScoped',
|
||||
dataIndex: 'scope',
|
||||
slotName: 'scope',
|
||||
typeOptions: [
|
||||
options: [
|
||||
{
|
||||
label: t('apiScenario.scenario'),
|
||||
value: 'SCENARIO',
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
title: 'apiScenario.params.type',
|
||||
dataIndex: 'paramType',
|
||||
slotName: 'paramType',
|
||||
typeOptions: [
|
||||
options: [
|
||||
{
|
||||
label: t('common.constant'),
|
||||
value: 'CONSTANT',
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
showDrag: true,
|
||||
hasRequired: false,
|
||||
columnSelectorDisabled: true,
|
||||
typeOptions: [
|
||||
options: [
|
||||
{
|
||||
label: t('common.constant'),
|
||||
value: 'CONSTANT',
|
||||
|
|
Loading…
Reference in New Issue