feat:套餐管理界面(已联调)
This commit is contained in:
@@ -84,3 +84,67 @@ export function doGetCheckCode() {
|
|||||||
url: '/code'
|
url: '/code'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/** Get rate limit list */
|
||||||
|
export function fetchRateLimitList() {
|
||||||
|
return request<Api.Auth.RateLimit[]>({
|
||||||
|
url: '/system/rateLimit/list',
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/** Add rate limit config */
|
||||||
|
export function addRateLimit(data: Api.Auth.RateLimitAdd) {
|
||||||
|
return request<any>({
|
||||||
|
url: '/system/rateLimit',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Edit rate limit config */
|
||||||
|
export function editRateLimit(data: Api.Auth.RateLimitAdd & { id: number }) {
|
||||||
|
return request<any>({
|
||||||
|
url: '/system/rateLimit',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove rate limit config */
|
||||||
|
export function removeRateLimit(ids: number | number[]) {
|
||||||
|
return request<any>({
|
||||||
|
url: `/system/rateLimit/${ids}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/** Get package list */
|
||||||
|
export function fetchPackageList() {
|
||||||
|
return request<Api.Auth.Package[]>({
|
||||||
|
url: '/u/package/list',
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/** Add package config */
|
||||||
|
export function addPackage(data: Api.Auth.PackageAdd) {
|
||||||
|
return request<any>({
|
||||||
|
url: '/system/package',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/** 修改套餐 */
|
||||||
|
export function updatePackage(data: Api.Auth.PackageAdd & { id: string }) {
|
||||||
|
return request<any>({
|
||||||
|
url: '/system/package',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除套餐 */
|
||||||
|
export function deletePackage(id: string) {
|
||||||
|
return request<any>({
|
||||||
|
url: `/system/package/${id}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
57
src/typings/api.d.ts
vendored
57
src/typings/api.d.ts
vendored
@@ -157,6 +157,62 @@ declare namespace Api {
|
|||||||
phonenumber?:string;
|
phonenumber?:string;
|
||||||
authType: string;
|
authType: string;
|
||||||
}
|
}
|
||||||
|
interface RateLimit {
|
||||||
|
id: number;
|
||||||
|
rateLimitName: string;
|
||||||
|
upLimit: number;
|
||||||
|
downLimit: number;
|
||||||
|
upLimitEnable: boolean;
|
||||||
|
downLimitEnable: boolean;
|
||||||
|
createBy: number;
|
||||||
|
createTime: string;
|
||||||
|
updateBy: number;
|
||||||
|
updateTime: string;
|
||||||
|
delFlag: boolean;
|
||||||
|
}
|
||||||
|
interface RateLimitAdd {
|
||||||
|
rateLimitName: string;
|
||||||
|
upLimitEnable: boolean;
|
||||||
|
downLimitEnable: boolean;
|
||||||
|
upLimit: number;
|
||||||
|
downLimit: number;
|
||||||
|
}
|
||||||
|
interface Package {
|
||||||
|
id: string;
|
||||||
|
packageName: string;
|
||||||
|
periodNum: number;
|
||||||
|
periodType: number;
|
||||||
|
price: number;
|
||||||
|
trafficEnable: boolean;
|
||||||
|
traffic: number;
|
||||||
|
rateLimitEnable: boolean;
|
||||||
|
rateLimitId?: number;
|
||||||
|
durationEnable: boolean;
|
||||||
|
duration: number;
|
||||||
|
clientNumEnable: boolean;
|
||||||
|
clientNum: number;
|
||||||
|
packageEnable: boolean;
|
||||||
|
createBy: number;
|
||||||
|
createTime: string;
|
||||||
|
updateBy: number;
|
||||||
|
updateTime: string;
|
||||||
|
delFlag: boolean;
|
||||||
|
}
|
||||||
|
interface PackageAdd {
|
||||||
|
packageName: string;
|
||||||
|
periodNum: number;
|
||||||
|
periodType: number;
|
||||||
|
price: number;
|
||||||
|
trafficEnable: boolean;
|
||||||
|
traffic: number;
|
||||||
|
rateLimitEnable: boolean;
|
||||||
|
rateLimitId?: number;
|
||||||
|
durationEnable: boolean;
|
||||||
|
duration: number;
|
||||||
|
clientNumEnable: boolean;
|
||||||
|
clientNum: number;
|
||||||
|
packageEnable: boolean;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -422,5 +478,6 @@ declare namespace Api {
|
|||||||
type DictSearchParams = Partial<Pick<Dict, 'dictName' | 'dictType' | 'status'> & CommonSearchParams>;
|
type DictSearchParams = Partial<Pick<Dict, 'dictName' | 'dictType' | 'status'> & CommonSearchParams>;
|
||||||
|
|
||||||
type DictList = Common.PaginatingQueryRecord<Dict>;
|
type DictList = Common.PaginatingQueryRecord<Dict>;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/typings/auto-imports.d.ts
vendored
21
src/typings/auto-imports.d.ts
vendored
@@ -10,6 +10,8 @@ declare global {
|
|||||||
const $notification: typeof import('ant-design-vue')['notification']
|
const $notification: typeof import('ant-design-vue')['notification']
|
||||||
const EffectScope: typeof import('vue')['EffectScope']
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||||
|
const addPackage: typeof import('../service/api/auth')['addPackage']
|
||||||
|
const addRateLimit: typeof import('../service/api/auth')['addRateLimit']
|
||||||
const addThemeVarsToHtml: typeof import('../store/modules/theme/shared')['addThemeVarsToHtml']
|
const addThemeVarsToHtml: typeof import('../store/modules/theme/shared')['addThemeVarsToHtml']
|
||||||
const afterAll: typeof import('vitest')['afterAll']
|
const afterAll: typeof import('vitest')['afterAll']
|
||||||
const afterEach: typeof import('vitest')['afterEach']
|
const afterEach: typeof import('vitest')['afterEach']
|
||||||
@@ -17,6 +19,8 @@ declare global {
|
|||||||
const assign: typeof import('lodash-es')['assign']
|
const assign: typeof import('lodash-es')['assign']
|
||||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||||
|
const bandwidthFactors: typeof import('../utils/units')['bandwidthFactors']
|
||||||
|
const bandwidthUnits: typeof import('../utils/units')['bandwidthUnits']
|
||||||
const beforeAll: typeof import('vitest')['beforeAll']
|
const beforeAll: typeof import('vitest')['beforeAll']
|
||||||
const beforeEach: typeof import('vitest')['beforeEach']
|
const beforeEach: typeof import('vitest')['beforeEach']
|
||||||
const chai: typeof import('vitest')['chai']
|
const chai: typeof import('vitest')['chai']
|
||||||
@@ -30,6 +34,9 @@ declare global {
|
|||||||
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||||
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||||
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||||
|
const convertBandwidth: typeof import('../utils/units')['convertBandwidth']
|
||||||
|
const convertStorage: typeof import('../utils/units')['convertStorage']
|
||||||
|
const convertTime: typeof import('../utils/units')['convertTime']
|
||||||
const createApp: typeof import('vue')['createApp']
|
const createApp: typeof import('vue')['createApp']
|
||||||
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||||
@@ -48,6 +55,7 @@ declare global {
|
|||||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
const defineComponent: typeof import('vue')['defineComponent']
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
const defineStore: typeof import('pinia')['defineStore']
|
const defineStore: typeof import('pinia')['defineStore']
|
||||||
|
const deletePackage: typeof import('../service/api/auth')['deletePackage']
|
||||||
const describe: typeof import('vitest')['describe']
|
const describe: typeof import('vitest')['describe']
|
||||||
const doAddDept: typeof import('../service/api/dept')['doAddDept']
|
const doAddDept: typeof import('../service/api/dept')['doAddDept']
|
||||||
const doAddDict: typeof import('../service/api/dict')['doAddDict']
|
const doAddDict: typeof import('../service/api/dict')['doAddDict']
|
||||||
@@ -90,6 +98,7 @@ declare global {
|
|||||||
const doPutRole: typeof import('../service/api/role')['doPutRole']
|
const doPutRole: typeof import('../service/api/role')['doPutRole']
|
||||||
const doPutUser: typeof import('../service/api/user')['doPutUser']
|
const doPutUser: typeof import('../service/api/user')['doPutUser']
|
||||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||||
|
const editRateLimit: typeof import('../service/api/auth')['editRateLimit']
|
||||||
const effectScope: typeof import('vue')['effectScope']
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
const emptyInfo: typeof import('../store/modules/auth/shared')['emptyInfo']
|
const emptyInfo: typeof import('../store/modules/auth/shared')['emptyInfo']
|
||||||
const expect: typeof import('vitest')['expect']
|
const expect: typeof import('vitest')['expect']
|
||||||
@@ -101,12 +110,17 @@ declare global {
|
|||||||
const fetchGetMenuTree: typeof import('../service/api/menu')['fetchGetMenuTree']
|
const fetchGetMenuTree: typeof import('../service/api/menu')['fetchGetMenuTree']
|
||||||
const fetchIsRouteExist: typeof import('../service/api/route')['fetchIsRouteExist']
|
const fetchIsRouteExist: typeof import('../service/api/route')['fetchIsRouteExist']
|
||||||
const fetchLogin: typeof import('../service/api/auth')['fetchLogin']
|
const fetchLogin: typeof import('../service/api/auth')['fetchLogin']
|
||||||
|
const fetchPackageList: typeof import('../service/api/auth')['fetchPackageList']
|
||||||
|
const fetchRateLimitList: typeof import('../service/api/auth')['fetchRateLimitList']
|
||||||
const fetchRefreshToken: typeof import('../service/api/auth')['fetchRefreshToken']
|
const fetchRefreshToken: typeof import('../service/api/auth')['fetchRefreshToken']
|
||||||
const fetchRegister: typeof import('../service/api/auth')['fetchRegister']
|
const fetchRegister: typeof import('../service/api/auth')['fetchRegister']
|
||||||
const filterAuthRoutesByRoles: typeof import('../store/modules/route/shared')['filterAuthRoutesByRoles']
|
const filterAuthRoutesByRoles: typeof import('../store/modules/route/shared')['filterAuthRoutesByRoles']
|
||||||
const filterTabsById: typeof import('../store/modules/tab/shared')['filterTabsById']
|
const filterTabsById: typeof import('../store/modules/tab/shared')['filterTabsById']
|
||||||
const filterTabsByIds: typeof import('../store/modules/tab/shared')['filterTabsByIds']
|
const filterTabsByIds: typeof import('../store/modules/tab/shared')['filterTabsByIds']
|
||||||
const findTabByRouteName: typeof import('../store/modules/tab/shared')['findTabByRouteName']
|
const findTabByRouteName: typeof import('../store/modules/tab/shared')['findTabByRouteName']
|
||||||
|
const formatBandwidth: typeof import('../utils/units')['formatBandwidth']
|
||||||
|
const formatStorage: typeof import('../utils/units')['formatStorage']
|
||||||
|
const formatTime: typeof import('../utils/units')['formatTime']
|
||||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||||
const getAllTabs: typeof import('../store/modules/tab/shared')['getAllTabs']
|
const getAllTabs: typeof import('../store/modules/tab/shared')['getAllTabs']
|
||||||
const getAntdTheme: typeof import('../store/modules/theme/shared')['getAntdTheme']
|
const getAntdTheme: typeof import('../store/modules/theme/shared')['getAntdTheme']
|
||||||
@@ -186,6 +200,7 @@ declare global {
|
|||||||
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||||
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||||
const removeEmptyChildren: typeof import('../utils/menu')['removeEmptyChildren']
|
const removeEmptyChildren: typeof import('../utils/menu')['removeEmptyChildren']
|
||||||
|
const removeRateLimit: typeof import('../service/api/auth')['removeRateLimit']
|
||||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||||
@@ -197,6 +212,8 @@ declare global {
|
|||||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
const shallowRef: typeof import('vue')['shallowRef']
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
const sortRoutesByOrder: typeof import('../store/modules/route/shared')['sortRoutesByOrder']
|
const sortRoutesByOrder: typeof import('../store/modules/route/shared')['sortRoutesByOrder']
|
||||||
|
const storageFactors: typeof import('../utils/units')['storageFactors']
|
||||||
|
const storageUnits: typeof import('../utils/units')['storageUnits']
|
||||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||||
const suite: typeof import('vitest')['suite']
|
const suite: typeof import('vitest')['suite']
|
||||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||||
@@ -205,6 +222,8 @@ declare global {
|
|||||||
const test: typeof import('vitest')['test']
|
const test: typeof import('vitest')['test']
|
||||||
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
||||||
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||||
|
const timeFactors: typeof import('../utils/units')['timeFactors']
|
||||||
|
const timeUnits: typeof import('../utils/units')['timeUnits']
|
||||||
const toRaw: typeof import('vue')['toRaw']
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||||
const toRef: typeof import('vue')['toRef']
|
const toRef: typeof import('vue')['toRef']
|
||||||
@@ -222,10 +241,12 @@ declare global {
|
|||||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||||
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||||
|
const unitFactors: typeof import('../utils/bandwidth')['unitFactors']
|
||||||
const unref: typeof import('vue')['unref']
|
const unref: typeof import('vue')['unref']
|
||||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||||
const until: typeof import('@vueuse/core')['until']
|
const until: typeof import('@vueuse/core')['until']
|
||||||
const updateLocaleOfGlobalMenus: typeof import('../store/modules/route/shared')['updateLocaleOfGlobalMenus']
|
const updateLocaleOfGlobalMenus: typeof import('../store/modules/route/shared')['updateLocaleOfGlobalMenus']
|
||||||
|
const updatePackage: typeof import('../service/api/auth')['updatePackage']
|
||||||
const updateTabByI18nKey: typeof import('../store/modules/tab/shared')['updateTabByI18nKey']
|
const updateTabByI18nKey: typeof import('../store/modules/tab/shared')['updateTabByI18nKey']
|
||||||
const updateTabsByI18nKey: typeof import('../store/modules/tab/shared')['updateTabsByI18nKey']
|
const updateTabsByI18nKey: typeof import('../store/modules/tab/shared')['updateTabsByI18nKey']
|
||||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||||
|
|||||||
546
src/views/billing/package/index.vue
Normal file
546
src/views/billing/package/index.vue
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full">
|
||||||
|
<SimpleScrollbar>
|
||||||
|
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||||
|
<ACard
|
||||||
|
title="套餐管理"
|
||||||
|
:bordered="false"
|
||||||
|
:body-style="{ flex: 1, overflow: 'hidden' }"
|
||||||
|
class="flex-col-stretch sm:flex-1-hidden card-wrapper"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<AButton type="primary" @click="handleAdd">
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
|
新增
|
||||||
|
</AButton>
|
||||||
|
<TableHeaderOperation
|
||||||
|
v-model:columns="columnChecks"
|
||||||
|
:loading="loading"
|
||||||
|
:show-delete="false"
|
||||||
|
:show-add="false"
|
||||||
|
@refresh="getData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<ATable
|
||||||
|
ref="wrapperEl"
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="data"
|
||||||
|
:loading="loading"
|
||||||
|
row-key="id"
|
||||||
|
size="small"
|
||||||
|
:pagination="mobilePagination"
|
||||||
|
:scroll="scrollConfig"
|
||||||
|
class="h-full"
|
||||||
|
/>
|
||||||
|
</ACard>
|
||||||
|
</div>
|
||||||
|
</SimpleScrollbar>
|
||||||
|
|
||||||
|
<!-- 新增套餐弹窗 -->
|
||||||
|
<AModal
|
||||||
|
v-model:open="showModal"
|
||||||
|
title="新增套餐"
|
||||||
|
@ok="handleOk"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<AForm
|
||||||
|
ref="formRef"
|
||||||
|
:model="formState"
|
||||||
|
:rules="rules"
|
||||||
|
:label-col="{ span: 6 }"
|
||||||
|
:wrapper-col="{ span: 16 }"
|
||||||
|
>
|
||||||
|
<AFormItem label="套餐名称" name="packageName">
|
||||||
|
<AInput v-model:value="formState.packageName" placeholder="请输入套餐名称" />
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="计费周期" required>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<AFormItem name="periodNum" class="mb-0 flex-1">
|
||||||
|
<AInputNumber
|
||||||
|
v-model:value="formState.periodNum"
|
||||||
|
placeholder="请输入周期数"
|
||||||
|
:min="1"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</AFormItem>
|
||||||
|
<AFormItem name="periodType" class="mb-0 flex-1">
|
||||||
|
<ASelect v-model:value="formState.periodType" :options="periodOptions" />
|
||||||
|
</AFormItem>
|
||||||
|
</div>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="价格" name="price">
|
||||||
|
<AInputNumber
|
||||||
|
v-model:value="formState.price"
|
||||||
|
placeholder="请输入价格"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
addon-after="元"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="流量限制" name="traffic">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<ASwitch v-model:checked="formState.trafficEnable" />
|
||||||
|
<div v-if="formState.trafficEnable" class="flex gap-2 flex-1">
|
||||||
|
<AInputNumber
|
||||||
|
v-model:value="formState.traffic"
|
||||||
|
placeholder="请输入流量"
|
||||||
|
:min="1"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<ASelect
|
||||||
|
v-model:value="formState.trafficUnit"
|
||||||
|
:options="storageUnits.map(unit => ({ label: unit, value: unit }))"
|
||||||
|
style="width: 100px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="带宽限制" name="rateLimitId">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<ASwitch v-model:checked="formState.rateLimitEnable" />
|
||||||
|
<ASelect
|
||||||
|
v-if="formState.rateLimitEnable"
|
||||||
|
v-model:value="formState.rateLimitId"
|
||||||
|
placeholder="请选择带宽限速配置"
|
||||||
|
:options="rateLimitOptions"
|
||||||
|
:loading="!rateLimitOptions.length"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="时长限制" name="duration">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<ASwitch v-model:checked="formState.durationEnable" />
|
||||||
|
<div v-if="formState.durationEnable" class="flex gap-2 flex-1">
|
||||||
|
<AInputNumber
|
||||||
|
v-model:value="formState.duration"
|
||||||
|
placeholder="请输入时长"
|
||||||
|
:min="1"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<ASelect
|
||||||
|
v-model:value="formState.durationUnit"
|
||||||
|
:options="timeUnits.map(unit => ({ label: unit, value: unit }))"
|
||||||
|
style="width: 100px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="设备数限制" name="clientNum">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<ASwitch v-model:checked="formState.clientNumEnable" />
|
||||||
|
<AInputNumber
|
||||||
|
v-if="formState.clientNumEnable"
|
||||||
|
v-model:value="formState.clientNum"
|
||||||
|
placeholder="请输入设备数"
|
||||||
|
:min="1"
|
||||||
|
addon-after="台"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="套餐启用">
|
||||||
|
<ASwitch v-model:checked="formState.packageEnable" />
|
||||||
|
</AFormItem>
|
||||||
|
</AForm>
|
||||||
|
</AModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { useTable } from '@/hooks/common/table';
|
||||||
|
import { SimpleScrollbar } from '~/packages/materials/src';
|
||||||
|
import { computed, shallowRef, ref, onMounted } from 'vue';
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
import { fetchPackageList, addPackage, fetchRateLimitList, updatePackage, deletePackage } from '@/service/api/auth';
|
||||||
|
import { Button as AButton, message, Modal, Form as AForm, Input as AInput, InputNumber as AInputNumber, Select as ASelect, Switch as ASwitch } from 'ant-design-vue';
|
||||||
|
import { PlusOutlined,FormOutlined,DeleteOutlined } from '@ant-design/icons-vue';
|
||||||
|
import type { Rule } from 'ant-design-vue/es/form';
|
||||||
|
import {
|
||||||
|
formatBandwidth,
|
||||||
|
formatStorage,
|
||||||
|
formatTime,
|
||||||
|
convertStorage,
|
||||||
|
convertTime,
|
||||||
|
storageUnits,
|
||||||
|
timeUnits,
|
||||||
|
type StorageUnit,
|
||||||
|
type TimeUnit
|
||||||
|
} from '@/utils/units';
|
||||||
|
|
||||||
|
const wrapperEl = shallowRef<HTMLElement | null>(null);
|
||||||
|
const { height: wrapperElHeight } = useElementSize(wrapperEl);
|
||||||
|
|
||||||
|
const scrollConfig = computed(() => {
|
||||||
|
return {
|
||||||
|
y: wrapperElHeight.value - 72,
|
||||||
|
x: 1000
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const periodMap: Record<number, string> = {
|
||||||
|
0: '小时',
|
||||||
|
1: '天',
|
||||||
|
2: '月',
|
||||||
|
3: '年'
|
||||||
|
};
|
||||||
|
|
||||||
|
const { columns, columnChecks, data, loading, getData, mobilePagination } = useTable({
|
||||||
|
apiFn: async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetchPackageList();
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
rows: response.data || [],
|
||||||
|
total: response.data?.length || 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
rows: [],
|
||||||
|
total: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
rowKey: 'id',
|
||||||
|
columns: (): AntDesign.TableColumn<Api.Auth.Package>[] => [
|
||||||
|
{
|
||||||
|
key: 'packageName',
|
||||||
|
dataIndex: 'packageName',
|
||||||
|
title: '套餐名称',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'periodNum',
|
||||||
|
dataIndex: 'periodNum',
|
||||||
|
title: '计费周期',
|
||||||
|
align: 'center',
|
||||||
|
customRender: ({ record }) => {
|
||||||
|
const { periodNum, periodType } = record;
|
||||||
|
const unit = periodMap[periodType as number] || '';
|
||||||
|
return `${periodNum}${unit}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'price',
|
||||||
|
dataIndex: 'price',
|
||||||
|
title: '价格',
|
||||||
|
align: 'center',
|
||||||
|
customRender: ({ text }) => {
|
||||||
|
if (typeof text === 'number') {
|
||||||
|
return `¥${text.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
return text || '-';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'traffic',
|
||||||
|
dataIndex: 'traffic',
|
||||||
|
title: '流量限制',
|
||||||
|
align: 'center',
|
||||||
|
customRender: ({ text, record }) => {
|
||||||
|
if (!record.trafficEnable) return '无限制';
|
||||||
|
const { value, unit } = formatStorage(text);
|
||||||
|
return `${value} ${unit}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rateLimitId',
|
||||||
|
dataIndex: 'rateLimitId',
|
||||||
|
title: '带宽限制',
|
||||||
|
align: 'center',
|
||||||
|
customRender: ({ record }) => {
|
||||||
|
if (!record.rateLimitEnable) return '无限制';
|
||||||
|
const rateLimit = rateLimitData.value.find(item => item.id === record.rateLimitId);
|
||||||
|
if (!rateLimit) return '-';
|
||||||
|
const upLimit = formatBandwidth(rateLimit.upLimit);
|
||||||
|
const downLimit = formatBandwidth(rateLimit.downLimit);
|
||||||
|
return `${rateLimit.rateLimitName} (上行:${upLimit.value} ${upLimit.unit}/下行:${downLimit.value} ${downLimit.unit})`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'duration',
|
||||||
|
dataIndex: 'duration',
|
||||||
|
title: '时长限制',
|
||||||
|
align: 'center',
|
||||||
|
customRender: ({ text, record }) => {
|
||||||
|
if (!record.durationEnable) return '无限制';
|
||||||
|
const { value, unit } = formatTime(text);
|
||||||
|
return `${value} ${unit}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clientNum',
|
||||||
|
dataIndex: 'clientNum',
|
||||||
|
title: '设备数限制',
|
||||||
|
align: 'center',
|
||||||
|
customRender: ({ text, record }) => {
|
||||||
|
if (!record.clientNumEnable) return '无限制';
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'operate',
|
||||||
|
title: '操作',
|
||||||
|
align: 'center',
|
||||||
|
width: 200,
|
||||||
|
customRender: ({ record }) => (
|
||||||
|
<div class="flex justify-center gap-2">
|
||||||
|
<AButton type="link" onClick={() => handleEdit(record)}>
|
||||||
|
<FormOutlined />
|
||||||
|
</AButton>
|
||||||
|
<AButton type="link" danger onClick={() => handleDelete(record)}>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</AButton>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const showModal = ref(false);
|
||||||
|
const formRef = ref();
|
||||||
|
|
||||||
|
interface PackageForm extends Omit<Api.Auth.PackageAdd, 'traffic' | 'duration'> {
|
||||||
|
traffic: number;
|
||||||
|
trafficUnit: StorageUnit;
|
||||||
|
duration: number;
|
||||||
|
durationUnit: TimeUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formState = ref<PackageForm>({
|
||||||
|
packageName: '',
|
||||||
|
periodNum: 1,
|
||||||
|
periodType: 2,
|
||||||
|
price: 0,
|
||||||
|
trafficEnable: false,
|
||||||
|
traffic: 0,
|
||||||
|
trafficUnit: 'GB',
|
||||||
|
rateLimitEnable: false,
|
||||||
|
rateLimitId: undefined,
|
||||||
|
durationEnable: false,
|
||||||
|
duration: 0,
|
||||||
|
durationUnit: '小时',
|
||||||
|
clientNumEnable: false,
|
||||||
|
clientNum: 0,
|
||||||
|
packageEnable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules: Record<string, Rule[]> = {
|
||||||
|
packageName: [{ required: true, message: '请输入套餐名称', trigger: 'blur' }],
|
||||||
|
periodNum: [{ required: true, message: '请输入计费周期数', trigger: 'blur' }],
|
||||||
|
periodType: [{ required: true, message: '请选择计费周期单位', trigger: 'change' }],
|
||||||
|
price: [{ required: true, message: '请输入价格', trigger: 'blur' }],
|
||||||
|
traffic: [
|
||||||
|
{
|
||||||
|
validator: (_rule: Rule, value: number) => {
|
||||||
|
if (!formState.value.trafficEnable) return Promise.resolve();
|
||||||
|
if (!value || value <= 0) return Promise.reject('请输入大于0的流量值');
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rateLimitId: [
|
||||||
|
{
|
||||||
|
validator: (_rule: Rule, value: number) => {
|
||||||
|
if (!formState.value.rateLimitEnable) return Promise.resolve();
|
||||||
|
if (!value) return Promise.reject('请选择带宽限速配置');
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
duration: [
|
||||||
|
{
|
||||||
|
validator: (_rule: Rule, value: number) => {
|
||||||
|
if (!formState.value.durationEnable) return Promise.resolve();
|
||||||
|
if (!value || value <= 0) return Promise.reject('请输入大于0的时长值');
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
clientNum: [
|
||||||
|
{
|
||||||
|
validator: (_rule: Rule, value: number) => {
|
||||||
|
if (!formState.value.clientNumEnable) return Promise.resolve();
|
||||||
|
if (!value || value <= 0) return Promise.reject('请输入大于0的设备数');
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const periodOptions = [
|
||||||
|
{ label: '小时', value: 0 },
|
||||||
|
{ label: '天', value: 1 },
|
||||||
|
{ label: '月', value: 2 },
|
||||||
|
{ label: '年', value: 3 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const rateLimitOptions = ref<{ label: string; value: number }[]>([]);
|
||||||
|
|
||||||
|
const rateLimitData = ref<Api.Auth.RateLimit[]>([]);
|
||||||
|
|
||||||
|
const getRateLimitData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetchRateLimitList();
|
||||||
|
rateLimitData.value = response.data || [];
|
||||||
|
rateLimitOptions.value = rateLimitData.value.map(item => {
|
||||||
|
const upLimit = formatBandwidth(item.upLimit);
|
||||||
|
const downLimit = formatBandwidth(item.downLimit);
|
||||||
|
return {
|
||||||
|
label: `${item.rateLimitName} (上行:${upLimit.value} ${upLimit.unit}/下行:${downLimit.value} ${downLimit.unit})`,
|
||||||
|
value: item.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取带宽限速配置失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑状态
|
||||||
|
const isEdit = ref(false);
|
||||||
|
const editId = ref<string>();
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
isEdit.value = false;
|
||||||
|
editId.value = undefined;
|
||||||
|
showModal.value = true;
|
||||||
|
getRateLimitData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
showModal.value = false;
|
||||||
|
isEdit.value = false;
|
||||||
|
editId.value = undefined;
|
||||||
|
formState.value = {
|
||||||
|
packageName: '',
|
||||||
|
periodNum: 1,
|
||||||
|
periodType: 2,
|
||||||
|
price: 0,
|
||||||
|
trafficEnable: false,
|
||||||
|
traffic: 0,
|
||||||
|
trafficUnit: 'GB',
|
||||||
|
rateLimitEnable: false,
|
||||||
|
rateLimitId: undefined,
|
||||||
|
durationEnable: false,
|
||||||
|
duration: 0,
|
||||||
|
durationUnit: '小时',
|
||||||
|
clientNumEnable: false,
|
||||||
|
clientNum: 0,
|
||||||
|
packageEnable: true
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate();
|
||||||
|
const submitData = {
|
||||||
|
...formState.value,
|
||||||
|
traffic: formState.value.trafficEnable
|
||||||
|
? Math.round(convertStorage(formState.value.traffic, formState.value.trafficUnit, 'KB'))
|
||||||
|
: 0,
|
||||||
|
duration: formState.value.durationEnable
|
||||||
|
? Math.round(convertTime(formState.value.duration, formState.value.durationUnit, '秒'))
|
||||||
|
: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEdit.value) {
|
||||||
|
if (!editId.value) {
|
||||||
|
message.error('编辑ID不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 编辑模式
|
||||||
|
await updatePackage({
|
||||||
|
...submitData,
|
||||||
|
id: editId.value
|
||||||
|
});
|
||||||
|
message.success('修改成功');
|
||||||
|
} else {
|
||||||
|
// 新增模式
|
||||||
|
await addPackage(submitData);
|
||||||
|
message.success('添加成功');
|
||||||
|
}
|
||||||
|
handleCancel();
|
||||||
|
getData();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(isEdit.value ? '修改失败' : '添加失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = async (record: Api.Auth.Package) => {
|
||||||
|
isEdit.value = true;
|
||||||
|
editId.value = record.id;
|
||||||
|
showModal.value = true;
|
||||||
|
await getRateLimitData();
|
||||||
|
|
||||||
|
const traffic = formatStorage(record.traffic);
|
||||||
|
const duration = formatTime(record.duration);
|
||||||
|
|
||||||
|
formState.value = {
|
||||||
|
packageName: record.packageName,
|
||||||
|
periodNum: record.periodNum,
|
||||||
|
periodType: record.periodType,
|
||||||
|
price: record.price,
|
||||||
|
trafficEnable: record.trafficEnable,
|
||||||
|
traffic: traffic.value,
|
||||||
|
trafficUnit: traffic.unit,
|
||||||
|
rateLimitEnable: record.rateLimitEnable,
|
||||||
|
rateLimitId: record.rateLimitId,
|
||||||
|
durationEnable: record.durationEnable,
|
||||||
|
duration: duration.value,
|
||||||
|
durationUnit: duration.unit,
|
||||||
|
clientNumEnable: record.clientNumEnable,
|
||||||
|
clientNum: record.clientNum,
|
||||||
|
packageEnable: record.packageEnable
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (record: Api.Auth.Package) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: '是否确认删除该套餐?',
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
okType: 'danger',
|
||||||
|
async onOk() {
|
||||||
|
try {
|
||||||
|
await deletePackage(record.id);
|
||||||
|
message.success('删除成功');
|
||||||
|
getData();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getRateLimitData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.h-full {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user