Files
yunzerwebsiteallinone/platform/src/views/basicSettings/pricing/index.vue
T
2026-09-16 14:28:07 +08:00

341 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>