@windrun-huaiin/third-ui 7.3.4 → 7.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main/faq.js +0 -1
- package/dist/main/faq.mjs +0 -1
- package/dist/main/gallery.js +0 -1
- package/dist/main/gallery.mjs +0 -1
- package/dist/main/money-price/money-price-interactive.js +25 -68
- package/dist/main/money-price/money-price-interactive.mjs +28 -71
- package/dist/main/money-price/money-price.js +1 -21
- package/dist/main/money-price/money-price.mjs +1 -21
- package/dist/main/price-plan.js +0 -1
- package/dist/main/price-plan.mjs +0 -1
- package/dist/node_modules/.pnpm/cose-base@1.0.3/node_modules/cose-base/cose-base.mjs +1 -1
- package/dist/node_modules/.pnpm/cose-base@2.2.0/node_modules/cose-base/cose-base.mjs +1 -1
- package/package.json +1 -1
- package/src/main/money-price/money-price-interactive.tsx +45 -87
- package/src/main/money-price/money-price.tsx +4 -31
package/dist/main/faq.js
CHANGED
|
@@ -14,7 +14,6 @@ var faqInteractive = require('./faq-interactive.js');
|
|
|
14
14
|
require('next/navigation');
|
|
15
15
|
require('@clerk/nextjs');
|
|
16
16
|
require('../clerk/fingerprint/fingerprint-provider.js');
|
|
17
|
-
require('react-dom/client');
|
|
18
17
|
require('next-themes');
|
|
19
18
|
require('fumadocs-core/framework');
|
|
20
19
|
require('next/link');
|
package/dist/main/faq.mjs
CHANGED
|
@@ -12,7 +12,6 @@ import { FAQInteractive } from './faq-interactive.mjs';
|
|
|
12
12
|
import 'next/navigation';
|
|
13
13
|
import '@clerk/nextjs';
|
|
14
14
|
import '../clerk/fingerprint/fingerprint-provider.mjs';
|
|
15
|
-
import 'react-dom/client';
|
|
16
15
|
import 'next-themes';
|
|
17
16
|
import 'fumadocs-core/framework';
|
|
18
17
|
import 'next/link';
|
package/dist/main/gallery.js
CHANGED
|
@@ -14,7 +14,6 @@ require('next/navigation');
|
|
|
14
14
|
var galleryInteractive = require('./gallery-interactive.js');
|
|
15
15
|
require('@clerk/nextjs');
|
|
16
16
|
require('../clerk/fingerprint/fingerprint-provider.js');
|
|
17
|
-
require('react-dom/client');
|
|
18
17
|
require('next-themes');
|
|
19
18
|
require('fumadocs-core/framework');
|
|
20
19
|
require('next/link');
|
package/dist/main/gallery.mjs
CHANGED
|
@@ -12,7 +12,6 @@ import 'next/navigation';
|
|
|
12
12
|
import { GalleryInteractive } from './gallery-interactive.mjs';
|
|
13
13
|
import '@clerk/nextjs';
|
|
14
14
|
import '../clerk/fingerprint/fingerprint-provider.mjs';
|
|
15
|
-
import 'react-dom/client';
|
|
16
15
|
import 'next-themes';
|
|
17
16
|
import 'fumadocs-core/framework';
|
|
18
17
|
import 'next/link';
|
|
@@ -8,13 +8,11 @@ var React = require('react');
|
|
|
8
8
|
var fingerprintProvider = require('../../clerk/fingerprint/fingerprint-provider.js');
|
|
9
9
|
var utils = require('@windrun-huaiin/lib/utils');
|
|
10
10
|
var navigation = require('next/navigation');
|
|
11
|
-
var client = require('react-dom/client');
|
|
12
11
|
var moneyPriceButton = require('./money-price-button.js');
|
|
13
12
|
var moneyPriceConfigUtil = require('./money-price-config-util.js');
|
|
14
13
|
var moneyPriceTypes = require('./money-price-types.js');
|
|
15
14
|
|
|
16
15
|
function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath }) {
|
|
17
|
-
var _a, _b, _c, _d;
|
|
18
16
|
const fingerprintContext = fingerprintProvider.useFingerprintContextSafe();
|
|
19
17
|
const { redirectToSignIn } = nextjs.useClerk();
|
|
20
18
|
const router = navigation.useRouter();
|
|
@@ -22,7 +20,7 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
22
20
|
const [isProcessing, setIsProcessing] = React.useState(false);
|
|
23
21
|
const [tooltip, setTooltip] = React.useState({ show: false, content: '', x: 0, y: 0 });
|
|
24
22
|
// 确定用户状态
|
|
25
|
-
const getUserState = () => {
|
|
23
|
+
const getUserState = React.useCallback(() => {
|
|
26
24
|
var _a, _b;
|
|
27
25
|
if (!fingerprintContext)
|
|
28
26
|
return moneyPriceTypes.UserState.Anonymous;
|
|
@@ -36,34 +34,35 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
36
34
|
if ((_b = xSubscription.priceName) === null || _b === void 0 ? void 0 : _b.includes('Ultra'))
|
|
37
35
|
return moneyPriceTypes.UserState.UltraUser;
|
|
38
36
|
return moneyPriceTypes.UserState.FreeUser;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
}, [fingerprintContext]);
|
|
38
|
+
// 优化 userContext 使用 useMemo
|
|
39
|
+
const userContext = React.useMemo(() => {
|
|
40
|
+
var _a, _b, _c, _d;
|
|
41
|
+
return ({
|
|
42
|
+
isAuthenticated: !!((_a = fingerprintContext === null || fingerprintContext === void 0 ? void 0 : fingerprintContext.xUser) === null || _a === void 0 ? void 0 : _a.clerkUserId),
|
|
43
|
+
subscriptionStatus: getUserState(),
|
|
44
|
+
subscriptionType: ((_c = (_b = fingerprintContext === null || fingerprintContext === void 0 ? void 0 : fingerprintContext.xSubscription) === null || _b === void 0 ? void 0 : _b.priceId) === null || _c === void 0 ? void 0 : _c.includes('yearly')) ? 'yearly' : 'monthly',
|
|
45
|
+
subscriptionEndDate: (_d = fingerprintContext === null || fingerprintContext === void 0 ? void 0 : fingerprintContext.xSubscription) === null || _d === void 0 ? void 0 : _d.subPeriodEnd
|
|
46
|
+
});
|
|
47
|
+
}, [fingerprintContext, getUserState]);
|
|
46
48
|
// 处理登录
|
|
47
|
-
const handleLogin = () => {
|
|
49
|
+
const handleLogin = React.useCallback(() => {
|
|
48
50
|
if (signInPath) {
|
|
49
51
|
router.push(signInPath);
|
|
50
52
|
}
|
|
51
53
|
else {
|
|
52
54
|
redirectToSignIn();
|
|
53
55
|
}
|
|
54
|
-
};
|
|
56
|
+
}, [signInPath, redirectToSignIn, router]);
|
|
55
57
|
// 处理升级
|
|
56
|
-
const handleUpgrade = (plan, billingType) => tslib_es6.__awaiter(this, void 0, void 0, function* () {
|
|
57
|
-
// 如果没有配置 API 端点,跳转到首页
|
|
58
|
+
const handleUpgrade = React.useCallback((plan, billingType) => tslib_es6.__awaiter(this, void 0, void 0, function* () {
|
|
58
59
|
if (!upgradeApiEndpoint) {
|
|
59
60
|
router.push('/');
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
62
63
|
setIsProcessing(true);
|
|
63
64
|
try {
|
|
64
|
-
// 获取价格配置
|
|
65
65
|
const pricing = moneyPriceConfigUtil.getProductPricing(plan, billingType, config.activeProvider, config);
|
|
66
|
-
// 调用 API 创建支付会话
|
|
67
66
|
const response = yield fetch(upgradeApiEndpoint, {
|
|
68
67
|
method: 'POST',
|
|
69
68
|
headers: {
|
|
@@ -78,7 +77,6 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
78
77
|
});
|
|
79
78
|
const result = yield response.json();
|
|
80
79
|
if (result.success && result.checkoutUrl) {
|
|
81
|
-
// 跳转到支付页面
|
|
82
80
|
window.location.href = result.checkoutUrl;
|
|
83
81
|
}
|
|
84
82
|
else {
|
|
@@ -91,14 +89,13 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
91
89
|
finally {
|
|
92
90
|
setIsProcessing(false);
|
|
93
91
|
}
|
|
94
|
-
});
|
|
92
|
+
}), [upgradeApiEndpoint, config, router]);
|
|
95
93
|
// 更新价格显示
|
|
96
|
-
const updatePriceDisplay = (newBillingType) => {
|
|
94
|
+
const updatePriceDisplay = React.useCallback((newBillingType) => {
|
|
97
95
|
const providerConfig = moneyPriceConfigUtil.getActiveProviderConfig(config);
|
|
98
96
|
data.plans.forEach((plan) => {
|
|
99
97
|
const productConfig = providerConfig.products[plan.key];
|
|
100
98
|
const pricing = productConfig.plans[newBillingType];
|
|
101
|
-
// 更新价格值
|
|
102
99
|
const priceValueElement = document.querySelector(`[data-price-value="${plan.key}"]`);
|
|
103
100
|
if (priceValueElement) {
|
|
104
101
|
if (pricing.amount === 0) {
|
|
@@ -108,13 +105,11 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
108
105
|
priceValueElement.textContent = `${data.currency}${pricing.amount}`;
|
|
109
106
|
}
|
|
110
107
|
}
|
|
111
|
-
// 更新单位
|
|
112
108
|
const priceUnitElement = document.querySelector(`[data-price-unit="${plan.key}"]`);
|
|
113
109
|
if (priceUnitElement) {
|
|
114
110
|
const billingOption = data.billingSwitch.options.find(opt => opt.key === newBillingType);
|
|
115
111
|
priceUnitElement.textContent = (billingOption === null || billingOption === void 0 ? void 0 : billingOption.unit) || '/month';
|
|
116
112
|
}
|
|
117
|
-
// 更新原价和折扣
|
|
118
113
|
const priceOriginalElement = document.querySelector(`[data-price-original="${plan.key}"]`);
|
|
119
114
|
const priceDiscountElement = document.querySelector(`[data-price-discount="${plan.key}"]`);
|
|
120
115
|
if (pricing.originalAmount && pricing.discountPercent) {
|
|
@@ -136,16 +131,15 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
136
131
|
if (priceDiscountElement)
|
|
137
132
|
priceDiscountElement.style.display = 'none';
|
|
138
133
|
}
|
|
139
|
-
// 更新副标题
|
|
140
134
|
const priceSubtitleElement = document.querySelector(`[data-price-subtitle="${plan.key}"]`);
|
|
141
135
|
if (priceSubtitleElement && plan.showBillingSubTitle !== false) {
|
|
142
136
|
const billingOption = data.billingSwitch.options.find(opt => opt.key === newBillingType);
|
|
143
137
|
priceSubtitleElement.textContent = (billingOption === null || billingOption === void 0 ? void 0 : billingOption.subTitle) || '';
|
|
144
138
|
}
|
|
145
139
|
});
|
|
146
|
-
};
|
|
140
|
+
}, [config, data]);
|
|
147
141
|
// 更新按钮样式
|
|
148
|
-
const updateButtonStyles = (newBillingType) => {
|
|
142
|
+
const updateButtonStyles = React.useCallback((newBillingType) => {
|
|
149
143
|
const monthlyButton = document.querySelector('[data-billing-button="monthly"]');
|
|
150
144
|
const yearlyButton = document.querySelector('[data-billing-button="yearly"]');
|
|
151
145
|
const activeClasses = 'text-white bg-gradient-to-r from-purple-400 to-pink-500 hover:from-purple-500 hover:to-pink-600 dark:from-purple-500 dark:to-pink-600 dark:hover:from-purple-600 rounded-full shadow-sm';
|
|
@@ -160,15 +154,14 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
160
154
|
yearlyButton.className = utils.cn('min-w-[120px] px-6 py-2 font-medium transition text-lg relative', activeClasses);
|
|
161
155
|
}
|
|
162
156
|
}
|
|
163
|
-
};
|
|
157
|
+
}, []);
|
|
164
158
|
// 更新折扣信息
|
|
165
|
-
const updateDiscountInfo = (newBillingType) => {
|
|
159
|
+
const updateDiscountInfo = React.useCallback((newBillingType) => {
|
|
166
160
|
const discountInfoElement = document.querySelector('[data-discount-info]');
|
|
167
161
|
if (!discountInfoElement)
|
|
168
162
|
return;
|
|
169
163
|
const billingOption = data.billingSwitch.options.find(opt => opt.key === newBillingType);
|
|
170
164
|
const providerConfig = moneyPriceConfigUtil.getActiveProviderConfig(config);
|
|
171
|
-
// 检查是否有折扣
|
|
172
165
|
let hasDiscount = false;
|
|
173
166
|
let discountPercent = 0;
|
|
174
167
|
['pro', 'ultra'].forEach(planKey => {
|
|
@@ -179,7 +172,6 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
179
172
|
discountPercent = pricing.discountPercent;
|
|
180
173
|
}
|
|
181
174
|
});
|
|
182
|
-
// 清空内容
|
|
183
175
|
discountInfoElement.innerHTML = '';
|
|
184
176
|
if (hasDiscount && (billingOption === null || billingOption === void 0 ? void 0 : billingOption.discountText)) {
|
|
185
177
|
const discountBadge = document.createElement('span');
|
|
@@ -187,39 +179,7 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
187
179
|
discountBadge.textContent = billingOption.discountText.replace('{percent}', String(discountPercent));
|
|
188
180
|
discountInfoElement.appendChild(discountBadge);
|
|
189
181
|
}
|
|
190
|
-
};
|
|
191
|
-
// 使用 useRef 存储 root 实例,避免重复创建
|
|
192
|
-
const rootsRef = React.useRef(new Map());
|
|
193
|
-
// 动态替换按钮
|
|
194
|
-
React.useEffect(() => {
|
|
195
|
-
data.plans.forEach((plan) => {
|
|
196
|
-
const placeholder = document.querySelector(`[data-button-placeholder="${plan.key}"]`);
|
|
197
|
-
if (placeholder) {
|
|
198
|
-
let root = rootsRef.current.get(plan.key);
|
|
199
|
-
// 如果还没有创建 root,则创建一个
|
|
200
|
-
if (!root) {
|
|
201
|
-
root = client.createRoot(placeholder);
|
|
202
|
-
rootsRef.current.set(plan.key, root);
|
|
203
|
-
}
|
|
204
|
-
// 渲染按钮
|
|
205
|
-
root.render(jsxRuntime.jsx(moneyPriceButton.MoneyPriceButton, { planKey: plan.key, userContext: userContext, billingType: billingType, onLogin: handleLogin, onUpgrade: handleUpgrade, texts: data.buttonTexts, isProcessing: isProcessing }));
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
}, [userContext, billingType, isProcessing, data.plans, data.buttonTexts, handleLogin, handleUpgrade]);
|
|
209
|
-
// 组件卸载时清理
|
|
210
|
-
React.useEffect(() => {
|
|
211
|
-
return () => {
|
|
212
|
-
rootsRef.current.forEach((root) => {
|
|
213
|
-
try {
|
|
214
|
-
root.unmount();
|
|
215
|
-
}
|
|
216
|
-
catch (e) {
|
|
217
|
-
// 忽略卸载错误
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
rootsRef.current.clear();
|
|
221
|
-
};
|
|
222
|
-
}, []);
|
|
182
|
+
}, [config, data]);
|
|
223
183
|
// 处理月付/年付切换和 tooltip 功能
|
|
224
184
|
React.useEffect(() => {
|
|
225
185
|
const monthlyButton = document.querySelector('[data-billing-button="monthly"]');
|
|
@@ -242,7 +202,6 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
242
202
|
if (yearlyButton) {
|
|
243
203
|
yearlyButton.addEventListener('click', handleYearlyClick);
|
|
244
204
|
}
|
|
245
|
-
// 添加 tooltip 功能
|
|
246
205
|
const tooltipHandlers = [];
|
|
247
206
|
data.plans.forEach((plan) => {
|
|
248
207
|
var _a;
|
|
@@ -274,10 +233,8 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
274
233
|
}
|
|
275
234
|
});
|
|
276
235
|
});
|
|
277
|
-
// 初始化价格显示
|
|
278
236
|
updatePriceDisplay(billingType);
|
|
279
237
|
updateDiscountInfo(billingType);
|
|
280
|
-
// 清理
|
|
281
238
|
return () => {
|
|
282
239
|
if (monthlyButton) {
|
|
283
240
|
monthlyButton.removeEventListener('click', handleMonthlyClick);
|
|
@@ -285,14 +242,13 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
285
242
|
if (yearlyButton) {
|
|
286
243
|
yearlyButton.removeEventListener('click', handleYearlyClick);
|
|
287
244
|
}
|
|
288
|
-
// 清理 tooltip 事件监听器
|
|
289
245
|
tooltipHandlers.forEach(({ element, handlers }) => {
|
|
290
246
|
element.removeEventListener('mouseenter', handlers.mouseenter);
|
|
291
247
|
element.removeEventListener('mousemove', handlers.mousemove);
|
|
292
248
|
element.removeEventListener('mouseleave', handlers.mouseleave);
|
|
293
249
|
});
|
|
294
250
|
};
|
|
295
|
-
}, [data]);
|
|
251
|
+
}, [data, billingType, updatePriceDisplay, updateButtonStyles, updateDiscountInfo]);
|
|
296
252
|
// Tooltip 组件
|
|
297
253
|
const Tooltip = ({ show, content, x, y }) => {
|
|
298
254
|
if (!show)
|
|
@@ -309,7 +265,8 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
309
265
|
};
|
|
310
266
|
return (jsxRuntime.jsx("div", { style: style, className: "bg-gray-700 dark:bg-gray-200 text-gray-100 dark:text-gray-800 text-xs leading-relaxed px-3 py-2 rounded-lg shadow-lg border border-gray-300 dark:border-gray-600 backdrop-blur-sm", children: content }));
|
|
311
267
|
};
|
|
312
|
-
|
|
268
|
+
// 直接渲染按钮,确保宽度占满
|
|
269
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [data.plans.map((plan) => (jsxRuntime.jsx("div", { "data-button-placeholder": plan.key, children: jsxRuntime.jsx(moneyPriceButton.MoneyPriceButton, { planKey: plan.key, userContext: userContext, billingType: billingType, onLogin: handleLogin, onUpgrade: handleUpgrade, texts: data.buttonTexts, isProcessing: isProcessing }) }, plan.key))), jsxRuntime.jsx(Tooltip, Object.assign({}, tooltip))] }));
|
|
313
270
|
}
|
|
314
271
|
|
|
315
272
|
exports.MoneyPriceInteractive = MoneyPriceInteractive;
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { __awaiter } from '../../node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.2/node_modules/tslib/tslib.es6.mjs';
|
|
3
|
-
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
4
4
|
import { useClerk } from '@clerk/nextjs';
|
|
5
|
-
import
|
|
5
|
+
import { useState, useCallback, useMemo, useEffect } from 'react';
|
|
6
6
|
import { useFingerprintContextSafe } from '../../clerk/fingerprint/fingerprint-provider.mjs';
|
|
7
7
|
import { cn } from '@windrun-huaiin/lib/utils';
|
|
8
8
|
import { useRouter } from 'next/navigation';
|
|
9
|
-
import { createRoot } from 'react-dom/client';
|
|
10
9
|
import { MoneyPriceButton } from './money-price-button.mjs';
|
|
11
|
-
import {
|
|
10
|
+
import { getActiveProviderConfig, getProductPricing } from './money-price-config-util.mjs';
|
|
12
11
|
import { UserState } from './money-price-types.mjs';
|
|
13
12
|
|
|
14
13
|
function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath }) {
|
|
15
|
-
var _a, _b, _c, _d;
|
|
16
14
|
const fingerprintContext = useFingerprintContextSafe();
|
|
17
15
|
const { redirectToSignIn } = useClerk();
|
|
18
16
|
const router = useRouter();
|
|
@@ -20,7 +18,7 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
20
18
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
21
19
|
const [tooltip, setTooltip] = useState({ show: false, content: '', x: 0, y: 0 });
|
|
22
20
|
// 确定用户状态
|
|
23
|
-
const getUserState = () => {
|
|
21
|
+
const getUserState = useCallback(() => {
|
|
24
22
|
var _a, _b;
|
|
25
23
|
if (!fingerprintContext)
|
|
26
24
|
return UserState.Anonymous;
|
|
@@ -34,34 +32,35 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
34
32
|
if ((_b = xSubscription.priceName) === null || _b === void 0 ? void 0 : _b.includes('Ultra'))
|
|
35
33
|
return UserState.UltraUser;
|
|
36
34
|
return UserState.FreeUser;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
}, [fingerprintContext]);
|
|
36
|
+
// 优化 userContext 使用 useMemo
|
|
37
|
+
const userContext = useMemo(() => {
|
|
38
|
+
var _a, _b, _c, _d;
|
|
39
|
+
return ({
|
|
40
|
+
isAuthenticated: !!((_a = fingerprintContext === null || fingerprintContext === void 0 ? void 0 : fingerprintContext.xUser) === null || _a === void 0 ? void 0 : _a.clerkUserId),
|
|
41
|
+
subscriptionStatus: getUserState(),
|
|
42
|
+
subscriptionType: ((_c = (_b = fingerprintContext === null || fingerprintContext === void 0 ? void 0 : fingerprintContext.xSubscription) === null || _b === void 0 ? void 0 : _b.priceId) === null || _c === void 0 ? void 0 : _c.includes('yearly')) ? 'yearly' : 'monthly',
|
|
43
|
+
subscriptionEndDate: (_d = fingerprintContext === null || fingerprintContext === void 0 ? void 0 : fingerprintContext.xSubscription) === null || _d === void 0 ? void 0 : _d.subPeriodEnd
|
|
44
|
+
});
|
|
45
|
+
}, [fingerprintContext, getUserState]);
|
|
44
46
|
// 处理登录
|
|
45
|
-
const handleLogin = () => {
|
|
47
|
+
const handleLogin = useCallback(() => {
|
|
46
48
|
if (signInPath) {
|
|
47
49
|
router.push(signInPath);
|
|
48
50
|
}
|
|
49
51
|
else {
|
|
50
52
|
redirectToSignIn();
|
|
51
53
|
}
|
|
52
|
-
};
|
|
54
|
+
}, [signInPath, redirectToSignIn, router]);
|
|
53
55
|
// 处理升级
|
|
54
|
-
const handleUpgrade = (plan, billingType) => __awaiter(this, void 0, void 0, function* () {
|
|
55
|
-
// 如果没有配置 API 端点,跳转到首页
|
|
56
|
+
const handleUpgrade = useCallback((plan, billingType) => __awaiter(this, void 0, void 0, function* () {
|
|
56
57
|
if (!upgradeApiEndpoint) {
|
|
57
58
|
router.push('/');
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
61
|
setIsProcessing(true);
|
|
61
62
|
try {
|
|
62
|
-
// 获取价格配置
|
|
63
63
|
const pricing = getProductPricing(plan, billingType, config.activeProvider, config);
|
|
64
|
-
// 调用 API 创建支付会话
|
|
65
64
|
const response = yield fetch(upgradeApiEndpoint, {
|
|
66
65
|
method: 'POST',
|
|
67
66
|
headers: {
|
|
@@ -76,7 +75,6 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
76
75
|
});
|
|
77
76
|
const result = yield response.json();
|
|
78
77
|
if (result.success && result.checkoutUrl) {
|
|
79
|
-
// 跳转到支付页面
|
|
80
78
|
window.location.href = result.checkoutUrl;
|
|
81
79
|
}
|
|
82
80
|
else {
|
|
@@ -89,14 +87,13 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
89
87
|
finally {
|
|
90
88
|
setIsProcessing(false);
|
|
91
89
|
}
|
|
92
|
-
});
|
|
90
|
+
}), [upgradeApiEndpoint, config, router]);
|
|
93
91
|
// 更新价格显示
|
|
94
|
-
const updatePriceDisplay = (newBillingType) => {
|
|
92
|
+
const updatePriceDisplay = useCallback((newBillingType) => {
|
|
95
93
|
const providerConfig = getActiveProviderConfig(config);
|
|
96
94
|
data.plans.forEach((plan) => {
|
|
97
95
|
const productConfig = providerConfig.products[plan.key];
|
|
98
96
|
const pricing = productConfig.plans[newBillingType];
|
|
99
|
-
// 更新价格值
|
|
100
97
|
const priceValueElement = document.querySelector(`[data-price-value="${plan.key}"]`);
|
|
101
98
|
if (priceValueElement) {
|
|
102
99
|
if (pricing.amount === 0) {
|
|
@@ -106,13 +103,11 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
106
103
|
priceValueElement.textContent = `${data.currency}${pricing.amount}`;
|
|
107
104
|
}
|
|
108
105
|
}
|
|
109
|
-
// 更新单位
|
|
110
106
|
const priceUnitElement = document.querySelector(`[data-price-unit="${plan.key}"]`);
|
|
111
107
|
if (priceUnitElement) {
|
|
112
108
|
const billingOption = data.billingSwitch.options.find(opt => opt.key === newBillingType);
|
|
113
109
|
priceUnitElement.textContent = (billingOption === null || billingOption === void 0 ? void 0 : billingOption.unit) || '/month';
|
|
114
110
|
}
|
|
115
|
-
// 更新原价和折扣
|
|
116
111
|
const priceOriginalElement = document.querySelector(`[data-price-original="${plan.key}"]`);
|
|
117
112
|
const priceDiscountElement = document.querySelector(`[data-price-discount="${plan.key}"]`);
|
|
118
113
|
if (pricing.originalAmount && pricing.discountPercent) {
|
|
@@ -134,16 +129,15 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
134
129
|
if (priceDiscountElement)
|
|
135
130
|
priceDiscountElement.style.display = 'none';
|
|
136
131
|
}
|
|
137
|
-
// 更新副标题
|
|
138
132
|
const priceSubtitleElement = document.querySelector(`[data-price-subtitle="${plan.key}"]`);
|
|
139
133
|
if (priceSubtitleElement && plan.showBillingSubTitle !== false) {
|
|
140
134
|
const billingOption = data.billingSwitch.options.find(opt => opt.key === newBillingType);
|
|
141
135
|
priceSubtitleElement.textContent = (billingOption === null || billingOption === void 0 ? void 0 : billingOption.subTitle) || '';
|
|
142
136
|
}
|
|
143
137
|
});
|
|
144
|
-
};
|
|
138
|
+
}, [config, data]);
|
|
145
139
|
// 更新按钮样式
|
|
146
|
-
const updateButtonStyles = (newBillingType) => {
|
|
140
|
+
const updateButtonStyles = useCallback((newBillingType) => {
|
|
147
141
|
const monthlyButton = document.querySelector('[data-billing-button="monthly"]');
|
|
148
142
|
const yearlyButton = document.querySelector('[data-billing-button="yearly"]');
|
|
149
143
|
const activeClasses = 'text-white bg-gradient-to-r from-purple-400 to-pink-500 hover:from-purple-500 hover:to-pink-600 dark:from-purple-500 dark:to-pink-600 dark:hover:from-purple-600 rounded-full shadow-sm';
|
|
@@ -158,15 +152,14 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
158
152
|
yearlyButton.className = cn('min-w-[120px] px-6 py-2 font-medium transition text-lg relative', activeClasses);
|
|
159
153
|
}
|
|
160
154
|
}
|
|
161
|
-
};
|
|
155
|
+
}, []);
|
|
162
156
|
// 更新折扣信息
|
|
163
|
-
const updateDiscountInfo = (newBillingType) => {
|
|
157
|
+
const updateDiscountInfo = useCallback((newBillingType) => {
|
|
164
158
|
const discountInfoElement = document.querySelector('[data-discount-info]');
|
|
165
159
|
if (!discountInfoElement)
|
|
166
160
|
return;
|
|
167
161
|
const billingOption = data.billingSwitch.options.find(opt => opt.key === newBillingType);
|
|
168
162
|
const providerConfig = getActiveProviderConfig(config);
|
|
169
|
-
// 检查是否有折扣
|
|
170
163
|
let hasDiscount = false;
|
|
171
164
|
let discountPercent = 0;
|
|
172
165
|
['pro', 'ultra'].forEach(planKey => {
|
|
@@ -177,7 +170,6 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
177
170
|
discountPercent = pricing.discountPercent;
|
|
178
171
|
}
|
|
179
172
|
});
|
|
180
|
-
// 清空内容
|
|
181
173
|
discountInfoElement.innerHTML = '';
|
|
182
174
|
if (hasDiscount && (billingOption === null || billingOption === void 0 ? void 0 : billingOption.discountText)) {
|
|
183
175
|
const discountBadge = document.createElement('span');
|
|
@@ -185,39 +177,7 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
185
177
|
discountBadge.textContent = billingOption.discountText.replace('{percent}', String(discountPercent));
|
|
186
178
|
discountInfoElement.appendChild(discountBadge);
|
|
187
179
|
}
|
|
188
|
-
};
|
|
189
|
-
// 使用 useRef 存储 root 实例,避免重复创建
|
|
190
|
-
const rootsRef = React.useRef(new Map());
|
|
191
|
-
// 动态替换按钮
|
|
192
|
-
useEffect(() => {
|
|
193
|
-
data.plans.forEach((plan) => {
|
|
194
|
-
const placeholder = document.querySelector(`[data-button-placeholder="${plan.key}"]`);
|
|
195
|
-
if (placeholder) {
|
|
196
|
-
let root = rootsRef.current.get(plan.key);
|
|
197
|
-
// 如果还没有创建 root,则创建一个
|
|
198
|
-
if (!root) {
|
|
199
|
-
root = createRoot(placeholder);
|
|
200
|
-
rootsRef.current.set(plan.key, root);
|
|
201
|
-
}
|
|
202
|
-
// 渲染按钮
|
|
203
|
-
root.render(jsx(MoneyPriceButton, { planKey: plan.key, userContext: userContext, billingType: billingType, onLogin: handleLogin, onUpgrade: handleUpgrade, texts: data.buttonTexts, isProcessing: isProcessing }));
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
}, [userContext, billingType, isProcessing, data.plans, data.buttonTexts, handleLogin, handleUpgrade]);
|
|
207
|
-
// 组件卸载时清理
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
return () => {
|
|
210
|
-
rootsRef.current.forEach((root) => {
|
|
211
|
-
try {
|
|
212
|
-
root.unmount();
|
|
213
|
-
}
|
|
214
|
-
catch (e) {
|
|
215
|
-
// 忽略卸载错误
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
rootsRef.current.clear();
|
|
219
|
-
};
|
|
220
|
-
}, []);
|
|
180
|
+
}, [config, data]);
|
|
221
181
|
// 处理月付/年付切换和 tooltip 功能
|
|
222
182
|
useEffect(() => {
|
|
223
183
|
const monthlyButton = document.querySelector('[data-billing-button="monthly"]');
|
|
@@ -240,7 +200,6 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
240
200
|
if (yearlyButton) {
|
|
241
201
|
yearlyButton.addEventListener('click', handleYearlyClick);
|
|
242
202
|
}
|
|
243
|
-
// 添加 tooltip 功能
|
|
244
203
|
const tooltipHandlers = [];
|
|
245
204
|
data.plans.forEach((plan) => {
|
|
246
205
|
var _a;
|
|
@@ -272,10 +231,8 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
272
231
|
}
|
|
273
232
|
});
|
|
274
233
|
});
|
|
275
|
-
// 初始化价格显示
|
|
276
234
|
updatePriceDisplay(billingType);
|
|
277
235
|
updateDiscountInfo(billingType);
|
|
278
|
-
// 清理
|
|
279
236
|
return () => {
|
|
280
237
|
if (monthlyButton) {
|
|
281
238
|
monthlyButton.removeEventListener('click', handleMonthlyClick);
|
|
@@ -283,14 +240,13 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
283
240
|
if (yearlyButton) {
|
|
284
241
|
yearlyButton.removeEventListener('click', handleYearlyClick);
|
|
285
242
|
}
|
|
286
|
-
// 清理 tooltip 事件监听器
|
|
287
243
|
tooltipHandlers.forEach(({ element, handlers }) => {
|
|
288
244
|
element.removeEventListener('mouseenter', handlers.mouseenter);
|
|
289
245
|
element.removeEventListener('mousemove', handlers.mousemove);
|
|
290
246
|
element.removeEventListener('mouseleave', handlers.mouseleave);
|
|
291
247
|
});
|
|
292
248
|
};
|
|
293
|
-
}, [data]);
|
|
249
|
+
}, [data, billingType, updatePriceDisplay, updateButtonStyles, updateDiscountInfo]);
|
|
294
250
|
// Tooltip 组件
|
|
295
251
|
const Tooltip = ({ show, content, x, y }) => {
|
|
296
252
|
if (!show)
|
|
@@ -307,7 +263,8 @@ function MoneyPriceInteractive({ data, config, upgradeApiEndpoint, signInPath })
|
|
|
307
263
|
};
|
|
308
264
|
return (jsx("div", { style: style, className: "bg-gray-700 dark:bg-gray-200 text-gray-100 dark:text-gray-800 text-xs leading-relaxed px-3 py-2 rounded-lg shadow-lg border border-gray-300 dark:border-gray-600 backdrop-blur-sm", children: content }));
|
|
309
265
|
};
|
|
310
|
-
|
|
266
|
+
// 直接渲染按钮,确保宽度占满
|
|
267
|
+
return (jsxs(Fragment, { children: [data.plans.map((plan) => (jsx("div", { "data-button-placeholder": plan.key, children: jsx(MoneyPriceButton, { planKey: plan.key, userContext: userContext, billingType: billingType, onLogin: handleLogin, onUpgrade: handleUpgrade, texts: data.buttonTexts, isProcessing: isProcessing }) }, plan.key))), jsx(Tooltip, Object.assign({}, tooltip))] }));
|
|
311
268
|
}
|
|
312
269
|
|
|
313
270
|
export { MoneyPriceInteractive };
|
|
@@ -2,19 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var tslib_es6 = require('../../node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.2/node_modules/tslib/tslib.es6.js');
|
|
4
4
|
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
-
require('@windrun-huaiin/base-ui/components/server');
|
|
6
|
-
require('next-themes');
|
|
7
|
-
require('react');
|
|
8
|
-
require('fumadocs-core/framework');
|
|
9
5
|
var utils = require('@windrun-huaiin/lib/utils');
|
|
10
|
-
require('next/link');
|
|
11
|
-
var gradientButton = require('../../fuma/mdx/gradient-button.js');
|
|
12
|
-
require('next/navigation');
|
|
13
|
-
require('fumadocs-ui/utils/use-copy-button');
|
|
14
|
-
require('fumadocs-core/link');
|
|
15
|
-
require('@windrun-huaiin/base-ui/ui');
|
|
16
|
-
require('fumadocs-ui/components/ui/collapsible');
|
|
17
|
-
require('../../fuma/mdx/banner.js');
|
|
18
6
|
var server = require('next-intl/server');
|
|
19
7
|
var moneyPriceConfigUtil = require('./money-price-config-util.js');
|
|
20
8
|
var moneyPriceInteractive = require('./money-price-interactive.js');
|
|
@@ -31,15 +19,11 @@ function MoneyPrice(_a) {
|
|
|
31
19
|
buttonTexts: t.raw('buttonTexts'),
|
|
32
20
|
currency: config.display.currency
|
|
33
21
|
};
|
|
34
|
-
// 获取激活的支付供应商配置
|
|
35
22
|
const providerConfig = moneyPriceConfigUtil.getActiveProviderConfig(config);
|
|
36
23
|
const minPlanFeaturesCount = config.display.minFeaturesCount;
|
|
37
|
-
// 使用默认计费类型进行静态渲染
|
|
38
24
|
const defaultBilling = data.billingSwitch.defaultKey;
|
|
39
25
|
const defaultBillingDisplay = data.billingSwitch.options.find((opt) => opt.key === defaultBilling) || data.billingSwitch.options[0];
|
|
40
|
-
// 计算特性数量
|
|
41
26
|
const maxFeaturesCount = Math.max(...data.plans.map((plan) => { var _a; return ((_a = plan.features) === null || _a === void 0 ? void 0 : _a.length) || 0; }), minPlanFeaturesCount || 0);
|
|
42
|
-
// 处理卡片高度对齐
|
|
43
27
|
const getFeatureRows = (plan) => {
|
|
44
28
|
const features = plan.features || [];
|
|
45
29
|
const filled = [...features];
|
|
@@ -47,17 +31,14 @@ function MoneyPrice(_a) {
|
|
|
47
31
|
filled.push(null);
|
|
48
32
|
return filled;
|
|
49
33
|
};
|
|
50
|
-
// 静态价格渲染逻辑
|
|
51
34
|
function renderPrice(plan, billingKey = data.billingSwitch.defaultKey) {
|
|
52
35
|
const productConfig = providerConfig.products[plan.key];
|
|
53
36
|
const pricing = productConfig.plans[billingKey];
|
|
54
37
|
const currentBillingDisplay = data.billingSwitch.options.find((opt) => opt.key === billingKey) || defaultBillingDisplay;
|
|
55
38
|
const billingSubTitle = (currentBillingDisplay === null || currentBillingDisplay === void 0 ? void 0 : currentBillingDisplay.subTitle) || '';
|
|
56
|
-
// 免费计划
|
|
57
39
|
if (pricing.amount === 0) {
|
|
58
40
|
return (jsxRuntime.jsxs("div", { className: "flex flex-col items-start w-full", "data-price-container": plan.key, children: [jsxRuntime.jsx("div", { className: "flex items-end gap-2", children: jsxRuntime.jsx("span", { className: "text-4xl font-extrabold text-gray-900 dark:text-gray-100", "data-price-value": plan.key, children: "Free" }) }), jsxRuntime.jsx("div", { className: "flex items-center gap-2 min-h-[24px] mt-1", children: jsxRuntime.jsx("span", { className: utils.cn('text-xs text-gray-700 dark:text-gray-300 font-medium', plan.showBillingSubTitle === false && 'opacity-0 select-none'), "data-price-subtitle": plan.key, children: plan.showBillingSubTitle === false ? '' : billingSubTitle }) })] }));
|
|
59
41
|
}
|
|
60
|
-
// 付费计划
|
|
61
42
|
const hasDiscount = pricing.discountPercent && pricing.discountPercent > 0;
|
|
62
43
|
const unit = currentBillingDisplay.unit || '';
|
|
63
44
|
let discountText = '';
|
|
@@ -72,7 +53,6 @@ function MoneyPrice(_a) {
|
|
|
72
53
|
? 'text-white bg-gradient-to-r from-purple-400 to-pink-500 hover:from-purple-500 hover:to-pink-600 dark:from-purple-500 dark:to-pink-600 dark:hover:from-purple-600 rounded-full shadow-sm'
|
|
73
54
|
: 'text-gray-800 dark:text-gray-200 hover:text-gray-900 dark:hover:text-gray-100 rounded-full'), "data-billing-button": "yearly", type: "button", children: ((_c = data.billingSwitch.options.find((opt) => opt.key === 'yearly')) === null || _c === void 0 ? void 0 : _c.name) || 'Yearly' })] }) }), jsxRuntime.jsx("div", { className: "h-8 flex items-center justify-center mb-3", "data-discount-info": true, children: (() => {
|
|
74
55
|
const opt = data.billingSwitch.options.find((opt) => opt.key === data.billingSwitch.defaultKey);
|
|
75
|
-
// 检查默认计费类型是否有折扣
|
|
76
56
|
let hasDiscount = false;
|
|
77
57
|
let discountPercent = 0;
|
|
78
58
|
['pro', 'ultra'].forEach(planKey => {
|
|
@@ -86,7 +66,7 @@ function MoneyPrice(_a) {
|
|
|
86
66
|
if (!(opt && hasDiscount && opt.discountText))
|
|
87
67
|
return null;
|
|
88
68
|
return (jsxRuntime.jsx("span", { className: "px-2 py-1 text-xs rounded bg-yellow-100 text-yellow-800 font-semibold align-middle text-center inline-flex items-center justify-center whitespace-nowrap", children: opt.discountText.replace('{percent}', String(discountPercent)) }));
|
|
89
|
-
})() })] }), jsxRuntime.jsx("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-8", children: data.plans.map((plan, _idx) => (jsxRuntime.jsxs("div", { "data-price-plan": plan.key, className: utils.cn('flex flex-col bg-white dark:bg-gray-800/60 rounded-2xl border border-gray-300 dark:border-[#7c3aed40] transition p-8 h-full shadow-sm dark:shadow-none', 'hover:border-2 hover:border-purple-500', 'focus-within:border-2 focus-within:border-purple-500'), style: { minHeight: maxFeaturesCount * 100 }, children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-2", children: [jsxRuntime.jsx("span", { className: "text-xl font-bold text-gray-900 dark:text-gray-100", children: plan.title }), plan.titleTags && plan.titleTags.map((tag, i) => (jsxRuntime.jsx("span", { className: "px-2 py-0.5 text-xs rounded bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200 font-semibold align-middle", children: tag }, i)))] }), renderPrice(plan), jsxRuntime.jsx("ul", { className: "flex-1 mb-6 mt-4", children: getFeatureRows(plan).map((feature, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-2 mb-2 min-h-[28px]", "data-feature-item": `${plan.key}-${i}`, children: [feature ? (jsxRuntime.jsx("span", { className: "inline-flex items-center justify-center w-5 h-5 rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200 mr-1", children: feature.icon ? jsxRuntime.jsx("span", { children: feature.icon }) : jsxRuntime.jsx("span", { className: "font-bold", children: "\u2713" }) })) : (jsxRuntime.jsx("span", { className: "inline-flex items-center justify-center w-5 h-5 rounded-full mr-1", children: "\u00A0" })), feature && feature.tag && (jsxRuntime.jsx("span", { className: "px-1 py-0.5 text-[6px] rounded bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 font-semibold align-middle", children: feature.tag })), feature ? (jsxRuntime.jsxs("span", { className: "relative group cursor-pointer text-sm text-gray-800 dark:text-gray-200", children: [feature.description, feature.tooltip && (jsxRuntime.jsx("span", { className: "ml-1 align-middle inline-flex", "data-tooltip-trigger": `${plan.key}-${i}`, "data-tooltip-content": feature.tooltip, children: jsxRuntime.jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }) }))] })) : (jsxRuntime.jsx("span", { children: "\u00A0" }))] }, i))) }), jsxRuntime.jsx("div", { className: "flex-1" }), jsxRuntime.jsx("div", { "data-button-placeholder": plan.key,
|
|
69
|
+
})() })] }), jsxRuntime.jsx("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-8", children: data.plans.map((plan, _idx) => (jsxRuntime.jsxs("div", { "data-price-plan": plan.key, className: utils.cn('flex flex-col bg-white dark:bg-gray-800/60 rounded-2xl border border-gray-300 dark:border-[#7c3aed40] transition p-8 h-full shadow-sm dark:shadow-none', 'hover:border-2 hover:border-purple-500', 'focus-within:border-2 focus-within:border-purple-500'), style: { minHeight: maxFeaturesCount * 100 }, children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-2", children: [jsxRuntime.jsx("span", { className: "text-xl font-bold text-gray-900 dark:text-gray-100", children: plan.title }), plan.titleTags && plan.titleTags.map((tag, i) => (jsxRuntime.jsx("span", { className: "px-2 py-0.5 text-xs rounded bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200 font-semibold align-middle", children: tag }, i)))] }), renderPrice(plan), jsxRuntime.jsx("ul", { className: "flex-1 mb-6 mt-4", children: getFeatureRows(plan).map((feature, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-2 mb-2 min-h-[28px]", "data-feature-item": `${plan.key}-${i}`, children: [feature ? (jsxRuntime.jsx("span", { className: "inline-flex items-center justify-center w-5 h-5 rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200 mr-1", children: feature.icon ? jsxRuntime.jsx("span", { children: feature.icon }) : jsxRuntime.jsx("span", { className: "font-bold", children: "\u2713" }) })) : (jsxRuntime.jsx("span", { className: "inline-flex items-center justify-center w-5 h-5 rounded-full mr-1", children: "\u00A0" })), feature && feature.tag && (jsxRuntime.jsx("span", { className: "px-1 py-0.5 text-[6px] rounded bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 font-semibold align-middle", children: feature.tag })), feature ? (jsxRuntime.jsxs("span", { className: "relative group cursor-pointer text-sm text-gray-800 dark:text-gray-200", children: [feature.description, feature.tooltip && (jsxRuntime.jsx("span", { className: "ml-1 align-middle inline-flex", "data-tooltip-trigger": `${plan.key}-${i}`, "data-tooltip-content": feature.tooltip, children: jsxRuntime.jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }) }))] })) : (jsxRuntime.jsx("span", { children: "\u00A0" }))] }, i))) }), jsxRuntime.jsx("div", { className: "flex-1" }), jsxRuntime.jsx("div", { "data-button-placeholder": plan.key, className: "w-full" })] }, plan.key))) }), jsxRuntime.jsx(moneyPriceInteractive.MoneyPriceInteractive, { data: data, config: config, upgradeApiEndpoint: upgradeApiEndpoint, signInPath: signInPath })] }));
|
|
90
70
|
});
|
|
91
71
|
}
|
|
92
72
|
|
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
import { __awaiter } from '../../node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.2/node_modules/tslib/tslib.es6.mjs';
|
|
2
2
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
3
|
-
import '@windrun-huaiin/base-ui/components/server';
|
|
4
|
-
import 'next-themes';
|
|
5
|
-
import 'react';
|
|
6
|
-
import 'fumadocs-core/framework';
|
|
7
3
|
import { cn } from '@windrun-huaiin/lib/utils';
|
|
8
|
-
import 'next/link';
|
|
9
|
-
import { GradientButton } from '../../fuma/mdx/gradient-button.mjs';
|
|
10
|
-
import 'next/navigation';
|
|
11
|
-
import 'fumadocs-ui/utils/use-copy-button';
|
|
12
|
-
import 'fumadocs-core/link';
|
|
13
|
-
import '@windrun-huaiin/base-ui/ui';
|
|
14
|
-
import 'fumadocs-ui/components/ui/collapsible';
|
|
15
|
-
import '../../fuma/mdx/banner.mjs';
|
|
16
4
|
import { getTranslations } from 'next-intl/server';
|
|
17
5
|
import { getActiveProviderConfig } from './money-price-config-util.mjs';
|
|
18
6
|
import { MoneyPriceInteractive } from './money-price-interactive.mjs';
|
|
@@ -29,15 +17,11 @@ function MoneyPrice(_a) {
|
|
|
29
17
|
buttonTexts: t.raw('buttonTexts'),
|
|
30
18
|
currency: config.display.currency
|
|
31
19
|
};
|
|
32
|
-
// 获取激活的支付供应商配置
|
|
33
20
|
const providerConfig = getActiveProviderConfig(config);
|
|
34
21
|
const minPlanFeaturesCount = config.display.minFeaturesCount;
|
|
35
|
-
// 使用默认计费类型进行静态渲染
|
|
36
22
|
const defaultBilling = data.billingSwitch.defaultKey;
|
|
37
23
|
const defaultBillingDisplay = data.billingSwitch.options.find((opt) => opt.key === defaultBilling) || data.billingSwitch.options[0];
|
|
38
|
-
// 计算特性数量
|
|
39
24
|
const maxFeaturesCount = Math.max(...data.plans.map((plan) => { var _a; return ((_a = plan.features) === null || _a === void 0 ? void 0 : _a.length) || 0; }), minPlanFeaturesCount || 0);
|
|
40
|
-
// 处理卡片高度对齐
|
|
41
25
|
const getFeatureRows = (plan) => {
|
|
42
26
|
const features = plan.features || [];
|
|
43
27
|
const filled = [...features];
|
|
@@ -45,17 +29,14 @@ function MoneyPrice(_a) {
|
|
|
45
29
|
filled.push(null);
|
|
46
30
|
return filled;
|
|
47
31
|
};
|
|
48
|
-
// 静态价格渲染逻辑
|
|
49
32
|
function renderPrice(plan, billingKey = data.billingSwitch.defaultKey) {
|
|
50
33
|
const productConfig = providerConfig.products[plan.key];
|
|
51
34
|
const pricing = productConfig.plans[billingKey];
|
|
52
35
|
const currentBillingDisplay = data.billingSwitch.options.find((opt) => opt.key === billingKey) || defaultBillingDisplay;
|
|
53
36
|
const billingSubTitle = (currentBillingDisplay === null || currentBillingDisplay === void 0 ? void 0 : currentBillingDisplay.subTitle) || '';
|
|
54
|
-
// 免费计划
|
|
55
37
|
if (pricing.amount === 0) {
|
|
56
38
|
return (jsxs("div", { className: "flex flex-col items-start w-full", "data-price-container": plan.key, children: [jsx("div", { className: "flex items-end gap-2", children: jsx("span", { className: "text-4xl font-extrabold text-gray-900 dark:text-gray-100", "data-price-value": plan.key, children: "Free" }) }), jsx("div", { className: "flex items-center gap-2 min-h-[24px] mt-1", children: jsx("span", { className: cn('text-xs text-gray-700 dark:text-gray-300 font-medium', plan.showBillingSubTitle === false && 'opacity-0 select-none'), "data-price-subtitle": plan.key, children: plan.showBillingSubTitle === false ? '' : billingSubTitle }) })] }));
|
|
57
39
|
}
|
|
58
|
-
// 付费计划
|
|
59
40
|
const hasDiscount = pricing.discountPercent && pricing.discountPercent > 0;
|
|
60
41
|
const unit = currentBillingDisplay.unit || '';
|
|
61
42
|
let discountText = '';
|
|
@@ -70,7 +51,6 @@ function MoneyPrice(_a) {
|
|
|
70
51
|
? 'text-white bg-gradient-to-r from-purple-400 to-pink-500 hover:from-purple-500 hover:to-pink-600 dark:from-purple-500 dark:to-pink-600 dark:hover:from-purple-600 rounded-full shadow-sm'
|
|
71
52
|
: 'text-gray-800 dark:text-gray-200 hover:text-gray-900 dark:hover:text-gray-100 rounded-full'), "data-billing-button": "yearly", type: "button", children: ((_c = data.billingSwitch.options.find((opt) => opt.key === 'yearly')) === null || _c === void 0 ? void 0 : _c.name) || 'Yearly' })] }) }), jsx("div", { className: "h-8 flex items-center justify-center mb-3", "data-discount-info": true, children: (() => {
|
|
72
53
|
const opt = data.billingSwitch.options.find((opt) => opt.key === data.billingSwitch.defaultKey);
|
|
73
|
-
// 检查默认计费类型是否有折扣
|
|
74
54
|
let hasDiscount = false;
|
|
75
55
|
let discountPercent = 0;
|
|
76
56
|
['pro', 'ultra'].forEach(planKey => {
|
|
@@ -84,7 +64,7 @@ function MoneyPrice(_a) {
|
|
|
84
64
|
if (!(opt && hasDiscount && opt.discountText))
|
|
85
65
|
return null;
|
|
86
66
|
return (jsx("span", { className: "px-2 py-1 text-xs rounded bg-yellow-100 text-yellow-800 font-semibold align-middle text-center inline-flex items-center justify-center whitespace-nowrap", children: opt.discountText.replace('{percent}', String(discountPercent)) }));
|
|
87
|
-
})() })] }), jsx("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-8", children: data.plans.map((plan, _idx) => (jsxs("div", { "data-price-plan": plan.key, className: cn('flex flex-col bg-white dark:bg-gray-800/60 rounded-2xl border border-gray-300 dark:border-[#7c3aed40] transition p-8 h-full shadow-sm dark:shadow-none', 'hover:border-2 hover:border-purple-500', 'focus-within:border-2 focus-within:border-purple-500'), style: { minHeight: maxFeaturesCount * 100 }, children: [jsxs("div", { className: "flex items-center gap-2 mb-2", children: [jsx("span", { className: "text-xl font-bold text-gray-900 dark:text-gray-100", children: plan.title }), plan.titleTags && plan.titleTags.map((tag, i) => (jsx("span", { className: "px-2 py-0.5 text-xs rounded bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200 font-semibold align-middle", children: tag }, i)))] }), renderPrice(plan), jsx("ul", { className: "flex-1 mb-6 mt-4", children: getFeatureRows(plan).map((feature, i) => (jsxs("li", { className: "flex items-center gap-2 mb-2 min-h-[28px]", "data-feature-item": `${plan.key}-${i}`, children: [feature ? (jsx("span", { className: "inline-flex items-center justify-center w-5 h-5 rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200 mr-1", children: feature.icon ? jsx("span", { children: feature.icon }) : jsx("span", { className: "font-bold", children: "\u2713" }) })) : (jsx("span", { className: "inline-flex items-center justify-center w-5 h-5 rounded-full mr-1", children: "\u00A0" })), feature && feature.tag && (jsx("span", { className: "px-1 py-0.5 text-[6px] rounded bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 font-semibold align-middle", children: feature.tag })), feature ? (jsxs("span", { className: "relative group cursor-pointer text-sm text-gray-800 dark:text-gray-200", children: [feature.description, feature.tooltip && (jsx("span", { className: "ml-1 align-middle inline-flex", "data-tooltip-trigger": `${plan.key}-${i}`, "data-tooltip-content": feature.tooltip, children: jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }) }))] })) : (jsx("span", { children: "\u00A0" }))] }, i))) }), jsx("div", { className: "flex-1" }), jsx("div", { "data-button-placeholder": plan.key,
|
|
67
|
+
})() })] }), jsx("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-8", children: data.plans.map((plan, _idx) => (jsxs("div", { "data-price-plan": plan.key, className: cn('flex flex-col bg-white dark:bg-gray-800/60 rounded-2xl border border-gray-300 dark:border-[#7c3aed40] transition p-8 h-full shadow-sm dark:shadow-none', 'hover:border-2 hover:border-purple-500', 'focus-within:border-2 focus-within:border-purple-500'), style: { minHeight: maxFeaturesCount * 100 }, children: [jsxs("div", { className: "flex items-center gap-2 mb-2", children: [jsx("span", { className: "text-xl font-bold text-gray-900 dark:text-gray-100", children: plan.title }), plan.titleTags && plan.titleTags.map((tag, i) => (jsx("span", { className: "px-2 py-0.5 text-xs rounded bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200 font-semibold align-middle", children: tag }, i)))] }), renderPrice(plan), jsx("ul", { className: "flex-1 mb-6 mt-4", children: getFeatureRows(plan).map((feature, i) => (jsxs("li", { className: "flex items-center gap-2 mb-2 min-h-[28px]", "data-feature-item": `${plan.key}-${i}`, children: [feature ? (jsx("span", { className: "inline-flex items-center justify-center w-5 h-5 rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200 mr-1", children: feature.icon ? jsx("span", { children: feature.icon }) : jsx("span", { className: "font-bold", children: "\u2713" }) })) : (jsx("span", { className: "inline-flex items-center justify-center w-5 h-5 rounded-full mr-1", children: "\u00A0" })), feature && feature.tag && (jsx("span", { className: "px-1 py-0.5 text-[6px] rounded bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 font-semibold align-middle", children: feature.tag })), feature ? (jsxs("span", { className: "relative group cursor-pointer text-sm text-gray-800 dark:text-gray-200", children: [feature.description, feature.tooltip && (jsx("span", { className: "ml-1 align-middle inline-flex", "data-tooltip-trigger": `${plan.key}-${i}`, "data-tooltip-content": feature.tooltip, children: jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }) }))] })) : (jsx("span", { children: "\u00A0" }))] }, i))) }), jsx("div", { className: "flex-1" }), jsx("div", { "data-button-placeholder": plan.key, className: "w-full" })] }, plan.key))) }), jsx(MoneyPriceInteractive, { data: data, config: config, upgradeApiEndpoint: upgradeApiEndpoint, signInPath: signInPath })] }));
|
|
88
68
|
});
|
|
89
69
|
}
|
|
90
70
|
|
package/dist/main/price-plan.js
CHANGED
|
@@ -14,7 +14,6 @@ var pricePlanInteractive = require('./price-plan-interactive.js');
|
|
|
14
14
|
require('@clerk/nextjs');
|
|
15
15
|
require('../clerk/fingerprint/fingerprint-provider.js');
|
|
16
16
|
require('next/navigation');
|
|
17
|
-
require('react-dom/client');
|
|
18
17
|
require('next-themes');
|
|
19
18
|
require('fumadocs-core/framework');
|
|
20
19
|
require('next/link');
|
package/dist/main/price-plan.mjs
CHANGED
|
@@ -12,7 +12,6 @@ import { PricePlanInteractive } from './price-plan-interactive.mjs';
|
|
|
12
12
|
import '@clerk/nextjs';
|
|
13
13
|
import '../clerk/fingerprint/fingerprint-provider.mjs';
|
|
14
14
|
import 'next/navigation';
|
|
15
|
-
import 'react-dom/client';
|
|
16
15
|
import 'next-themes';
|
|
17
16
|
import 'fumadocs-core/framework';
|
|
18
17
|
import 'next/link';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __module as coseBase$1 } from '../../../../../_virtual/cose-
|
|
1
|
+
import { __module as coseBase$1 } from '../../../../../_virtual/cose-base.mjs';
|
|
2
2
|
import { __require as requireLayoutBase } from '../../../layout-base@1.0.2/node_modules/layout-base/layout-base.mjs';
|
|
3
3
|
|
|
4
4
|
var coseBase = coseBase$1.exports;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __module as coseBase$1 } from '../../../../../_virtual/cose-
|
|
1
|
+
import { __module as coseBase$1 } from '../../../../../_virtual/cose-base2.mjs';
|
|
2
2
|
import { __require as requireLayoutBase } from '../../../layout-base@2.0.1/node_modules/layout-base/layout-base.mjs';
|
|
3
3
|
|
|
4
4
|
var coseBase = coseBase$1.exports;
|
package/package.json
CHANGED
|
@@ -4,8 +4,7 @@ import { useClerk } from '@clerk/nextjs';
|
|
|
4
4
|
import { useFingerprintContextSafe } from '@third-ui/clerk/fingerprint';
|
|
5
5
|
import { cn } from '@windrun-huaiin/lib/utils';
|
|
6
6
|
import { useRouter } from 'next/navigation';
|
|
7
|
-
import React, { useEffect, useState } from 'react';
|
|
8
|
-
import { createRoot } from 'react-dom/client';
|
|
7
|
+
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
|
9
8
|
import { MoneyPriceButton } from './money-price-button';
|
|
10
9
|
import { getActiveProviderConfig, getProductPricing } from './money-price-config-util';
|
|
11
10
|
import {
|
|
@@ -33,9 +32,9 @@ export function MoneyPriceInteractive({
|
|
|
33
32
|
x: number;
|
|
34
33
|
y: number;
|
|
35
34
|
}>({ show: false, content: '', x: 0, y: 0 });
|
|
36
|
-
|
|
35
|
+
|
|
37
36
|
// 确定用户状态
|
|
38
|
-
const getUserState = (): UserState => {
|
|
37
|
+
const getUserState = useCallback((): UserState => {
|
|
39
38
|
if (!fingerprintContext) return UserState.Anonymous;
|
|
40
39
|
const { xUser, xSubscription } = fingerprintContext;
|
|
41
40
|
|
|
@@ -44,27 +43,27 @@ export function MoneyPriceInteractive({
|
|
|
44
43
|
if (xSubscription.priceName?.includes('Pro')) return UserState.ProUser;
|
|
45
44
|
if (xSubscription.priceName?.includes('Ultra')) return UserState.UltraUser;
|
|
46
45
|
return UserState.FreeUser;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
}, [fingerprintContext]);
|
|
47
|
+
|
|
48
|
+
// 优化 userContext 使用 useMemo
|
|
49
|
+
const userContext = useMemo<UserContext>(() => ({
|
|
50
50
|
isAuthenticated: !!fingerprintContext?.xUser?.clerkUserId,
|
|
51
51
|
subscriptionStatus: getUserState(),
|
|
52
52
|
subscriptionType: fingerprintContext?.xSubscription?.priceId?.includes('yearly') ? 'yearly' : 'monthly',
|
|
53
53
|
subscriptionEndDate: fingerprintContext?.xSubscription?.subPeriodEnd
|
|
54
|
-
};
|
|
55
|
-
|
|
54
|
+
}), [fingerprintContext, getUserState]);
|
|
55
|
+
|
|
56
56
|
// 处理登录
|
|
57
|
-
const handleLogin = () => {
|
|
57
|
+
const handleLogin = useCallback(() => {
|
|
58
58
|
if (signInPath) {
|
|
59
59
|
router.push(signInPath);
|
|
60
60
|
} else {
|
|
61
61
|
redirectToSignIn();
|
|
62
62
|
}
|
|
63
|
-
};
|
|
64
|
-
|
|
63
|
+
}, [signInPath, redirectToSignIn, router]);
|
|
64
|
+
|
|
65
65
|
// 处理升级
|
|
66
|
-
const handleUpgrade = async (plan: string, billingType: string) => {
|
|
67
|
-
// 如果没有配置 API 端点,跳转到首页
|
|
66
|
+
const handleUpgrade = useCallback(async (plan: string, billingType: string) => {
|
|
68
67
|
if (!upgradeApiEndpoint) {
|
|
69
68
|
router.push('/');
|
|
70
69
|
return;
|
|
@@ -72,7 +71,6 @@ export function MoneyPriceInteractive({
|
|
|
72
71
|
|
|
73
72
|
setIsProcessing(true);
|
|
74
73
|
try {
|
|
75
|
-
// 获取价格配置
|
|
76
74
|
const pricing = getProductPricing(
|
|
77
75
|
plan as 'free' | 'pro' | 'ultra',
|
|
78
76
|
billingType as 'monthly' | 'yearly',
|
|
@@ -80,7 +78,6 @@ export function MoneyPriceInteractive({
|
|
|
80
78
|
config
|
|
81
79
|
);
|
|
82
80
|
|
|
83
|
-
// 调用 API 创建支付会话
|
|
84
81
|
const response = await fetch(upgradeApiEndpoint, {
|
|
85
82
|
method: 'POST',
|
|
86
83
|
headers: {
|
|
@@ -97,7 +94,6 @@ export function MoneyPriceInteractive({
|
|
|
97
94
|
const result = await response.json();
|
|
98
95
|
|
|
99
96
|
if (result.success && result.checkoutUrl) {
|
|
100
|
-
// 跳转到支付页面
|
|
101
97
|
window.location.href = result.checkoutUrl;
|
|
102
98
|
} else {
|
|
103
99
|
console.error('Failed to create checkout session:', result.error);
|
|
@@ -107,17 +103,16 @@ export function MoneyPriceInteractive({
|
|
|
107
103
|
} finally {
|
|
108
104
|
setIsProcessing(false);
|
|
109
105
|
}
|
|
110
|
-
};
|
|
111
|
-
|
|
106
|
+
}, [upgradeApiEndpoint, config, router]);
|
|
107
|
+
|
|
112
108
|
// 更新价格显示
|
|
113
|
-
const updatePriceDisplay = (newBillingType: string) => {
|
|
109
|
+
const updatePriceDisplay = useCallback((newBillingType: string) => {
|
|
114
110
|
const providerConfig = getActiveProviderConfig(config);
|
|
115
111
|
|
|
116
112
|
data.plans.forEach((plan: any) => {
|
|
117
113
|
const productConfig = providerConfig.products[plan.key as 'free' | 'pro' | 'ultra'];
|
|
118
114
|
const pricing = productConfig.plans[newBillingType as 'monthly' | 'yearly'];
|
|
119
115
|
|
|
120
|
-
// 更新价格值
|
|
121
116
|
const priceValueElement = document.querySelector(`[data-price-value="${plan.key}"]`) as HTMLElement;
|
|
122
117
|
if (priceValueElement) {
|
|
123
118
|
if (pricing.amount === 0) {
|
|
@@ -127,14 +122,12 @@ export function MoneyPriceInteractive({
|
|
|
127
122
|
}
|
|
128
123
|
}
|
|
129
124
|
|
|
130
|
-
// 更新单位
|
|
131
125
|
const priceUnitElement = document.querySelector(`[data-price-unit="${plan.key}"]`) as HTMLElement;
|
|
132
126
|
if (priceUnitElement) {
|
|
133
127
|
const billingOption = data.billingSwitch.options.find(opt => opt.key === newBillingType);
|
|
134
128
|
priceUnitElement.textContent = billingOption?.unit || '/month';
|
|
135
129
|
}
|
|
136
130
|
|
|
137
|
-
// 更新原价和折扣
|
|
138
131
|
const priceOriginalElement = document.querySelector(`[data-price-original="${plan.key}"]`) as HTMLElement;
|
|
139
132
|
const priceDiscountElement = document.querySelector(`[data-price-discount="${plan.key}"]`) as HTMLElement;
|
|
140
133
|
|
|
@@ -158,17 +151,16 @@ export function MoneyPriceInteractive({
|
|
|
158
151
|
if (priceDiscountElement) priceDiscountElement.style.display = 'none';
|
|
159
152
|
}
|
|
160
153
|
|
|
161
|
-
// 更新副标题
|
|
162
154
|
const priceSubtitleElement = document.querySelector(`[data-price-subtitle="${plan.key}"]`) as HTMLElement;
|
|
163
155
|
if (priceSubtitleElement && plan.showBillingSubTitle !== false) {
|
|
164
156
|
const billingOption = data.billingSwitch.options.find(opt => opt.key === newBillingType);
|
|
165
157
|
priceSubtitleElement.textContent = billingOption?.subTitle || '';
|
|
166
158
|
}
|
|
167
159
|
});
|
|
168
|
-
};
|
|
169
|
-
|
|
160
|
+
}, [config, data]);
|
|
161
|
+
|
|
170
162
|
// 更新按钮样式
|
|
171
|
-
const updateButtonStyles = (newBillingType: string) => {
|
|
163
|
+
const updateButtonStyles = useCallback((newBillingType: string) => {
|
|
172
164
|
const monthlyButton = document.querySelector('[data-billing-button="monthly"]') as HTMLButtonElement;
|
|
173
165
|
const yearlyButton = document.querySelector('[data-billing-button="yearly"]') as HTMLButtonElement;
|
|
174
166
|
|
|
@@ -184,17 +176,16 @@ export function MoneyPriceInteractive({
|
|
|
184
176
|
yearlyButton.className = cn('min-w-[120px] px-6 py-2 font-medium transition text-lg relative', activeClasses);
|
|
185
177
|
}
|
|
186
178
|
}
|
|
187
|
-
};
|
|
188
|
-
|
|
179
|
+
}, []);
|
|
180
|
+
|
|
189
181
|
// 更新折扣信息
|
|
190
|
-
const updateDiscountInfo = (newBillingType: string) => {
|
|
182
|
+
const updateDiscountInfo = useCallback((newBillingType: string) => {
|
|
191
183
|
const discountInfoElement = document.querySelector('[data-discount-info]') as HTMLElement;
|
|
192
184
|
if (!discountInfoElement) return;
|
|
193
185
|
|
|
194
186
|
const billingOption = data.billingSwitch.options.find(opt => opt.key === newBillingType);
|
|
195
187
|
const providerConfig = getActiveProviderConfig(config);
|
|
196
188
|
|
|
197
|
-
// 检查是否有折扣
|
|
198
189
|
let hasDiscount = false;
|
|
199
190
|
let discountPercent = 0;
|
|
200
191
|
|
|
@@ -207,7 +198,6 @@ export function MoneyPriceInteractive({
|
|
|
207
198
|
}
|
|
208
199
|
});
|
|
209
200
|
|
|
210
|
-
// 清空内容
|
|
211
201
|
discountInfoElement.innerHTML = '';
|
|
212
202
|
|
|
213
203
|
if (hasDiscount && billingOption?.discountText) {
|
|
@@ -216,54 +206,8 @@ export function MoneyPriceInteractive({
|
|
|
216
206
|
discountBadge.textContent = billingOption.discountText.replace('{percent}', String(discountPercent));
|
|
217
207
|
discountInfoElement.appendChild(discountBadge);
|
|
218
208
|
}
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
// 使用 useRef 存储 root 实例,避免重复创建
|
|
222
|
-
const rootsRef = React.useRef<Map<string, any>>(new Map());
|
|
209
|
+
}, [config, data]);
|
|
223
210
|
|
|
224
|
-
// 动态替换按钮
|
|
225
|
-
useEffect(() => {
|
|
226
|
-
data.plans.forEach((plan: any) => {
|
|
227
|
-
const placeholder = document.querySelector(`[data-button-placeholder="${plan.key}"]`);
|
|
228
|
-
if (placeholder) {
|
|
229
|
-
let root = rootsRef.current.get(plan.key);
|
|
230
|
-
|
|
231
|
-
// 如果还没有创建 root,则创建一个
|
|
232
|
-
if (!root) {
|
|
233
|
-
root = createRoot(placeholder);
|
|
234
|
-
rootsRef.current.set(plan.key, root);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// 渲染按钮
|
|
238
|
-
root.render(
|
|
239
|
-
<MoneyPriceButton
|
|
240
|
-
planKey={plan.key}
|
|
241
|
-
userContext={userContext}
|
|
242
|
-
billingType={billingType}
|
|
243
|
-
onLogin={handleLogin}
|
|
244
|
-
onUpgrade={handleUpgrade}
|
|
245
|
-
texts={data.buttonTexts}
|
|
246
|
-
isProcessing={isProcessing}
|
|
247
|
-
/>
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
}, [userContext, billingType, isProcessing, data.plans, data.buttonTexts, handleLogin, handleUpgrade]);
|
|
252
|
-
|
|
253
|
-
// 组件卸载时清理
|
|
254
|
-
useEffect(() => {
|
|
255
|
-
return () => {
|
|
256
|
-
rootsRef.current.forEach((root) => {
|
|
257
|
-
try {
|
|
258
|
-
root.unmount();
|
|
259
|
-
} catch (e) {
|
|
260
|
-
// 忽略卸载错误
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
rootsRef.current.clear();
|
|
264
|
-
};
|
|
265
|
-
}, []);
|
|
266
|
-
|
|
267
211
|
// 处理月付/年付切换和 tooltip 功能
|
|
268
212
|
useEffect(() => {
|
|
269
213
|
const monthlyButton = document.querySelector('[data-billing-button="monthly"]') as HTMLButtonElement;
|
|
@@ -290,7 +234,6 @@ export function MoneyPriceInteractive({
|
|
|
290
234
|
yearlyButton.addEventListener('click', handleYearlyClick);
|
|
291
235
|
}
|
|
292
236
|
|
|
293
|
-
// 添加 tooltip 功能
|
|
294
237
|
const tooltipHandlers: Array<{
|
|
295
238
|
element: HTMLElement;
|
|
296
239
|
handlers: {
|
|
@@ -332,11 +275,9 @@ export function MoneyPriceInteractive({
|
|
|
332
275
|
});
|
|
333
276
|
});
|
|
334
277
|
|
|
335
|
-
// 初始化价格显示
|
|
336
278
|
updatePriceDisplay(billingType);
|
|
337
279
|
updateDiscountInfo(billingType);
|
|
338
280
|
|
|
339
|
-
// 清理
|
|
340
281
|
return () => {
|
|
341
282
|
if (monthlyButton) {
|
|
342
283
|
monthlyButton.removeEventListener('click', handleMonthlyClick);
|
|
@@ -345,15 +286,14 @@ export function MoneyPriceInteractive({
|
|
|
345
286
|
yearlyButton.removeEventListener('click', handleYearlyClick);
|
|
346
287
|
}
|
|
347
288
|
|
|
348
|
-
// 清理 tooltip 事件监听器
|
|
349
289
|
tooltipHandlers.forEach(({ element, handlers }) => {
|
|
350
290
|
element.removeEventListener('mouseenter', handlers.mouseenter);
|
|
351
291
|
element.removeEventListener('mousemove', handlers.mousemove);
|
|
352
292
|
element.removeEventListener('mouseleave', handlers.mouseleave);
|
|
353
293
|
});
|
|
354
294
|
};
|
|
355
|
-
}, [data]);
|
|
356
|
-
|
|
295
|
+
}, [data, billingType, updatePriceDisplay, updateButtonStyles, updateDiscountInfo]);
|
|
296
|
+
|
|
357
297
|
// Tooltip 组件
|
|
358
298
|
const Tooltip = ({ show, content, x, y }: typeof tooltip) => {
|
|
359
299
|
if (!show) return null;
|
|
@@ -376,6 +316,24 @@ export function MoneyPriceInteractive({
|
|
|
376
316
|
</div>
|
|
377
317
|
);
|
|
378
318
|
};
|
|
379
|
-
|
|
380
|
-
|
|
319
|
+
|
|
320
|
+
// 直接渲染按钮,确保宽度占满
|
|
321
|
+
return (
|
|
322
|
+
<>
|
|
323
|
+
{data.plans.map((plan: any) => (
|
|
324
|
+
<div key={plan.key} data-button-placeholder={plan.key}>
|
|
325
|
+
<MoneyPriceButton
|
|
326
|
+
planKey={plan.key}
|
|
327
|
+
userContext={userContext}
|
|
328
|
+
billingType={billingType}
|
|
329
|
+
onLogin={handleLogin}
|
|
330
|
+
onUpgrade={handleUpgrade}
|
|
331
|
+
texts={data.buttonTexts}
|
|
332
|
+
isProcessing={isProcessing}
|
|
333
|
+
/>
|
|
334
|
+
</div>
|
|
335
|
+
))}
|
|
336
|
+
<Tooltip {...tooltip} />
|
|
337
|
+
</>
|
|
338
|
+
);
|
|
381
339
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { GradientButton } from '@third-ui/fuma/mdx';
|
|
3
2
|
import { cn } from '@windrun-huaiin/lib/utils';
|
|
4
3
|
import { getTranslations } from 'next-intl/server';
|
|
5
4
|
import { getActiveProviderConfig } from './money-price-config-util';
|
|
6
5
|
import { MoneyPriceInteractive } from './money-price-interactive';
|
|
7
|
-
import type {
|
|
6
|
+
import type { MoneyPriceProps, MoneyPriceData } from './money-price-types';
|
|
8
7
|
|
|
9
8
|
export async function MoneyPrice({
|
|
10
9
|
locale,
|
|
@@ -39,23 +38,19 @@ export async function MoneyPrice({
|
|
|
39
38
|
currency: config.display.currency
|
|
40
39
|
};
|
|
41
40
|
|
|
42
|
-
// 获取激活的支付供应商配置
|
|
43
41
|
const providerConfig = getActiveProviderConfig(config);
|
|
44
42
|
const minPlanFeaturesCount = config.display.minFeaturesCount;
|
|
45
|
-
|
|
46
|
-
// 使用默认计费类型进行静态渲染
|
|
43
|
+
|
|
47
44
|
const defaultBilling = data.billingSwitch.defaultKey;
|
|
48
45
|
const defaultBillingDisplay = data.billingSwitch.options.find(
|
|
49
46
|
(opt: any) => opt.key === defaultBilling
|
|
50
47
|
) || data.billingSwitch.options[0];
|
|
51
48
|
|
|
52
|
-
// 计算特性数量
|
|
53
49
|
const maxFeaturesCount = Math.max(
|
|
54
50
|
...data.plans.map((plan: any) => plan.features?.length || 0),
|
|
55
51
|
minPlanFeaturesCount || 0
|
|
56
52
|
);
|
|
57
53
|
|
|
58
|
-
// 处理卡片高度对齐
|
|
59
54
|
const getFeatureRows = (plan: any) => {
|
|
60
55
|
const features = plan.features || [];
|
|
61
56
|
const filled = [...features];
|
|
@@ -63,7 +58,6 @@ export async function MoneyPrice({
|
|
|
63
58
|
return filled;
|
|
64
59
|
};
|
|
65
60
|
|
|
66
|
-
// 静态价格渲染逻辑
|
|
67
61
|
function renderPrice(plan: any, billingKey = data.billingSwitch.defaultKey) {
|
|
68
62
|
const productConfig = providerConfig.products[plan.key as 'free' | 'pro' | 'ultra'];
|
|
69
63
|
const pricing = productConfig.plans[billingKey as 'monthly' | 'yearly'];
|
|
@@ -72,7 +66,6 @@ export async function MoneyPrice({
|
|
|
72
66
|
) || defaultBillingDisplay;
|
|
73
67
|
const billingSubTitle = currentBillingDisplay?.subTitle || '';
|
|
74
68
|
|
|
75
|
-
// 免费计划
|
|
76
69
|
if (pricing.amount === 0) {
|
|
77
70
|
return (
|
|
78
71
|
<div className="flex flex-col items-start w-full" data-price-container={plan.key}>
|
|
@@ -92,7 +85,6 @@ export async function MoneyPrice({
|
|
|
92
85
|
);
|
|
93
86
|
}
|
|
94
87
|
|
|
95
|
-
// 付费计划
|
|
96
88
|
const hasDiscount = pricing.discountPercent && pricing.discountPercent > 0;
|
|
97
89
|
const unit = currentBillingDisplay.unit || '';
|
|
98
90
|
let discountText = '';
|
|
@@ -140,7 +132,6 @@ export async function MoneyPrice({
|
|
|
140
132
|
|
|
141
133
|
return (
|
|
142
134
|
<section id="money-pricing" className={cn("px-4 py-10 md:px-16 md:py-16 mx-auto max-w-7xl scroll-mt-10", sectionClassName)}>
|
|
143
|
-
{/* 标题和副标题 */}
|
|
144
135
|
<h2 className="text-3xl md:text-4xl font-bold text-center mb-3">
|
|
145
136
|
{data.title}
|
|
146
137
|
</h2>
|
|
@@ -148,7 +139,6 @@ export async function MoneyPrice({
|
|
|
148
139
|
{data.subtitle}
|
|
149
140
|
</p>
|
|
150
141
|
|
|
151
|
-
{/* 计费切换按钮 */}
|
|
152
142
|
<div className="flex flex-col items-center">
|
|
153
143
|
<div className="flex items-center relative mb-3">
|
|
154
144
|
<div className="flex bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-full p-1" data-billing-switch>
|
|
@@ -179,12 +169,10 @@ export async function MoneyPrice({
|
|
|
179
169
|
</div>
|
|
180
170
|
</div>
|
|
181
171
|
|
|
182
|
-
{/* 折扣信息 - 默认计费的静态渲染 */}
|
|
183
172
|
<div className="h-8 flex items-center justify-center mb-3" data-discount-info>
|
|
184
173
|
{(() => {
|
|
185
174
|
const opt = data.billingSwitch.options.find((opt: any) => opt.key === data.billingSwitch.defaultKey);
|
|
186
175
|
|
|
187
|
-
// 检查默认计费类型是否有折扣
|
|
188
176
|
let hasDiscount = false;
|
|
189
177
|
let discountPercent = 0;
|
|
190
178
|
|
|
@@ -211,7 +199,6 @@ export async function MoneyPrice({
|
|
|
211
199
|
</div>
|
|
212
200
|
</div>
|
|
213
201
|
|
|
214
|
-
{/* 价格卡片区域 */}
|
|
215
202
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
216
203
|
{data.plans.map((plan: any, _idx: number) => (
|
|
217
204
|
<div
|
|
@@ -224,7 +211,6 @@ export async function MoneyPrice({
|
|
|
224
211
|
)}
|
|
225
212
|
style={{ minHeight: maxFeaturesCount*100 }}
|
|
226
213
|
>
|
|
227
|
-
{/* 标题和标签 */}
|
|
228
214
|
<div className="flex items-center gap-2 mb-2">
|
|
229
215
|
<span className="text-xl font-bold text-gray-900 dark:text-gray-100">{plan.title}</span>
|
|
230
216
|
{plan.titleTags && plan.titleTags.map((tag: string, i: number) => (
|
|
@@ -234,14 +220,11 @@ export async function MoneyPrice({
|
|
|
234
220
|
))}
|
|
235
221
|
</div>
|
|
236
222
|
|
|
237
|
-
{/* 价格和单位/折扣 */}
|
|
238
223
|
{renderPrice(plan)}
|
|
239
224
|
|
|
240
|
-
{/* 特性列表 */}
|
|
241
225
|
<ul className="flex-1 mb-6 mt-4">
|
|
242
226
|
{getFeatureRows(plan).map((feature: any, i: number) => (
|
|
243
227
|
<li key={i} className="flex items-center gap-2 mb-2 min-h-[28px]" data-feature-item={`${plan.key}-${i}`}>
|
|
244
|
-
{/* 图标 */}
|
|
245
228
|
{feature ? (
|
|
246
229
|
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200 mr-1">
|
|
247
230
|
{feature.icon ? <span>{feature.icon}</span> : <span className="font-bold">✓</span>}
|
|
@@ -249,13 +232,11 @@ export async function MoneyPrice({
|
|
|
249
232
|
) : (
|
|
250
233
|
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full mr-1"> </span>
|
|
251
234
|
)}
|
|
252
|
-
{/* 标签 */}
|
|
253
235
|
{feature && feature.tag && (
|
|
254
236
|
<span className="px-1 py-0.5 text-[6px] rounded bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 font-semibold align-middle">
|
|
255
237
|
{feature.tag}
|
|
256
238
|
</span>
|
|
257
239
|
)}
|
|
258
|
-
{/* 描述 + 提示 */}
|
|
259
240
|
{feature ? (
|
|
260
241
|
<span className="relative group cursor-pointer text-sm text-gray-800 dark:text-gray-200">
|
|
261
242
|
{feature.description}
|
|
@@ -279,23 +260,15 @@ export async function MoneyPrice({
|
|
|
279
260
|
))}
|
|
280
261
|
</ul>
|
|
281
262
|
|
|
282
|
-
{/* 占位符,确保卡片高度一致 */}
|
|
283
263
|
<div className="flex-1" />
|
|
284
264
|
|
|
285
|
-
{
|
|
286
|
-
|
|
287
|
-
<GradientButton
|
|
288
|
-
title={data.buttonTexts.getStarted}
|
|
289
|
-
disabled={true}
|
|
290
|
-
align="center"
|
|
291
|
-
className="w-full"
|
|
292
|
-
/>
|
|
265
|
+
<div data-button-placeholder={plan.key} className="w-full">
|
|
266
|
+
{/* MoneyPriceInteractive will render the button here */}
|
|
293
267
|
</div>
|
|
294
268
|
</div>
|
|
295
269
|
))}
|
|
296
270
|
</div>
|
|
297
271
|
|
|
298
|
-
{/* 客户端增强组件 */}
|
|
299
272
|
<MoneyPriceInteractive
|
|
300
273
|
data={data}
|
|
301
274
|
config={config}
|