Skip to content

Commit 68f6fb4

Browse files
committed
add item list layout
1 parent e5dbce8 commit 68f6fb4

File tree

6 files changed

+360
-12
lines changed

6 files changed

+360
-12
lines changed

src/components/ChainItem.vue

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<script lang="ts">
2+
export default {
3+
name: "chainItem",
4+
}
5+
6+
interface ChainItemProp {
7+
hash: string
8+
id: string
9+
img: string
10+
name: string
11+
start?: boolean
12+
lv?: string
13+
to: ChainItemProp[] | "0"
14+
}
15+
</script>
16+
<script lang="ts" setup>
17+
import { BoltIcon } from "@heroicons/vue/20/solid"
18+
import { Ref, ref, toRefs } from "vue"
19+
20+
import { useApi } from "../composables/useApi"
21+
import { useRouter } from "vue-router"
22+
import { useStorage } from "@vueuse/core"
23+
import { WindowCreator } from "../composables/useWindow"
24+
25+
const $props = defineProps<{
26+
chainTo: ChainItemProp
27+
}>()
28+
const { chainTo } = toRefs($props)
29+
const { iconStaticURL } = useApi()
30+
const angelIconHashMap = useStorage("rocox-angel-icon-hash-map", new Map())
31+
32+
const iconSrc = (hash: string, angelId: string) => {
33+
let hashedSrc = angelIconHashMap.value.get(hash)
34+
if (hashedSrc) return hashedSrc
35+
36+
let id = parseInt(angelId)
37+
return `${iconStaticURL}${id < 100 ? "0" : ""}${id}-.png`
38+
}
39+
40+
const lastLevelClass = (to: string | ChainItemProp[]) => {
41+
return to == "0" ? "last-level-in-chain" : null
42+
}
43+
44+
const $router = useRouter()
45+
const alwaysTargetNewWindow = useStorage("rocox-new-window-target", false)
46+
const angelPageTitle = useStorage("rocox-angel-page-title", "")
47+
const AngelWindow: Ref<WindowCreator | null> = ref(null)
48+
49+
function setupWindowParams(id: string, name: string, hash: string) {
50+
angelPageTitle.value = `#${id} ${name}`
51+
AngelWindow.value = new WindowCreator(id, {
52+
url: `/#/angel/${hash}`,
53+
title: angelPageTitle.value,
54+
})
55+
56+
goAngelView(hash)
57+
}
58+
function goAngelView(hash: string) {
59+
if (!hash) return false
60+
if (alwaysTargetNewWindow.value) AngelWindow.value!.setup()
61+
else
62+
$router.push({
63+
name: "Angel",
64+
params: { hash },
65+
})
66+
}
67+
</script>
68+
69+
<template>
70+
<div :class="['chain-item', lastLevelClass(chainTo.to)]">
71+
<template
72+
v-if="chainTo.start && !Array.isArray(chainTo)"
73+
class="chain-children"
74+
>
75+
<span
76+
class="angel-item"
77+
@click="setupWindowParams(chainTo.id, chainTo.name, chainTo.hash)"
78+
>
79+
<img
80+
class="angel-img"
81+
v-if="chainTo.id"
82+
:src="iconSrc(chainTo.hash, chainTo.id)"
83+
alt="Angel icon"
84+
draggable="false"
85+
loading="lazy"
86+
/>
87+
<span class="name">#{{ chainTo.id }} · {{ chainTo.name }}</span>
88+
</span>
89+
<span class="option">
90+
<BoltIcon class="icon" />
91+
<span class="lv" v-if="chainTo.lv">lv {{ chainTo.lv }}</span>
92+
<span class="lv" v-else>Super</span>
93+
</span>
94+
<chainItem
95+
v-if="!(typeof chainTo.to === 'string')"
96+
:chainTo="chainTo.to"
97+
/>
98+
</template>
99+
<template v-if="!chainTo.start && Array.isArray(chainTo)">
100+
<span v-for="item of chainTo" class="chain-children">
101+
<span
102+
class="angel-item"
103+
@click="setupWindowParams(item.id, item.name, item.hash)"
104+
>
105+
<img
106+
class="angel-img"
107+
v-if="item.id"
108+
:src="iconSrc(item.hash, item.id)"
109+
alt="Angel icon"
110+
draggable="false"
111+
loading="lazy"
112+
/>
113+
<span class="name">#{{ item.id }} · {{ item.name }}</span>
114+
</span>
115+
<span class="option" v-if="!(typeof item.to === 'string')">
116+
<span class="lv" v-if="item.lv">Level {{ item.lv }}</span>
117+
<span class="lv" v-else>Super</span>
118+
<BoltIcon class="icon" />
119+
</span>
120+
<chainItem v-if="!(typeof item.to === 'string')" :chainTo="item.to" />
121+
</span>
122+
</template>
123+
</div>
124+
</template>
125+
126+
<style lang="postcss" scoped>
127+
.chain-item {
128+
@apply w-full inline-flex flex-col justify-center items-center flex-wrap;
129+
}
130+
131+
.chain-item:not(:has(> .chain-item)) {
132+
@apply inline-flex flex-row items-start justify-evenly;
133+
}
134+
.chain-item:has(> .chain-item) {
135+
@apply inline-flex w-full items-center justify-between;
136+
}
137+
138+
.chain-children {
139+
@apply inline-flex flex-col justify-center items-center;
140+
}
141+
142+
.angel-item {
143+
@apply relative inline-flex items-center justify-center h-8 pr-8 pl-1 py-0.5 my-2
144+
border-2 rounded hover:bg-slate-200 dark:hover:bg-slate-500
145+
bg-slate-100 dark:bg-slate-600
146+
active:bg-slate-300 dark:active:bg-slate-400
147+
border-green-400 dark:border-green-500
148+
cursor-pointer overflow-hidden transition-all;
149+
}
150+
.option {
151+
@apply relative inline-flex justify-center items-center;
152+
}
153+
.option .lv {
154+
@apply absolute left-full inline-flex w-fit py-px px-1
155+
border border-slate-400
156+
bg-slate-200 dark:bg-slate-600
157+
translate-x-4
158+
whitespace-nowrap rounded font-semibold font-mono text-xs;
159+
}
160+
.option .icon {
161+
@apply w-5 h-5;
162+
}
163+
.angel-img {
164+
@apply absolute right-0 inline-block w-7;
165+
}
166+
.name {
167+
@apply inline-block w-fit
168+
font-semibold text-xs;
169+
}
170+
</style>

