feat(json-schema): ms-json-schema组件-细节交互&预览

This commit is contained in:
baiqi 2024-07-02 17:56:34 +08:00 committed by 刘瑞斌
parent cc26849807
commit ce391aac3a
19 changed files with 484 additions and 157 deletions

View File

@ -603,7 +603,7 @@
showDrag: true, showDrag: true,
columnSelectorDisabled: true, columnSelectorDisabled: true,
addLineDisabled: true, addLineDisabled: true,
typeOptions: [ options: [
{ label: 'object', value: 'object' }, { label: 'object', value: 'object' },
{ label: 'array', value: 'array' }, { label: 'array', value: 'array' },
{ label: 'string', value: 'string' }, { label: 'string', value: 'string' },

View File

@ -828,7 +828,7 @@
* @param fullJson 脑图导出的完整数据 * @param fullJson 脑图导出的完整数据
* @param callback 保存成功回调 * @param callback 保存成功回调
*/ */
async function handleMinderSave(fullJson: MinderJson, callback: () => void) { async function handleMinderSave(fullJson: MinderJson, callback: (refersh: boolean) => void) {
try { try {
loading.value = true; loading.value = true;
await saveCaseMinder(makeMinderParams(fullJson)); await saveCaseMinder(makeMinderParams(fullJson));
@ -836,7 +836,7 @@
Message.success(t('common.saveSuccess')); Message.success(t('common.saveSuccess'));
resetMinderParams(); resetMinderParams();
emit('save'); emit('save');
callback(); callback(true);
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);

View File

@ -1,61 +1,63 @@
<template> <template>
<a-popover position="tl" :disabled="!modelValue || modelValue.trim() === ''" class="ms-params-input-popover"> <div>
<template #content> <a-popover position="tl" :disabled="!modelValue || modelValue.trim() === ''" class="ms-params-input-popover">
<div v-if="props.title" class="ms-params-popover-title"> <template #content>
{{ props.title }} <div v-if="props.title" class="ms-params-popover-title">
</div> {{ props.title }}
<div class="ms-params-popover-value"> </div>
{{ modelValue }} <div class="ms-params-popover-value">
</div> {{ props.popoverTitle || modelValue }}
</template> </div>
<a-input </template>
v-if="props.type === 'input'" <a-input
ref="inputRef" v-if="props.type === 'input'"
v-model:model-value="modelValue" ref="inputRef"
:class="props.class" v-model:model-value="modelValue"
:disabled="props.disabled" :class="props.class"
:size="props.size" :disabled="props.disabled"
:max-length="props.maxLength" :size="props.size"
:placeholder="props.placeholder" :max-length="props.maxLength"
:trigger-props="{ contentClass: 'ms-form-table-input-trigger' }" :placeholder="props.placeholder"
:allow-clear="props.allowClear" :trigger-props="{ contentClass: 'ms-form-table-input-trigger' }"
@input="(val) => emit('input', val)" :allow-clear="props.allowClear"
@change="(val) => emit('change', val)" @input="(val) => emit('input', val)"
/> @change="(val) => emit('change', val)"
<a-textarea />
v-else <a-textarea
ref="inputRef" v-else
v-model:model-value="modelValue" ref="inputRef"
:class="props.class" v-model:model-value="modelValue"
:disabled="props.disabled" :class="props.class"
:size="props.size" :disabled="props.disabled"
:placeholder="props.placeholder" :size="props.size"
:max-length="props.maxLength" :placeholder="props.placeholder"
:auto-size="{ minRows: 1, maxRows: 1 }" :max-length="props.maxLength"
:allow-clear="props.allowClear" :auto-size="{ minRows: 1, maxRows: 1 }"
@input="(val) => emit('input', val)" :allow-clear="props.allowClear"
@change="(val) => emit('change', val)" @input="(val) => emit('input', val)"
/> @change="(val) => emit('change', val)"
</a-popover> />
<a-modal </a-popover>
v-model:visible="showQuickInput" <a-modal
:title="props.title" v-model:visible="showQuickInput"
:ok-text="t('common.save')" :title="props.title"
:ok-button-props="{ disabled: !quickInputValue || quickInputValue.trim() === '' }" :ok-text="t('common.save')"
class="ms-modal-form" :ok-button-props="{ disabled: !quickInputValue || quickInputValue.trim() === '' }"
body-class="!p-0" class="ms-modal-form"
:width="480" body-class="!p-0"
title-align="start" :width="480"
@ok="applyQuickInputDesc" title-align="start"
@close="clearQuickInputDesc" @ok="applyQuickInputDesc"
> @close="clearQuickInputDesc"
<a-textarea >
v-model:model-value="quickInputValue" <a-textarea
:placeholder="props.placeholder" v-model:model-value="quickInputValue"
:auto-size="{ minRows: 2 }" :placeholder="props.placeholder"
:max-length="1000" :auto-size="{ minRows: 2 }"
></a-textarea> :max-length="1000"
</a-modal> />
</a-modal>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -67,6 +69,7 @@
defineProps<{ defineProps<{
type?: 'input' | 'textarea'; type?: 'input' | 'textarea';
title?: string; title?: string;
popoverTitle?: string;
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
size?: 'small' | 'large' | 'medium' | 'mini'; size?: 'small' | 'large' | 'medium' | 'mini';
@ -85,7 +88,6 @@
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'input', val: string): void; (e: 'input', val: string): void;
(e: 'change', val: string): void; (e: 'change', val: string): void;
(e: 'dblclick'): void;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();

View File

@ -305,6 +305,7 @@
theme: currentTheme.value, theme: currentTheme.value,
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
lineDecorationsWidth: 0, lineDecorationsWidth: 0,
tabSize: 2,
}); });
editor.getModel()?.setEOL(monaco.editor.EndOfLineSequence.LF); // editor.getModel()?.setEOL(monaco.editor.EndOfLineSequence.LF); //

View File

@ -59,8 +59,9 @@
<div>*</div> <div>*</div>
</MsButton> </MsButton>
</a-tooltip> </a-tooltip>
<div v-if="item.isNull && item.isNull(record)" class="ms-form-table-td-text">-</div>
<a-input <a-input
v-if="item.inputType === 'input'" v-else-if="item.inputType === 'input'"
v-model:model-value="record[item.dataIndex as string]" v-model:model-value="record[item.dataIndex as string]"
:placeholder="t(item.locale)" :placeholder="t(item.locale)"
class="ms-form-table-input" class="ms-form-table-input"
@ -71,7 +72,7 @@
<a-select <a-select
v-else-if="item.inputType === 'select'" v-else-if="item.inputType === 'select'"
v-model:model-value="record[item.dataIndex as string]" v-model:model-value="record[item.dataIndex as string]"
:options="item.typeOptions || []" :options="item.options || []"
class="ms-form-table-input w-full" class="ms-form-table-input w-full"
:size="item.size || 'medium'" :size="item.size || 'medium'"
@change="() => handleFormChange(record, rowIndex, item)" @change="() => handleFormChange(record, rowIndex, item)"
@ -249,6 +250,7 @@
step?: number; step?: number;
precision?: number; precision?: number;
valueFormat?: (record: Record<string, any>) => string; // inputTypetext valueFormat?: (record: Record<string, any>) => string; // inputTypetext
isNull?: (record: Record<string, any>) => boolean; // -
[key: string]: any; // [key: string]: any; //
} }
@ -512,6 +514,9 @@
} }
} }
} }
.ms-form-table-td-text {
padding: 0 8px;
}
.arco-table-col-fixed-right { .arco-table-col-fixed-right {
.arco-table-cell { .arco-table-cell {
padding: 0 8px !important; padding: 0 8px !important;

View File

@ -16,15 +16,19 @@
:scroll="{ x: 'max-content' }" :scroll="{ x: 'max-content' }"
show-setting show-setting
class="ms-json-schema" 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> <template #batchAddTitle>
<MsButton type="text" size="mini" class="!mr-0" @click="batchAdd"> <MsButton type="text" size="mini" class="!mr-0" @click="batchAdd">
{{ t('apiTestDebug.batchAdd') }} {{ t('apiTestDebug.batchAdd') }}
</MsButton> </MsButton>
</template> </template>
<template #title="{ record, columnConfig }"> <template #title="{ record, columnConfig, rowIndex }">
<span v-if="record.title === 'root'" class="px-[8px]">root</span> <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 <a-popover
v-else v-else
position="tl" position="tl"
@ -73,8 +77,9 @@
@change="handleTypeChange(record)" @change="handleTypeChange(record)"
/> />
</template> </template>
<template #value="{ record }"> <template #example="{ record }">
<MsParamsInput v-model:value="record.value" size="medium" @dblclick="() => quickInputParams(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>
<template #minLength="{ record }"> <template #minLength="{ record }">
<a-input-number <a-input-number
@ -85,7 +90,7 @@
:precision="0" :precision="0"
size="medium" size="medium"
/> />
<div v-else class="ms-json-schema-td-text">-</div> <div v-else class="ms-form-table-td-text">-</div>
</template> </template>
<template #maxLength="{ record }"> <template #maxLength="{ record }">
<a-input-number <a-input-number
@ -96,7 +101,7 @@
:precision="0" :precision="0"
size="medium" size="medium"
/> />
<div v-else class="ms-json-schema-td-text">-</div> <div v-else class="ms-form-table-td-text">-</div>
</template> </template>
<template #minimum="{ record }"> <template #minimum="{ record }">
<a-input-number <a-input-number
@ -113,7 +118,7 @@
:step="1" :step="1"
:precision="0" :precision="0"
/> />
<div v-else class="ms-json-schema-td-text">-</div> <div v-else class="ms-form-table-td-text">-</div>
</template> </template>
<template #maximum="{ record }"> <template #maximum="{ record }">
<a-input-number <a-input-number
@ -130,7 +135,31 @@
:step="1" :step="1"
:precision="0" :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>
<template #defaultValue="{ record }"> <template #defaultValue="{ record }">
<a-input-number <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 <a-input
v-else v-else
v-model:model-value="record.defaultValue" v-model:model-value="record.defaultValue"
@ -171,6 +200,18 @@
class="ms-form-table-input" class="ms-form-table-input"
/> />
</template> </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 }"> <template #action="{ record, rowIndex }">
<div class="flex w-full items-center gap-[8px]"> <div class="flex w-full items-center gap-[8px]">
<a-tooltip :content="t('common.advancedSettings')"> <a-tooltip :content="t('common.advancedSettings')">
@ -233,14 +274,19 @@
asterisk-position="end" asterisk-position="end"
> >
<a-input <a-input
v-model:model-value="activeRecord.name" v-model:model-value="activeRecord.title"
:max-length="255" :max-length="255"
:placeholder="t('common.pleaseInput')" :placeholder="t('common.pleaseInput')"
:disabled="activeRecord.id === 'root'" :disabled="activeRecord.id === 'root'"
@change="handleSettingFormChange"
/> />
</a-form-item> </a-form-item>
<a-form-item :label="t('common.desc')"> <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> </a-form-item>
<template v-if="!['object', 'array', 'null'].includes(activeRecord.type)"> <template v-if="!['object', 'array', 'null'].includes(activeRecord.type)">
<div class="flex items-center justify-between gap-[24px]"> <div class="flex items-center justify-between gap-[24px]">
@ -252,6 +298,7 @@
:min="0" :min="0"
:step="1" :step="1"
:precision="0" :precision="0"
@change="handleSettingFormChange"
/> />
</a-form-item> </a-form-item>
<a-form-item :label="t('ms.json.schema.maxLength')" class="w-[144px]"> <a-form-item :label="t('ms.json.schema.maxLength')" class="w-[144px]">
@ -261,6 +308,7 @@
:min="0" :min="0"
:step="1" :step="1"
:precision="0" :precision="0"
@change="handleSettingFormChange"
/> />
</a-form-item> </a-form-item>
</template> </template>
@ -272,8 +320,14 @@
mode="button" mode="button"
:step="1" :step="1"
:precision="0" :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>
<a-form-item :label="t('ms.json.schema.maximum')" class="w-[144px]"> <a-form-item :label="t('ms.json.schema.maximum')" class="w-[144px]">
<a-input-number <a-input-number
@ -282,8 +336,14 @@
mode="button" mode="button"
:step="1" :step="1"
:precision="0" :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> </a-form-item>
</template> </template>
<a-form-item :label="t('ms.json.schema.default')" class="flex-1"> <a-form-item :label="t('ms.json.schema.default')" class="flex-1">
@ -292,6 +352,7 @@
v-model:model-value="activeRecord.defaultValue" v-model:model-value="activeRecord.defaultValue"
mode="button" mode="button"
:placeholder="t('common.pleaseInput')" :placeholder="t('common.pleaseInput')"
@change="handleSettingFormChange"
/> />
<a-input-number <a-input-number
v-else-if="activeRecord.type === 'integer'" v-else-if="activeRecord.type === 'integer'"
@ -300,33 +361,71 @@
:placeholder="t('common.pleaseInput')" :placeholder="t('common.pleaseInput')"
:step="1" :step="1"
:precision="0" :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> </a-form-item>
</div> </div>
<template v-if="activeRecord.type !== 'boolean'"> <template v-if="activeRecord.type !== 'boolean'">
<a-form-item :label="t('ms.json.schema.enum')"> <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>
<a-form-item :label="t('ms.json.schema.regex')"> <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>
<a-form-item :label="t('ms.json.schema.format')"> <a-form-item :label="t('ms.json.schema.format')">
<a-select <a-select
v-model:model-value="activeRecord.format" v-model:model-value="activeRecord.format"
:placeholder="t('common.pleaseSelect')" :placeholder="t('common.pleaseSelect')"
:options="formatOptions" :options="formatOptions"
@change="handleSettingFormChange"
/> />
</a-form-item> </a-form-item>
</template> </template>
</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>
<div class="mb-[8px]">{{ t('ms.json.schema.preview') }}</div> <div class="mb-[8px]">{{ t('ms.json.schema.preview') }}</div>
<MsCodeEditor <MsCodeEditor
v-model:model-value="activePreviewValue" v-model:model-value="activePreviewValue"
theme="vs" theme="vs"
height="300px" height="500px"
:show-full-screen="false" :show-full-screen="false"
:language="LanguageEnum.JSON"
read-only read-only
> >
</MsCodeEditor> </MsCodeEditor>
@ -362,6 +461,16 @@
</template> </template>
</MsCodeEditor> </MsCodeEditor>
</MsDrawer> </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> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -374,21 +483,23 @@
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsFormTable, { FormTableColumn } from '@/components/pure/ms-form-table/index.vue'; import MsFormTable, { FormTableColumn } from '@/components/pure/ms-form-table/index.vue';
import MsParamsInput from '@/components/business/ms-params-input/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 { useI18n } from '@/hooks/useI18n';
import { getGenerateId, traverseTree } from '@/utils'; import { getGenerateId, traverseTree } from '@/utils';
import { TableKeyEnum } from '@/enums/tableEnum'; import { TableKeyEnum } from '@/enums/tableEnum';
import { JsonSchemaItem } from './types'; import { JsonSchemaTableItem } from './types';
import { convertToJsonSchema } from './utils';
const { t } = useI18n(); const { t } = useI18n();
const defaultItem: JsonSchemaItem = { const defaultItem: JsonSchemaTableItem = {
id: '', id: '',
type: 'string', type: 'string',
title: '', title: '',
value: '', example: '',
description: '', description: '',
enable: true, enable: true,
defaultValue: '', defaultValue: '',
@ -401,13 +512,13 @@
format: undefined, format: undefined,
children: undefined, children: undefined,
}; };
const data = defineModel<JsonSchemaItem[]>('data', { const data = defineModel<JsonSchemaTableItem[]>('data', {
default: () => [ default: () => [
{ {
id: 'root', id: 'root',
title: 'root', title: 'root',
type: 'object', type: 'object',
value: '', example: '',
description: '', description: '',
enable: true, enable: true,
defaultValue: '', defaultValue: '',
@ -508,8 +619,8 @@
}, },
{ {
title: t('ms.json.schema.value'), title: t('ms.json.schema.value'),
dataIndex: 'value', dataIndex: 'example',
slotName: 'value', slotName: 'example',
addLineDisabled: true, addLineDisabled: true,
columnSelectorDisabled: true, columnSelectorDisabled: true,
}, },
@ -565,6 +676,30 @@
showInTable: false, showInTable: false,
width: 120, 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'), title: t('ms.json.schema.default'),
dataIndex: 'defaultValue', dataIndex: 'defaultValue',
@ -576,8 +711,8 @@
}, },
{ {
title: t('ms.json.schema.enum'), title: t('ms.json.schema.enum'),
dataIndex: 'enum', dataIndex: 'enumValues',
slotName: 'enum', slotName: 'enumValues',
inputType: 'textarea', inputType: 'textarea',
size: 'medium', size: 'medium',
addLineDisabled: true, addLineDisabled: true,
@ -591,6 +726,7 @@
size: 'medium', size: 'medium',
addLineDisabled: true, addLineDisabled: true,
showInTable: false, showInTable: false,
isNull: (record) => ['object', 'array', 'null', 'boolean'].includes(record.type),
}, },
{ {
title: t('ms.json.schema.format'), title: t('ms.json.schema.format'),
@ -601,6 +737,7 @@
options: formatOptions, options: formatOptions,
addLineDisabled: true, addLineDisabled: true,
showInTable: false, showInTable: false,
isNull: (record) => ['object', 'array', 'null', 'boolean'].includes(record.type),
}, },
{ {
title: '', title: '',
@ -617,8 +754,8 @@
/** /**
* 获取类型选项根节点只能是 object array * 获取类型选项根节点只能是 object array
*/ */
function getTypeOptions(record: Record<string, any>) { function getTypeOptions(record: JsonSchemaTableItem) {
if (record.name === 'root') { if (record.id === 'root') {
return typeOptions.filter((item) => ['object', 'array'].includes(item.value as string)); return typeOptions.filter((item) => ['object', 'array'].includes(item.value as string));
} }
return typeOptions; 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.type === 'object' || record.type === 'array') {
if (!record.children) { if (!record.children) {
// //
@ -641,7 +778,7 @@
/** /**
* 添加子节点 * 添加子节点
*/ */
function addChild(record: Record<string, any>) { function addChild(record: JsonSchemaTableItem) {
if (!record.children) { if (!record.children) {
record.children = []; record.children = [];
} }
@ -664,8 +801,8 @@
/** /**
* 删除行 * 删除行
*/ */
function deleteLine(record: Record<string, any>, rowIndex: number) { function deleteLine(record: JsonSchemaTableItem, rowIndex: number) {
if (record.parent) { if (record.parent?.children) {
record.parent.children.splice(rowIndex, 1); record.parent.children.splice(rowIndex, 1);
} else { } else {
data.value.splice(rowIndex, 1); 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(() => { nextTick(() => {
if (record.enable && record.children && record.children.length > 0) { if (record.enable && record.children && record.children.length > 0) {
// //
traverseTree(record.children, (item: Record<string, any>) => { traverseTree<JsonSchemaTableItem>(record.children, (item) => {
item.enable = true; item.enable = true;
if (!selectedKeys.value.includes(item.id)) { if (!selectedKeys.value.includes(item.id)) {
selectedKeys.value.push(item.id); selectedKeys.value.push(item.id);
@ -704,12 +841,26 @@
const activeRecord = ref<any>({}); const activeRecord = ref<any>({});
const activePreviewValue = ref(''); const activePreviewValue = ref('');
function openSetting(record: Record<string, any>) { function openSetting(record: JsonSchemaTableItem) {
// parent children // parent children
activeRecord.value = { activeRecord.value = {
...record, ...record,
}; };
settingDrawerVisible.value = true; 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')
);
} }
/** /**
@ -717,12 +868,12 @@
*/ */
function applySetting() { function applySetting() {
if (activeRecord.value.id === 'root') { if (activeRecord.value.id === 'root') {
data.value[0] = activeRecord.value; data.value = [{ ...activeRecord.value }];
} else { } else {
const brothers = activeRecord.value.parent?.children || []; const brothers = activeRecord.value.parent?.children || [];
const index = brothers.findIndex((item: any) => item.id === activeRecord.value.id); const index = brothers.findIndex((item: any) => item.id === activeRecord.value.id);
if (index > -1) { if (index > -1) {
brothers.splice(index, 1, activeRecord.value); brothers.splice(index, 1, { ...activeRecord.value });
} }
} }
settingDrawerVisible.value = false; settingDrawerVisible.value = false;
@ -748,13 +899,20 @@
showQuickInputParam.value = false; showQuickInputParam.value = false;
clearQuickInputParam(); clearQuickInputParam();
} }
const previewDrawerVisible = ref(false);
function previewSchema() {
previewDrawerVisible.value = true;
activePreviewValue.value = JSON.stringify(convertToJsonSchema(data.value[0]));
}
defineExpose({
previewSchema,
});
</script> </script>
<style lang="less"> <style lang="less">
.ms-json-schema { .ms-json-schema {
.ms-json-schema-td-text {
padding: 0 8px;
}
.ms-json-schema-icon-button { .ms-json-schema-icon-button {
@apply !mr-0; @apply !mr-0;
&:hover { &:hover {

View File

@ -13,9 +13,12 @@ export default {
'ms.json.schema.enum': '枚举值', 'ms.json.schema.enum': '枚举值',
'ms.json.schema.enumPlaceholder': '1 行 1 个枚举值', 'ms.json.schema.enumPlaceholder': '1 行 1 个枚举值',
'ms.json.schema.regex': '正则表达式', 'ms.json.schema.regex': '正则表达式',
'ms.json.schema.regexPlaceholder': '如 /<title(.*?)</title>', 'ms.json.schema.regexPlaceholder': '如 {reg}',
'ms.json.schema.format': '格式化', 'ms.json.schema.format': '格式化',
'ms.json.schema.preview': '预览', 'ms.json.schema.preview': '预览',
'ms.json.schema.batchAdd': '批量添加', 'ms.json.schema.batchAdd': '批量添加',
'ms.json.schema.batchAddTip': '书写格式:"键":"值",如"nama":"natural"', 'ms.json.schema.batchAddTip': '书写格式:"键":"值",如"nama":"natural"',
'ms.json.schema.convertFailed': '数据转换失败,请重试',
'ms.json.schema.minItems': '最小元素数量',
'ms.json.schema.maxItems': '最大元素数量',
}; };

View File

@ -1,22 +1,38 @@
export interface JsonSchemaItem { export interface JsonSchemaCommon {
id: string; id: string;
title: string; // 参数名称 title: string; // 参数名称
type: string; // 参数类型 type: string; // 参数类型
description: string; // 参数描述 description: string; // 参数描述
enable: boolean; // 是否启用 enable: boolean; // 是否启用
value: string; // 参数值 example: string; // 参数值
defaultValue: string | number | boolean; // 默认值 defaultValue: string | number | boolean; // 默认值
example?: Record<string, any>;
items?: string; // 子级,当 type 为array 时,使用该值
properties?: Record<string, any>; // 子级,当 type 为object 时,使用该值
required?: string[]; // 必填参数 这里的值是参数的title
pattern?: string; // 正则表达式 pattern?: string; // 正则表达式
maxLength?: number; maxLength?: number;
minLength?: number; minLength?: number;
minimum?: number; minimum?: number;
maximum?: number; maximum?: number;
minItems?: number;
maxItems?: number;
format?: string; // 格式化 format?: string; // 格式化
enumValues?: string; // 参数值的枚举 }
// 前端渲染字段 // json-schema 表格组件的表格项
children?: JsonSchemaItem[]; 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[];
} }

View File

@ -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 {};

View File

@ -203,7 +203,7 @@
data = cloneDeep(fullJson); data = cloneDeep(fullJson);
importJson.value = fullJson; importJson.value = fullJson;
} }
emit('save', data, () => { emit('save', data, (refresh = false) => {
importJson.value.root.children = mapTree<MinderJsonNode>( importJson.value.root.children = mapTree<MinderJsonNode>(
importJson.value.root.children || [], importJson.value.root.children || [],
(node, path, level) => ({ (node, path, level) => ({
@ -216,12 +216,14 @@
}, },
}) })
); );
// if (innerImportJson.value.treePath?.length > 1) { if (refresh) {
// switchNode(innerImportJson.value.root.data); if (innerImportJson.value.treePath?.length > 1) {
// } else { switchNode(innerImportJson.value.root.data);
// innerImportJson.value = importJson.value; } else {
// window.minder.importJson(importJson.value); innerImportJson.value = importJson.value;
// } window.minder.importJson(importJson.value);
}
}
minderStore.setMinderUnsaved(false); minderStore.setMinderUnsaved(false);
floatMenuVisible.value = false; floatMenuVisible.value = false;
}); });

View File

@ -7,6 +7,60 @@ import { Recordable } from '#/global';
export default function useLocalForage() { export default function useLocalForage() {
const appStore = useAppStore(); 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 * @param key key
@ -19,7 +73,7 @@ export default function useLocalForage() {
if (!res) { if (!res) {
return null; return null;
} }
return res; return deserializeValue<T>(res);
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(e); console.log(e);
@ -36,7 +90,7 @@ export default function useLocalForage() {
const setItem = async (key: string, val: string | number | boolean | Recordable, notIsolatedByProject = false) => { const setItem = async (key: string, val: string | number | boolean | Recordable, notIsolatedByProject = false) => {
try { try {
const itemKey = notIsolatedByProject ? key : `${appStore.currentProjectId}-${key}`; const itemKey = notIsolatedByProject ? key : `${appStore.currentProjectId}-${key}`;
await localforage.setItem(itemKey, val); await localforage.setItem(itemKey, serializeValue(val));
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(e); console.log(e);

View File

@ -835,7 +835,7 @@ if (!result){
title: 'apiTestDebug.paramType', title: 'apiTestDebug.paramType',
dataIndex: 'variableType', dataIndex: 'variableType',
slotName: 'variableType', slotName: 'variableType',
typeOptions: extractTypeOptions.map((item) => { options: extractTypeOptions.map((item) => {
return { return {
label: t(item.label), label: t(item.label),
value: item.value, value: item.value,
@ -847,7 +847,7 @@ if (!result){
title: 'apiTestDebug.mode', title: 'apiTestDebug.mode',
dataIndex: 'extractType', dataIndex: 'extractType',
slotName: 'extractType', slotName: 'extractType',
typeOptions: [ options: [
{ {
label: 'JSONPath', label: 'JSONPath',
value: RequestExtractExpressionEnum.JSON_PATH, value: RequestExtractExpressionEnum.JSON_PATH,
@ -867,7 +867,7 @@ if (!result){
title: 'apiTestDebug.range', title: 'apiTestDebug.range',
dataIndex: 'extractScope', dataIndex: 'extractScope',
slotName: 'extractScope', slotName: 'extractScope',
typeOptions: [ options: [
{ {
label: 'Body', label: 'Body',
value: RequestExtractScope.BODY, value: RequestExtractScope.BODY,

View File

@ -167,7 +167,7 @@
<a-select <a-select
v-model:model-value="record.paramType" v-model:model-value="record.paramType"
:disabled="props.disabledExceptParam" :disabled="props.disabledExceptParam"
:options="columnConfig.typeOptions || []" :options="columnConfig.options || []"
class="ms-form-table-input w-full" class="ms-form-table-input w-full"
@change="(val) => handleTypeChange(val, record, rowIndex, columnConfig.addLineDisabled)" @change="(val) => handleTypeChange(val, record, rowIndex, columnConfig.addLineDisabled)"
/> />
@ -177,7 +177,7 @@
<a-select <a-select
v-model:model-value="record.extractType" v-model:model-value="record.extractType"
:disabled="props.disabledExceptParam" :disabled="props.disabledExceptParam"
:options="columnConfig.typeOptions || []" :options="columnConfig.options || []"
class="ms-form-table-input w-[110px]" class="ms-form-table-input w-[110px]"
@change="() => addTableLine(rowIndex)" @change="() => addTableLine(rowIndex)"
/> />
@ -187,7 +187,7 @@
<a-select <a-select
v-model:model-value="record.variableType" v-model:model-value="record.variableType"
:disabled="props.disabledExceptParam" :disabled="props.disabledExceptParam"
:options="columnConfig.typeOptions || []" :options="columnConfig.options || []"
class="ms-form-table-input w-[110px]" class="ms-form-table-input w-[110px]"
@change="() => addTableLine(rowIndex)" @change="() => addTableLine(rowIndex)"
/> />
@ -197,7 +197,7 @@
<a-select <a-select
v-model:model-value="record.extractScope" v-model:model-value="record.extractScope"
:disabled="props.disabledExceptParam || record.extractType !== RequestExtractExpressionEnum.REGEX" :disabled="props.disabledExceptParam || record.extractType !== RequestExtractExpressionEnum.REGEX"
:options="columnConfig.typeOptions || []" :options="columnConfig.options || []"
class="ms-form-table-input w-[180px]" class="ms-form-table-input w-[180px]"
@change="() => addTableLine(rowIndex)" @change="() => addTableLine(rowIndex)"
/> />
@ -211,7 +211,7 @@
<a-select <a-select
v-model:model-value="record.scope" v-model:model-value="record.scope"
:disabled="props.disabledExceptParam" :disabled="props.disabledExceptParam"
:options="columnConfig.typeOptions || []" :options="columnConfig.options || []"
class="ms-form-table-input w-[180px]" class="ms-form-table-input w-[180px]"
@change="(val) => handleScopeChange(val, record, rowIndex, columnConfig.addLineDisabled)" @change="(val) => handleScopeChange(val, record, rowIndex, columnConfig.addLineDisabled)"
/> />
@ -625,7 +625,7 @@
isAutoComplete?: boolean; // key / isAutoComplete?: boolean; // key /
isNormal?: boolean; // value MsParamsInput isNormal?: boolean; // value MsParamsInput
hasRequired?: boolean; // type required hasRequired?: boolean; // type required
typeOptions?: { label: string; value: string }[]; // type options?: { label: string; value: string }[]; // type
typeTitleTooltip?: string | string[]; // type tooltip typeTitleTooltip?: string | string[]; // type tooltip
hasDisable?: boolean; // operation enable hasDisable?: boolean; // operation enable
moreAction?: ActionsItem[]; // operation moreAction?: ActionsItem[]; // operation
@ -847,7 +847,7 @@
// //
const id = getGenerateId(); const id = getGenerateId();
const lastLineData = paramsData.value[rowIndex]; // 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 = { const nextLine = {
id, id,
enable: true, // enable: true, //

View File

@ -86,22 +86,36 @@
</div> --> </div> -->
</div> </div>
<div v-else class="h-[calc(100%-34px)]"> <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">
<MsButton <div class="flex items-center gap-[8px]">
type="text" <MsButton
class="!mr-0" type="text"
:class="jsonType === 'Schema' ? 'font-medium !text-[rgb(var(--primary-5))]' : '!text-[var(--color-text-4)]'" class="!mr-0"
@click="jsonType = 'Schema'" :class="jsonType === 'Schema' ? 'font-medium !text-[rgb(var(--primary-5))]' : '!text-[var(--color-text-4)]'"
>Schema</MsButton @click="jsonType = 'Schema'"
> >Schema</MsButton
<a-divider :margin="0" direction="vertical"></a-divider> >
<MsButton <a-divider :margin="0" direction="vertical"></a-divider>
type="text" <MsButton
class="!mr-0" type="text"
:class="jsonType === 'Json' ? 'font-medium !text-[rgb(var(--primary-5))]' : '!text-[var(--color-text-4)]'" class="!mr-0"
@click="jsonType = 'Json'" :class="jsonType === 'Json' ? 'font-medium !text-[rgb(var(--primary-5))]' : '!text-[var(--color-text-4)]'"
>Json</MsButton @click="jsonType = 'Json'"
>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> </div>
<MsCodeEditor <MsCodeEditor
v-if="jsonType === 'Json'" v-if="jsonType === 'Json'"
@ -116,7 +130,7 @@
is-adaptive is-adaptive
> >
</MsCodeEditor> </MsCodeEditor>
<MsJsonSchema v-else /> <MsJsonSchema v-else ref="jsonSchemaRef" />
</div> </div>
<batchAddKeyVal <batchAddKeyVal
v-if="showParamTable" v-if="showParamTable"
@ -222,7 +236,7 @@
} }
} }
const typeOptions = computed(() => { const options = computed(() => {
const fullOptions = Object.values(RequestParamsType).map((val) => ({ const fullOptions = Object.values(RequestParamsType).map((val) => ({
label: val, label: val,
value: val, value: val,
@ -246,7 +260,7 @@
dataIndex: 'paramType', dataIndex: 'paramType',
slotName: 'paramType', slotName: 'paramType',
hasRequired: true, hasRequired: true,
typeOptions: typeOptions.value, options: options.value,
width: 130, width: 130,
}, },
{ {
@ -307,6 +321,14 @@
}); });
const jsonType = ref<'Schema' | 'Json'>('Schema'); const jsonType = ref<'Schema' | 'Json'>('Schema');
const jsonSchemaRef = ref<InstanceType<typeof MsJsonSchema>>();
function previewJsonSchema() {
if (jsonType.value === 'Schema') {
jsonSchemaRef.value?.previewSchema();
}
}
// //
const currentBodyCode = computed({ const currentBodyCode = computed({
get() { get() {

View File

@ -73,7 +73,7 @@
dataIndex: 'paramType', dataIndex: 'paramType',
slotName: 'paramType', slotName: 'paramType',
hasRequired: true, hasRequired: true,
typeOptions: Object.values(RequestParamsType) options: Object.values(RequestParamsType)
.filter((val) => ![RequestParamsType.JSON, RequestParamsType.FILE].includes(val)) .filter((val) => ![RequestParamsType.JSON, RequestParamsType.FILE].includes(val))
.map((val) => ({ .map((val) => ({
label: val, label: val,

View File

@ -74,7 +74,7 @@
dataIndex: 'paramType', dataIndex: 'paramType',
slotName: 'paramType', slotName: 'paramType',
hasRequired: true, hasRequired: true,
typeOptions: Object.values(RequestParamsType) options: Object.values(RequestParamsType)
.filter((val) => ![RequestParamsType.JSON, RequestParamsType.FILE].includes(val as RequestParamsType)) .filter((val) => ![RequestParamsType.JSON, RequestParamsType.FILE].includes(val as RequestParamsType))
.map((val) => ({ .map((val) => ({
label: val, label: val,

View File

@ -146,7 +146,7 @@
title: 'apiScenario.params.csvScoped', title: 'apiScenario.params.csvScoped',
dataIndex: 'scope', dataIndex: 'scope',
slotName: 'scope', slotName: 'scope',
typeOptions: [ options: [
{ {
label: t('apiScenario.scenario'), label: t('apiScenario.scenario'),
value: 'SCENARIO', value: 'SCENARIO',

View File

@ -90,7 +90,7 @@
title: 'apiScenario.params.type', title: 'apiScenario.params.type',
dataIndex: 'paramType', dataIndex: 'paramType',
slotName: 'paramType', slotName: 'paramType',
typeOptions: [ options: [
{ {
label: t('common.constant'), label: t('common.constant'),
value: 'CONSTANT', value: 'CONSTANT',

View File

@ -90,7 +90,7 @@
showDrag: true, showDrag: true,
hasRequired: false, hasRequired: false,
columnSelectorDisabled: true, columnSelectorDisabled: true,
typeOptions: [ options: [
{ {
label: t('common.constant'), label: t('common.constant'),
value: 'CONSTANT', value: 'CONSTANT',