Files
Situation-Awareness-Platfor…/src/views/Mobile/Repo/Issue/Detail/index.vue
2025-03-12 18:41:20 +08:00

496 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<page-layout class="issue-detail-head">
<d-skeleton :loading="loading && !issueData.title">
<issue-description
:key="issueDescKey"
:title="issueData?.title"
:created-time="issueData?.created_at"
:closed-time="issueData?.closed_at"
:user-name="issueData?.author?.name"
:status-text="issueStat?.label"
:statusIconColor="issueStat?.color"
:statusBgColor="issueStat?.bgColor"
:statusIcon="issueStat?.icon" :index-label="issueData?.iid + ''"
:can-edit="isIssueAuthor || isDeveloper" @handle-save="updateTitle"
/>
<template #placeholder>
<div style="display: flex; gap: 0 16px;">
<gc-skeleton-item style="width: 88px; height: 60px;"></gc-skeleton-item>
<div style="flex: 1;">
<gc-skeleton-item variant="square" style="height: 32px;"></gc-skeleton-item>
<gc-skeleton-item style="width: 150px; height: 20px; margin-top: 8px;"></gc-skeleton-item>
</div>
</div>
</template>
</d-skeleton>
<template #right>
<div style="text-align: right;">
<d-skeleton :loading="loading && !issueData.title">
<d-button icon="add" variant="solid" class="search-bar-create"
@click="$router.push({ name: 'repoIssueCreate' })" v-if="accountInfo?.username">
新建 Issue
</d-button>
<template #placeholder>
<gc-skeleton-item style="display: inline-block; width: 120px; height: 30px;"></gc-skeleton-item>
</template>
</d-skeleton>
</div>
</template>
</page-layout>
<page-layout class="issue-detail-main">
<ScrollContainer @touch-bottom="onTouchBottom" :bottom="700" :finished="pageData.finished"
:loading="pageData.loading">
<d-skeleton :loading="loading && !issueData.title">
<div class="g-content-card">
<!-- issue 描述 -->
<CommentDescription v-if="issueData?.id" :data="issueData" :author="issueData?.author" :user="accountInfo" @quote-reply="addQuoteReply" />
<template v-for="item in pageData.commentlist" :key="item.id">
<!-- 评论 -->
<DiscussionItem
v-if="item?.notes"
:noteNum="item.notes.length"
:author="item?.author || (item.notes && item.notes[0]?.author)"
:user="accountInfo"
:data="item.notes[0]"
:created_at="item.created_at || item?.notes[0].created_at"
:can-edit="item.notes[0]?.author.username === accountInfo.username"
:noteId="item.id"
:targetId="item?.notes[0].id"
:repo-id="repoId"
event-msg="评论"
@delete="deleteCommentSelf(item)"
@quote-reply="addQuoteReply"
/>
<!-- issue event -->
<EventItem
v-else
:author="item?.author"
:event-msg="item.body"
:create-time="item?.created_at"
:eventType="item?.action"
/>
</template>
</div>
<template #placeholder>
<div style="display: flex;justify-content: space-between;gap: 0 16px;margin-bottom: 1em;">
<gc-skeleton-item style="width: 200px; height: 16px;"></gc-skeleton-item>
<gc-skeleton-item style="width: 100px; height: 16px;"></gc-skeleton-item>
</div>
<d-skeleton></d-skeleton>
<template v-for="i in 2" :key="i">
<div style="display: flex;justify-content: space-between;gap: 0 16px;margin-bottom: 1em;margin-top: 1em;">
<gc-skeleton-item style="width: 200px; height: 16px;"></gc-skeleton-item>
<gc-skeleton-item style="width: 100px; height: 16px;"></gc-skeleton-item>
</div>
<d-skeleton></d-skeleton>
</template>
<div style="display: flex;align-items: center; gap: 0 16px; margin-bottom: 1em;margin-top: 1em;">
<gc-skeleton-item variant="circle" style="width: 40px; height: 40px;"></gc-skeleton-item>
<gc-skeleton-item style="width: 50px; height: 30px;" v-for="i in 8" :key="i"></gc-skeleton-item>
</div>
<d-skeleton style="padding-left: 56px;"></d-skeleton>
</template>
</d-skeleton>
<template v-if="pageData.loading && issueData.title">
<template v-for="i in 1" :key="i">
<div style="display: flex;justify-content: space-between;gap: 0 16px;margin-bottom: 1em;margin-top: 1em;">
<gc-skeleton-item style="width: 200px; height: 16px;"></gc-skeleton-item>
<gc-skeleton-item style="width: 100px; height: 16px;"></gc-skeleton-item>
</div>
<d-skeleton></d-skeleton>
</template>
</template>
<!-- issue 添加评论 -->
<section :class="{'mt-[24px]':true, 'edit-loading': pageData.loading }">
<AddDiscussion
@add-discussion="addDiscussion"
@update-state="handleUpdateIssue"
:loading="addIng" ref="RefAddDiscussion"
:state="issueData?.state + ''"
:updateDisabled="!(isDeveloper||isIssueAuthor)"
:submit-disabled="!accountInfo.username"
/>
</section>
</ScrollContainer>
<template #right>
<d-skeleton :loading="loading && !issueData.title">
<IssueAsideSetGroup v-model="issueData" :participants="participants" :project_id="issueData.project_id"
:repoId="repoId" :issueId="$route.params.serialNumber" />
<template #placeholder>
<div style="display: flex; flex-direction: column; gap: 16px 0;">
<template v-for="i in 3" :key="i">
<div style="display: flex;justify-content: space-between;gap: 0 16px;">
<gc-skeleton-item style="flex: 1; height: 24px;"></gc-skeleton-item>
<gc-skeleton-item style="width: 24px; height: 24px;"></gc-skeleton-item>
</div>
<d-skeleton :rows="1"></d-skeleton>
<br />
</template>
<br />
<gc-skeleton-item v-for="i in 3" :key="i" style="width: 150px; height: 16px;"></gc-skeleton-item>
</div>
</template>
</d-skeleton>
</template>
</page-layout>
</template>
<script lang="ts" setup>
defineOptions({ name: 'IssueDetail' });
import { useRouter, useRoute } from 'vue-router';
import { ref, reactive, onMounted, watch, computed, onBeforeMount, nextTick, provide, type Ref } from 'vue';
import PageLayout from '@/components/PageLayout/index.vue';
import IssueDescription from '@/components/IssueDescription/index.vue';
import IssueAsideSetGroup from '@/views/Repo/components/IssueAsideSetGroup/index.vue';
import ScrollContainer from '@/components/ScrollContainer/index.vue';
import CommentDescription from './IssueDescription.vue';
import DiscussionItem from './DiscussionItem.vue';
import EventItem from './EventItem.vue';
import AddDiscussion from '@/views/Repo/components/AddDiscussion/index.vue';
import { Message } from 'vue-devui/message';
import { useAccountStore } from '@/stores/user';
import { useRepoId } from '@/utils/hooks/useRepoId';
import { reqCatch } from '@/utils/catch';
import { getRepoRole } from '@/api/repo/index';
import { repoInfoStore } from '@/stores/Repo/index';
import { issueStateOption } from '@/constant/issue';
import type { ISSUE_STATE } from '@/constant/issue';
import type { IIssue, INote, Idiscussions } from '@/api/issue/types';
import { useOrgId } from '@/utils/hooks/useOrgId';
import { fetchIssue, updateIssue, fetchIssueDiscussions, issueParticipants, createIssueDiscussions, updateIssueStat } from '@/api/issue';
import { promiseTimeout } from '@vueuse/core';
const { isAdmin, isDeveloper, isVisitor } = repoInfoStore();
const { orgId } = useOrgId('/');
const { repoId } = useRepoId();
const { accountInfo = {}} = useAccountStore();
const route = useRoute();
const loading = ref(false);
const addIng = ref(false);
const RefAddDiscussion = ref();
const roleData = ref({});
const issueDescKey = ref<number>(0);
const issueData: Ref<IIssue> = ref({});
const participants = ref([]); // 参与者
const pageData = reactive<{
commentlist: Idiscussions | []; // 评论
page_num: number;
page_size: number;
page_count: number;
total: number;
finished: boolean;
loading: boolean;
end_id?: number;
end_system_id?: number;
}>({
commentlist: [],
page_count: 0,
page_num: 1,
page_size: 100,
total: 0,
finished: false,
loading: false
});
const isIssueAuthor = computed(() => issueData.value.author?.username === accountInfo.username);
const issueStat = computed(() => ({
label: issueStateOption[issueData.value?.state]?.label,
color: issueStateOption[issueData.value?.state ]?.color,
bgColor: issueStateOption[issueData.value?.state]?.bgColor,
icon: issueStateOption[issueData.value?.state]?.icon
}));
onBeforeMount(() => {
fetchIssueData();
accountInfo?.username && fetchRole();
});
onMounted(async() => {
fetchParticipants();
await nextTick();
fetchIssueCommentList();
});
const fetchRole = async() => {
const res = await reqCatch(getRepoRole, { repoId: repoId.value });
if (!res.error) {
roleData.value = res.data.data;
}
};
/**
* issue 详情
*/
const fetchIssueData = async() => {
loading.value = true;
const res = await reqCatch(fetchIssue, {
project_id: repoId.value,
issue_iid: route.params.serialNumber
});
if (!res.error) {
issueData.value = res.data.data;
issueDescKey.value++;
}
loading.value = false;
};
/**
* 参与者 列表
*/
const fetchParticipants = async() => {
const res = await reqCatch(issueParticipants, {
project_id: repoId.value,
issue_iid: route.params.serialNumber
});
if (!res.error) {
participants.value = res.data.data;
}
};
/**
* 获取评论列表
* @params {string} type 已经存在的数据
*/
const fetchIssueCommentList = async(type?: 'exist') => {
pageData.loading = true;
const res = await reqCatch(fetchIssueDiscussions, {
issue_iid: route.params.serialNumber,
project_id: repoId.value,
type: 'user',
page: pageData.page_num,
per_page: pageData.page_size,
end_id: pageData.end_id,
end_system_id: pageData.end_system_id
});
if (!res.error) {
const { content, page_count, page_num, total } = res.data.data;
const { data = [], end_id, end_system_id } = content;
if (type === 'exist') {
// 获取重复分页,添加不存在的数据
const newdata = data.filter(
(item: Idiscussions) =>
pageData.commentlist.findIndex(
(value: Idiscussions) => value.id === item.id
) === -1
);
newdata.length > 0 && pageData.commentlist.push(...newdata);
} else {
// 添加数据
pageData.commentlist.push(...data);
}
pageData.total = total;
if (page_num >= page_count) {
pageData.loading = false;
pageData.finished = true;
}
pageData.end_id = end_id;
pageData.end_system_id = end_system_id;
}
pageData.loading = false;
};
/**
* 添加 评论
*/
const addDiscussion = async(obj:{body:string}) => {
if (addIng.value) return;
addIng.value = true;
const res = await reqCatch(createIssueDiscussions, {
project_id: repoId.value,
issue_iid: issueData.value.iid,
body: obj.body
});
addIng.value = false;
if (!res.error) {
pageData.commentlist.push(res.data.data);
RefAddDiscussion.value.clear();
if (!participants.value.some((e) => e?.username === accountInfo?.username)) {
// 有新增评论者 更新参与者
fetchParticipants();
}
}
};
/**
* 删除评论
*/
const deleteCommentSelf = (item: object) => {
const index = pageData.commentlist?.findIndex(e => e.id === item.id);
if (index > -1) {
pageData.commentlist.splice(index, 1);
}
};
/**
* 引用回复
*/
const addQuoteReply = (content:string) => {
if (!content.startsWith('>')) {
content = '> ' + content;
}
RefAddDiscussion.value.setBody(content + '\n\n');
};
/**
* issue 状态切换
*/
const handleUpdateIssue = async(obj:any) => {
const res = await reqCatch(updateIssueStat, {
project_id: repoId.value,
issue_iid: issueData.value.iid,
discussion_locked: obj.discussion_locked,
state_event: obj.state_event,
discussions: obj.body
});
if (!res.error) {
issueData.value = res.data.data;
RefAddDiscussion.value.clear();
updatePage();
}
};
const updateStat = () => {
fetchIssueData();
updatePage();
};
/**
* 修改 标题
*/
const updateTitle = async(title: string) => {
if (!title) {
Message.warning('标题不能为空!');
return issueDescKey.value++;
} else if (!/^.{1,200}$/.test(title)) {
Message.warning('标题限制 1~200 个字符!');
return issueDescKey.value++;
} else if (issueData.value.title === title) {
return issueDescKey.value++;
}
const res = await reqCatch(updateIssue, {
project_id: issueData.value.project_id,
issue_iid: issueData.value.iid,
title }
);
if (!res.error) {
issueData.value.title = res.data.data.title;
issueData.value.canEdit = false;
issueDescKey.value++;
updateStat();
}
};
/**
* 触底加载更多
*/
const onTouchBottom = () => {
if (pageData.loading || pageData.finished) return;
pageData.page_num++;
};
/**
* 有新增内容时重新获取
*/
const updatePage = () => {
if (pageData.page_num * pageData.page_size > pageData.commentlist.length) {
fetchIssueCommentList('exist');
} else {
pageData.page_num++;
}
};
watch(() => pageData.page_num, () => {
fetchIssueCommentList();
});
provide('roleData', roleData);
provide('repoId', repoId.value);
provide('issueId', route.params.serialNumber);
provide('isVisitor', isVisitor);
provide('isDeveloper', isDeveloper);
provide('isAdmin', isAdmin);
provide('isIssueAuthor', isIssueAuthor);
provide('updatePage', updatePage);
</script>
<style lang="scss" scoped>
.issue {
&-detail-head {
margin-top: 24px;
}
&-detail-main {
margin-top: 24px;
margin-bottom: 24px;
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading {
animation: spin 2s linear infinite;
transform-origin: center center;
}
.loading-comment {
display: flex;
justify-content: center;
align-items: center;
line-height: 48px;
}
.add-btn-vertical-line {
display: inline-block;
width: 1px;
height: 20px;
background: #e6e7e8;
margin-right: 10px;
vertical-align: middle;
}
.edit-loading {
position: relative;
&::after {
content: '';
display: block;
position: absolute;
top: 0;
bottom: 0;
width: 100%;
background-color: rgba($color: #fff, $alpha: 0.7);
z-index: 99;
}
}
</style>