增加租户套餐

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,321 @@
<template>
<div class="container-box">
<div class="header-bar">
<h2>租户套餐</h2>
<div class="header-actions">
<el-button v-if="activeTab === 'package'" type="primary" @click="handleAddPackage">
<el-icon><Plus /></el-icon>
添加套餐
</el-button>
<el-button v-else type="primary" @click="handleAddQuota">
<el-icon><Plus /></el-icon>
添加加购套餐
</el-button>
<el-button @click="refresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
<el-divider></el-divider>
<el-alert type="info" :closable="false" show-icon class="tip-alert">
<template #default>
<p>1、套餐决定租户能开通哪些功能:勾选「功能模块」后,绑定该套餐的租户端只展示对应功能菜单(如 ERP 套餐开通进销存)。</p>
<p>2、所有租户都有用户数上限(默认 20 人):达到上限后无法再开账号,可在「租户管理 → 租户详情 → 套餐与用户数」中按单个(如 200 元/人)或加购套餐(如 5 人 500 元)增购。</p>
</template>
</el-alert>
<el-tabs v-model="activeTab" class="package-tabs">
<!-- 功能套餐 -->
<el-tab-pane label="功能套餐" name="package">
<el-table :data="packages" style="width: 100%" v-loading="loading">
<el-table-column prop="id" label="ID" width="70" align="center" />
<el-table-column prop="name" label="套餐名称" min-width="150" align="center">
<template #default="{ row }">
<span>{{ row.name }}</span>
<el-tag v-if="Number(row.is_default) === 1" type="success" size="small" class="ml6">
默认
</el-tag>
</template>
</el-table-column>
<el-table-column prop="code" label="套餐编码" width="120" align="center">
<template #default="{ row }">
<el-tag size="small" type="info" effect="plain">{{ row.code }}</el-tag>
</template>
</el-table-column>
<el-table-column label="开通功能" min-width="260">
<template #default="{ row }">
<el-tag
v-for="m in row.modules"
:key="m.module_code"
size="small"
class="module-tag"
>
{{ m.module_name || m.module_code }}
</el-tag>
<span v-if="!row.modules || row.modules.length === 0" class="muted">未配置功能</span>
</template>
</el-table-column>
<el-table-column prop="user_quota" label="包含用户数" width="110" align="center">
<template #default="{ row }">{{ row.user_quota }} 人</template>
</el-table-column>
<el-table-column label="单用户增购价" width="120" align="center">
<template #default="{ row }">{{ formatMoney(row.extra_user_price) }}</template>
</el-table-column>
<el-table-column prop="tenant_count" label="使用租户" width="90" align="center">
<template #default="{ row }">{{ row.tenant_count || 0 }}</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="Number(row.status) === 1 ? 'success' : 'danger'" size="small">
{{ Number(row.status) === 1 ? "启用" : "禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="handleEditPackage(row)">
编辑
</el-button>
<el-button size="small" type="danger" link @click="handleDeletePackage(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- 用户数加购套餐 -->
<el-tab-pane label="用户数加购套餐" name="quota">
<el-table :data="quotaPackages" style="width: 100%" v-loading="quotaLoading">
<el-table-column prop="id" label="ID" width="70" align="center" />
<el-table-column prop="name" label="规格名称" min-width="160" align="center" />
<el-table-column prop="user_count" label="包含用户数" width="120" align="center">
<template #default="{ row }">{{ row.user_count }} 人</template>
</el-table-column>
<el-table-column label="售价" width="140" align="center">
<template #default="{ row }">{{ formatMoney(row.price) }}</template>
</el-table-column>
<el-table-column label="折算单价" width="140" align="center">
<template #default="{ row }">{{ formatMoney(unitPrice(row)) }}/人</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="Number(row.status) === 1 ? 'success' : 'danger'" size="small">
{{ Number(row.status) === 1 ? "启用" : "禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="handleEditQuota(row)">
编辑
</el-button>
<el-button size="small" type="danger" link @click="handleDeleteQuota(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<PackageEditDialog v-model="packageDialogVisible" :pkg="currentPackage" @success="loadPackages" />
<QuotaEditDialog v-model="quotaDialogVisible" :item="currentQuota" @success="loadQuotaPackages" />
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Refresh } from "@element-plus/icons-vue";
import {
getTenantPackageList,
deleteTenantPackage,
getQuotaPackageList,
deleteQuotaPackage,
} from "@/api/tenantPackage";
import PackageEditDialog from "./components/edit.vue";
import QuotaEditDialog from "./components/quotaEdit.vue";
const activeTab = ref("package");
const loading = ref(false);
const quotaLoading = ref(false);
const packages = ref<any[]>([]);
const quotaPackages = ref<any[]>([]);
const packageDialogVisible = ref(false);
const quotaDialogVisible = ref(false);
const currentPackage = ref<any>(null);
const currentQuota = ref<any>(null);
const formatMoney = (v: any) => `¥${Number(v || 0).toFixed(2)}`;
const unitPrice = (row: any) => {
const count = Number(row.user_count || 0);
if (count <= 0) return 0;
return Number(row.price || 0) / count;
};
const loadPackages = async () => {
loading.value = true;
try {
const res = await getTenantPackageList();
if (res.code === 200) {
packages.value = res.data?.list || [];
} else {
ElMessage.error(res.msg || "获取套餐列表失败");
}
} catch (e: any) {
ElMessage.error(e?.message || "获取套餐列表失败");
} finally {
loading.value = false;
}
};
const loadQuotaPackages = async () => {
quotaLoading.value = true;
try {
const res = await getQuotaPackageList();
if (res.code === 200) {
quotaPackages.value = res.data?.list || [];
} else {
ElMessage.error(res.msg || "获取加购套餐失败");
}
} catch (e: any) {
ElMessage.error(e?.message || "获取加购套餐失败");
} finally {
quotaLoading.value = false;
}
};
const refresh = () => {
loadPackages();
loadQuotaPackages();
};
const handleAddPackage = () => {
currentPackage.value = null;
packageDialogVisible.value = true;
};
const handleEditPackage = (row: any) => {
currentPackage.value = { ...row };
packageDialogVisible.value = true;
};
const handleDeletePackage = async (row: any) => {
try {
await ElMessageBox.confirm(
`确定要删除套餐「${row.name}」吗?删除后绑定该套餐的租户将失去对应功能。`,
"警告",
{ type: "warning" }
);
} catch {
return;
}
try {
const res = await deleteTenantPackage(row.id);
if (res.code === 200) {
ElMessage.success("删除成功");
loadPackages();
} else {
ElMessage.error(res.msg || "删除失败");
}
} catch (e: any) {
ElMessage.error(e?.message || "删除失败");
}
};
const handleAddQuota = () => {
currentQuota.value = null;
quotaDialogVisible.value = true;
};
const handleEditQuota = (row: any) => {
currentQuota.value = { ...row };
quotaDialogVisible.value = true;
};
const handleDeleteQuota = async (row: any) => {
try {
await ElMessageBox.confirm(`确定要删除加购套餐「${row.name}」吗?`, "警告", {
type: "warning",
});
} catch {
return;
}
try {
const res = await deleteQuotaPackage(row.id);
if (res.code === 200) {
ElMessage.success("删除成功");
loadQuotaPackages();
} else {
ElMessage.error(res.msg || "删除失败");
}
} catch (e: any) {
ElMessage.error(e?.message || "删除失败");
}
};
onMounted(refresh);
</script>
<style scoped lang="less">
.container-box {
padding: 4px;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.header-actions {
display: flex;
gap: 12px;
}
.tip-alert {
margin-bottom: 8px;
p {
margin: 4px 0;
font-size: 13px;
line-height: 1.6;
}
}
.package-tabs {
margin-top: 8px;
}
.module-tag {
margin: 0 6px 6px 0;
}
.ml6 {
margin-left: 6px;
}
.muted {
color: var(--el-text-color-secondary);
font-size: 12px;
}
:deep(.el-empty) {
padding: 4px 0;
}
</style>