408 lines
13 KiB
Vue
408 lines
13 KiB
Vue
<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>
|