feat: add renew
This commit is contained in:
4
frontend/superadmin/dist/index.html
vendored
4
frontend/superadmin/dist/index.html
vendored
@@ -7,8 +7,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Sakai Vue</title>
|
<title>Sakai Vue</title>
|
||||||
<link href="https://fonts.cdnfonts.com/css/lato" rel="stylesheet">
|
<link href="https://fonts.cdnfonts.com/css/lato" rel="stylesheet">
|
||||||
<script type="module" crossorigin src="./assets/index-CDRO-wVH.js"></script>
|
<script type="module" crossorigin src="./assets/index-5TB6SaKe.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-BMyA_4RT.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-7wg5D3fl.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Aura from '@primeuix/themes/aura';
|
|||||||
import PrimeVue from 'primevue/config';
|
import PrimeVue from 'primevue/config';
|
||||||
import ConfirmationService from 'primevue/confirmationservice';
|
import ConfirmationService from 'primevue/confirmationservice';
|
||||||
import ToastService from 'primevue/toastservice';
|
import ToastService from 'primevue/toastservice';
|
||||||
|
import Tooltip from 'primevue/tooltip';
|
||||||
|
|
||||||
import '@/assets/tailwind.css';
|
import '@/assets/tailwind.css';
|
||||||
import '@/assets/styles.scss';
|
import '@/assets/styles.scss';
|
||||||
@@ -23,5 +24,6 @@ app.use(PrimeVue, {
|
|||||||
});
|
});
|
||||||
app.use(ToastService);
|
app.use(ToastService);
|
||||||
app.use(ConfirmationService);
|
app.use(ConfirmationService);
|
||||||
|
app.directive('tooltip', Tooltip);
|
||||||
|
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ function normalizeItems(items) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TenantService = {
|
export const TenantService = {
|
||||||
async listTenants({ page, limit, name, sortField, sortOrder } = {}) {
|
async listTenants({ page, limit, name, code, sortField, sortOrder } = {}) {
|
||||||
const query = { page, limit, name };
|
const query = { page, limit, name, code };
|
||||||
if (sortField && sortOrder) {
|
if (sortField && sortOrder) {
|
||||||
if (sortOrder === 1) query.asc = sortField;
|
if (sortOrder === 1) query.asc = sortField;
|
||||||
if (sortOrder === -1) query.desc = sortField;
|
if (sortOrder === -1) query.desc = sortField;
|
||||||
@@ -21,5 +21,11 @@ export const TenantService = {
|
|||||||
total: data?.total ?? 0,
|
total: data?.total ?? 0,
|
||||||
items: normalizeItems(data?.items)
|
items: normalizeItems(data?.items)
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
async renewTenantExpire({ tenantID, duration }) {
|
||||||
|
return requestJson(`/super/v1/tenants/${tenantID}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: { duration }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { TenantService } from '@/service/TenantService';
|
import { TenantService } from '@/service/TenantService';
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ const totalRecords = ref(0);
|
|||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const rows = ref(10);
|
const rows = ref(10);
|
||||||
|
|
||||||
const name = ref('');
|
const keyword = ref('');
|
||||||
const sortField = ref('id');
|
const sortField = ref('id');
|
||||||
const sortOrder = ref(-1);
|
const sortOrder = ref(-1);
|
||||||
|
|
||||||
@@ -36,13 +36,33 @@ function getStatusSeverity(status) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getExpiryDaysInfo(expiredAt) {
|
||||||
|
if (!expiredAt) return { daysLeft: null, tooltipText: '未设置过期时间', textClass: '' };
|
||||||
|
const expiredDate = new Date(expiredAt);
|
||||||
|
if (Number.isNaN(expiredDate.getTime())) return { daysLeft: null, tooltipText: String(expiredAt), textClass: '' };
|
||||||
|
|
||||||
|
const msPerDay = 24 * 60 * 60 * 1000;
|
||||||
|
const deltaDays = Math.ceil((expiredDate.getTime() - Date.now()) / msPerDay);
|
||||||
|
const isExpired = deltaDays < 0;
|
||||||
|
const daysAbs = Math.abs(deltaDays);
|
||||||
|
const tooltipText = isExpired ? `已过期 ${daysAbs} 天` : `剩余 ${deltaDays} 天`;
|
||||||
|
|
||||||
|
let textClass = '';
|
||||||
|
if (isExpired) textClass = 'text-red-500';
|
||||||
|
else if (deltaDays < 10) textClass = 'text-red-500';
|
||||||
|
else if (deltaDays < 30) textClass = 'text-orange-500';
|
||||||
|
|
||||||
|
return { daysLeft: deltaDays, tooltipText, textClass };
|
||||||
|
}
|
||||||
|
|
||||||
async function loadTenants() {
|
async function loadTenants() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await TenantService.listTenants({
|
const result = await TenantService.listTenants({
|
||||||
page: page.value,
|
page: page.value,
|
||||||
limit: rows.value,
|
limit: rows.value,
|
||||||
name: name.value,
|
name: keyword.value,
|
||||||
|
code: keyword.value,
|
||||||
sortField: sortField.value,
|
sortField: sortField.value,
|
||||||
sortOrder: sortOrder.value
|
sortOrder: sortOrder.value
|
||||||
});
|
});
|
||||||
@@ -66,7 +86,7 @@ function onSearch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onReset() {
|
function onReset() {
|
||||||
name.value = '';
|
keyword.value = '';
|
||||||
sortField.value = 'id';
|
sortField.value = 'id';
|
||||||
sortOrder.value = -1;
|
sortOrder.value = -1;
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
@@ -86,6 +106,41 @@ function onSort(event) {
|
|||||||
loadTenants();
|
loadTenants();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renewDialogVisible = ref(false);
|
||||||
|
const renewing = ref(false);
|
||||||
|
const renewTenant = ref(null);
|
||||||
|
const renewDuration = ref(30);
|
||||||
|
const durationOptions = [
|
||||||
|
{ label: '7 天', value: 7 },
|
||||||
|
{ label: '30 天', value: 30 },
|
||||||
|
{ label: '90 天', value: 90 },
|
||||||
|
{ label: '180 天', value: 180 },
|
||||||
|
{ label: '365 天', value: 365 }
|
||||||
|
];
|
||||||
|
|
||||||
|
function openRenewDialog(item) {
|
||||||
|
renewTenant.value = item;
|
||||||
|
renewDuration.value = 30;
|
||||||
|
renewDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmRenew() {
|
||||||
|
const tenantID = renewTenant.value?.id;
|
||||||
|
if (!tenantID) return;
|
||||||
|
|
||||||
|
renewing.value = true;
|
||||||
|
try {
|
||||||
|
await TenantService.renewTenantExpire({ tenantID, duration: renewDuration.value });
|
||||||
|
toast.add({ severity: 'success', summary: '续期成功', detail: `TenantID: ${tenantID}`, life: 3000 });
|
||||||
|
renewDialogVisible.value = false;
|
||||||
|
await loadTenants();
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '续期失败', detail: error?.message || '无法续期', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
renewing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadTenants();
|
loadTenants();
|
||||||
});
|
});
|
||||||
@@ -104,61 +159,74 @@ onMounted(() => {
|
|||||||
<InputIcon>
|
<InputIcon>
|
||||||
<i class="pi pi-search" />
|
<i class="pi pi-search" />
|
||||||
</InputIcon>
|
</InputIcon>
|
||||||
<InputText v-model="name" placeholder="按名称搜索" @keyup.enter="onSearch" />
|
<InputText v-model="keyword" placeholder="名称 / Code" @keyup.enter="onSearch" />
|
||||||
</IconField>
|
</IconField>
|
||||||
<Button label="查询" icon="pi pi-search" severity="secondary" @click="onSearch" />
|
<Button label="查询" icon="pi pi-search" severity="secondary" @click="onSearch" />
|
||||||
<Button label="重置" icon="pi pi-refresh" severity="secondary" @click="onReset" />
|
<Button label="重置" icon="pi pi-refresh" severity="secondary" @click="onReset" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable :value="tenants" dataKey="id" :loading="loading" lazy :paginator="true" :rows="rows"
|
||||||
:value="tenants"
|
:totalRecords="totalRecords" :first="(page - 1) * rows" :rowsPerPageOptions="[10, 20, 50, 100]"
|
||||||
dataKey="id"
|
sortMode="single" :sortField="sortField" :sortOrder="sortOrder" @page="onPage" @sort="onSort"
|
||||||
: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} 条"
|
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
|
||||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||||
scrollable
|
scrollable scrollHeight="flex" responsiveLayout="scroll">
|
||||||
scrollHeight="flex"
|
|
||||||
responsiveLayout="scroll"
|
|
||||||
>
|
|
||||||
<Column field="id" header="ID" sortable style="min-width: 6rem" />
|
<Column field="id" header="ID" sortable style="min-width: 6rem" />
|
||||||
<Column field="uuid" header="UUID" style="min-width: 14rem" />
|
|
||||||
<Column field="code" header="Code" style="min-width: 10rem" />
|
<Column field="code" header="Code" style="min-width: 10rem" />
|
||||||
<Column field="name" header="名称" sortable style="min-width: 14rem" />
|
<Column field="name" header="名称" sortable style="min-width: 14rem" />
|
||||||
<Column field="status" header="状态" sortable style="min-width: 10rem">
|
<Column field="status_description" header="状态" style="min-width: 10rem">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<Tag :value="data.status || '-'" :severity="getStatusSeverity(data.status)" />
|
<Tag :value="data.status_description || '-'" :severity="getStatusSeverity(data.status)" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="user_count" header="用户数" sortable style="min-width: 8rem" />
|
||||||
|
<Column field="user_balance" header="余额" sortable style="min-width: 8rem" />
|
||||||
|
<Column field="expired_at" header="过期时间" sortable style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span v-if="data.expired_at" v-tooltip="getExpiryDaysInfo(data.expired_at).tooltipText"
|
||||||
|
:class="getExpiryDaysInfo(data.expired_at).textClass">
|
||||||
|
{{ formatDate(data.expired_at) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column field="userCount" header="用户数" sortable style="min-width: 8rem" />
|
|
||||||
<Column field="userBalance" header="余额" sortable style="min-width: 8rem" />
|
|
||||||
<Column field="created_at" header="创建时间" sortable style="min-width: 14rem">
|
<Column field="created_at" header="创建时间" sortable style="min-width: 14rem">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ formatDate(data.created_at) }}
|
{{ formatDate(data.created_at) }}
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column field="expired_at" header="过期时间" sortable style="min-width: 14rem">
|
|
||||||
<template #body="{ data }">
|
|
||||||
{{ formatDate(data.expired_at) }}
|
|
||||||
</template>
|
|
||||||
</Column>
|
|
||||||
<Column field="updated_at" header="更新时间" sortable style="min-width: 14rem">
|
<Column field="updated_at" header="更新时间" sortable style="min-width: 14rem">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ formatDate(data.updated_at) }}
|
{{ formatDate(data.updated_at) }}
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
<Column header="操作" :exportable="false" style="min-width: 10rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Button label="续期" icon="pi pi-refresh" size="small" severity="secondary"
|
||||||
|
@click="openRenewDialog(data)" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Dialog v-model:visible="renewDialogVisible" :modal="true" :style="{ width: '420px' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium">为【 {{ renewTenant?.name ?? '-' }} 】续期</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">续期时长</label>
|
||||||
|
<Select v-model="renewDuration" :options="durationOptions" optionLabel="label" optionValue="value"
|
||||||
|
placeholder="选择续期时长" fluid />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="取消" icon="pi pi-times" text @click="renewDialogVisible = false" :disabled="renewing" />
|
||||||
|
<Button label="确认续期" icon="pi pi-check" @click="confirmRenew" :loading="renewing" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user