341 lines
10 KiB
Vue
341 lines
10 KiB
Vue
<template>
|
||
<div class="container-box">
|
||
<div class="header-bar">
|
||
<h2>产品定价</h2>
|
||
<div class="header-actions">
|
||
<el-button v-if="activeTab === 'product'" type="primary" @click="handleAdd">
|
||
<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、产品只维护「名称 / 编码 / 价格 / 关联功能 / 上下架」,功能清单在编辑弹窗里按树形维护(支持父子层级)。</p>
|
||
<p>2、产品编码建议与功能模块编码(yz_system_modules.code)一致,租户端 home 未开通卡片点「购买」即可定位到对应产品页。</p>
|
||
<p>3、租户下单支付成功后,由平台在「购买订单」里开通(订单绑定套餐后自动为租户开通对应功能)。</p>
|
||
</template>
|
||
</el-alert>
|
||
|
||
<el-tabs v-model="activeTab" class="pricing-tabs">
|
||
<!-- 产品定价 -->
|
||
<el-tab-pane label="产品定价" name="product">
|
||
<el-table :data="products" style="width: 100%" v-loading="loading">
|
||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||
<el-table-column label="产品名称" min-width="180">
|
||
<template #default="{ row }">
|
||
<span class="product-name">{{ row.name }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="产品编码" width="130" 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="130" align="center">
|
||
<template #default="{ row }">
|
||
<span class="price">¥{{ Number(row.price || 0).toFixed(2) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="关联功能" width="140" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.module_code" size="small" effect="plain">
|
||
{{ moduleName(row.module_code) }}
|
||
</el-tag>
|
||
<span v-else class="muted">未关联</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="功能节点" width="100" align="center">
|
||
<template #default="{ row }">{{ row.features?.length || 0 }} 个</template>
|
||
</el-table-column>
|
||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||
<el-table-column label="状态" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="Number(row.status) === 1 ? 'success' : 'info'" 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="handleEdit(row)">编辑</el-button>
|
||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-tab-pane>
|
||
|
||
<!-- 购买订单 -->
|
||
<el-tab-pane label="购买订单" name="order">
|
||
<div class="order-filter">
|
||
<el-select v-model="orderStatus" placeholder="订单状态" clearable style="width: 160px">
|
||
<el-option label="待支付" value="pending" />
|
||
<el-option label="已支付" value="paid" />
|
||
<el-option label="已开通" value="activated" />
|
||
<el-option label="已关闭" value="closed" />
|
||
</el-select>
|
||
<el-button type="primary" @click="loadOrders">查询</el-button>
|
||
</div>
|
||
<el-table :data="orders" style="width: 100%" v-loading="orderLoading">
|
||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||
<el-table-column prop="order_no" label="订单号" min-width="170" show-overflow-tooltip />
|
||
<el-table-column prop="tenant_name" label="租户" min-width="160" show-overflow-tooltip />
|
||
<el-table-column prop="product_name" label="产品" min-width="160" show-overflow-tooltip />
|
||
<el-table-column label="金额" width="110" align="center">
|
||
<template #default="{ row }">¥{{ Number(row.amount || 0).toFixed(2) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="绑定套餐" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.package_id" size="small" effect="plain">{{ row.package_id }}</el-tag>
|
||
<span v-else class="muted">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="orderStatusType(row.status)" size="small">
|
||
{{ orderStatusText(row.status) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="create_time" label="下单时间" width="170" align="center" />
|
||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button
|
||
v-if="row.status !== 'activated'"
|
||
size="small"
|
||
type="primary"
|
||
link
|
||
@click="handleActivate(row)"
|
||
>
|
||
手动开通
|
||
</el-button>
|
||
<span v-else class="muted">已开通</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
|
||
<ProductEditDialog v-model="dialogVisible" :product="currentProduct" @success="loadProducts" />
|
||
</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 {
|
||
getProductList,
|
||
deleteProduct,
|
||
getProductOrders,
|
||
activateProductOrder,
|
||
} from "@/api/product";
|
||
import { getTenantPackageModuleOptions } from "@/api/tenantPackage";
|
||
import ProductEditDialog from "./components/edit.vue";
|
||
|
||
const activeTab = ref("product");
|
||
const loading = ref(false);
|
||
const orderLoading = ref(false);
|
||
const products = ref<any[]>([]);
|
||
const orders = ref<any[]>([]);
|
||
const orderStatus = ref("");
|
||
const moduleOptions = ref<any[]>([]);
|
||
|
||
const dialogVisible = ref(false);
|
||
const currentProduct = ref<any>(null);
|
||
|
||
const moduleName = (code: string) => {
|
||
const m = moduleOptions.value.find((x) => x.code === code);
|
||
return m ? m.name : code;
|
||
};
|
||
|
||
const orderStatusText = (s: string) =>
|
||
({ pending: "待支付", paid: "已支付", activated: "已开通", closed: "已关闭" }[s] || s);
|
||
const orderStatusType = (s: string) =>
|
||
({ pending: "warning", paid: "primary", activated: "success", closed: "info" }[s] || "info");
|
||
|
||
const loadModules = async () => {
|
||
try {
|
||
const res = await getTenantPackageModuleOptions();
|
||
if (res.code === 200) moduleOptions.value = res.data?.list || [];
|
||
} catch {
|
||
moduleOptions.value = [];
|
||
}
|
||
};
|
||
|
||
const loadProducts = async () => {
|
||
loading.value = true;
|
||
try {
|
||
const res = await getProductList();
|
||
if (res.code === 200) {
|
||
products.value = res.data?.list || [];
|
||
} else {
|
||
ElMessage.error(res.msg || "获取产品列表失败");
|
||
}
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.message || "获取产品列表失败");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const loadOrders = async () => {
|
||
orderLoading.value = true;
|
||
try {
|
||
const res = await getProductOrders({ status: orderStatus.value });
|
||
if (res.code === 200) {
|
||
orders.value = res.data?.list || [];
|
||
} else {
|
||
ElMessage.error(res.msg || "获取订单失败");
|
||
}
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.message || "获取订单失败");
|
||
} finally {
|
||
orderLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const refresh = () => {
|
||
loadProducts();
|
||
loadOrders();
|
||
};
|
||
|
||
const handleAdd = () => {
|
||
currentProduct.value = null;
|
||
dialogVisible.value = true;
|
||
};
|
||
|
||
const handleEdit = (row: any) => {
|
||
currentProduct.value = { ...row };
|
||
dialogVisible.value = true;
|
||
};
|
||
|
||
const handleDelete = async (row: any) => {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定要删除产品「${row.name}」吗?删除后租户端购买页将不再展示该产品。`,
|
||
"警告",
|
||
{ type: "warning" }
|
||
);
|
||
} catch {
|
||
return;
|
||
}
|
||
try {
|
||
const res = await deleteProduct(row.id);
|
||
if (res.code === 200) {
|
||
ElMessage.success("删除成功");
|
||
loadProducts();
|
||
} else {
|
||
ElMessage.error(res.msg || "删除失败");
|
||
}
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.message || "删除失败");
|
||
}
|
||
};
|
||
|
||
const handleActivate = async (row: any) => {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确认手动开通订单「${row.order_no}」?开通后租户将获得对应功能。`,
|
||
"手动开通",
|
||
{ type: "warning" }
|
||
);
|
||
} catch {
|
||
return;
|
||
}
|
||
try {
|
||
const res = await activateProductOrder(row.id);
|
||
if (res.code === 200) {
|
||
ElMessage.success("开通成功");
|
||
loadOrders();
|
||
} else {
|
||
ElMessage.error(res.msg || "开通失败");
|
||
}
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.message || "开通失败");
|
||
}
|
||
};
|
||
|
||
onMounted(async () => {
|
||
await loadModules();
|
||
refresh();
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.container-box {
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.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: 16px;
|
||
|
||
p {
|
||
margin: 4px 0;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
}
|
||
}
|
||
|
||
.pricing-tabs {
|
||
margin-top: 0;
|
||
|
||
:deep(.el-tabs__header) {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
:deep(.el-tabs__nav-wrap::after) {
|
||
height: 1px;
|
||
}
|
||
}
|
||
|
||
.product-name {
|
||
font-weight: 500;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.price {
|
||
color: #f56c6c;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.order-filter {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.muted {
|
||
color: var(--el-text-color-secondary);
|
||
font-size: 12px;
|
||
}
|
||
</style>
|