This commit is contained in:
zhx 2026-06-18 18:58:30 +08:00
parent 591b316c16
commit e4124b14f9
20 changed files with 617 additions and 7 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -0,0 +1,46 @@
<svg width="375" height="812" viewBox="0 0 375 812" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="375" height="812" fill="#26084E"/>
<rect width="375" height="812" fill="url(#bgGlow)"/>
<path d="M236 -12C229 38 244 78 287 111C328 143 365 161 393 212" stroke="#8C43D2" stroke-opacity=".18" stroke-width="2"/>
<path d="M251 -8C257 33 278 63 315 80C351 96 377 122 392 159" stroke="#A35AE7" stroke-opacity=".16" stroke-width="1.4"/>
<path d="M67 830C73 760 52 707 4 671C-35 642 -58 599 -61 542" stroke="#9C4DDA" stroke-opacity=".2" stroke-width="2"/>
<path d="M34 812C26 757 4 720 -31 699C-61 680 -75 647 -72 599" stroke="#B65BEE" stroke-opacity=".16" stroke-width="1.4"/>
<g opacity=".42" stroke="#A55BE9" stroke-width="1.1">
<path d="M296 11c29-9 57 9 60 39 2 23-15 43-38 45 12-12 13-29 3-40-11-13-31-11-41 3"/>
<path d="M323 13c17 11 20 35 6 49"/>
<path d="M351 46c-8-14-24-21-40-16"/>
<path d="M18 716c31-12 63 8 66 41 3 26-17 49-43 51 14-14 15-33 4-46-13-15-36-13-49 4"/>
<path d="M47 716c19 13 23 40 7 56"/>
<path d="M80 755c-10-16-29-24-47-18"/>
<path d="M325 728c24-4 45 12 47 36 2 19-12 36-31 38 10-10 10-23 2-32-9-10-25-9-33 3"/>
</g>
<g opacity=".95">
<circle cx="18" cy="401" r="1.3" fill="#FFFFFF"/>
<circle cx="61" cy="659" r="1.9" fill="#FFFFFF"/>
<circle cx="133" cy="512" r="1.2" fill="#DFB8FF"/>
<circle cx="187" cy="710" r="1.6" fill="#FFFFFF"/>
<circle cx="216" cy="438" r="1.3" fill="#FFFFFF"/>
<circle cx="268" cy="596" r="1.5" fill="#CFA4FF"/>
<circle cx="327" cy="505" r="1.9" fill="#FFFFFF"/>
<circle cx="356" cy="420" r="1.3" fill="#E8D1FF"/>
<circle cx="334" cy="735" r="1.7" fill="#FFFFFF"/>
<circle cx="7" cy="218" r="1.2" fill="#FFFFFF"/>
<circle cx="242" cy="133" r="1.4" fill="#FFFFFF"/>
<circle cx="318" cy="76" r="1.2" fill="#DAB5FF"/>
<circle cx="36" cy="185" r="1.6" fill="#DDBDFF"/>
<circle cx="184" cy="83" r="1.3" fill="#FFFFFF"/>
</g>
<g opacity=".72">
<path d="M8 430l2.2 5.2 5.3 2.2-5.3 2.1L8 445l-2.2-5.5-5.3-2.1 5.3-2.2L8 430Z" fill="#FFFFFF"/>
<path d="M341 497l2.8 6.6 6.6 2.8-6.6 2.7-2.8 6.7-2.7-6.7-6.7-2.7 6.7-2.8 2.7-6.6Z" fill="#FFFFFF"/>
<path d="M204 641l2 4.8 4.8 2-4.8 2-2 4.8-2-4.8-4.8-2 4.8-2 2-4.8Z" fill="#F2E3FF"/>
<path d="M356 704l1.9 4.4 4.4 1.9-4.4 1.8-1.9 4.5-1.8-4.5-4.5-1.8 4.5-1.9 1.8-4.4Z" fill="#DAB6FF"/>
</g>
<defs>
<radialGradient id="bgGlow" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(309 52) rotate(116) scale(227 143)">
<stop stop-color="#6E239D" stop-opacity=".62"/>
<stop offset=".44" stop-color="#3A0E68" stop-opacity=".82"/>
<stop offset="1" stop-color="#230747"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -0,0 +1,77 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
/>
<title data-i18n="cpSpace.pageTitle">Best Friend</title>
<link rel="icon" href="data:," />
<link
rel="stylesheet"
href="../../common/theme.css?v=20260618-cp-space"
/>
<link rel="stylesheet" href="./style.css?v=20260618-cp-space" />
</head>
<body>
<div class="app-shell">
<main
class="cp-space"
id="page"
data-tab="cp"
aria-label="Best Friend"
data-i18n-aria="cpSpace.pageLabel"
>
<nav
class="top-nav"
aria-label="Best Friend navigation"
data-i18n-aria="cpSpace.navLabel"
>
<button
class="back-button"
id="backButton"
type="button"
aria-label="Back"
data-i18n-aria="cpSpace.back"
>
<span aria-hidden="true"></span>
</button>
<h1 data-i18n="cpSpace.title">Best Friend</h1>
</nav>
<section
class="tab-bar"
aria-label="Best Friend tabs"
data-i18n-aria="cpSpace.tabsLabel"
>
<button
class="tab-button is-active"
type="button"
data-tab="cp"
>
<span data-i18n="cpSpace.tabCp">CP</span>
</button>
<button class="tab-button" type="button" data-tab="brother">
<span data-i18n="cpSpace.tabBrother">Brother</span>
</button>
<button class="tab-button" type="button" data-tab="sister">
<span data-i18n="cpSpace.tabSister">Sister</span>
</button>
</section>
<section
class="content-panel"
id="contentPanel"
aria-live="polite"
></section>
</main>
</div>
<script src="../../common/i18n.js?v=20260618-cp-space"></script>
<script src="../../common/api.js?v=20260618-cp-space"></script>
<script src="../../common/jsbridge.js?v=20260618-cp-space"></script>
<script src="../../common/toast.js?v=20260618-cp-space"></script>
<script src="./script.js?v=20260618-cp-space"></script>
</body>
</html>

