Files
yunzerwebsiteallinone/platform/src/views/basicSettings/tenantpackage/index.vue
T
2026-09-16 00:22:17 +08:00

408 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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、套餐按「有效时长」计费(默认 365 天,价格即元/年):到期后自动切换为基础套餐,续费后恢复对应功能。</p>
<p>3、所有租户都有用户数上限(默认 20 人):达到上限后无法再开账号,可在「租户管理 → 租户详情 → 套餐与用户数」中按单个(如 200 元/人)或加购套餐(如 5 人 500 元)增购。</p>
</template>
</el-alert>
<div class="purchase-url-bar">
<span class="bar-label">套餐购买链接</span>
<el-input
v-model="purchaseUrl"
placeholder="租户端未开通功能卡片「购买」按钮跳转的链接,如 https://www.example.com/package"
clearable
class="url-input"
/>
<el-button type="primary" :loading="purchaseUrlSaving" @click="handleSavePurchaseUrl">
保存
</el-button>
<span class="muted">留空时租户端点击「购买」仅提示联系管理员</span>
</div>
<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="套餐价格" width="120" align="center">
<template #default="{ row }">
<span :class="{ 'price-zero': !Number(row.price) }">{{ formatMoney(row.price) }}</span>
<span class="muted">{{ priceUnit(row) }}</span>
</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,
getPackagePurchaseUrl,
savePackagePurchaseUrl,
} 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 purchaseUrl = ref("");
const purchaseUrlSaving = ref(false);
const loadPurchaseUrl = async () => {
try {
const res = await getPackagePurchaseUrl();
if (res.code === 200) {
purchaseUrl.value = res.data?.purchase_url || "";
}
} catch (e) {
console.error("获取套餐购买链接失败:", e);
}
};
const handleSavePurchaseUrl = async () => {
purchaseUrlSaving.value = true;
try {
const res = await savePackagePurchaseUrl(purchaseUrl.value);
if (res.code === 200) {
ElMessage.success("保存成功");
} else {
ElMessage.error(res.msg || "保存失败");
}
} catch (e: any) {
ElMessage.error(e?.message || "保存失败");
} finally {
purchaseUrlSaving.value = false;
}
};
const formatMoney = (v: any) => `¥${Number(v || 0).toFixed(2)}`;
/** 价格单位随有效时长变化:365天=元/年,30=元/月,90=元/季,其余=元/N天 */
const priceUnit = (row: any) => {
const d = Number(row.duration_days || 365);
if (d === 365) return "/年";
if (d === 30) return "/月";
if (d === 90) return "/季";
return `/${d}天`;
};
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();
loadPurchaseUrl();
};
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;
}
.purchase-url-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
flex-wrap: wrap;
.bar-label {
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-primary);
}
.url-input {
width: 420px;
max-width: 100%;
}
}
.module-tag {
margin: 0 6px 6px 0;
}
.ml6 {
margin-left: 6px;
}
.muted {
color: var(--el-text-color-secondary);
font-size: 12px;
}
.price-zero {
color: var(--el-text-color-secondary);
}
:deep(.el-empty) {
padding: 4px 0;
}
</style>