feat: add superadmin creator review and coupon governance
This commit is contained in:
@@ -69,6 +69,46 @@ export const CouponService = {
|
||||
method: 'POST',
|
||||
body: { user_ids: userIDs }
|
||||
});
|
||||
},
|
||||
async updateCouponStatus(couponID, status) {
|
||||
if (!couponID) throw new Error('couponID is required');
|
||||
if (!status) throw new Error('status is required');
|
||||
return requestJson(`/super/v1/coupons/${couponID}/status`, {
|
||||
method: 'PATCH',
|
||||
body: { status }
|
||||
});
|
||||
},
|
||||
async listCouponGrants({ page, limit, coupon_id, tenant_id, tenant_code, tenant_name, user_id, username, status, created_at_from, created_at_to, used_at_from, used_at_to } = {}) {
|
||||
const iso = (d) => {
|
||||
if (!d) return undefined;
|
||||
const date = d instanceof Date ? d : new Date(d);
|
||||
if (Number.isNaN(date.getTime())) return undefined;
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
const query = {
|
||||
page,
|
||||
limit,
|
||||
coupon_id,
|
||||
tenant_id,
|
||||
tenant_code,
|
||||
tenant_name,
|
||||
user_id,
|
||||
username,
|
||||
status,
|
||||
created_at_from: iso(created_at_from),
|
||||
created_at_to: iso(created_at_to),
|
||||
used_at_from: iso(used_at_from),
|
||||
used_at_to: iso(used_at_to)
|
||||
};
|
||||
|
||||
const data = await requestJson('/super/v1/coupon-grants', { query });
|
||||
return {
|
||||
page: data?.page ?? page ?? 1,
|
||||
limit: data?.limit ?? limit ?? 10,
|
||||
total: data?.total ?? 0,
|
||||
items: normalizeItems(data?.items)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -68,6 +68,41 @@ export const CreatorService = {
|
||||
items: normalizeItems(data?.items)
|
||||
};
|
||||
},
|
||||
async listCreatorApplications({ page, limit, id, user_id, name, code, status, created_at_from, created_at_to } = {}) {
|
||||
const iso = (d) => {
|
||||
if (!d) return undefined;
|
||||
const date = d instanceof Date ? d : new Date(d);
|
||||
if (Number.isNaN(date.getTime())) return undefined;
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
const query = {
|
||||
page,
|
||||
limit,
|
||||
id,
|
||||
user_id,
|
||||
name,
|
||||
code,
|
||||
status,
|
||||
created_at_from: iso(created_at_from),
|
||||
created_at_to: iso(created_at_to)
|
||||
};
|
||||
|
||||
const data = await requestJson('/super/v1/creator-applications', { query });
|
||||
return {
|
||||
page: data?.page ?? page ?? 1,
|
||||
limit: data?.limit ?? limit ?? 10,
|
||||
total: data?.total ?? 0,
|
||||
items: normalizeItems(data?.items)
|
||||
};
|
||||
},
|
||||
async reviewCreatorApplication(tenantID, { action, reason } = {}) {
|
||||
if (!tenantID) throw new Error('tenantID is required');
|
||||
return requestJson(`/super/v1/creator-applications/${tenantID}/review`, {
|
||||
method: 'POST',
|
||||
body: { action, reason }
|
||||
});
|
||||
},
|
||||
async reviewJoinRequest(requestID, { action, reason } = {}) {
|
||||
if (!requestID) throw new Error('requestID is required');
|
||||
return requestJson(`/super/v1/tenant-join-requests/${requestID}/review`, {
|
||||
@@ -75,6 +110,39 @@ export const CreatorService = {
|
||||
body: { action, reason }
|
||||
});
|
||||
},
|
||||
async listPayoutAccounts({ page, limit, tenant_id, tenant_code, tenant_name, user_id, username, type, created_at_from, created_at_to } = {}) {
|
||||
const iso = (d) => {
|
||||
if (!d) return undefined;
|
||||
const date = d instanceof Date ? d : new Date(d);
|
||||
if (Number.isNaN(date.getTime())) return undefined;
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
const query = {
|
||||
page,
|
||||
limit,
|
||||
tenant_id,
|
||||
tenant_code,
|
||||
tenant_name,
|
||||
user_id,
|
||||
username,
|
||||
type,
|
||||
created_at_from: iso(created_at_from),
|
||||
created_at_to: iso(created_at_to)
|
||||
};
|
||||
|
||||
const data = await requestJson('/super/v1/payout-accounts', { query });
|
||||
return {
|
||||
page: data?.page ?? page ?? 1,
|
||||
limit: data?.limit ?? limit ?? 10,
|
||||
total: data?.total ?? 0,
|
||||
items: normalizeItems(data?.items)
|
||||
};
|
||||
},
|
||||
async removePayoutAccount(accountID) {
|
||||
if (!accountID) throw new Error('accountID is required');
|
||||
return requestJson(`/super/v1/payout-accounts/${accountID}`, { method: 'DELETE' });
|
||||
},
|
||||
async createInvite(tenantID, { max_uses, expires_at, remark } = {}) {
|
||||
if (!tenantID) throw new Error('tenantID is required');
|
||||
return requestJson(`/super/v1/tenants/${tenantID}/invites`, {
|
||||
|
||||
@@ -7,6 +7,8 @@ import { onMounted, ref } from 'vue';
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const tabValue = ref('coupons');
|
||||
|
||||
const coupons = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -26,6 +28,23 @@ const createdAtTo = ref(null);
|
||||
const sortField = ref('created_at');
|
||||
const sortOrder = ref(-1);
|
||||
|
||||
const grants = ref([]);
|
||||
const grantsLoading = ref(false);
|
||||
const grantsTotal = ref(0);
|
||||
const grantsPage = ref(1);
|
||||
const grantsRows = ref(10);
|
||||
const grantCouponIDFilter = ref(null);
|
||||
const grantTenantID = ref(null);
|
||||
const grantTenantCode = ref('');
|
||||
const grantTenantName = ref('');
|
||||
const grantUserID = ref(null);
|
||||
const grantUsername = ref('');
|
||||
const grantStatus = ref('');
|
||||
const grantCreatedAtFrom = ref(null);
|
||||
const grantCreatedAtTo = ref(null);
|
||||
const grantUsedAtFrom = ref(null);
|
||||
const grantUsedAtTo = ref(null);
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '固定金额', value: 'fix_amount' },
|
||||
@@ -39,6 +58,13 @@ const statusOptions = [
|
||||
{ label: '已过期', value: 'expired' }
|
||||
];
|
||||
|
||||
const grantStatusOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '未使用', value: 'unused' },
|
||||
{ label: '已使用', value: 'used' },
|
||||
{ label: '已过期', value: 'expired' }
|
||||
];
|
||||
|
||||
const editDialogVisible = ref(false);
|
||||
const couponSubmitting = ref(false);
|
||||
const editingCoupon = ref(null);
|
||||
@@ -58,6 +84,10 @@ const grantSubmitting = ref(false);
|
||||
const grantCoupon = ref(null);
|
||||
const grantUserIDsText = ref('');
|
||||
|
||||
const freezeDialogVisible = ref(false);
|
||||
const freezeSubmitting = ref(false);
|
||||
const freezeTarget = ref(null);
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return '-';
|
||||
if (String(value).startsWith('0001-01-01')) return '-';
|
||||
@@ -92,6 +122,19 @@ function getStatusSeverity(value) {
|
||||
}
|
||||
}
|
||||
|
||||
function getGrantStatusSeverity(value) {
|
||||
switch (value) {
|
||||
case 'unused':
|
||||
return 'success';
|
||||
case 'used':
|
||||
return 'secondary';
|
||||
case 'expired':
|
||||
return 'danger';
|
||||
default:
|
||||
return 'secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function resetCouponForm() {
|
||||
formTenantID.value = null;
|
||||
formTitle.value = '';
|
||||
@@ -197,6 +240,27 @@ async function confirmGrant() {
|
||||
}
|
||||
}
|
||||
|
||||
function openFreezeDialog(row) {
|
||||
freezeTarget.value = row;
|
||||
freezeDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function confirmFreeze() {
|
||||
const couponIDValue = freezeTarget.value?.id;
|
||||
if (!couponIDValue) return;
|
||||
freezeSubmitting.value = true;
|
||||
try {
|
||||
await CouponService.updateCouponStatus(couponIDValue, 'frozen');
|
||||
toast.add({ severity: 'success', summary: '已冻结', detail: `CouponID: ${couponIDValue}`, life: 3000 });
|
||||
freezeDialogVisible.value = false;
|
||||
await loadCoupons();
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '冻结失败', detail: error?.message || '无法冻结优惠券', life: 4000 });
|
||||
} finally {
|
||||
freezeSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCoupons() {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -224,11 +288,43 @@ async function loadCoupons() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGrants() {
|
||||
grantsLoading.value = true;
|
||||
try {
|
||||
const result = await CouponService.listCouponGrants({
|
||||
page: grantsPage.value,
|
||||
limit: grantsRows.value,
|
||||
coupon_id: grantCouponIDFilter.value || undefined,
|
||||
tenant_id: grantTenantID.value || undefined,
|
||||
tenant_code: grantTenantCode.value,
|
||||
tenant_name: grantTenantName.value,
|
||||
user_id: grantUserID.value || undefined,
|
||||
username: grantUsername.value,
|
||||
status: grantStatus.value || undefined,
|
||||
created_at_from: grantCreatedAtFrom.value || undefined,
|
||||
created_at_to: grantCreatedAtTo.value || undefined,
|
||||
used_at_from: grantUsedAtFrom.value || undefined,
|
||||
used_at_to: grantUsedAtTo.value || undefined
|
||||
});
|
||||
grants.value = result.items;
|
||||
grantsTotal.value = result.total;
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载发放记录', life: 4000 });
|
||||
} finally {
|
||||
grantsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
page.value = 1;
|
||||
loadCoupons();
|
||||
}
|
||||
|
||||
function onGrantSearch() {
|
||||
grantsPage.value = 1;
|
||||
loadGrants();
|
||||
}
|
||||
|
||||
function onReset() {
|
||||
couponID.value = null;
|
||||
tenantID.value = null;
|
||||
@@ -246,12 +342,35 @@ function onReset() {
|
||||
loadCoupons();
|
||||
}
|
||||
|
||||
function onGrantReset() {
|
||||
grantCouponIDFilter.value = null;
|
||||
grantTenantID.value = null;
|
||||
grantTenantCode.value = '';
|
||||
grantTenantName.value = '';
|
||||
grantUserID.value = null;
|
||||
grantUsername.value = '';
|
||||
grantStatus.value = '';
|
||||
grantCreatedAtFrom.value = null;
|
||||
grantCreatedAtTo.value = null;
|
||||
grantUsedAtFrom.value = null;
|
||||
grantUsedAtTo.value = null;
|
||||
grantsPage.value = 1;
|
||||
grantsRows.value = 10;
|
||||
loadGrants();
|
||||
}
|
||||
|
||||
function onPage(event) {
|
||||
page.value = (event.page ?? 0) + 1;
|
||||
rows.value = event.rows ?? rows.value;
|
||||
loadCoupons();
|
||||
}
|
||||
|
||||
function onGrantPage(event) {
|
||||
grantsPage.value = (event.page ?? 0) + 1;
|
||||
grantsRows.value = event.rows ?? grantsRows.value;
|
||||
loadGrants();
|
||||
}
|
||||
|
||||
function onSort(event) {
|
||||
sortField.value = event.sortField ?? sortField.value;
|
||||
sortOrder.value = event.sortOrder ?? sortOrder.value;
|
||||
@@ -260,131 +379,254 @@ function onSort(event) {
|
||||
|
||||
onMounted(() => {
|
||||
loadCoupons();
|
||||
loadGrants();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="m-0">优惠券</h4>
|
||||
<Button label="新建优惠券" icon="pi pi-plus" @click="openCreateDialog" />
|
||||
</div>
|
||||
|
||||
<SearchPanel :loading="loading" @search="onSearch" @reset="onReset">
|
||||
<SearchField label="CouponID">
|
||||
<InputNumber v-model="couponID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="TenantID">
|
||||
<InputNumber v-model="tenantID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="Tenant Code">
|
||||
<InputText v-model="tenantCode" placeholder="模糊匹配" class="w-full" @keyup.enter="onSearch" />
|
||||
</SearchField>
|
||||
<SearchField label="Tenant Name">
|
||||
<InputText v-model="tenantName" placeholder="模糊匹配" class="w-full" @keyup.enter="onSearch" />
|
||||
</SearchField>
|
||||
<SearchField label="关键词">
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="keyword" placeholder="标题/描述" class="w-full" @keyup.enter="onSearch" />
|
||||
</IconField>
|
||||
</SearchField>
|
||||
<SearchField label="类型">
|
||||
<Select v-model="type" :options="typeOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="状态">
|
||||
<Select v-model="status" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="创建时间 From">
|
||||
<DatePicker v-model="createdAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="创建时间 To">
|
||||
<DatePicker v-model="createdAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||
</SearchField>
|
||||
</SearchPanel>
|
||||
|
||||
<DataTable
|
||||
:value="coupons"
|
||||
dataKey="id"
|
||||
:loading="loading"
|
||||
lazy
|
||||
:paginator="true"
|
||||
:rows="rows"
|
||||
:totalRecords="totalRecords"
|
||||
:first="(page - 1) * rows"
|
||||
:rowsPerPageOptions="[10, 20, 50, 100]"
|
||||
sortMode="single"
|
||||
:sortField="sortField"
|
||||
:sortOrder="sortOrder"
|
||||
@page="onPage"
|
||||
@sort="onSort"
|
||||
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
scrollable
|
||||
scrollHeight="flex"
|
||||
responsiveLayout="scroll"
|
||||
>
|
||||
<Column field="id" header="ID" sortable style="min-width: 6rem" />
|
||||
<Column header="租户" style="min-width: 16rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ data.tenant_name || '-' }}</span>
|
||||
<span class="text-xs text-muted-color">Code: {{ data.tenant_code || '-' }}</span>
|
||||
<Tabs v-model:value="tabValue" value="coupons">
|
||||
<TabList>
|
||||
<Tab value="coupons">券模板</Tab>
|
||||
<Tab value="grants">发放记录</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel value="coupons">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="m-0">优惠券</h4>
|
||||
<Button label="新建优惠券" icon="pi pi-plus" @click="openCreateDialog" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="title" header="标题" sortable style="min-width: 16rem" />
|
||||
<Column header="类型" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
{{ data.type_description || data.type || '-' }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="value" header="面额/折扣" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
<span v-if="data.type === 'discount'">{{ data.value ?? '-' }}%</span>
|
||||
<span v-else>{{ formatCny(data.value) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="min_order_amount" header="门槛" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatCny(data.min_order_amount) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="使用情况" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
<span v-if="data.total_quantity === 0">不限量</span>
|
||||
<span v-else>{{ data.used_quantity ?? 0 }} / {{ data.total_quantity ?? 0 }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="status" header="状态" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="data.status_description || data.status || '-'" :severity="getStatusSeverity(data.status)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="start_at" header="开始时间" sortable style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatDate(data.start_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="end_at" header="结束时间" sortable style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatDate(data.end_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="created_at" header="创建时间" sortable style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatDate(data.created_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="操作" style="min-width: 12rem">
|
||||
<template #body="{ data }">
|
||||
<Button label="编辑" icon="pi pi-pencil" text size="small" class="p-0 mr-3" @click="openEditDialog(data)" />
|
||||
<Button label="发放" icon="pi pi-send" text size="small" class="p-0" @click="openGrantDialog(data)" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
|
||||
<SearchPanel :loading="loading" @search="onSearch" @reset="onReset">
|
||||
<SearchField label="CouponID">
|
||||
<InputNumber v-model="couponID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="TenantID">
|
||||
<InputNumber v-model="tenantID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="Tenant Code">
|
||||
<InputText v-model="tenantCode" placeholder="模糊匹配" class="w-full" @keyup.enter="onSearch" />
|
||||
</SearchField>
|
||||
<SearchField label="Tenant Name">
|
||||
<InputText v-model="tenantName" placeholder="模糊匹配" class="w-full" @keyup.enter="onSearch" />
|
||||
</SearchField>
|
||||
<SearchField label="关键词">
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="keyword" placeholder="标题/描述" class="w-full" @keyup.enter="onSearch" />
|
||||
</IconField>
|
||||
</SearchField>
|
||||
<SearchField label="类型">
|
||||
<Select v-model="type" :options="typeOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="状态">
|
||||
<Select v-model="status" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="创建时间 From">
|
||||
<DatePicker v-model="createdAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="创建时间 To">
|
||||
<DatePicker v-model="createdAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||
</SearchField>
|
||||
</SearchPanel>
|
||||
|
||||
<DataTable
|
||||
:value="coupons"
|
||||
dataKey="id"
|
||||
:loading="loading"
|
||||
lazy
|
||||
:paginator="true"
|
||||
:rows="rows"
|
||||
:totalRecords="totalRecords"
|
||||
:first="(page - 1) * rows"
|
||||
:rowsPerPageOptions="[10, 20, 50, 100]"
|
||||
sortMode="single"
|
||||
:sortField="sortField"
|
||||
:sortOrder="sortOrder"
|
||||
@page="onPage"
|
||||
@sort="onSort"
|
||||
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
scrollable
|
||||
scrollHeight="flex"
|
||||
responsiveLayout="scroll"
|
||||
>
|
||||
<Column field="id" header="ID" sortable style="min-width: 6rem" />
|
||||
<Column header="租户" style="min-width: 16rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ data.tenant_name || '-' }}</span>
|
||||
<span class="text-xs text-muted-color">Code: {{ data.tenant_code || '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="title" header="标题" sortable style="min-width: 16rem" />
|
||||
<Column header="类型" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
{{ data.type_description || data.type || '-' }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="value" header="面额/折扣" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
<span v-if="data.type === 'discount'">{{ data.value ?? '-' }}%</span>
|
||||
<span v-else>{{ formatCny(data.value) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="min_order_amount" header="门槛" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatCny(data.min_order_amount) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="使用情况" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
<span v-if="data.total_quantity === 0">不限量</span>
|
||||
<span v-else>{{ data.used_quantity ?? 0 }} / {{ data.total_quantity ?? 0 }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="status" header="状态" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="data.status_description || data.status || '-'" :severity="getStatusSeverity(data.status)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="start_at" header="开始时间" sortable style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatDate(data.start_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="end_at" header="结束时间" sortable style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatDate(data.end_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="created_at" header="创建时间" sortable style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatDate(data.created_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="操作" style="min-width: 12rem">
|
||||
<template #body="{ data }">
|
||||
<Button label="编辑" icon="pi pi-pencil" text size="small" class="p-0 mr-3" @click="openEditDialog(data)" />
|
||||
<Button label="发放" icon="pi pi-send" text size="small" class="p-0 mr-3" @click="openGrantDialog(data)" />
|
||||
<Button label="冻结" icon="pi pi-lock" text size="small" severity="danger" class="p-0" :disabled="data?.status === 'expired'" @click="openFreezeDialog(data)" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</TabPanel>
|
||||
<TabPanel value="grants">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="m-0">发放记录</h4>
|
||||
</div>
|
||||
|
||||
<SearchPanel :loading="grantsLoading" @search="onGrantSearch" @reset="onGrantReset">
|
||||
<SearchField label="CouponID">
|
||||
<InputNumber v-model="grantCouponIDFilter" :min="1" placeholder="精确匹配" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="TenantID">
|
||||
<InputNumber v-model="grantTenantID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="Tenant Code">
|
||||
<InputText v-model="grantTenantCode" placeholder="模糊匹配" class="w-full" @keyup.enter="onGrantSearch" />
|
||||
</SearchField>
|
||||
<SearchField label="Tenant Name">
|
||||
<InputText v-model="grantTenantName" placeholder="模糊匹配" class="w-full" @keyup.enter="onGrantSearch" />
|
||||
</SearchField>
|
||||
<SearchField label="UserID">
|
||||
<InputNumber v-model="grantUserID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="Username">
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="grantUsername" placeholder="模糊匹配" class="w-full" @keyup.enter="onGrantSearch" />
|
||||
</IconField>
|
||||
</SearchField>
|
||||
<SearchField label="状态">
|
||||
<Select v-model="grantStatus" :options="grantStatusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="领取时间 From">
|
||||
<DatePicker v-model="grantCreatedAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="领取时间 To">
|
||||
<DatePicker v-model="grantCreatedAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="使用时间 From">
|
||||
<DatePicker v-model="grantUsedAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="使用时间 To">
|
||||
<DatePicker v-model="grantUsedAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||
</SearchField>
|
||||
</SearchPanel>
|
||||
|
||||
<DataTable
|
||||
:value="grants"
|
||||
dataKey="id"
|
||||
:loading="grantsLoading"
|
||||
lazy
|
||||
:paginator="true"
|
||||
:rows="grantsRows"
|
||||
:totalRecords="grantsTotal"
|
||||
:first="(grantsPage - 1) * grantsRows"
|
||||
:rowsPerPageOptions="[10, 20, 50, 100]"
|
||||
@page="onGrantPage"
|
||||
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
scrollable
|
||||
scrollHeight="flex"
|
||||
responsiveLayout="scroll"
|
||||
>
|
||||
<Column field="id" header="记录ID" style="min-width: 8rem" />
|
||||
<Column header="优惠券" style="min-width: 16rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ data.coupon_title || '-' }}</span>
|
||||
<span class="text-xs text-muted-color">CouponID: {{ data.coupon_id ?? '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="租户" style="min-width: 16rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ data.tenant_name || '-' }}</span>
|
||||
<span class="text-xs text-muted-color">Code: {{ data.tenant_code || '-' }} / ID: {{ data.tenant_id ?? '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="用户" style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
<router-link v-if="data.user_id" class="inline-flex items-center gap-1 font-medium text-primary hover:underline" :to="`/superadmin/users/${data.user_id}`">
|
||||
<span class="truncate max-w-[200px]">{{ data.username || `ID:${data.user_id}` }}</span>
|
||||
<i class="pi pi-external-link text-xs" />
|
||||
</router-link>
|
||||
<div class="text-xs text-muted-color">ID: {{ data.user_id ?? '-' }}</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="status" header="状态" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="data.status_description || data.status || '-'" :severity="getGrantStatusSeverity(data.status)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="order_id" header="使用订单" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
{{ data.order_id ? data.order_id : '-' }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="used_at" header="使用时间" style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatDate(data.used_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="created_at" header="领取时间" style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatDate(data.created_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:visible="editDialogVisible" :modal="true" :style="{ width: '600px' }">
|
||||
@@ -470,4 +712,24 @@ onMounted(() => {
|
||||
<Button label="确认发放" icon="pi pi-send" @click="confirmGrant" :loading="grantSubmitting" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="freezeDialogVisible" :modal="true" :style="{ width: '420px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">冻结优惠券</span>
|
||||
<span class="text-muted-color">ID: {{ freezeTarget?.id ?? '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="text-sm text-muted-color">冻结后将停止该券的发放与使用,请确认。</div>
|
||||
<div class="text-sm">
|
||||
<span class="font-medium">{{ freezeTarget?.title || '-' }}</span>
|
||||
<span class="text-muted-color">(TenantID: {{ freezeTarget?.tenant_id ?? '-' }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="取消" icon="pi pi-times" text @click="freezeDialogVisible = false" :disabled="freezeSubmitting" />
|
||||
<Button label="确认冻结" icon="pi pi-lock" severity="danger" @click="confirmFreeze" :loading="freezeSubmitting" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -4,7 +4,7 @@ import SearchPanel from '@/components/SearchPanel.vue';
|
||||
import { CreatorService } from '@/service/CreatorService';
|
||||
import { TenantService } from '@/service/TenantService';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
@@ -34,6 +34,8 @@ const statusUpdating = ref(false);
|
||||
const statusTenant = ref(null);
|
||||
const statusValue = ref(null);
|
||||
|
||||
const applicationStatusOptions = computed(() => [{ label: '全部', value: '' }, ...(statusOptions.value || [])]);
|
||||
|
||||
const joinRequests = ref([]);
|
||||
const joinRequestsLoading = ref(false);
|
||||
const joinRequestsTotal = ref(0);
|
||||
@@ -48,6 +50,33 @@ const joinStatus = ref('pending');
|
||||
const joinCreatedAtFrom = ref(null);
|
||||
const joinCreatedAtTo = ref(null);
|
||||
|
||||
const applications = ref([]);
|
||||
const applicationsLoading = ref(false);
|
||||
const applicationsTotal = ref(0);
|
||||
const applicationsPage = ref(1);
|
||||
const applicationsRows = ref(10);
|
||||
const applicationTenantID = ref(null);
|
||||
const applicationOwnerUserID = ref(null);
|
||||
const applicationName = ref('');
|
||||
const applicationCode = ref('');
|
||||
const applicationStatus = ref('pending_verify');
|
||||
const applicationCreatedAtFrom = ref(null);
|
||||
const applicationCreatedAtTo = ref(null);
|
||||
|
||||
const payoutAccounts = ref([]);
|
||||
const payoutAccountsLoading = ref(false);
|
||||
const payoutAccountsTotal = ref(0);
|
||||
const payoutAccountsPage = ref(1);
|
||||
const payoutAccountsRows = ref(10);
|
||||
const payoutTenantID = ref(null);
|
||||
const payoutTenantCode = ref('');
|
||||
const payoutTenantName = ref('');
|
||||
const payoutUserID = ref(null);
|
||||
const payoutUsername = ref('');
|
||||
const payoutType = ref('');
|
||||
const payoutCreatedAtFrom = ref(null);
|
||||
const payoutCreatedAtTo = ref(null);
|
||||
|
||||
const joinStatusOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '待审核', value: 'pending' },
|
||||
@@ -61,6 +90,16 @@ const reviewAction = ref('approve');
|
||||
const reviewReason = ref('');
|
||||
const reviewTarget = ref(null);
|
||||
|
||||
const applicationReviewDialogVisible = ref(false);
|
||||
const applicationReviewSubmitting = ref(false);
|
||||
const applicationReviewAction = ref('approve');
|
||||
const applicationReviewReason = ref('');
|
||||
const applicationReviewTarget = ref(null);
|
||||
|
||||
const payoutRemoveDialogVisible = ref(false);
|
||||
const payoutRemoveSubmitting = ref(false);
|
||||
const payoutRemoveTarget = ref(null);
|
||||
|
||||
const inviteDialogVisible = ref(false);
|
||||
const inviteSubmitting = ref(false);
|
||||
const inviteTenantID = ref(null);
|
||||
@@ -166,6 +205,53 @@ async function loadJoinRequests() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCreatorApplications() {
|
||||
applicationsLoading.value = true;
|
||||
try {
|
||||
const result = await CreatorService.listCreatorApplications({
|
||||
page: applicationsPage.value,
|
||||
limit: applicationsRows.value,
|
||||
id: applicationTenantID.value || undefined,
|
||||
user_id: applicationOwnerUserID.value || undefined,
|
||||
name: applicationName.value,
|
||||
code: applicationCode.value,
|
||||
status: applicationStatus.value || undefined,
|
||||
created_at_from: applicationCreatedAtFrom.value || undefined,
|
||||
created_at_to: applicationCreatedAtTo.value || undefined
|
||||
});
|
||||
applications.value = result.items;
|
||||
applicationsTotal.value = result.total;
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载创作者申请', life: 4000 });
|
||||
} finally {
|
||||
applicationsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPayoutAccounts() {
|
||||
payoutAccountsLoading.value = true;
|
||||
try {
|
||||
const result = await CreatorService.listPayoutAccounts({
|
||||
page: payoutAccountsPage.value,
|
||||
limit: payoutAccountsRows.value,
|
||||
tenant_id: payoutTenantID.value || undefined,
|
||||
tenant_code: payoutTenantCode.value,
|
||||
tenant_name: payoutTenantName.value,
|
||||
user_id: payoutUserID.value || undefined,
|
||||
username: payoutUsername.value,
|
||||
type: payoutType.value || undefined,
|
||||
created_at_from: payoutCreatedAtFrom.value || undefined,
|
||||
created_at_to: payoutCreatedAtTo.value || undefined
|
||||
});
|
||||
payoutAccounts.value = result.items;
|
||||
payoutAccountsTotal.value = result.total;
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载结算账户', life: 4000 });
|
||||
} finally {
|
||||
payoutAccountsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
page.value = 1;
|
||||
loadCreators();
|
||||
@@ -223,6 +309,55 @@ function onJoinPage(event) {
|
||||
loadJoinRequests();
|
||||
}
|
||||
|
||||
function onApplicationSearch() {
|
||||
applicationsPage.value = 1;
|
||||
loadCreatorApplications();
|
||||
}
|
||||
|
||||
function onApplicationReset() {
|
||||
applicationTenantID.value = null;
|
||||
applicationOwnerUserID.value = null;
|
||||
applicationName.value = '';
|
||||
applicationCode.value = '';
|
||||
applicationStatus.value = 'pending_verify';
|
||||
applicationCreatedAtFrom.value = null;
|
||||
applicationCreatedAtTo.value = null;
|
||||
applicationsPage.value = 1;
|
||||
applicationsRows.value = 10;
|
||||
loadCreatorApplications();
|
||||
}
|
||||
|
||||
function onApplicationPage(event) {
|
||||
applicationsPage.value = (event.page ?? 0) + 1;
|
||||
applicationsRows.value = event.rows ?? applicationsRows.value;
|
||||
loadCreatorApplications();
|
||||
}
|
||||
|
||||
function onPayoutSearch() {
|
||||
payoutAccountsPage.value = 1;
|
||||
loadPayoutAccounts();
|
||||
}
|
||||
|
||||
function onPayoutReset() {
|
||||
payoutTenantID.value = null;
|
||||
payoutTenantCode.value = '';
|
||||
payoutTenantName.value = '';
|
||||
payoutUserID.value = null;
|
||||
payoutUsername.value = '';
|
||||
payoutType.value = '';
|
||||
payoutCreatedAtFrom.value = null;
|
||||
payoutCreatedAtTo.value = null;
|
||||
payoutAccountsPage.value = 1;
|
||||
payoutAccountsRows.value = 10;
|
||||
loadPayoutAccounts();
|
||||
}
|
||||
|
||||
function onPayoutPage(event) {
|
||||
payoutAccountsPage.value = (event.page ?? 0) + 1;
|
||||
payoutAccountsRows.value = event.rows ?? payoutAccountsRows.value;
|
||||
loadPayoutAccounts();
|
||||
}
|
||||
|
||||
async function openStatusDialog(tenant) {
|
||||
statusTenant.value = tenant;
|
||||
statusValue.value = tenant?.status ?? null;
|
||||
@@ -280,6 +415,57 @@ async function confirmReview() {
|
||||
}
|
||||
}
|
||||
|
||||
function openApplicationReviewDialog(row, action) {
|
||||
applicationReviewTarget.value = row;
|
||||
applicationReviewAction.value = action || 'approve';
|
||||
applicationReviewReason.value = '';
|
||||
applicationReviewDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function confirmApplicationReview() {
|
||||
const targetID = applicationReviewTarget.value?.id;
|
||||
if (!targetID) return;
|
||||
const reason = applicationReviewReason.value.trim();
|
||||
if (applicationReviewAction.value === 'reject' && !reason) {
|
||||
toast.add({ severity: 'warn', summary: '请输入原因', detail: '驳回时需填写原因', life: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
applicationReviewSubmitting.value = true;
|
||||
try {
|
||||
await CreatorService.reviewCreatorApplication(targetID, { action: applicationReviewAction.value, reason });
|
||||
toast.add({ severity: 'success', summary: '审核完成', detail: `TenantID: ${targetID}`, life: 3000 });
|
||||
applicationReviewDialogVisible.value = false;
|
||||
await loadCreatorApplications();
|
||||
await loadCreators();
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '审核失败', detail: error?.message || '无法审核申请', life: 4000 });
|
||||
} finally {
|
||||
applicationReviewSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openPayoutRemoveDialog(row) {
|
||||
payoutRemoveTarget.value = row;
|
||||
payoutRemoveDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function confirmRemovePayoutAccount() {
|
||||
const targetID = payoutRemoveTarget.value?.id;
|
||||
if (!targetID) return;
|
||||
payoutRemoveSubmitting.value = true;
|
||||
try {
|
||||
await CreatorService.removePayoutAccount(targetID);
|
||||
toast.add({ severity: 'success', summary: '已删除', detail: `账户ID: ${targetID}`, life: 3000 });
|
||||
payoutRemoveDialogVisible.value = false;
|
||||
await loadPayoutAccounts();
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '删除失败', detail: error?.message || '无法删除结算账户', life: 4000 });
|
||||
} finally {
|
||||
payoutRemoveSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openInviteDialog(row) {
|
||||
inviteTenantID.value = Number(row?.tenant_id ?? row?.tenant?.id ?? 0) || null;
|
||||
inviteMaxUses.value = 1;
|
||||
@@ -315,6 +501,8 @@ onMounted(() => {
|
||||
loadCreators();
|
||||
ensureStatusOptionsLoaded().catch(() => {});
|
||||
loadJoinRequests();
|
||||
loadCreatorApplications();
|
||||
loadPayoutAccounts();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -323,7 +511,9 @@ onMounted(() => {
|
||||
<Tabs v-model:value="tabValue" value="creators">
|
||||
<TabList>
|
||||
<Tab value="creators">创作者列表</Tab>
|
||||
<Tab value="applications">申请审核</Tab>
|
||||
<Tab value="members">成员审核</Tab>
|
||||
<Tab value="payouts">结算账户</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel value="creators">
|
||||
@@ -412,6 +602,96 @@ onMounted(() => {
|
||||
</Column>
|
||||
</DataTable>
|
||||
</TabPanel>
|
||||
<TabPanel value="applications">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex flex-col">
|
||||
<h4 class="m-0">创作者申请</h4>
|
||||
<span class="text-muted-color">审核待开通创作者</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchPanel :loading="applicationsLoading" @search="onApplicationSearch" @reset="onApplicationReset">
|
||||
<SearchField label="TenantID">
|
||||
<InputNumber v-model="applicationTenantID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="Owner UserID">
|
||||
<InputNumber v-model="applicationOwnerUserID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="名称">
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="applicationName" placeholder="模糊匹配" class="w-full" @keyup.enter="onApplicationSearch" />
|
||||
</IconField>
|
||||
</SearchField>
|
||||
<SearchField label="Code">
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="applicationCode" placeholder="模糊匹配" class="w-full" @keyup.enter="onApplicationSearch" />
|
||||
</IconField>
|
||||
</SearchField>
|
||||
<SearchField label="状态">
|
||||
<Select v-model="applicationStatus" :options="applicationStatusOptions" optionLabel="label" optionValue="value" placeholder="请选择" :loading="statusOptionsLoading" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="申请时间 From">
|
||||
<DatePicker v-model="applicationCreatedAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="申请时间 To">
|
||||
<DatePicker v-model="applicationCreatedAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||
</SearchField>
|
||||
</SearchPanel>
|
||||
|
||||
<DataTable
|
||||
:value="applications"
|
||||
dataKey="id"
|
||||
:loading="applicationsLoading"
|
||||
lazy
|
||||
:paginator="true"
|
||||
:rows="applicationsRows"
|
||||
:totalRecords="applicationsTotal"
|
||||
:first="(applicationsPage - 1) * applicationsRows"
|
||||
:rowsPerPageOptions="[10, 20, 50, 100]"
|
||||
@page="onApplicationPage"
|
||||
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
scrollable
|
||||
scrollHeight="flex"
|
||||
responsiveLayout="scroll"
|
||||
>
|
||||
<Column field="id" header="ID" style="min-width: 6rem" />
|
||||
<Column field="code" header="Code" style="min-width: 10rem" />
|
||||
<Column field="name" header="名称" style="min-width: 16rem">
|
||||
<template #body="{ data }">
|
||||
<Button :label="data.name || '-'" icon="pi pi-external-link" text size="small" class="p-0" as="router-link" :to="`/superadmin/tenants/${data.id}`" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="Owner" style="min-width: 12rem">
|
||||
<template #body="{ data }">
|
||||
<span>{{ data?.owner?.username ?? '-' }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="status" header="状态" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="data.status_description || data.status || '-'" :severity="getStatusSeverity(data.status)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="created_at" header="申请时间" style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatDate(data.created_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="操作" style="min-width: 12rem">
|
||||
<template #body="{ data }">
|
||||
<Button v-if="data.status === 'pending_verify'" label="通过" icon="pi pi-check" text size="small" class="p-0 mr-3" @click="openApplicationReviewDialog(data, 'approve')" />
|
||||
<Button v-if="data.status === 'pending_verify'" label="驳回" icon="pi pi-times" severity="danger" text size="small" class="p-0" @click="openApplicationReviewDialog(data, 'reject')" />
|
||||
<span v-if="data.status !== 'pending_verify'" class="text-muted-color">已处理</span>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</TabPanel>
|
||||
<TabPanel value="members">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex flex-col">
|
||||
@@ -514,6 +794,97 @@ onMounted(() => {
|
||||
</Column>
|
||||
</DataTable>
|
||||
</TabPanel>
|
||||
<TabPanel value="payouts">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex flex-col">
|
||||
<h4 class="m-0">结算账户</h4>
|
||||
<span class="text-muted-color">跨租户结算账户审查</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchPanel :loading="payoutAccountsLoading" @search="onPayoutSearch" @reset="onPayoutReset">
|
||||
<SearchField label="TenantID">
|
||||
<InputNumber v-model="payoutTenantID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="TenantCode">
|
||||
<InputText v-model="payoutTenantCode" placeholder="模糊匹配" class="w-full" @keyup.enter="onPayoutSearch" />
|
||||
</SearchField>
|
||||
<SearchField label="TenantName">
|
||||
<InputText v-model="payoutTenantName" placeholder="模糊匹配" class="w-full" @keyup.enter="onPayoutSearch" />
|
||||
</SearchField>
|
||||
<SearchField label="UserID">
|
||||
<InputNumber v-model="payoutUserID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="Username">
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="payoutUsername" placeholder="模糊匹配" class="w-full" @keyup.enter="onPayoutSearch" />
|
||||
</IconField>
|
||||
</SearchField>
|
||||
<SearchField label="类型">
|
||||
<InputText v-model="payoutType" placeholder="bank/alipay" class="w-full" @keyup.enter="onPayoutSearch" />
|
||||
</SearchField>
|
||||
<SearchField label="创建时间 From">
|
||||
<DatePicker v-model="payoutCreatedAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||
</SearchField>
|
||||
<SearchField label="创建时间 To">
|
||||
<DatePicker v-model="payoutCreatedAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||
</SearchField>
|
||||
</SearchPanel>
|
||||
|
||||
<DataTable
|
||||
:value="payoutAccounts"
|
||||
dataKey="id"
|
||||
:loading="payoutAccountsLoading"
|
||||
lazy
|
||||
:paginator="true"
|
||||
:rows="payoutAccountsRows"
|
||||
:totalRecords="payoutAccountsTotal"
|
||||
:first="(payoutAccountsPage - 1) * payoutAccountsRows"
|
||||
:rowsPerPageOptions="[10, 20, 50, 100]"
|
||||
@page="onPayoutPage"
|
||||
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
scrollable
|
||||
scrollHeight="420px"
|
||||
responsiveLayout="scroll"
|
||||
>
|
||||
<Column field="id" header="ID" style="min-width: 6rem" />
|
||||
<Column header="租户" style="min-width: 16rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ data.tenant_name || '-' }}</span>
|
||||
<span class="text-xs text-muted-color">Code: {{ data.tenant_code || '-' }} / ID: {{ data.tenant_id ?? '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="用户" style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
<router-link v-if="data.user_id" class="inline-flex items-center gap-1 font-medium text-primary hover:underline" :to="`/superadmin/users/${data.user_id}`">
|
||||
<span class="truncate max-w-[200px]">{{ data.username || `ID:${data.user_id}` }}</span>
|
||||
<i class="pi pi-external-link text-xs" />
|
||||
</router-link>
|
||||
<div class="text-xs text-muted-color">ID: {{ data.user_id ?? '-' }}</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="type" header="类型" style="min-width: 10rem" />
|
||||
<Column field="name" header="账户名称" style="min-width: 14rem" />
|
||||
<Column field="account" header="账号" style="min-width: 14rem" />
|
||||
<Column field="realname" header="收款人" style="min-width: 12rem" />
|
||||
<Column field="created_at" header="创建时间" style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatDate(data.created_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="操作" style="min-width: 8rem">
|
||||
<template #body="{ data }">
|
||||
<Button label="删除" icon="pi pi-trash" severity="danger" text size="small" class="p-0" @click="openPayoutRemoveDialog(data)" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
@@ -570,6 +941,66 @@ onMounted(() => {
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="applicationReviewDialogVisible" :modal="true" :style="{ width: '520px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">创作者申请审核</span>
|
||||
<span class="text-muted-color">TenantID: {{ applicationReviewTarget?.id ?? '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-sm text-muted-color">审核创作者申请,请确认处理动作与备注。</div>
|
||||
<div>
|
||||
<label class="block font-medium mb-2">审核动作</label>
|
||||
<Select
|
||||
v-model="applicationReviewAction"
|
||||
:options="[
|
||||
{ label: '通过', value: 'approve' },
|
||||
{ label: '驳回', value: 'reject' }
|
||||
]"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-medium mb-2">审核说明</label>
|
||||
<InputText v-model="applicationReviewReason" placeholder="驳回时建议填写原因" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="取消" icon="pi pi-times" text @click="applicationReviewDialogVisible = false" :disabled="applicationReviewSubmitting" />
|
||||
<Button
|
||||
label="确认审核"
|
||||
icon="pi pi-check"
|
||||
severity="success"
|
||||
@click="confirmApplicationReview"
|
||||
:loading="applicationReviewSubmitting"
|
||||
:disabled="applicationReviewSubmitting || (applicationReviewAction === 'reject' && !applicationReviewReason.trim())"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="payoutRemoveDialogVisible" :modal="true" :style="{ width: '420px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">删除结算账户</span>
|
||||
<span class="text-muted-color">账户ID: {{ payoutRemoveTarget?.id ?? '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="text-sm text-muted-color">该操作将移除结算账户信息,请确认。</div>
|
||||
<div class="text-sm">
|
||||
<span class="font-medium">{{ payoutRemoveTarget?.name || '-' }}</span>
|
||||
<span class="text-muted-color">({{ payoutRemoveTarget?.account || '-' }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="取消" icon="pi pi-times" text @click="payoutRemoveDialogVisible = false" :disabled="payoutRemoveSubmitting" />
|
||||
<Button label="确认删除" icon="pi pi-trash" severity="danger" @click="confirmRemovePayoutAccount" :loading="payoutRemoveSubmitting" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="inviteDialogVisible" :modal="true" :style="{ width: '520px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user