707 lines
23 KiB
Vue
707 lines
23 KiB
Vue
<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>
|