初始化2
This commit is contained in:
564
app/pages/developer/requests.vue
Normal file
564
app/pages/developer/requests.vue
Normal file
@@ -0,0 +1,564 @@
|
||||
<template>
|
||||
<div class="dev-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">📋 权限申请记录</h2>
|
||||
<p class="page-desc">查看你提交的 Git 仓库加组申请及审核状态。</p>
|
||||
</div>
|
||||
<a-button type="primary" @click="showApplyModal = true">+ 提交新申请</a-button>
|
||||
</div>
|
||||
|
||||
<div class="page-body">
|
||||
<!-- 状态卡片 -->
|
||||
<a-row :gutter="[14, 14]" class="mb-5">
|
||||
<a-col :xs="12" :md="6" v-for="s in statusSummary" :key="s.label">
|
||||
<div class="status-card" :class="s.color">
|
||||
<div class="status-num">
|
||||
<template v-if="loading.stats">-</template>
|
||||
<template v-else>{{ s.num }}</template>
|
||||
</div>
|
||||
<div class="status-label">{{ s.label }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 申请记录列表 -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">申请记录</span>
|
||||
<a-radio-group v-model:value="filterStatus" size="small" button-style="solid">
|
||||
<a-radio-button value="">全部</a-radio-button>
|
||||
<a-radio-button value="pending">待审核</a-radio-button>
|
||||
<a-radio-button value="approved">已通过</a-radio-button>
|
||||
<a-radio-button value="rejected">已拒绝</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading.page" class="loading-state">
|
||||
<div class="loading-icon">
|
||||
<span class="loading-dot"></span>
|
||||
<span class="loading-dot"></span>
|
||||
<span class="loading-dot"></span>
|
||||
</div>
|
||||
<div class="loading-text">正在加载申请记录...</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="filteredRequests.length === 0" class="empty-state">
|
||||
<div class="empty-icon">📋</div>
|
||||
<div class="empty-title">暂无申请记录</div>
|
||||
<div class="empty-desc">绑定 Git 账号后,提交仓库访问申请</div>
|
||||
<a-space class="mt-4">
|
||||
<a-button type="primary" @click="showApplyModal = true">提交申请</a-button>
|
||||
<a-button @click="navigateTo('/developer/git')">绑定 Git 账号</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 申请列表 -->
|
||||
<div v-else class="request-list">
|
||||
<div
|
||||
v-for="req in filteredRequests"
|
||||
:key="req.id"
|
||||
class="request-item"
|
||||
>
|
||||
<div class="request-status-dot" :class="req.status" />
|
||||
<div class="request-content">
|
||||
<div class="request-title-row">
|
||||
<span class="request-title">{{ req.repoName || req.repo }}</span>
|
||||
<a-tag :color="statusColor(req.status)">{{ statusText(req.status) }}</a-tag>
|
||||
</div>
|
||||
<div class="request-desc">{{ req.reason }}</div>
|
||||
<div class="request-meta">
|
||||
<span>🐙 {{ req.gitUsername || '未指定' }}</span>
|
||||
<span class="meta-dot" />
|
||||
<span>📅 {{ req.createdAt }}</span>
|
||||
<span v-if="req.reviewedAt" class="meta-dot" />
|
||||
<span v-if="req.reviewedAt">✓ {{ req.reviewedAt }} 审核</span>
|
||||
<span v-if="req.reviewerName" class="meta-dot" />
|
||||
<span v-if="req.reviewerName">👤 {{ req.reviewerName }}</span>
|
||||
</div>
|
||||
<div v-if="req.status === 'rejected' && req.rejectReason" class="reject-reason">
|
||||
❌ 拒绝原因:{{ req.rejectReason }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提交申请弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showApplyModal"
|
||||
title="📋 提交仓库访问申请"
|
||||
ok-text="提交申请"
|
||||
cancel-text="取消"
|
||||
:ok-button-props="{ loading: loading.submit }"
|
||||
:cancel-button-props="{ disabled: loading.submit }"
|
||||
:mask-closable="!loading.submit"
|
||||
@ok="handleApply"
|
||||
>
|
||||
<div v-if="loading.repos" class="modal-loading">
|
||||
<a-spin size="small" />
|
||||
<span class="ml-2">正在加载仓库列表...</span>
|
||||
</div>
|
||||
|
||||
<a-form layout="vertical" class="mt-2" v-else>
|
||||
<a-form-item label="申请仓库" required>
|
||||
<a-select
|
||||
v-model:value="applyForm.repo"
|
||||
placeholder="请选择需要访问的仓库"
|
||||
:loading="loading.repos"
|
||||
:disabled="loading.repos"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option v-for="r in repoOptions" :key="r.value" :value="r.value" :disabled="r.disabled">
|
||||
{{ r.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<div class="form-hint">
|
||||
灰色选项表示你已有该仓库的访问权限
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="Git 用户名">
|
||||
<a-input v-model:value="applyForm.gitUsername" placeholder="你绑定的 Gitea 用户名" />
|
||||
<div class="form-hint">
|
||||
未绑定?<a @click="navigateTo('/developer/git')">前往绑定</a>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="申请理由" required>
|
||||
<a-textarea
|
||||
v-model:value="applyForm.reason"
|
||||
:rows="3"
|
||||
placeholder="简述你申请该仓库的用途和背景..."
|
||||
:maxlength="300"
|
||||
show-count
|
||||
:disabled="loading.submit"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { SelectProps } from 'ant-design-vue'
|
||||
import {
|
||||
getPermissionRequests,
|
||||
createPermissionRequest,
|
||||
getPermissionRequestStats,
|
||||
getAvailableRepositories
|
||||
} from '@/api/developer'
|
||||
|
||||
definePageMeta({ layout: 'developer' })
|
||||
useHead({ title: '权限申请记录 - 开发者中心' })
|
||||
|
||||
const showApplyModal = ref(false)
|
||||
const filterStatus = ref('')
|
||||
const loading = ref({
|
||||
page: true,
|
||||
stats: true,
|
||||
repos: true,
|
||||
submit: false,
|
||||
})
|
||||
const statsData = ref({
|
||||
pending: 0,
|
||||
approved: 0,
|
||||
rejected: 0,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
const applyForm = reactive({
|
||||
repo: undefined as string | undefined,
|
||||
gitUsername: '',
|
||||
reason: '',
|
||||
})
|
||||
|
||||
const statusSummary = computed(() => [
|
||||
{ num: statsData.value.total, label: '全部申请', color: 'gray' },
|
||||
{ num: statsData.value.pending, label: '待审核', color: 'orange' },
|
||||
{ num: statsData.value.approved, label: '已通过', color: 'green' },
|
||||
{ num: statsData.value.rejected, label: '已拒绝', color: 'red' },
|
||||
])
|
||||
|
||||
// 申请记录数据
|
||||
const requests = ref<any[]>([])
|
||||
|
||||
// 仓库选项
|
||||
const repoOptions = ref<SelectProps['options']>([])
|
||||
|
||||
const filteredRequests = computed(() => {
|
||||
if (!filterStatus.value) return requests.value
|
||||
return requests.value.filter(r => r.status === filterStatus.value)
|
||||
})
|
||||
|
||||
function statusText(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待审核',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
function statusColor(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'orange',
|
||||
approved: 'green',
|
||||
rejected: 'red',
|
||||
}
|
||||
return map[status] || 'default'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr: string) {
|
||||
const date = new Date(dateStr)
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays === 0) {
|
||||
// 今天
|
||||
return `今天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
} else if (diffDays === 1) {
|
||||
// 昨天
|
||||
return `昨天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
} else if (diffDays < 7) {
|
||||
// 本周内
|
||||
return `${diffDays}天前`
|
||||
} else {
|
||||
// 超过一周
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
try {
|
||||
loading.value.page = true
|
||||
loading.value.stats = true
|
||||
|
||||
// 并行加载申请列表和统计数据
|
||||
const [requestsRes, statsRes] = await Promise.all([
|
||||
getPermissionRequests(),
|
||||
getPermissionRequestStats()
|
||||
])
|
||||
|
||||
if (requestsRes.data.code === 200 || requestsRes.data.code === 0) {
|
||||
requests.value = requestsRes.data.data.records.map((item: any) => ({
|
||||
...item,
|
||||
createdAt: formatDate(item.createdAt),
|
||||
reviewedAt: item.reviewedAt ? formatDate(item.reviewedAt) : null
|
||||
}))
|
||||
}
|
||||
|
||||
if (statsRes.data.code === 200 || statsRes.data.code === 0) {
|
||||
statsData.value = statsRes.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载申请数据失败:', error)
|
||||
message.error('加载申请数据失败,请稍后重试')
|
||||
} finally {
|
||||
loading.value.page = false
|
||||
loading.value.stats = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载可申请仓库列表
|
||||
async function loadAvailableRepos() {
|
||||
try {
|
||||
loading.value.repos = true
|
||||
const res = await getAvailableRepositories()
|
||||
if (res.data.code === 200 || res.data.code === 0) {
|
||||
repoOptions.value = res.data.data.map((repo: any) => ({
|
||||
value: repo.value,
|
||||
label: repo.label,
|
||||
disabled: repo.isAccessible // 已可访问的仓库禁用
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载仓库列表失败:', error)
|
||||
message.error('加载仓库列表失败,请稍后重试')
|
||||
} finally {
|
||||
loading.value.repos = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApply() {
|
||||
if (!applyForm.repo || !applyForm.reason.trim()) {
|
||||
message.error('请填写完整申请信息')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value.submit = true
|
||||
const res = await createPermissionRequest({
|
||||
repo: applyForm.repo,
|
||||
reason: applyForm.reason.trim(),
|
||||
gitUsername: applyForm.gitUsername.trim() || undefined
|
||||
})
|
||||
|
||||
if (res.data.code === 200 || res.data.code === 0) {
|
||||
message.success('申请提交成功,请等待审核')
|
||||
showApplyModal.value = false
|
||||
// 重置表单
|
||||
Object.assign(applyForm, { repo: undefined, gitUsername: '', reason: '' })
|
||||
// 重新加载数据
|
||||
await loadData()
|
||||
} else {
|
||||
message.error(res.data.message || '申请提交失败,请稍后重试')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('提交申请失败:', error)
|
||||
if (error.response?.status === 400) {
|
||||
message.error('申请参数错误,请检查填写内容')
|
||||
} else if (error.response?.status === 409) {
|
||||
message.error('该仓库已存在待审核的申请')
|
||||
} else {
|
||||
message.error('申请提交失败,请稍后重试')
|
||||
}
|
||||
} finally {
|
||||
loading.value.submit = false
|
||||
}
|
||||
}
|
||||
|
||||
// 页面初始化
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadAvailableRepos()
|
||||
})
|
||||
|
||||
// 监听过滤状态变化
|
||||
watch(filterStatus, () => {
|
||||
// 可以在这里添加额外的逻辑,比如重新加载筛选后的数据
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dev-page { min-height: 100%; }
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24px 28px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-body {
|
||||
padding: 20px 24px 28px;
|
||||
}
|
||||
|
||||
/* 状态卡片 */
|
||||
.status-card {
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid transparent;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-card.gray { background: #f9fafb; border-color: #f0f0f0; }
|
||||
.status-card.orange { background: #fff7ed; border-color: #fed7aa; }
|
||||
.status-card.green { background: #f0fdf4; border-color: #bbf7d0; }
|
||||
.status-card.red { background: #fef2f2; border-color: #fecaca; }
|
||||
|
||||
.status-num {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 52px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon { font-size: 48px; margin-bottom: 12px; }
|
||||
.empty-title { font-size: 16px; font-weight: 600; color: rgba(0, 0, 0, 0.7); }
|
||||
.empty-desc { font-size: 13px; color: rgba(0, 0, 0, 0.4); margin-top: 6px; }
|
||||
|
||||
/* 申请列表 */
|
||||
.request-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.request-item {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f9f9f9;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.request-item:hover { background: #fafafa; }
|
||||
.request-item:last-child { border-bottom: none; }
|
||||
|
||||
.request-status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.request-status-dot.pending { background: #f59e0b; }
|
||||
.request-status-dot.approved { background: #16a34a; }
|
||||
.request-status-dot.rejected { background: #dc2626; }
|
||||
|
||||
.request-content { flex: 1; min-width: 0; }
|
||||
|
||||
.request-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.request-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.request-desc {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
margin-bottom: 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.request-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
|
||||
.meta-dot {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.reject-reason {
|
||||
font-size: 12px;
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.form-hint a {
|
||||
color: #4f46e5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.loading-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #4f46e5;
|
||||
animation: pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loading-dot:nth-child(2) { animation-delay: 0.2s; }
|
||||
.loading-dot:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.3; transform: scale(0.8); }
|
||||
50% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
/* 弹窗加载状态 */
|
||||
.modal-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 0;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.ml-2 { margin-left: 8px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user