595 lines
18 KiB
Vue
595 lines
18 KiB
Vue
<template>
|
|
<div class="h-full">
|
|
<SimpleScrollbar>
|
|
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
|
<ACard
|
|
:title="t('page.package.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">
|
|
<TableHeaderOperation
|
|
v-model:columns="columnChecks"
|
|
:loading="loading"
|
|
:show-delete="false"
|
|
@add="handleAdd"
|
|
@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="t('page.package.add')"
|
|
@ok="handleOk"
|
|
@cancel="handleCancel"
|
|
width="600px"
|
|
>
|
|
<AForm
|
|
ref="formRef"
|
|
:model="formState"
|
|
:rules="rules"
|
|
:label-col="{ span: 6 }"
|
|
:wrapper-col="{ span: 16 }"
|
|
>
|
|
<AFormItem :label="t('page.package.packagename')" name="packageName">
|
|
<AInput v-model:value="formState.packageName" :placeholder="t('page.package.plepackagename')" />
|
|
</AFormItem>
|
|
|
|
<AFormItem :label="t('page.package.period')" required>
|
|
<div class="flex gap-2">
|
|
<AFormItem name="periodNum" class="mb-0 flex-1">
|
|
<AInputNumber
|
|
v-model:value="formState.periodNum"
|
|
:placeholder="t('page.package.pleperiod')"
|
|
: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="t('page.package.price')" name="price" :rules="[{ required: true, message: t('page.package.pleprice') }]">
|
|
<AInputNumber
|
|
v-model:value="formState.price"
|
|
:placeholder="t('page.package.pleprice')"
|
|
:min="0.01"
|
|
:precision="2"
|
|
style="width: 100%"
|
|
:addon-before="currencyStore.symbol"
|
|
/>
|
|
</AFormItem>
|
|
|
|
<AFormItem :label="t('page.package.traffic')" 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="t('page.package.pletraffic')"
|
|
:min="1"
|
|
:max="1024"
|
|
:precision="0"
|
|
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="t('page.package.limit')" 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="t('page.package.plelimit')"
|
|
:options="rateLimitOptions"
|
|
:loading="!rateLimitOptions.length"
|
|
style="width: 100%"
|
|
/>
|
|
</div>
|
|
</AFormItem>
|
|
|
|
<AFormItem :label="t('page.package.duration')" 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="t('page.package.pleduration')"
|
|
:min="1"
|
|
style="width: 100%"
|
|
/>
|
|
<ASelect
|
|
v-model:value="formState.durationUnit"
|
|
:options="timeUnits.map(item => ({
|
|
label: item.label,
|
|
value: item.key }))"
|
|
style="width: 100px"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</AFormItem>
|
|
|
|
<AFormItem :label="t('page.package.client')" 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="t('page.package.pleclient')"
|
|
:min="1"
|
|
:addon-after="t('page.package.unit')"
|
|
style="width: 100%"
|
|
/>
|
|
</div>
|
|
</AFormItem>
|
|
|
|
<AFormItem :label="t('page.package.usepackage')">
|
|
<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, Tag as ATag } from 'ant-design-vue';
|
|
import type { Rule } from 'ant-design-vue/es/form';
|
|
import { useCurrencyStore } from '@/views/billing/rule/modules/currency';
|
|
import {
|
|
formatBandwidth,
|
|
formatStorage,
|
|
useFormatTime,
|
|
convertStorage,
|
|
convertTime,
|
|
storageUnits,
|
|
useTimeUnits,
|
|
type StorageUnit,
|
|
type TimeUnit
|
|
} from '@/utils/units';
|
|
import { useI18n } from 'vue-i18n';
|
|
const { t } = useI18n();
|
|
const timeUnits = useTimeUnits();
|
|
const formatTime = useFormatTime();
|
|
const wrapperEl = shallowRef<HTMLElement | null>(null);
|
|
const { height: wrapperElHeight } = useElementSize(wrapperEl);
|
|
const currencyStore = useCurrencyStore();
|
|
|
|
const scrollConfig = computed(() => {
|
|
return {
|
|
y: wrapperElHeight.value - 72,
|
|
x: 1000
|
|
};
|
|
});
|
|
|
|
const periodMap: Record<number, string> = {
|
|
1: t('page.package.day'),
|
|
2: t('page.package.month'),
|
|
3: t('page.package.year')
|
|
};
|
|
|
|
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: t('page.package.packagename'),
|
|
align: 'center'
|
|
},
|
|
{
|
|
key: 'periodNum',
|
|
dataIndex: 'periodNum',
|
|
title: t('page.package.period'),
|
|
align: 'center',
|
|
customRender: ({ record }) => {
|
|
const { periodNum, periodType } = record;
|
|
const unit = periodMap[periodType as number] || '';
|
|
return `${periodNum}${unit}`;
|
|
}
|
|
},
|
|
{
|
|
key: 'price',
|
|
dataIndex: 'price',
|
|
title: t('page.package.price')+currencyStore.symbol,
|
|
align: 'center',
|
|
customRender: ({ text }) => `${Number(text).toFixed(2)} ${currencyStore.symbol}`
|
|
},
|
|
{
|
|
key: 'traffic',
|
|
dataIndex: 'traffic',
|
|
title: t('page.package.traffic'),
|
|
align: 'center',
|
|
customRender: ({ text, record }) => {
|
|
if (!record.trafficEnable) return t('page.package.unlimit');
|
|
const { value, unit } = formatStorage(text);
|
|
return `${value} ${unit}`;
|
|
}
|
|
},
|
|
{
|
|
key: 'rateLimitId',
|
|
dataIndex: 'rateLimitId',
|
|
title: t('page.package.limit'),
|
|
align: 'center',
|
|
customRender: ({ record }) => {
|
|
if (!record.rateLimitEnable) return t('page.package.unlimit');
|
|
const rateLimit = rateLimitData.value.find(item => item.id === record.rateLimitId);
|
|
if (!rateLimit) return '-';
|
|
const upLimitText = rateLimit.upLimitEnable
|
|
? `${t('page.package.up')}:${formatBandwidth(rateLimit.upLimit).value} ${formatBandwidth(rateLimit.upLimit).unit}`
|
|
: `${t('page.package.up')}:${t('page.package.unlimit')}`;
|
|
const downLimitText = rateLimit.downLimitEnable
|
|
? `${t('page.package.down')}:${formatBandwidth(rateLimit.downLimit).value} ${formatBandwidth(rateLimit.downLimit).unit}`
|
|
: `${t('page.package.down')}:${t('page.package.unlimit')}`;
|
|
return `${rateLimit.rateLimitName} (${upLimitText}/${downLimitText})`;
|
|
}
|
|
},
|
|
{
|
|
key: 'duration',
|
|
dataIndex: 'duration',
|
|
title: t('page.package.duration'),
|
|
align: 'center',
|
|
customRender: ({ text, record }) => {
|
|
if (!record.durationEnable) return t('page.package.unlimit');
|
|
const { value, unit } = formatTime(text);
|
|
return `${value} ${unit}`;
|
|
}
|
|
},
|
|
{
|
|
key: 'clientNum',
|
|
dataIndex: 'clientNum',
|
|
title: t('page.package.client'),
|
|
align: 'center',
|
|
customRender: ({ text, record }) => {
|
|
if (!record.clientNumEnable) return t('page.package.unlimit');
|
|
return text;
|
|
}
|
|
},
|
|
{
|
|
key: 'packageEnable',
|
|
dataIndex: 'packageEnable',
|
|
title: t('page.package.status'),
|
|
align: 'center',
|
|
customRender: ({ text }) => (
|
|
<ATag color={text ? 'success' : 'error'}>
|
|
{text ? t('page.package.use') : t('page.package.unuse')}
|
|
</ATag>
|
|
)
|
|
},
|
|
{
|
|
key: 'operate',
|
|
title: t('page.package.operate'),
|
|
align: 'center',
|
|
width: 200,
|
|
customRender: ({ record }) => (
|
|
<div class="flex justify-center gap-2">
|
|
<AButton type="link" onClick={() => handleEdit(record)}>
|
|
{t('page.package.edit')}
|
|
</AButton>
|
|
<AButton type="link" danger onClick={() => handleDelete(record)}>
|
|
{t('page.package.delete')}
|
|
</AButton>
|
|
</div>
|
|
)
|
|
}
|
|
]
|
|
});
|
|
|
|
const showModal = ref(false);
|
|
const formRef = ref();
|
|
|
|
interface PackageForm {
|
|
id?: string;
|
|
packageName: string;
|
|
periodNum: number;
|
|
periodType: number;
|
|
price: string;
|
|
trafficEnable: boolean;
|
|
traffic: number;
|
|
trafficUnit: StorageUnit;
|
|
rateLimitEnable: boolean;
|
|
rateLimitId?: number;
|
|
durationEnable: boolean;
|
|
duration: number;
|
|
durationUnit: TimeUnit;
|
|
clientNumEnable: boolean;
|
|
clientNum: number;
|
|
packageEnable: boolean;
|
|
}
|
|
|
|
const formState = ref<PackageForm>({
|
|
packageName: '',
|
|
periodNum: 1,
|
|
periodType: 2,
|
|
price: '',
|
|
trafficEnable: false,
|
|
traffic: 0,
|
|
trafficUnit: 'GB',
|
|
rateLimitEnable: false,
|
|
rateLimitId: undefined,
|
|
durationEnable: false,
|
|
duration: 0,
|
|
durationUnit: 'hour',
|
|
clientNumEnable: false,
|
|
clientNum: 0,
|
|
packageEnable: true
|
|
});
|
|
|
|
const rules: Record<string, Rule[]> = {
|
|
packageName: [{ required: true, message: t('page.package.plepackagename'), trigger: 'blur' }],
|
|
periodNum: [{ required: true, message: t('page.package.pleperiod'), trigger: 'blur' }],
|
|
periodType: [{ required: true, message: t('page.package.pleunit'), trigger: 'change' }],
|
|
price: [{ required: true, message: t('page.package.pleprice'), trigger: 'blur' }],
|
|
traffic: [
|
|
{
|
|
validator: (_rule: Rule, value: number) => {
|
|
if (!formState.value.trafficEnable) return Promise.resolve();
|
|
if (!value || value <= 0) return Promise.reject(t('page.package.rejtraffic'));
|
|
return Promise.resolve();
|
|
},
|
|
trigger: 'blur'
|
|
}
|
|
],
|
|
rateLimitId: [
|
|
{
|
|
validator: (_rule: Rule, value: number) => {
|
|
if (!formState.value.rateLimitEnable) return Promise.resolve();
|
|
if (!value) return Promise.reject(t('page.package.plelimit'));
|
|
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(t('page.package.rejduration'));
|
|
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(t('page.package.rejclient'));
|
|
return Promise.resolve();
|
|
},
|
|
trigger: 'blur'
|
|
}
|
|
]
|
|
};
|
|
|
|
const periodOptions =computed(()=> [
|
|
{ label: t('page.package.hour'), value: 0 },
|
|
{ label: t('page.package.day'), value: 1 },
|
|
{ label: t('page.package.month'), value: 2 },
|
|
{ label: t('page.package.year'), 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 upLimitText = item.upLimitEnable
|
|
? `${t('page.package.up')}:${formatBandwidth(item.upLimit).value} ${formatBandwidth(item.upLimit).unit}`
|
|
: `${t('page.package.up')}:${t('page.package.unlimit')}`;
|
|
const downLimitText = item.downLimitEnable
|
|
? `${t('page.package.down')}:${formatBandwidth(item.downLimit).value} ${formatBandwidth(item.downLimit).unit}`
|
|
: `${t('page.package.down')}:${t('page.package.unlimit')}`;
|
|
|
|
return {
|
|
label: `${item.rateLimitName} (${upLimitText}/${downLimitText})`,
|
|
value: item.id
|
|
};
|
|
});
|
|
} catch (error) {
|
|
// console.error('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: '',
|
|
trafficEnable: false,
|
|
traffic: 0,
|
|
trafficUnit: 'GB',
|
|
rateLimitEnable: false,
|
|
rateLimitId: undefined,
|
|
durationEnable: false,
|
|
duration: 0,
|
|
durationUnit:'hour',
|
|
clientNumEnable: false,
|
|
clientNum: 0,
|
|
packageEnable: true
|
|
};
|
|
};
|
|
|
|
const handleOk = async () => {
|
|
try {
|
|
await formRef.value?.validate();
|
|
// 准备基础数据
|
|
const baseData = {
|
|
packageName: formState.value.packageName,
|
|
periodNum: formState.value.periodNum,
|
|
periodType: formState.value.periodType,
|
|
price: formState.value.price,
|
|
trafficEnable: formState.value.trafficEnable,
|
|
rateLimitEnable: formState.value.rateLimitEnable,
|
|
rateLimitId: formState.value.rateLimitId,
|
|
durationEnable: formState.value.durationEnable,
|
|
clientNumEnable: formState.value.clientNumEnable,
|
|
clientNum: formState.value.clientNum,
|
|
packageEnable: formState.value.packageEnable
|
|
};
|
|
|
|
// 计算流量和时长
|
|
const traffic = formState.value.trafficEnable
|
|
? Math.round(convertStorage(formState.value.traffic, formState.value.trafficUnit, 'B'))
|
|
: 0;
|
|
|
|
const duration = formState.value.durationEnable
|
|
? Math.round(convertTime(formState.value.duration, formState.value.durationUnit, 'second'))
|
|
: 0;
|
|
|
|
if (isEdit.value) {
|
|
if (!editId.value) {
|
|
message.error(t('page.package.isnull'));
|
|
return;
|
|
}
|
|
// 编辑模式
|
|
await updatePackage({
|
|
...baseData,
|
|
id: editId.value,
|
|
traffic,
|
|
duration
|
|
});
|
|
message.success(t('page.package.editsuc'));
|
|
} else {
|
|
// 新增模式
|
|
await addPackage({
|
|
...baseData,
|
|
traffic,
|
|
duration
|
|
});
|
|
message.success(t('page.package.addsuc'));
|
|
}
|
|
handleCancel();
|
|
getData();
|
|
} catch (error) {
|
|
// message.error(isEdit.value ? t('page.package.editerr') : t('page.package.adderr'));
|
|
}
|
|
};
|
|
|
|
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: t('page.package.confirmdelete'),
|
|
content: t('page.package.deletecontent'),
|
|
okText: t('page.package.confirm'),
|
|
cancelText: t('page.package.close'),
|
|
okType: 'danger',
|
|
async onOk() {
|
|
try {
|
|
await deletePackage(record.id);
|
|
message.success(t('page.package.deletesuc'));
|
|
getData();
|
|
} catch (error) {
|
|
// message.error(t('page.package.deleteerr'));
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
onMounted(() => {
|
|
getRateLimitData();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.h-full {
|
|
height: 100%;
|
|
}
|
|
</style>
|