188
activity/cp-space/script.js Normal file
View File

@ -0,0 +1,188 @@
(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 page = document.getElementById('page');
var contentPanel = document.getElementById('contentPanel');
var backButton = document.getElementById('backButton');
var tabButtons = Array.prototype.slice.call(
document.querySelectorAll('.tab-button[data-tab]')
);
var state = {
activeTab: normalizeTab(readParam('tab')) || 'cp',
tabs: {
cp: [normalizeLevel(readParam('level')) || 1],
brother: [2, 2],
sister: [4, 5],
},
};
function readParam(name) {
var params = new URLSearchParams(window.location.search || '');
return params.get(name);
}
function normalizeTab(value) {
var tab = String(value || '')
.trim()
.toLowerCase();
return VALID_TABS.indexOf(tab) >= 0 ? tab : '';
}
function normalizeLevel(value) {
var level = Number(value);
return Number.isFinite(level) && level >= 1 && level <= 5
? Math.floor(level)
: 0;
}
function t(key, fallback) {
if (window.HyAppI18n && window.HyAppI18n.t) {
return window.HyAppI18n.t(key, fallback);
}
return fallback || key;
}
function escapeHTML(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function setActiveTab(tab) {
var nextTab = normalizeTab(tab) || 'cp';
state.activeTab = nextTab;
page.setAttribute('data-tab', nextTab);
tabButtons.forEach(function (button) {
button.classList.toggle(
'is-active',
button.getAttribute('data-tab') === nextTab
);
});
render();
}
function cardHTML(level, index) {
var safeLevel = normalizeLevel(level) || 1;
var label = t('cpSpace.levelCard', 'Best friend level {level}').replace(
'{level}',
String(safeLevel)
);
return [
'<article class="relation-card" data-level="',
safeLevel,
'">',
'<img src="',
LEVEL_IMAGES[safeLevel],
'" alt="',
escapeHTML(label),
'" draggable="false" />',
'<button class="cancel-hotspot" type="button" data-cancel-index="',
index,
'" aria-label="',
escapeHTML(t('cpSpace.cancel', 'Cancel')),
'"></button>',
'</article>',
].join('');
}
function render() {
var levels = state.tabs[state.activeTab] || [];
if (!levels.length) {
contentPanel.innerHTML =
'<div class="empty-state">' +
escapeHTML(t('cpSpace.empty', 'No relationship data yet.')) +
'</div>';
return;
}
contentPanel.innerHTML =
'<div class="relation-list">' +
levels.map(cardHTML).join('') +
'</div>';
}
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
.map(function (item) {
return normalizeLevel(
typeof item === 'object' ? item.level : item
);
})
.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];
});
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 () {
if (window.HyAppBridge && window.HyAppBridge.back) {
window.HyAppBridge.back();
return;
}
window.history.back();
});
}
tabButtons.forEach(function (button) {
button.addEventListener('click', function () {
setActiveTab(button.getAttribute('data-tab'));
});
});
contentPanel.addEventListener('click', function (event) {
var target = event.target.closest('[data-cancel-index]');
if (!target) return;
handleCancel(target.getAttribute('data-cancel-index'));
});
window.addEventListener('hyapp:cp-space-data', function (event) {
applyExternalData(event.detail || {});
});
window.addEventListener('hyapp:i18n-ready', render);
}
bindEvents();
if (window.HyAppCPSpaceData) applyExternalData(window.HyAppCPSpaceData);
else setActiveTab(state.activeTab);
if (window.HyAppBridge && window.HyAppBridge.ready) {
window.HyAppBridge.ready({ page: 'cp-space' });
}
})();

