增加功能报价

This commit is contained in:
2026-09-16 00:22:17 +08:00
parent 71a14c46f9
commit 60b2658d4b
32 changed files with 3578 additions and 77 deletions
+216 -8
View File
@@ -71,16 +71,24 @@
v-for="module in basicModules"
:key="module.path"
class="module-card"
:class="{ 'is-locked': module.locked }"
@click="handleNavigate(module)"
>
<div class="card-header">
<span class="card-title">{{ module.name }}</span>
<span class="card-title">
{{ module.name }}
<el-icon v-if="module.locked" class="lock-badge"><Lock /></el-icon>
</span>
<div class="card-icon-wrapper" v-html="module.icon"></div>
</div>
<div class="card-divider"></div>
<div class="card-content">
<p class="card-desc">{{ module.description || "暂无描述" }}</p>
</div>
<div v-if="module.locked" class="card-lock-tip">
<span class="lock-text">套餐未开通</span>
<span class="buy-btn" @click.stop="handleBuy(module)">购买</span>
</div>
</div>
</div>
</div>
@@ -93,16 +101,24 @@
v-for="module in systemModules"
:key="module.path"
class="module-card"
:class="{ 'is-locked': module.locked }"
@click="handleNavigate(module)"
>
<div class="card-header">
<span class="card-title">{{ module.name }}</span>
<span class="card-title">
{{ module.name }}
<el-icon v-if="module.locked" class="lock-badge"><Lock /></el-icon>
</span>
<div class="card-icon-wrapper" v-html="module.icon"></div>
</div>
<div class="card-divider"></div>
<div class="card-content">
<p class="card-desc">{{ module.description || "暂无描述" }}</p>
</div>
<div v-if="module.locked" class="card-lock-tip">
<span class="lock-text">套餐未开通</span>
<span class="buy-btn" @click.stop="handleBuy(module)">购买</span>
</div>
</div>
</div>
</div>
@@ -115,16 +131,24 @@
v-for="module in uncategorizedModules"
:key="module.path"
class="module-card"
:class="{ 'is-locked': module.locked }"
@click="handleNavigate(module)"
>
<div class="card-header">
<span class="card-title">{{ module.name }}</span>
<span class="card-title">
{{ module.name }}
<el-icon v-if="module.locked" class="lock-badge"><Lock /></el-icon>
</span>
<div class="card-icon-wrapper" v-html="module.icon"></div>
</div>
<div class="card-divider"></div>
<div class="card-content">
<p class="card-desc">{{ module.description || "暂无描述" }}</p>
</div>
<div v-if="module.locked" class="card-lock-tip">
<span class="lock-text">套餐未开通</span>
<span class="buy-btn" @click.stop="handleBuy(module)">购买</span>
</div>
</div>
</div>
</div>
@@ -146,7 +170,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { ElMessage, ElMessageBox } from "element-plus";
import {
ArrowDown,
@@ -157,6 +181,7 @@ import {
Sunny,
Moon,
OfficeBuilding,
Lock,
} from "@element-plus/icons-vue";
import { getTenantList } from "@/api/modules";
@@ -175,21 +200,30 @@ interface ModuleItem {
status: number;
type: number;
title?: string;
locked?: boolean;
is_show?: number;
}
const router = useRouter();
const authStore = useAuthStore();
const menuStore = useMenuStore();
// 已开通模块排在前面,套餐未开通(锁定)的模块沉到后方
function sortUnlockedFirst(list: ModuleItem[]): ModuleItem[] {
return [...list].sort(
(a, b) => Number(!!a.locked) - Number(!!b.locked)
);
}
// 根据type分组模块
const basicModules = computed(() =>
moduleList.value.filter((item) => item.type === 1)
sortUnlockedFirst(moduleList.value.filter((item) => item.type === 1))
);
const systemModules = computed(() =>
moduleList.value.filter((item) => item.type === 2)
sortUnlockedFirst(moduleList.value.filter((item) => item.type === 2))
);
const uncategorizedModules = computed(() =>
moduleList.value.filter((item) => item.type === 0)
sortUnlockedFirst(moduleList.value.filter((item) => item.type === 0))
);
const moduleList = ref<ModuleItem[]>([]);
@@ -265,8 +299,32 @@ const fetchUserProfile = async () => {
// 当前租户(企业)名称,来自后端 /backend/getCurrentUser 或登录接口返回的 tenant_name
const tenantName = computed(() => authStore.user?.tenant_name || "");
// 当前租户生效套餐名称(用于未开通提示)
const packageName = ref("");
// 套餐购买链接(平台端配置,为空时点击「购买」仅提示联系管理员)
const purchaseUrl = ref("");
// 套餐未开通模块:不跳转,提示购买套餐后使用
function showLockedTip(module: ModuleItem) {
const pkg = packageName.value ? `「${packageName.value}」` : "当前";
ElMessageBox.alert(
`${pkg}套餐未包含「${module.name}」功能模块,请购买或升级套餐后使用。如有疑问请联系平台管理员。`,
"功能未开通",
{
confirmButtonText: "我知道了",
type: "warning",
customClass: "module-locked-tip"
}
).catch(() => {});
}
// 处理导航跳转
function handleNavigate(module: ModuleItem) {
if (module.locked) {
showLockedTip(module);
return;
}
const isWebsiteManagement = module.name === "网站管理" || module.path.startsWith("/apps/cms");
const targetPath = isWebsiteManagement
? "/apps/cms/analytics/content"
@@ -275,6 +333,22 @@ function handleNavigate(module: ModuleItem) {
if (targetPath) router.push(targetPath);
}
// 点击「购买」:跳转该产品(按功能模块编码)的购买页,展示产品介绍 / 功能节点 / 价格档位
function handleBuy(module: ModuleItem) {
const code = (module.code || "").trim();
if (code) {
router.push(`/product/${code}`);
return;
}
// 无模块编码时回退到平台配置的外部购买链接,仍无则提示联系管理员
const url = (purchaseUrl.value || "").trim();
if (url) {
window.open(url, "_blank", "noopener,noreferrer");
return;
}
showLockedTip(module);
}
// 刷新菜单
async function handleRefreshMenus() {
refreshLoading.value = true;
@@ -331,10 +405,13 @@ async function loadModules() {
const mapped = {
...item,
type: item.type ?? 0, // 默认为0(未分类)
locked: !!item.locked, // 套餐未开通模块:置灰并锁定
title: item.name // 添加title字段用于显示分类标题
};
return mapped;
});
packageName.value = res.data?.package_name || "";
purchaseUrl.value = res.data?.purchase_url || "";
}
} catch (error) {
console.error("加载模块列表失败:", error);
@@ -580,7 +657,7 @@ onMounted(() => {
}
.module-card {
height: 180px;
min-height: 180px;
background: #fff;
border-radius: 12px;
padding: 30px;
@@ -606,9 +683,17 @@ onMounted(() => {
}
.card-title {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 18px;
font-weight: 600;
color: @text-main;
.lock-badge {
font-size: 14px;
color: #c0c4cc;
}
}
}
@@ -631,6 +716,79 @@ onMounted(() => {
}
}
.card-lock-tip {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 12px;
font-size: 12px;
color: #c0c4cc;
.buy-btn {
flex-shrink: 0;
padding: 2px 10px;
border: 1px solid #3973ff;
border-radius: 12px;
color: #3973ff;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #3973ff;
border-color: #3973ff;
color: #fff;
}
}
}
// 套餐未开通模块:置灰 + 禁止点击效果
&.is-locked {
background: #fafafa;
border-style: dashed;
border-color: #dcdfe6;
box-shadow: none;
cursor: not-allowed;
filter: grayscale(1);
opacity: 0.8;
.card-title {
color: #909399;
}
.card-desc {
color: #a8abb2;
}
.card-icon-wrapper {
color: #c0c4cc;
}
.card-divider {
border-color: #e4e7ed;
}
&:hover {
transform: none;
background: #fafafa;
box-shadow: none;
.card-title,
.card-desc {
color: #909399;
}
.card-icon-wrapper {
color: #c0c4cc;
}
.card-divider {
opacity: 1;
}
}
}
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.15);
@@ -947,6 +1105,43 @@ onMounted(() => {
.card-divider {
border-color: #3d3d3d;
}
&.is-locked {
background: #1f1f1f;
.card-title {
color: #8c8c8c !important;
font-weight: 600;
}
.card-desc {
color: #6b6b6b !important;
}
.card-icon-wrapper {
color: #5a5a5a;
}
.lock-badge {
color: #8c8c8c;
}
.card-lock-tip {
color: #6b6b6b;
}
&:hover {
background: #1f1f1f;
.card-title {
color: #8c8c8c !important;
}
.card-desc {
color: #6b6b6b !important;
}
}
}
}
}
@@ -972,6 +1167,19 @@ onMounted(() => {
.modules-section .module-card .card-desc {
color: #8c8c8c !important;
}
.modules-section .module-card.is-locked {
background: #1f1f1f;
.card-title {
color: #8c8c8c !important;
font-weight: 600;
}
.card-desc {
color: #6b6b6b !important;
}
}
}
}
</style>