src/components/ItemList.vue

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<script lang="ts" setup>
2+
import { CubeTransparentIcon, BugAntIcon } from "@heroicons/vue/20/solid"
3+
import { ref, toRefs, watch } from "vue"
4+
import { useApi } from "../composables/useApi"
5+
import { computedAsync, useStorage } from "@vueuse/core"
6+
7+
const $props = withDefaults(
8+
defineProps<{
9+
id?: string
10+
search?: string
11+
page?: number
12+
}>(),
13+
{
14+
id: "",
15+
search: "",
16+
page: 1,
17+
}
18+
)
19+
20+
const { id, search, page } = toRefs($props)
21+
const { getItemList, itemStaticURL } = useApi()
22+
23+
const listData = computedAsync(async (onCancel) => {
24+
const abortController = new AbortController()
25+
26+
onCancel(() => abortController.abort())
27+
return await getItemList(
28+
{
29+
id: id.value,
30+
search: search.value,
31+
page: page.value,
32+
},
33+
abortController.signal
34+
)
35+
})
36+
37+
const loadErrorList = ref<string[]>([])
38+
function getItemSrc(itemId: string) {
39+
return `${itemStaticURL}${itemId}.png`
40+
}
41+
function onerrorList(_e: Event, id: string) {
42+
loadErrorList.value.push(id)
43+
}
44+
45+
const isEmpty = computedAsync(() => listData.value.length === 0)
46+
const pageSize = useStorage("rocox-api-item-list-size", 21)
47+
const totalFromID = useStorage("rocox-api-item-max-id", 0)
48+
49+
const $emits = defineEmits(["update:sizes"])
50+
51+
watch(listData, (val: any[]) => {
52+
if (search.value === "" && page.value === 1) {
53+
pageSize.value = val.length
54+
totalFromID.value = parseInt(val[0].id)
55+
}
56+
$emits("update:sizes", {
57+
listSize: val.length,
58+
pageSize: pageSize.value,
59+
total: totalFromID.value,
60+
})
61+
console.log(listData.value)
62+
})
63+
</script>
64+
65+
<template>
66+
<div class="item-list-main custom-scrollbar">
67+
<div class="item-card" v-for="item in listData" :key="item.hash">
68+
<span class="name-text">
69+
<span class="id">#{{ item.id }}</span> ·
70+
<span class="name">{{ item.name }}</span>
71+
</span>
72+
<span class="details">
73+
<img
74+
v-if="item.id && !loadErrorList.includes(item.id)"
75+
class="icon"
76+
@error="(e) => onerrorList(e, item.id)"
77+
:src="getItemSrc(item.id)"
78+
alt="item image"
79+
draggable="false"
80+
/>
81+
<CubeTransparentIcon v-else class="icon" />
82+
<span class="text">
83+
<span class="unique"
84+
>持有 · {{ parseInt(item.Unique) ? "" : "不" }}唯一</span
85+
>
86+
<span class="price">售价 · {{ item.Price }}</span>
87+
<span class="desc">描述 · {{ item.Desc }}</span>
88+
</span>
89+
</span>
90+
</div>
91+
<Transition name="slidedown" mode="in-out" :appear="true">
92+
<div class="empty-palceholder" v-if="isEmpty">
93+
<span class="empty-icons">
94+
<CubeTransparentIcon class="icon" /> ·
95+
<BugAntIcon class="icon" />
96+
</span>
97+
<span class="empty-text"
98+
>筛选结果为空,但不排除小概率因接口、网络等引起的故障。</span
99+
>
100+
</div>
101+
</Transition>
102+
</div>
103+
</template>
104+
105+
<style lang="postcss" scoped>
106+
.item-list-main {
107+
@apply flex flex-wrap w-96 mt-8 h-[22rem] items-start justify-center content-start
108+
overflow-auto select-none snap-y snap-mandatory;
109+
}
110+
111+
.item-card {
112+
@apply inline-flex flex-col;
113+
}
114+
115+
.name-text {
116+
@apply inline-flex items-center
117+
font-semibold text-base;
118+
}
119+
.details {
120+
@apply inline-flex;
121+
}
122+
.details .icon {
123+
@apply w-12 h-12;
124+
}
125+
.details .text {
126+
@apply inline-flex flex-wrap;
127+
}
128+
129+
.text .unique,
130+
.text .price,
131+
.text .desc {
132+
@apply inline-block px-1 py-0.5 mr-1 mb-1
133+
border-2 rounded;
134+
}
135+
136+
.empty-palceholder {
137+
@apply w-full h-full flex flex-col items-center justify-center
138+
select-none;
139+
}
140+
.empty-text {
141+
@apply inline-block
142+
text-sm font-bold text-center;
143+
}
144+
.empty-icons {
145+
@apply inline-flex justify-center items-center pb-4;
146+
}
147+
.empty-icons .icon {
148+
@apply w-12 h-12 inline-block m-2
149+
animate-pulse;
150+
}
151+
</style>