217
activity/cp-space/style.css Normal file
View File

@ -0,0 +1,217 @@
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
:root {
--cp-space-bg: #2d165d;
--cp-space-panel: #4d236d;
--cp-space-muted: #8c76d2;
--cp-space-card-top: 125px;
}
html,
body {
margin: 0;
width: 100%;
min-height: 100%;
overflow-x: hidden;
background: var(--cp-space-bg);
color: #fff;
font-family:
'Source Han Sans SC', 'Noto Sans CJK SC', Arial, 'Helvetica Neue',
Helvetica, sans-serif;
}
button {
border: 0;
padding: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
}
.app-shell {
width: 100%;
max-width: 768px;
min-height: 100vh;
margin: 0 auto;
background: var(--cp-space-bg);
}
.cp-space {
position: relative;
width: min(100vw, 768px);
min-height: 100vh;
margin: 0 auto;
overflow: hidden;
background: var(--cp-space-bg);
}
.cp-space::before,
.cp-space::after {
position: absolute;
inset: 0;
pointer-events: none;
content: '';
}
.cp-space::before {
z-index: 0;
background: url('./assets/space-bg.svg') center top / 100% auto repeat-y;
}
.cp-space::after {
display: none;
}
.top-nav {
position: relative;
z-index: 2;
height: 44px;
}
.back-button {
position: absolute;
left: 16px;
top: 10px;
width: 24px;
height: 24px;
}
.back-button span {
position: absolute;
left: 8px;
top: 5px;
width: 10px;
height: 10px;
border-left: 2px solid #fff;
border-bottom: 2px solid #fff;
transform: rotate(45deg);
}
.top-nav h1 {
position: absolute;
left: 50%;
top: 12px;
margin: 0;
color: #fff;
font-size: 18px;
font-weight: 500;
line-height: 20px;
letter-spacing: 0;
text-align: center;
transform: translateX(-50%);
}
.tab-bar {
position: relative;
z-index: 2;
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: center;
width: calc(100% - 32px);
max-width: 343px;
height: 47px;
margin: 16px auto 0;
padding: 0;
border-radius: 27.5px;
background: var(--cp-space-panel);
}
.tab-button {
position: relative;
height: 47px;
color: var(--cp-space-muted);
font-size: 18px;
font-weight: 500;
line-height: 18px;
letter-spacing: 0;
text-align: center;
}
.tab-button span {
position: relative;
z-index: 1;
}
.tab-button.is-active {
color: #fff;
}
.tab-button.is-active::before {
position: absolute;
left: 50%;
top: 0;
width: min(130px, calc(100% + 16px));
height: 47px;
background: url('./assets/tab-active.png') center / 100% 100% no-repeat;
content: '';
transform: translateX(-50%);
}
.content-panel {
position: relative;
z-index: 2;
width: 100%;
padding: 18px 10px 40px;
}
.relation-list {
display: grid;
gap: 10px;
justify-items: center;
width: 100%;
}
.relation-card {
position: relative;
width: min(355px, calc(100vw - 20px));
border-radius: 10px;
}
.relation-card img {
display: block;
width: 100%;
height: auto;
user-select: none;
-webkit-user-drag: none;
}
.cancel-hotspot {
position: absolute;
left: 35%;
bottom: 10%;
width: 30%;
height: 18%;
border-radius: 999px;
}
.empty-state {
width: min(355px, calc(100vw - 20px));
margin: 0 auto;
padding: 56px 18px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 12px;
background: rgba(77, 35, 109, 0.68);
color: rgba(255, 255, 255, 0.72);
font-size: 14px;
line-height: 20px;
text-align: center;
}
[dir='rtl'] .back-button span {
transform: rotate(225deg);
}
@media (min-width: 769px) {
.cp-space {
width: 768px;
}
.content-panel {
padding-right: 206px;
padding-left: 206px;
}
}

