搜索结果列表页面开发
This commit is contained in:
172
src/components/Setting/CreateProtectBranch/index.vue
Normal file
172
src/components/Setting/CreateProtectBranch/index.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<d-modal model-value title="保护分支" class="create-protect-branch" :before-close="handleClose">
|
||||
<template #header>
|
||||
<gc-modal-header>
|
||||
<span class="text-G900 text-base font-bold leading-[24px]">创建保护分支</span>
|
||||
</gc-modal-header>
|
||||
<div class="line bg-G200"></div>
|
||||
</template>
|
||||
<d-form
|
||||
ref="formRef"
|
||||
layout="vertical"
|
||||
:data="formModel"
|
||||
:rules="rules"
|
||||
:pop-position="['right']"
|
||||
>
|
||||
<d-form-item field="name" label="选择需要保护的分支">
|
||||
<BranchSelector
|
||||
v-model="formModel.name"
|
||||
type="branch"
|
||||
:repoId="repoId"
|
||||
@input-change="handleInput"
|
||||
:branchParams="{
|
||||
branch_type: 'not_protect'
|
||||
}"
|
||||
showExtra
|
||||
/>
|
||||
</d-form-item>
|
||||
<d-form-item field="push_access_levels" label="推送权限">
|
||||
<d-select v-model="formModel.push_access_levels" :options="options" placeholder="请选择推送权限" />
|
||||
</d-form-item>
|
||||
<d-form-item field="merge_access_levels" label="合并权限">
|
||||
<d-select v-model="formModel.merge_access_levels" :options="options" placeholder="请选择合并权限" />
|
||||
</d-form-item>
|
||||
</d-form>
|
||||
<template #footer>
|
||||
<div class="flex justify-end px-5 pb-5 pt-0 gap-2">
|
||||
<d-button @click="handleClose">取消</d-button>
|
||||
<d-button variant="solid" @click="handleConfirm">确定</d-button>
|
||||
</div>
|
||||
</template>
|
||||
</d-modal>
|
||||
</template>
|
||||
|
||||
<!-- 保护分支页面 -->
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import { Message } from 'vue-devui/message';
|
||||
|
||||
import { createProtectedBranches } from '@/api/repo';
|
||||
import { useRepoId } from '@/utils/hooks/useRepoId';
|
||||
import BranchSelector from '@/components/BranchTagSelector/SingleSelector.vue';
|
||||
|
||||
// import { branchRegex } from '@/utils/regex';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
branch?: string; // 需要设定默认分支的分支名称
|
||||
options: { name: string, value: string }[],
|
||||
}>(), {
|
||||
options: () => {
|
||||
return [
|
||||
// {
|
||||
// name: '开发者',
|
||||
// value: '30'
|
||||
// },
|
||||
{
|
||||
name: '管理员',
|
||||
value: '50'
|
||||
},
|
||||
{
|
||||
name: '开发者 + 管理员',
|
||||
value: '30,50'
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
const { repoId } = useRepoId();
|
||||
|
||||
const formModel = reactive({
|
||||
name: props.branch || '',
|
||||
push_access_levels: '',
|
||||
merge_access_levels: ''
|
||||
});
|
||||
|
||||
const inputName = ref('');
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请选择分支或者输入一个通配符号', trigger: 'change' },{ message: '请选择分支或者输入一个通配符号', trigger: 'change', validator: (rule: any, val: string, cb: Function) => {
|
||||
if (val || inputName.value) {
|
||||
return cb();
|
||||
} else {
|
||||
return cb(new Error(rule.message));
|
||||
}
|
||||
} }],
|
||||
push_access_levels: [{ required: true, message: '请选择推送权限', trigger: 'change' }],
|
||||
merge_access_levels: [{ required: true, message: '请选合并择权限', trigger: 'change' }]
|
||||
};
|
||||
|
||||
const emits = defineEmits(['onProtected', 'onClose']);
|
||||
const handleClose = () => {
|
||||
emits('onClose');
|
||||
};
|
||||
|
||||
const setBranchProtected = async(config: any) => {
|
||||
const { name, push_access_levels, merge_access_levels } = config;
|
||||
const params = {
|
||||
project_id: repoId.value,
|
||||
conf: {
|
||||
names: [name],
|
||||
access_level_string: {
|
||||
push_access_levels,
|
||||
merge_access_levels
|
||||
},
|
||||
access_level: {},
|
||||
merge_setting: {}
|
||||
}
|
||||
};
|
||||
return await createProtectedBranches(params);
|
||||
};
|
||||
|
||||
const formRef = ref<any>(null);
|
||||
const handleConfirm = () => {
|
||||
formRef?.value?.validate(async(isValid: boolean) => {
|
||||
if (isValid) {
|
||||
const data = {
|
||||
name: formModel.name || inputName.value,
|
||||
push_access_levels: formModel.push_access_levels,
|
||||
merge_access_levels: formModel.merge_access_levels
|
||||
};
|
||||
const res = await setBranchProtected(data);
|
||||
if (!res.error) {
|
||||
inputName.value = '';
|
||||
Message.success('创建保护分支成功');
|
||||
emits('onProtected', data);
|
||||
emits('onClose');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleInput = (val: string) => {
|
||||
inputName.value = val;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.create-protect-branch {
|
||||
width: 410px;
|
||||
box-sizing: border-box;
|
||||
// padding: 0 0 20px;
|
||||
|
||||
.devui-modal__body {
|
||||
padding: 20px 24px 24px;
|
||||
}
|
||||
|
||||
.tip-desc {
|
||||
// @apply text-G900 px-[22px] my-[18px] leading-[28px] break-all;
|
||||
}
|
||||
.devui-modal__header {
|
||||
padding: 14px 24px;
|
||||
height: auto!important;
|
||||
}
|
||||
.btn-close {
|
||||
right: 24px;
|
||||
top: 16px;
|
||||
}
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
119
src/components/Setting/CreateTags/index.vue
Normal file
119
src/components/Setting/CreateTags/index.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<d-modal v-model="vModels" class="repo-setting-modal">
|
||||
<template #header>
|
||||
<gc-modal-header>
|
||||
<span class="text-G900 text-base font-bold leading-[24px]">保护tag</span>
|
||||
</gc-modal-header>
|
||||
<div class="line bg-G200"></div>
|
||||
</template>
|
||||
<d-form
|
||||
ref="FormRef"
|
||||
layout="vertical"
|
||||
:data="formModel"
|
||||
:rules="rules"
|
||||
:pop-position="['right']"
|
||||
class="px-[24px] pt-[20px] pb-24"
|
||||
>
|
||||
<d-form-item field="name" label="选择需要保护的 Tag">
|
||||
<TagSelector type="tag" v-model="formModel.name" :repoId="repoId" @input-change="handleInput" showExtra />
|
||||
</d-form-item>
|
||||
<d-form-item field="create_access_level" label="允许创建" class="mb-0">
|
||||
<d-select v-model="formModel.create_access_level" :options="options" placeholder="请选择推送权限" />
|
||||
</d-form-item>
|
||||
</d-form>
|
||||
<template #footer>
|
||||
<div class="flex justify-end px-[24px] pb-[24px] gap-2">
|
||||
<d-button @click="handleCancel">取消</d-button>
|
||||
<d-button variant="solid" :disabled="disabled" @click="handleConfirm">确定</d-button>
|
||||
</div>
|
||||
</template>
|
||||
</d-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import { useModel } from '@/utils/hooks/useModel';
|
||||
import TagSelector from '@/components/BranchTagSelector/SingleSelector.vue';
|
||||
|
||||
const FormRef = ref();
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: boolean
|
||||
'onUpdate:modelValue'?: Function
|
||||
repoId: string
|
||||
disabled?: boolean,
|
||||
options: { name: string, value: string }[]
|
||||
}>(), {
|
||||
disabled: false
|
||||
});
|
||||
const emits = defineEmits(['confirm', 'update:modelValue']);
|
||||
const { vModels } = useModel(props, emits);
|
||||
|
||||
const formModel = reactive({
|
||||
name: '',
|
||||
create_access_level: ''
|
||||
});
|
||||
|
||||
const inputName = ref('');
|
||||
|
||||
// 表单校验
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请选择Tag或者创建一个通配符', trigger: 'change' },{ message: '请选择Tag或者创建一个通配符', trigger: 'change', validator: (rule: any, val: string, callback: Function) => {
|
||||
if (val || inputName.value) {
|
||||
return callback();
|
||||
} else {
|
||||
return callback(new Error(rule.message));
|
||||
}
|
||||
} }],
|
||||
create_access_level: [{ required: true, message: '请选择推送权限', trigger: 'change' }]
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
vModels.value = false;
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
FormRef?.value?.validate((isValid:any) => {
|
||||
if (isValid) {
|
||||
emits('confirm', {
|
||||
name: formModel.name || inputName.value,
|
||||
create_access_level: formModel.create_access_level
|
||||
});
|
||||
emits('update:modelValue', false);
|
||||
inputName.value = '';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleInput = (val: string) => {
|
||||
inputName.value = val;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.repo-setting-modal {
|
||||
width: 410px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.devui-modal__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tip-desc {
|
||||
// @apply text-G900 px-[22px] my-[18px] leading-[28px] break-all;
|
||||
}
|
||||
.devui-modal__header {
|
||||
padding: 14px 24px;
|
||||
height: auto!important;
|
||||
}
|
||||
.btn-close {
|
||||
right: 24px;
|
||||
top: 16px;
|
||||
}
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
70
src/components/Setting/DeleteTip/index.vue
Normal file
70
src/components/Setting/DeleteTip/index.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<d-modal v-model="vModels" :title="title" class="repo-settings-modal"
|
||||
>
|
||||
<div class="tip-desc">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end px-[22px]">
|
||||
<d-button @click="closeModal()" class="mr-[8px]">取消</d-button>
|
||||
<d-button :variant="confirmColor === 'primary' ? 'solid' : 'outline'" :color="confirmColor" @click="confirm">{{ confirmText }}</d-button>
|
||||
</div>
|
||||
</template>
|
||||
</d-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useModel } from '@/utils/hooks/useModel';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: any
|
||||
'onUpdate:modelValue'?: Function
|
||||
title?: string
|
||||
autoClose?: boolean
|
||||
confirmText?: string
|
||||
confirmColor?: 'primary' | 'danger'
|
||||
}>(), {
|
||||
title: '删除操作',
|
||||
autoClose: true,
|
||||
confirmText: '确定',
|
||||
confirmColor: 'primary'
|
||||
});
|
||||
|
||||
const emits = defineEmits(['confirm', 'update:modelValue']);
|
||||
|
||||
const { vModels } = useModel(props, emits);
|
||||
|
||||
const closeModal = () => {
|
||||
vModels.value = false;
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
vModels.value = true;
|
||||
};
|
||||
const confirm = () => {
|
||||
emits('confirm');
|
||||
if (props.autoClose) {
|
||||
vModels.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
openModal
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.repo-settings-modal {
|
||||
width: 370px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 0 20px;
|
||||
.devui-modal__body {
|
||||
padding: 0;
|
||||
}
|
||||
.tip-desc {
|
||||
@apply text-G900 px-[22px] my-[18px] leading-[28px] break-all;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
119
src/components/Setting/GModal/index.vue
Normal file
119
src/components/Setting/GModal/index.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<d-modal v-model="vModels" :title="title" class="repo-settings-modal" :showClose="showCloseIcon" :escapable="escapable" :close-on-click-overlay="closeByOverlay">
|
||||
<template #header>
|
||||
<gc-modal-header>
|
||||
<Icon v-if="showWarnIcon" name="gt-warn" color="#F2050D" class="mr-[8px]"></Icon>
|
||||
<span class="text-G900 text-base font-bold leading-[24px]">{{ title }}</span>
|
||||
</gc-modal-header>
|
||||
<div class="line bg-G200"></div>
|
||||
</template>
|
||||
<div v-if="warnText" class="px-24 py-[12px] bg-[#F2050D0C] text-[var(--color-R500)] text-sm font-normal leading-[20px]">
|
||||
{{ warnText }}
|
||||
</div>
|
||||
<div class="pt-20 px-24 pb-24">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<template #footer v-if="showFooter">
|
||||
<slot name="footerGroup"></slot>
|
||||
<div class="flex justify-end px-[24px] pb-24" v-if="!customFooter">
|
||||
<d-button @click="closeModal()" class="mr-[8px]">取消</d-button>
|
||||
<d-button :loading="confirmLoading" :variant="confirmColor === 'primary' ? 'solid' : 'outline'" :color="confirmColor" @click="confirm" :disabled="disabledButton">
|
||||
{{ confirmText }}
|
||||
</d-button>
|
||||
</div>
|
||||
</template>
|
||||
</d-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useModel } from '@/utils/hooks/useModel';
|
||||
|
||||
defineSlots<{
|
||||
default?:() => any,
|
||||
footerGroup?: () => any
|
||||
}>();
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: any
|
||||
'onUpdate:modelValue'?: Function
|
||||
title?: string
|
||||
warnText?: string
|
||||
autoClose?: boolean
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
confirmColor?: 'primary' | 'danger'
|
||||
showWarnIcon?: boolean
|
||||
showFooter?: boolean
|
||||
disabledButton?: boolean
|
||||
customFooter?: boolean
|
||||
showCloseIcon?: boolean
|
||||
escapable?: boolean
|
||||
closeByOverlay?: boolean,
|
||||
confirmLoading?: boolean
|
||||
}>(), {
|
||||
title: '删除操作',
|
||||
autoClose: true,
|
||||
confirmText: '确定',
|
||||
confirmColor: 'primary',
|
||||
showFooter: true,
|
||||
disabledButton: false,
|
||||
customFooter: false,
|
||||
showCloseIcon: true,
|
||||
escapable: true,
|
||||
closeByOverlay: true,
|
||||
confirmLoading: false
|
||||
});
|
||||
|
||||
const emits = defineEmits(['closeModal', 'confirm', 'update:modelValue']);
|
||||
|
||||
const { vModels } = useModel(props, emits);
|
||||
|
||||
const closeModal = () => {
|
||||
emits('closeModal');
|
||||
vModels.value = false;
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
vModels.value = true;
|
||||
};
|
||||
const confirm = () => {
|
||||
emits('confirm');
|
||||
if (props.autoClose) {
|
||||
vModels.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
openModal
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.repo-settings-modal {
|
||||
width: 410px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 0;
|
||||
|
||||
.devui-modal__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tip-desc {
|
||||
// @apply text-G900 px-[22px] my-[18px] leading-[28px] break-all;
|
||||
}
|
||||
.devui-modal__header {
|
||||
padding: 14px 24px;
|
||||
height: auto!important;
|
||||
}
|
||||
.btn-close {
|
||||
right: 24px;
|
||||
top: 16px;
|
||||
}
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
32
src/components/Setting/ImgRadio/Index.vue
Normal file
32
src/components/Setting/ImgRadio/Index.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
const emit = defineEmits<{(e: 'update:modelValue', selected: string): void }>();
|
||||
|
||||
const props = defineProps<{
|
||||
src: string;
|
||||
alt: string;
|
||||
modelValue: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="image-radio">
|
||||
<div class="border-b border-G300"><img :src="src" :alt="alt" /></div>
|
||||
<div class="w-full flex px-5 py-3">
|
||||
<d-radio
|
||||
:modelValue="props.modelValue"
|
||||
@update:modelValue="(v: string) => emit('update:modelValue', v)"
|
||||
:value="alt"
|
||||
>{{ alt }}</d-radio
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
img {
|
||||
width: 320px;
|
||||
height: 225px;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
27
src/components/Setting/InfoCheckBox/InfoCheckbox.vue
Normal file
27
src/components/Setting/InfoCheckBox/InfoCheckbox.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ label: string }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="checkbox-container">
|
||||
<d-checkbox v-bind="$attrs">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
</d-checkbox>
|
||||
<p class="sub-text text-CG600"><slot></slot></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.checkbox-container {
|
||||
display: inline-block;
|
||||
|
||||
.label-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
margin: 10px 26px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
38
src/components/Setting/SettingItem/README.md
Normal file
38
src/components/Setting/SettingItem/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# SettingItem
|
||||
|
||||
### 说明
|
||||
- 高级设置页面的设置项
|
||||
|
||||
``` js
|
||||
import SettingItem from '@/components/Setting/SettingItem/index.vue';
|
||||
|
||||
<SettingItem title="删除项目" buttonText="删除项目" warning @onClick="handleClick">
|
||||
删除项目将删除其存储库和所有相关资源,包括Issue、合并请求等。并且删除后将无法恢复,请谨慎操作!
|
||||
</SettingItem>
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|--------------|------------------------|----------------------------|----------|-----------|
|
||||
| title? | 设置项的左上角标题(可以通过#title插槽自定义) | string | - | - |
|
||||
| buttonText? | 设置项的右上角按钮文字(可以通过#operation插槽自定义) | string | - | - |
|
||||
| warning? | 是否启用waring样式 | boolean | - | false |
|
||||
|
||||
|
||||
|
||||
### Emits
|
||||
|
||||
| 方法 | 说明 | 返回值 |
|
||||
|-----------------|------------------------------------|---------------------|
|
||||
| onClick | 右上角按钮点击事件 | |
|
||||
|
||||
|
||||
### Slots
|
||||
|
||||
| 名称 | 说明 |
|
||||
|---------------------|---------------------------------------------|
|
||||
| default | 内容区域 |
|
||||
| title | 左上角标题 |
|
||||
| operation | 右上角操作区域 |
|
||||
|
||||
69
src/components/Setting/SettingItem/index.vue
Normal file
69
src/components/Setting/SettingItem/index.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="g-setting-item" :class="{ 'g-setting-item-warning': warning }">
|
||||
<div class="setting">
|
||||
<span class="setting-title">
|
||||
<slot name="title">{{ title }}</slot>
|
||||
</span>
|
||||
<div class="setting-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="operation">
|
||||
<slot name="operation">
|
||||
<d-button class="operation-btn" @click="handleClick">{{ buttonText }}</d-button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface IProps {
|
||||
// 声明props内容
|
||||
title?: string,
|
||||
buttonText?: string,
|
||||
warning?: boolean
|
||||
}
|
||||
defineProps<IProps>();
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
const handleClick = () => {
|
||||
emit('click');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$gap: 16px;
|
||||
$border-radius: 4px;
|
||||
.g-setting-item {
|
||||
--item-color: #000;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: $gap;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid var(--item-color);
|
||||
.setting {
|
||||
flex: 1;
|
||||
margin-right: $gap;
|
||||
&-title {
|
||||
color: var(--item-color);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
&-body {
|
||||
margin-top: $gap;
|
||||
color: var(--black-60, #606060);
|
||||
}
|
||||
}
|
||||
.operation {
|
||||
&-btn {
|
||||
background-color: var(--item-color);
|
||||
color: #fff;
|
||||
padding: 8px 32px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
&-warning {
|
||||
--item-color: #FF3241;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
15
src/components/Setting/SettingText/index.vue
Normal file
15
src/components/Setting/SettingText/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{text?: string}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="setting-title text-G900 text-sm">{{text}}<slot></slot></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.setting-title{
|
||||
// font-size: 16px;
|
||||
font-weight: 400;
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
20
src/components/Setting/SettingTitle/RoundTag.vue
Normal file
20
src/components/Setting/SettingTitle/RoundTag.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="key-count"><slot></slot></span>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'devui-theme/styles-var/devui-var.scss';
|
||||
.key-count {
|
||||
display: inline-block;
|
||||
background: $devui-global-bg;
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
line-height: 27px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
31
src/components/Setting/SettingTitle/index.vue
Normal file
31
src/components/Setting/SettingTitle/index.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import RoundTag from '@/components/Setting/SettingTitle/RoundTag.vue';
|
||||
|
||||
defineProps<{ title: string, count?: number }>();
|
||||
const emits = defineEmits(['click']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="setting-title text-G900 text-2xl g-font-bold">
|
||||
<span class="cursor-pointer" @click="emits('click')">{{ title }}</span><RoundTag class="ml-10" v-if="count !== undefined">{{ count }}</RoundTag><span class="extend-text"><slot></slot></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.setting-title {
|
||||
// font-size: 18px;
|
||||
// font-weight: 500;
|
||||
display: inline-block;
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.extend-text {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
224
src/components/Setting/WebHookEdit/Index.vue
Normal file
224
src/components/Setting/WebHookEdit/Index.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, toRaw, watchEffect } from 'vue';
|
||||
import { customEventList } from './customEventList';
|
||||
import { useRouter } from 'vue-router';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { urlRegExp } from '@/utils/regex';
|
||||
const emits = defineEmits(['create', 'change']);
|
||||
const nameMaxLen = 500;
|
||||
const props = defineProps<{
|
||||
data?: Record<string, any>;
|
||||
editable?: boolean;
|
||||
loading?: boolean;
|
||||
}>();
|
||||
const router = useRouter();
|
||||
const formRef = ref<any>(null);
|
||||
const form = reactive<Record<string, any>>({
|
||||
url: '',
|
||||
push_events: true,
|
||||
tag_push_events: false,
|
||||
merge_requests_events: false,
|
||||
note_events: false,
|
||||
content_type: 'application/json',
|
||||
token: '',
|
||||
active: true
|
||||
});
|
||||
|
||||
const hookToken = ref('');
|
||||
|
||||
type WebHookEventKey = keyof typeof form
|
||||
|
||||
const contentTypeList = ref(['application/json', 'application/x-www-form-urlencoded']);
|
||||
|
||||
// const eventTypeList = shallowRef<{ key: WebHookEventKey, label: string }[]>([
|
||||
// // {
|
||||
// // key: 'all',
|
||||
// // label: '全部事件'
|
||||
// // },
|
||||
// {
|
||||
// key: 'push_events',
|
||||
// label: '仅限 push 事件'
|
||||
// },
|
||||
// {
|
||||
// key: 'custom_ctrl_item_events',
|
||||
// label: '自定义事件'
|
||||
// }
|
||||
// ]);
|
||||
|
||||
const handleClick = async() => {
|
||||
const valid = await formRef.value.validate();
|
||||
if (valid) {
|
||||
if (props?.editable) {
|
||||
const { token, ...config } = toRaw(form);
|
||||
if (token) {
|
||||
emits('change', { token, ...config });
|
||||
} else {
|
||||
emits('change', config);
|
||||
}
|
||||
} else {
|
||||
emits('create', toRaw(form));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
const regex = /^[^\*\u4e00-\u9fa5\s]*$/;
|
||||
|
||||
const handleChange = (val: string) => {
|
||||
if (props.editable) {
|
||||
form.token = val;
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
if (props.editable) {
|
||||
if (!form.token) {
|
||||
hookToken.value = '';
|
||||
} else {
|
||||
hookToken.value = form.token;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
if (props.editable) {
|
||||
if (!form.token) {
|
||||
hookToken.value = props?.data?.token ? '*'.repeat(props.data.token.length) : '********';
|
||||
} else {
|
||||
hookToken.value = form.token;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = debounce((val: string) => {
|
||||
if (val && formRef.value) {
|
||||
form.token = val;
|
||||
if (regex.test(val) && val.length <= 2000) {
|
||||
formRef.value.clearValidate(['token']);
|
||||
} else {
|
||||
formRef.value.validateFields(['token']);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.data) {
|
||||
for (const key in form) {
|
||||
if (key === 'token' && props.editable) {
|
||||
form.token = '';
|
||||
hookToken.value = props?.data?.token ? '*'.repeat(props.data.token.length) : '********';
|
||||
} else {
|
||||
form[key] = props.data[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
function restricVal(event:any) {
|
||||
let value = event.target.value;
|
||||
if (event.type === 'paste') value = (event.clipboardData || (window as any).clipboardData).getData('text');
|
||||
if (value.length < nameMaxLen) return;
|
||||
form.url = value.slice(0, nameMaxLen);
|
||||
event.preventDefault();
|
||||
}
|
||||
const formRules = { // 表单校验
|
||||
url: [
|
||||
{ required: true, message: '请填写WebHook的url信息', trigger: 'blur' },
|
||||
{ max: nameMaxLen, message: 'WebHook的url信息长度不能超过500' },
|
||||
{ message: '请输入正确的URL地址', pattern: urlRegExp },
|
||||
],
|
||||
content_type: [{ required: true, message: '请选择POST请求内容类型', trigger: 'blur' }],
|
||||
token: [
|
||||
{ required: !props.editable, message: '请输入Token', trigger: 'blur' },
|
||||
{ message: 'Token不能包含中文、空格、*', pattern: regex },
|
||||
{ max: 2000, message: 'WebHook的Token信息长度不能超过2000' }
|
||||
]
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="web-hook-edit g-content-card p-20">
|
||||
<d-form class="max-w-[900px]" :pop-position="['right']" :data="form" layout="vertical" ref="formRef" :rules="formRules">
|
||||
<d-form-item field="url" label="URL">
|
||||
<d-input maxlength="500" v-model="form.url" placeholder="请输入 WebHook 的 URL 地址" v-on:keypress="restricVal" v-on:paste="restricVal">
|
||||
<template #suffix><span>{{form.url?.length}}/{{ nameMaxLen }}</span></template>
|
||||
</d-input>
|
||||
</d-form-item>
|
||||
<d-form-item field="token" label="Token">
|
||||
<div v-if="editable">
|
||||
<div class="text-CG600 text-[14px] mb-[10px]">
|
||||
如果你忘记了之前设置的Token, 你可以重新添加添加一个Token, 为避免影响正常使用, 请将所有应用到该Token的配置更新为最新的Token。
|
||||
</div>
|
||||
<d-input maxlength="2000" placeholder="请输入Token" v-model="hookToken" @change="handleChange" @focus="handleFocus" @blur="handleBlur" @input="handleInput">
|
||||
<template #suffix><span>{{hookToken?.length}}/2000</span></template>
|
||||
</d-input>
|
||||
</div>
|
||||
<d-input maxlength="2000" placeholder="请输入Token" v-model="form.token" v-else>
|
||||
<template #suffix><span>{{form.token?.length}}/2000</span></template>
|
||||
</d-input>
|
||||
</d-form-item>
|
||||
<d-form-item field="content_type" class="no-label">
|
||||
<div class="flex items-center">
|
||||
<div class="text-G900 mr-[10px]">POST 请求内容类型</div>
|
||||
<d-select
|
||||
class="web-hook-select"
|
||||
v-model="form.content_type"
|
||||
:placeholder="form.content_type"
|
||||
>
|
||||
<gc-option v-for="(item,index) of contentTypeList.filter(name => name !== form.content_type)" :key="index" :value="item" :title="item">
|
||||
<span>{{ item }}</span>
|
||||
</gc-option>
|
||||
</d-select>
|
||||
</div>
|
||||
</d-form-item>
|
||||
|
||||
<div class="my-[10px]">事件类型</div>
|
||||
|
||||
<div class="webhook-checkbox flex flex-wrap">
|
||||
<d-checkbox class="my-[10px] w-[25%]" v-for="item of customEventList" :key="item.key" v-model="form[item.key as WebHookEventKey]">{{ item.title }}</d-checkbox>
|
||||
</div>
|
||||
|
||||
<d-checkbox class="my-[10px]" v-model="form.active">
|
||||
<span class="inline-block mr-[1px]">激活</span>
|
||||
<span class="text-G600">(当以上指定事件触发时将发送请求)</span>
|
||||
</d-checkbox>
|
||||
<div class="flex items-center mt-5 justify-end">
|
||||
<d-button class="mr-4 w-[64px]" @click="handleCancel"><span>取消</span></d-button>
|
||||
<d-button variant="solid" :loading="loading" @click="handleClick">{{ editable ? '保存' : ' 创建' }}</d-button>
|
||||
</div>
|
||||
</d-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.web-hook-edit {
|
||||
.web-hook-select {
|
||||
width: 320px;
|
||||
input::placeholder {
|
||||
color: var(--color-G900);
|
||||
}
|
||||
}
|
||||
.webhook-checkbox {
|
||||
.devui-checkbox label>span.devui-checkbox__label-text {
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
.devui-form__item--vertical {
|
||||
margin-bottom: 10px;
|
||||
&:nth-of-type(3) {
|
||||
.devui-form__label--vertical {
|
||||
height: 0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.no-label{
|
||||
:deep(.devui-form__label){
|
||||
display:none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
92
src/components/Setting/WebHookEdit/customEventList.ts
Normal file
92
src/components/Setting/WebHookEdit/customEventList.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
export const customEventList = [
|
||||
{
|
||||
key: 'push_events',
|
||||
title: '推送事件'
|
||||
},
|
||||
{
|
||||
key: 'merge_requests_events',
|
||||
title: 'Pull Request事件'
|
||||
// desc: '合并请求请求事件,包括合并请求的新建、分配负责人、取消负责人、关闭、转换成草稿(WIP)、新增 Label、移除 Label、新增里程碑、移除里程碑、重新打开事件'
|
||||
},
|
||||
{
|
||||
key: 'tag_push_events',
|
||||
title: 'Tag推送事件'
|
||||
},
|
||||
{
|
||||
key: 'note_events',
|
||||
title: '评论事件'
|
||||
// desc: '新建分支或新建 Tag'
|
||||
}
|
||||
// {
|
||||
// title: '删除分支或 Tag',
|
||||
// desc: '删除分支或删除 Tag'
|
||||
// },
|
||||
// {
|
||||
// title: '保护分支更新',
|
||||
// desc: '新增、删除或修改保护分支设置'
|
||||
// },
|
||||
// {
|
||||
// title: 'Forks',
|
||||
// desc: '项目的 Fork 事件'
|
||||
// },
|
||||
// {
|
||||
// title: '项目成员更新',
|
||||
// desc: '新增、移除项目成员,或项目成员权限变更'
|
||||
// },
|
||||
// {
|
||||
// title: 'Star',
|
||||
// desc: '项目新增 Star 或取消 Star'
|
||||
// },
|
||||
// {
|
||||
// key: 'issues_events',
|
||||
// title: 'Issues',
|
||||
// desc: 'Issues 事件,包括 Issue 的新增,编辑、删除、转移、置顶、取消置顶、关闭、重新打开、分配负责人、取消负责人、新增 Label、移除 Label、新增里程碑、移除里程碑、锁定和取消锁定事件'
|
||||
// },
|
||||
// {
|
||||
// title: '提交评论',
|
||||
// desc: '提交或 Diff 的评论事件'
|
||||
// },
|
||||
// {
|
||||
// title: '里程碑',
|
||||
// desc: '里程碑事件,包括里程碑的新增、关闭、打开、编辑和删除事件'
|
||||
// },
|
||||
// {
|
||||
// title: '看板',
|
||||
// desc: '新增、更新和删除看板'
|
||||
// },
|
||||
// {
|
||||
// title: 'Pages',
|
||||
// desc: 'Pages 站点更新'
|
||||
// },
|
||||
// {
|
||||
// title: '看板事件',
|
||||
// desc: '看板内容更新事件,包括新增、编辑、删除、归档、恢复、转换成 Issue 和排序'
|
||||
// },
|
||||
// {
|
||||
// key: 'push_events',
|
||||
// title: 'push 事件',
|
||||
// desc: '代码库的 git push 事件'
|
||||
// },
|
||||
// {
|
||||
// key: 'change_request_events',
|
||||
// title: '合并请求评审状态事件',
|
||||
// desc: '合并请求的代码评审事件(评审通过/不通过相关)'
|
||||
// },
|
||||
// {
|
||||
// title: '代码评审评论',
|
||||
// desc: '代码评审的评论事件'
|
||||
// },
|
||||
// {
|
||||
// key: 'wiki_page_events',
|
||||
// title: 'Wiki',
|
||||
// desc: 'Wiki 更新'
|
||||
// }
|
||||
// {
|
||||
// title: '项目导入',
|
||||
// desc: '项目导入成功、失败或取消'
|
||||
// },
|
||||
// {
|
||||
// title: '公开项目',
|
||||
// desc: '项目由私密项目更改为公开项目'
|
||||
// }
|
||||
];
|
||||
265
src/components/Setting/WebHookLog/Index.vue
Normal file
265
src/components/Setting/WebHookLog/Index.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import LogItem from './LogItem.vue';
|
||||
import LogDetail from './LogDetail.vue';
|
||||
import { convertRgbaColor } from '@/utils/color';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const props = defineProps<{ logs: any[], getWebhookLogDetail: Function, repoId: string | null }>();
|
||||
|
||||
const display = ref('');
|
||||
const route = useRoute();
|
||||
const { webhookId } = route.params;
|
||||
const tab = ref('request' as 'request' | 'response');
|
||||
|
||||
interface RequestConfig {
|
||||
header: string,
|
||||
body: string
|
||||
}
|
||||
|
||||
const request = reactive<RequestConfig>({
|
||||
header: '',
|
||||
body: ''
|
||||
});
|
||||
const response = reactive<RequestConfig>({
|
||||
header: '',
|
||||
body: ''
|
||||
});
|
||||
|
||||
const successColor = '#0EB07B';
|
||||
const failColor = '#F2050D';
|
||||
|
||||
const maxLen = ref(80);
|
||||
|
||||
const getColorByCode = (code: string) => {
|
||||
if (Number(code) < 300) {
|
||||
return {
|
||||
color: successColor,
|
||||
bgColor: convertRgbaColor(successColor, 0.1)[0]
|
||||
};
|
||||
}
|
||||
return {
|
||||
color: failColor,
|
||||
bgColor: convertRgbaColor(failColor, 0.1)[0]
|
||||
};
|
||||
};
|
||||
|
||||
function formatObject(obj: Record<string, any>, indent = 1, maxLineWidth = maxLen.value): string {
|
||||
const formattedProperties: string[] = [];
|
||||
for (const key in obj) {
|
||||
const value = obj[key];
|
||||
formattedProperties.push(formatProperty(key, value, indent, maxLineWidth) as string);
|
||||
}
|
||||
const filterEmpty = formattedProperties.filter(item => item);
|
||||
if (filterEmpty?.length) {
|
||||
return `{\n${filterEmpty.join(',\n')}\n${' '.repeat((indent - 1) * 2)}}`;
|
||||
} else {
|
||||
return `{}`;
|
||||
}
|
||||
}
|
||||
|
||||
function formatArray(arr: any[], indent: number, maxLineWidth = maxLen.value): string {
|
||||
if (arr.length === 0) {
|
||||
return '[]';
|
||||
}
|
||||
const formattedItems: string[] = [];
|
||||
for (const item of arr) {
|
||||
formattedItems.push(formatProperty('', item, indent, maxLineWidth) as string);
|
||||
}
|
||||
const filterEmpty = formattedItems.filter(item => item);
|
||||
if (filterEmpty?.length) {
|
||||
return `[\n${filterEmpty.join(',\n')}\n${' '.repeat((indent - 1) * 2)}]`;
|
||||
} else {
|
||||
return '[]';
|
||||
}
|
||||
}
|
||||
|
||||
function formatProperty(key: string, value: any, indent: number, maxLineWidth: number): string | undefined {
|
||||
const indentation = ' '.repeat(indent * 2);
|
||||
const keyString = key ? `${indentation}"${key}": ` : indentation;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const formattedArray = formatArray(value, indent + 1, maxLineWidth);
|
||||
return `${keyString}${formattedArray}`;
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
const formattedObject = formatObject(value, indent + 1, maxLineWidth);
|
||||
return `${keyString}${formattedObject}`;
|
||||
} else if (typeof value === 'object' && value === null) {
|
||||
return `${keyString}null`;
|
||||
} else if (typeof value === 'number') {
|
||||
return `${keyString}${value}`;
|
||||
} else if (typeof value === 'boolean') {
|
||||
return `${keyString}${value}`;
|
||||
} else { /** string */
|
||||
const formattedValue = `${value}`;
|
||||
try {
|
||||
const deepObj = JSON.parse(formattedValue);
|
||||
if (Array.isArray(value)) {
|
||||
const formattedArray = formatArray(deepObj, indent + 1, maxLineWidth);
|
||||
return `${keyString}${formattedArray}`;
|
||||
} else if (typeof deepObj === 'object' && deepObj !== null) {
|
||||
const formattedObject = formatObject(deepObj, indent + 1, maxLineWidth);
|
||||
return `${keyString}${formattedObject}`;
|
||||
}
|
||||
} catch (e) {
|
||||
if (!formattedValue) {
|
||||
return `${keyString}""`;
|
||||
} else {
|
||||
const str = formattedValue.replace(/\n/g, '');
|
||||
if (str.length <= maxLineWidth - keyString.length - 2) {
|
||||
return `${keyString}"${str}"`;
|
||||
} else {
|
||||
const lines = [];
|
||||
const maxLineLength = maxLineWidth - keyString.length - 2; // 2 for the quotes
|
||||
const chunks = chunkString(str, maxLineLength);
|
||||
lines.push(keyString + `"${chunks[0]}`);
|
||||
for (let i = 1; i < chunks.length; i++) {
|
||||
lines.push(`${' '.repeat(keyString.length + 1)}${chunks[i]}`);
|
||||
}
|
||||
return `${lines.join('\n')}"`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function chunkString(str: string, chunkSize: number): string[] {
|
||||
const chunks: string[] = [];
|
||||
for (let i = 0; i < str.length; i += chunkSize) {
|
||||
chunks.push(str.substr(i, chunkSize));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
const getHookSize = () => {
|
||||
const element = document.getElementsByClassName('web-hook-collapse');
|
||||
if (element.length) {
|
||||
const width = element[0].clientWidth - 70;
|
||||
const len = Math.ceil(width / 8.5);
|
||||
maxLen.value = len >= 110 ? 110 : len;
|
||||
} else {
|
||||
maxLen.value = 110;
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = async(val: string) => {
|
||||
if (val) {
|
||||
display.value = '';
|
||||
setTimeout(() => {
|
||||
display.value = val;
|
||||
}, 300);
|
||||
}
|
||||
const item = props.logs.find(l => l.uuid === val);
|
||||
tab.value = 'request';
|
||||
if (item?.id) {
|
||||
const res = await props.getWebhookLogDetail({ repoId: props.repoId, hook_id: webhookId as string, id: item.id as string });
|
||||
if (!res.error) {
|
||||
const { request_headers, response_headers, request_data, response_body } = res.data.data;
|
||||
if (Object.prototype.hasOwnProperty.call(request_headers, 'X-GitCode-Token')) {
|
||||
request_headers['X-GitCode-Token'] = '********';
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(response_headers, 'X-GitCode-Token')) {
|
||||
response_headers['X-GitCode-Token'] = '********';
|
||||
}
|
||||
request.header = '\n' + formatObject(request_headers) + '\n\n';
|
||||
request.body = '\n' + formatObject(request_data) + '\n\n';
|
||||
response.header = '\n' + formatObject(response_headers) + '\n\n';
|
||||
response.body = '\n' + response_body?.toString() + '\n';
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
getHookSize();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="web-hook-collapse">
|
||||
<d-collapse v-model="display" accordion @change="handleChange">
|
||||
<d-collapse-item v-for="item in logs" :key="item.uuid" :name="item.uuid" class="my-[10px]">
|
||||
<template #title>
|
||||
<LogItem :log="item" />
|
||||
</template>
|
||||
<div class="relative">
|
||||
<div class="absolute inline-flex items-center right-[18px] top-[4px]">
|
||||
<!-- <d-button icon="undo" variant="text" class="mr-[18px]">重新发送请求</d-button> -->
|
||||
<span class="text-G900 text-[14px]">请求响应返回时间:{{ item.execution_duration }}s</span>
|
||||
</div>
|
||||
<d-tabs v-model="tab" type="wrapped">
|
||||
<d-tab id="request" title="请求信息">
|
||||
<LogDetail type="request" :res="request" />
|
||||
</d-tab>
|
||||
|
||||
<d-tab id="response">
|
||||
<template #title>
|
||||
<div class="inline-flex items-center">
|
||||
<span class="inline-block mr-[8px]">响应返回</span>
|
||||
<div class="log-tag log-item-tag" :style="{background: getColorByCode(item.response_status).bgColor, color: getColorByCode(item.response_status).color}">{{ item.response_status }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<LogDetail type="response" :res="response" />
|
||||
</d-tab>
|
||||
</d-tabs>
|
||||
</div>
|
||||
</d-collapse-item>
|
||||
</d-collapse>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.web-hook-collapse {
|
||||
.devui-collapse {
|
||||
background-color: transparent;
|
||||
overflow-y: hidden;
|
||||
box-shadow: none;
|
||||
&__item-content {
|
||||
height: auto !important;
|
||||
padding: 0 10px;
|
||||
border-style: solid;
|
||||
border-color: var(color-G300);
|
||||
border-width: 1px;
|
||||
border-top-width: 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
&__item-title {
|
||||
border: 1px solid var(--color-G300);
|
||||
box-shadow: 0px 2px 3px 0px rgba(148,163,184,0.05);
|
||||
border-radius: 3px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
&--open {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
.devui-tabs__nav--wrapped {
|
||||
padding-left: 1px;
|
||||
}
|
||||
.devui-tabs__nav--wrapped>li.active {
|
||||
span {
|
||||
color: var(--color-G900);
|
||||
}
|
||||
}
|
||||
.devui-tabs__nav--wrapped>li {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
border-bottom-width: 0;
|
||||
span {
|
||||
color: var(--color-CG500);
|
||||
}
|
||||
}
|
||||
}
|
||||
.log-tag {
|
||||
@apply box-border rounded-[3px] text-[12px] h-[18px] leading-[18px] text-center;
|
||||
}
|
||||
.log-item-tag {
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
48
src/components/Setting/WebHookLog/LogDetail.vue
Normal file
48
src/components/Setting/WebHookLog/LogDetail.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { LogDetailRes } from './type';
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'request' | 'response';
|
||||
res: LogDetailRes;
|
||||
}>();
|
||||
|
||||
const typeComputed = computed(() => {
|
||||
const type = props.type;
|
||||
const first = type[0].toUpperCase();
|
||||
const left = type.substring(1);
|
||||
return first + left;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="box-border px-[18px]">
|
||||
<section>
|
||||
<div class="header">{{ typeComputed }} Header</div>
|
||||
<pre class="web-hook-log-box">{{ res.header }}</pre>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="header my-[12px]">{{ typeComputed }} Body</div>
|
||||
<pre class="web-hook-log-box">{{ res.body }}</pre>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
div, pre {
|
||||
@apply text-[13px];
|
||||
}
|
||||
.header {
|
||||
color: var(--color-G900);
|
||||
@apply text-[14px] mb-[12px];
|
||||
}
|
||||
.web-hook-log-box {
|
||||
color: var(--color-CG600);
|
||||
background: var(--color-G100);
|
||||
border: 1px solid var(--color-G200);
|
||||
white-space: pre;
|
||||
@apply box-border px-[24px] rounded-[3px] w-[100%] overflow-x-auto;
|
||||
}
|
||||
</style>
|
||||
71
src/components/Setting/WebHookLog/LogItem.vue
Normal file
71
src/components/Setting/WebHookLog/LogItem.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { convertRgbaColor } from '@/utils/color';
|
||||
|
||||
defineProps<{
|
||||
log: Record<string, any>;
|
||||
}>();
|
||||
|
||||
const successColor = '#0EB07B';
|
||||
const failColor = '#F2050D';
|
||||
|
||||
const getColorByCode = (code: string) => {
|
||||
if (Number(code) < 300) {
|
||||
return {
|
||||
color: successColor,
|
||||
bgColor: convertRgbaColor(successColor, 0.1)[0]
|
||||
};
|
||||
}
|
||||
return {
|
||||
color: failColor,
|
||||
bgColor: convertRgbaColor(failColor, 0.1)[0]
|
||||
};
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="log-item">
|
||||
<div class="flex-1 flex items-center">
|
||||
<div class="log-tag log-item-tag" :style="{background: getColorByCode(log.response_status).bgColor, color: getColorByCode(log.response_status).color}">{{ log.response_status }}</div>
|
||||
<d-popover :content="log.uuid" trigger="hover">
|
||||
<div class="log-item-label">{{ log.uuid }}</div>
|
||||
</d-popover>
|
||||
<div class="log-tag log-item-event">{{ log.trigger }}</div>
|
||||
</div>
|
||||
<div class="text-CG500 text-[14px]">
|
||||
{{ log.created_at }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-right: 24px;
|
||||
}
|
||||
.log-tag {
|
||||
@apply box-border rounded-[3px] text-[12px] h-[18px] leading-[18px] text-center;
|
||||
}
|
||||
.log-item-tag {
|
||||
max-width: 100px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0px 4px;
|
||||
width: auto!important;
|
||||
}
|
||||
.log-item-label {
|
||||
color: var(--color-G900);
|
||||
margin: 0 16px;
|
||||
width: 260px;
|
||||
@apply truncate text-[14px];
|
||||
}
|
||||
.log-item-event {
|
||||
background-color: var(--color-CG200);
|
||||
color: var(--color-G900);
|
||||
@apply px-[5px];
|
||||
}
|
||||
</style>
|
||||
11
src/components/Setting/WebHookLog/type.ts
Normal file
11
src/components/Setting/WebHookLog/type.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface LogItemProp {
|
||||
uuid: string;
|
||||
code: number;
|
||||
event: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export interface LogDetailRes {
|
||||
header: string;
|
||||
body: string;
|
||||
}
|
||||
117
src/components/Setting/WebHookOverview/Index.vue
Normal file
117
src/components/Setting/WebHookOverview/Index.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef, onMounted, computed, defineProps } from 'vue';
|
||||
import WebHookItem from './WebHookItem.vue';
|
||||
import { Message } from 'vue-devui/message';
|
||||
|
||||
const visible = ref(false);
|
||||
const currentId = ref('');
|
||||
const loading = ref(true);
|
||||
|
||||
// 请求API都由父级决定 抽离方便复用
|
||||
const props = withDefaults(defineProps<{
|
||||
getRepoWebhooks: Function,
|
||||
editWebhook: Function,
|
||||
deleteWebHook: Function,
|
||||
repoId: string | null
|
||||
}>(), {
|
||||
getRepoWebhooks: () => {},
|
||||
editWebhook: () => {},
|
||||
deleteWebHook: () => {},
|
||||
});
|
||||
|
||||
const empty = computed(() => {
|
||||
return !loading.value && !webHookList.value.length;
|
||||
});
|
||||
|
||||
const emit = defineEmits(['create', 'edit']);
|
||||
|
||||
const webHookList = shallowRef<any[]>([]);
|
||||
|
||||
const getConfig = async() => {
|
||||
const res = await props.getRepoWebhooks({ repoId: props.repoId, conf: { page: 1, per_page: 50 }});
|
||||
if (!res.error) {
|
||||
webHookList.value = res?.data?.data;
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const handleDeal = async(data: Record<string, any>) => {
|
||||
const { hook_id, ...config } = data;
|
||||
const res = await props.editWebhook({ repoId: props.repoId, hook_id, conf: config });
|
||||
if (!res.error) {
|
||||
Message.success('操作成功');
|
||||
getConfig();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDel = (data: { hook_id: string }) => {
|
||||
const { hook_id } = data;
|
||||
visible.value = true;
|
||||
currentId.value = hook_id;
|
||||
};
|
||||
|
||||
const handleConfirm = async() => {
|
||||
|
||||
const res = await props.deleteWebHook({ repoId: props.repoId, hook_id: currentId.value });
|
||||
if (!res.error) {
|
||||
Message.success('webhook移除成功');
|
||||
getConfig();
|
||||
visible.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (data: any) => {
|
||||
emit('edit', data);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getConfig();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="box-border py-[24px]">
|
||||
<div class="repo-settings-title">
|
||||
<div>WebHook</div>
|
||||
</div>
|
||||
<div class="g-content-card pb-[18px]">
|
||||
<div class="text-sm mb-[18px] flex items-center justify-between p-[18px] border-box border-b-[1px] border-solid border-[var(--color-CG200)]">
|
||||
<div class="mr-2">
|
||||
WebHook 将允许 GitCode 向你的外部服务进行通知,当某些特定事件发生时,我们将向你指定的 URL中发送一个 POST 请求。
|
||||
</div>
|
||||
<d-button variant="solid" color="primary" icon="add" @click="emit('create')">新建 WebHook</d-button>
|
||||
</div>
|
||||
<DataPanel :loading="loading" :empty="empty" :skeleton="loading" :card="false">
|
||||
<div class="border-box px-[18px]">
|
||||
<div class="repo-table-item" v-for="hook of webHookList" :key="hook.id">
|
||||
<WebHookItem
|
||||
:hook="hook"
|
||||
@edit="handleEdit"
|
||||
@run="handleDeal"
|
||||
@pause="handleDeal"
|
||||
@delete="handleDel"
|
||||
></WebHookItem>
|
||||
</div>
|
||||
</div>
|
||||
</DataPanel>
|
||||
</div>
|
||||
<GModal v-model="visible" title="你正在删除webhook" @confirm="handleConfirm">
|
||||
<div class="px-[8px]">删除后将无法恢复, 确认继续么?</div>
|
||||
</GModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.repo-table-item {
|
||||
&:nth-last-of-type(1) {
|
||||
@apply border-b-[1px];
|
||||
}
|
||||
@apply flex items-center py-[14px] px-[24px] justify-between border-t-[1px] border-l-[1px] border-r-[1px] border-solid border-[var(--color-CG200)];
|
||||
}
|
||||
.g-content-card{
|
||||
.devui-button{
|
||||
min-width: 155px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
165
src/components/Setting/WebHookOverview/WebHookItem.vue
Normal file
165
src/components/Setting/WebHookOverview/WebHookItem.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
import { WebHookState } from './type';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const emits = defineEmits(['edit', 'run', 'pause', 'delete']);
|
||||
|
||||
const props = defineProps<{ hook: { state: WebHookState, [x: string]: any }}>();
|
||||
|
||||
const onRun = () => {
|
||||
const { url, content_type, push_events, tag_push_events, merge_requests_events, note_events } = props.hook;
|
||||
emits('run', { hook_id: props.hook.id as string, active: true, url, content_type, push_events, tag_push_events, merge_requests_events, note_events, enable_ssl_verification: true });
|
||||
};
|
||||
|
||||
const onPause = () => {
|
||||
const { url, content_type, push_events, tag_push_events, merge_requests_events, note_events } = props.hook;
|
||||
emits('pause', { hook_id: props.hook.id as string, active: false, url, content_type, push_events, tag_push_events, merge_requests_events, note_events, enable_ssl_verification: true });
|
||||
};
|
||||
|
||||
const onDel = () => {
|
||||
emits('delete', { hook_id: props.hook.id as string });
|
||||
};
|
||||
|
||||
const onEdit = () => {
|
||||
emits('edit', { hook_id: props.hook.id as string });
|
||||
};
|
||||
|
||||
const defaultTags = [
|
||||
{
|
||||
name: '推送事件',
|
||||
value: 'push_events'
|
||||
},
|
||||
{
|
||||
name: 'Tag推送事件',
|
||||
value: 'tag_push_events'
|
||||
},
|
||||
{
|
||||
name: 'Pull Request事件',
|
||||
value: 'merge_requests_events'
|
||||
},
|
||||
{
|
||||
name: '评论事件',
|
||||
value: 'note_events'
|
||||
}
|
||||
];
|
||||
|
||||
const tags = computed(() => {
|
||||
return defaultTags.filter((item) => {
|
||||
const key = item.value;
|
||||
return props.hook[key];
|
||||
});
|
||||
});
|
||||
|
||||
// 对应 devui 完整色板色值:bg-5, font-70
|
||||
// const hookState2BgColorStyleMap = {
|
||||
// [WebHookState.Pass]: '#f0ffe6',
|
||||
// [WebHookState.PassWithWarn]: '#fff3e8',
|
||||
// [WebHookState.Fail]: '#ffeeed'
|
||||
// };
|
||||
|
||||
// const hookState2ColorStyleMap = {
|
||||
// [WebHookState.Pass]: '#7eba50',
|
||||
// [WebHookState.PassWithWarn]: '#e37d29',
|
||||
// [WebHookState.Fail]: '#c73636'
|
||||
// };
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="web-hook-item">
|
||||
<div class="hook-info overflow-hidden" @click="onEdit">
|
||||
<div class="hook-url" :title="hook.url">
|
||||
{{ hook.url }}
|
||||
</div>
|
||||
<div class="hook-tags break-all leading-5 text-CG500 cursor-pointer">
|
||||
{{ tags.map(item => item.name).join('・ ') }}
|
||||
<!-- <li class="hook-tag" v-for="tag of tags" :key="tag.value">
|
||||
<CustomTag :title="tag.name" />
|
||||
</li> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="state">
|
||||
<CustomTag
|
||||
:title="hook.state"
|
||||
icon="connect"
|
||||
:bg-color="hookState2BgColorStyleMap[hook.state]"
|
||||
:icon-color="hookState2ColorStyleMap[hook.state]"
|
||||
:color="hookState2ColorStyleMap[hook.state]"
|
||||
/>
|
||||
</div> -->
|
||||
|
||||
<div class="tools">
|
||||
<!-- <button @click="onEdit">
|
||||
<d-icon name="edit" color="$devui-form-control-line"></d-icon>
|
||||
</button> -->
|
||||
<button v-if="!hook.active" @click="onRun">
|
||||
<d-popover content="激活hook" :position="['top']" trigger="hover">
|
||||
<Icon name="gt-run" color="#0EB07B" class="g-pointer"/>
|
||||
</d-popover>
|
||||
</button>
|
||||
<button v-else @click="onPause">
|
||||
<d-popover content="停止hook" :position="['top']" trigger="hover">
|
||||
<Icon name="gt-suspend" color="#707A87" class="g-icon-pointer "/>
|
||||
</d-popover>
|
||||
</button>
|
||||
<button @click="onDel">
|
||||
<d-popover content="移除hook" :position="['top']" trigger="hover">
|
||||
<Icon name="gt-delete" color="#707A87" class="g-icon-pointer "/>
|
||||
</d-popover>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.web-hook-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.web-hook-item > *:not(:last-child) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.hook-info {
|
||||
flex: 1;
|
||||
margin-right: 48px;
|
||||
cursor: pointer;
|
||||
width: 0;
|
||||
}
|
||||
.hook-url {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--color-G900);
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.hook-tag {
|
||||
// font-size: $devui-font-size-sm;
|
||||
line-height: 1.75em;
|
||||
// border-radius: $g-commit-border-radius;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.tools > *:not(:last-child) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.state,
|
||||
.tools {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
.tools > button {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
font-size: 20px;
|
||||
// border: 1px solid $devui-list-item-hover-bg;
|
||||
// border-radius: $g-commit-border-radius;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
12
src/components/Setting/WebHookOverview/type.ts
Normal file
12
src/components/Setting/WebHookOverview/type.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export enum WebHookState {
|
||||
Pass = '通过',
|
||||
Fail = '未通过',
|
||||
PassWithWarn = '通过(有告警)'
|
||||
}
|
||||
|
||||
export interface WebHook {
|
||||
uuid: string;
|
||||
url: string;
|
||||
tags: string[];
|
||||
state: WebHookState;
|
||||
}
|
||||
10
src/components/Setting/index.ts
Normal file
10
src/components/Setting/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { default as SettingTitle } from './SettingTitle/index.vue';
|
||||
export { default as SettingText } from './SettingText/index.vue';
|
||||
export { default as SettingItem } from './SettingItem/index.vue';
|
||||
export { default as DeleteTip } from './DeleteTip/index.vue';
|
||||
export { default as GModal } from './GModal/index.vue';
|
||||
export { default as CreateTags } from './CreateTags/index.vue';
|
||||
export { default as WebHookOverview } from './WebHookOverview/Index.vue';
|
||||
export { default as WebHookEdit } from './WebHookEdit/Index.vue';
|
||||
export { default as WebHookLog } from './WebHookLog/Index.vue';
|
||||
export { default as ImgRadio } from './ImgRadio/Index.vue';
|
||||
5
src/components/Setting/index.vue
Normal file
5
src/components/Setting/index.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
设置相关组件放这里
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user