222 lines
8.0 KiB
Vue
222 lines
8.0 KiB
Vue
<template>
|
||
<section class="space-y-6">
|
||
<header class="space-y-2">
|
||
<h1 class="mos-title text-2xl font-semibold">邀请用户</h1>
|
||
<p class="mos-muted text-sm">发送邀请链接给新成员。</p>
|
||
</header>
|
||
|
||
<div class="mos-card p-5 space-y-4">
|
||
<div>
|
||
<label class="text-xs font-semibold text-mosquito-ink/70">邮箱</label>
|
||
<input class="mos-input mt-2 w-full" v-model="form.email" placeholder="name@company.com" />
|
||
</div>
|
||
<div>
|
||
<label class="text-xs font-semibold text-mosquito-ink/70">角色</label>
|
||
<select class="mos-input mt-2 w-full" v-model="form.role">
|
||
<option value="super_admin">超级管理员</option>
|
||
<option value="system_admin">系统管理员</option>
|
||
<option value="operation_manager">运营经理</option>
|
||
<option value="operation_specialist">运营专员</option>
|
||
<option value="marketing_manager">市场经理</option>
|
||
<option value="marketing_specialist">市场专员</option>
|
||
<option value="finance_manager">财务经理</option>
|
||
<option value="finance_specialist">财务专员</option>
|
||
<option value="risk_manager">风控经理</option>
|
||
<option value="risk_specialist">风控专员</option>
|
||
<option value="cs_agent">客服专员</option>
|
||
<option value="cs_manager">客服主管</option>
|
||
<option value="auditor">审计员</option>
|
||
<option value="viewer">只读</option>
|
||
</select>
|
||
</div>
|
||
<PermissionButton permission="user.index.create.ALL" variant="primary" class="w-full" :disabled="loading" @click="sendInvite">
|
||
{{ loading ? '处理中...' : '发送邀请' }}
|
||
</PermissionButton>
|
||
</div>
|
||
|
||
<ListSection :page="page" :total-pages="totalPages" @prev="page--" @next="page++">
|
||
<template #title>邀请记录</template>
|
||
<template #filters>
|
||
<input class="mos-input !py-1 !px-2 !text-xs w-56" v-model="query" placeholder="搜索邮箱" />
|
||
<select class="mos-input !py-1 !px-2 !text-xs" v-model="statusFilter">
|
||
<option value="">全部状态</option>
|
||
<option value="待接受">待接受</option>
|
||
<option value="已接受">已接受</option>
|
||
<option value="已拒绝">已拒绝</option>
|
||
<option value="已过期">已过期</option>
|
||
</select>
|
||
</template>
|
||
<template #default>
|
||
<div v-if="pagedInvites.length" class="space-y-3">
|
||
<div v-for="invite in pagedInvites" :key="invite.id" class="flex items-center justify-between rounded-xl border border-mosquito-line px-4 py-3">
|
||
<div>
|
||
<div class="text-sm font-semibold text-mosquito-ink">{{ invite.email }}</div>
|
||
<div class="mos-muted text-xs">角色:{{ roleLabel(invite.role) }}</div>
|
||
<div class="mos-muted text-xs">邀请时间:{{ formatDate(invite.invitedAt) }}</div>
|
||
<div v-if="invite.expiredAt" class="mos-muted text-xs">过期时间:{{ formatDate(invite.expiredAt) }}</div>
|
||
</div>
|
||
<div class="flex items-center gap-2 text-xs text-mosquito-ink/70">
|
||
<span class="rounded-full bg-mosquito-accent/10 px-2 py-1 text-[10px] font-semibold text-mosquito-brand">{{ invite.status }}</span>
|
||
<PermissionButton
|
||
v-if="invite.status !== '已接受'"
|
||
permission="user.index.update.ALL"
|
||
variant="secondary"
|
||
@click="resendInvite(invite.id)"
|
||
>
|
||
<span class="!py-1 !px-2 !text-xs">重发</span>
|
||
</PermissionButton>
|
||
<PermissionButton
|
||
v-if="invite.status === '待接受'"
|
||
permission="user.index.update.ALL"
|
||
variant="secondary"
|
||
@click="expireInvite(invite.id)"
|
||
>
|
||
<span class="!py-1 !px-2 !text-xs">设为过期</span>
|
||
</PermissionButton>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template #empty>
|
||
<div v-if="!pagedInvites.length" class="text-sm text-mosquito-ink/60">暂无邀请记录</div>
|
||
</template>
|
||
</ListSection>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onMounted, ref, watch } from 'vue'
|
||
import { useAuditStore } from '../stores/audit'
|
||
import { useUserStore } from '../stores/users'
|
||
import { useDataService } from '../services'
|
||
import ListSection from '../components/ListSection.vue'
|
||
import PermissionButton from '../components/PermissionButton.vue'
|
||
import { RoleLabels, type AdminRole } from '../auth/roles'
|
||
|
||
const auditStore = useAuditStore()
|
||
const userStore = useUserStore()
|
||
const service = useDataService()
|
||
const query = ref('')
|
||
const statusFilter = ref('')
|
||
const page = ref(0)
|
||
const pageSize = 6
|
||
const loading = ref(false)
|
||
const form = ref({
|
||
email: '',
|
||
role: 'operation_manager'
|
||
})
|
||
|
||
// 状态映射:后端英文 -> 前端中文
|
||
const statusMap: Record<string, string> = {
|
||
'PENDING': '待接受',
|
||
'ACCEPTED': '已接受',
|
||
'REJECTED': '已拒绝',
|
||
'EXPIRED': '已过期'
|
||
}
|
||
|
||
const roleLabel = (role: string) => {
|
||
return RoleLabels[role as AdminRole] || role
|
||
}
|
||
|
||
const formatDate = (value: string) => new Date(value).toLocaleString('zh-CN')
|
||
|
||
// 从后端数据转换为前端显示格式
|
||
const convertBackendInvite = (invite: any) => ({
|
||
id: invite.id,
|
||
email: invite.email,
|
||
role: invite.role,
|
||
status: statusMap[invite.status] || invite.status,
|
||
statusRaw: invite.status,
|
||
invitedAt: invite.invitedAt,
|
||
expiredAt: invite.expiredAt
|
||
})
|
||
|
||
// 加载邀请列表
|
||
const loadInvites = async () => {
|
||
loading.value = true
|
||
try {
|
||
const invites = await service.getInvites()
|
||
const converted = Array.isArray(invites) ? invites.map(convertBackendInvite) : []
|
||
userStore.init([], converted, [])
|
||
} catch (error) {
|
||
console.error('加载邀请列表失败:', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await loadInvites()
|
||
})
|
||
|
||
const sendInvite = async () => {
|
||
if (!form.value.email) {
|
||
alert('请输入邮箱地址')
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
await service.createInvite(form.value.email, form.value.role)
|
||
auditStore.addLog('发送用户邀请', form.value.email)
|
||
form.value.email = ''
|
||
form.value.role = 'operation_manager'
|
||
await loadInvites()
|
||
alert('邀请发送成功')
|
||
} catch (error: any) {
|
||
alert(error.message || '发送邀请失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const resendInvite = async (id: number | string) => {
|
||
const numericId = typeof id === 'string' ? parseInt(id, 10) || 0 : id
|
||
loading.value = true
|
||
try {
|
||
await service.resendInvite(numericId)
|
||
const invite = userStore.invites.find((item) => String(item.id) === String(id))
|
||
auditStore.addLog('重发邀请', invite?.email ?? String(id))
|
||
await loadInvites()
|
||
alert('邀请重发成功')
|
||
} catch (error: any) {
|
||
alert(error.message || '重发邀请失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const expireInvite = async (id: number | string) => {
|
||
const numericId = typeof id === 'string' ? parseInt(id, 10) || 0 : id
|
||
loading.value = true
|
||
try {
|
||
await service.expireInvite(numericId)
|
||
const invite = userStore.invites.find((item) => String(item.id) === String(id))
|
||
auditStore.addLog('设置邀请过期', invite?.email ?? String(id))
|
||
await loadInvites()
|
||
alert('邀请已设置为过期')
|
||
} catch (error: any) {
|
||
alert(error.message || '设置邀请过期失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const filteredInvites = computed(() => {
|
||
return userStore.invites.filter((invite) => {
|
||
const matchesQuery = invite.email.includes(query.value.trim())
|
||
const matchesStatus = statusFilter.value ? invite.status === statusFilter.value : true
|
||
return matchesQuery && matchesStatus
|
||
})
|
||
})
|
||
|
||
const totalPages = computed(() => Math.max(1, Math.ceil(filteredInvites.value.length / pageSize)))
|
||
const pagedInvites = computed(() => {
|
||
const start = page.value * pageSize
|
||
return filteredInvites.value.slice(start, start + pageSize)
|
||
})
|
||
|
||
watch([query, statusFilter], () => {
|
||
page.value = 0
|
||
})
|
||
</script>
|