AvueAvue
首页
  • 开发指南
  • Skill开发
  • 在线测试工具
  • Form组件
  • Crud组件
  • Default组件
  • Data组件
  • Component组件
产品
工作台
授权
联系
2.x文档
个人支付接口
首页
  • 开发指南
  • Skill开发
  • 在线测试工具
  • Form组件
  • Crud组件
  • Default组件
  • Data组件
  • Component组件
产品
工作台
授权
联系
2.x文档
个人支付接口
  • 介绍
  • 更新日志
  • 贡献指南
  • 快速上手
  • 组件与依赖索引
  • Skill 开发指南
  • TypeScript 使用
  • 全局配置
  • 国际化
  • 全局 API
  • Option 配置校验
  • 在线测试工具
  • 远程协助
  • 企业须知

在线测试工具

提示

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>
最后更新:
贡献者: smallwei
Prev
Option 配置校验
Next
远程协助