vip.html 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
  7. <title>BEX会员等级</title>
  8. <link rel="stylesheet" href="/css/vip.css">
  9. </head>
  10. <body>
  11. <div class="container">
  12. <div class="header">
  13. <svg id="backButton" width="16" height="16" viewBox="0 0 16 16" fill="none"
  14. xmlns="http://www.w3.org/2000/svg">
  15. <path d="M10.4 13.6L4.8 8L10.4 2.4" stroke="white" stroke-width="1.6" stroke-linecap="round"
  16. stroke-linejoin="round" />
  17. </svg>
  18. <div class="header-title" data-i18n="headerTitle">BEX会员等级</div>
  19. <div></div>
  20. </div>
  21. <div class="vip-card">
  22. <i class="vip-bg"></i>
  23. <div class="vip-box">
  24. <div class="vip-card-header">
  25. <div class="vip-level-badge">
  26. <div class="vip-level-title" data-i18n="vipLevelTitle">会员等级</div>
  27. <div class="vip-level-text" data-field="vipLevel"></div>
  28. </div>
  29. </div>
  30. <div class="progress-items">
  31. <div class="progress-item">
  32. <div class="progress-label" data-i18n="progressContribution"> <span
  33. data-field="contribution"></span> </div>
  34. <div class="progress-bar">
  35. <div class="progress-bar-fill" data-field="contributionProgress"></div>
  36. </div>
  37. </div>
  38. <div class="progress-item">
  39. <div class="progress-label" data-i18n="progressStaking"><span data-field="staking"></span>
  40. </div>
  41. <div class="progress-bar">
  42. <div class="progress-bar-fill" data-field="stakingProgress"></div>
  43. </div>
  44. </div>
  45. </div>
  46. <div class="vip-card-tip" data-i18n="vipTip">*满足以上条件即可升级至 VIP3</div>
  47. </div>
  48. </div>
  49. <!-- <div class="section-title" data-i18n="benefitsTitle">等级权益</div> -->
  50. <div class="benefits-tabs">
  51. <div class="benefits-tab active" data-tab="requirements" data-i18n="benefitsTabRequirements">等级权益</div>
  52. <div class="benefits-tab" data-tab="benefits" data-i18n="benefitsTabBenefits">升级规则</div>
  53. </div>
  54. <div class="benefits-table">
  55. <div>
  56. <div class="benefits-header requirements-header" style="display: none;">
  57. <div data-i18n="benefitsVipLevel" class="td">会员等级</div>
  58. <div data-i18n="benefitsContribution" class="td">贡献值</div>
  59. <div data-i18n="benefitsStaking" class="td">个人质押要求</div>
  60. </div>
  61. <div class="benefits-header benefits-ben-header">
  62. <div data-i18n="benefitsVipLevel" class="td">会员等级</div>
  63. <div data-i18n="benefitsTeamReward" class="td">团队质押动态奖励</div>
  64. <div data-i18n="benefitsTradeReturn" class="td">交易返佣比例</div>
  65. </div>
  66. <div id="benefitsBody" style="display: none;"></div>
  67. <div id="benefitsBodyAlt"></div>
  68. </div>
  69. </div>
  70. <div class="contribution-section">
  71. <div class="contribution-title" data-i18n="contributionTitle">贡献值获取方式</div>
  72. <div class="contribution-card">
  73. <div class="contribution-item" data-i18n="contributionPersonal">个人质押:质押 <span class="highlight"
  74. data-field="rules.personalStaking">100枚</span> = <span class="highlight"
  75. data-i18n="contributionUnit">1个</span>贡献值</div>
  76. <div class="contribution-item" data-i18n="contributionTeam">团队质押:质押 <span class="highlight"
  77. data-field="rules.teamStaking">1000枚</span> = <span class="highlight"
  78. data-i18n="contributionUnit">1个</span>贡献值</div>
  79. <div class="contribution-item" data-i18n="contributionSpot">现货交易:团队(含个人)交易 <span class="highlight"
  80. data-field="rules.spotTrading">5000$</span> = <span class="highlight"
  81. data-i18n="contributionUnit">1个</span>贡献值</div>
  82. <div class="contribution-item" data-i18n="contributionFutures">合约交易:团队(含个人)交易 <span class="highlight"
  83. data-field="rules.futuresTrading">50,0000$</span> = <span class="highlight"
  84. data-i18n="contributionUnit">1个</span>贡献值</div>
  85. </div>
  86. <div class="contribution-tip" data-i18n="contributionTip">*降级规则: 所有级别如贡献值下降,原级别保留 1 个月;下个月如继续未达标,则降级至对应实际级别
  87. </div>
  88. </div>
  89. <div class="loading-overlay" id="loadingOverlay" style="display: none;">
  90. <div class="loading-content">
  91. <div class="loading-spinner"></div>
  92. <span class="loading-text" data-i18n="loadingText">加载中...</span>
  93. </div>
  94. </div>
  95. <div class="error-overlay" id="errorOverlay" style="display: none;">
  96. <div class="error-content">
  97. <div class="error-icon">!</div>
  98. <span class="error-text" data-i18n="errorText">加载失败,请稍后重试</span>
  99. <button class="error-retry" id="retryBtn" data-i18n="retryBtn">重试</button>
  100. </div>
  101. </div>
  102. </div>
  103. <script>
  104. (function () {
  105. const translations = {
  106. 'zh': {
  107. headerTitle: 'BEX会员等级',
  108. vipLevelTitle: '会员等级',
  109. progressContribution: '距离升级至 {nextVipLevel} 还需要 <span data-field="contribution"></span> (当前贡献值: <span data-field="personalValue"></span>)',
  110. progressStaking: '距离升级至 {nextVipLevel} 还需要个人质押 <span data-field="staking">799枚</span> (当前质押: <span data-field="personalStakeValue"></span>)',
  111. vipTip: '*满足以上条件即可升级至 {nextVipLevel}',
  112. benefitsTitle: '等级权益',
  113. benefitsTabRequirements: '等级权益',
  114. benefitsTabBenefits: '升级规则',
  115. benefitsVipLevel: '会员等级',
  116. benefitsContribution: '贡献值',
  117. benefitsStaking: '个人质押要求',
  118. benefitsTeamReward: '团队质押动态奖励',
  119. benefitsTradeReturn: '交易返佣比例',
  120. benefitsDailyLimit: '提币额度',
  121. benefitsFeeDiscount: '手续费折扣',
  122. benefitsCustomerService: '专属客服',
  123. benefitsAirDrop: '空投权益',
  124. contributionTitle: '贡献值获取方式',
  125. contributionPersonal: '个人质押:质押 <span class="highlight" data-field="rules.personalStaking">100枚</span> = <span class="highlight">1个</span>贡献值',
  126. contributionTeam: '团队质押:质押 <span class="highlight" data-field="rules.teamStaking">1000枚</span> = <span class="highlight">1个</span>贡献值',
  127. contributionSpot: '现货交易:团队(含个人)交易 <span class="highlight" data-field="rules.spotTrading">5000$</span> = <span class="highlight">1个</span>贡献值',
  128. contributionFutures: '合约交易:团队(含个人)交易 <span class="highlight" data-field="rules.futuresTrading">50,0000$</span> = <span class="highlight">1个</span>贡献值',
  129. contributionTip: '*降级规则: 所有级别如贡献值下降,原级别保留 1 个月;下个月如继续未达标,则降级至对应实际级别',
  130. loadingText: '加载中...',
  131. errorText: '加载失败,请稍后重试',
  132. retryBtn: '重试',
  133. contributionUnit: '1个',
  134. stakingUnit: '枚',
  135. teamRewardUnit: '%',
  136. tradeReturnUnit: '%'
  137. },
  138. 'en': {
  139. headerTitle: 'BEX VIP Level',
  140. vipLevelTitle: 'VIP Level',
  141. progressContribution: 'Need <span data-field="contribution"></span> more contribution points to upgrade to {nextVipLevel} (Current: <span data-field="personalValue"></span>)',
  142. progressStaking: 'Need <span data-field="staking">799</span> more staking coins to upgrade to {nextVipLevel} (Current: <span data-field="personalStakeValue"></span>)',
  143. vipTip: '*Meet the above conditions to upgrade to {nextVipLevel}',
  144. benefitsTitle: 'Level Benefits',
  145. benefitsTabRequirements: 'Upgrade Requirements',
  146. benefitsTabBenefits: 'Benefits',
  147. benefitsVipLevel: 'VIP Level',
  148. benefitsContribution: 'Contribution',
  149. benefitsStaking: 'Personal Staking',
  150. benefitsTeamReward: 'Team Reward',
  151. benefitsTradeReturn: 'Team commission ratio',
  152. benefitsDailyLimit: 'Withdrawal Limit',
  153. benefitsFeeDiscount: 'Fee Discount',
  154. benefitsCustomerService: 'VIP Support',
  155. benefitsAirDrop: 'Airdrop',
  156. contributionTitle: 'How to Earn Contribution',
  157. contributionPersonal: 'Personal Staking: Stake <span class="highlight" data-field="rules.personalStaking">100 coins</span> = <span class="highlight">1</span> contribution',
  158. contributionTeam: 'Team Staking: Stake <span class="highlight" data-field="rules.teamStaking">1000 coins</span> = <span class="highlight">1</span> contribution',
  159. contributionSpot: 'Spot Trading: Team(including personal) trade <span class="highlight" data-field="rules.spotTrading">5000$</span> = <span class="highlight">1</span> contribution',
  160. contributionFutures: 'Futures Trading: Team(including personal) trade <span class="highlight" data-field="rules.futuresTrading">50,0000$</span> = <span class="highlight">1</span> contribution',
  161. contributionTip: '*Downgrade Rule: If contribution drops, current level is retained for 1 month; if still not met next month, downgrade to actual level',
  162. loadingText: 'Loading...',
  163. errorText: 'Failed to load, please try again later',
  164. retryBtn: 'Retry',
  165. contributionUnit: '1',
  166. stakingUnit: 'coins',
  167. teamRewardUnit: '%',
  168. tradeReturnUnit: '%'
  169. }
  170. };
  171. function getUrlParam(name) {
  172. const urlParams = new URLSearchParams(window.location.search);
  173. return urlParams.get(name);
  174. }
  175. function getCurrentLang() {
  176. const lang = getUrlParam('lang');
  177. let l = '';
  178. if (lang === 'zh' || lang === 'zh-CN' || lang === 'zh-TW' || lang === 'zh-HK') {
  179. l = 'zh';
  180. } else {
  181. l = 'en';
  182. }
  183. return l;
  184. }
  185. function applyTranslations(lang, nextVipLevel) {
  186. const translation = translations[lang] || translations['en'];
  187. const elements = document.querySelectorAll('[data-i18n]');
  188. elements.forEach(element => {
  189. const key = element.dataset.i18n;
  190. if (translation[key]) {
  191. let text = translation[key];
  192. if (nextVipLevel) {
  193. text = text.replace(/{nextVipLevel}/g, 'VIP' + nextVipLevel);
  194. }
  195. element.innerHTML = text;
  196. }
  197. });
  198. document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
  199. }
  200. const API_URL = '/api/v1';
  201. const loadingOverlay = document.getElementById('loadingOverlay');
  202. const errorOverlay = document.getElementById('errorOverlay');
  203. const retryBtn = document.getElementById('retryBtn');
  204. const benefitsBody = document.getElementById('benefitsBody');
  205. const benefitsBodyAlt = document.getElementById('benefitsBodyAlt');
  206. const backButton = document.getElementById('backButton');
  207. const benefitsTabs = document.querySelectorAll('.benefits-tab');
  208. benefitsTabs.forEach(tab => {
  209. tab.addEventListener('click', function () {
  210. const tabName = this.dataset.tab;
  211. benefitsTabs.forEach(t => t.classList.remove('active'));
  212. this.classList.add('active');
  213. if (tabName === 'requirements') {
  214. document.querySelector('.requirements-header.requirements-header').style.display = 'none';
  215. document.querySelector('.benefits-header.benefits-ben-header').style.display = 'grid';
  216. benefitsBody.style.display = 'none';
  217. benefitsBodyAlt.style.display = 'block';
  218. } else {
  219. document.querySelector('.requirements-header.requirements-header').style.display = 'grid';
  220. document.querySelector('.benefits-header.benefits-ben-header').style.display = 'none';
  221. benefitsBody.style.display = 'block';
  222. benefitsBodyAlt.style.display = 'none';
  223. }
  224. });
  225. });
  226. backButton.addEventListener('click', function () {
  227. if (window.flutter_inappwebview && window.flutter_inappwebview.callHandler) {
  228. window.flutter_inappwebview.callHandler('closeWebview');
  229. }
  230. });
  231. function showLoading() {
  232. loadingOverlay.style.display = 'flex';
  233. errorOverlay.style.display = 'none';
  234. }
  235. function hideLoading() {
  236. loadingOverlay.style.display = 'none';
  237. }
  238. function showError() {
  239. loadingOverlay.style.display = 'none';
  240. errorOverlay.style.display = 'flex';
  241. }
  242. function renderBenefitsRows(vipList) {
  243. if (!Array.isArray(vipList) || vipList.length === 0) {
  244. console.warn('VIP列表数据为空或格式错误');
  245. return;
  246. }
  247. benefitsBody.innerHTML = '';
  248. const lang = getCurrentLang();
  249. const translation = translations[lang] || translations['en'];
  250. const stakingUnit = lang === 'zh' ? '枚' : 'coins';
  251. vipList.forEach(item => {
  252. const row = document.createElement('div');
  253. row.className = 'benefits-row';
  254. row.dataset.vip = item.vipLevel;
  255. row.innerHTML = `
  256. <div class="vip-name td">
  257. <img src="/images/vip${item.vipLevel}.png" alt="${item.vipName}">
  258. <span>${item.vipName}</span>
  259. </div>
  260. <div class="td">${item.contributionValue}</div>
  261. <div class="td">${item.personalPledgeRequirements}${stakingUnit}</div>
  262. `;
  263. benefitsBody.appendChild(row);
  264. });
  265. }
  266. function renderBenefitsAltRows(vipList) {
  267. if (!Array.isArray(vipList) || vipList.length === 0) {
  268. console.warn('VIP列表数据为空或格式错误');
  269. return;
  270. }
  271. benefitsBodyAlt.innerHTML = '';
  272. const lang = getCurrentLang();
  273. const translation = translations[lang] || translations['en'];
  274. const stakingUnit = lang === 'zh' ? '枚' : 'coins';
  275. vipList.forEach(item => {
  276. const row = document.createElement('div');
  277. row.className = 'benefits-row';
  278. row.dataset.vip = item.vipLevel;
  279. row.innerHTML = `
  280. <div class="vip-name td">
  281. <img src="/images/vip${item.vipLevel}.png" alt="${item.vipName}">
  282. <span>${item.vipName}</span>
  283. </div>
  284. <div class="td">${item.teamStakingDynamicRewards}${translation.teamRewardUnit}</div>
  285. <div class="td">${item.transactionRefundRatio}${translation.tradeReturnUnit}</div>
  286. `;
  287. benefitsBodyAlt.appendChild(row);
  288. });
  289. }
  290. function insertData(data) {
  291. const vipBenefitManagements = data.vipBenefitManagements || [];
  292. const contributionRuleConfigs = data.contributionRuleConfigs || [];
  293. if (vipBenefitManagements.length > 0) {
  294. renderBenefitsRows(vipBenefitManagements);
  295. renderBenefitsAltRows(vipBenefitManagements);
  296. }
  297. const rules = {};
  298. contributionRuleConfigs.forEach(item => {
  299. if (item.ruleCode === 'PERSONAL_STAKE') {
  300. rules.personalStaking = item.threshold + (getCurrentLang() === 'zh' ? '枚' : ' coins');
  301. } else if (item.ruleCode === 'TEAM_STAKE') {
  302. rules.teamStaking = item.threshold + (getCurrentLang() === 'zh' ? '枚' : ' coins');
  303. } else if (item.ruleCode === 'SPOT_TRADE') {
  304. rules.spotTrading = item.threshold + '$';
  305. } else if (item.ruleCode === 'FUTURES_TRADE') {
  306. rules.futuresTrading = item.threshold + '$';
  307. }
  308. });
  309. const currentVipLevel = data.vipLevel || 0;
  310. const personalValue = data.personalValue || 0;
  311. const personalStakeValue = data.personalStakeValue || 0;
  312. const currentLevelInfo = vipBenefitManagements.find(item => item.vipLevel === currentVipLevel);
  313. const nextLevelInfo = vipBenefitManagements.find(item => item.vipLevel === currentVipLevel + 1);
  314. let contributionProgress = 0;
  315. let stakingProgress = 0;
  316. let contributionNeeded = 0;
  317. let stakingNeeded = 0;
  318. if (nextLevelInfo) {
  319. const currentContribution = currentLevelInfo ? currentLevelInfo.contributionValue || 0 : 0;
  320. const currentStaking = currentLevelInfo ? currentLevelInfo.personalPledgeRequirements || 0 : 0;
  321. const nextContribution = nextLevelInfo.contributionValue || 0;
  322. const nextStaking = nextLevelInfo.personalPledgeRequirements || 0;
  323. const contributionDelta = nextContribution - currentContribution;
  324. const stakingDelta = nextStaking - currentStaking;
  325. const currentContributionIncrement = personalValue - currentContribution;
  326. const currentStakingIncrement = personalStakeValue - currentStaking;
  327. contributionNeeded = Math.max(0, contributionDelta - currentContributionIncrement);
  328. stakingNeeded = Math.max(0, stakingDelta - currentStakingIncrement);
  329. contributionProgress = contributionDelta > 0 ? Math.min(100, Math.max(0, (currentContributionIncrement / contributionDelta) * 100)) : 0;
  330. stakingProgress = stakingDelta > 0 ? Math.min(100, Math.max(0, (currentStakingIncrement / stakingDelta) * 100)) : 0;
  331. } else {
  332. contributionProgress = 100;
  333. stakingProgress = 100;
  334. }
  335. const lang = getCurrentLang();
  336. const stakingUnit = lang === 'zh' ? '枚' : ' coins';
  337. const elements = document.querySelectorAll('[data-field]');
  338. elements.forEach(element => {
  339. const field = element.dataset.field;
  340. let value = getNestedValue(data, field);
  341. if (field === 'vipLevel' && data.vipName) {
  342. value = data.vipName;
  343. }
  344. if (field.startsWith('rules.')) {
  345. const ruleKey = field.replace('rules.', '');
  346. value = rules[ruleKey];
  347. }
  348. if (field === 'contribution') {
  349. value = contributionNeeded + (lang === 'zh' ? '贡献值' : ' contribution');
  350. }
  351. if (field === 'staking') {
  352. value = stakingNeeded + stakingUnit;
  353. }
  354. if (field === 'personalValue') {
  355. value = value + (lang === 'zh' ? '贡献值' : ' contribution');
  356. }
  357. if (field === 'personalStakeValue') {
  358. value = value + stakingUnit;
  359. }
  360. if (field === 'contributionProgress') {
  361. value = contributionProgress.toFixed(0);
  362. }
  363. if (field === 'stakingProgress') {
  364. value = stakingProgress.toFixed(0);
  365. }
  366. if (value !== undefined && value !== null) {
  367. if (field.includes('Progress')) {
  368. element.style.width = value + '%';
  369. } else {
  370. element.textContent = value;
  371. }
  372. }
  373. });
  374. }
  375. function getNestedValue(obj, path) {
  376. return path.split('.').reduce((current, key) => {
  377. return current && current[key] !== undefined ? current[key] : undefined;
  378. }, obj);
  379. }
  380. let token = '';
  381. async function getFilterFromNative() {
  382. try {
  383. const filter = await window.flutter_inappwebview.callHandler('getToken');
  384. token = filter;
  385. return filter;
  386. } catch (e) {
  387. console.error('JSBridge 调用失败', e);
  388. return null;
  389. }
  390. }
  391. async function fetchData() {
  392. showLoading();
  393. try {
  394. const headers = {
  395. 'Content-Type': 'application/json',
  396. 'Accept': 'application/json',
  397. 'X-Requested-With': 'XMLHttpRequest',
  398. };
  399. if (token) {
  400. headers['Authorization'] = token;
  401. }
  402. const response = await fetch(API_URL + '/userVipInfo/info', {
  403. method: 'GET',
  404. headers: headers,
  405. credentials: 'include',
  406. timeout: 10000
  407. });
  408. if (!response.ok) {
  409. throw new Error(`请求失败! status: ${response.status}`);
  410. }
  411. const result = await response.json();
  412. const data = result.data || {};
  413. const currentVipLevel = data.vipLevel || 0;
  414. const nextVipLevel = currentVipLevel + 1;
  415. applyTranslations(getCurrentLang(), nextVipLevel);
  416. insertData(data);
  417. hideLoading();
  418. } catch (error) {
  419. console.error('Data fetch error:', error);
  420. hideLoading();
  421. }
  422. }
  423. retryBtn.addEventListener('click', fetchData);
  424. document.addEventListener('DOMContentLoaded', async function () {
  425. const lang = getCurrentLang();
  426. applyTranslations(lang);
  427. await getFilterFromNative();
  428. fetchData();
  429. });
  430. })();
  431. </script>
  432. </body>
  433. </html>