完善购买和套餐功能
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user