2
0
Files
fe.wfc/src/views/billing/package/index.vue
2025-04-27 19:19:03 +08:00

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>