feat: add creator member management
This commit is contained in:
@@ -31,6 +31,25 @@ export const creatorApi = {
|
||||
request(`/creator/coupons/${id}`, { method: "PUT", body: data }),
|
||||
grantCoupon: (id, data) =>
|
||||
request(`/creator/coupons/${id}/grant`, { method: "POST", body: data }),
|
||||
listMembers: (params) => {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
return request(`/creator/members?${qs}`);
|
||||
},
|
||||
removeMember: (id) => request(`/creator/members/${id}`, { method: "DELETE" }),
|
||||
listMemberInvites: (params) => {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
return request(`/creator/members/invites?${qs}`);
|
||||
},
|
||||
createMemberInvite: (data) =>
|
||||
request("/creator/members/invite", { method: "POST", body: data }),
|
||||
disableMemberInvite: (id) =>
|
||||
request(`/creator/members/invites/${id}`, { method: "DELETE" }),
|
||||
listMemberJoinRequests: (params) => {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
return request(`/creator/members/join-requests?${qs}`);
|
||||
},
|
||||
reviewMemberJoinRequest: (id, data) =>
|
||||
request(`/creator/members/${id}/review`, { method: "POST", body: data }),
|
||||
getSettings: () => request("/creator/settings"),
|
||||
updateSettings: (data) =>
|
||||
request("/creator/settings", { method: "PUT", body: data }),
|
||||
|
||||
@@ -108,6 +108,16 @@ const isFullWidth = computed(() => {
|
||||
配置
|
||||
</div>
|
||||
|
||||
<router-link
|
||||
:to="tenantRoute('/creator/members')"
|
||||
active-class="bg-primary-600 text-white shadow-md shadow-primary-900/20"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-slate-800 hover:text-white transition-all group"
|
||||
>
|
||||
<i
|
||||
class="pi pi-users text-lg group-hover:scale-110 transition-transform"
|
||||
></i>
|
||||
<span class="font-medium">成员管理</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="tenantRoute('/creator/settings')"
|
||||
active-class="bg-primary-600 text-white shadow-md shadow-primary-900/20"
|
||||
|
||||
@@ -149,6 +149,11 @@ const router = createRouter({
|
||||
name: "creator-orders",
|
||||
component: () => import("../views/creator/OrdersView.vue"),
|
||||
},
|
||||
{
|
||||
path: "members",
|
||||
name: "creator-members",
|
||||
component: () => import("../views/creator/MembersView.vue"),
|
||||
},
|
||||
{
|
||||
path: "coupons",
|
||||
name: "creator-coupons",
|
||||
|
||||
706
frontend/portal/src/views/creator/MembersView.vue
Normal file
706
frontend/portal/src/views/creator/MembersView.vue
Normal file
@@ -0,0 +1,706 @@
|
||||
<script setup>
|
||||
import Dialog from "primevue/dialog";
|
||||
import Toast from "primevue/toast";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { creatorApi } from "../../api/creator";
|
||||
|
||||
const toast = useToast();
|
||||
const activeTab = ref("members");
|
||||
const loading = reactive({
|
||||
members: false,
|
||||
invites: false,
|
||||
requests: false,
|
||||
});
|
||||
|
||||
const memberKeyword = ref("");
|
||||
const memberRole = ref("");
|
||||
const memberStatus = ref("");
|
||||
const members = ref([]);
|
||||
const memberPager = ref({ page: 1, limit: 10, total: 0 });
|
||||
|
||||
const inviteStatus = ref("");
|
||||
const invites = ref([]);
|
||||
const invitePager = ref({ page: 1, limit: 10, total: 0 });
|
||||
const inviteDialog = ref(false);
|
||||
const inviteForm = reactive({
|
||||
max_uses: 1,
|
||||
expires_at: "",
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const requestKeyword = ref("");
|
||||
const requestStatus = ref("pending");
|
||||
const requests = ref([]);
|
||||
const requestPager = ref({ page: 1, limit: 10, total: 0 });
|
||||
const reviewDialog = ref(false);
|
||||
const reviewAction = ref("approve");
|
||||
const reviewReason = ref("");
|
||||
const reviewTarget = ref(null);
|
||||
|
||||
const tabs = [
|
||||
{ key: "members", label: "成员列表" },
|
||||
{ key: "requests", label: "加入申请" },
|
||||
{ key: "invites", label: "邀请记录" },
|
||||
];
|
||||
|
||||
const fetchMembers = async () => {
|
||||
loading.members = true;
|
||||
try {
|
||||
const res = await creatorApi.listMembers({
|
||||
page: memberPager.value.page,
|
||||
limit: memberPager.value.limit,
|
||||
keyword: memberKeyword.value,
|
||||
role: memberRole.value,
|
||||
status: memberStatus.value,
|
||||
});
|
||||
members.value = res.items || [];
|
||||
memberPager.value.total = res.total || 0;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.members = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchInvites = async () => {
|
||||
loading.invites = true;
|
||||
try {
|
||||
const res = await creatorApi.listMemberInvites({
|
||||
page: invitePager.value.page,
|
||||
limit: invitePager.value.limit,
|
||||
status: inviteStatus.value,
|
||||
});
|
||||
invites.value = res.items || [];
|
||||
invitePager.value.total = res.total || 0;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.invites = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchJoinRequests = async () => {
|
||||
loading.requests = true;
|
||||
try {
|
||||
const res = await creatorApi.listMemberJoinRequests({
|
||||
page: requestPager.value.page,
|
||||
limit: requestPager.value.limit,
|
||||
status: requestStatus.value,
|
||||
keyword: requestKeyword.value,
|
||||
});
|
||||
requests.value = res.items || [];
|
||||
requestPager.value.total = res.total || 0;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.requests = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchMembers();
|
||||
fetchInvites();
|
||||
fetchJoinRequests();
|
||||
});
|
||||
|
||||
const memberStatusStyle = (status) => {
|
||||
switch (status) {
|
||||
case "verified":
|
||||
return { bg: "bg-emerald-50", text: "text-emerald-600" };
|
||||
case "banned":
|
||||
return { bg: "bg-rose-50", text: "text-rose-600" };
|
||||
default:
|
||||
return { bg: "bg-slate-100", text: "text-slate-500" };
|
||||
}
|
||||
};
|
||||
|
||||
const inviteStatusStyle = (status) => {
|
||||
switch (status) {
|
||||
case "active":
|
||||
return { bg: "bg-emerald-50", text: "text-emerald-600" };
|
||||
case "disabled":
|
||||
return { bg: "bg-slate-100", text: "text-slate-500" };
|
||||
case "expired":
|
||||
return { bg: "bg-amber-50", text: "text-amber-700" };
|
||||
default:
|
||||
return { bg: "bg-slate-100", text: "text-slate-500" };
|
||||
}
|
||||
};
|
||||
|
||||
const requestStatusStyle = (status) => {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return { bg: "bg-amber-50", text: "text-amber-700" };
|
||||
case "approved":
|
||||
return { bg: "bg-emerald-50", text: "text-emerald-600" };
|
||||
case "rejected":
|
||||
return { bg: "bg-rose-50", text: "text-rose-600" };
|
||||
default:
|
||||
return { bg: "bg-slate-100", text: "text-slate-500" };
|
||||
}
|
||||
};
|
||||
|
||||
const openInviteDialog = () => {
|
||||
inviteForm.max_uses = 1;
|
||||
inviteForm.expires_at = "";
|
||||
inviteForm.remark = "";
|
||||
inviteDialog.value = true;
|
||||
};
|
||||
|
||||
const createInvite = async () => {
|
||||
try {
|
||||
const payload = {
|
||||
max_uses: Number(inviteForm.max_uses) || 1,
|
||||
remark: inviteForm.remark,
|
||||
};
|
||||
if (inviteForm.expires_at) {
|
||||
payload.expires_at = new Date(inviteForm.expires_at).toISOString();
|
||||
}
|
||||
await creatorApi.createMemberInvite(payload);
|
||||
inviteDialog.value = false;
|
||||
toast.add({ severity: "success", summary: "已创建邀请", life: 2000 });
|
||||
fetchInvites();
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "创建失败",
|
||||
detail: e.message,
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const disableInvite = async (invite) => {
|
||||
if (!invite || !invite.id) return;
|
||||
if (!window.confirm("确认撤销该邀请?")) return;
|
||||
try {
|
||||
await creatorApi.disableMemberInvite(invite.id);
|
||||
toast.add({ severity: "success", summary: "已撤销", life: 2000 });
|
||||
fetchInvites();
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "撤销失败",
|
||||
detail: e.message,
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeMember = async (member) => {
|
||||
if (!member || !member.id) return;
|
||||
if (!window.confirm("确认移除该成员?")) return;
|
||||
try {
|
||||
await creatorApi.removeMember(member.id);
|
||||
toast.add({ severity: "success", summary: "成员已移除", life: 2000 });
|
||||
fetchMembers();
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "移除失败",
|
||||
detail: e.message,
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const openReviewDialog = (item, action) => {
|
||||
reviewTarget.value = item;
|
||||
reviewAction.value = action;
|
||||
reviewReason.value = "";
|
||||
reviewDialog.value = true;
|
||||
};
|
||||
|
||||
const submitReview = async () => {
|
||||
if (!reviewTarget.value) return;
|
||||
try {
|
||||
await creatorApi.reviewMemberJoinRequest(reviewTarget.value.id, {
|
||||
action: reviewAction.value,
|
||||
reason: reviewReason.value,
|
||||
});
|
||||
reviewDialog.value = false;
|
||||
toast.add({ severity: "success", summary: "已处理申请", life: 2000 });
|
||||
fetchJoinRequests();
|
||||
fetchMembers();
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "处理失败",
|
||||
detail: e.message,
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const tabClass = (key) =>
|
||||
key === activeTab.value
|
||||
? "bg-slate-900 text-white"
|
||||
: "bg-white text-slate-600 border border-slate-200 hover:bg-slate-50";
|
||||
|
||||
const pendingRequests = computed(() =>
|
||||
requests.value.filter((item) => item.status === "pending"),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Toast />
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-slate-900">成员管理</h1>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="px-4 py-2 rounded-lg text-sm font-semibold transition-colors"
|
||||
:class="tabClass(tab.key)"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'members'" class="space-y-6">
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border border-slate-100 p-4 flex flex-wrap gap-4 items-center"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-bold text-slate-500">角色:</span>
|
||||
<select
|
||||
v-model="memberRole"
|
||||
class="h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none bg-white"
|
||||
>
|
||||
<option value="">全部</option>
|
||||
<option value="member">成员</option>
|
||||
<option value="tenant_admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-bold text-slate-500">状态:</span>
|
||||
<select
|
||||
v-model="memberStatus"
|
||||
class="h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none bg-white"
|
||||
>
|
||||
<option value="">全部</option>
|
||||
<option value="verified">已审核</option>
|
||||
<option value="banned">已封禁</option>
|
||||
<option value="active">正常</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ml-auto relative w-72">
|
||||
<i
|
||||
class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"
|
||||
></i>
|
||||
<input
|
||||
type="text"
|
||||
v-model="memberKeyword"
|
||||
@keyup.enter="fetchMembers"
|
||||
placeholder="搜索昵称/手机号..."
|
||||
class="w-full h-9 pl-9 pr-4 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg bg-slate-900 text-white text-sm font-semibold"
|
||||
@click="fetchMembers"
|
||||
>
|
||||
筛选
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden"
|
||||
>
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead
|
||||
class="bg-slate-50 text-slate-500 font-bold border-b border-slate-200"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-6 py-4">成员</th>
|
||||
<th class="px-6 py-4">角色</th>
|
||||
<th class="px-6 py-4">状态</th>
|
||||
<th class="px-6 py-4">加入时间</th>
|
||||
<th class="px-6 py-4 text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tr v-if="loading.members">
|
||||
<td colspan="5" class="px-6 py-6 text-center text-slate-500">
|
||||
加载中...
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else-if="members.length === 0">
|
||||
<td colspan="5" class="px-6 py-8 text-center text-slate-400">
|
||||
暂无成员数据
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="member in members"
|
||||
:key="member.id"
|
||||
class="hover:bg-slate-50"
|
||||
>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<img
|
||||
:src="
|
||||
member.user?.avatar ||
|
||||
`https://api.dicebear.com/7.x/avataaars/svg?seed=${member.user?.id}`
|
||||
"
|
||||
class="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<div class="font-semibold text-slate-900">
|
||||
{{
|
||||
member.user?.nickname || member.user?.username || "-"
|
||||
}}
|
||||
</div>
|
||||
<div class="text-xs text-slate-400">
|
||||
{{ member.user?.phone || "--" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-700">
|
||||
{{ member.role_description?.join(" / ") || "成员" }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span
|
||||
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||
:class="`${memberStatusStyle(member.status).bg} ${memberStatusStyle(member.status).text}`"
|
||||
>
|
||||
{{ member.status_description || member.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-500">
|
||||
{{ member.created_at || "--" }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button
|
||||
class="text-rose-600 text-sm font-semibold hover:text-rose-700"
|
||||
@click="removeMember(member)"
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'requests'" class="space-y-6">
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border border-slate-100 p-4 flex flex-wrap gap-4 items-center"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-bold text-slate-500">状态:</span>
|
||||
<select
|
||||
v-model="requestStatus"
|
||||
class="h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none bg-white"
|
||||
>
|
||||
<option value="pending">待审核</option>
|
||||
<option value="approved">已通过</option>
|
||||
<option value="rejected">已拒绝</option>
|
||||
<option value="">全部</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ml-auto relative w-72">
|
||||
<i
|
||||
class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"
|
||||
></i>
|
||||
<input
|
||||
type="text"
|
||||
v-model="requestKeyword"
|
||||
@keyup.enter="fetchJoinRequests"
|
||||
placeholder="搜索昵称/手机号..."
|
||||
class="w-full h-9 pl-9 pr-4 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg bg-slate-900 text-white text-sm font-semibold"
|
||||
@click="fetchJoinRequests"
|
||||
>
|
||||
筛选
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden"
|
||||
>
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead
|
||||
class="bg-slate-50 text-slate-500 font-bold border-b border-slate-200"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-6 py-4">申请人</th>
|
||||
<th class="px-6 py-4">申请理由</th>
|
||||
<th class="px-6 py-4">状态</th>
|
||||
<th class="px-6 py-4">申请时间</th>
|
||||
<th class="px-6 py-4 text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tr v-if="loading.requests">
|
||||
<td colspan="5" class="px-6 py-6 text-center text-slate-500">
|
||||
加载中...
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else-if="requests.length === 0">
|
||||
<td colspan="5" class="px-6 py-8 text-center text-slate-400">
|
||||
暂无申请记录
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="req in requests" :key="req.id" class="hover:bg-slate-50">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<img
|
||||
:src="
|
||||
req.user?.avatar ||
|
||||
`https://api.dicebear.com/7.x/avataaars/svg?seed=${req.user?.id}`
|
||||
"
|
||||
class="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<div class="font-semibold text-slate-900">
|
||||
{{ req.user?.nickname || req.user?.username || "-" }}
|
||||
</div>
|
||||
<div class="text-xs text-slate-400">
|
||||
{{ req.user?.phone || "--" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-600">
|
||||
{{ req.reason || "-" }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span
|
||||
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||
:class="`${requestStatusStyle(req.status).bg} ${requestStatusStyle(req.status).text}`"
|
||||
>
|
||||
{{ req.status_description || req.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-500">
|
||||
{{ req.created_at || "--" }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div
|
||||
v-if="req.status === 'pending'"
|
||||
class="flex justify-end gap-2"
|
||||
>
|
||||
<button
|
||||
class="px-3 py-1 text-sm rounded-lg bg-emerald-500 text-white"
|
||||
@click="openReviewDialog(req, 'approve')"
|
||||
>
|
||||
通过
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1 text-sm rounded-lg bg-rose-500 text-white"
|
||||
@click="openReviewDialog(req, 'reject')"
|
||||
>
|
||||
拒绝
|
||||
</button>
|
||||
</div>
|
||||
<span v-else class="text-slate-400 text-sm">已处理</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="pendingRequests.length === 0" class="text-sm text-slate-500">
|
||||
当前暂无待审核申请。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border border-slate-100 p-4 flex gap-4 items-center"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-bold text-slate-500">状态:</span>
|
||||
<select
|
||||
v-model="inviteStatus"
|
||||
class="h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none bg-white"
|
||||
>
|
||||
<option value="">全部</option>
|
||||
<option value="active">可用</option>
|
||||
<option value="disabled">已撤销</option>
|
||||
<option value="expired">已过期</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg bg-slate-900 text-white text-sm font-semibold"
|
||||
@click="fetchInvites"
|
||||
>
|
||||
筛选
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg bg-primary-600 text-white text-sm font-semibold shadow-sm"
|
||||
@click="openInviteDialog"
|
||||
>
|
||||
<i class="pi pi-plus mr-1"></i> 新建邀请
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden"
|
||||
>
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead
|
||||
class="bg-slate-50 text-slate-500 font-bold border-b border-slate-200"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-6 py-4">邀请码</th>
|
||||
<th class="px-6 py-4">状态</th>
|
||||
<th class="px-6 py-4">使用情况</th>
|
||||
<th class="px-6 py-4">过期时间</th>
|
||||
<th class="px-6 py-4">备注</th>
|
||||
<th class="px-6 py-4 text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tr v-if="loading.invites">
|
||||
<td colspan="6" class="px-6 py-6 text-center text-slate-500">
|
||||
加载中...
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else-if="invites.length === 0">
|
||||
<td colspan="6" class="px-6 py-8 text-center text-slate-400">
|
||||
暂无邀请记录
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="invite in invites"
|
||||
:key="invite.id"
|
||||
class="hover:bg-slate-50"
|
||||
>
|
||||
<td class="px-6 py-4 font-mono text-slate-700">
|
||||
{{ invite.code }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span
|
||||
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||
:class="`${inviteStatusStyle(invite.status).bg} ${inviteStatusStyle(invite.status).text}`"
|
||||
>
|
||||
{{ invite.status_description || invite.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-600">
|
||||
{{ invite.used_count }} / {{ invite.max_uses }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-500">
|
||||
{{ invite.expires_at || "长期有效" }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-500">
|
||||
{{ invite.remark || "-" }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button
|
||||
v-if="invite.status === 'active'"
|
||||
class="text-rose-600 text-sm font-semibold hover:text-rose-700"
|
||||
@click="disableInvite(invite)"
|
||||
>
|
||||
撤销
|
||||
</button>
|
||||
<span v-else class="text-slate-400 text-sm">--</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
v-model:visible="inviteDialog"
|
||||
modal
|
||||
header="新建邀请"
|
||||
class="w-[420px]"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm text-slate-600 mb-2">最大使用次数</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
v-model="inviteForm.max_uses"
|
||||
class="w-full h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-slate-600 mb-2">过期时间</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
v-model="inviteForm.expires_at"
|
||||
class="w-full h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-slate-600 mb-2">备注</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="inviteForm.remark"
|
||||
placeholder="可选"
|
||||
class="w-full h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg border border-slate-200 text-sm text-slate-600 mr-2"
|
||||
@click="inviteDialog = false"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg bg-slate-900 text-white text-sm font-semibold"
|
||||
@click="createInvite"
|
||||
>
|
||||
创建
|
||||
</button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
v-model:visible="reviewDialog"
|
||||
modal
|
||||
header="审核申请"
|
||||
class="w-[420px]"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="text-sm text-slate-600">
|
||||
当前操作:
|
||||
<span class="font-semibold text-slate-900">{{
|
||||
reviewAction === "approve" ? "通过" : "拒绝"
|
||||
}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-slate-600 mb-2">备注说明</label>
|
||||
<textarea
|
||||
v-model="reviewReason"
|
||||
rows="3"
|
||||
placeholder="可选"
|
||||
class="w-full px-3 py-2 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg border border-slate-200 text-sm text-slate-600 mr-2"
|
||||
@click="reviewDialog = false"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg bg-slate-900 text-white text-sm font-semibold"
|
||||
@click="submitReview"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user