<script lang="ts" setup>
import { ref, reactive, onMounted, watch, computed } from 'vue';
import { useRouter } from 'vue-router';
import { debounce } from 'lodash-es';
import { useI18n } from 'vue-i18n';
import { useLangStore } from '@/stores';
import { GroupInfo } from '@/shared/@types/type-sig';
import {
getUrlParam,
handleUploadImage,
statisticalPoint,
rules,
emailRules,
privacyRules,
codeRules,
} from '@/shared';
import {
getReposData,
getIssueSelectOption,
verifySubmitterEmail,
createIssue,
uploadIssueFile,
} from '@/api/api-quick-issue';
import { OptionList, IssueData } from '@/shared/@types/type-quick-issue';
import { getSigLandscape } from '@/api/api-sig';
import AppEditor from '@/components/AppEditor.vue';
import AppSlideVerify from '@/components/AppSlideVerify.vue';
import AppContent from '@/components/AppContent.vue';
import SigLandscapeFeature from '@/components/SigLandscapeFeature.vue';
import OIcon from 'opendesign/icon/OIcon.vue';
import { ElMessage, genFileId } from 'element-plus';
import type {
FormInstance,
UploadUserFile,
UploadInstance,
UploadProps,
UploadRawFile,
} from 'element-plus';
import IconGitee from '~icons/app/icon-gitee.svg';
import IconDown from '~icons/app/icon-pulldown.svg';
import IconAdd from '~icons/app/icon-add.svg';
import IconSearch from '~icons/app/icon-search.svg';
interface TypesList {
id: number;
name: string;
template: string;
}
const router = useRouter();
const formRef = ref<FormInstance>();
const { t } = useI18n();
const landscapeInfo = ref<GroupInfo[]>([]);
const isMenuShown = ref(false);
const isSlideVerifyShown = ref(false);
const lang = computed(() => {
return useLangStore().lang;
});
const content = ref(t('quickIssue.SEND_CODE'));
const totalTime = ref(60);
const isGiteeUser = ref(false);
const fileList = ref<UploadUserFile[]>([]);
const upload = ref<UploadInstance>();
const clock = ref();
const reposList = ref<OptionList>({
page: 1,
data: [],
keyword: '',
});
const typesList = ref<Array<TypesList>>();
const repoParams = reactive({
page: 1,
per_page: 40,
keyword: '',
public: true,
status: '开始',
sig: '',
total: 0,
});
const issueData: IssueData = reactive({
title: decodeURI(getUrlParam('title')) || '',
issue_type_id: '',
sig: '',
project_id: Number(getUrlParam('repo_id')) || 7642569,
repo: getUrlParam('repo') || 'opengauss/community',
email: '',
code: '',
description: '',
privacy: [],
});
function getSigValue(val: string) {
if (issueData.sig && issueData.sig === val) {
isMenuShown.value = false;
return false;
}
issueData.sig = val;
isMenuShown.value = false;
repoParams.sig = val;
repoParams.page = 1;
repoParams.keyword = '';
reposList.value.keyword = '';
reposList.value.data = [];
issueData.repo = '';
}
function getRepoBySigName() {
getReposData(repoParams).then((res) => {
if (res.data) {
reposList.value.total = res.total;
reposList.value.data = [...reposList.value.data, ...res.data];
} else if (!res.total && !repoParams.keyword) {
ElMessage({
message: t('quickIssue.EMPTY_REPO'),
type: 'warning',
duration: 10000,
});
issueData.repo = 'opengauss/community';
issueData.project_id = 7642569;
}
});
}
function changeStash() {
isGiteeUser.value = !isGiteeUser.value;
}
function changeEmail() {
if (totalTime.value !== 60) {
window.clearInterval(clock.value);
content.value = t('quickIssue.SEND_CODE');
totalTime.value = 60;
}
}
async function getCodeByEmail(verify: FormInstance | undefined) {
if (!verify) {
return;
}
rules.code = [];
rules.privacy = privacyRules;
rules.email = emailRules;
verify.validate(async (res) => {
if (totalTime.value === 60 && res) {
isSlideVerifyShown.value = true;
}
});
}
function sendVerifyEmail() {
isSlideVerifyShown.value = false;
verifySubmitterEmail({ email: issueData.email }).then((res) => {
if (res?.code === 200) {
clock.value = window.setInterval(function () {
totalTime.value--;
content.value = t('quickIssue.RESEND1', [`${totalTime.value}s`]);
if (totalTime.value < 0) {
//当倒计时小于0时清除定时器
window.clearInterval(clock.value);
content.value = t('quickIssue.RESEND');
totalTime.value = 60;
}
}, 1000);
ElMessage({
message: t('quickIssue.SUCCESS_SEND_MAIL'),
type: 'success',
});
} else {
ElMessage({
message: res?.msg,
type: 'error',
});
}
});
}
async function goGitee(verify: FormInstance | undefined) {
if (!verify) {
return;
}
rules.email = [];
rules.code = [];
rules.privacy = [];
verify.validate(async (res: boolean) => {
if (res) {
const url = `https://gitee.com/${issueData.repo}/issues/new?title=${issueData.title}&issue%5Bissue_type_id%5D=${issueData.issue_type_id}`;
// 埋点数据统计
const sensors = (window as any)['sensorsDataAnalytic201505'];
sensors?.setProfile({
profileType: 'toGiteeCreateIssue',
...((window as any)['sensorsCustomBuriedData'] || {}),
$utm_source: 'quick_issue',
jump_url: url,
});
window.open(url);
}
});
}
async function submitForm(
verify: FormInstance | undefined,
isGoGitee: boolean
) {
if (!verify) {
return;
}
rules.privacy = privacyRules;
rules.email = emailRules;
rules.code = codeRules;
verify.validate(async (res: boolean) => {
if (res) {
const parmes = JSON.parse(JSON.stringify(issueData));
// 邮箱隐藏 添加提交人邮箱
parmes.description = `${
issueData.description
} \n \n -- submited by ${getHiddenEmail(issueData.email)}`;
handelCreatIssue(parmes, isGoGitee, verify);
} else {
verify.scrollToField('title');
}
});
}
function getHiddenEmail(email: string): string {
return `${email.split('@')[0]}@***${email.charAt(email.length - 1)}`;
}
function handelCreatIssue(
parmes: IssueData,
isGoGitee: boolean,
verify: FormInstance
) {
createIssue(parmes).then(async (res) => {
if (res.code === 201) {
if (fileList.value.length && fileList.value[0].raw) {
// 携带附件
await handleUpload(fileList.value[0].raw, res.data.issue_id).then(
(res) => {
if (res?.code === 200) {
ElMessage({
message: t('quickIssue.SUCCESS_UPLOAD'),
type: 'success',
});
} else {
ElMessage({
message: res?.msg,
type: 'error',
});
}
}
);
}
const jump_url = `https://gitee.com/${issueData.repo}/issues/${res.data.number}`;
// 埋点
statisticalPoint(jump_url, parmes.email);
if (isGoGitee) {
window.open(jump_url);
}
// 重置表单
resetForm(verify);
ElMessage({
message: t('quickIssue.SUCCESS_CREATED'),
type: 'success',
});
} else {
ElMessage({
message: res?.msg,
type: 'error',
});
}
});
}
function resetForm(verify: FormInstance) {
verify.resetFields();
fileList.value = [];
repoParams.sig = '';
issueData.privacy = ['true'];
issueData.description = '';
verify.scrollToField('title');
}
function optionClick(item: any) {
if (item?.enterprise_number) {
issueData.project_id = item.enterprise_number;
}
}
const handleClick = (path: string) => {
if (path.startsWith('https:')) {
window.open(path, '_blank');
} else {
router.push(path);
}
};
async function handleUpload(file: File, id: string) {
const formData = new FormData();
formData.append('file', file);
formData.append('attach_id', id);
return await uploadIssueFile(formData);
}
function sigValueChange(val: string) {
repoParams.sig = val;
repoParams.page = 1;
reposList.value.page = 1;
reposList.value.keyword = '';
reposList.value.data = [];
issueData.repo = '';
}
function onChange(rawFile: UploadUserFile) {
if (!rawFile?.size) {
return false;
}
if (rawFile.size / 1024 / 1024 > 10) {
ElMessage.warning(t('quickIssue.SIZE_LIMIT'));
fileList.value = [];
return false;
}
fileList.value[0] = rawFile;
}
function getNextPage() {
if (reposList.value.total) {
reposList.value.total > repoParams.page * repoParams.per_page
? repoParams.page++
: '';
}
}
function changeState(stash: boolean) {
if (!stash) {
repoParams.page = 1;
repoParams.keyword = '';
} else {
if (reposList.value.keyword) {
reposList.value.data = [];
repoParams.keyword = reposList.value.keyword;
}
}
}
function handleTypeChange(val: number) {
issueData.description =
typesList.value?.find((item) => item.id === val)?.template || '';
}
// element 单文件上传,新文件覆盖旧文件
const handleExceed: UploadProps['onExceed'] = (files) => {
upload.value?.clearFiles();
const file = files[0] as UploadRawFile;
file.uid = genFileId();
upload.value?.handleStart(file);
};
const debounceEvent = debounce(
(val) => {
if (val === undefined) {
return false;
}
if (val !== repoParams.keyword) {
reposList.value.page = 1;
reposList.value.data = [];
repoParams.page = 1;
repoParams.keyword = val;
}
},
500,
{
trailing: true,
}
);
onMounted(async () => {
getRepoBySigName();
try {
if (getUrlParam('sig')) {
issueData.sig = getUrlParam('sig');
repoParams.sig = issueData.sig;
}
await getIssueSelectOption('types', null).then((res) => {
typesList.value = res.data;
});
if (getUrlParam('type')) {
const targetType = typesList.value?.find((item) => {
return item.name === decodeURI(getUrlParam('type'));
});
issueData.issue_type_id = targetType?.id;
issueData.description = targetType?.template || '';
}
landscapeInfo.value = await getSigLandscape(lang.value);
} catch (err: any) {
console.error(err);
}
});
watch(
() => repoParams,
() => {
getRepoBySigName();
},
{
deep: true,
}
);
</script>
<template>
<AppContent class="submit-issue">
<div class="inline-box">
<h1 id="create-issue">{{ t('quickIssue.ISSUE_TITLE') }}</h1>
<el-form
ref="formRef"
:model="issueData"
:rules="rules"
label-position="left"
hide-required-asterisk
class="issue-form"
:class="lang === 'en' ? 'en-form' : ''"
>
<div class="form-liner">
<el-form-item
:label="t('quickIssue.TITLE')"
prop="title"
class="fill-width"
>
<OInput
v-model="issueData.title"
:placeholder="t('quickIssue.INPUT')"
></OInput>
</el-form-item>
<el-form-item :label="t('quickIssue.TYPE')" prop="issue_type_id">
<OSelect
v-model.string="issueData.issue_type_id"
:placeholder="t('quickIssue.SELECT')"
@change="handleTypeChange"
>
<ElOption
v-for="item in typesList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</OSelect>
</el-form-item>
</div>
<div class="form-liner">
<el-form-item label="SIG" prop="sig" class="fill-width">
<OInput
v-model="issueData.sig"
:placeholder="t('quickIssue.INPUT')"
@change="sigValueChange"
></OInput>
<OButton
class="select-sig-btn"
type="primary"
size="small"
@click="isMenuShown = true"
>{{ t('quickIssue.SELECT_SIG') }}</OButton
>
</el-form-item>
<!-- 仓库查询 -->
<el-form-item :label="t('quickIssue.REPO_NAME')" prop="repo">
<OSelect
v-model="issueData.repo"
:listener-scorll="true"
@scorll-bottom="getNextPage()"
@visible-change="changeState"
>
<template #prefix>
<OIcon><IconSearch /></OIcon>
</template>
<div class="search-box">
<OSearch
v-model="reposList.keyword"
:placeholder="t('quickIssue.SEARCH_PLACEHOLDER')"
style="padding: 0 8px"
@input="debounceEvent"
></OSearch>
</div>
<el-scrollbar>
<ElOption
v-if="!reposList.data.length"
label=""
value=""
:disabled="true"
style="text-align: center"
>
<span>no data</span>
</ElOption>
<ElOption
v-for="item in reposList.data"
:key="item.repo"
:label="item.repo"
:value="item.repo"
@click="optionClick(item)"
/>
</el-scrollbar>
</OSelect>
</el-form-item>
</div>
<div class="is-gitee-user">
<div class="gitee-user">
<OButton size="small" @click="goGitee(formRef)">
<template #prefixIcon>
<OIcon>
<IconGitee />
</OIcon>
</template>
{{ t('quickIssue.GITTE_USER') }}
</OButton>
</div>
<div class="unregistered" @click="changeStash">
<OButton size="small">{{ t('quickIssue.NOT_GITEE_USER') }}</OButton>
<OIcon class="icon-arrow" :class="isGiteeUser ? 'reversal' : ''">
<IconDown />
</OIcon>
</div>
</div>
<transition-group name="fadeHeight">
<div v-if="isGiteeUser" class="not-gitee-user">
<div class="form-liner editor">
<el-form-item
:label="t('quickIssue.DESCRIPTIVE')"
class="fill-width"
>
<AppEditor
v-model="issueData.description"
@upload-image="handleUploadImage"
>
</AppEditor>
</el-form-item>
</div>
<div class="form-liner verify-email">
<el-form-item :label="t('quickIssue.FILE')" class="upload-item">
<el-upload
ref="upload"
:on-change="onChange"
:multiple="false"
:auto-upload="false"
:file-list="fileList"
:on-exceed="handleExceed"
class="upload-file"
action=""
:limit="1"
>
<template #trigger>
<OIcon>
<IconAdd />
</OIcon>
</template>
</el-upload>
</el-form-item>
</div>
<div class="form-liner verify-email">
<el-form-item :label="t('quickIssue.EMAIL')" prop="email">
<OInput
v-model="issueData.email"
:placeholder="t('quickIssue.INPUT')"
@input="changeEmail()"
></OInput>
</el-form-item>
<el-form-item
:label="t('quickIssue.CODE')"
prop="code"
class="verify-code"
>
<OInput
v-model="issueData.code"
:placeholder="t('quickIssue.INPUT')"
></OInput>
<OButton
class="select-sig-btn"
type="primary"
size="small"
:disabled="totalTime !== 60"
@click="getCodeByEmail(formRef)"
>{{ content }}</OButton
>
</el-form-item>
</div>
<div class="form-liner form-radio">
<el-form-item prop="privacy">
<OCheckboxGroup v-model="issueData.privacy">
<OCheckbox value="true">
{{ t('quickIssue.PRIVACY_TEXT') }}
<a
:href="`https://www.opengauss.org/${lang}/privacyPolicy/`"
target="_blank"
>{{ t('quickIssue.PRIVACY') }}</a
>
</OCheckbox>
</OCheckboxGroup>
</el-form-item>
</div>
<div class="obuton-box">
<OButton
size="small"
type="primary"
@click="submitForm(formRef, true)"
>{{ t('quickIssue.CREATE') }}</OButton
>
<OButton
size="small"
class="center-button"
@click="submitForm(formRef, false)"
>{{ t('quickIssue.CONTINUE') }}</OButton
>
<OButton size="small" @click="handleClick(`/${lang}/issues/`)">{{
t('quickIssue.CANCEL')
}}</OButton>
</div>
</div>
</transition-group>
</el-form>
</div>
</AppContent>
<div class="mo-content"></div>
<ODialog v-model="isMenuShown" class="menu-dialog" :show-close="true">
<div
v-for="group in landscapeInfo"
:key="group.groupName"
class="landscape-group"
>
<SigLandscapeFeature
:info="group?.features"
@sig-click="getSigValue"
></SigLandscapeFeature>
</div>
</ODialog>
<ODialog v-model="isSlideVerifyShown" class="slide-dialog" :show-close="true">
<AppSlideVerify v-show="isSlideVerifyShown" @succuss="sendVerifyEmail">
</AppSlideVerify>
</ODialog>
</template>
<style lang="scss">
.fadeHeight-enter-active,
.fadeHeight-leave-active {
transition: all 0.5s;
max-height: 530px;
}
.fadeHeight-enter,
.fadeHeight-leave-to {
opacity: 0;
max-height: 0px;
}
.submit-issue {
color: var(--o-color-text1);
.inline-box {
background-color: var(--o-color-bg2);
padding: 40px;
}
.el-input__suffix {
height: 34px;
}
.o-select {
.el-input__wrapper {
min-width: 250px;
box-shadow: 0 0 0 1px var(--o-color-border1) inset;
&:hover {
box-shadow: 0 0 0 1px var(--o-color-border1) inset;
}
.o-icon {
font-size: var(--o-font-size-h7);
}
}
}
h1 {
font-size: var(--o-font-size-h4);
font-weight: 300;
}
.issue-form {
margin-top: var(--o-spacing-h2);
.form-liner {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--o-spacing-h4);
.el-form-item {
display: flex;
margin: 0;
flex-wrap: nowrap;
align-items: center;
&.is-error {
.el-input__wrapper {
box-shadow: 0 0 0 1px var(--o-color-error1) inset;
.el-icon {
color: var(--o-color-error1);
}
}
}
.el-form-item__content {
display: flex;
flex-wrap: nowrap;
min-width: 208px;
.el-form-item__error {
padding-top: var(--o-spacing-h9);
}
.o-select {
.o-icon {
padding: 0;
color: inherit;
font-size: var(--o-font-size-h7);
}
}
.select-sig-btn {
position: relative;
flex-shrink: 0;
margin-left: -1px;
padding: 7px 34px;
color: #fff;
z-index: 1;
}
.o-icon {
cursor: pointer;
padding-left: var(--o-spacing-h5);
font-size: 32px;
color: var(--o-color-brand1);
}
.upload-file {
display: flex;
.o-icon {
padding: 0;
font-size: 24px;
color: var(--o-color-text1);
}
}
}
.el-form-item__label {
width: 52px;
text-align: right;
justify-content: flex-end;
color: var(--o-color-text1);
font-size: var(--o-font-size-h7);
flex-shrink: 0;
padding-right: var(--o-spacing-h5);
}
}
.fill-width {
margin-right: var(--o-spacing-h2);
width: 100%;
}
}
.verify-email {
margin: var(--o-spacing-h2) 0;
justify-content: flex-start;
.upload-item {
width: 100%;
}
.verify-code {
.el-form-item__label {
width: 120px;
}
}
}
.form-radio {
margin: var(--o-spacing-h2);
justify-content: center;
.o-checkbox-group {
.o-checkbox-label {
font-size: 16px;
}
}
}
.editor {
margin-top: var(--o-spacing-h3);
.fill-width {
margin: 0;
}
}
.obuton-box {
display: flex;
justify-content: center;
.o-button-type-primary {
color: #fff;
}
.center-button {
margin: 0 var(--o-spacing-h4);
}
}
.is-gitee-user {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: var(--o-spacing-h2);
.o-button {
min-width: 200px;
padding: 6px 0;
align-items: center;
justify-content: center;
}
.gitee-user {
position: relative;
padding: 1px;
overflow: hidden;
.o-button {
position: relative;
border: none;
background-color: var(--o-color-bg2);
}
&::before {
position: absolute;
content: '';
top: 0;
left: 0;
width: 300%;
height: 100%;
background: linear-gradient(
115deg,
#fc756cff,
#a767e5,
#7d32eaff,
rgb(232, 169, 164),
#fc756cff
);
background-size: 50% 100%;
animation: rainbowSlide 6s linear infinite;
@keyframes rainbowSlide {
100% {
background-position: -400% 0;
}
}
}
}
.unregistered {
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
margin-top: var(--o-spacing-h4);
color: var(--o-color-brand1);
&:hover {
color: var(--o-color-brand2);
}
}
.icon-arrow {
transition: all 0.3s;
margin-top: var(--o-spacing-h6);
}
.reversal {
transform: rotate(180deg);
}
}
}
.en-form {
.form-liner {
.el-form-item {
.el-form-item__label {
width: 105px;
justify-content: flex-start;
}
}
}
.verify-email {
.verify-code {
.el-form-item__label {
width: 180px;
justify-content: flex-end;
}
}
}
}
}
.menu-dialog {
margin-top: 10vh;
max-width: 1430px;
width: 100%;
.el-tabs__header {
padding-top: 40px;
position: sticky;
top: 0;
background-color: var(--o-color-bg2);
z-index: 1;
}
.el-dialog__header {
padding: 0;
}
.el-dialog__footer {
padding: 0;
}
.el-dialog__headerbtn {
top: 18px;
right: 18px;
font-size: var(--o-font-size-h5);
width: fit-content;
height: fit-content;
z-index: 10;
.el-dialog__close {
color: var(--o-color-text1);
}
}
.el-dialog__body {
padding: var(--o-spacing-h2);
padding-top: 0;
max-height: 80vh;
overflow-y: scroll;
background-color: var(--o-color-bg2);
&::-webkit-scrollbar-track {
border-radius: 4px;
background-color: var(--o-color-bg2);
}
&::-webkit-scrollbar {
width: 6px;
background-color: var(--o-color-bg2);
}
&::-webkit-scrollbar-thumb {
border-radius: 4px;
background: var(--o-color-division1);
}
}
.landscape-group {
margin-top: var(--o-spacing-h2);
overflow: hidden;
h1 {
&::before {
content: '';
display: block;
height: 80px;
margin-top: -80px;
visibility: hidden;
}
}
}
}
.slide-dialog {
margin-top: 30vh;
width: fit-content;
.el-dialog__header {
padding: 12px 0;
}
.el-dialog__footer {
padding: 0;
}
}
</style>