141 lines
5.3 KiB
Vue
141 lines
5.3 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="管理员">管理员</option>
|
|||
|
|
<option value="运营">运营</option>
|
|||
|
|
<option value="只读">只读</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<button class="mos-btn mos-btn-accent w-full" @click="sendInvite">发送邀请(演示)</button>
|
|||
|
|
</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>
|
|||
|
|
<button
|
|||
|
|
v-if="invite.status !== '已接受'"
|
|||
|
|
class="mos-btn mos-btn-secondary !py-1 !px-2 !text-xs"
|
|||
|
|
@click="resendInvite(invite.id)"
|
|||
|
|
>
|
|||
|
|
重发
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
v-if="invite.status === '待接受'"
|
|||
|
|
class="mos-btn mos-btn-secondary !py-1 !px-2 !text-xs"
|
|||
|
|
@click="expireInvite(invite.id)"
|
|||
|
|
>
|
|||
|
|
设为过期
|
|||
|
|
</button>
|
|||
|
|
</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'
|
|||
|
|
|
|||
|
|
const auditStore = useAuditStore()
|
|||
|
|
const userStore = useUserStore()
|
|||
|
|
const service = useDataService()
|
|||
|
|
const query = ref('')
|
|||
|
|
const statusFilter = ref('')
|
|||
|
|
const page = ref(0)
|
|||
|
|
const pageSize = 6
|
|||
|
|
const form = ref({
|
|||
|
|
email: '',
|
|||
|
|
role: '运营'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onMounted(async () => {
|
|||
|
|
const invites = await service.getInvites()
|
|||
|
|
userStore.init([], invites, [])
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const roleLabel = (role: string) => {
|
|||
|
|
if (role === 'admin') return '管理员'
|
|||
|
|
if (role === 'operator') return '运营'
|
|||
|
|
return '只读'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const formatDate = (value: string) => new Date(value).toLocaleString('zh-CN')
|
|||
|
|
|
|||
|
|
const sendInvite = () => {
|
|||
|
|
userStore.addInvite(form.value.email || '未填写邮箱', form.value.role === '管理员' ? 'admin' : form.value.role === '运营' ? 'operator' : 'viewer')
|
|||
|
|
auditStore.addLog('发送用户邀请', form.value.email || '未填写邮箱')
|
|||
|
|
form.value.email = ''
|
|||
|
|
form.value.role = '运营'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const resendInvite = (id: string) => {
|
|||
|
|
userStore.resendInvite(id)
|
|||
|
|
const invite = userStore.invites.find((item) => item.id === id)
|
|||
|
|
auditStore.addLog('重发邀请', invite?.email ?? id)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const expireInvite = (id: string) => {
|
|||
|
|
userStore.expireInvite(id)
|
|||
|
|
const invite = userStore.invites.find((item) => item.id === id)
|
|||
|
|
auditStore.addLog('设置邀请过期', invite?.email ?? id)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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>
|