2025-12-12 17:34:11 +08:00
|
|
|
|
<template>
|
2026-04-28 14:25:17 +08:00
|
|
|
|
<div class="subscription-plan">
|
|
|
|
|
|
<a-card class="search-card" :bordered="false">
|
|
|
|
|
|
<a-form
|
|
|
|
|
|
class="search-form"
|
|
|
|
|
|
layout="inline"
|
|
|
|
|
|
:model="searchForm"
|
|
|
|
|
|
:label-col="{ style: { width: '14rem' } }"
|
|
|
|
|
|
:wrapper-col="{ style: { width: '20rem' } }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-form-item label="ID">
|
|
|
|
|
|
<a-input v-model:value="searchForm.id" allow-clear placeholder="Input the id" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<!-- <a-form-item label="Name">
|
2025-12-12 17:34:11 +08:00
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="searchForm.name"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
placeholder="Input the name"
|
|
|
|
|
|
/>
|
2026-01-07 16:17:43 +08:00
|
|
|
|
</a-form-item> -->
|
2026-04-28 14:25:17 +08:00
|
|
|
|
<a-form-item label="Time Range">
|
|
|
|
|
|
<a-range-picker
|
|
|
|
|
|
v-model:value="searchForm.dateRange"
|
|
|
|
|
|
value-format="X"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="Organization">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="searchForm.organizationId"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
placeholder="Select Organization"
|
|
|
|
|
|
style="width: 180px"
|
|
|
|
|
|
@popupScroll="handleOrganizationScroll"
|
|
|
|
|
|
@select="handleOrganizationSelect"
|
|
|
|
|
|
@change="handleOrganizationChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-select-option value="ADD_ORGANIZATION" class="add-organization-option">
|
|
|
|
|
|
+ Create Organization
|
|
|
|
|
|
</a-select-option>
|
|
|
|
|
|
<a-select-option
|
|
|
|
|
|
v-for="item in organizationOptions"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
:value="item.id"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item.name }}
|
|
|
|
|
|
</a-select-option>
|
|
|
|
|
|
</a-select>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="Admin Account">
|
|
|
|
|
|
<SelectUser v-model="searchForm.adminAccId" labelKey="email" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="Status">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="searchForm.status"
|
|
|
|
|
|
mode="multiple"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
placeholder="Select Status"
|
|
|
|
|
|
style="width: 220px"
|
|
|
|
|
|
:options="statusOption"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="Country or Region">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="searchForm.countryOrRegion"
|
|
|
|
|
|
:allowClear="true"
|
|
|
|
|
|
show-search
|
|
|
|
|
|
style="width: 250px"
|
|
|
|
|
|
:filter-option="filterOption"
|
|
|
|
|
|
placeholder="Select the country or region"
|
|
|
|
|
|
max-tag-count="responsive"
|
|
|
|
|
|
:options="countryList"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item class="search-form__actions">
|
|
|
|
|
|
<a-space>
|
|
|
|
|
|
<a-button type="primary" @click="handleSearch">Search</a-button>
|
|
|
|
|
|
<a-button @click="handleReset">Reset</a-button>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
|
|
|
|
|
|
<a-card class="table-card" :bordered="false">
|
|
|
|
|
|
<div class="table-card__header">
|
|
|
|
|
|
<a-button type="primary" @click="openCreate">New Subscription Plan</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div ref="historyTable" class="table-wrapper">
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
:data-source="tableData"
|
|
|
|
|
|
:columns="columns"
|
|
|
|
|
|
:loading="tableLoading"
|
|
|
|
|
|
:bordered="false"
|
|
|
|
|
|
row-key="id"
|
|
|
|
|
|
:customRow="customPlanRow"
|
|
|
|
|
|
@change="changePage"
|
|
|
|
|
|
@resizeColumn="handleResizeColumn"
|
|
|
|
|
|
:scroll="{ y: historyTableHeight }"
|
|
|
|
|
|
:pagination="{
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
current: searchForm.page,
|
|
|
|
|
|
pageSize: searchForm.size,
|
|
|
|
|
|
total: searchForm.total,
|
|
|
|
|
|
showQuickJumper: true,
|
|
|
|
|
|
bordered: false
|
|
|
|
|
|
}"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
|
<template
|
|
|
|
|
|
v-if="
|
|
|
|
|
|
column.key === 'currentPeriodStart' ||
|
|
|
|
|
|
column.key === 'currentPeriodEnd'
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ formatTime(record[column.key], "YYYY-MM-DD hh:mm:ss") }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-if="column.key === 'status'">
|
|
|
|
|
|
<a-tag :color="getStatusColor(record.status)">
|
|
|
|
|
|
{{ record.status }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else-if="column.key === 'actions'">
|
|
|
|
|
|
<a-space class="plan-row-actions" @click.stop>
|
|
|
|
|
|
<a @click.stop="openEdit(record)">Edit</a>
|
|
|
|
|
|
<a-popconfirm
|
|
|
|
|
|
title="Confirm to delete this subscription plan?"
|
|
|
|
|
|
ok-text="Confirm"
|
|
|
|
|
|
cancel-text="Cancel"
|
|
|
|
|
|
@confirm="removePlan(record.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a class="danger-text" @click.stop>Delete</a>
|
|
|
|
|
|
</a-popconfirm>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
v-model:visible="userInfoModalVisible"
|
|
|
|
|
|
title="User Info"
|
|
|
|
|
|
width="80%"
|
|
|
|
|
|
:footer="null"
|
|
|
|
|
|
:destroy-on-close="true"
|
|
|
|
|
|
class="user-info-modal"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
:columns="userInfoColumns"
|
|
|
|
|
|
:data-source="userInfoList"
|
|
|
|
|
|
:loading="userInfoLoading"
|
|
|
|
|
|
:pagination="{
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
current: userInfoPage.current,
|
|
|
|
|
|
pageSize: userInfoPage.size,
|
|
|
|
|
|
total: userInfoPage.total,
|
|
|
|
|
|
showQuickJumper: true,
|
|
|
|
|
|
bordered: false
|
|
|
|
|
|
}"
|
|
|
|
|
|
row-key="id"
|
|
|
|
|
|
:scroll="{ x: 1280, y: 420 }"
|
|
|
|
|
|
@change="changeUserInfoPage"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
|
<template
|
|
|
|
|
|
v-if="
|
|
|
|
|
|
column.key === 'validStartTime' ||
|
|
|
|
|
|
column.key === 'validEndTime' ||
|
|
|
|
|
|
column.key === 'createDate'
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ formatUserTime(record[column.key]) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'systemUser'">
|
|
|
|
|
|
{{ getSystemUserLabel(record.systemUser) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'isBeginner'">
|
|
|
|
|
|
{{ record.isBeginner == 1 ? "Yes" : "No" }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="subscriptionPlanModal" ref="subscriptionPlanModal"></div>
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
class="subscriptionPlan_modal generalModel"
|
|
|
|
|
|
v-model:visible="modalVisible"
|
|
|
|
|
|
:footer="null"
|
|
|
|
|
|
:get-container="() => $refs.subscriptionPlanModal"
|
|
|
|
|
|
width="50%"
|
|
|
|
|
|
:maskClosable="false"
|
|
|
|
|
|
:centered="true"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
:mask="true"
|
|
|
|
|
|
wrapClassName="#app"
|
|
|
|
|
|
:keyboard="false"
|
|
|
|
|
|
destroy-on-close
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="generalModel_btn">
|
|
|
|
|
|
<div class="generalModel_closeIcon" @click.stop="cancelModal">
|
|
|
|
|
|
<svg
|
|
|
|
|
|
width="100%"
|
|
|
|
|
|
height="100%"
|
|
|
|
|
|
viewBox="0 0 46 46"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
|
>
|
|
|
|
|
|
<circle cx="23" cy="23" r="23" fill="#000" fill-opacity="0.3" />
|
|
|
|
|
|
<rect
|
|
|
|
|
|
x="32.5063"
|
|
|
|
|
|
y="12"
|
|
|
|
|
|
width="3"
|
|
|
|
|
|
height="29"
|
|
|
|
|
|
rx="1.5"
|
|
|
|
|
|
transform="rotate(45 32.5063 12)"
|
|
|
|
|
|
fill="white"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<rect
|
|
|
|
|
|
x="34.6274"
|
|
|
|
|
|
y="32.5059"
|
|
|
|
|
|
width="3"
|
|
|
|
|
|
height="29"
|
|
|
|
|
|
rx="1.5"
|
|
|
|
|
|
transform="rotate(135 34.6274 32.5059)"
|
|
|
|
|
|
fill="white"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal_title_text">
|
|
|
|
|
|
<div>{{ modalTitle }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="subscriptionPlan_center admin_page">
|
|
|
|
|
|
<div class="form_content">
|
|
|
|
|
|
<div class="admin_state_item" v-if="!isEditMode">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
Name:
|
|
|
|
|
|
<span>*</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="formState.name"
|
|
|
|
|
|
placeholder="Input the name"
|
|
|
|
|
|
style="width: 250px"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="admin_state_item" v-if="!isEditMode">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
Organization:
|
|
|
|
|
|
<span>*</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="formState.organizationId"
|
|
|
|
|
|
placeholder="Select the organization"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
style="width: 250px"
|
|
|
|
|
|
@popupScroll="handleOrganizationScroll"
|
|
|
|
|
|
@select="handleOrganizationSelect"
|
|
|
|
|
|
@change="handleOrganizationChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-select-option
|
|
|
|
|
|
value="ADD_ORGANIZATION"
|
|
|
|
|
|
class="add-organization-option"
|
|
|
|
|
|
>
|
|
|
|
|
|
+ Create Organization
|
|
|
|
|
|
</a-select-option>
|
|
|
|
|
|
<a-select-option
|
|
|
|
|
|
v-for="item in organizationOptions"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
:value="item.id"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item.name }}
|
|
|
|
|
|
</a-select-option>
|
|
|
|
|
|
</a-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="admin_state_item">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
Country Or Region:
|
|
|
|
|
|
<span>*</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="formState.countryOrRegion"
|
|
|
|
|
|
placeholder="Select the country or region"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
show-search
|
|
|
|
|
|
:filter-option="filterOption"
|
|
|
|
|
|
style="width: 250px"
|
|
|
|
|
|
:options="countryList"
|
|
|
|
|
|
></a-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="admin_state_item">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
Admin Account:
|
|
|
|
|
|
<span>*</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<SelectUser ref="userRef" v-model="formState.adminAccId" labelKey="email" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="admin_state_item">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
Start Time:
|
|
|
|
|
|
<span>*</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<a-date-picker
|
|
|
|
|
|
v-model:value="formState.currentPeriodStart"
|
|
|
|
|
|
value-format="X"
|
|
|
|
|
|
:disabled="isEditMode && formState.status !== 'PENDING'"
|
|
|
|
|
|
style="width: 250px"
|
|
|
|
|
|
:disabledDate="disabledDate"
|
|
|
|
|
|
:show-time="{ format: 'HH:mm' }"
|
|
|
|
|
|
class="range_picker"
|
|
|
|
|
|
placeholder="Select the start time"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #suffixIcon>
|
|
|
|
|
|
<span class="icon iconfont range_picker_icon icon-rili"></span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-date-picker>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="admin_state_item">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
End Time:
|
|
|
|
|
|
<span>*</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<a-date-picker
|
|
|
|
|
|
v-model:value="formState.currentPeriodEnd"
|
|
|
|
|
|
value-format="X"
|
|
|
|
|
|
:show-time="{ format: 'HH:mm' }"
|
|
|
|
|
|
:disabledDate="disableEndDate"
|
|
|
|
|
|
:disabledTime="disableEndTime"
|
|
|
|
|
|
:show-now="!isEditMode"
|
|
|
|
|
|
style="width: 250px"
|
|
|
|
|
|
class="range_picker"
|
|
|
|
|
|
placeholder="Select the end time"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #suffixIcon>
|
|
|
|
|
|
<span class="icon iconfont range_picker_icon icon-rili"></span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-date-picker>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="admin_state_item">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
Sub-Account Num:
|
|
|
|
|
|
<span>*</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
v-model:value="formState.accountNum"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
style="width: 250px"
|
|
|
|
|
|
placeholder="Input the account number"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="admin_state_item">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
Credit Limit:
|
|
|
|
|
|
<span>*</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
v-model:value="formState.creditLimit"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
style="width: 250px"
|
|
|
|
|
|
placeholder="Input the credit limit"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="admin_state_item" v-if="!isEditMode">
|
|
|
|
|
|
<span>Status:</span>
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="formState.status"
|
|
|
|
|
|
placeholder="Select status"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
style="width: 250px"
|
|
|
|
|
|
:options="statusOption"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="subscriptionPlan_btn admin_page">
|
|
|
|
|
|
<div class="admin_search_item" @click="cancelModal">Close</div>
|
|
|
|
|
|
<div class="admin_search_item" @click="handleSubmitDebounced">OK</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="organizationModal" ref="organizationModal"></div>
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
class="organization_modal"
|
|
|
|
|
|
v-model:visible="organizationModalVisible"
|
|
|
|
|
|
:footer="null"
|
|
|
|
|
|
:get-container="() => $refs.organizationModal"
|
|
|
|
|
|
:maskClosable="false"
|
|
|
|
|
|
:centered="true"
|
|
|
|
|
|
:mask="true"
|
|
|
|
|
|
wrapClassName="#app"
|
|
|
|
|
|
:keyboard="false"
|
|
|
|
|
|
destroy-on-close
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="modal_title_text">
|
|
|
|
|
|
<div>Create Organization</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="subscriptionPlan_center admin_page">
|
|
|
|
|
|
<div class="form_content">
|
|
|
|
|
|
<div class="admin_state_item">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
Name:
|
|
|
|
|
|
<span>*</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="organizationForm.name"
|
|
|
|
|
|
placeholder="Input the name"
|
|
|
|
|
|
style="width: 250px"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="admin_state_item">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
Type:
|
|
|
|
|
|
<span>*</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="organizationForm.type"
|
|
|
|
|
|
placeholder="Select type"
|
|
|
|
|
|
style="width: 250px"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-select-option value="Enterprise">Enterprise</a-select-option>
|
|
|
|
|
|
<a-select-option value="Education">Education</a-select-option>
|
|
|
|
|
|
</a-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="organization_footer">
|
|
|
|
|
|
<div class="footer_btn ant-btn ant-btn-primary" @click="cancelOrganizationModal">
|
|
|
|
|
|
Close
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="footer_btn ant-btn ant-btn-primary" @click="handleCreateOrganization">
|
|
|
|
|
|
OK
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
</div>
|
2025-12-12 17:34:11 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-12-18 13:29:30 +08:00
|
|
|
|
import {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
reactive,
|
|
|
|
|
|
ref,
|
|
|
|
|
|
onMounted,
|
|
|
|
|
|
onBeforeUnmount,
|
|
|
|
|
|
onActivated,
|
|
|
|
|
|
computed,
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
useTemplateRef
|
|
|
|
|
|
} from "vue"
|
|
|
|
|
|
import SelectUser from "@/component/common/SelectUser.vue"
|
|
|
|
|
|
import { message } from "ant-design-vue"
|
|
|
|
|
|
import { Https } from "@/tool/https"
|
|
|
|
|
|
import { formatTime } from "@/tool/util"
|
|
|
|
|
|
import store from "@/store"
|
|
|
|
|
|
import type { FormInstance, Rule } from "ant-design-vue/es/form"
|
|
|
|
|
|
import { debounce } from "lodash-es"
|
|
|
|
|
|
import dayjs, { Dayjs } from "dayjs"
|
|
|
|
|
|
|
|
|
|
|
|
type PlanStatus = "PENDING" | "ACTIVE" | "EXPIRED"
|
2025-12-12 17:34:11 +08:00
|
|
|
|
interface SubscriptionPlan {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
id: number
|
|
|
|
|
|
userId?: string | number
|
|
|
|
|
|
name: string
|
|
|
|
|
|
currentPeriodStart: string
|
|
|
|
|
|
currentPeriodEnd: string
|
|
|
|
|
|
organizationId: string
|
|
|
|
|
|
adminAccId: string
|
|
|
|
|
|
adminAccEmail?: string
|
|
|
|
|
|
status: PlanStatus
|
|
|
|
|
|
creditLimit: number
|
|
|
|
|
|
accountNum?: number
|
|
|
|
|
|
startStamp: number
|
|
|
|
|
|
endStamp: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface UserInfoRecord {
|
|
|
|
|
|
id: number
|
|
|
|
|
|
userEmail?: string
|
|
|
|
|
|
userName?: string
|
|
|
|
|
|
language?: string
|
|
|
|
|
|
validStartTime?: number | string
|
|
|
|
|
|
validEndTime?: number | string
|
|
|
|
|
|
country?: string
|
|
|
|
|
|
createDate?: number | string
|
|
|
|
|
|
isBeginner?: number
|
|
|
|
|
|
browserIdentifiers?: string
|
|
|
|
|
|
credits?: number
|
|
|
|
|
|
systemUser?: number | string
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-06 17:33:00 +08:00
|
|
|
|
const countryList = ref([])
|
2026-01-07 15:06:25 +08:00
|
|
|
|
const userRef = ref(null)
|
2026-01-06 17:33:00 +08:00
|
|
|
|
|
2025-12-12 17:34:11 +08:00
|
|
|
|
const searchForm = reactive({
|
2026-04-28 14:25:17 +08:00
|
|
|
|
name: "",
|
|
|
|
|
|
startTime: "",
|
|
|
|
|
|
endTime: "",
|
|
|
|
|
|
organizationId: undefined as string | undefined,
|
|
|
|
|
|
adminAccId: undefined as string | undefined,
|
|
|
|
|
|
status: [] as PlanStatus[] | [],
|
|
|
|
|
|
id: "",
|
|
|
|
|
|
countryOrRegion: null,
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
size: 10,
|
|
|
|
|
|
total: 0
|
2025-12-12 17:34:11 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const toSeconds = (dateStr: string) => Math.floor(new Date(dateStr).getTime() / 1000)
|
|
|
|
|
|
|
|
|
|
|
|
const tableData = ref<SubscriptionPlan[]>([])
|
|
|
|
|
|
const tableLoading = ref(false)
|
2026-04-28 14:25:17 +08:00
|
|
|
|
const userInfoModalVisible = ref(false)
|
|
|
|
|
|
const userInfoLoading = ref(false)
|
|
|
|
|
|
const userInfoList = ref<UserInfoRecord[]>([])
|
|
|
|
|
|
const currentUserInfoPlanId = ref<string | number | null>(null)
|
|
|
|
|
|
const userInfoPage = reactive({
|
|
|
|
|
|
current: 1,
|
|
|
|
|
|
size: 10,
|
|
|
|
|
|
total: 0
|
|
|
|
|
|
})
|
2025-12-12 17:34:11 +08:00
|
|
|
|
|
|
|
|
|
|
const modalVisible = ref(false)
|
|
|
|
|
|
const confirmLoading = ref(false)
|
2026-04-28 14:25:17 +08:00
|
|
|
|
const modalTitle = ref("New Subscription Plan")
|
2025-12-12 17:34:11 +08:00
|
|
|
|
const isEditMode = ref(false)
|
|
|
|
|
|
const formState = reactive({
|
2026-04-28 14:25:17 +08:00
|
|
|
|
name: "",
|
|
|
|
|
|
currentPeriodStart: "",
|
|
|
|
|
|
currentPeriodEnd: "",
|
|
|
|
|
|
organizationId: undefined as string | undefined,
|
|
|
|
|
|
adminAccId: undefined as string | undefined,
|
|
|
|
|
|
creditLimit: null as number | null,
|
|
|
|
|
|
accountNum: null as number | null,
|
|
|
|
|
|
status: undefined as PlanStatus | undefined,
|
|
|
|
|
|
countryOrRegion: null as string | null
|
2025-12-12 17:34:11 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-15 10:39:19 +08:00
|
|
|
|
const organizationModalVisible = ref(false)
|
|
|
|
|
|
const organizationForm = reactive({
|
2026-04-28 14:25:17 +08:00
|
|
|
|
name: "",
|
|
|
|
|
|
type: undefined as string | undefined
|
2025-12-15 10:39:19 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-12 17:34:11 +08:00
|
|
|
|
const statusLabelMap: Record<PlanStatus, string> = {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
PENDING: "Pending",
|
|
|
|
|
|
ACTIVE: "Active",
|
|
|
|
|
|
EXPIRED: "Expired"
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
const statusColorMap: Record<PlanStatus, string> = {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
PENDING: "blue",
|
|
|
|
|
|
ACTIVE: "green",
|
|
|
|
|
|
EXPIRED: "red"
|
2025-12-16 17:32:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const statusOption = ref([
|
2026-04-28 14:25:17 +08:00
|
|
|
|
{
|
|
|
|
|
|
label: "Pending",
|
|
|
|
|
|
value: "PENDING"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: "Active",
|
|
|
|
|
|
value: "ACTIVE"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: "Expired",
|
|
|
|
|
|
value: "EXPIRED"
|
|
|
|
|
|
}
|
2025-12-16 17:32:33 +08:00
|
|
|
|
])
|
|
|
|
|
|
|
2026-01-07 16:17:43 +08:00
|
|
|
|
const disabledDate = (current: Dayjs) => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
return current && current < dayjs().subtract(1, "days").endOf("day")
|
2026-01-07 16:17:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const disableEndDate = (current: Dayjs) => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
if (isEditMode.value) {
|
|
|
|
|
|
const specificTime = dayjs(formState.currentPeriodEnd)
|
|
|
|
|
|
return current && current < dayjs(formState.currentPeriodEnd * 1000).startOf("day")
|
|
|
|
|
|
}
|
|
|
|
|
|
return disabledDate(current)
|
2026-01-07 16:17:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
const range = (start: number, end: number) => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
const result = []
|
|
|
|
|
|
for (let i = start; i < end; i++) {
|
|
|
|
|
|
result.push(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
2026-01-07 16:17:43 +08:00
|
|
|
|
}
|
2026-04-28 14:25:17 +08:00
|
|
|
|
const disableEndTime = (date) => {
|
|
|
|
|
|
if (!formState.currentPeriodEnd || !isEditMode.value)
|
|
|
|
|
|
return {
|
|
|
|
|
|
disabledHours: () => [],
|
|
|
|
|
|
disabledMinutes: () => [],
|
|
|
|
|
|
disabledSeconds: () => []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const specificTime = dayjs.unix(formState.currentPeriodEnd)
|
|
|
|
|
|
|
|
|
|
|
|
if (date && date.isSame(specificTime, "day")) {
|
|
|
|
|
|
// 如果是指定日期当天,禁用时间戳之前的时间
|
|
|
|
|
|
const hour = specificTime.hour()
|
|
|
|
|
|
const minute = specificTime.minute()
|
|
|
|
|
|
const second = specificTime.second()
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
disabledHours: () => Array.from({ length: hour }, (_, i) => i), // 禁用小时之前
|
|
|
|
|
|
disabledMinutes: (selectedHour) => {
|
|
|
|
|
|
if (selectedHour === hour) {
|
|
|
|
|
|
return Array.from({ length: minute }, (_, i) => i) // 同小时,禁用分钟之前
|
|
|
|
|
|
}
|
|
|
|
|
|
return []
|
|
|
|
|
|
},
|
|
|
|
|
|
disabledSeconds: (selectedHour, selectedMinute) => {
|
|
|
|
|
|
if (selectedHour === hour && selectedMinute === minute) {
|
|
|
|
|
|
return Array.from({ length: second }, (_, i) => i) // 同小时分钟,禁用秒之前
|
|
|
|
|
|
}
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
disabledHours: () => [],
|
|
|
|
|
|
disabledMinutes: () => [],
|
|
|
|
|
|
disabledSeconds: () => []
|
|
|
|
|
|
}
|
2026-01-07 16:17:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-16 17:32:33 +08:00
|
|
|
|
const normalizeStatus = (status?: string): PlanStatus | undefined => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
if (!status) return undefined
|
|
|
|
|
|
const upper = status.toUpperCase() as PlanStatus
|
|
|
|
|
|
return upper
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
2025-12-16 17:32:33 +08:00
|
|
|
|
const getStatusColor = (status?: string) =>
|
2026-04-28 14:25:17 +08:00
|
|
|
|
statusColorMap[normalizeStatus(status) as PlanStatus] || "default"
|
2025-12-12 17:34:11 +08:00
|
|
|
|
|
|
|
|
|
|
const columns = [
|
2026-04-28 14:25:17 +08:00
|
|
|
|
{ title: "Name", dataIndex: "name", key: "name", align: "center", width: 180 },
|
|
|
|
|
|
{ title: "ID", dataIndex: "id", key: "id", align: "center", width: 80 },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "Organization",
|
|
|
|
|
|
dataIndex: "organizationName",
|
|
|
|
|
|
key: "organizationName",
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
width: 180
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "Admin Account",
|
|
|
|
|
|
dataIndex: "adminAccEmail",
|
|
|
|
|
|
key: "adminAccEmail",
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
width: 180,
|
|
|
|
|
|
ellipsis: true
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "Sub-Account Num",
|
|
|
|
|
|
dataIndex: "accountNum",
|
|
|
|
|
|
key: "accountNum",
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
ellipsis: true
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "Country or Region",
|
|
|
|
|
|
dataIndex: "countryOrRegion",
|
|
|
|
|
|
key: "countryOrRegion",
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
ellipsis: true
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "Start Time",
|
|
|
|
|
|
dataIndex: "currentPeriodStart",
|
|
|
|
|
|
key: "currentPeriodStart",
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
width: 200
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "End Time",
|
|
|
|
|
|
dataIndex: "currentPeriodEnd",
|
|
|
|
|
|
key: "currentPeriodEnd",
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
width: 200
|
|
|
|
|
|
},
|
|
|
|
|
|
{ title: "Status", dataIndex: "status", key: "status", align: "center", width: 100 },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "Credit Limit",
|
|
|
|
|
|
dataIndex: "creditLimit",
|
|
|
|
|
|
key: "creditLimit",
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
width: 120
|
|
|
|
|
|
},
|
|
|
|
|
|
{ title: "Operations", key: "actions", width: 160, align: "center", fixed: "right" }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const userInfoColumns = [
|
|
|
|
|
|
{ title: "User Id", dataIndex: "id", key: "id", align: "center", width: 100 },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "Email",
|
|
|
|
|
|
dataIndex: "userEmail",
|
|
|
|
|
|
key: "userEmail",
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
width: 200,
|
|
|
|
|
|
ellipsis: true
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "User Name",
|
|
|
|
|
|
dataIndex: "userName",
|
|
|
|
|
|
key: "userName",
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
width: 150,
|
|
|
|
|
|
ellipsis: true
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "Valid Start Time",
|
|
|
|
|
|
dataIndex: "validStartTime",
|
|
|
|
|
|
key: "validStartTime",
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
width: 200
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "Valid End Time",
|
|
|
|
|
|
dataIndex: "validEndTime",
|
|
|
|
|
|
key: "validEndTime",
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
width: 200
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "Country or Region",
|
|
|
|
|
|
dataIndex: "country",
|
|
|
|
|
|
key: "country",
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
width: 180,
|
|
|
|
|
|
ellipsis: true
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "Credits",
|
|
|
|
|
|
dataIndex: "credits",
|
|
|
|
|
|
key: "credits",
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
width: 100
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "User Type",
|
|
|
|
|
|
dataIndex: "systemUser",
|
|
|
|
|
|
key: "systemUser",
|
|
|
|
|
|
align: "center",
|
|
|
|
|
|
width: 120
|
|
|
|
|
|
}
|
2025-12-12 17:34:11 +08:00
|
|
|
|
]
|
2025-12-17 14:02:58 +08:00
|
|
|
|
|
2026-04-28 14:25:17 +08:00
|
|
|
|
const systemUserLabelMap: Record<string, string> = {
|
|
|
|
|
|
"0": "visitor",
|
|
|
|
|
|
"1": "yearly",
|
|
|
|
|
|
"2": "monthly",
|
|
|
|
|
|
"3": "trial",
|
|
|
|
|
|
"4": "userInEvent",
|
|
|
|
|
|
"7": "Edu Admin"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getSystemUserLabel = (systemUser?: number | string) => {
|
|
|
|
|
|
if (systemUser === undefined || systemUser === null) return ""
|
|
|
|
|
|
return systemUserLabelMap[String(systemUser)] || String(systemUser)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const formatUserTime = (value?: number | string) => {
|
|
|
|
|
|
if (!value) return ""
|
|
|
|
|
|
if (typeof value === "number") {
|
|
|
|
|
|
return formatTime(value / 1000, "YYYY-MM-DD hh:mm:ss")
|
|
|
|
|
|
}
|
|
|
|
|
|
if (/^\d+$/.test(value)) {
|
|
|
|
|
|
return formatTime(Number(value) / 1000, "YYYY-MM-DD hh:mm:ss")
|
|
|
|
|
|
}
|
|
|
|
|
|
return value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const normalizeUserInfoList = (res: any): UserInfoRecord[] => {
|
|
|
|
|
|
if (Array.isArray(res)) return res
|
|
|
|
|
|
if (Array.isArray(res?.records)) return res.records
|
|
|
|
|
|
if (res) return [res]
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const buildUserInfoParams = (id: string | number) => ({
|
|
|
|
|
|
endTime: "",
|
|
|
|
|
|
startTime: "",
|
|
|
|
|
|
size: userInfoPage.size,
|
|
|
|
|
|
page: userInfoPage.current,
|
|
|
|
|
|
systemUser: "",
|
|
|
|
|
|
country: "",
|
|
|
|
|
|
email: "",
|
|
|
|
|
|
userType: "",
|
|
|
|
|
|
ids: [],
|
|
|
|
|
|
occupation: "",
|
|
|
|
|
|
order: "",
|
|
|
|
|
|
orderBy: "",
|
|
|
|
|
|
userName: "",
|
|
|
|
|
|
subscriptionPlanId: id
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const fetchUserInfo = async () => {
|
|
|
|
|
|
if (currentUserInfoPlanId.value === null) return
|
|
|
|
|
|
userInfoLoading.value = true
|
|
|
|
|
|
userInfoList.value = []
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await Https.axiosPost(
|
|
|
|
|
|
Https.httpUrls.getUserInfo,
|
|
|
|
|
|
buildUserInfoParams(currentUserInfoPlanId.value)
|
|
|
|
|
|
)
|
|
|
|
|
|
const records = normalizeUserInfoList(res)
|
|
|
|
|
|
userInfoList.value = records
|
|
|
|
|
|
userInfoPage.total = Number(res?.total ?? records.length)
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
message.error(error.message || "Failed to load user info")
|
|
|
|
|
|
console.error(error)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
userInfoLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const openUserInfo = async (record: SubscriptionPlan) => {
|
|
|
|
|
|
console.log(record)
|
|
|
|
|
|
// debugger
|
|
|
|
|
|
|
|
|
|
|
|
currentUserInfoPlanId.value = record.id
|
|
|
|
|
|
userInfoPage.current = 1
|
|
|
|
|
|
userInfoModalVisible.value = true
|
|
|
|
|
|
await fetchUserInfo()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const changeUserInfoPage = (pagination: any) => {
|
|
|
|
|
|
userInfoPage.current = pagination.current
|
|
|
|
|
|
userInfoPage.size = pagination.pageSize
|
|
|
|
|
|
fetchUserInfo()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const customPlanRow = (record: SubscriptionPlan) => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
onClick: (event: MouseEvent) => {
|
|
|
|
|
|
const target = event.target as HTMLElement | null
|
|
|
|
|
|
if (target?.closest(".plan-row-actions")) return
|
|
|
|
|
|
openUserInfo(record)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const historyTable = ref<HTMLElement | null>(null)
|
2025-12-17 14:02:58 +08:00
|
|
|
|
const historyTableHeight = ref(0)
|
2026-04-28 14:25:17 +08:00
|
|
|
|
const minTableBodyHeight = 120
|
|
|
|
|
|
let tableResizeObserver: ResizeObserver | null = null
|
|
|
|
|
|
let tableResizeTimer: ReturnType<typeof window.setTimeout> | null = null
|
|
|
|
|
|
|
2025-12-17 14:02:58 +08:00
|
|
|
|
const handleResizeColumn = (w: any, col: any) => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
col.width = w
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getElementOuterHeight = (element: Element | null) => {
|
|
|
|
|
|
if (!element) return 0
|
|
|
|
|
|
const htmlElement = element as HTMLElement
|
|
|
|
|
|
const style = window.getComputedStyle(htmlElement)
|
|
|
|
|
|
return (
|
|
|
|
|
|
htmlElement.offsetHeight +
|
|
|
|
|
|
Number.parseFloat(style.marginTop || "0") +
|
|
|
|
|
|
Number.parseFloat(style.marginBottom || "0")
|
|
|
|
|
|
)
|
2025-12-17 14:02:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const calculateTableHeight = () => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
if (tableResizeTimer) {
|
|
|
|
|
|
window.clearTimeout(tableResizeTimer)
|
|
|
|
|
|
}
|
|
|
|
|
|
tableResizeTimer = window.setTimeout(() => {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
const tableWrapper = historyTable.value
|
|
|
|
|
|
if (!tableWrapper) {
|
|
|
|
|
|
tableResizeTimer = null
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const tableHead = tableWrapper.querySelector(".ant-table-thead") ?? null
|
|
|
|
|
|
const pagination = tableWrapper.querySelector(".ant-pagination") ?? null
|
|
|
|
|
|
const tableHeadHeight = getElementOuterHeight(tableHead) || 55
|
|
|
|
|
|
const paginationHeight = getElementOuterHeight(pagination) || 48
|
|
|
|
|
|
const reservedHeight = tableHeadHeight + paginationHeight + 8
|
|
|
|
|
|
|
|
|
|
|
|
historyTableHeight.value = Math.max(
|
|
|
|
|
|
minTableBodyHeight,
|
|
|
|
|
|
tableWrapper.clientHeight - reservedHeight
|
|
|
|
|
|
)
|
|
|
|
|
|
tableResizeTimer = null
|
|
|
|
|
|
})
|
|
|
|
|
|
}, 50)
|
2025-12-17 14:02:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleResize = () => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
calculateTableHeight()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const setupTableResizeObserver = () => {
|
|
|
|
|
|
if (!historyTable.value || typeof ResizeObserver === "undefined") return
|
|
|
|
|
|
tableResizeObserver?.disconnect()
|
|
|
|
|
|
tableResizeObserver = new ResizeObserver(() => {
|
|
|
|
|
|
calculateTableHeight()
|
|
|
|
|
|
})
|
|
|
|
|
|
tableResizeObserver.observe(historyTable.value)
|
|
|
|
|
|
|
|
|
|
|
|
const searchCard = historyTable.value
|
|
|
|
|
|
.closest(".subscription-plan")
|
|
|
|
|
|
?.querySelector(".search-card")
|
|
|
|
|
|
if (searchCard) {
|
|
|
|
|
|
tableResizeObserver.observe(searchCard)
|
|
|
|
|
|
}
|
2025-12-17 14:02:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 17:34:11 +08:00
|
|
|
|
onMounted(async () => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
await getOrganizationList()
|
|
|
|
|
|
await handleSearch()
|
|
|
|
|
|
calculateTableHeight()
|
|
|
|
|
|
setupTableResizeObserver()
|
|
|
|
|
|
window.addEventListener("resize", handleResize)
|
|
|
|
|
|
const list = sessionStorage.getItem("allCountry")
|
|
|
|
|
|
countryList.value = list ? JSON.parse(list) : []
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onActivated(() => {
|
|
|
|
|
|
calculateTableHeight()
|
2025-12-17 14:02:58 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
window.removeEventListener("resize", handleResize)
|
|
|
|
|
|
tableResizeObserver?.disconnect()
|
|
|
|
|
|
if (tableResizeTimer) {
|
|
|
|
|
|
window.clearTimeout(tableResizeTimer)
|
|
|
|
|
|
tableResizeTimer = null
|
|
|
|
|
|
}
|
2025-12-12 17:34:11 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const handleFetchTableData = async () => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
tableLoading.value = true
|
|
|
|
|
|
return Https.axiosPost(Https.httpUrls.searchAllSubscribePlan, searchForm)
|
|
|
|
|
|
.then((res) => {
|
|
|
|
|
|
tableData.value = res.records
|
|
|
|
|
|
searchForm.total = res.total
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
tableLoading.value = false
|
|
|
|
|
|
calculateTableHeight()
|
|
|
|
|
|
})
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const resetFormState = () => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
formState.name = ""
|
|
|
|
|
|
formState.currentPeriodStart = ""
|
|
|
|
|
|
formState.currentPeriodEnd = ""
|
|
|
|
|
|
formState.organizationId = undefined
|
|
|
|
|
|
formState.adminAccId = undefined
|
|
|
|
|
|
formState.creditLimit = null
|
|
|
|
|
|
formState.accountNum = null
|
|
|
|
|
|
formState.status = undefined
|
|
|
|
|
|
formState.countryOrRegion = null
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-17 14:02:58 +08:00
|
|
|
|
const changePage = (pagination: any) => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
searchForm.page = pagination.current
|
|
|
|
|
|
searchForm.size = pagination.pageSize
|
|
|
|
|
|
handleFetchTableData()
|
2025-12-17 14:02:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 17:34:11 +08:00
|
|
|
|
const handleSearch = () => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
searchForm.page = 1
|
|
|
|
|
|
handleFetchTableData()
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleReset = () => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
searchForm.name = ""
|
|
|
|
|
|
searchForm.startTime = ""
|
|
|
|
|
|
searchForm.endTime = ""
|
|
|
|
|
|
searchForm.organizationId = undefined
|
|
|
|
|
|
searchForm.adminAccId = undefined
|
|
|
|
|
|
searchForm.status = []
|
|
|
|
|
|
searchForm.id = ""
|
|
|
|
|
|
searchForm.countryOrRegion = null
|
|
|
|
|
|
handleSearch()
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const openCreate = () => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
modalTitle.value = "New Subscription Plan"
|
|
|
|
|
|
isEditMode.value = false
|
|
|
|
|
|
resetFormState()
|
|
|
|
|
|
modalVisible.value = true
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const openEdit = (record: SubscriptionPlan) => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
modalTitle.value = "Edit Subscription Plan"
|
|
|
|
|
|
isEditMode.value = true
|
|
|
|
|
|
formState.name = record.name
|
|
|
|
|
|
formState.currentPeriodStart = String(record.currentPeriodStart)
|
|
|
|
|
|
formState.currentPeriodEnd = String(record.currentPeriodEnd)
|
|
|
|
|
|
formState.organizationId = record.organizationId
|
|
|
|
|
|
formState.adminAccId = record.adminAccId
|
|
|
|
|
|
formState.creditLimit = record.creditLimit
|
|
|
|
|
|
formState.accountNum = (record as any).accountNum || null
|
|
|
|
|
|
formState.status = record.status
|
|
|
|
|
|
formState.id = record.id
|
|
|
|
|
|
formState.countryOrRegion = (record as any).countryOrRegion || null
|
|
|
|
|
|
|
|
|
|
|
|
// 检查组织ID是否在已加载的组织列表中,如果不在,则添加临时项
|
|
|
|
|
|
if (record.organizationId) {
|
|
|
|
|
|
const orgExists = organizationOptions.value.some(
|
|
|
|
|
|
(org: any) =>
|
|
|
|
|
|
org.id === record.organizationId || String(org.id) === String(record.organizationId)
|
|
|
|
|
|
)
|
|
|
|
|
|
if (!orgExists) {
|
|
|
|
|
|
const orgName = (record as any).organizationName
|
|
|
|
|
|
if (orgName) {
|
|
|
|
|
|
organizationOptions.value = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: record.organizationId,
|
|
|
|
|
|
name: orgName
|
|
|
|
|
|
},
|
|
|
|
|
|
...organizationOptions.value
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
modalVisible.value = true
|
|
|
|
|
|
if (record.adminAccId) {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
userRef.value.patchList({
|
|
|
|
|
|
label: record.name,
|
|
|
|
|
|
value: record.adminAccId,
|
|
|
|
|
|
email: record.adminAccEmail
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const validateForm = (): boolean => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
interface FieldRule {
|
|
|
|
|
|
value: any
|
|
|
|
|
|
message: string
|
|
|
|
|
|
checkNull?: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const requiredFields: FieldRule[] = [
|
|
|
|
|
|
{ value: formState.currentPeriodStart, message: "Please select the start time" },
|
|
|
|
|
|
{ value: formState.currentPeriodEnd, message: "Please select the end time" },
|
|
|
|
|
|
{ value: formState.adminAccId, message: "Please select the admin account" },
|
|
|
|
|
|
{
|
|
|
|
|
|
value: formState.creditLimit,
|
|
|
|
|
|
message: "Please input credit limit",
|
|
|
|
|
|
checkNull: true
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
value: formState.accountNum,
|
|
|
|
|
|
message: "Please input account number",
|
|
|
|
|
|
checkNull: true
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
if (!isEditMode.value) {
|
|
|
|
|
|
requiredFields.push(
|
|
|
|
|
|
{ value: formState.name, message: "Please input the name" },
|
|
|
|
|
|
{ value: formState.organizationId, message: "Please select organization" }
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const field of requiredFields) {
|
|
|
|
|
|
const isEmpty = field.checkNull
|
|
|
|
|
|
? field.value === null || field.value === undefined
|
|
|
|
|
|
: !field.value
|
|
|
|
|
|
if (isEmpty) {
|
|
|
|
|
|
message.warning(field.message)
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
if (!validateForm()) return
|
|
|
|
|
|
confirmLoading.value = true
|
|
|
|
|
|
const params = {
|
|
|
|
|
|
...formState,
|
|
|
|
|
|
currentPeriodStart: Number(formState.currentPeriodStart),
|
|
|
|
|
|
currentPeriodEnd: Number(formState.currentPeriodEnd)
|
|
|
|
|
|
}
|
|
|
|
|
|
let res = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (isEditMode.value) {
|
|
|
|
|
|
res = await Https.axiosPost(Https.httpUrls.updateSubscribePlan, params)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res = await Https.axiosPost(Https.httpUrls.createSubscribePlan, params)
|
|
|
|
|
|
}
|
|
|
|
|
|
message.success(
|
|
|
|
|
|
`${isEditMode.value ? "Subscription plan updated" : "Subscription plan created"}`
|
|
|
|
|
|
)
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
message.error(error.message)
|
|
|
|
|
|
console.error(error)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
confirmLoading.value = false
|
|
|
|
|
|
modalVisible.value = false
|
|
|
|
|
|
resetFormState()
|
|
|
|
|
|
isEditMode.value = false
|
|
|
|
|
|
handleSearch()
|
|
|
|
|
|
}
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-16 17:32:33 +08:00
|
|
|
|
// 防抖包装,避免重复点击
|
|
|
|
|
|
const handleSubmitDebounced = debounce(handleSubmit, 500, {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
leading: true,
|
|
|
|
|
|
trailing: false
|
2025-12-16 17:32:33 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-12 17:34:11 +08:00
|
|
|
|
const cancelModal = () => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
modalVisible.value = false
|
|
|
|
|
|
resetFormState()
|
|
|
|
|
|
isEditMode.value = false
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const removePlan = (id: number) => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
tableLoading.value = true
|
|
|
|
|
|
Https.axiosGet(Https.httpUrls.deleteSubscribePlan, { params: { id } })
|
|
|
|
|
|
.then((res) => {
|
|
|
|
|
|
message.success("Subscription plan deleted")
|
|
|
|
|
|
handleReset()
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((error: any) => {
|
|
|
|
|
|
message.error(error.message)
|
|
|
|
|
|
console.error(error)
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
tableLoading.value = false
|
|
|
|
|
|
})
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const organizationOptions = ref([])
|
|
|
|
|
|
const organizationParams = reactive({
|
2026-04-28 14:25:17 +08:00
|
|
|
|
page: 1,
|
|
|
|
|
|
size: 10,
|
|
|
|
|
|
total: 0
|
2025-12-12 17:34:11 +08:00
|
|
|
|
})
|
|
|
|
|
|
const organizationLoading = ref(false)
|
|
|
|
|
|
const getOrganizationList = async (isLoadMore = false) => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
if (organizationLoading.value) return
|
|
|
|
|
|
if (isLoadMore) {
|
|
|
|
|
|
const loaded = organizationParams.page * organizationParams.size
|
|
|
|
|
|
if (organizationParams.total && loaded >= organizationParams.total) return
|
|
|
|
|
|
organizationParams.page += 1
|
|
|
|
|
|
} else {
|
|
|
|
|
|
organizationParams.page = 1
|
|
|
|
|
|
organizationOptions.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
organizationLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { total, ...requestParams } = organizationParams
|
|
|
|
|
|
const rv: any = await Https.axiosPost(Https.httpUrls.queryOrganization, requestParams)
|
|
|
|
|
|
if (rv) {
|
|
|
|
|
|
const newRecords = rv.records || []
|
|
|
|
|
|
// 遍历新数据,如果已存在则覆盖,不存在则追加
|
|
|
|
|
|
newRecords.forEach((newOrg: any) => {
|
|
|
|
|
|
const newOrgId = String(newOrg.id)
|
|
|
|
|
|
const existingIndex = organizationOptions.value.findIndex(
|
|
|
|
|
|
(org: any) => String(org.id) === newOrgId
|
|
|
|
|
|
)
|
|
|
|
|
|
if (existingIndex !== -1) {
|
|
|
|
|
|
// 如果已存在,用新数据覆盖旧项
|
|
|
|
|
|
organizationOptions.value[existingIndex] = newOrg
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果不存在,追加到末尾
|
|
|
|
|
|
organizationOptions.value.push(newOrg)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
organizationParams.total = rv.total || 0
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
organizationLoading.value = false
|
|
|
|
|
|
}
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
const handleOrganizationScroll = (e: any) => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
const target = e?.target
|
|
|
|
|
|
if (!target) return
|
|
|
|
|
|
const nearBottom = target.scrollTop + target.clientHeight >= target.scrollHeight - 20
|
|
|
|
|
|
if (nearBottom) {
|
|
|
|
|
|
getOrganizationList(true)
|
|
|
|
|
|
}
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
2025-12-15 10:39:19 +08:00
|
|
|
|
|
|
|
|
|
|
const handleOrganizationSelect = (value: string) => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
if (value === "ADD_ORGANIZATION") {
|
|
|
|
|
|
// 打开添加组织弹窗
|
|
|
|
|
|
organizationModalVisible.value = true
|
|
|
|
|
|
// 使用nextTick确保值被重置,使其不被选中
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
if (searchForm.organizationId === "ADD_ORGANIZATION") {
|
|
|
|
|
|
searchForm.organizationId = undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
if (formState.organizationId === "ADD_ORGANIZATION") {
|
|
|
|
|
|
formState.organizationId = undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-12-15 10:39:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleOrganizationChange = (value: string) => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
// 如果change事件触发时值是"添加组织",立即重置
|
|
|
|
|
|
if (value === "ADD_ORGANIZATION") {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
if (searchForm.organizationId === "ADD_ORGANIZATION") {
|
|
|
|
|
|
searchForm.organizationId = undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
if (formState.organizationId === "ADD_ORGANIZATION") {
|
|
|
|
|
|
formState.organizationId = undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-12-15 10:39:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cancelOrganizationModal = () => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
organizationModalVisible.value = false
|
|
|
|
|
|
organizationForm.name = ""
|
|
|
|
|
|
organizationForm.type = undefined
|
2025-12-15 10:39:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleCreateOrganization = async () => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
if (!organizationForm.name || !organizationForm.type) {
|
|
|
|
|
|
message.warning("Please fill in name and type")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res: any = await Https.axiosGet(Https.httpUrls.addOrganization, {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
name: organizationForm.name,
|
|
|
|
|
|
type: organizationForm.type
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
message.success("Organization created successfully")
|
|
|
|
|
|
cancelOrganizationModal()
|
|
|
|
|
|
// 刷新组织列表
|
|
|
|
|
|
await getOrganizationList()
|
|
|
|
|
|
// 如果是在编辑/新建弹窗中,自动选择新创建的组织
|
|
|
|
|
|
if (modalVisible.value) {
|
|
|
|
|
|
const newOrgId = res?.id || res?.data?.id || res
|
|
|
|
|
|
if (newOrgId) {
|
|
|
|
|
|
formState.organizationId = String(newOrgId)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
message.error(error.message || "Failed to create organization")
|
|
|
|
|
|
console.error(error)
|
|
|
|
|
|
}
|
2025-12-15 10:39:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 17:34:11 +08:00
|
|
|
|
const filterOption = (input: string, option: any) => {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
const label = option?.label ?? option?.children ?? option?.key?.label ?? ""
|
|
|
|
|
|
return String(label).toLowerCase().includes(input.toLowerCase())
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
|
|
|
.subscription-plan {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
padding: 2rem 2.4rem 3.2rem 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
|
|
|
|
.search-card {
|
|
|
|
|
|
margin-bottom: 1.6rem;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-card {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-card-body) {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
padding: 2.4rem;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-card__header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
margin-bottom: 2.6rem;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-wrapper {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-table-wrapper),
|
|
|
|
|
|
:deep(.ant-spin-nested-loading),
|
|
|
|
|
|
:deep(.ant-spin-container) {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-spin-container) {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-table) {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-table-container) {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-table-content) {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-table-body) {
|
|
|
|
|
|
overflow-y: auto !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-pagination) {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.danger-text {
|
|
|
|
|
|
color: #ff4d4f;
|
|
|
|
|
|
}
|
|
|
|
|
|
:deep(.ant-table-cell::before) {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-table-thead > tr > th) {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
:deep(.ant-table-tbody > tr > td) {
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-table-tbody > tr:hover > td) {
|
|
|
|
|
|
background: rgb(202, 202, 202);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-table-tbody > tr) {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.plan-row-actions) {
|
|
|
|
|
|
cursor: default;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-info-modal {
|
|
|
|
|
|
:deep(.ant-modal-body) {
|
|
|
|
|
|
padding: 2.4rem;
|
|
|
|
|
|
}
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
2026-04-28 14:25:17 +08:00
|
|
|
|
|
2025-12-12 17:34:11 +08:00
|
|
|
|
:deep(.subscriptionPlan_modal) {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
.ant-modal-body {
|
|
|
|
|
|
// height: calc(65rem * 1.2);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
padding: 2.5rem 3rem;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.subscriptionPlan_modal {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
.form_content {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
row-gap: 2rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.admin_state_item {
|
|
|
|
|
|
> span {
|
|
|
|
|
|
width: 17rem !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.modal_title_text {
|
|
|
|
|
|
margin-bottom: 2rem;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.subscriptionPlan_center {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding: 2rem 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.subscriptionPlan_btn {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
padding: 1.5rem 0 0 0;
|
|
|
|
|
|
margin-top: auto;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
.admin_search_item {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.search-form) {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
--search-label-width: 14rem;
|
|
|
|
|
|
--search-control-width: 20rem;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
column-gap: 1.8rem;
|
|
|
|
|
|
row-gap: 1.8rem;
|
|
|
|
|
|
|
|
|
|
|
|
.ant-form-item {
|
|
|
|
|
|
min-width: calc(var(--search-label-width) + var(--search-control-width));
|
|
|
|
|
|
margin-right: 0;
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-form-item-label {
|
|
|
|
|
|
flex: 0 0 var(--search-label-width);
|
|
|
|
|
|
max-width: var(--search-label-width);
|
|
|
|
|
|
overflow: visible;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
|
|
|
|
|
|
> label {
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-form-item-control {
|
|
|
|
|
|
flex: 0 0 var(--search-control-width);
|
|
|
|
|
|
max-width: var(--search-control-width);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-input,
|
|
|
|
|
|
.ant-input-affix-wrapper,
|
|
|
|
|
|
.ant-picker,
|
|
|
|
|
|
.ant-select {
|
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-form__actions {
|
|
|
|
|
|
min-width: auto;
|
|
|
|
|
|
|
|
|
|
|
|
.ant-form-item-control {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
max-width: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 1600px) {
|
|
|
|
|
|
:deep(.search-form) {
|
|
|
|
|
|
--search-control-width: 22rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 760px) {
|
|
|
|
|
|
:deep(.search-form) {
|
|
|
|
|
|
.ant-form-item {
|
|
|
|
|
|
flex: 1 1 100%;
|
|
|
|
|
|
min-width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-form-item-control {
|
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
|
max-width: calc(100% - var(--search-label-width));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-12 17:34:11 +08:00
|
|
|
|
}
|
2025-12-15 10:39:19 +08:00
|
|
|
|
|
|
|
|
|
|
:deep(.ant-select-dropdown) {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
.add-organization-option {
|
|
|
|
|
|
color: #1890ff !important;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
background-color: #f0f7ff !important;
|
|
|
|
|
|
border-bottom: 1px solid #e6f4ff;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
padding-bottom: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background-color: #e6f4ff !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-15 10:39:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.organization_modal) {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
.ant-modal-body {
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
min-height: 300px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
padding: 2rem 2.5rem;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.subscriptionPlan_center {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
overflow: visible;
|
|
|
|
|
|
padding: 1rem 0;
|
|
|
|
|
|
min-height: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal_title_text {
|
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
|
}
|
2025-12-15 10:39:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
.organization_footer {
|
2026-04-28 14:25:17 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
column-gap: 3rem;
|
|
|
|
|
|
.footer_btn {
|
|
|
|
|
|
border-radius: 3.3rem;
|
|
|
|
|
|
}
|
2025-12-15 10:39:19 +08:00
|
|
|
|
}
|
2025-12-12 17:34:11 +08:00
|
|
|
|
</style>
|