完善购买和套餐功能

This commit is contained in:
2026-09-16 17:26:02 +08:00
parent 898dedd75a
commit b48688cbab
19 changed files with 554 additions and 58 deletions
+8
View File
@@ -63,3 +63,11 @@ export function activateProductOrder(id) {
method: "post",
});
}
/** 关闭已开通的单品功能(回收模块权益,可在购买订单中重新开通) */
export function closeProductOrder(id) {
return request({
url: `/platform/product/order/close/${id}`,
method: "post",
});
}
@@ -57,9 +57,16 @@
:min="0"
:max="999999"
:precision="2"
:disabled="Number(form.allow_repurchase) === 0"
controls-position="right"
/>
<span class="field-tip">元/人,单个增购用户数时的默认单价(如 200)</span>
<span class="field-tip">
{{
Number(form.allow_repurchase) === 0
? "体验套餐(仅可购买一次)不涉及用户数增购,无需设置"
: "元/人,单个增购用户数时的默认单价(如 200)"
}}
</span>
</el-form-item>
<el-form-item label="套餐价格" prop="price">
@@ -103,6 +110,11 @@
<span class="field-tip">新建租户未指定套餐时默认使用</span>
</el-form-item>
<el-form-item label="允许重复购买" prop="allow_repurchase">
<el-switch v-model="form.allow_repurchase" :active-value="1" :inactive-value="0" />
<span class="field-tip">关闭后每个租户仅可购买一次(体验套餐),已购买过则无法再次下单</span>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="1">启用</el-radio>
@@ -161,6 +173,7 @@ const emptyForm = () => ({
user_quota: 20,
extra_user_price: 200,
is_default: 0,
allow_repurchase: 1,
sort: 0,
status: 1,
remark: "",
@@ -206,6 +219,16 @@ const handleCheckAllModules = (checked: boolean) => {
form.value.modules = checked ? moduleOptions.value.map((m) => m.code) : [];
};
// 体验套餐(不允许重复购买)不涉及用户数增购:增购价自动清零
watch(
() => form.value.allow_repurchase,
(val) => {
if (Number(val) === 0) {
form.value.extra_user_price = 0;
}
}
);
const loadModuleOptions = async () => {
try {
const res = await getTenantPackageModuleOptions();
@@ -232,6 +255,7 @@ const loadDetail = async (id: number) => {
user_quota: Number(d.user_quota || 20),
extra_user_price: Number(d.extra_user_price || 0),
is_default: Number(d.is_default || 0),
allow_repurchase: Number(d.allow_repurchase ?? 1),
sort: Number(d.sort || 0),
status: Number(d.status ?? 1),
remark: d.remark || "",
@@ -20,13 +20,13 @@
<el-divider></el-divider>
<el-alert type="info" :closable="false" show-icon class="tip-alert">
<!-- <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>
</el-alert> -->
<div class="purchase-url-bar">
<span class="bar-label">套餐购买链接</span>
@@ -88,6 +88,17 @@
<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 label="重复购买" width="100" align="center">
<template #default="{ row }">
<el-tag
size="small"
:type="Number(row.allow_repurchase) === 0 ? 'warning' : 'success'"
effect="plain"
>
{{ Number(row.allow_repurchase) === 0 ? "仅一次" : "允许" }}
</el-tag>
</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 }">
@@ -101,7 +112,14 @@
<el-button size="small" type="primary" link @click="handleEditPackage(row)">
编辑
</el-button>
<el-button size="small" type="danger" link @click="handleDeletePackage(row)">
<!-- 基础套餐(code=basic)为系统内置兜底套餐,不提供删除入口 -->
<el-button
v-if="!isBasicPackage(row)"
size="small"
type="danger"
link
@click="handleDeletePackage(row)"
>
删除
</el-button>
</template>
@@ -271,6 +289,10 @@ const handleEditPackage = (row: any) => {
packageDialogVisible.value = true;
};
/** 基础套餐(code=basic)为系统内置兜底套餐(套餐到期自动降级目标),不允许删除 */
const isBasicPackage = (row: any) =>
String(row?.code || "").trim().toLowerCase() === "basic";
const handleDeletePackage = async (row: any) => {
try {
await ElMessageBox.confirm(
@@ -1,5 +1,11 @@
<template>
<div class="tenant-package-tab" v-loading="loading">
<div class="panel-toolbar">
<el-button size="small" :loading="loading" @click="refresh">
<el-icon><Refresh /></el-icon>
刷新数据
</el-button>
</div>
<el-descriptions :column="2" border class="quota-desc">
<el-descriptions-item label="当前套餐">
<el-select
@@ -32,13 +38,27 @@
<span v-else class="muted">永久有效</span>
</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 }}
<template v-if="info.modules && info.modules.length">
<el-tag v-for="(m, i) in info.modules" :key="i" size="small" class="module-tag">
{{ m.module_name || m.module_code }}
</el-tag>
</template>
<span v-else class="muted">未配置功能</span>
</el-descriptions-item>
<el-descriptions-item label="单品功能">
<template v-if="info.product_modules && info.product_modules.length">
<el-tag
v-for="(m, i) in info.product_modules"
:key="i"
size="small"
type="success"
class="module-tag"
>
{{ m.module_name || m.module_code }}
</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 }} 人
@@ -52,7 +72,10 @@
<span class="muted">剩余 {{ info.remaining }} 个名额</span>
</el-descriptions-item>
<el-descriptions-item label="增购单价">
<span>{{ formatMoney(info.extra_user_price) }}/人</span>
<template v-if="Number(info.extra_user_price) > 0">
<span>{{ formatMoney(info.extra_user_price) }}/人</span>
</template>
<span v-else class="muted">未设置</span>
<el-button type="primary" link class="ml8" @click="openRecharge">增购用户数</el-button>
</el-descriptions-item>
</el-descriptions>
@@ -74,6 +97,51 @@
title="该租户套餐即将到期,到期后将自动切换为基础套餐,请及时续费"
/>
<div class="section-title">已购功能(单品购买)</div>
<el-table :data="info.product_orders || []" style="width: 100%" size="small">
<el-table-column prop="product_name" label="产品" min-width="100" show-overflow-tooltip />
<el-table-column label="功能模块" width="150" align="center">
<template #default="{ row }">
<el-tag v-if="row.module_name || row.module_code" size="small" effect="plain">
{{ row.module_name || row.module_code }}
</el-tag>
<span v-else class="muted">未关联</span>
</template>
</el-table-column>
<el-table-column prop="order_no" label="订单号" min-width="180" show-overflow-tooltip />
<el-table-column label="金额" width="100" align="center">
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
</el-table-column>
<el-table-column label="开通时间" min-width="120" align="center">
<template #default="{ row }">
{{ row.activate_time ? formatDate(row.activate_time) : "-" }}
</template>
</el-table-column>
<el-table-column label="过期时间" min-width="180" align="center">
<template #default="{ row }">
<template v-if="row.expire_time">
<span>{{ formatDate(row.expire_time) }}</span>
<el-tag
size="small"
:type="row.expired ? 'danger' : 'success'"
effect="plain"
class="ml8"
>
{{ row.expired ? "已过期" : `剩余 ${remainingDays(row.expire_time)} 天` }}
</el-tag>
</template>
<span v-else class="muted">长期有效</span>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center">
<template #default="{ row }">
<el-button size="small" type="danger" link @click="handleCloseEntitlement(row)">
关闭功能
</el-button>
</template>
</el-table-column>
</el-table>
<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" />
@@ -171,7 +239,8 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { formatDateTime } from "@/utils/datetime";
import { Refresh } from "@element-plus/icons-vue";
import { formatDate, formatDateTime } from "@/utils/datetime";
import {
getTenantQuotaInfo,
getTenantQuotaOrders,
@@ -179,6 +248,7 @@ import {
setTenantPackage,
rechargeTenantQuota,
} from "@/api/tenantPackage";
import { closeProductOrder } from "@/api/product";
const props = defineProps<{
/** 当前租户 ID,为空时不请求 */
@@ -203,6 +273,11 @@ const rechargeForm = ref({
});
const formatMoney = (v: any) => `¥${Number(v || 0).toFixed(2)}`;
/** 距到期剩余天数(向上取整,最小 0) */
const remainingDays = (t: string) => {
const ms = new Date(t).getTime() - Date.now();
return Math.max(0, Math.ceil(ms / 86400000));
};
/** 套餐价格单位随有效时长变化:365天=元/年,30=元/月,90=元/季,其余=元/N天 */
const priceUnit = (row: any) => {
const d = Number(row?.duration_days || 365);
@@ -336,6 +411,32 @@ const handleSetPackage = async (id: number) => {
}
};
/** 关闭已开通的单品功能:订单置为已关闭,租户端对应模块权益即时回收 */
const handleCloseEntitlement = async (row: any) => {
if (!row?.order_id) return;
const label = row.module_name || row.product_name || "该";
try {
await ElMessageBox.confirm(
`关闭后该租户将立即失去「${label}」功能的使用权限(订单保留,可在「产品定价 - 购买订单」中重新开通),确定继续吗?`,
"关闭功能",
{ type: "warning", confirmButtonText: "确认关闭" }
);
} catch {
return;
}
try {
const res = await closeProductOrder(row.order_id);
if (res.code === 200) {
ElMessage.success("功能已关闭");
refresh();
} else {
ElMessage.error(res.msg || "关闭失败");
}
} catch (e: any) {
ElMessage.error(e?.message || "关闭失败");
}
};
const openRecharge = () => {
rechargeForm.value = {
type: 1,
@@ -391,6 +492,11 @@ defineExpose({ refresh });
</script>
<style scoped>
.panel-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 8px;
}
.quota-desc {
margin-bottom: 12px;
}
@@ -35,11 +35,11 @@
<el-table-column prop="name" label="姓名" min-width="120" align="center" />
<el-table-column prop="phone" label="手机号" min-width="140" align="center" />
<el-table-column prop="email" label="邮箱" min-width="180" align="center" />
<el-table-column prop="org_id" label="部门" min-width="160" align="center">
<!-- <el-table-column prop="org_id" label="部门" min-width="160" align="center">
<template #default="{ row }">
{{ orgNameMap[Number(row.org_id)] || (Number(row.org_id) ? `部门${row.org_id}` : "未分配") }}
</template>
</el-table-column>
</el-table-column> -->
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="Number(row.status) === 1 ? 'success' : 'danger'">
@@ -47,20 +47,21 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="320" align="center" fixed="right">
<el-table-column label="操作" width="280" align="center" fixed="right">
<template #default="{ row }">
<el-button text type="primary" @click="openEditDialog(row)">编辑</el-button>
<el-button text type="primary" @click="openPasswordDialog(row)">
<el-button text size="small" type="primary" @click="openEditDialog(row)">编辑</el-button>
<el-button text size="small" type="primary" @click="openPasswordDialog(row)">
修改密码
</el-button>
<el-button
text
size="small"
:type="Number(row.status) === 1 ? 'warning' : 'success'"
@click="handleToggleStatus(row)"
>
{{ Number(row.status) === 1 ? "禁用" : "启用" }}
</el-button>
<el-button text type="danger" @click="handleDelete(row)">删除</el-button>
<el-button text size="small" type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>