src/components/native/TitleBar.vue

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { appWindow } from "@tauri-apps/api/window"
1414
1515
import { useDarkMode } from "../../composables/useDarkMode"
1616
import { register, unregisterAll } from "@tauri-apps/api/globalShortcut"
17-
import { app } from "@tauri-apps/api"
1817
1918
const $props = withDefaults(
2019
defineProps<{
@@ -29,7 +28,6 @@ const $route = useRoute()
2928
const $router = useRouter()
3029
3130
const title = ref("Rocox Codex")
32-
const titleStack = useStorage<string[]>("rocox-title-stack", [])
3331
const isAlwaysonTop = useStorage("rocox-always-on-top", false)
3432
const category = useStorage("rocox-category", "angels")
3533
const categoryNameMap = new Map([
@@ -46,6 +44,7 @@ function updateTitle() {
4644
if (!!$route.meta.titleStorageKey)
4745
title.value =
4846
routeNameMap.get($route.name as string)! +
47+
" · " +
4948
localStorage.getItem($route.meta.titleStorageKey as string)!
5049
else
5150
title.value =
@@ -76,7 +75,6 @@ function getIspinnedClass() {
7675
// Initial top
7776
onMounted(async () => {
7877
await appWindow.setAlwaysOnTop(isAlwaysonTop.value)
79-
titleStack.value = []
8078
})
8179
8280
async function toggleIspinned() {

src/composables/useApi.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const headers = {
2424
// "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188",
2525
}
2626
const iconStaticURL = "https://res.17roco.qq.com/res/combat/icons/"
27+
const itemStaticURL = "https://res.17roco.qq.com/res/item/"
2728
const featureStaticURL = "https://res.17roco.qq.com/res/combat/property/"
2829
const damageTypeStaticMap = new Map([
2930
[
@@ -171,6 +172,7 @@ export const useApi = () => {
171172
headers,
172173
timeout,
173174
iconStaticURL,
175+
itemStaticURL,
174176
featureStaticURL,
175177
talentStaticURL,
176178
damageTypeStaticMap,

0 commit comments

Comments
 (0)