搜索结果列表页面开发

This commit is contained in:
付民康
2025-03-12 18:41:20 +08:00
commit 7cebe8fc00
739 changed files with 88149 additions and 0 deletions

View 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)
```

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

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

View 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>

View File

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

View 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>

View File

@@ -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>

View 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

View 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>

View File

@@ -0,0 +1,3 @@
import RendererForm from './mode/form/index.vue';
export * from './types';
export default RendererForm

View 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创建流程

View 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

View 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
`

View 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>

View 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>

View File

@@ -0,0 +1,3 @@
import RendererPreview from './mode/preview/index.vue';
export * from './types';
export default RendererPreview

View 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[]
}