在线测试工具
提示
3.9.2+
效果预览可直接操作下方示例
集成 Option 预览、配置校验、配置差异对比和基础页面代码生成,可用于快速测试配置
正在加载示例…
<template>
<div class="online-tools">
<el-tabs v-model="active">
<el-tab-pane label="Option 预览"
name="playground">
<div class="tool-grid">
<section>
<div class="tool-bar">
<el-radio-group v-model="componentType"
size="small">
<el-radio-button label="crud">CRUD</el-radio-button>
<el-radio-button label="form">Form</el-radio-button>
</el-radio-group>
<el-button size="small"
@click="formatPlayground">格式化</el-button>
<el-button size="small"
type="primary"
@click="applyPlayground">应用</el-button>
</div>
<textarea v-model="playgroundText"
spellcheck="false"></textarea>
</section>
<section class="preview-panel">
<avue-crud v-if="componentType === 'crud'"
:key="'crud' + previewKey"
v-model="form"
v-model:page="page"
:option="playgroundOption"
:data="rows"></avue-crud>
<avue-form v-else
:key="'form' + previewKey"
v-model="form"
:option="playgroundOption"></avue-form>
</section>
</div>
</el-tab-pane>
<el-tab-pane label="Option 校验"
name="validate">
<div class="tool-grid">
<section>
<div class="tool-bar">
<el-button size="small"
type="primary"
@click="runValidate">校验</el-button>
</div>
<textarea v-model="validateText"
spellcheck="false"></textarea>
</section>
<el-table :data="validateWarnings"
border
size="small">
<el-table-column prop="path"
label="路径"
min-width="180"></el-table-column>
<el-table-column prop="message"
label="提示"
min-width="260"></el-table-column>
</el-table>
</div>
</el-tab-pane>
<el-tab-pane label="差异对比"
name="diff">
<div class="diff-grid">
<textarea v-model="leftText"
spellcheck="false"></textarea>
<textarea v-model="rightText"
spellcheck="false"></textarea>
</div>
<div class="tool-bar">
<el-button size="small"
@click="formatDiff">格式化</el-button>
<el-button size="small"
type="primary"
@click="compare">对比</el-button>
</div>
<el-table :data="diffList"
border
size="small">
<el-table-column prop="type"
label="类型"
width="90"></el-table-column>
<el-table-column prop="path"
label="路径"
min-width="220"></el-table-column>
<el-table-column label="旧值"
min-width="220">
<template #default="{ row }">
<pre class="value-cell">{{ stringifyValue(row.oldValue) }}</pre>
</template>
</el-table-column>
<el-table-column label="新值"
min-width="220">
<template #default="{ row }">
<pre class="value-cell">{{ stringifyValue(row.newValue) }}</pre>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="代码生成"
name="generator">
<div class="tool-grid">
<section>
<div class="tool-bar">
<el-input v-model="entityName"
size="small"
placeholder="实体名"></el-input>
<el-input v-model="moduleName"
size="small"
placeholder="模块名"></el-input>
<el-button size="small"
@click="addField">新增字段</el-button>
</div>
<el-table :data="fields"
border
size="small">
<el-table-column label="标签"
min-width="110">
<template #default="{ row }">
<el-input v-model="row.label"
size="small"></el-input>
</template>
</el-table-column>
<el-table-column label="字段"
min-width="110">
<template #default="{ row }">
<el-input v-model="row.prop"
size="small"></el-input>
</template>
</el-table-column>
<el-table-column label="类型"
min-width="120">
<template #default="{ row }">
<el-select v-model="row.type"
size="small">
<el-option v-for="type in fieldTypes"
:key="type"
:label="type || 'input'"
:value="type"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="搜索"
width="70"
align="center">
<template #default="{ row }">
<el-checkbox v-model="row.search"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="必填"
width="70"
align="center">
<template #default="{ row }">
<el-checkbox v-model="row.required"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="操作"
width="70"
align="center">
<template #default="{ $index }">
<el-button link
type="danger"
@click="removeField($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</section>
<pre class="code-output">{{ generatedCode }}</pre>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { computed, getCurrentInstance, ref } from 'vue';
import { ElMessage } from 'element-plus';
const instance = getCurrentInstance();
const avue = instance?.appContext.config.globalProperties || {};
const sampleOption = {
columnStateKey: 'online-user-list',
border: true,
stripe: true,
index: true,
selection: true,
columnBtn: true,
column: [
{
label: '姓名',
prop: 'name',
search: true,
rules: [{ required: true, message: '请输入姓名', trigger: 'blur' }]
},
{
label: '部门',
prop: 'dept',
type: 'select',
search: true,
dicData: [
{ label: '研发部', value: 'dev' },
{ label: '产品部', value: 'product' }
]
},
{
label: '状态',
prop: 'status',
type: 'radio',
dicData: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 }
]
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
span: 24
}
]
};
const active = ref('playground');
const componentType = ref('crud');
const playgroundText = ref(JSON.stringify(sampleOption, null, 2));
const playgroundOption = ref(JSON.parse(playgroundText.value));
const validateText = ref(JSON.stringify(sampleOption, null, 2));
const validateWarnings = ref([]);
const previewKey = ref(1);
const form = ref({});
const page = ref({ total: 3, currentPage: 1, pageSize: 10 });
const rows = ref([
{ id: 1, name: '张三', dept: 'dev', status: 1, remark: '测试数据 1' },
{ id: 2, name: '李四', dept: 'product', status: 0, remark: '测试数据 2' },
{ id: 3, name: '王五', dept: 'dev', status: 1, remark: '测试数据 3' }
]);
const leftText = ref(JSON.stringify({
border: true,
column: [
{ label: '姓名', prop: 'name' },
{ label: '状态', prop: 'status', type: 'select' }
]
}, null, 2));
const rightText = ref(JSON.stringify(sampleOption, null, 2));
const diffList = ref([]);
const entityName = ref('User');
const moduleName = ref('user');
const fieldTypes = ['', 'textarea', 'number', 'select', 'radio', 'checkbox', 'date', 'datetime', 'switch', 'upload'];
const fields = ref([
{ label: '姓名', prop: 'name', type: '', search: true, required: true },
{ label: '部门', prop: 'deptId', type: 'select', search: true, required: false },
{ label: '状态', prop: 'status', type: 'radio', search: true, required: true },
{ label: '备注', prop: 'remark', type: 'textarea', search: false, required: false }
]);
const parseJson = (text) => JSON.parse(text);
const formatPlayground = () => {
playgroundText.value = JSON.stringify(parseJson(playgroundText.value), null, 2);
};
const applyPlayground = () => {
try {
playgroundOption.value = parseJson(playgroundText.value);
previewKey.value += 1;
ElMessage.success('已应用');
} catch (error) {
ElMessage.error(error.message);
}
};
const localValidate = (option) => {
const result = [];
const propMap = {};
if (!Array.isArray(option.column)) {
return [{ path: 'option.column', message: '缺少 column 配置' }];
}
option.column.forEach((item, index) => {
const path = `option.column[${index}]`;
if (!item.prop) result.push({ path, message: '缺少 prop' });
if (item.prop && propMap[item.prop]) result.push({ path, message: `prop "${item.prop}" 重复` });
if (item.prop) propMap[item.prop] = true;
if (!item.label) result.push({ path, message: '建议补充 label' });
if (item.span && (item.span < 1 || item.span > 24)) {
result.push({ path: `${path}.span`, message: 'span 应在 1 到 24 之间' });
}
});
return result;
};
const runValidate = () => {
try {
const option = parseJson(validateText.value);
validateWarnings.value = avue.validateOption
? avue.validateOption(option, componentType.value)
: localValidate(option);
} catch (error) {
validateWarnings.value = [{ path: 'JSON', message: error.message }];
}
};
const isObject = (value) => value && Object.prototype.toString.call(value) === '[object Object]';
const normalizeArray = (path, value) => {
if (path.endsWith('column') && Array.isArray(value) && value.every((item) => item && item.prop)) {
return value.reduce((result, item) => {
result[item.prop] = item;
return result;
}, {});
}
return value;
};
const walkDiff = (left, right, path, result) => {
left = normalizeArray(path, left);
right = normalizeArray(path, right);
if (Array.isArray(left) || Array.isArray(right)) {
if (JSON.stringify(left) !== JSON.stringify(right)) {
result.push({ type: '变更', path, oldValue: left, newValue: right });
}
return;
}
if (isObject(left) && isObject(right)) {
const keys = Array.from(new Set(Object.keys(left).concat(Object.keys(right))));
keys.forEach((key) => {
const nextPath = path ? path + '.' + key : key;
if (!(key in left)) {
result.push({ type: '新增', path: nextPath, oldValue: undefined, newValue: right[key] });
} else if (!(key in right)) {
result.push({ type: '删除', path: nextPath, oldValue: left[key], newValue: undefined });
} else {
walkDiff(left[key], right[key], nextPath, result);
}
});
return;
}
if (left !== right) result.push({ type: '变更', path, oldValue: left, newValue: right });
};
const compare = () => {
try {
const result = [];
walkDiff(parseJson(leftText.value), parseJson(rightText.value), 'option', result);
diffList.value = result;
} catch (error) {
diffList.value = [{ type: '错误', path: 'JSON', oldValue: error.message, newValue: '' }];
}
};
const formatDiff = () => {
leftText.value = JSON.stringify(parseJson(leftText.value), null, 2);
rightText.value = JSON.stringify(parseJson(rightText.value), null, 2);
};
const stringifyValue = (value) => {
if (value === undefined) return '-';
if (typeof value === 'string') return value;
return JSON.stringify(value, null, 2);
};
const toColumn = (field) => {
const column = {
label: field.label,
prop: field.prop,
span: field.type === 'textarea' || field.type === 'upload' ? 24 : 12
};
if (field.type) column.type = field.type;
if (field.search) column.search = true;
if (['select', 'radio', 'checkbox'].includes(field.type)) {
column.dicData = [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 }
];
}
if (field.required) {
column.rules = [{ required: true, message: '请输入' + field.label, trigger: 'blur' }];
}
return column;
};
const generatedOption = computed(() => ({
border: true,
stripe: true,
index: true,
selection: true,
rowKey: 'id',
columnStateKey: moduleName.value + '-crud',
column: fields.value.filter((field) => field.label && field.prop).map(toColumn)
}));
const generatedCode = computed(() => {
const optionCode = JSON.stringify(generatedOption.value, null, 2);
return [
'<template>',
' <avue-crud v-model="form" :option="option" :data="data" />',
'</template>',
'',
'<script setup>',
"import { reactive, ref } from 'vue'",
'',
'const form = ref({})',
'const data = ref([])',
'const option = reactive(' + optionCode + ')',
'<\/script>',
'',
'export interface ' + entityName.value + 'Item {',
' id?: string | number',
...fields.value.map((field) => ' ' + field.prop + '?: string | number'),
'}'
].join('\n');
});
const addField = () => {
const index = fields.value.length + 1;
fields.value.push({
label: '字段' + index,
prop: 'field' + index,
type: '',
search: false,
required: false
});
};
const removeField = (index) => {
fields.value.splice(index, 1);
};
runValidate();
compare();
</script>
<style scoped>
.online-tools {
border: 1px solid #dcdfe6;
padding: 12px;
}
.tool-grid {
display: grid;
grid-template-columns: minmax(320px, 42%) minmax(0, 1fr);
gap: 12px;
}
.diff-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-bottom: 12px;
}
.tool-bar {
margin-bottom: 10px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.preview-panel {
min-width: 0;
overflow: auto;
}
textarea {
min-height: 340px;
width: 100%;
padding: 12px;
border: 1px solid #dcdfe6;
resize: vertical;
outline: none;
font-family: Consolas, 'Courier New', monospace;
font-size: 13px;
line-height: 1.55;
}
.code-output,
.value-cell {
margin: 0;
white-space: pre-wrap;
word-break: break-all;
font-family: Consolas, 'Courier New', monospace;
font-size: 12px;
line-height: 1.5;
}
.code-output {
min-height: 420px;
padding: 12px;
overflow: auto;
color: #e5e7eb;
background: #111827;
}
@media (max-width: 900px) {
.tool-grid,
.diff-grid {
grid-template-columns: 1fr;
}
}
</style>
