Files
admin-ui/public/web/subscribe.html

767 lines
19 KiB
HTML
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.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>服务开通 - 智能营销服务平台</title>
<link rel="stylesheet" href="main.css" />
<link rel="stylesheet" href="style.css" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;800&display=swap" rel="stylesheet" />
<style>
* {
box-sizing: border-box;
}
.subscribe-container {
max-width: 800px;
margin: 0 auto;
padding: 32px 24px;
}
.subscribe-header {
text-align: center;
margin-bottom: 32px;
}
.subscribe-header h1 {
font-size: 1.75rem;
font-weight: 700;
color: #1e293b;
margin: 0 0 8px 0;
}
.subscribe-header p {
color: #64748b;
font-size: 0.95rem;
margin: 0;
}
.asset-info {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 28px;
border: 1px solid #bae6fd;
display: flex;
align-items: flex-start;
gap: 16px;
}
.asset-info .icon-wrapper {
width: 44px;
height: 44px;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.asset-info .icon-wrapper svg {
width: 24px;
height: 24px;
color: #fff;
}
.asset-info .info-content {
flex: 1;
}
.asset-info .asset-name {
font-size: 1.1rem;
font-weight: 600;
color: #0c4a6e;
margin: 0 0 6px 0;
}
.asset-info .description {
color: #0369a1;
font-size: 0.875rem;
line-height: 1.5;
margin: 0;
}
.asset-info .description p {
margin: 0;
}
.section-title {
font-size: 0.875rem;
font-weight: 600;
color: #475569;
margin-bottom: 12px;
}
.type-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 24px;
}
@media (max-width: 600px) {
.type-grid {
grid-template-columns: 1fr;
}
}
.type-card {
background: #fff;
border: 2px solid #e2e8f0;
border-radius: 10px;
padding: 14px 16px;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
position: relative;
}
.type-card:hover {
border-color: #10b981;
background: #f0fdf4;
}
.type-card.selected {
border-color: #10b981;
background: #ecfdf5;
}
.type-card.selected::after {
content: '';
position: absolute;
top: 8px;
right: 8px;
width: 18px;
height: 18px;
background: #10b981
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E")
center/12px no-repeat;
border-radius: 50%;
}
.type-card .type-name {
font-size: 0.95rem;
font-weight: 500;
color: #334155;
}
.sku-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 28px;
}
@media (max-width: 600px) {
.sku-grid {
grid-template-columns: 1fr;
}
}
.sku-card {
background: #fff;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
display: flex;
flex-direction: column;
}
.sku-card:hover {
border-color: #3b82f6;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.12);
}
.sku-card.selected {
border-color: #3b82f6;
background: #f8faff;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
}
.sku-card.selected::after {
content: '';
position: absolute;
top: 12px;
right: 12px;
width: 22px;
height: 22px;
background: #3b82f6
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E")
center/14px no-repeat;
border-radius: 50%;
}
.sku-name {
font-size: 1rem;
font-weight: 600;
color: #334155;
margin-bottom: 12px;
padding-right: 30px;
}
.sku-content {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.sku-specs {
display: flex;
align-items: baseline;
gap: 2px;
}
.sku-specs .count {
font-size: 2rem;
font-weight: 700;
color: #3b82f6;
line-height: 1;
}
.sku-specs .unit {
font-size: 0.875rem;
color: #64748b;
margin-left: 2px;
}
.sku-price {
text-align: right;
}
.sku-price .amount {
color: #ef4444;
font-size: 1.25rem;
font-weight: 700;
}
.sku-price .symbol {
font-size: 0.875rem;
}
.subscribe-actions {
display: flex;
gap: 12px;
justify-content: center;
padding-top: 8px;
}
.btn-subscribe {
padding: 12px 36px;
font-size: 1rem;
font-weight: 600;
border-radius: 8px;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-subscribe.primary {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: #fff;
}
.btn-subscribe.primary:hover {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
.btn-subscribe.primary:disabled {
background: #94a3b8;
cursor: not-allowed;
box-shadow: none;
}
.btn-subscribe.secondary {
background: #fff;
color: #64748b;
border: 1px solid #e2e8f0;
}
.btn-subscribe.secondary:hover {
border-color: #cbd5e1;
background: #f8fafc;
}
.loading-container {
text-align: center;
padding: 48px 20px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #e2e8f0;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 12px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.empty-container {
text-align: center;
padding: 48px 20px;
color: #64748b;
}
.empty-container svg {
width: 56px;
height: 56px;
margin-bottom: 12px;
color: #cbd5e1;
}
.error-message {
background: #fef2f2;
border: 1px solid #fecaca;
color: #dc2626;
padding: 14px 20px;
border-radius: 8px;
margin-bottom: 20px;
text-align: center;
font-size: 0.95rem;
}
.success-message {
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
border: 1px solid #86efac;
color: #166534;
padding: 32px;
border-radius: 12px;
text-align: center;
}
.success-message h3 {
font-size: 1.25rem;
margin: 0 0 8px 0;
}
.success-message p {
margin: 0;
color: #15803d;
}
</style>
</head>
<body class="auth-body">
<!-- 导航 -->
<nav class="auth-navbar">
<div class="auth-navbar-container">
<a href="/index.html#/home" class="auth-back-link">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
返回首页
</a>
<span class="auth-title">服务开通</span>
</div>
</nav>
<main class="subscribe-container">
<div class="subscribe-header">
<h1>开通服务</h1>
<p>选择适合您的套餐,立即开通使用</p>
</div>
<!-- 加载中 -->
<div id="loading" class="loading-container">
<div class="loading-spinner"></div>
<p>正在加载套餐信息...</p>
</div>
<!-- 错误信息 -->
<div id="error" class="error-message" style="display: none"></div>
<!-- 资产信息 -->
<div id="asset-info" class="asset-info" style="display: none">
<div class="icon-wrapper">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
<div class="info-content">
<h3 id="asset-name" class="asset-name"></h3>
<div id="asset-description" class="description"></div>
</div>
</div>
<!-- 用户类型选择 -->
<div id="type-section" style="display: none">
<div class="section-title">选择类型</div>
<div id="type-list" class="type-grid"></div>
</div>
<!-- SKU 列表 -->
<div id="sku-section" style="display: none">
<div class="section-title">选择套餐</div>
<div id="sku-list" class="sku-grid"></div>
</div>
<!-- 空状态 -->
<div id="empty" class="empty-container" style="display: none">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
></path>
</svg>
<p>暂无可用套餐</p>
</div>
<!-- 成功信息 -->
<div id="success" class="success-message" style="display: none">
<h3>🎉 开通成功!</h3>
<p>正在返回...</p>
</div>
<!-- 操作按钮 -->
<div id="actions" class="subscribe-actions" style="display: none">
<button class="btn-subscribe secondary" onclick="handleCancel()">取消</button>
<button id="btn-submit" class="btn-subscribe primary" onclick="handleSubscribe()" disabled>立即开通</button>
</div>
</main>
<script>
// ============================================================
// API 基础地址配置
// 切换环境时,修改下面的 SERVER_IP 即可
// ============================================================
const SERVER_IP = '192.168.3.11'; // 后端服务器IP备用: 192.168.3.200
const API_BASE_NEW = `http://${SERVER_IP}:8000`; // 新功能服务端口8000- 资产、SKU查询
const API_BASE_MAIN = `http://${SERVER_IP}:8808`; // 主服务端口8808- 模块开通
// 页面状态
let assetId = '';
let returnUrl = '';
let selectedSku = null;
let selectedType = null;
let assetData = null;
let tenantModuleTypes = [];
// 初始化
window.addEventListener('DOMContentLoaded', () => {
const params = new URLSearchParams(window.location.search);
assetId = params.get('assetId') || '';
returnUrl = params.get('returnUrl') || '/index.html';
if (!assetId) {
showError('缺少资产ID参数');
return;
}
loadAssetAndSku();
});
// 加载资产和SKU信息
async function loadAssetAndSku() {
showLoading(true);
try {
const token = getToken();
console.log('[subscribe] token:', token);
const headers = {
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${API_BASE_NEW}/assets/asset/getAssetAndSku?assetId=${assetId}`, {
headers: headers,
});
// 先获取文本检查是否为有效JSON
const text = await response.text();
console.log('[subscribe] response text:', text);
let result;
try {
result = JSON.parse(text);
} catch (e) {
throw new Error('服务器返回数据格式错误');
}
if (result.code !== 0) {
throw new Error(result.message || '加载失败');
}
assetData = result.data;
tenantModuleTypes = assetData.tenantModuleType || [];
renderAssetInfo(assetData);
renderTypeList(tenantModuleTypes);
renderSkuList(assetData.skus || []);
} catch (error) {
console.error('加载失败:', error);
showError(error.message || '加载套餐信息失败,请稍后重试');
} finally {
showLoading(false);
}
}
// 渲染资产信息
function renderAssetInfo(data) {
document.getElementById('asset-name').textContent = data.name || '服务';
document.getElementById('asset-description').innerHTML = data.description || '';
document.getElementById('asset-info').style.display = 'flex';
}
// 渲染用户类型列表
function renderTypeList(types) {
const container = document.getElementById('type-list');
const section = document.getElementById('type-section');
if (!types || types.length === 0) {
section.style.display = 'none';
return;
}
container.innerHTML = types
.map(
(type) => `
<div class="type-card" data-type-key="${type.key}" onclick="selectType('${type.key}')">
<div class="type-name">${type.value}</div>
</div>
`
)
.join('');
section.style.display = 'block';
// 默认选中第一个
if (types.length > 0) {
selectType(types[0].key);
}
}
// 选择用户类型
function selectType(typeKey) {
// 移除之前的选中状态
document.querySelectorAll('.type-card').forEach((card) => {
card.classList.remove('selected');
});
// 添加选中状态
const card = document.querySelector(`.type-card[data-type-key="${typeKey}"]`);
if (card) {
card.classList.add('selected');
}
// 保存选中的类型
selectedType = tenantModuleTypes.find((t) => t.key === typeKey) || null;
// 更新按钮状态
updateSubmitButton();
}
// 渲染SKU列表
function renderSkuList(skus) {
const container = document.getElementById('sku-list');
const section = document.getElementById('sku-section');
const actions = document.getElementById('actions');
const empty = document.getElementById('empty');
if (!skus || skus.length === 0) {
empty.style.display = 'block';
return;
}
container.innerHTML = skus
.map(
(sku) => `
<div class="sku-card" data-sku-id="${sku.id}" onclick="selectSku('${sku.id}')">
<div class="sku-name">${sku.skuName}</div>
<div class="sku-content">
<div class="sku-specs">
<span class="count">${sku.specsCount}</span>
<span class="unit">${sku.specsUnit?.value || ''}</span>
</div>
<div class="sku-price">
<span class="amount"><span class="symbol">¥</span>${(sku.price / 100).toFixed(2)}</span>
</div>
</div>
</div>
`
)
.join('');
section.style.display = 'block';
actions.style.display = 'flex';
// 默认选中第一个
if (skus.length > 0) {
selectSku(skus[0].id);
}
}
// 选择SKU
function selectSku(skuId) {
// 移除之前的选中状态
document.querySelectorAll('.sku-card').forEach((card) => {
card.classList.remove('selected');
});
// 添加选中状态
const card = document.querySelector(`.sku-card[data-sku-id="${skuId}"]`);
if (card) {
card.classList.add('selected');
}
// 保存选中的SKU
selectedSku = assetData?.skus?.find((s) => s.id === skuId) || null;
// 更新按钮状态
updateSubmitButton();
}
// 更新提交按钮状态
function updateSubmitButton() {
const btn = document.getElementById('btn-submit');
// 如果有用户类型选项,则需要同时选择类型和套餐
if (tenantModuleTypes.length > 0) {
btn.disabled = !selectedSku || !selectedType;
} else {
btn.disabled = !selectedSku;
}
}
// 开通服务
async function handleSubscribe() {
if (!selectedSku) {
alert('请选择套餐');
return;
}
// 如果有用户类型选项但未选择
if (tenantModuleTypes.length > 0 && !selectedType) {
alert('请选择用户类型');
return;
}
const btn = document.getElementById('btn-submit');
btn.disabled = true;
btn.textContent = '开通中...';
try {
const token = getToken();
// 构建请求参数
const requestBody = {
assetSkuId: selectedSku.id,
};
// 如果选择了用户类型,添加到请求参数
if (selectedType) {
requestBody.tenantModuleType = selectedType.key;
}
const response = await fetch(`${API_BASE_MAIN}/admin-go/api/v1/system/moduleTenant/add`, {
method: 'POST',
headers: {
Authorization: token ? `Bearer ${token}` : '',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});
const result = await response.json();
if (result.code !== 0) {
throw new Error(result.message || '开通失败');
}
// 显示成功
document.getElementById('type-section').style.display = 'none';
document.getElementById('sku-section').style.display = 'none';
document.getElementById('actions').style.display = 'none';
document.getElementById('asset-info').style.display = 'none';
document.getElementById('success').style.display = 'block';
// 设置开通时间标记防止跳转后立即又触发402
sessionStorage.setItem('lastSubscribeTime', Date.now().toString());
// 延迟跳转回原页面
const targetUrl = decodeURIComponent(returnUrl);
// console.log('[subscribe] 开通成功,即将跳转到:', targetUrl);
// console.log('[subscribe] 原始 returnUrl:', returnUrl);
setTimeout(() => {
let finalUrl;
// 如果 returnUrl 包含当前开通页面路径,则跳转到首页
if (targetUrl.includes('/web/subscribe') || targetUrl.includes('subscribe.html')) {
console.log('[subscribe] returnUrl 指向开通页面,改为跳转首页');
finalUrl = '/index.html#/home';
} else {
finalUrl = targetUrl;
}
// 使用 replace 跳转,然后强制刷新
window.location.replace(finalUrl);
// 延迟一点再刷新,确保 URL 已经改变
setTimeout(() => {
window.location.reload(true);
}, 100);
}, 2000);
} catch (error) {
console.error('开通失败:', error);
alert(error.message || '开通失败,请稍后重试');
btn.disabled = false;
btn.textContent = '立即开通';
}
}
// 取消 - 跳转到后台首页避免循环触发402
function handleCancel() {
window.location.href = '/index.html#/home';
}
// 获取Token从Cookie获取
function getToken() {
try {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const parts = cookie.trim().split('=');
const name = parts[0];
// token 值可能包含 = 号,所以用 slice 获取剩余部分
const value = parts.slice(1).join('=');
if (name === 'token' && value) {
// Cookie 中的值可能被 URL 编码,需要解码
try {
return decodeURIComponent(value);
} catch (e) {
return value;
}
}
}
} catch (e) {
console.error('获取token失败:', e);
}
return '';
}
// 显示/隐藏加载
function showLoading(show) {
document.getElementById('loading').style.display = show ? 'block' : 'none';
}
// 显示错误
function showError(message) {
const errorEl = document.getElementById('error');
errorEl.textContent = message;
errorEl.style.display = 'block';
document.getElementById('loading').style.display = 'none';
}
</script>
</body>
</html>