2
0

feat:套餐管理界面(已联调)

This commit is contained in:
zhongzm
2024-12-24 16:51:41 +08:00
parent f98fdf7e27
commit 756f42a92f
4 changed files with 688 additions and 0 deletions

View 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>