feat: 超级管理员订阅计划页面
This commit is contained in:
669
src/component/Administrator/subscriptionPlan.vue
Normal file
669
src/component/Administrator/subscriptionPlan.vue
Normal file
@@ -0,0 +1,669 @@
|
||||
<template>
|
||||
<div class="subscription-plan">
|
||||
<a-card class="search-card" :bordered="false">
|
||||
<a-form
|
||||
class="search-form"
|
||||
layout="inline"
|
||||
:model="searchForm"
|
||||
:label-col="{ style: { width: '12rem' } }"
|
||||
:wrapper-col="{ style: { width: '22rem' } }"
|
||||
>
|
||||
<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">
|
||||
<a-input
|
||||
v-model:value="searchForm.name"
|
||||
allow-clear
|
||||
placeholder="Input the name"
|
||||
/>
|
||||
</a-form-item>
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
<a-select
|
||||
v-model:value="searchForm.adminAccId"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
placeholder="Select Account"
|
||||
style="width: 180px"
|
||||
:options="allUserList"
|
||||
></a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<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">
|
||||
<div class="table-card__title">Subscription Plan</div>
|
||||
<a-button type="primary" @click="openCreate">New Subscription Plan</a-button>
|
||||
</div>
|
||||
<a-table
|
||||
:data-source="tableData"
|
||||
:columns="columns"
|
||||
:loading="tableLoading"
|
||||
row-key="id"
|
||||
: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 === 'organizationId'">
|
||||
{{ organizationOptions.find(item => item.id === record[column.key]).name }}
|
||||
</template> -->
|
||||
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="statusColorMap[record.status]">
|
||||
{{ statusLabelMap[record.status] }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'adminAccId'">
|
||||
{{ allUserList.find(item => item.value === record.adminAccId)?.label }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a @click="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">Delete</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<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">
|
||||
<span>
|
||||
Name:
|
||||
<span>*</span>
|
||||
</span>
|
||||
<a-input
|
||||
v-model:value="formState.name"
|
||||
placeholder="Input the name"
|
||||
style="width: 250px"
|
||||
:disabled="isEditMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>
|
||||
Organization:
|
||||
<span>*</span>
|
||||
</span>
|
||||
<a-select
|
||||
v-model:value="formState.organizationId"
|
||||
placeholder="Select the organization"
|
||||
allow-clear
|
||||
style="width: 250px"
|
||||
@popupScroll="handleOrganizationScroll"
|
||||
:disabled="isEditMode"
|
||||
>
|
||||
<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>
|
||||
Admin Account:
|
||||
<span>*</span>
|
||||
</span>
|
||||
<a-select
|
||||
v-model:value="formState.adminAccId"
|
||||
placeholder="Select the admin account"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
style="width: 250px"
|
||||
:options="allUserList"
|
||||
></a-select>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>
|
||||
Start Time:
|
||||
<span>*</span>
|
||||
</span>
|
||||
<a-date-picker
|
||||
v-model:value="formState.currentPeriodStart"
|
||||
value-format="X"
|
||||
style="width: 250px"
|
||||
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"
|
||||
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>
|
||||
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>
|
||||
</div>
|
||||
<div class="subscriptionPlan_btn admin_page">
|
||||
<div class="admin_search_item" @click="cancelModal">Close</div>
|
||||
<div class="admin_search_item" @click="handleSubmit">OK</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted, computed } from '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'
|
||||
|
||||
type PlanStatus = 'active' | 'paused' | 'ended'
|
||||
interface SubscriptionPlan {
|
||||
id: number
|
||||
name: string
|
||||
currentPeriodStart: string
|
||||
currentPeriodEnd: string
|
||||
organizationId: string
|
||||
adminAccId: string
|
||||
status: PlanStatus
|
||||
creditLimit: number
|
||||
accountNum?: number
|
||||
startStamp: number
|
||||
endStamp: number
|
||||
}
|
||||
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
organizationId: undefined as string | undefined,
|
||||
adminAccId: undefined as string | undefined,
|
||||
id: '',
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const toSeconds = (dateStr: string) => Math.floor(new Date(dateStr).getTime() / 1000)
|
||||
|
||||
const tableData = ref<SubscriptionPlan[]>([])
|
||||
const tableLoading = ref(false)
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const confirmLoading = ref(false)
|
||||
const modalTitle = ref('New Subscription Plan')
|
||||
const isEditMode = ref(false)
|
||||
const formState = reactive({
|
||||
name: '',
|
||||
currentPeriodStart: '',
|
||||
currentPeriodEnd: '',
|
||||
organizationId: undefined as string | undefined,
|
||||
adminAccId: undefined as string | undefined,
|
||||
creditLimit: null as number | null,
|
||||
accountNum: null as number | null
|
||||
})
|
||||
|
||||
const statusLabelMap: Record<PlanStatus, string> = {
|
||||
active: 'Active',
|
||||
paused: 'Paused',
|
||||
ended: 'Ended'
|
||||
}
|
||||
const statusColorMap: Record<PlanStatus, string> = {
|
||||
active: 'green',
|
||||
paused: 'orange',
|
||||
ended: 'red'
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: 'Name', dataIndex: 'name', key: 'name' },
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: 'Organization', dataIndex: 'organizationName', key: 'organizationName' },
|
||||
{ title: 'Admin Account', dataIndex: 'adminAccId', key: 'adminAccId' },
|
||||
{ title: 'Account Num', dataIndex: 'accountNum', key: 'accountNum' },
|
||||
{
|
||||
title: 'Start Time',
|
||||
dataIndex: 'currentPeriodStart',
|
||||
key: 'currentPeriodStart'
|
||||
},
|
||||
{
|
||||
title: 'End Time',
|
||||
dataIndex: 'currentPeriodEnd',
|
||||
key: 'currentPeriodEnd'
|
||||
},
|
||||
{ title: 'Status', dataIndex: 'status', key: 'status' },
|
||||
{ title: 'Credit Limit', dataIndex: 'creditLimit', key: 'creditLimit' },
|
||||
{ title: 'Operations', key: 'actions', width: 160 }
|
||||
]
|
||||
onMounted(async () => {
|
||||
await getOrganizationList()
|
||||
await handleSearch()
|
||||
})
|
||||
|
||||
const handleFetchTableData = async () => {
|
||||
tableLoading.value = true
|
||||
return Https.axiosPost(Https.httpUrls.searchAllSubscribePlan, searchForm)
|
||||
.then(res => {
|
||||
tableData.value = res.records.map(item => {
|
||||
const organization = organizationOptions.value.find(
|
||||
el => el.id === item.organizationId
|
||||
) || { name: '' }
|
||||
return {
|
||||
...item,
|
||||
organizationName: organization.name || ''
|
||||
}
|
||||
debugger
|
||||
})
|
||||
searchForm.total = res.total
|
||||
})
|
||||
.finally(() => {
|
||||
tableLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const resetFormState = () => {
|
||||
formState.name = ''
|
||||
formState.currentPeriodStart = ''
|
||||
formState.currentPeriodEnd = ''
|
||||
formState.organizationId = undefined
|
||||
formState.adminAccId = undefined
|
||||
formState.creditLimit = null
|
||||
formState.accountNum = null
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
searchForm.page = 1
|
||||
handleFetchTableData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.name = ''
|
||||
searchForm.startTime = ''
|
||||
searchForm.endTime = ''
|
||||
searchForm.organizationId = undefined
|
||||
searchForm.adminAccId = undefined
|
||||
searchForm.id = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const allUserList = computed(() => {
|
||||
return store.state.adminPage.allUserList
|
||||
})
|
||||
const openCreate = () => {
|
||||
modalTitle.value = 'New Subscription Plan'
|
||||
isEditMode.value = false
|
||||
resetFormState()
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const openEdit = (record: SubscriptionPlan) => {
|
||||
modalTitle.value = 'Edit Subscription Plan'
|
||||
isEditMode.value = true
|
||||
formState.name = record.name
|
||||
formState.currentPeriodStart = record.currentPeriodStart
|
||||
formState.currentPeriodEnd = record.currentPeriodEnd
|
||||
formState.organizationId = record.organizationId
|
||||
formState.adminAccId = record.adminAccId
|
||||
formState.creditLimit = record.creditLimit
|
||||
formState.accountNum = (record as any).accountNum || null
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
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
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
const cancelModal = () => {
|
||||
modalVisible.value = false
|
||||
resetFormState()
|
||||
isEditMode.value = false
|
||||
}
|
||||
|
||||
const removePlan = (id: number) => {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
const organizationOptions = ref([])
|
||||
const organizationParams = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
const organizationLoading = ref(false)
|
||||
const getOrganizationList = async (isLoadMore = false) => {
|
||||
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 rv: any = await Https.axiosPost(
|
||||
Https.httpUrls.queryOrganization,
|
||||
organizationParams
|
||||
)
|
||||
if (rv) {
|
||||
organizationOptions.value = [...organizationOptions.value, ...(rv.records || [])]
|
||||
organizationParams.total = rv.total || 0
|
||||
}
|
||||
} finally {
|
||||
organizationLoading.value = false
|
||||
}
|
||||
}
|
||||
const handleOrganizationScroll = (e: any) => {
|
||||
const target = e?.target
|
||||
if (!target) return
|
||||
const nearBottom = target.scrollTop + target.clientHeight >= target.scrollHeight - 20
|
||||
if (nearBottom) {
|
||||
getOrganizationList(true)
|
||||
}
|
||||
}
|
||||
const filterOption = (input: string, option: any) => {
|
||||
const label = option?.label ?? option?.children ?? option?.key?.label ?? ''
|
||||
return String(label).toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.subscription-plan {
|
||||
padding: 20px 24px 32px 0;
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
.table-card__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.table-card__title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.danger-text {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.subscriptionPlan_modal) {
|
||||
.ant-modal-body {
|
||||
height: calc(65rem * 1.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2.5rem 3rem;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.subscriptionPlan_modal {
|
||||
> .admin_state_item {
|
||||
> span {
|
||||
width: 15rem;
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.search-form) {
|
||||
column-gap: 2rem;
|
||||
row-gap: 2rem;
|
||||
.ant-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -854,6 +854,7 @@ export default defineComponent({
|
||||
height: 4rem;
|
||||
box-sizing: border-box;
|
||||
line-height: 4rem;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
> .payMethod_payAffirm_clause {
|
||||
text-align: center;
|
||||
|
||||
Reference in New Issue
Block a user