View File

@ -39,6 +39,7 @@
stageClass: 'wheel-classic',
lightPrefix: 'lucky_box_',
minSpeed: 2000,
slotCount: 8,
sequence: [1, 2, 3, 5, 8, 7, 6, 4, 1, 2, 3, 5, 8, 7, 6, 4, 1],
},
luxury: {
@ -62,6 +63,7 @@
stageClass: 'wheel-advanced',
minSpeed: 1000,
hasMarqueeBox: true,
slotCount: 12,
sequence: [1, 2, 3, 4, 6, 8, 12, 11, 10, 9, 7, 5, 1],
},
};
@ -332,6 +334,14 @@
: [];
}
function visibleWheelRewards(data, config) {
var items = normalizeWheelRewards(data);
var slotCount = Number(config && config.slotCount);
// H5 格子数量是固定视觉契约classic 只有 8 个外圈奖品位center 是抽奖按钮。
// 如果旧配置或缓存仍返回第 9 个奖品,前端不渲染多余格子,避免按钮被奖品占位或抽奖动画索引错位。
return slotCount > 0 ? items.slice(0, slotCount) : items;
}
function normalizeRewardItem(item) {
var target = normalizeImageObject(
item,
@ -1000,7 +1010,7 @@
]) !== false;
// 后端配置来自资源组:金币只要求预览图字段为空也能展示金额,礼物/道具/装扮则统一读取 metadata 的预览图和动态图。
state.gridItems = this.enabled[config.key]
? normalizeWheelRewards(data)
? visibleWheelRewards(data, config)
: [];
}.bind(this)
)

View File

@ -595,5 +595,17 @@
"agencyOpening.guildName": "اسم النقابة:",
"agencyOpening.created": "تاريخ الإنشاء:",
"agencyOpening.recordRevenue": "إيراد الافتتاح:",
"agencyOpening.recordReward": "المكافأة:"
"agencyOpening.recordReward": "المكافأة:",
"cpSpace.pageTitle": "أفضل صديق",
"cpSpace.pageLabel": "مساحة أفضل صديق",
"cpSpace.navLabel": "تنقل أفضل صديق",
"cpSpace.back": "رجوع",
"cpSpace.title": "أفضل صديق",
"cpSpace.tabsLabel": "تبويبات أفضل صديق",
"cpSpace.tabCp": "CP",
"cpSpace.tabBrother": "Brother",
"cpSpace.tabSister": "Sister",
"cpSpace.levelCard": "مستوى أفضل صديق {level}",
"cpSpace.cancel": "إلغاء",
"cpSpace.empty": "لا توجد بيانات علاقة بعد."
}

View File

@ -601,5 +601,17 @@
"agencyOpening.guildName": "Guild Name:",
"agencyOpening.created": "Created:",
"agencyOpening.recordRevenue": "Opening Revenue:",
"agencyOpening.recordReward": "Reward:"
"agencyOpening.recordReward": "Reward:",
"cpSpace.pageTitle": "Best Friend",
"cpSpace.pageLabel": "Best Friend space",
"cpSpace.navLabel": "Best Friend navigation",
"cpSpace.back": "Back",
"cpSpace.title": "Best Friend",
"cpSpace.tabsLabel": "Best Friend tabs",
"cpSpace.tabCp": "CP",
"cpSpace.tabBrother": "Brother",
"cpSpace.tabSister": "Sister",
"cpSpace.levelCard": "Best friend level {level}",
"cpSpace.cancel": "Cancel",
"cpSpace.empty": "No relationship data yet."
}

