增加租户套餐

This commit is contained in:
2026-09-15 17:28:08 +08:00
parent a82b300b1a
commit 71a14c46f9
37 changed files with 3855 additions and 178 deletions
@@ -0,0 +1,389 @@
<template>
<div class="tenant-package-tab" v-loading="loading">
<el-descriptions :column="2" border class="quota-desc">
<el-descriptions-item label="当前套餐">
<el-select
v-model="selectedPackageId"
placeholder="请选择套餐"
size="small"
style="width: 220px"
@change="handleSetPackage"
>
<el-option
v-for="p in packageOptions"
:key="p.id"
:label="p.name"
:value="p.id"
/>
</el-select>
</el-descriptions-item>
<el-descriptions-item label="开通功能">
<template v-if="info.module_names && info.module_names.length">
<el-tag v-for="(m, i) in info.module_names" :key="i" size="small" class="module-tag">
{{ m }}
</el-tag>
</template>
<span v-else class="muted">未配置功能</span>
</el-descriptions-item>
<el-descriptions-item label="用户数使用">
<span class="quota-text">
<b :class="{ danger: info.remaining <= 0 }">{{ info.used }}</b> / {{ info.quota }} 人
</span>
<el-progress
:percentage="usedPercent"
:status="info.remaining <= 0 ? 'exception' : undefined"
:stroke-width="8"
class="quota-progress"
/>
<span class="muted">剩余 {{ info.remaining }} 个名额</span>
</el-descriptions-item>
<el-descriptions-item label="增购单价">
<span>{{ formatMoney(info.extra_user_price) }}/人</span>
<el-button type="primary" link class="ml8" @click="openRecharge">增购用户数</el-button>
</el-descriptions-item>
</el-descriptions>
<el-alert
v-if="info.remaining <= 0"
type="warning"
show-icon
:closable="false"
class="quota-alert"
title="该租户用户数已达上限,新增账号前请先增购用户数"
/>
<div class="section-title">增购记录</div>
<el-table :data="orders" style="width: 100%" size="small" v-loading="ordersLoading">
<el-table-column prop="id" label="ID" width="70" align="center" />
<el-table-column label="增购方式" width="110" align="center">
<template #default="{ row }">
<el-tag size="small" :type="Number(row.type) === 2 ? 'success' : 'info'" effect="plain">
{{ Number(row.type) === 2 ? "套餐增购" : "单个增购" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="package_name" label="规格" min-width="120" align="center" />
<el-table-column label="用户数" width="90" align="center">
<template #default="{ row }">+{{ row.user_count }}</template>
</el-table-column>
<el-table-column label="单价" width="110" align="center">
<template #default="{ row }">{{ formatMoney(row.unit_price) }}</template>
</el-table-column>
<el-table-column label="金额" width="110" align="center">
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
</el-table-column>
<el-table-column label="上限变化" width="130" align="center">
<template #default="{ row }">
{{ row.quota_before }} → <b>{{ row.quota_after }}</b>
</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="120" align="center" />
<el-table-column label="时间" min-width="160" align="center">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
</el-table>
<!-- 增购用户数 -->
<el-dialog v-model="rechargeVisible" title="增购用户数" width="520px" destroy-on-close>
<el-form :model="rechargeForm" label-width="100px">
<el-form-item label="增购方式">
<el-radio-group v-model="rechargeForm.type">
<el-radio :label="1">单个增购</el-radio>
<el-radio :label="2">按套餐增购</el-radio>
</el-radio-group>
</el-form-item>
<template v-if="rechargeForm.type === 1">
<el-form-item label="增购数量">
<el-input-number
v-model="rechargeForm.user_count"
:min="1"
:max="9999"
controls-position="right"
/>
<span class="field-tip">个用户</span>
</el-form-item>
<el-form-item label="单价">
<el-input-number
v-model="rechargeForm.unit_price"
:min="0"
:max="999999"
:precision="2"
controls-position="right"
/>
<span class="field-tip">元/人(默认为套餐设置的单用户增购价)</span>
</el-form-item>
</template>
<template v-else>
<el-form-item label="加购套餐">
<el-select v-model="rechargeForm.quota_package_id" placeholder="请选择" style="width: 100%">
<el-option
v-for="q in info.quota_packages || []"
:key="q.id"
:label="`${q.name}(${q.user_count}人 / ${formatMoney(q.price)})`"
:value="q.id"
/>
</el-select>
</el-form-item>
</template>
<el-form-item label="应付金额">
<span class="amount-text">{{ formatMoney(rechargeAmount) }}</span>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="rechargeForm.remark" maxlength="200" placeholder="选填,如:客户已付款" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="rechargeVisible = false">取消</el-button>
<el-button type="primary" :loading="recharging" @click="submitRecharge">确认增购</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { formatDateTime } from "@/utils/datetime";
import {
getTenantQuotaInfo,
getTenantQuotaOrders,
getTenantPackageSelectList,
setTenantPackage,
rechargeTenantQuota,
} from "@/api/tenantPackage";
const props = defineProps<{
/** 当前租户 ID,为空时不请求 */
tid: number | null;
}>();
const loading = ref(false);
const ordersLoading = ref(false);
const recharging = ref(false);
const info = ref<any>({});
const orders = ref<any[]>([]);
const packageOptions = ref<any[]>([]);
const selectedPackageId = ref<number | undefined>(undefined);
const rechargeVisible = ref(false);
const rechargeForm = ref({
type: 1,
user_count: 1,
unit_price: 0,
quota_package_id: undefined as number | undefined,
remark: "",
});
const formatMoney = (v: any) => `¥${Number(v || 0).toFixed(2)}`;
const usedPercent = computed(() => {
const quota = Number(info.value.quota || 0);
const used = Number(info.value.used || 0);
if (quota <= 0) return 0;
return Math.min(100, Math.round((used / quota) * 100));
});
const rechargeAmount = computed(() => {
if (Number(rechargeForm.value.type) === 2) {
const pkg = (info.value.quota_packages || []).find(
(q: any) => Number(q.id) === Number(rechargeForm.value.quota_package_id)
);
return Number(pkg?.price || 0);
}
return Number(rechargeForm.value.user_count || 0) * Number(rechargeForm.value.unit_price || 0);
});
const loadInfo = async () => {
const tid = props.tid;
if (tid == null) return;
loading.value = true;
try {
const res = await getTenantQuotaInfo(tid);
if (res.code === 200) {
info.value = res.data || {};
selectedPackageId.value = Number(info.value.package_id || 0) || undefined;
} else {
ElMessage.error(res.msg || "获取租户套餐信息失败");
}
} catch (e: any) {
ElMessage.error(e?.message || "获取租户套餐信息失败");
} finally {
loading.value = false;
}
};
const loadOrders = async () => {
const tid = props.tid;
if (tid == null) return;
ordersLoading.value = true;
try {
const res = await getTenantQuotaOrders(tid);
if (res.code === 200) {
orders.value = res.data?.list || [];
}
} catch {
orders.value = [];
} finally {
ordersLoading.value = false;
}
};
const loadPackageOptions = async () => {
try {
const res = await getTenantPackageSelectList();
if (res.code === 200) {
packageOptions.value = res.data || [];
}
} catch {
packageOptions.value = [];
}
};
const refresh = () => {
loadInfo();
loadOrders();
};
watch(
() => props.tid,
(val) => {
if (val != null) {
refresh();
if (packageOptions.value.length === 0) loadPackageOptions();
}
},
{ immediate: true }
);
const handleSetPackage = async (id: number) => {
const tid = props.tid;
if (tid == null || !id) return;
try {
await ElMessageBox.confirm(
"调整套餐会改变该租户可用的功能模块(用户数上限只增不减),确定继续吗?",
"提示",
{ type: "warning" }
);
} catch {
// 取消时回滚选择
selectedPackageId.value = Number(info.value.package_id || 0) || undefined;
return;
}
try {
const res = await setTenantPackage({ tid, package_id: id, sync_quota: true });
if (res.code === 200) {
ElMessage.success("套餐设置成功");
refresh();
} else {
ElMessage.error(res.msg || "设置失败");
selectedPackageId.value = Number(info.value.package_id || 0) || undefined;
}
} catch (e: any) {
ElMessage.error(e?.message || "设置失败");
}
};
const openRecharge = () => {
rechargeForm.value = {
type: 1,
user_count: 1,
unit_price: Number(info.value.extra_user_price || 0),
quota_package_id: (info.value.quota_packages || [])[0]?.id,
remark: "",
};
rechargeVisible.value = true;
};
const submitRecharge = async () => {
const tid = props.tid;
if (tid == null) return;
const payload: any = {
tid,
type: rechargeForm.value.type,
remark: rechargeForm.value.remark,
};
if (Number(rechargeForm.value.type) === 2) {
if (!rechargeForm.value.quota_package_id) {
ElMessage.warning("请选择加购套餐");
return;
}
payload.quota_package_id = rechargeForm.value.quota_package_id;
} else {
if (Number(rechargeForm.value.user_count) <= 0) {
ElMessage.warning("请输入增购数量");
return;
}
payload.user_count = rechargeForm.value.user_count;
payload.unit_price = rechargeForm.value.unit_price;
}
recharging.value = true;
try {
const res = await rechargeTenantQuota(payload);
if (res.code === 200) {
ElMessage.success(`增购成功,本次增加 ${res.data?.add || 0} 个用户数`);
rechargeVisible.value = false;
refresh();
} else {
ElMessage.error(res.msg || "增购失败");
}
} catch (e: any) {
ElMessage.error(e?.message || "增购失败");
} finally {
recharging.value = false;
}
};
defineExpose({ refresh });
</script>
<style scoped>
.quota-desc {
margin-bottom: 12px;
}
.module-tag {
margin: 0 6px 6px 0;
}
.quota-text b {
font-size: 16px;
}
.quota-text b.danger {
color: var(--el-color-danger);
}
.quota-progress {
margin: 6px 0 2px;
max-width: 220px;
}
.muted {
color: var(--el-text-color-secondary);
font-size: 12px;
}
.ml8 {
margin-left: 8px;
}
.quota-alert {
margin-bottom: 12px;
}
.section-title {
margin: 16px 0 8px;
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.amount-text {
font-size: 16px;
font-weight: 600;
color: var(--el-color-danger);
}
.field-tip {
margin-left: 10px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
</style>