增加功能报价
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user