2025-04-12 17:39:38 +08:00

296 lines
7.7 KiB
Vue

<template>
<Dialog
v-model="show"
:options="{
title: '代码审查',
size: '6xl'
}"
>
<template v-if="getResults" #body-content>
<div class="container mx-auto px-4">
<div class="grid gap-3">
<div class="flex items-center mb-2">
<div class="flex items-center">
<span class="text-lg font-medium text-gray-700 mr-2"
>状态:</span
>
<Badge
:variant="'subtle'"
size="lg"
:label="$resources.codeScreening.pg.status"
:theme="getBadgeTheme"
/>
</div>
<!-- <Button
iconLeft="check"
v-if="
isSystemUser && $resources.codeScreening.pg.status == 'Open'
"
:variant="'subtle'"
theme="gray"
size="sm"
class="mr-4 ml-auto"
@click="approveRelease"
>
批准发布
</Button>
<Button
iconLeft="x"
v-if="
isSystemUser && $resources.codeScreening.pg.status == 'Open'
"
:variant="'subtle'"
theme="gray"
size="sm"
class="mr-4"
@click="showRejectReleaseDialog = true"
>
拒绝发布
</Button> -->
</div>
<Dialog
v-model="showRejectReleaseDialog"
:options="{
title: '确认',
size: 'xl',
actions: [
{
label: '拒绝发布',
variant: 'solid',
onClick: rejectRelease
}
]
}"
>
<template #body-content>
<FormControl
:type="'textarea'"
size="sm"
variant="subtle"
placeholder="输入拒绝的原因"
:disabled="false"
v-model="rejectionReason"
/>
</template>
</Dialog>
<div
class="card bg-white shadow-md rounded-md overflow-hidden"
v-for="file in getResults"
:key="file.name"
>
<div class="card-header bg-gray-200 p-3">
<h2 class="text-md font-semibold text-gray-800">
{{ file.name }} - {{ file.score }} 个问题
</h2>
</div>
<div class="p-4">
<div
class="issues space-y-4"
v-for="line in file.lines"
:key="line.context.line_number"
>
<div
class="issue-item p-4 mb-4"
v-for="issue in line.issues"
:key="issue.violation"
>
<div class="flex items-center mb-3">
<span class="text-red-600 mr-2">{{ issue.severity }}</span>
<span class="text-gray-800">({{ issue.violation }})</span>
<span class="text-orange-500 font-semibold pl-2">
- {{ issue.match }}</span
>
</div>
<div class="border border-gray-400 p-2 rounded-md">
<div
style="background-color: #d1f8d9"
class="border rounded-md"
>
<div
v-for="(lineText, i) in line.context.lines"
:key="i"
:class="{
'bg-yellow-200 border rounded-md':
lineText.includes(issue.match) &&
line.context.line_range[i] ===
line.context.line_number
}"
>
<code class="p-2 text-sm whitespace-pre-wrap">
<span>{{ line.context.line_range[i] }}:</span>
{{ lineText }}
</code>
</div>
</div>
<div
v-if="
getCommentsForLine(file.name, line.context.line_number)
.length
"
>
<hr class="h-2 mt-2" />
<div
class="comment-item mt-2 p-2"
v-for="comment in getCommentsForLine(
file.name,
line.context.line_number
)"
:key="comment.name"
>
<div class="flex items-center">
<Avatar
:shape="'circle'"
:image="null"
:label="comment.commented_by"
size="md"
/>
<strong class="text-gray-900 pl-2 pr-2 text-lg">
{{ comment.commented_by }}
</strong>
<Tooltip
:text="formatTime(comment.time)"
:placement="'top'"
>
<span class="text-gray-600 text-sm">
{{ $dayjs(comment.time).fromNow() }}
</span>
</Tooltip>
</div>
<p class="text-gray-800 text-base p-2 ml-6">
{{ comment.comment }}
</p>
</div>
</div>
<hr class="h-2 mt-2" />
<NewComment
v-if="$resources.codeScreening.pg.status == 'Open'"
:approval_request_name="row.approval_request_name"
:filename="file.name"
:line_number="line.context.line_number"
@comment-submitted="handleCommentSubmitted"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</Dialog>
</template>
<script>
import NewComment from './NewComment.vue';
import { toast } from 'vue-sonner';
export default {
components: {
NewComment
},
props: ['row', 'app', 'isSystemUser'],
data() {
return {
show: true,
showRejectReleaseDialog: false,
rejectionReason: ''
};
},
computed: {
getResults() {
if (this.$resources.codeScreening.pg) {
const results = JSON.parse(this.$resources.codeScreening.pg.result);
return results;
}
},
getComments() {
if (this.$resources.codeScreening.pg) {
const results = this.$resources.codeScreening.pg.code_comments;
return results;
}
},
getBadgeTheme() {
const status = this.$resources.codeScreening.pg.status.toLowerCase();
if (status === 'open') {
return 'blue';
} else if (status === 'approved') {
return 'green';
} else {
return 'red';
}
}
},
methods: {
getCommentsForLine(filename, lineNumber) {
// 通过匹配文件名和行号过滤评论
let filteredComments = this.getComments.filter(
comment =>
comment.filename === filename && comment.line_number === lineNumber
);
// 按时间降序排序评论(最新的在前)
filteredComments.sort((a, b) => new Date(a.time) - new Date(b.time));
return filteredComments;
},
formatTime(time) {
const date = new Date(time);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
});
},
handleCommentSubmitted() {
// 数据库写入需要一些时间。即时重新加载无法快速反映更改。理想情况下,缓存应该在客户端更新
setTimeout(() => {
this.$resources.codeScreening.reload();
}, 1200);
},
approveRelease() {
this.$resources.codeScreening.setValue.submit({
status: 'Approved',
reviewed_by: this.$team?.pg?.user
});
},
rejectRelease() {
// 检查拒绝原因是否为空
if (!this.rejectionReason) {
toast.error('拒绝原因是必填项');
return;
}
// 如果提供了原因,则继续拒绝
this.$resources.codeScreening.setValue.submit({
status: 'Rejected',
reason_for_rejection: this.rejectionReason,
reviewed_by: this.$team?.pg?.user
});
this.showRejectReleaseDialog = false;
}
},
resources: {
codeScreening() {
return {
type: 'document',
pagetype: 'App Release Approval Request',
name: this.row.approval_request_name,
fields: [
'name',
'marketplace_app',
'screening_status',
'app_release',
'status',
'result',
'code_comments'
],
auto: true
};
}
}
};
</script>