Files
wenzi/frontend/admin/src/views/InviteUserView.vue

222 lines
8.0 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>
<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>