搜索结果列表页面开发
This commit is contained in:
30
src/components/renderer-yaml/api.md
Normal file
30
src/components/renderer-yaml/api.md
Normal file
@@ -0,0 +1,30 @@
|
||||
```
|
||||
mock数据地址: renderer-yaml/mock/data.ts
|
||||
```
|
||||
### 预览组件
|
||||
1. 使用方式
|
||||
```js
|
||||
import rendererPreview from '@/components/renderer-yaml/mode/preview'
|
||||
```
|
||||
2. Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| content | yaml文件内容 | string | empty |
|
||||
|
||||
### Form提交组件
|
||||
1. 使用方式
|
||||
```js
|
||||
import rendererPreview from '@/components/renderer-yaml/mode/form'
|
||||
```
|
||||
2. Props
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
### Yaml 转换成 markdown协议字符串
|
||||
1. 使用方式
|
||||
```js
|
||||
import toMarkdown from '@/components/renderer-yaml/helper/toMarkdown';
|
||||
const markdown = toMarkdown(yamlContent);
|
||||
console.log(markdown)
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<d-checkbox
|
||||
class="renderer-checkbox"
|
||||
:disabled="disabled"
|
||||
v-for="(item, index) in optionsComputed"
|
||||
:key="index"
|
||||
:modelValue="getCurrentValue(item.label)"
|
||||
@change="()=> handleChange(item)"
|
||||
>
|
||||
{{item.label}}<i v-if="item.required" class="mr-[4px] text-[var(--color-danger)]">*</i>
|
||||
</d-checkbox>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { defineOptions, defineEmits, computed, type ComputedRef, ref, type Ref } from 'vue';
|
||||
import { type DefaultValue, type Options, type CheckBoxItem, splitStr} from '../../types';
|
||||
defineOptions({ name: 'rendererCheckbox' })
|
||||
const props = defineProps<{
|
||||
modelValue: DefaultValue,
|
||||
options: Options,
|
||||
disabled: boolean
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const currentModelValue:ComputedRef<string[]> = computed(()=> {
|
||||
const modelValue = (props.modelValue || '') as string;
|
||||
return modelValue.split(splitStr);
|
||||
})
|
||||
const optionsComputed = computed(()=> {
|
||||
return props.options as CheckBoxItem[]
|
||||
})
|
||||
const getCurrentValue = (value:string)=> {
|
||||
return currentModelValue.value.includes(String(value));
|
||||
}
|
||||
const handleChange = (value: CheckBoxItem)=> {
|
||||
let splitModelValue = (props.modelValue || '').toString().split(splitStr);
|
||||
const index = splitModelValue.indexOf(String(value.label));
|
||||
if (index === -1) {
|
||||
splitModelValue = [...new Set([...splitModelValue, value.label])].filter(Boolean);
|
||||
} else {
|
||||
splitModelValue.splice(index, 1);
|
||||
}
|
||||
emit('update:modelValue', (splitModelValue || []).join(splitStr));
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.renderer-checkbox {
|
||||
.devui-checkbox label {
|
||||
height: auto;
|
||||
line-height: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.devui-checkbox label>span.devui-checkbox__label-text {
|
||||
white-space: normal;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.devui-checkbox {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<!-- <d-dropdown
|
||||
:visible="visible"
|
||||
trigger="manually"
|
||||
:position="['left-start']"
|
||||
@toggle="handleTrigger"
|
||||
close-scope="blank"
|
||||
:align="null"
|
||||
> -->
|
||||
<d-select class="renderer-selected mb-2"
|
||||
:disabled="disabled"
|
||||
:modelValue="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:allow-clear="true"
|
||||
@update:model-value="handleUpdate"
|
||||
>
|
||||
<gc-option v-for="(item, index) in options" :key="index" :value="item" :name="item"></gc-option>
|
||||
</d-select>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { defineOptions, defineEmits, withDefaults } from 'vue';
|
||||
import { Dropdown } from 'vue-devui';
|
||||
import { type DefaultValue, type Options } from '../../types'
|
||||
defineOptions({
|
||||
'd-dropdown': Dropdown
|
||||
});
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: DefaultValue,
|
||||
options: Options,
|
||||
disabled: boolean,
|
||||
placeholder: string
|
||||
}>(),{
|
||||
modelValue: undefined ,
|
||||
options: ()=> [],
|
||||
disabled: false,
|
||||
placeholder: ''
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const handleUpdate = (value: any)=> {
|
||||
if (props.disabled) return;
|
||||
emit('update:modelValue', value);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.renderer-selected{
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="renderer-header">
|
||||
<renderer-md :content="rendererContent" :delay="false"></renderer-md>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, defineOptions, type Ref } from 'vue';
|
||||
|
||||
import rendererMd from '../renderer-md/index.vue';
|
||||
import type { ValidationsSchemaOutput } from '../../helper';
|
||||
|
||||
defineOptions({name: 'RendererHeader'});
|
||||
const props = defineProps<{
|
||||
schema: ValidationsSchemaOutput|Record<string, any>
|
||||
}>();
|
||||
|
||||
const rendererContent:Ref<string> = computed(()=> {
|
||||
const keys:string[] = ['name', 'about', 'labels', 'assignees'];
|
||||
let str:string = '';
|
||||
keys.forEach((key, index) => {
|
||||
str += index === 0 ? '' : '|'
|
||||
str+= `${key.charAt(0).toUpperCase() + key.slice(1)}`
|
||||
});
|
||||
str += `\n---|---|---|---\n`;
|
||||
keys.forEach((key, index) => {
|
||||
const sKey = key === 'about' ? 'description' : key;
|
||||
const value = (props.schema as any)[sKey];
|
||||
str += `|${ value || '-'}`;
|
||||
if (index === keys.length - 1) str += '\n';
|
||||
});
|
||||
return str
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Message } from 'vue-devui';
|
||||
export const copyText = (content: string) => {//复制
|
||||
// content = content.replace(/^\s/,'')
|
||||
navigator.clipboard.writeText(content).then(function () {
|
||||
Message({ type: 'success', message: '复制成功' });
|
||||
}).catch(function () {
|
||||
(function (content) {
|
||||
document.oncopy = function (e) {
|
||||
e.clipboardData?.setData('text', content);
|
||||
e.preventDefault();
|
||||
document.oncopy = null;
|
||||
Message({ type: 'success', message: '复制成功' });
|
||||
};
|
||||
})(content);
|
||||
document.execCommand('copy');
|
||||
});
|
||||
};
|
||||
export const utf8HexToString = function(utf8Hex:string){
|
||||
let str = '';
|
||||
for (let i = 0; i < utf8Hex.length; i += 2) {
|
||||
let byte = parseInt(utf8Hex.substr(i, 2), 16);
|
||||
if (byte < 128) {
|
||||
str += String.fromCharCode(byte);
|
||||
} else if (byte >= 192 && byte < 224) {
|
||||
str += String.fromCharCode(((byte & 31) << 6) | (parseInt(utf8Hex.substr(i + 2, 2), 16) & 63));
|
||||
i += 2;
|
||||
} else {
|
||||
str += String.fromCharCode(((byte & 15) << 12) | ((parseInt(utf8Hex.substr(i + 2, 2), 16) & 63) << 6) | (parseInt(utf8Hex.substr(i + 4, 2), 16) & 63));
|
||||
i += 4;
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
234
src/components/renderer-yaml/components/renderer-md/index.vue
Normal file
234
src/components/renderer-yaml/components/renderer-md/index.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: "renderer-md" });
|
||||
import { copyText, utf8HexToString } from './helper';
|
||||
import { onMounted, ref, watch, watchEffect, type Ref } from 'vue';
|
||||
import 'highlight.js/styles/vs2015.min.css';
|
||||
import md from "./markdown";
|
||||
|
||||
const markdown:Ref<any> = ref(null);
|
||||
const sleep = (during:number) => {
|
||||
return new Promise(function(rs,rj){setTimeout(rs,during);})
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
content: string | any; // md内容
|
||||
delay: boolean; // 延迟渲染
|
||||
}>(), {
|
||||
content: "",
|
||||
delay: true,
|
||||
});
|
||||
|
||||
const runing = ref(false);
|
||||
const mdDelay = ref('');//延迟渲染的md内容
|
||||
const mdContent = ref('');//延迟渲染的md html
|
||||
const WORDS = 1;//打印字数
|
||||
const interval = ref(Math.floor(1000 / 60));//最小间隔时长
|
||||
const preTime = ref(0);
|
||||
|
||||
const render = async () => {
|
||||
if (props.content.length - mdDelay.value.length <= WORDS) {
|
||||
runing.value = false;
|
||||
mdDelay.value = props.content;
|
||||
mdContent.value = md.render(props.content);
|
||||
} else {
|
||||
runing.value = true;
|
||||
mdDelay.value = props.content.substring(0, mdDelay.value.length + WORDS);
|
||||
mdContent.value = md.render(mdDelay.value);
|
||||
await sleep(interval.value);
|
||||
await render();
|
||||
}
|
||||
mdContent.value = md.render(props.content);
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.delay) {
|
||||
if (!runing.value) render();
|
||||
} else {
|
||||
// if (runing.value) return;
|
||||
mdDelay.value = props.content;
|
||||
mdContent.value = md.render(props.content);
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.content, (newVal, oldVal) => {
|
||||
const now = Date.now();
|
||||
if (preTime.value) {
|
||||
interval.value = Math.floor((now - preTime.value) / (newVal.length - oldVal.length));
|
||||
// console.log('间隔:', Math.floor((now - preTime.value)), 'ms', ' 每字间隔:', interval.value, 'ms', ' 变化字符:', newVal.replace(oldVal, ''));
|
||||
}
|
||||
preTime.value = now;
|
||||
});
|
||||
function addMarkdownEvent() {
|
||||
markdown.value.addEventListener('click', (e:any) => {
|
||||
if (e.target.id === 'copy') {
|
||||
copyText(utf8HexToString(e.target?.dataset?.copy));
|
||||
}
|
||||
})
|
||||
}
|
||||
onMounted(()=> {
|
||||
addMarkdownEvent();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-html="mdContent" ref="markdown" class="renderer-md"></div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.renderer-md {
|
||||
letter-spacing: 0em;
|
||||
text-align: left;
|
||||
color: #000000;
|
||||
max-width: 100%;
|
||||
table {
|
||||
font-size: 14px;
|
||||
theader {
|
||||
font-weight: bold;
|
||||
}
|
||||
td,th {
|
||||
padding: 6px 16px;
|
||||
border: #d0d7d1 solid 1px;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: #0969da;
|
||||
}
|
||||
pre {
|
||||
position: relative;
|
||||
}
|
||||
pre code.hljs {
|
||||
width: auto;
|
||||
}
|
||||
code.hljs {
|
||||
border-radius: 6px;
|
||||
padding-top: 20px;
|
||||
width: auto;
|
||||
@media screen and (min-width:1536px) {
|
||||
width: 960px;
|
||||
}
|
||||
|
||||
@media screen and (max-width:1536px) and (min-width:1024px) {
|
||||
width: calc(100vw - 400px - 64px - 32px * 2);
|
||||
}
|
||||
|
||||
@media screen and (max-width:1024px) and (min-width:768px) {
|
||||
width: calc(100vw - 32px * 2);
|
||||
}
|
||||
|
||||
@media screen and (max-width:768px) {
|
||||
width: calc(100vw - 16px * 2);
|
||||
}
|
||||
}
|
||||
|
||||
p,
|
||||
code.hljs {
|
||||
margin-bottom: 10px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 22px;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
/* 标题通用格式 */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: #000000;
|
||||
margin: 24px 0 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 16px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
/* 列表(有序,无序) */
|
||||
ul,
|
||||
ol {
|
||||
margin: 0 0 8px 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #000000; // var(--color-CG600);
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4px 0 0 20px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol>li {
|
||||
list-style-type: decimal;
|
||||
// 表达式,修复有序列表序号展示不全的问题
|
||||
// &:nth-child(n + 10) {
|
||||
// margin-left: 30px;
|
||||
// }
|
||||
|
||||
// &:nth-child(n + 100) {
|
||||
// margin-left: 30px;
|
||||
// }
|
||||
}
|
||||
|
||||
ul>li {
|
||||
list-style-type: disc;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
margin-right: 11px;
|
||||
margin-bottom: 1rem;
|
||||
color: #000000; // var(--color-G900);
|
||||
}
|
||||
|
||||
ol ul,
|
||||
ol ul>li,
|
||||
ul ul,
|
||||
ul ul li {
|
||||
// list-style: circle;
|
||||
font-size: 16px;
|
||||
list-style: none;
|
||||
margin-left: 6px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ul ul ul,
|
||||
ul ul ul li,
|
||||
ol ol,
|
||||
ol ol>li,
|
||||
ol ul ul,
|
||||
ol ul ul>li,
|
||||
ul ol,
|
||||
ul ol>li {
|
||||
list-style: square;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
import markdownit from 'markdown-it';
|
||||
import hljs from 'highlight.js'; // https://highlightjs.org
|
||||
import katexPlugin from '@iktakahiro/markdown-it-katex';
|
||||
const stringToUtf8Hex = function(str:string){
|
||||
let utf8Hex = '';
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
let code = str.charCodeAt(i);
|
||||
if (code < 128) {
|
||||
utf8Hex += code.toString(16).padStart(2, '0');
|
||||
} else if (code < 2048) {
|
||||
utf8Hex += (192 | (code >> 6)).toString(16).padStart(2, '0');
|
||||
utf8Hex += (128 | (code & 63)).toString(16).padStart(2, '0');
|
||||
} else {
|
||||
utf8Hex += (224 | (code >> 12)).toString(16).padStart(2, '0');
|
||||
utf8Hex += (128 | ((code >> 6) & 63)).toString(16).padStart(2, '0');
|
||||
utf8Hex += (128 | (code & 63)).toString(16).padStart(2, '0');
|
||||
}
|
||||
}
|
||||
return utf8Hex;
|
||||
}
|
||||
const codeTool = (text: string) => `<svg id="copy" class="icon" aria-hidden="true" style="font-size:16px;display: inline-block;color:#fff;position:absolute;right:8px;top:6px;cursor:pointer;" data-copy="${text}"><use xlink:href="#gt-line-copy"></use></svg>`;
|
||||
|
||||
const md = markdownit({
|
||||
html: true,
|
||||
linkfy: true,
|
||||
highlight: function (str: string, lang: string) {
|
||||
const baseText = stringToUtf8Hex(str);
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return '<pre><code class="hljs">' +
|
||||
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
|
||||
'</code>' + codeTool(baseText) + '</pre>';
|
||||
} catch (__) { }
|
||||
}
|
||||
return '<pre><code class="hljs">' + md.utils.escapeHtml(str) + '</code>' + codeTool(baseText) + '</pre>';
|
||||
}
|
||||
});
|
||||
|
||||
md.use(katexPlugin);
|
||||
|
||||
export default md;
|
||||
100
src/components/renderer-yaml/components/renderer-scene/index.vue
Normal file
100
src/components/renderer-yaml/components/renderer-scene/index.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="renderer-scene-item" :class="{'mt-32' : rendererItem.attributes.label}">
|
||||
<renderer-title
|
||||
v-if="rendererItem.attributes.label"
|
||||
:title="rendererItem.attributes.label"
|
||||
:required="rendererItem.validations?.required"
|
||||
:hideRequired="hideRequired"
|
||||
/>
|
||||
<div class="renderer-scene-item-content" :class="{'mt-10': rendererItem.attributes.label}">
|
||||
<renderer-md class="mt-xs" :content="rendererItem.attributes.description" :delay="false" />
|
||||
<renderer-md v-if="type === 'markdown'" :content="currentValue" :delay="false" />
|
||||
<md-editor
|
||||
v-else-if="type === 'textarea' && rendererItem.attributes.render !== 'markdown' && renderer === 'form'"
|
||||
v-model="currentValue"
|
||||
:disabled="isDisabledWrite"
|
||||
/>
|
||||
<d-textarea
|
||||
v-else-if="type === 'textarea'"
|
||||
v-model="currentValue"
|
||||
:disabled="isDisabledWrite"
|
||||
:rows="4"
|
||||
/>
|
||||
<renderer-dropdown
|
||||
v-else-if="type === 'dropdown'"
|
||||
v-model="currentValue"
|
||||
:disabled="isDisabledWrite"
|
||||
:options="options"
|
||||
:placeholder="rendererItem.attributes.placeholder || ''"
|
||||
/>
|
||||
<renderer-checkbox
|
||||
v-else-if="type === 'checkboxes'"
|
||||
v-model="currentValue"
|
||||
:disabled="isDisabledWrite"
|
||||
:options="options"
|
||||
/>
|
||||
<d-input v-else
|
||||
v-model="currentValue"
|
||||
:disabled="isDisabledWrite"
|
||||
:placeholder="rendererItem.attributes.placeholder || ''"
|
||||
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, defineOptions, type Ref, onMounted, watch } from 'vue';
|
||||
import { Input, Textarea } from 'vue-devui';
|
||||
import rendererMd from '../renderer-md/index.vue';
|
||||
import rendererTitle from '../renderer-title/index.vue';
|
||||
import rendererDropdown from '../renderer-dropdown/index.vue';
|
||||
import rendererCheckbox from '../renderer-checkbox/index.vue';
|
||||
import MdEditor from '@/components/MdEditor/index.vue';
|
||||
import { type ComponentName, type DefaultValue, type SchemaItem, type RendererTypeName, RendererType } from '../../types';
|
||||
defineOptions({
|
||||
name: 'renderer-scene',
|
||||
components: {
|
||||
'd-input': Input,
|
||||
'renderer-dropdown': rendererDropdown,
|
||||
'd-textarea': Textarea,
|
||||
'renderer-md': rendererMd,
|
||||
'renderer-title': rendererTitle,
|
||||
'renderer-checkbox': rendererCheckbox,
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const props = defineProps<{
|
||||
modelValue: DefaultValue,
|
||||
type: ComponentName,
|
||||
rendererItem: SchemaItem,
|
||||
renderer: RendererTypeName,
|
||||
hideRequired: boolean|undefined
|
||||
}>();
|
||||
const currentValue:Ref<DefaultValue> = ref('');
|
||||
const options = computed(()=> {
|
||||
return props.rendererItem.attributes.options || [];
|
||||
})
|
||||
const isDisabledWrite = computed(()=> {
|
||||
return props.renderer !== RendererType.form
|
||||
})
|
||||
// 监听组件的model事件
|
||||
watch(()=> props.modelValue, (newVal:DefaultValue) => {
|
||||
currentValue.value = newVal;
|
||||
});
|
||||
watch(()=> currentValue.value, (newVal:DefaultValue) => {
|
||||
emit('update:modelValue', newVal);
|
||||
})
|
||||
const initProps = () => {
|
||||
currentValue.value = props.modelValue;
|
||||
}
|
||||
onMounted(()=> {
|
||||
initProps();
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.renderer-scene-item {
|
||||
.g-md-container {
|
||||
border: var(--gray-border-color) solid 1px!important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="renderer-title text-base font-bold">{{ title }}<i v-if="required && !hideRequired" class="text-[var(--color-danger)]">*</i></div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { defineOptions, defineProps } from 'vue';
|
||||
defineOptions({ name: 'renderer-title' });
|
||||
defineProps<{
|
||||
title: string,
|
||||
required: boolean|undefined,
|
||||
hideRequired?: boolean|undefined
|
||||
}>()
|
||||
</script>
|
||||
25
src/components/renderer-yaml/example.md
Normal file
25
src/components/renderer-yaml/example.md
Normal file
@@ -0,0 +1,25 @@
|
||||
## GitCode Issue 模板功能支持
|
||||
参考地址: [https://help.gitee.com/issue/templates](https://help.gitee.com/issue/templates)
|
||||
## 工作流流程梳理
|
||||
### 1. 创建 ISSUE 的时候检测项目
|
||||
1.1 不包含.gitcode/ISSUE_TEMPLATE/config.yml文件直接走正常的创建 ISSUE流程,流程结束<br>
|
||||
1.2 包含.gitcode/ISSUE_TEMPLATE/config.yml 目。<br>
|
||||
1.2.1 blank_issues_enabled 为 true 的时候可以自定义跳转第三方链接地址读取的是 contact_links 的配置字段<br>
|
||||
1.2.2 blank_issues_enabled 为 false 的时候读取的是 .gitcode/ISSUE_TEMPLATE/ 中除了 config.yml 文件以外的所有文件
|
||||
```yaml
|
||||
// .github/ISSUE_TEMPLATE/config.yml(配置创建ISSUE时的模板)
|
||||
# 是否允许创建空模版
|
||||
blank_issues_enabled: true
|
||||
#blank_issues_enabled = true的时候能自定义链接地址,用户自定义提交 markdown格式字段
|
||||
contact_links:
|
||||
- name: 🆕 Create new issue
|
||||
url: https://new-issue.ant.design
|
||||
about: The issue which is not created via https://new-issue.ant.design will be closed immediately.
|
||||
- name: 🆕 创建一个新 Issue
|
||||
url: https://new-issue.ant.design
|
||||
about: ⚠️ 注意请不要使用上面的 Report a vulnerability(报告安全漏洞)来报告组件库的 bug 和特性请求,请点击右侧 Open 按钮。不是用 https://new-issue.ant.design 创建的 issue 会被机器人自动关闭。
|
||||
```
|
||||
### 二 提交 ISSUE 流程(包含 .yml 文件,比如bug_report.yml/feature_report.yml)
|
||||
2.1 解析 .yml 文件(参考gitee解析规则和验证规则)<br>
|
||||
2.2 提交的数据是 markdown 格式
|
||||
### 三 创建 .gitcode/ISSUE_TEMPLATE/bug_report.yml
|
||||
36
src/components/renderer-yaml/example.vue
Normal file
36
src/components/renderer-yaml/example.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="p-[50px]">
|
||||
<d-radio-group direction="row" v-model="state.current" size="md" style="margin-bottom: 10px;">
|
||||
<d-radio-button v-for="item in state.buttons" :key="item.name" :value="item.name">{{ item.label }}</d-radio-button>
|
||||
</d-radio-group>
|
||||
<div class="mt-32">
|
||||
<renderer-preview v-if="state.current === 'preview'" :content="state.content" />
|
||||
<renderer-form v-if="state.current === 'form'" :content="state.content" />
|
||||
<renderer-md v-if="state.current === 'render'" :content="state.markdown" :delay="false" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue';
|
||||
import RendererPreview from './renderer-yaml/preview';
|
||||
import RendererForm from './renderer-yaml/form';
|
||||
import RendererMd from './renderer-yaml/components/renderer-md/index.vue';
|
||||
import { validateSchema } from './renderer-yaml/helper';
|
||||
import { mockContent } from './renderer-yaml/mock/data';
|
||||
import toMarkdown from './renderer-yaml/helper/toMarkdown';
|
||||
interface State {
|
||||
content: string,
|
||||
markdown: string,
|
||||
current: string,
|
||||
buttons: Record<string, string>[]
|
||||
}
|
||||
defineOptions({ name: 'renderMdExample1'})
|
||||
const state:State = reactive({
|
||||
content: mockContent,
|
||||
markdown: '',
|
||||
// preview form render
|
||||
current: 'form',
|
||||
buttons: [{ name: 'preview', label: '预览' }, { name: 'form', label: '提交' }, { name: 'render', label: '渲染'}]
|
||||
})
|
||||
state.markdown = toMarkdown(validateSchema(mockContent)?.body || []);
|
||||
</script>
|
||||
3
src/components/renderer-yaml/form.ts
Normal file
3
src/components/renderer-yaml/form.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import RendererForm from './mode/form/index.vue';
|
||||
export * from './types';
|
||||
export default RendererForm
|
||||
92
src/components/renderer-yaml/helper/index.ts
Normal file
92
src/components/renderer-yaml/helper/index.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 参考协议文档
|
||||
* https://help.gitee.com/community/syntax-for-gitees-form-schema
|
||||
* https://docs.github.com/zh/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema
|
||||
*
|
||||
*
|
||||
*/
|
||||
import { parse } from 'yaml';
|
||||
import { type SchemaRoot,type DefaultValue, type SchemaRootOutLink, ComponentNames, ComponentType} from '../types';
|
||||
import isBoolean from 'lodash/isBoolean';
|
||||
export interface ErrorItem {
|
||||
message: string
|
||||
}
|
||||
export interface errorItemOutput {
|
||||
errors?: ErrorItem[]
|
||||
}
|
||||
export interface ValidationsSchemaOutput extends SchemaRoot, errorItemOutput, SchemaRootOutLink {}
|
||||
function validate (schema:ValidationsSchemaOutput):ValidationsSchemaOutput {
|
||||
if (!schema.name) {
|
||||
schema.errors?.push({ message: 'name 必须填写' });
|
||||
return schema;
|
||||
}
|
||||
if (!schema.description) {
|
||||
schema.errors?.push({ message: 'description 必须填写' });
|
||||
return schema;
|
||||
}
|
||||
if (schema.labels && !Array.isArray(schema.labels)) {
|
||||
schema.errors?.push({ message: `labels 必须是数组([${schema.labels}])` });
|
||||
return schema;
|
||||
}
|
||||
if (!schema.body) {
|
||||
schema.errors?.push({ message: 'body 必须填写' });
|
||||
return schema;
|
||||
};
|
||||
schema.body.forEach((item, index) => {
|
||||
if (!item.attributes) {
|
||||
schema.errors?.push({ message: `body[${index}] attributes 必须填写` });
|
||||
}
|
||||
if (!item.type) {
|
||||
schema.errors?.push({ message: `body[${index}] type 必须填写` });
|
||||
}
|
||||
if (item.type && !ComponentNames.includes(item.type)) {
|
||||
schema.errors?.push({ message: `body[${index}] type 只包含${ComponentNames.join(',')}组件` });
|
||||
}
|
||||
if (item.validations && !isBoolean(item.validations.required)) {
|
||||
schema.errors?.push({ message: `body[${index}] validations.required 必须是true或false` });
|
||||
}
|
||||
if (item.type === ComponentType.checkboxes ) {
|
||||
if (!item.attributes.options) {
|
||||
schema.errors?.push({ message: `body[${index}] attributes.options 必须填写` })
|
||||
} else {
|
||||
const findLabel = (item.attributes.options || []).find(option => typeof option === 'string');
|
||||
if (findLabel) {
|
||||
schema.errors?.push({ message: `body[${index}] attributes.options 必须包含label` })
|
||||
}
|
||||
(item.attributes.options || []).find((option, optionIndex) => {
|
||||
if (typeof option !== 'string' && !isBoolean(option.required) && 'required' in option ) {
|
||||
schema.errors?.push({ message: `body[${index}] attributes.options[${optionIndex}] required 必须是true或false` })
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
return schema;
|
||||
}
|
||||
// 验证yaml字段的合法性
|
||||
export function validateSchema(content:DefaultValue):ValidationsSchemaOutput|null{
|
||||
if (!content) return null
|
||||
let schema = {} as ValidationsSchemaOutput;
|
||||
// todo 验证字段合法性
|
||||
try {
|
||||
schema = parse(content as string);
|
||||
schema.errors = [];
|
||||
validate(schema);
|
||||
} catch (error:any) {
|
||||
schema.errors = [{message: error.message}]
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
*
|
||||
* JSON解析错误信息提示
|
||||
There are some problems with this template
|
||||
|
||||
body[2]: Required attribute key label is missing. Learn more about error 1.
|
||||
|
||||
body[2]: attribute is not a permitted key. Learn more about error 2.
|
||||
*
|
||||
*/
|
||||
// 路由根据github创建流程
|
||||
43
src/components/renderer-yaml/helper/toMarkdown.ts
Normal file
43
src/components/renderer-yaml/helper/toMarkdown.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ComponentType, splitStr, type SchemaItem, type CheckBoxItem } from "../types";
|
||||
function renderToMarkdown (schemaItem:SchemaItem):string {
|
||||
const { type } = schemaItem;
|
||||
let { label, value, render, options = [] } = schemaItem.attributes;
|
||||
let markdownStr = '';
|
||||
markdownStr += label ?
|
||||
`### ${label}
|
||||
`
|
||||
: '\n';
|
||||
// 特殊处理 checkobox的value值
|
||||
// - [X] 1
|
||||
// - [X] 2
|
||||
// - [ ] 3
|
||||
// - [X] 4
|
||||
if (type === ComponentType.checkboxes) {
|
||||
const checkboxesList:string[] = value.split(splitStr);
|
||||
options.forEach((item) => {
|
||||
const optionLabel = typeof item !== 'string' && item.label;
|
||||
const isChecked = checkboxesList.includes(optionLabel as string);
|
||||
value += `<br/>[${isChecked ? 'x' : ' '}] ${optionLabel}\n`
|
||||
})
|
||||
}
|
||||
// 处理语言模块的渲染
|
||||
const isRenderMarkdown = render && render !== 'markdown';
|
||||
if (isRenderMarkdown) {
|
||||
markdownStr += '\n```' + render
|
||||
}
|
||||
markdownStr += `\n${(value || '')}\n`;
|
||||
if (isRenderMarkdown) {
|
||||
markdownStr += '```\n'
|
||||
}
|
||||
return markdownStr
|
||||
}
|
||||
function toMarkdown(schemaList:SchemaItem[]):string {
|
||||
let markdown = '';
|
||||
if (!schemaList) return '';
|
||||
for (let i = 0; i < schemaList.length; i++) {
|
||||
const item = schemaList[i];
|
||||
markdown += renderToMarkdown(item)
|
||||
}
|
||||
return markdown
|
||||
}
|
||||
export default toMarkdown
|
||||
235
src/components/renderer-yaml/mock/data.ts
Normal file
235
src/components/renderer-yaml/mock/data.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
export const mockContent =
|
||||
// `
|
||||
// name: 报告安全漏洞
|
||||
// description: 向我们报告代码安全漏洞和隐私泄漏等敏感信息,促进项目的安全性和可靠性
|
||||
// title: "<安全漏洞>:"
|
||||
// labels: [安全问题]
|
||||
// body:
|
||||
// - type: markdown
|
||||
// attributes:
|
||||
// value: |
|
||||
// ## GitCode 安全漏洞报告指引
|
||||
|
||||
// GitCode 安全漏洞报告功能允许用户能够快速、安全地报告代码安全漏洞和隐私泄漏等敏感信息,以促进项目的安全性和可靠性。它涵盖了多个方面,包括代码漏洞、依赖关系漏洞、安全警报等:
|
||||
|
||||
// - **漏洞报告**:漏洞报告指的是在代码中发现的潜在安全问题或漏洞。这些漏洞可能导致系统受到攻击或遭受损害,例如 SQL 注入、跨站脚本 (XSS)、路径遍历等
|
||||
// - **依赖漏洞**:项目所依赖的外部库或软件包中存在的安全问题,避免攻击者可以利用依赖漏洞来执行恶意代码
|
||||
// - **安全警报**:代码中存在的安全风险,例如编码错误、设计缺陷或未经授权的访问等原因而产生的安全风险等
|
||||
|
||||
// - type: input
|
||||
// attributes:
|
||||
// label: CVE 编号
|
||||
// placeholder: |
|
||||
// eg. CVE-2024-####
|
||||
// description: |
|
||||
// 若有漏洞的\`CVE\`编号,请提供;若不清楚或尚未知晓,请忽略
|
||||
|
||||
// validations:
|
||||
// required: false
|
||||
// render: markdown
|
||||
|
||||
// - type: textarea
|
||||
// attributes:
|
||||
// label: 影响程度
|
||||
// placeholder: |
|
||||
// 这是什么类型的漏洞?谁会受到影响?
|
||||
|
||||
// description: |
|
||||
// 安全漏洞的影响程度通常取决于漏洞的类型和攻击向量。例如,如果漏洞是跨站脚本(XSS),受影响的用户可能包括访问受感染页面的所有用户。对于其他类型的漏洞,例如身份验证绕过或权限提升,影响范围可能更加有限,但仍然会对系统的安全性产生重大影响
|
||||
|
||||
// validations:
|
||||
// required: false
|
||||
|
||||
// - type: textarea
|
||||
// attributes:
|
||||
// label: 补丁
|
||||
// placeholder: |
|
||||
// 问题是否已修补?应升级到哪些版本?
|
||||
|
||||
// description: |
|
||||
// 如果安全漏洞已经得到修补,相关的补丁信息应当提供给用户。这可能包括指导项目成员升级到包含修复程序的特定软件版本。通常,最新版本包含了最新的安全修复。
|
||||
|
||||
// validations:
|
||||
// required: false
|
||||
|
||||
// - type: textarea
|
||||
// attributes:
|
||||
// label: 解决方法
|
||||
// placeholder: |
|
||||
// 是否有办法在不升级的情况下修复或纠正漏洞?
|
||||
|
||||
// description: |
|
||||
// 即使没有可用的补丁,项目成员仍然可以通过执行一些临时措施来减轻安全漏洞的风险。这些临时措施通常称为“解决方法”或“临时修复”。这些方法可能包括配置系统设置、禁用受影响的功能或模块,或者实施其他安全控制来限制漏洞的利用。
|
||||
|
||||
// validations:
|
||||
// required: false
|
||||
|
||||
// - type: textarea
|
||||
// attributes:
|
||||
// label: 参考资料
|
||||
// placeholder: |
|
||||
// 可以访问哪些链接以获取更多信息?
|
||||
|
||||
// description: |
|
||||
// 为了帮助我们深入了解漏洞以及相关的修复和解决方法,请提供更多参考资料。这些资料可能包括安全漏洞报告、官方补丁说明、安全建议或其他相关文档的链接。项目成员可以通过查阅这些参考资料来获取更多关于漏洞和其影响的详细信息,以及如何采取适当的措施来保护系统。
|
||||
|
||||
// validations:
|
||||
// required: false
|
||||
|
||||
// - type: textarea
|
||||
// attributes:
|
||||
// label: 常见弱点枚举(CWE)
|
||||
// placeholder: |
|
||||
// 请输入常见弱点枚举(CWE)编号或关键词
|
||||
|
||||
// description: |
|
||||
// CWE 中的弱点可以涵盖各种安全问题,例如缓冲区溢出、跨站脚本(XSS)、SQL 注入等。通过了解和识别软件中可能存在的弱点,开发人员可以采取相应的措施来提高系统的安全性,例如加强输入验证、使用安全编码实践等。
|
||||
|
||||
// validations:
|
||||
// required: false
|
||||
|
||||
// - type: dropdown
|
||||
// id: severity
|
||||
// attributes:
|
||||
// label: 严重程度
|
||||
// description: |
|
||||
// 漏洞或安全问题对系统安全性的潜在影响程度。通常使用未知(Unknown)、低(Low)、中等(Moderate)、高(High)、严重(Critical)等不同的级别来表示漏洞的严重性
|
||||
|
||||
// options:
|
||||
// - 未知(Unknown)
|
||||
// - 低(Low)
|
||||
// - 中等(Moderate)
|
||||
// - 高(High)
|
||||
// - 严重(Critical)
|
||||
// default: 0
|
||||
// validations:
|
||||
// required: true
|
||||
|
||||
// - type: input
|
||||
// attributes:
|
||||
// label: 报告人信息
|
||||
// description: |
|
||||
// 请填写你的联系方式
|
||||
|
||||
// validations:
|
||||
// required: false
|
||||
// `
|
||||
`
|
||||
name: Bug Report
|
||||
description: Report a bug to Apache ECharts
|
||||
title: "[Bug] "
|
||||
labels: [bug]
|
||||
assignees:
|
||||
- a
|
||||
- b
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
The issue list is reserved exclusively for bug reports and feature requests.
|
||||
|
||||
For usage questions, please use the following resources:
|
||||
|
||||
- Read the [docs](https://echarts.apache.org/option.html)
|
||||
- Find in [examples](https://echarts.apache.org/examples/)
|
||||
- Look for / ask questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/echarts)
|
||||
|
||||
For non-technical support or general questions, you can email [dev@echarts.apache.org](mailto:dev@echarts.apache.org). And don't forget to subscribe to our [mailing list](https://echarts.apache.org/maillist.html) to get updated with the project.
|
||||
|
||||
Also try to search for your issue - it may have already been answered or even fixed in the development branch. However, if you find that an old, closed issue still persists in the latest version, you should open a new issue using the form below instead of commenting on the old issue.
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- I have searched the existing issues
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: |
|
||||
Check if the issue is reproducible with the latest stable version of Apache ECharts.
|
||||
placeholder: |
|
||||
e.g. 5.2.2
|
||||
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Link to Minimal Reproduction
|
||||
description: |
|
||||
If the reproduction does not need a build setup, please provide a link to [Official Editor](https://echarts.apache.org/examples/editor.html), [JSFiddle](https://jsfiddle.net/plainheart/e46ozpqj/7/), [JSBin](https://jsbin.com/) or [CodePen](https://codepen.io/Ovilia/pen/dyYWXWM). If it requires a build setup, you can use [CodeSandbox](https://codesandbox.io/s/echarts-basic-example-template-mpfz1s) or provide a GitHub repo.
|
||||
The reproduction should be **minimal** - i.e. it should contain only the bare minimum amount of code needed to show the bug.
|
||||
Please do not just fill in a random link. The issue will be closed if no valid reproduction is provided. [Why?](https://antfu.me/posts/why-reproductions-are-required)
|
||||
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: |
|
||||
What do we need to do after opening your repo in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.
|
||||
|
||||
placeholder: |
|
||||
1. How do you create the chart.
|
||||
2. What's the chart option
|
||||
3. User interactions before the error happens.
|
||||
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: A concise description of what you're experiencing.
|
||||
value: |
|
||||
const a = 1;
|
||||
const b = 2;
|
||||
render: javascript
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
e.g.
|
||||
- **OS**: macOS Monterey
|
||||
- **Browser**: Chrome 96.0.4664.55
|
||||
- **Framework** Vue@3
|
||||
value: |
|
||||
- OS:
|
||||
- Browser:
|
||||
- Framework:
|
||||
render: markdown
|
||||
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Any additional comments?
|
||||
description: |
|
||||
e.g. some background/context of how you ran into this bug.
|
||||
|
||||
validations:
|
||||
required: false
|
||||
|
||||
`
|
||||
114
src/components/renderer-yaml/mode/form/index.vue
Normal file
114
src/components/renderer-yaml/mode/form/index.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="renderer-yaml">
|
||||
<div class="renderer-yaml-user-title mb-[16px]">
|
||||
<h3 class="font-bold text-[16px] mb-[8px]">添加标题<i class="text-[var(--color-danger)]">*</i></h3>
|
||||
<d-input v-model="title" placeholder="请填写 Issue 标题" maxlength="100">
|
||||
<template #suffix>
|
||||
<span>{{ title.length }}/100</span>
|
||||
</template>
|
||||
</d-input>
|
||||
</div>
|
||||
<template v-for="item in previewList">
|
||||
<renderer-scene
|
||||
:type="item.type"
|
||||
v-model="item.attributes.value"
|
||||
:renderer-item="item"
|
||||
:renderer="rendererType"
|
||||
:hideRequired="false"
|
||||
>
|
||||
</renderer-scene>
|
||||
</template>
|
||||
<div class="flex justify-end mt-32">
|
||||
<d-button size="lg" @click="handleCancel">取 消</d-button>
|
||||
<d-button class="ml-[8px]" :loading="loading" :disabled="disabledSubmit" size="lg" variant="solid" color="primary" @click="handleSubmit">提交 Issue</d-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineOptions, ref, watch, type Ref, onMounted, computed, type ComputedRef } from 'vue';
|
||||
import { type DefaultValue, type RendererTypeName, type SchemaItem, RendererType, ComponentType, type CheckBoxItem, splitStr } from '../../types';
|
||||
import rendererScene from '../../components/renderer-scene/index.vue';
|
||||
import { validateSchema, type ValidationsSchemaOutput } from '../../helper';
|
||||
import { Message } from 'vue-devui';
|
||||
// component name
|
||||
defineOptions({ name: 'renderer-yaml' });
|
||||
const emit = defineEmits(['success', 'update:loading', 'update-setting', 'cancel']);
|
||||
// props
|
||||
const props = defineProps<{
|
||||
content: DefaultValue,
|
||||
loading?: boolean
|
||||
}>();
|
||||
// data
|
||||
const yamlContent:Ref<DefaultValue> = ref('');
|
||||
const rendererType:Ref<RendererTypeName> = ref(RendererType.form)
|
||||
const schema:Ref<ValidationsSchemaOutput|Record<string, any>> = ref({});
|
||||
const disabledSubmit:Ref<boolean> = ref(true);
|
||||
const title:Ref<string> = ref('');
|
||||
const previewList:ComputedRef<SchemaItem[]> = computed(()=> {
|
||||
return schema.value.body || []
|
||||
})
|
||||
const parseYaml = () => {
|
||||
if (!yamlContent.value) {
|
||||
return;
|
||||
}
|
||||
schema.value = validateSchema(yamlContent.value as string) as ValidationsSchemaOutput;
|
||||
title.value = schema.value.title || '';
|
||||
const { labels = [], assignees = [] } = schema.value;
|
||||
if (labels || assignees) {
|
||||
emit('update-setting', { labels, assignees })
|
||||
}
|
||||
}
|
||||
const validateSchemaValue = ():boolean=> {
|
||||
let validate = true;
|
||||
previewList.value.forEach((item)=> {
|
||||
if (item.type === ComponentType.checkboxes) {
|
||||
const value = (item.attributes.value || '').split(splitStr);
|
||||
let isValid = 0;
|
||||
const values = item.attributes.options?.filter((option)=> typeof option !== 'string' && option.required)?.map((option)=> typeof option !== 'string' && option.label || '') || [];
|
||||
values.forEach((item:string) => {
|
||||
if (value.includes(item)) {
|
||||
isValid++;
|
||||
}
|
||||
})
|
||||
validate = isValid >= values.length;
|
||||
}
|
||||
if (item.validations && item.validations.required && (item.attributes.value || '').trim() === '') {
|
||||
validate = false;
|
||||
}
|
||||
})
|
||||
return validate;
|
||||
}
|
||||
const handleSubmit = ()=> {
|
||||
const isValid = validateSchemaValue();
|
||||
if (!isValid) {
|
||||
Message.error('请填写必填项');
|
||||
return;
|
||||
}
|
||||
if (!title.value) {
|
||||
Message.error('请填写问题标题');
|
||||
return
|
||||
}
|
||||
emit('update:loading', true);
|
||||
emit('success', { previewList: previewList.value, title: title.value })
|
||||
}
|
||||
const handleCancel = ()=> {
|
||||
emit('cancel')
|
||||
}
|
||||
watch(()=> previewList, ()=> {
|
||||
disabledSubmit.value = !validateSchemaValue();
|
||||
}, {
|
||||
deep: true,
|
||||
immediate: true
|
||||
})
|
||||
watch( ()=> props.content, (newVal)=> {
|
||||
yamlContent.value = newVal;
|
||||
parseYaml();
|
||||
})
|
||||
const initProps = () => {
|
||||
yamlContent.value = props.content;
|
||||
}
|
||||
onMounted(()=> {
|
||||
initProps();
|
||||
parseYaml();
|
||||
})
|
||||
</script>
|
||||
58
src/components/renderer-yaml/mode/preview/index.vue
Normal file
58
src/components/renderer-yaml/mode/preview/index.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="renderer-yaml">
|
||||
<renderer-header :schema="schema" />
|
||||
<template v-for="item in previewList">
|
||||
<renderer-scene
|
||||
:type="item.type"
|
||||
:model-value="item.attributes.value"
|
||||
:renderer-item="item"
|
||||
:renderer="rendererType"
|
||||
:hide-required="false"
|
||||
>
|
||||
</renderer-scene>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineOptions, ref, watch, type Ref, onMounted, computed, type ComputedRef } from 'vue';
|
||||
import { RendererType, type DefaultValue, type RendererTypeName, type SchemaItem } from '../../types';
|
||||
import rendererScene from '../../components/renderer-scene/index.vue';
|
||||
import rendererHeader from '../../components/renderer-header/index.vue';
|
||||
import { validateSchema, type ValidationsSchemaOutput } from '../../helper';
|
||||
// component name
|
||||
defineOptions({ name: 'renderer-yaml' });
|
||||
// props
|
||||
const props = defineProps<{
|
||||
content: DefaultValue,
|
||||
}>();
|
||||
const emit = defineEmits(['error']);
|
||||
// data
|
||||
const yamlContent:Ref<DefaultValue> = ref('');
|
||||
const schema:Ref<ValidationsSchemaOutput|Record<string, any>> = ref({});
|
||||
const rendererType:Ref<RendererTypeName> = ref(RendererType.preview)
|
||||
const previewList:ComputedRef<SchemaItem[]> = computed(()=> {
|
||||
// console.log(schema.value.body)
|
||||
return schema.value.body || []
|
||||
})
|
||||
const parseYaml = () => {
|
||||
if (!yamlContent.value) {
|
||||
return;
|
||||
}
|
||||
schema.value = validateSchema(yamlContent.value) as ValidationsSchemaOutput;
|
||||
if (schema.value?.errors?.length > 0) {
|
||||
emit('error', schema.value.errors)
|
||||
}
|
||||
// console.log(schema.value)
|
||||
}
|
||||
watch( ()=> props.content, (newVal)=> {
|
||||
yamlContent.value = newVal;
|
||||
parseYaml();
|
||||
})
|
||||
const initProps = () => {
|
||||
yamlContent.value = props.content;
|
||||
}
|
||||
onMounted(()=> {
|
||||
initProps();
|
||||
parseYaml();
|
||||
})
|
||||
</script>
|
||||
3
src/components/renderer-yaml/preview.ts
Normal file
3
src/components/renderer-yaml/preview.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import RendererPreview from './mode/preview/index.vue';
|
||||
export * from './types';
|
||||
export default RendererPreview
|
||||
62
src/components/renderer-yaml/types/index.ts
Normal file
62
src/components/renderer-yaml/types/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
type ID = number | string;
|
||||
export enum RendererType {
|
||||
preview = 'preview',
|
||||
form = 'form'
|
||||
}
|
||||
export const splitStr = '||';
|
||||
export type RendererTypeName = keyof typeof RendererType;
|
||||
export enum ComponentType {
|
||||
checkboxes = 'checkboxes',
|
||||
dropdown = 'dropdown',
|
||||
input = 'input',
|
||||
markdown = 'markdown',
|
||||
textarea = 'textarea'
|
||||
}
|
||||
export type DefaultValue = string | number | boolean | undefined;
|
||||
export type ComponentName = keyof typeof ComponentType;
|
||||
export const ComponentNames: ComponentName[] = [...Object.values(ComponentType)]
|
||||
export type RenderType = 'markdown'|undefined|string
|
||||
export interface BaseItem {
|
||||
description: string
|
||||
placeholder?: string
|
||||
// 语言渲染类型 markdown 渲染片段
|
||||
render?: RenderType
|
||||
}
|
||||
export interface CheckBoxItem {
|
||||
label: string;
|
||||
required?: true;
|
||||
}
|
||||
export type Options = string[]| CheckBoxItem[]
|
||||
export interface SchemaItemAttr extends BaseItem {
|
||||
label?: string
|
||||
value: string,
|
||||
options?: Options
|
||||
}
|
||||
export interface Validations {
|
||||
required: boolean;
|
||||
}
|
||||
export interface SchemaItem {
|
||||
type: ComponentType;
|
||||
id?: ID;
|
||||
attributes: SchemaItemAttr
|
||||
validations?: Validations
|
||||
}
|
||||
export interface SchemaRootBase {
|
||||
title: string;
|
||||
name: string;
|
||||
labels: string[];
|
||||
// 指派给具体用户
|
||||
assignees?: string[];
|
||||
}
|
||||
export interface SchemaRoot extends BaseItem, SchemaRootBase{
|
||||
body: SchemaItem []
|
||||
}
|
||||
export interface ContactLinks {
|
||||
name: string
|
||||
url: string
|
||||
about: string
|
||||
}
|
||||
export interface SchemaRootOutLink{
|
||||
blank_issues_enabled?: boolean
|
||||
contact_links?: ContactLinks[]
|
||||
}
|
||||
Reference in New Issue
Block a user