From 8eec91f9d01ac10c4141d7fbe05d300a8d903ca6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:26:54 +0000 Subject: [PATCH 1/5] feat(zzz): add panel page and API client - Add getZzzPanel / getMyZzzPanel methods - Add ZzzPanel types - Add /zzz-panel page showing score, damage index, stats and drive discs --- app/composables/useApi.ts | 18 +++ app/pages/zzz-panel.vue | 333 ++++++++++++++++++++++++++++++++++++++ app/types/entities.ts | 92 +++++++++++ 3 files changed, 443 insertions(+) create mode 100644 app/pages/zzz-panel.vue diff --git a/app/composables/useApi.ts b/app/composables/useApi.ts index 6ba333e..671efe8 100644 --- a/app/composables/useApi.ts +++ b/app/composables/useApi.ts @@ -25,6 +25,7 @@ import type { PostCategory, ArticleFeed, ZzzRoleBadge, + ZzzPanel, LikeToggleResult, FavoriteToggleResult, FollowToggleResult, @@ -788,6 +789,20 @@ export function useApi() { return { success: data.success === true }; }; + const getZzzPanel = async (uid: string, avatarId?: number): Promise => { + const response = await $api("/api/zzz/panel", { + query: { uid, ...(avatarId ? { avatarId } : {}) }, + }); + return response as ZzzPanel; + }; + + const getMyZzzPanel = async (avatarId?: number): Promise => { + const response = await $api("/api/zzz/panel/me", { + query: avatarId ? { avatarId } : {}, + }); + return response as ZzzPanel; + }; + const getSelfUser = async (): Promise => { return cachedRead( qk.me.self, @@ -2342,6 +2357,9 @@ export function useApi() { pollMihoyoQr, getMihoyoBinding, unbindMihoyo, + // 绝区零面板 + getZzzPanel, + getMyZzzPanel, }; } diff --git a/app/pages/zzz-panel.vue b/app/pages/zzz-panel.vue new file mode 100644 index 0000000..e17053c --- /dev/null +++ b/app/pages/zzz-panel.vue @@ -0,0 +1,333 @@ + + + + + diff --git a/app/types/entities.ts b/app/types/entities.ts index 1d922a0..279c702 100644 --- a/app/types/entities.ts +++ b/app/types/entities.ts @@ -342,6 +342,98 @@ export interface MihoyoBinding { lastSyncedAt: string | null; } +export interface ZzzPanelStatValue { + flat: number; + pct: number; +} + +export interface ZzzPanelProperty { + PropertyId: number; + PropertyLevel: number; + PropertyValue: number; + actualValue: number; + displayValue: string; + meta?: { + id: number; + name: string; + format: string; + isPercentage: boolean; + statKey: string; + elementType?: string; + displayName: string; + }; +} + +export interface ZzzPanelDisc { + slot: number; + id: number; + level: number; + rarity: number; + mainStat: ZzzPanelProperty[]; + subStats: ZzzPanelProperty[]; +} + +export interface ZzzPanelScore { + total: number; + max: number; + discScores: { + slot: number; + score: number; + maxScore: number; + mainStat?: { propertyId: number; statKey: string; weight: number }; + subStats: { propertyId: number; statKey: string; level: number; weight: number; contribution: number }[]; + }[]; +} + +export interface ZzzPanelDamage { + damageIndex: number; + finalAtk: number; + finalHp: number; + finalDef: number; + critRate: number; + critDmg: number; + damageBonus: number; + anomalyProficiency: number; + anomalyControl: number; + impact: number; + energyRegen: number; +} + +export interface ZzzPanel { + uid: string; + region: string; + avatar: { + id: number; + name: string; + level: number; + promotionLevel: number; + mindscape: number; + coreSkill: number; + profession: string; + element: string; + weapon: unknown; + discs: ZzzPanelDisc[]; + baseStats: unknown; + buildStats: { + hp: ZzzPanelStatValue; + atk: ZzzPanelStatValue; + def: ZzzPanelStatValue; + crit_rate: number; + crit_dmg: number; + pen_ratio: number; + pen_delta: number; + impact: ZzzPanelStatValue; + energy_regen: number; + anomaly_proficiency: number; + anomaly_control: number; + anomaly_accumulation: number; + element_dmg: Partial>; + }; + score: ZzzPanelScore; + damage: ZzzPanelDamage; + }; +} + export interface LikeToggleResult { liked: boolean; likesCount: number; From 0fdce9bdb110da51dcb9f5875e56d471b38e40bd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:13:23 +0000 Subject: [PATCH 2/5] feat(zzz): new ZZZ panel page with multi-character swiper, property/weapon/disc/damage tabs, and profile badge link --- app/components/ZzzPanelDamage.vue | 264 ++++++++++++++++++++++++++ app/components/ZzzPanelDetail.vue | 238 +++++++++++++++++++++++ app/components/ZzzPanelDriveDiscs.vue | 168 ++++++++++++++++ app/components/ZzzPanelProperties.vue | 103 ++++++++++ app/components/ZzzPanelSwiper.vue | 187 ++++++++++++++++++ app/components/ZzzPanelWeapon.vue | 125 ++++++++++++ app/composables/useApi.ts | 24 +++ app/pages/profile/[id].vue | 14 +- app/pages/zzz-panel/[[uid]].vue | 147 ++++++++++++++ app/types/entities.ts | 161 ++++++++++++++++ 10 files changed, 1429 insertions(+), 2 deletions(-) create mode 100644 app/components/ZzzPanelDamage.vue create mode 100644 app/components/ZzzPanelDetail.vue create mode 100644 app/components/ZzzPanelDriveDiscs.vue create mode 100644 app/components/ZzzPanelProperties.vue create mode 100644 app/components/ZzzPanelSwiper.vue create mode 100644 app/components/ZzzPanelWeapon.vue create mode 100644 app/pages/zzz-panel/[[uid]].vue diff --git a/app/components/ZzzPanelDamage.vue b/app/components/ZzzPanelDamage.vue new file mode 100644 index 0000000..d15a669 --- /dev/null +++ b/app/components/ZzzPanelDamage.vue @@ -0,0 +1,264 @@ + + + + + diff --git a/app/components/ZzzPanelDetail.vue b/app/components/ZzzPanelDetail.vue new file mode 100644 index 0000000..2f81925 --- /dev/null +++ b/app/components/ZzzPanelDetail.vue @@ -0,0 +1,238 @@ + + + + + diff --git a/app/components/ZzzPanelDriveDiscs.vue b/app/components/ZzzPanelDriveDiscs.vue new file mode 100644 index 0000000..c9ccb4a --- /dev/null +++ b/app/components/ZzzPanelDriveDiscs.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/app/components/ZzzPanelProperties.vue b/app/components/ZzzPanelProperties.vue new file mode 100644 index 0000000..5989b54 --- /dev/null +++ b/app/components/ZzzPanelProperties.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/app/components/ZzzPanelSwiper.vue b/app/components/ZzzPanelSwiper.vue new file mode 100644 index 0000000..e306c66 --- /dev/null +++ b/app/components/ZzzPanelSwiper.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/app/components/ZzzPanelWeapon.vue b/app/components/ZzzPanelWeapon.vue new file mode 100644 index 0000000..9e44db1 --- /dev/null +++ b/app/components/ZzzPanelWeapon.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/app/composables/useApi.ts b/app/composables/useApi.ts index 6ba333e..f47a180 100644 --- a/app/composables/useApi.ts +++ b/app/composables/useApi.ts @@ -21,6 +21,8 @@ import type { ExamSubmitResult, MihoyoBinding, NsfwStatus, + ZzzPanel, + ZzzPanelsResult, Post, PostCategory, ArticleFeed, @@ -788,6 +790,25 @@ export function useApi() { return { success: data.success === true }; }; + // ── 绝区零角色面板 ──────────────────────────────── + const getZzzPanels = async (uid: string): Promise => { + const response = await cachedRead( + ["zzz-panels", uid], + async () => + $api("/api/zzz/panels", { + method: "GET", + query: { uid }, + }) as Promise, + STALE_LIST, + ); + return response as ZzzPanelsResult; + }; + + const getMyZzzPanels = async (): Promise => { + const response = await $api("/api/zzz/panels/me", { method: "GET" }); + return response as ZzzPanelsResult; + }; + const getSelfUser = async (): Promise => { return cachedRead( qk.me.self, @@ -2342,6 +2363,9 @@ export function useApi() { pollMihoyoQr, getMihoyoBinding, unbindMihoyo, + // 绝区零角色面板 + getZzzPanels, + getMyZzzPanels, }; } diff --git a/app/pages/profile/[id].vue b/app/pages/profile/[id].vue index 4a53e76..09955b9 100644 --- a/app/pages/profile/[id].vue +++ b/app/pages/profile/[id].vue @@ -532,9 +532,14 @@ onBeforeUnmount(() => {

{{ profile.name || profile.login || "匿名用户" }}

- + {{ profile.zzz.regionName || "绝区零" }} - +
@@ -971,12 +976,17 @@ onBeforeUnmount(() => { font-size: 13px; font-weight: 700; letter-spacing: 0.5px; + text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } +.ik-banner__zzz-badge:hover { + background: rgba(191, 255, 9, 0.15); +} + /* Hidden profile badge */ .ik-banner__hidden-badge { position: relative; diff --git a/app/pages/zzz-panel/[[uid]].vue b/app/pages/zzz-panel/[[uid]].vue new file mode 100644 index 0000000..eab2c51 --- /dev/null +++ b/app/pages/zzz-panel/[[uid]].vue @@ -0,0 +1,147 @@ + + + + + diff --git a/app/types/entities.ts b/app/types/entities.ts index 1d922a0..a161270 100644 --- a/app/types/entities.ts +++ b/app/types/entities.ts @@ -331,6 +331,167 @@ export interface ZzzRoleBadge { regionName?: string; } +export interface ZzzPanelProperty { + propertyId: number; + name: string; + statKey: string; + isPercentage: boolean; + base: number; + add: number; + final: number; + baseDisplay: string; + addDisplay: string; + finalDisplay: string; + weightClass: string; +} + +export interface ZzzPanelSkill { + index: number; + name: string; + level: number; +} + +export interface ZzzPanelWeaponStat { + propertyId: number; + name: string; + isPercentage: boolean; + value: number; + displayValue: string; +} + +export interface ZzzPanelWeapon { + id: number; + name: string; + level: number; + rarity: number; + rarityName: string; + star: number; + iconUrl: string; + mainStat: ZzzPanelWeaponStat[]; + subStat: ZzzPanelWeaponStat[]; +} + +export interface ZzzPanelDiscMainStat { + propertyId: number; + name: string; + isPercentage: boolean; + level: number; + value: number; + displayValue: string; +} + +export interface ZzzPanelDiscSubStat { + propertyId: number; + name: string; + isPercentage: boolean; + level: number; + value: number; + displayValue: string; +} + +export interface ZzzPanelDisc { + slot: number; + id: number; + level: number; + rarity: number; + suitId: number; + suitName: string; + suitIconUrl: string; + score: number; + comment: string; + mainStats: ZzzPanelDiscMainStat[]; + subStats: ZzzPanelDiscSubStat[]; +} + +export interface ZzzPanelScore { + total: number; + max: number; + comment: string; + weightRule: string; + propertyStats: Array<{ + propertyId: number; + name: string; + weight: number; + count: number; + value: string; + }>; + proficiencyScore: number; + discScores: Array<{ + slot: number; + score: number; + maxScore: number; + comment?: string; + mainStat?: { propertyId: number; statKey: string; weight: number }; + subStats: Array<{ + propertyId: number; + statKey: string; + level: number; + weight: number; + contribution: number; + }>; + }>; +} + +export interface ZzzPanelDamageDetails { + damageIndex: number; + selectedSkill?: { name: string; type: string }; + skills: Array<{ name: string; type: string; critDMG: number; expectDMG: number }>; + areas: Record; + buffs: Array<{ name: string; source?: string; type: string; value: number }>; + panelBuffs: Array<{ name: string; source?: string; type: string; value: number; max: number }>; +} + +export interface ZzzPanelDamage { + damageIndex: number; + finalAtk: number; + finalHp: number; + finalDef: number; + critRate: number; + critDmg: number; + damageBonus: number; + anomalyProficiency: number; + anomalyControl: number; + impact: number; + energyRegen: number; + details?: ZzzPanelDamageDetails; +} + +export interface ZzzPanelAvatar { + id: number; + name: string; + level: number; + promotionLevel: number; + mindscape: number; + coreSkill: number; + rank: number; + profession: string; + element: string; + rarity: number; + rarityName: string; + iconUrls: { portrait: string; circle: string; square: string }; + skills: ZzzPanelSkill[]; + properties: ZzzPanelProperty[]; + weapon: ZzzPanelWeapon | null; + discs: ZzzPanelDisc[]; + baseStats: unknown; + buildStats: unknown; + score: ZzzPanelScore; + damage: ZzzPanelDamage; +} + +export interface ZzzPanel { + uid: string; + region: string; + avatar: ZzzPanelAvatar; +} + +export interface ZzzPanelsResult { + uid: string; + region: string; + count: number; + avatars: ZzzPanelAvatar[]; +} + /** 当前用户的米游社绑定信息 */ export interface MihoyoBinding { aid: string; From 82d2f4060a75a3c47cecfd1c6ffc3d25993281fb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:37:35 +0000 Subject: [PATCH 3/5] fix(zzz): remove duplicate panel types and unused imports after merge --- app/composables/useApi.ts | 2 - app/types/entities.ts | 92 --------------------------------------- 2 files changed, 94 deletions(-) diff --git a/app/composables/useApi.ts b/app/composables/useApi.ts index d3b4d92..deef9a0 100644 --- a/app/composables/useApi.ts +++ b/app/composables/useApi.ts @@ -21,13 +21,11 @@ import type { ExamSubmitResult, MihoyoBinding, NsfwStatus, - ZzzPanel, ZzzPanelsResult, Post, PostCategory, ArticleFeed, ZzzRoleBadge, - ZzzPanel, LikeToggleResult, FavoriteToggleResult, FollowToggleResult, diff --git a/app/types/entities.ts b/app/types/entities.ts index 23a6cec..a161270 100644 --- a/app/types/entities.ts +++ b/app/types/entities.ts @@ -503,98 +503,6 @@ export interface MihoyoBinding { lastSyncedAt: string | null; } -export interface ZzzPanelStatValue { - flat: number; - pct: number; -} - -export interface ZzzPanelProperty { - PropertyId: number; - PropertyLevel: number; - PropertyValue: number; - actualValue: number; - displayValue: string; - meta?: { - id: number; - name: string; - format: string; - isPercentage: boolean; - statKey: string; - elementType?: string; - displayName: string; - }; -} - -export interface ZzzPanelDisc { - slot: number; - id: number; - level: number; - rarity: number; - mainStat: ZzzPanelProperty[]; - subStats: ZzzPanelProperty[]; -} - -export interface ZzzPanelScore { - total: number; - max: number; - discScores: { - slot: number; - score: number; - maxScore: number; - mainStat?: { propertyId: number; statKey: string; weight: number }; - subStats: { propertyId: number; statKey: string; level: number; weight: number; contribution: number }[]; - }[]; -} - -export interface ZzzPanelDamage { - damageIndex: number; - finalAtk: number; - finalHp: number; - finalDef: number; - critRate: number; - critDmg: number; - damageBonus: number; - anomalyProficiency: number; - anomalyControl: number; - impact: number; - energyRegen: number; -} - -export interface ZzzPanel { - uid: string; - region: string; - avatar: { - id: number; - name: string; - level: number; - promotionLevel: number; - mindscape: number; - coreSkill: number; - profession: string; - element: string; - weapon: unknown; - discs: ZzzPanelDisc[]; - baseStats: unknown; - buildStats: { - hp: ZzzPanelStatValue; - atk: ZzzPanelStatValue; - def: ZzzPanelStatValue; - crit_rate: number; - crit_dmg: number; - pen_ratio: number; - pen_delta: number; - impact: ZzzPanelStatValue; - energy_regen: number; - anomaly_proficiency: number; - anomaly_control: number; - anomaly_accumulation: number; - element_dmg: Partial>; - }; - score: ZzzPanelScore; - damage: ZzzPanelDamage; - }; -} - export interface LikeToggleResult { liked: boolean; likesCount: number; From c39cd108ab4c4ff32c42c2e0b331eefc5842237d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:15:05 +0000 Subject: [PATCH 4/5] feat(zzz): replicate zenless-tools character selector card row --- app/components/ZzzCharacterAvatar.vue | 154 ++++++++++++++++++++++++++ app/components/ZzzPanelSwiper.vue | 122 ++++---------------- app/types/entities.ts | 2 +- 3 files changed, 178 insertions(+), 100 deletions(-) create mode 100644 app/components/ZzzCharacterAvatar.vue diff --git a/app/components/ZzzCharacterAvatar.vue b/app/components/ZzzCharacterAvatar.vue new file mode 100644 index 0000000..05f17b5 --- /dev/null +++ b/app/components/ZzzCharacterAvatar.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/app/components/ZzzPanelSwiper.vue b/app/components/ZzzPanelSwiper.vue index e306c66..24c4384 100644 --- a/app/components/ZzzPanelSwiper.vue +++ b/app/components/ZzzPanelSwiper.vue @@ -15,13 +15,6 @@ const emit = defineEmits<{ const emblaRef = shallowRef(null); const emblaApi = shallowRef(); -const sync = () => { - const api = emblaApi.value; - if (!api) return; - const index = api.selectedScrollSnap(); - emit("update:modelValue", index); -}; - const destroy = () => { if (emblaApi.value) { emblaApi.value.destroy(); @@ -34,12 +27,9 @@ const init = (el: HTMLElement) => { emblaApi.value = EmblaCarousel(el, { loop: false, align: "start", + dragFree: true, dragThreshold: 6, - slidesToScroll: 1, }); - emblaApi.value.on("select", sync); - emblaApi.value.on("reInit", sync); - sync(); }; watch( @@ -52,51 +42,25 @@ watch( { immediate: true, deep: false }, ); -watch( - () => props.modelValue, - (index) => { - const api = emblaApi.value; - if (!api) return; - if (api.selectedScrollSnap() !== index) { - api.scrollTo(index); - } - }, -); - onBeforeUnmount(() => destroy()); - -const elementClasses: Record = { - physical: "is-physical", - fire: "is-fire", - ice: "is-ice", - electric: "is-electric", - ether: "is-ether", -};