View File

@ -595,5 +595,17 @@
"agencyOpening.guildName": "Nombre del gremio:",
"agencyOpening.created": "Creado:",
"agencyOpening.recordRevenue": "Ingresos de apertura:",
"agencyOpening.recordReward": "Recompensa:"
"agencyOpening.recordReward": "Recompensa:",
"cpSpace.pageTitle": "Mejor amigo",
"cpSpace.pageLabel": "Espacio de mejor amigo",
"cpSpace.navLabel": "Navegación de mejor amigo",
"cpSpace.back": "Volver",
"cpSpace.title": "Mejor amigo",
"cpSpace.tabsLabel": "Pestañas de mejor amigo",
"cpSpace.tabCp": "CP",
"cpSpace.tabBrother": "Brother",
"cpSpace.tabSister": "Sister",
"cpSpace.levelCard": "Nivel de mejor amigo {level}",
"cpSpace.cancel": "Cancelar",
"cpSpace.empty": "Aún no hay datos de relación."
}

View File

@ -562,5 +562,17 @@
"agencyOpening.guildName": "Nama guild:",
"agencyOpening.created": "Dibuat:",
"agencyOpening.recordRevenue": "Pendapatan pembukaan:",
"agencyOpening.recordReward": "Hadiah:"
"agencyOpening.recordReward": "Hadiah:",
"cpSpace.pageTitle": "Sahabat Terbaik",
"cpSpace.pageLabel": "Ruang sahabat terbaik",
"cpSpace.navLabel": "Navigasi sahabat terbaik",
"cpSpace.back": "Kembali",
"cpSpace.title": "Sahabat Terbaik",
"cpSpace.tabsLabel": "Tab sahabat terbaik",
"cpSpace.tabCp": "CP",
"cpSpace.tabBrother": "Brother",
"cpSpace.tabSister": "Sister",
"cpSpace.levelCard": "Level sahabat terbaik {level}",
"cpSpace.cancel": "Batal",
"cpSpace.empty": "Belum ada data hubungan."
}

View File

@ -562,5 +562,17 @@
"agencyOpening.guildName": "Lonca adı:",
"agencyOpening.created": "Oluşturuldu:",
"agencyOpening.recordRevenue": "Açılış geliri:",
"agencyOpening.recordReward": "Ödül:"
"agencyOpening.recordReward": "Ödül:",
"cpSpace.pageTitle": "En İyi Arkadaş",
"cpSpace.pageLabel": "En iyi arkadaş alanı",
"cpSpace.navLabel": "En iyi arkadaş gezinmesi",
"cpSpace.back": "Geri",
"cpSpace.title": "En İyi Arkadaş",
"cpSpace.tabsLabel": "En iyi arkadaş sekmeleri",
"cpSpace.tabCp": "CP",
"cpSpace.tabBrother": "Brother",
"cpSpace.tabSister": "Sister",
"cpSpace.levelCard": "En iyi arkadaş seviyesi {level}",
"cpSpace.cancel": "İptal",
"cpSpace.empty": "Henüz ilişki verisi yok."
}

View File

@ -568,5 +568,17 @@
"agencyOpening.guildName": "公会名称:",
"agencyOpening.created": "创建时间:",
"agencyOpening.recordRevenue": "开业流水:",
"agencyOpening.recordReward": "奖励:"
"agencyOpening.recordReward": "奖励:",
"cpSpace.pageTitle": "挚友空间",
"cpSpace.pageLabel": "挚友空间",
"cpSpace.navLabel": "挚友空间导航",
"cpSpace.back": "返回",
"cpSpace.title": "Best Friend",
"cpSpace.tabsLabel": "挚友空间标签",
"cpSpace.tabCp": "CP",
"cpSpace.tabBrother": "Brother",
"cpSpace.tabSister": "Sister",
"cpSpace.levelCard": "挚友等级 {level}",
"cpSpace.cancel": "取消",
"cpSpace.empty": "暂无关系数据"
}