diff --git a/activity/cp-space/assets/parts/cp-lv1-avatar@2x.png b/activity/cp-space/assets/parts/cp-lv1-avatar@2x.png new file mode 100644 index 0000000..e802233 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv1-avatar@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv1-bg@2x.png b/activity/cp-space/assets/parts/cp-lv1-bg@2x.png new file mode 100644 index 0000000..1be79e1 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv1-bg@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv1-button@2x.png b/activity/cp-space/assets/parts/cp-lv1-button@2x.png new file mode 100644 index 0000000..b2ab234 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv1-button@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv1-heart@2x.png b/activity/cp-space/assets/parts/cp-lv1-heart@2x.png new file mode 100644 index 0000000..044cb94 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv1-heart@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv1-level-pill@2x.png b/activity/cp-space/assets/parts/cp-lv1-level-pill@2x.png new file mode 100644 index 0000000..d30cffe Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv1-level-pill@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv1-xp-fill@2x.png b/activity/cp-space/assets/parts/cp-lv1-xp-fill@2x.png new file mode 100644 index 0000000..04ea88b Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv1-xp-fill@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv1-xp-track@2x.png b/activity/cp-space/assets/parts/cp-lv1-xp-track@2x.png new file mode 100644 index 0000000..91a9f69 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv1-xp-track@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv2-avatar@2x.png b/activity/cp-space/assets/parts/cp-lv2-avatar@2x.png new file mode 100644 index 0000000..cff3000 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv2-avatar@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv2-bg@2x.png b/activity/cp-space/assets/parts/cp-lv2-bg@2x.png new file mode 100644 index 0000000..17820a0 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv2-bg@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv2-button@2x.png b/activity/cp-space/assets/parts/cp-lv2-button@2x.png new file mode 100644 index 0000000..5502dc1 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv2-button@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv2-heart@2x.png b/activity/cp-space/assets/parts/cp-lv2-heart@2x.png new file mode 100644 index 0000000..9967e3f Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv2-heart@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv2-level-pill@2x.png b/activity/cp-space/assets/parts/cp-lv2-level-pill@2x.png new file mode 100644 index 0000000..6807f2d Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv2-level-pill@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv2-xp-fill@2x.png b/activity/cp-space/assets/parts/cp-lv2-xp-fill@2x.png new file mode 100644 index 0000000..9b99cf2 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv2-xp-fill@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv2-xp-track@2x.png b/activity/cp-space/assets/parts/cp-lv2-xp-track@2x.png new file mode 100644 index 0000000..e63633c Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv2-xp-track@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv3-avatar@2x.png b/activity/cp-space/assets/parts/cp-lv3-avatar@2x.png new file mode 100644 index 0000000..1791385 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv3-avatar@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv3-bg@2x.png b/activity/cp-space/assets/parts/cp-lv3-bg@2x.png new file mode 100644 index 0000000..1a53aab Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv3-bg@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv3-button@2x.png b/activity/cp-space/assets/parts/cp-lv3-button@2x.png new file mode 100644 index 0000000..5df7f62 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv3-button@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv3-heart@2x.png b/activity/cp-space/assets/parts/cp-lv3-heart@2x.png new file mode 100644 index 0000000..425ae1b Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv3-heart@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv3-level-pill@2x.png b/activity/cp-space/assets/parts/cp-lv3-level-pill@2x.png new file mode 100644 index 0000000..00d3679 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv3-level-pill@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv3-xp-fill@2x.png b/activity/cp-space/assets/parts/cp-lv3-xp-fill@2x.png new file mode 100644 index 0000000..ce944af Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv3-xp-fill@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv3-xp-track@2x.png b/activity/cp-space/assets/parts/cp-lv3-xp-track@2x.png new file mode 100644 index 0000000..5db6eda Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv3-xp-track@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv4-avatar@2x.png b/activity/cp-space/assets/parts/cp-lv4-avatar@2x.png new file mode 100644 index 0000000..0ceadae Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv4-avatar@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv4-bg@2x.png b/activity/cp-space/assets/parts/cp-lv4-bg@2x.png new file mode 100644 index 0000000..fbd0063 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv4-bg@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv4-button@2x.png b/activity/cp-space/assets/parts/cp-lv4-button@2x.png new file mode 100644 index 0000000..3873bbb Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv4-button@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv4-heart@2x.png b/activity/cp-space/assets/parts/cp-lv4-heart@2x.png new file mode 100644 index 0000000..83e1052 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv4-heart@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv4-level-pill@2x.png b/activity/cp-space/assets/parts/cp-lv4-level-pill@2x.png new file mode 100644 index 0000000..0459dd4 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv4-level-pill@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv4-xp-fill@2x.png b/activity/cp-space/assets/parts/cp-lv4-xp-fill@2x.png new file mode 100644 index 0000000..d61d527 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv4-xp-fill@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv4-xp-track@2x.png b/activity/cp-space/assets/parts/cp-lv4-xp-track@2x.png new file mode 100644 index 0000000..575a477 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv4-xp-track@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv5-avatar@2x.png b/activity/cp-space/assets/parts/cp-lv5-avatar@2x.png new file mode 100644 index 0000000..89eb9f2 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv5-avatar@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv5-bg@2x.png b/activity/cp-space/assets/parts/cp-lv5-bg@2x.png new file mode 100644 index 0000000..be153ac Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv5-bg@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv5-button@2x.png b/activity/cp-space/assets/parts/cp-lv5-button@2x.png new file mode 100644 index 0000000..909b14d Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv5-button@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv5-heart@2x.png b/activity/cp-space/assets/parts/cp-lv5-heart@2x.png new file mode 100644 index 0000000..477008e Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv5-heart@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv5-level-pill@2x.png b/activity/cp-space/assets/parts/cp-lv5-level-pill@2x.png new file mode 100644 index 0000000..0459dd4 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv5-level-pill@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv5-xp-fill@2x.png b/activity/cp-space/assets/parts/cp-lv5-xp-fill@2x.png new file mode 100644 index 0000000..da2d8c6 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv5-xp-fill@2x.png differ diff --git a/activity/cp-space/assets/parts/cp-lv5-xp-track@2x.png b/activity/cp-space/assets/parts/cp-lv5-xp-track@2x.png new file mode 100644 index 0000000..575a477 Binary files /dev/null and b/activity/cp-space/assets/parts/cp-lv5-xp-track@2x.png differ diff --git a/activity/cp-space/assets/space-bg.svg b/activity/cp-space/assets/space-bg.svg deleted file mode 100644 index f148f3f..0000000 --- a/activity/cp-space/assets/space-bg.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/activity/cp-space/assets/space-bg@4x.png b/activity/cp-space/assets/space-bg@4x.png new file mode 100644 index 0000000..be52093 Binary files /dev/null and b/activity/cp-space/assets/space-bg@4x.png differ diff --git a/activity/cp-space/index.html b/activity/cp-space/index.html index 762c60b..0260047 100644 --- a/activity/cp-space/index.html +++ b/activity/cp-space/index.html @@ -10,9 +10,9 @@ - +
@@ -68,10 +68,10 @@
- - - - - + + + + + diff --git a/activity/cp-space/script.js b/activity/cp-space/script.js index 327e8d8..0217ba6 100644 --- a/activity/cp-space/script.js +++ b/activity/cp-space/script.js @@ -1,12 +1,24 @@ (function () { var VALID_TABS = ['cp', 'brother', 'sister']; - var LEVEL_IMAGES = { - 1: './assets/level-1@4x.png', - 2: './assets/level-2@4x.png', - 3: './assets/level-3@4x.png', - 4: './assets/level-4@4x.png', - 5: './assets/level-5@4x.png', - }; + var PARTS_BASE = './assets/parts/'; + var PART_NAMES = [ + 'bg', + 'avatar', + 'heart', + 'button', + 'xp-track', + 'xp-fill', + 'level-pill', + ]; + var LEVEL_PARTS = {}; + + for (var partLevel = 1; partLevel <= 5; partLevel += 1) { + LEVEL_PARTS[partLevel] = {}; + PART_NAMES.forEach(function (partName) { + LEVEL_PARTS[partLevel][partName] = + PARTS_BASE + 'cp-lv' + partLevel + '-' + partName + '@2x.png'; + }); + } var page = document.getElementById('page'); var contentPanel = document.getElementById('contentPanel'); @@ -17,13 +29,24 @@ var state = { activeTab: normalizeTab(readParam('tab')) || 'cp', + requestSeq: 0, + cancelingId: '', tabs: { - cp: [normalizeLevel(readParam('level')) || 1], - brother: [2, 2], - sister: [4, 5], + cp: createTabState(), + brother: createTabState(), + sister: createTabState(), }, }; + function createTabState() { + return { + loading: false, + loaded: false, + error: '', + relationships: [], + }; + } + function readParam(name) { var params = new URLSearchParams(window.location.search || ''); return params.get(name); @@ -40,7 +63,16 @@ var level = Number(value); return Number.isFinite(level) && level >= 1 && level <= 5 ? Math.floor(level) - : 0; + : 1; + } + + function readNumber(value) { + var number = Number(value); + return Number.isFinite(number) ? number : 0; + } + + function readString(value) { + return String(value == null ? '' : value).trim(); } function t(key, fallback) { @@ -59,6 +91,123 @@ .replace(/'/g, '''); } + function showToast(message) { + if (window.HyAppToast && window.HyAppToast.show) { + window.HyAppToast.show(message); + return; + } + window.dispatchEvent( + new CustomEvent('hyapp:toast', { detail: { message: message } }) + ); + } + + function profileFromData(data) { + var json = data || {}; + return { + userId: readString(json.user_id || json.id), + displayUserId: readString(json.display_user_id), + username: readString(json.username || json.nickname || json.name), + avatar: readString( + json.avatar || + json.avatar_url || + json.avatar_path || + (json.profile && json.profile.avatar) + ), + }; + } + + function displayName(profile) { + return ( + readString(profile.username) || + readString(profile.displayUserId) || + readString(profile.userId) || + t('cpSpace.unknownUser', 'User') + ); + } + + function daysFromFormedAt(formedAtMs) { + var formedAt = readNumber(formedAtMs); + if (formedAt <= 0) return 0; + var now = Date.now(); + if (now <= formedAt) return 1; + return Math.floor((now - formedAt) / 86400000) + 1; + } + + function currentLevelExp(item) { + var total = Math.max(0, readNumber(item.intimacy_value)); + var currentThreshold = readNumber(item.current_level_threshold); + if (currentThreshold <= 0 || currentThreshold >= total) return total; + return total - currentThreshold; + } + + function nextLevelExp(item) { + if (item.max_level) return 0; + var nextThreshold = readNumber(item.next_level_threshold); + var currentThreshold = readNumber(item.current_level_threshold); + if (nextThreshold > 0) { + if (currentThreshold > 0 && nextThreshold > currentThreshold) { + return nextThreshold - currentThreshold; + } + return nextThreshold; + } + var total = Math.max(0, readNumber(item.intimacy_value)); + var remaining = readNumber(item.needed_for_next_level); + return remaining > 0 ? total + remaining : 0; + } + + function progressPercent(item) { + if (item.max_level) return 100; + var explicit = readNumber(item.level_progress_percent); + if (explicit > 0) return Math.max(0, Math.min(100, explicit)); + var next = nextLevelExp(item); + if (next <= 0) return 0; + return Math.max(0, Math.min(100, (currentLevelExp(item) / next) * 100)); + } + + function normalizeRelationship(item, tab) { + var relationType = normalizeTab(item && item.relation_type) || tab; + var level = normalizeLevel(item && item.level); + var me = profileFromData(item && item.me); + var partner = profileFromData(item && item.partner); + var currentExp = currentLevelExp(item || {}); + var nextExp = nextLevelExp(item || {}); + return { + relationshipId: readString(item && item.relationship_id), + relationType: relationType, + status: readString(item && item.status) || 'active', + level: level, + me: me, + partner: partner, + days: daysFromFormedAt(item && item.formed_at_ms), + intimacyValue: Math.max(0, readNumber(item && item.intimacy_value)), + currentExp: currentExp, + nextExp: nextExp, + progressPercent: progressPercent(item || {}), + maxLevel: Boolean(item && item.max_level), + breakupCostCoins: Math.max( + 0, + readNumber(item && item.breakup_cost_coins) + ), + }; + } + + function activeRelationships(data, tab) { + var list = Array.isArray(data && data.relationships) + ? data.relationships + : []; + return list + .map(function (item) { + return normalizeRelationship(item || {}, tab); + }) + .filter(function (item) { + return ( + item.relationshipId && + item.relationType === tab && + item.status === 'active' + ); + }); + } + function setActiveTab(tab) { var nextTab = normalizeTab(tab) || 'cp'; state.activeTab = nextTab; @@ -70,36 +219,184 @@ ); }); render(); + if (!state.tabs[nextTab].loaded && !state.tabs[nextTab].loading) { + loadTab(nextTab); + } } - function cardHTML(level, index) { - var safeLevel = normalizeLevel(level) || 1; + function visualLevel(item) { + return item.relationType === 'cp' ? item.level : 1; + } + + function partsFor(item) { + return LEVEL_PARTS[visualLevel(item)] || LEVEL_PARTS[1]; + } + + function nextLevelLabel(item) { + if (item.maxLevel) return 'Max'; + return t('cpSpace.levelValue', 'Lv.{level}').replace( + '{level}', + String(Math.min(5, item.level + 1)) + ); + } + + function expLabel(item) { + if (item.maxLevel || item.nextExp <= 0) { + return t('cpSpace.maxLevelExp', '{exp} EXP').replace( + '{exp}', + formatNumber(item.intimacyValue) + ); + } + return t('cpSpace.expProgress', '{current}/{next} EXP') + .replace('{current}', formatNumber(item.currentExp)) + .replace('{next}', formatNumber(item.nextExp)); + } + + function formatNumber(value) { + var number = Math.max(0, Math.floor(readNumber(value))); + return String(number).replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } + + function avatarHTML(profile, side, frameSrc) { + var name = displayName(profile); + var avatar = readString(profile.avatar); + return [ + '
', + avatar + ? [ + '',
+                      escapeHTML(name),
+                      '', + ].join('') + : [ + '
', + escapeHTML(name.charAt(0).toUpperCase() || '?'), + '
', + ].join(''), + '', + '
', + escapeHTML(name), + '
', + '
', + ].join(''); + } + + function cardHTML(item, index) { + var parts = partsFor(item); var label = t('cpSpace.levelCard', 'Best friend level {level}').replace( '{level}', - String(safeLevel) + String(item.level) ); + var daysText = t('cpSpace.daysValue', '{days} Days').replace( + '{days}', + String(item.days) + ); + var levelTitle = t('cpSpace.levelValue', 'Lv.{level}').replace( + '{level}', + String(item.level) + ); + var cardClass = + 'relation-card relation-card--' + escapeHTML(item.relationType); + if (item.relationType !== 'cp') cardClass += ' relation-card--shared'; return [ - '
', - '',
             escapeHTML(label),
             '', - '', + avatarHTML(item.me, 'left', parts.avatar), + avatarHTML(item.partner, 'right', parts.avatar), + '', + '
', + escapeHTML(String(item.days)), + '', + escapeHTML(t('cpSpace.daysUnit', 'Days')), + '
', + '
', + escapeHTML(levelTitle), + '
', + '
', + escapeHTML(nextLevelLabel(item)), + '
', + '
', + escapeHTML(expLabel(item)), + '
', + '
', + item.relationType === 'cp' + ? [ + '', + ].join('') + : '', '
', ].join(''); } function render() { - var levels = state.tabs[state.activeTab] || []; - if (!levels.length) { + var tabState = state.tabs[state.activeTab]; + if (tabState.loading && !tabState.loaded) { + contentPanel.innerHTML = + '
' + + escapeHTML(t('cpSpace.loading', 'Loading...')) + + '
'; + return; + } + if (tabState.error) { + contentPanel.innerHTML = + ''; + return; + } + if (!tabState.relationships.length) { contentPanel.innerHTML = '
' + escapeHTML(t('cpSpace.empty', 'No relationship data yet.')) + @@ -109,45 +406,124 @@ contentPanel.innerHTML = '
' + - levels.map(cardHTML).join('') + + tabState.relationships.map(cardHTML).join('') + '
'; } + function loadTab(tab) { + var tabState = state.tabs[tab]; + var seq = ++state.requestSeq; + tabState.loading = true; + tabState.error = ''; + render(); + + // 关系事实全部从 gateway profile 接口读取;H5 只负责按 relation_type 分页拉取,不本地推断用户是否有 CP/兄弟/姐妹。 + return window.HyAppAPI.cpSpace + .relationships({ + relation_type: tab, + page: 1, + page_size: tab === 'cp' ? 1 : 20, + }) + .then(function (data) { + if (seq < state.requestSeq && tab === state.activeTab) return; + tabState.relationships = activeRelationships(data, tab); + tabState.loaded = true; + }) + .catch(function (error) { + tabState.error = + (error && error.message) || + t('cpSpace.loadFailed', 'Load failed. Tap to retry.'); + }) + .then(function () { + tabState.loading = false; + render(); + }); + } + + function buildCommandId(relationshipId) { + var random = + window.crypto && window.crypto.randomUUID + ? window.crypto.randomUUID() + : String(Date.now()) + '_' + Math.random().toString(16).slice(2); + return ['h5_cp_break', relationshipId, random] + .join('_') + .replace(/[^A-Za-z0-9_-]/g, '_'); + } + + function confirmBreakup(item) { + var message = + item.breakupCostCoins > 0 + ? t( + 'cpSpace.cancelConfirmWithCost', + 'Canceling this CP will cost {coins} coins. Continue?' + ).replace('{coins}', formatNumber(item.breakupCostCoins)) + : t( + 'cpSpace.cancelConfirmFree', + 'Cancel this CP relationship?' + ); + return Promise.resolve(window.confirm(message)); + } + + function handleCancel(index) { + var item = + state.tabs[state.activeTab].relationships[Number(index)] || null; + if (!item || !item.relationshipId || state.cancelingId) return; + + confirmBreakup(item).then(function (confirmed) { + if (!confirmed) return; + state.cancelingId = item.relationshipId; + render(); + + // command_id 必须由本次点击生成并随请求提交,服务端用它串联占位、钱包扣费和确认解除,网络重试不会重复扣费。 + window.HyAppAPI.cpSpace + .breakRelationship(item.relationshipId, { + command_id: buildCommandId(item.relationshipId), + }) + .then(function (data) { + showToast(t('cpSpace.cancelSuccess', 'Canceled.')); + if (window.HyAppBridge && window.HyAppBridge.post) { + window.HyAppBridge.post('cpSpaceRelationCanceled', { + relationship_id: item.relationshipId, + relation_type: item.relationType, + breakup_cost_coins: readNumber( + data && data.breakup_cost_coins + ), + }); + } + state.tabs.cp.loaded = false; + return loadTab('cp'); + }) + .catch(function (error) { + showToast( + (error && error.message) || + t('cpSpace.cancelFailed', 'Cancel failed.') + ); + }) + .then(function () { + state.cancelingId = ''; + render(); + }); + }); + } + function applyExternalData(payload) { var data = payload || {}; - var nextTabs = {}; VALID_TABS.forEach(function (tab) { var raw = data[tab]; if (!Array.isArray(raw)) return; - var normalized = raw + state.tabs[tab].relationships = raw .map(function (item) { - return normalizeLevel( - typeof item === 'object' ? item.level : item - ); + return normalizeRelationship(item || {}, tab); }) - .filter(Boolean); - if (normalized.length) nextTabs[tab] = normalized; - }); - - // Flutter 可以在 WebView 注入 window.HyAppCPSpaceData 或派发 hyapp:cp-space-data; - // 页面只接受 1-5 的等级编号,避免 H5 自己猜测关系状态或渲染后端未确认的数据。 - Object.keys(nextTabs).forEach(function (tab) { - state.tabs[tab] = nextTabs[tab]; + .filter(function (item) { + return item.relationshipId; + }); + state.tabs[tab].loaded = true; }); if (normalizeTab(data.activeTab)) state.activeTab = data.activeTab; setActiveTab(state.activeTab); } - function handleCancel(index) { - var level = (state.tabs[state.activeTab] || [])[Number(index)] || 0; - if (window.HyAppBridge && window.HyAppBridge.post) { - window.HyAppBridge.post('cpSpaceCancel', { - relation_type: state.activeTab, - level: level, - }); - } - } - function bindEvents() { if (backButton) { backButton.addEventListener('click', function () { @@ -166,9 +542,13 @@ }); contentPanel.addEventListener('click', function (event) { - var target = event.target.closest('[data-cancel-index]'); - if (!target) return; - handleCancel(target.getAttribute('data-cancel-index')); + var cancelTarget = event.target.closest('[data-cancel-index]'); + if (cancelTarget) { + handleCancel(cancelTarget.getAttribute('data-cancel-index')); + return; + } + var retryTarget = event.target.closest('[data-retry-tab]'); + if (retryTarget) loadTab(retryTarget.getAttribute('data-retry-tab')); }); window.addEventListener('hyapp:cp-space-data', function (event) { diff --git a/activity/cp-space/style.css b/activity/cp-space/style.css index 381ad45..02b3b1c 100644 --- a/activity/cp-space/style.css +++ b/activity/cp-space/style.css @@ -59,7 +59,7 @@ button { .cp-space::before { z-index: 0; - background: url('./assets/space-bg.svg') center top / 100% auto repeat-y; + background: url('./assets/space-bg@4x.png') center top / 100% auto repeat-y; } .cp-space::after { @@ -168,26 +168,317 @@ button { .relation-card { position: relative; width: min(355px, calc(100vw - 20px)); + aspect-ratio: 355 / 203; + color: #ff0979; + --heart-size: 33.8%; + --button-width: 35.8%; + --button-height: 14.8%; + --accent-color: #ff0979; + --accent-shadow: rgba(110, 17, 108, 0.75); + --avatar-y: 6.9%; + --heart-y: 2.5%; + --button-y: 73.9%; border-radius: 10px; + isolation: isolate; } -.relation-card img { +.relation-card[data-level='2'] { + --accent-color: #0967ff; + --heart-size: 33.8%; + --button-width: 35.8%; + --button-height: 18.7%; +} + +.relation-card[data-level='3'] { + --accent-color: #7c09ff; + --heart-size: 40.6%; + --button-width: 45.9%; + --button-height: 17.2%; + --avatar-y: 9.9%; + --heart-y: 0; + --button-y: 76.4%; +} + +.relation-card[data-level='4'] { + --accent-color: #ff090d; + --heart-size: 37.7%; + --button-width: 36.6%; + --button-height: 20.7%; +} + +.relation-card[data-level='5'] { + --accent-color: #ff090d; + --heart-size: 42.8%; + --button-width: 37.5%; + --button-height: 17.2%; + --heart-y: 0; + --button-y: 76.8%; +} + +.relation-card-bg { + position: absolute; + inset: 0; + z-index: 0; display: block; width: 100%; - height: auto; + height: 100%; + border-radius: 10px; + object-fit: fill; user-select: none; -webkit-user-drag: none; } -.cancel-hotspot { +.relation-avatar { position: absolute; - left: 35%; - bottom: 10%; - width: 30%; - height: 18%; + z-index: 2; + top: var(--avatar-y); + width: 28.7%; + aspect-ratio: 1; + margin: 0; +} + +.relation-avatar--left { + left: 5.1%; +} + +.relation-avatar--right { + right: 5.1%; +} + +.relation-avatar-photo, +.relation-avatar-frame { + position: absolute; + display: block; + user-select: none; + -webkit-user-drag: none; +} + +.relation-avatar-photo { + left: 21.6%; + top: 17.6%; + z-index: 1; + width: 56.9%; + aspect-ratio: 1; + border-radius: 50%; + object-fit: cover; +} + +.relation-avatar-photo--placeholder { + display: grid; + place-items: center; + background: linear-gradient(135deg, #f65ca8, #7631dc); + color: #fff; + font-size: 22px; + font-weight: 700; + line-height: 1; +} + +.relation-avatar-frame { + inset: 0; + z-index: 2; + width: 100%; + height: 100%; + object-fit: contain; +} + +.relation-name { + position: absolute; + z-index: 3; + top: 67%; + width: 84%; + overflow: hidden; + color: #fff; + font-size: 11px; + font-weight: 700; + line-height: 14px; + text-align: center; + text-overflow: ellipsis; + text-shadow: 0 1px 4px rgba(93, 23, 88, 0.75); + white-space: nowrap; +} + +.relation-name--left { + left: 8%; +} + +.relation-name--right { + right: 8%; +} + +.relation-heart-art { + position: absolute; + z-index: 1; + left: 50%; + top: var(--heart-y); + display: block; + width: var(--heart-size); + aspect-ratio: 1; + object-fit: contain; + transform: translateX(-50%); + user-select: none; + -webkit-user-drag: none; +} + +.relation-days { + position: absolute; + z-index: 2; + left: 50%; + top: 23.5%; + display: flex; + align-items: baseline; + justify-content: center; + width: 28%; + height: 14%; + color: #fff; + text-align: center; + text-shadow: + 0 1px 0 var(--accent-color), + 0 2px 7px var(--accent-shadow); + transform: translateX(-50%); +} + +.relation-days strong { + font-size: 20px; + font-weight: 800; + line-height: 20px; +} + +.relation-days span { + margin-left: 3px; + font-size: 10px; + font-weight: 700; + line-height: 12px; +} + +.relation-level { + position: absolute; + z-index: 2; + top: 55.6%; + width: 8.45%; + aspect-ratio: 30 / 16; + color: #fff; + font-size: 10px; + font-weight: 700; + line-height: 1; + text-align: center; +} + +.relation-level--current { + left: 8.45%; +} + +.relation-level--next { + right: 8.45%; +} + +.relation-level img { + position: absolute; + inset: 0; + display: block; + width: 100%; + height: 100%; +} + +.relation-level span { + position: absolute; + inset: 0; + display: grid; + place-items: center; + text-shadow: 0 0 1px #fc8ab1; +} + +.relation-progress { + position: absolute; + z-index: 2; + left: 19.15%; + top: 58.1%; + width: 62%; + height: 3%; + overflow: hidden; border-radius: 999px; } +.relation-progress span { + position: absolute; + left: 0; + top: 0; + display: block; + width: 0; + height: 100%; + border-radius: inherit; + overflow: hidden; +} + +.relation-progress-track, +.relation-progress-fill { + position: absolute; + left: 0; + top: 0; + display: block; + width: 100%; + height: 100%; + user-select: none; + -webkit-user-drag: none; +} + +.relation-progress-fill { + width: auto; + min-width: 100%; +} + +.relation-exp { + position: absolute; + z-index: 2; + left: 25%; + top: 62.6%; + width: 50%; + overflow: hidden; + color: var(--accent-color); + font-size: 11px; + font-weight: 700; + line-height: 14px; + text-align: center; + text-overflow: ellipsis; + text-shadow: 0 0 1px rgba(255, 255, 255, 0.72); + white-space: nowrap; +} + +.cancel-button { + position: absolute; + z-index: 3; + left: 50%; + top: var(--button-y); + width: var(--button-width); + height: var(--button-height); + color: var(--accent-color); + font-size: 13px; + font-weight: 700; + line-height: 1; + text-align: center; + text-shadow: 0 0 1px rgba(255, 255, 255, 0.8); + transform: translateX(-50%); +} + +.cancel-button-art { + position: absolute; + inset: 0; + display: block; + width: 100%; + height: 100%; + user-select: none; + -webkit-user-drag: none; +} + +.cancel-button span { + position: relative; + z-index: 1; +} + +.cancel-button:disabled { + opacity: 0.7; + cursor: wait; +} + .empty-state { width: min(355px, calc(100vw - 20px)); margin: 0 auto; @@ -201,6 +492,11 @@ button { text-align: center; } +.empty-state--button { + display: block; + cursor: pointer; +} + [dir='rtl'] .back-button span { transform: rotate(225deg); } diff --git a/common/api.js b/common/api.js index f865729..012b199 100644 --- a/common/api.js +++ b/common/api.js @@ -756,6 +756,26 @@ var default_api = 'https://api.global-interaction.com/'; }, }; + var cpSpaceAPI = { + relationships: function (params) { + return request('/api/v1/cp/relationships', { + method: 'GET', + query: params || {}, + }); + }, + breakRelationship: function (relationshipID, payload) { + return request( + '/api/v1/cp/relationships/' + + encodeURIComponent(relationshipID || '') + + '/break', + { + method: 'POST', + body: payload || {}, + } + ); + }, + }; + // 转盘 H5 只通过 gateway 活动接口读配置、抽奖、查记录和跑马灯提示;页面不再内置 mock 奖品,避免后台资源组未配置时前端仍展示假奖池。 // 抽奖 command_id 由页面点击时生成并随请求提交,服务端用它做幂等,网络重试不会重复扣费或重复发奖。 var activityWheelAPI = { @@ -990,6 +1010,7 @@ var default_api = 'https://api.global-interaction.com/'; task: taskAPI, weeklyStar: weeklyStarAPI, cpWeeklyRank: cpWeeklyRankAPI, + cpSpace: cpSpaceAPI, inviteActivityReward: inviteActivityRewardAPI, game: gameAPI, }; diff --git a/common/locales/ar.json b/common/locales/ar.json index d7b0be4..bb05265 100644 --- a/common/locales/ar.json +++ b/common/locales/ar.json @@ -607,5 +607,18 @@ "cpSpace.tabSister": "Sister", "cpSpace.levelCard": "مستوى أفضل صديق {level}", "cpSpace.cancel": "إلغاء", - "cpSpace.empty": "لا توجد بيانات علاقة بعد." + "cpSpace.empty": "لا توجد بيانات علاقة بعد.", + "cpSpace.loading": "جار التحميل...", + "cpSpace.loadFailed": "فشل التحميل. اضغط للمحاولة مرة أخرى.", + "cpSpace.unknownUser": "User", + "cpSpace.daysUnit": "Days", + "cpSpace.daysValue": "{days} Days", + "cpSpace.levelValue": "Lv.{level}", + "cpSpace.expProgress": "{current}/{next} EXP", + "cpSpace.maxLevelExp": "{exp} EXP", + "cpSpace.canceling": "جار الإلغاء...", + "cpSpace.cancelConfirmWithCost": "إلغاء علاقة CP سيكلف {coins} عملة. هل تريد المتابعة؟", + "cpSpace.cancelConfirmFree": "هل تريد إلغاء علاقة CP؟", + "cpSpace.cancelSuccess": "تم الإلغاء.", + "cpSpace.cancelFailed": "فشل الإلغاء." } diff --git a/common/locales/en.json b/common/locales/en.json index 629dd12..6ffca58 100644 --- a/common/locales/en.json +++ b/common/locales/en.json @@ -613,5 +613,18 @@ "cpSpace.tabSister": "Sister", "cpSpace.levelCard": "Best friend level {level}", "cpSpace.cancel": "Cancel", - "cpSpace.empty": "No relationship data yet." + "cpSpace.empty": "No relationship data yet.", + "cpSpace.loading": "Loading...", + "cpSpace.loadFailed": "Load failed. Tap to retry.", + "cpSpace.unknownUser": "User", + "cpSpace.daysUnit": "Days", + "cpSpace.daysValue": "{days} Days", + "cpSpace.levelValue": "Lv.{level}", + "cpSpace.expProgress": "{current}/{next} EXP", + "cpSpace.maxLevelExp": "{exp} EXP", + "cpSpace.canceling": "Canceling...", + "cpSpace.cancelConfirmWithCost": "Canceling this CP will cost {coins} coins. Continue?", + "cpSpace.cancelConfirmFree": "Cancel this CP relationship?", + "cpSpace.cancelSuccess": "Canceled.", + "cpSpace.cancelFailed": "Cancel failed." } diff --git a/common/locales/es.json b/common/locales/es.json index 9053e2b..ff49f30 100644 --- a/common/locales/es.json +++ b/common/locales/es.json @@ -607,5 +607,18 @@ "cpSpace.tabSister": "Sister", "cpSpace.levelCard": "Nivel de mejor amigo {level}", "cpSpace.cancel": "Cancelar", - "cpSpace.empty": "Aún no hay datos de relación." + "cpSpace.empty": "Aún no hay datos de relación.", + "cpSpace.loading": "Cargando...", + "cpSpace.loadFailed": "Error al cargar. Toca para reintentar.", + "cpSpace.unknownUser": "User", + "cpSpace.daysUnit": "Days", + "cpSpace.daysValue": "{days} Days", + "cpSpace.levelValue": "Lv.{level}", + "cpSpace.expProgress": "{current}/{next} EXP", + "cpSpace.maxLevelExp": "{exp} EXP", + "cpSpace.canceling": "Cancelando...", + "cpSpace.cancelConfirmWithCost": "Cancelar este CP costará {coins} monedas. ¿Continuar?", + "cpSpace.cancelConfirmFree": "¿Cancelar esta relación CP?", + "cpSpace.cancelSuccess": "Cancelado.", + "cpSpace.cancelFailed": "Error al cancelar." } diff --git a/common/locales/id.json b/common/locales/id.json index a2bc8d6..caade2c 100644 --- a/common/locales/id.json +++ b/common/locales/id.json @@ -574,5 +574,18 @@ "cpSpace.tabSister": "Sister", "cpSpace.levelCard": "Level sahabat terbaik {level}", "cpSpace.cancel": "Batal", - "cpSpace.empty": "Belum ada data hubungan." + "cpSpace.empty": "Belum ada data hubungan.", + "cpSpace.loading": "Memuat...", + "cpSpace.loadFailed": "Gagal memuat. Ketuk untuk mencoba lagi.", + "cpSpace.unknownUser": "User", + "cpSpace.daysUnit": "Days", + "cpSpace.daysValue": "{days} Days", + "cpSpace.levelValue": "Lv.{level}", + "cpSpace.expProgress": "{current}/{next} EXP", + "cpSpace.maxLevelExp": "{exp} EXP", + "cpSpace.canceling": "Membatalkan...", + "cpSpace.cancelConfirmWithCost": "Membatalkan CP ini akan memakai {coins} koin. Lanjutkan?", + "cpSpace.cancelConfirmFree": "Batalkan hubungan CP ini?", + "cpSpace.cancelSuccess": "Dibatalkan.", + "cpSpace.cancelFailed": "Gagal membatalkan." } diff --git a/common/locales/tr.json b/common/locales/tr.json index 2461773..4d161cb 100644 --- a/common/locales/tr.json +++ b/common/locales/tr.json @@ -574,5 +574,18 @@ "cpSpace.tabSister": "Sister", "cpSpace.levelCard": "En iyi arkadaş seviyesi {level}", "cpSpace.cancel": "İptal", - "cpSpace.empty": "Henüz ilişki verisi yok." + "cpSpace.empty": "Henüz ilişki verisi yok.", + "cpSpace.loading": "Yükleniyor...", + "cpSpace.loadFailed": "Yüklenemedi. Tekrar denemek için dokun.", + "cpSpace.unknownUser": "User", + "cpSpace.daysUnit": "Days", + "cpSpace.daysValue": "{days} Days", + "cpSpace.levelValue": "Lv.{level}", + "cpSpace.expProgress": "{current}/{next} EXP", + "cpSpace.maxLevelExp": "{exp} EXP", + "cpSpace.canceling": "İptal ediliyor...", + "cpSpace.cancelConfirmWithCost": "Bu CP ilişkisini iptal etmek {coins} coin tutar. Devam edilsin mi?", + "cpSpace.cancelConfirmFree": "Bu CP ilişkisi iptal edilsin mi?", + "cpSpace.cancelSuccess": "İptal edildi.", + "cpSpace.cancelFailed": "İptal başarısız." } diff --git a/common/locales/zh.json b/common/locales/zh.json index 3e903a5..4f4e038 100644 --- a/common/locales/zh.json +++ b/common/locales/zh.json @@ -580,5 +580,18 @@ "cpSpace.tabSister": "Sister", "cpSpace.levelCard": "挚友等级 {level}", "cpSpace.cancel": "取消", - "cpSpace.empty": "暂无关系数据" + "cpSpace.empty": "暂无关系数据", + "cpSpace.loading": "加载中...", + "cpSpace.loadFailed": "加载失败,点击重试", + "cpSpace.unknownUser": "用户", + "cpSpace.daysUnit": "Days", + "cpSpace.daysValue": "{days} Days", + "cpSpace.levelValue": "Lv.{level}", + "cpSpace.expProgress": "{current}/{next} EXP", + "cpSpace.maxLevelExp": "{exp} EXP", + "cpSpace.canceling": "取消中...", + "cpSpace.cancelConfirmWithCost": "取消 CP 关系需要支付 {coins} 金币,是否继续?", + "cpSpace.cancelConfirmFree": "确定取消 CP 关系?", + "cpSpace.cancelSuccess": "已取消", + "cpSpace.cancelFailed": "取消失败" }