Files
jczxw-pc/app/pages/console/invites/index.vue
2026-04-23 16:30:57 +08:00

327 lines
7.3 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.

<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { useRouter } from 'vue-router'
import {
listPendingInvites,
acceptInvite,
rejectInvite,
type AppUser
} from '@/api/app/appUser'
import ConsoleLayout from '@/layouts/console.vue'
definePageMeta({
layout: 'console'
})
const router = useRouter()
const invites = ref<AppUser[]>([])
const loading = ref(false)
const activeTab = ref('pending')
// 待确认邀请
const pendingInvites = computed(() => invites.value)
// 加载邀请列表
async function loadInvites() {
try {
loading.value = true
invites.value = await listPendingInvites()
} catch (error) {
console.error('加载邀请列表失败:', error)
message.error('加载邀请列表失败')
} finally {
loading.value = false
}
}
// 接受邀请
async function handleAccept(invite: AppUser) {
if (!invite.id) return
try {
await acceptInvite(invite.id)
message.success('已接受邀请,加入应用成功')
invites.value = invites.value.filter(i => i.id !== invite.id)
// 刷新页面或跳转到应用
setTimeout(() => {
router.push('/developer/apps')
}, 500)
} catch (error: any) {
message.error(error.message || '接受邀请失败')
}
}
// 拒绝邀请
async function handleReject(invite: AppUser) {
if (!invite.id) return
Modal.confirm({
title: '确认拒绝邀请?',
content: `拒绝后将无法加入应用「${invite.productName || '未知应用'}`,
okText: '确认拒绝',
okType: 'danger',
cancelText: '取消',
async onOk() {
try {
await rejectInvite(invite.id!)
message.success('已拒绝邀请')
invites.value = invites.value.filter(i => i.id !== invite.id)
} catch (error: any) {
message.error(error.message || '拒绝邀请失败')
}
}
})
}
// 获取角色标签文本
function getRoleLabel(role?: string) {
const map: Record<string, string> = {
owner: '所有者',
admin: '管理员',
developer: '开发者',
viewer: '访客'
}
return map[role || ''] || role || '未知'
}
// 获取角色标签颜色
function getRoleColor(role?: string) {
const map: Record<string, string> = {
owner: 'orange',
admin: 'blue',
developer: 'green',
viewer: 'purple'
}
return map[role || ''] || 'default'
}
onMounted(() => {
loadInvites()
})
</script>
<template>
<ConsoleLayout>
<div class="invites-page">
<div class="page-header">
<h1 class="page-title">应用邀请</h1>
<p class="page-desc">管理您收到的应用加入邀请</p>
</div>
<a-card class="invites-card">
<a-tabs v-model:activeKey="activeTab">
<a-tab-pane key="pending" tab="待确认">
<a-spin :spinning="loading">
<div v-if="pendingInvites.length === 0" class="empty-state">
<a-empty description="暂无待确认的邀请">
<template #extra>
<p class="empty-tip">
当有人邀请您加入应用时邀请将显示在这里
</p>
</template>
</a-empty>
</div>
<div v-else class="invite-list">
<div
v-for="invite in pendingInvites"
:key="invite.id"
class="invite-item"
>
<div class="invite-main">
<a-avatar
:src="invite.icon || '/logo.png'"
:size="64"
class="app-icon"
/>
<div class="invite-info">
<div class="info-header">
<h3 class="app-name">{{ invite.productName || '未知应用' }}</h3>
<a-tag :color="getRoleColor(invite.role)">
{{ getRoleLabel(invite.role) }}
</a-tag>
</div>
<div class="info-meta">
<span class="meta-item">
<UserOutlined />
邀请人{{ invite.username || '未知用户' }}
</span>
<span class="meta-item">
<ClockCircleOutlined />
邀请时间{{ invite.inviteTime }}
</span>
<span v-if="invite.inviteExpireTime" class="meta-item expire">
<ExclamationCircleOutlined />
有效期至{{ invite.inviteExpireTime }}
</span>
</div>
</div>
</div>
<div class="invite-actions">
<a-button
size="large"
@click="handleReject(invite)"
>
拒绝
</a-button>
<a-button
type="primary"
size="large"
@click="handleAccept(invite)"
>
接受邀请
</a-button>
</div>
</div>
</div>
</a-spin>
</a-tab-pane>
<a-tab-pane key="history" tab="历史记录" disabled>
<a-empty description="功能开发中" />
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</ConsoleLayout>
</template>
<style scoped>
.invites-page {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
margin-bottom: 24px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #262626;
margin-bottom: 8px;
}
.page-desc {
color: #8c8c8c;
font-size: 14px;
}
.invites-card {
border-radius: 8px;
}
.empty-state {
padding: 60px 0;
}
.empty-tip {
color: #8c8c8c;
font-size: 14px;
margin-top: 8px;
}
.invite-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.invite-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px;
background: #fafafa;
border-radius: 8px;
border: 1px solid #f0f0f0;
transition: all 0.3s;
}
.invite-item:hover {
background: #f5f5f5;
border-color: #d9d9d9;
}
.invite-main {
display: flex;
align-items: center;
gap: 16px;
flex: 1;
min-width: 0;
}
.app-icon {
flex-shrink: 0;
border: 2px solid #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.invite-info {
flex: 1;
min-width: 0;
}
.info-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.app-name {
font-size: 18px;
font-weight: 600;
color: #262626;
margin: 0;
}
.info-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.meta-item {
display: flex;
align-items: center;
gap: 6px;
color: #595959;
font-size: 14px;
}
.meta-item.expire {
color: #faad14;
}
.invite-actions {
display: flex;
gap: 12px;
flex-shrink: 0;
margin-left: 24px;
}
@media (max-width: 768px) {
.invites-page {
padding: 16px;
}
.invite-item {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.invite-actions {
margin-left: 0;
width: 100%;
justify-content: flex-end;
}
.info-meta {
flex-direction: column;
gap: 8px;
}
}
</style>