装备栏、技能栏

This commit is contained in:
2025-03-15 11:13:34 +08:00
parent 5d5dc59526
commit 8be0aa2a5a
83 changed files with 353 additions and 60 deletions
+2 -1
View File
@@ -9,12 +9,13 @@ from api.core.exception import ResponseException
class AlterState: class AlterState:
def __init__(self, alter: str, token: str, charcater: Character, equVersion: str): def __init__(self, alter: str, token: str, charcater: Character, equVersion: str,time: int):
self.alter = alter self.alter = alter
self.token = token self.token = token
self.origin = alter.split('.')[-1] self.origin = alter.split('.')[-1]
self.character = charcater self.character = charcater
self.equVersion = equVersion self.equVersion = equVersion
self.time = time
def to_dict(self): def to_dict(self):
return self.__dict__ return self.__dict__
+32 -10
View File
@@ -1,26 +1,48 @@
import time
from typing import Annotated, Optional from typing import Annotated, Optional
from redis import Redis from redis import Redis
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from fastapi import Depends, Header from fastapi import Depends, Header
from api.core.exception import ResponseException from api.core.exception import ResponseException
from database.connect import get_db,get_redis from database.connect import get_db, get_redis
from api.core.Auth import AlterState, deleteToken, readToken from api.core.Auth import AlterState
import base64
import json
SessionDep = Annotated[Session, Depends(get_db)] SessionDep = Annotated[Session, Depends(get_db)]
RedisDep = Annotated[Redis, Depends(get_redis)] RedisDep = Annotated[Redis, Depends(get_redis)]
def authorize(redis:RedisDep, access_token: Optional[str] = Header(None)): def alterToken(alter_token: Optional[str] = Header(None)):
if access_token is not None: if alter_token is not None:
info = readToken(access_token, redis) try:
return info decoded_bytes = base64.b64decode(alter_token)
decoded_str = decoded_bytes.decode('utf-8')
alter = json.loads(decoded_str)
# 时间判断 后面再加
if (int(time.time() * 1000) - alter['time']) / (60 * 60 * 1000) > 6:
raise ResponseException('登录过期或无效Token,请刷新后重试')
return AlterState(alter['alter'], alter_token, {}, alter['equVersion'], alter['time'])
except (base64.binascii.Error, json.JSONDecodeError):
raise ResponseException('登录过期或无效Token,请刷新后重试')
else: else:
raise ResponseException('登录过期或无效Token,请刷新后重试') raise ResponseException('登录过期或无效Token,请刷新后重试')
pass
def unauthorize(redis:RedisDep, access_token: Optional[str] = Header(None)): AltersDep = Annotated[AlterState, Depends(alterToken)]
if access_token is not None:
return deleteToken(access_token, redis)
AltersDep = Annotated[AlterState, Depends(authorize)] # def authorize(redis:RedisDep, access_token: Optional[str] = Header(None)):
# if access_token is not None:
# info = readToken(access_token, redis)
# return info
# else:
# raise ResponseException('登录过期或无效Token,请刷新后重试')
# def unauthorize(redis:RedisDep, access_token: Optional[str] = Header(None)):
# if access_token is not None:
# return deleteToken(access_token, redis)
# TokenDep = Annotated[AlterState, Depends(authorize)]
+4 -3
View File
@@ -33,8 +33,9 @@ async def get_character_info(
request: Request, state: AltersDep ,redis:RedisDep request: Request, state: AltersDep ,redis:RedisDep
): ):
character = createCharacter(state.alter, state.equVersion) character = createCharacter(state.alter, state.equVersion)
def get_character(): # def get_character():
return character.getInfo() # return character.getInfo()
info = get_redis_info(redis, f"dcalc:character:{state.alter}", get_character) # info = get_redis_info(redis, f"dcalc:character:{state.alter}", get_character)
info = character.getInfo()
return response(data=info) return response(data=info)
+1
View File
@@ -56,6 +56,7 @@ class Suit:
point: int point: int
level: int level: int
count: int count: int
imageUrl: str
SkillAttack: float SkillAttack: float
Attack: float Attack: float
Binary file not shown.
Binary file not shown.
+1
View File
@@ -22,6 +22,7 @@ class SuitData(Base):
count = Column(Integer) count = Column(Integer)
SkillAttack = Column(Float) SkillAttack = Column(Float)
Attack = Column(Float) Attack = Column(Float)
imageUrl = Column(Text)
class EquData(Base): class EquData(Base):
-1
View File
@@ -1 +0,0 @@
* text=auto eol=lf
-30
View File
@@ -1,30 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
+2 -2
View File
@@ -59,10 +59,10 @@ export function defineRequest<T>(fn: (ax: AxiosInstance) => T) {
// const token = configStore.token // const token = configStore.token
// const uid = useAppStore().uid // const uid = useAppStore().uid
const token = useInfoStore().token const token = useInfoStore().alter_token
if (token) { if (token) {
request.headers['Access-Token'] = token request.headers['Alter-Token'] = token
} }
return request return request
+10 -1
View File
@@ -28,6 +28,7 @@ export interface ICharacterInfo {
weapons: string[] weapons: string[]
skills: ISkill[] skills: ISkill[]
suits: ISuit[] suits: ISuit[]
} }
export interface IEquipment { export interface IEquipment {
@@ -36,6 +37,9 @@ export interface IEquipment {
itemType: string itemType: string
itemDetailType: string itemDetailType: string
name: string name: string
categorize:string
suit: number[]
rarity:string
} }
export interface ISkill { export interface ISkill {
@@ -46,4 +50,9 @@ export interface ISkill {
positon: number positon: number
} }
export interface ISuit {} export interface ISuit {
id: number
name: string
imageUrl: string
suitId: number
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 977 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 937 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 939 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,81 @@
<template>
<template v-if="props.equipment">
<div class="relative" :class="{inactive: props.inactive}">
<div class="w-28px h-28px " :style="{ backgroundImage: `url(${getImageURL('equipment' + props.equipment?.imageUrl)})` }">
<img :src="getLocalImageURL(`./rarity/${rarity}.png`)" />
</div>
</div>
</template>
<template v-else>
<div class="w-26px h-26px border-1px border-solid border-#7a7a7a/50"></div>
</template>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type';
import { getImageURL } from '@/utils/images'
const props = defineProps<{
equipment?: IEquipment
inactive?: boolean
}>()
const rarity = computed(()=>{
switch(props.equipment?.rarity){
case '稀有':
return 'rare'
case '神器':
return 'artifact'
case '传说':
return 'legend'
case '史诗':
return 'epic'
case '太初':
return 'primeval'
default:
return ''
}
})
const getLocalImageURL = (url:string)=>{
return new URL(url, import.meta.url).href
}
</script>
<style lang="scss" scoped>
// @keyframes primeval {
// @for $i from 0 through 29 {
// #{percentage($i / 29)} {
// background-image: url("./effect/primeval/#{$i}.png");
// }
// }
// }
// .primeval_effect {
// width: 28px;
// height: 28px;
// background-size: cover;
// animation: primeval 2s infinite;
// // 设置动画名称,持续时间,和循环次数
// mix-blend-mode: color-dodge;
// }
.inactive {
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 28px;
height: 28px;
background-color: #00000060;
}
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

@@ -0,0 +1,40 @@
<template>
<div class="flex gap-8px">
<template v-for="(rarity,_) in rarities" :key="_">
<div class="w-14px h-14px shadow-[0px_0px_3px_#ffffff_inset,0px_0px_1px_#ffffff40]" :style="{background: rarity.background}" @click="value=rarity.value">
<div class="w-14px h-14px" :class="{'active': value === rarity.value}"></div>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
const value = defineModel<string>()
const rarities = [{
value: '稀有',
background: '#d06eff',
},{
value: '神器',
background: '#fe11ff',
},{
value: '传说',
background: '#fc8711',
},{
value: '史诗',
background: '#feca11',
},{
value: '太初',
background: 'linear-gradient(180deg, #70faa7 0%, #7bfefe 100%)',
}]
</script>
<style lang="scss" scoped>
.active{
background: url('@/assets/img/control/item_checked.png') no-repeat center;
background-size: 12px 10px;
}
</style>
@@ -0,0 +1,92 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div>
<div class="flex gao-10px">
<!-- 套装选择tab -->
<div class="flex flex-wrap w-300px">
<template v-for="(suit, _) in suits" :key="_">
<div class="flex items-center justify-center">
<img
class="w-40px h-40px object-contain"
:src="getImageURL('equipment' + suit.imageUrl)"
:alt="suit.name"
:style="{ filter: curSuit === suit.value ? '' : 'grayscale(100%)' }"
@click="curSuit = suit.value"
/>
</div>
</template>
</div>
<EquipmentRaritySelect v-model="curRarity" />
</div>
<div class="flex flex-wrap gap-4px content-start" :style="{width: `${rowCount * 32}px`}">
<template v-for="(equip,_) in suitEquipments" :key="_">
<EquipmentIcon :equipment="equip" />
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type'
import { useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
import EquipmentRaritySelect from '@/components/dnf/Equipment/RaritySelect/index.vue'
const infoStore = useInfoStore()
const curSuit = ref(16201)
const curRarity = ref()
const props = defineProps<{
rowCount?:number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const suits = computed(() => {
const uniqueSuits = new Map<number, { name: string; value: number; imageUrl: string }>()
infoStore.suits.forEach((a) => {
if (!uniqueSuits.has(a.suitId)) {
uniqueSuits.set(a.suitId, { name: a.name, value: a.suitId, imageUrl: a.imageUrl })
}
})
return Array.from(uniqueSuits.values())
})
const suitEquipments = computed(() => {
const equs = infoStore.equips.filter((a) => a.suit.includes(curSuit.value) && a.suit.length === 1)
const res: (IEquipment | undefined)[] = []
const raritiyList = ['神器','传说','史诗']
raritiyList.forEach((rarity) => {
const equ : (IEquipment | undefined)[] = equs.filter((a) => a.rarity === rarity && !a.name.includes('黑牙'))
equ.splice(5,0,undefined)
equ.splice(9,0,undefined)
res.push(
...equ,
...Array(Math.max(0,rowCount - equ.length)).fill(undefined),
)
})
const remind = equs.filter(a=>a.rarity == '太初' || a.name.includes('黑牙')).sort((a,b)=>(a.rarity == '太初' ? 1 : 0) - (b.rarity == '太初' ? 1 : 0))
res.push(undefined,undefined,...remind.filter(a=>a.itemDetailType == '项链'),undefined,...remind.filter(a=>a.itemDetailType == '手镯'),undefined,...remind.filter(a=>a.itemDetailType == '戒指'),...Array(Math.max(0,rowCount - remind.length -4)).fill(undefined))
return res
// const raritiyList = [['神器'],['传说'],['史诗','太初']]
// const partList = ['上衣','下装','头肩','腰带','鞋','项链','手镯','辅助装备','魔法石','耳环']
// // 一个品质占3行
// // 第一行 防具5+空+特殊3
// // 第二行 首饰3+3+3
// // 第三行 全空行
// partList.forEach((part) => {
// const equ = equs.filter((a) => a.itemDetailType === part)
// res.push(
// ...equ,
// ...Array(rowCount - equ.length).fill(undefined),
// )
// })
// return res
})
</script>
@@ -0,0 +1,41 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div class="flex flex-wrap gap-4px content-start" :style="{width: `${rowCount * 32}px`}">
<template v-for="(equ,_) in equs" :key="_">
<EquipmentIcon :equipment="equ" />
</template>
</div>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type'
import { useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
const props = defineProps<{
rowCount ?:number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const infoStore = useInfoStore()
const equs = computed(() => {
const items = infoStore.equips.filter(
(equip) => equip.categorize === '通宝装备' || equip.categorize === '稀有装备 套装',
)
const res: (IEquipment | undefined)[] = []
const raritiyList = ['稀有','神器','传说','史诗']
raritiyList.forEach((rarity) => {
const equ : (IEquipment | undefined)[] = items.filter((a) => a.rarity === rarity)
equ.splice(5,0,undefined)
equ.splice(9,0,undefined)
res.push(
...equ,
...Array(Math.max(0,rowCount - equ.length)).fill(undefined),
)
})
return res
})
</script>
@@ -1,18 +1,41 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template> <template>
<div class="flex flex-wrap gap-2px"> <div class="flex flex-wrap gap-4px content-start" :style="{width: `${rowCount * 32}px`}">
<template v-for="weapon in weapons" :key="weapon.id"> <template v-for="(weapon,_) in weapons" :key="_">
<div class="w-34px h-34px bg-#2c2d2c rounded-2px flex flex-col items-center"> <EquipmentIcon :equipment="weapon" />
<img class="w-30px h-30px" :src="getImageURL('equipment/'+weapon.imageUrl)" :alt="weapon.name" />
</div>
</template> </template>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useInfoStore } from '@/stores'; import type { IEquipment } from '@/api/info/type'
import { getImageURL } from '@/utils/images'; import { useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
const props = defineProps<{
rowCount ?:number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const infoStore = useInfoStore() const infoStore = useInfoStore()
const weapons = computed(()=>infoStore.equips.filter((equip) => equip.itemType === '武器' && infoStore.infos?.weapons.includes(equip.itemDetailType))) const weapons = computed(() => {
const items = infoStore.equips.filter(
(equip) => equip.itemType === '武器' && infoStore.infos?.weapons.includes(equip.itemDetailType),
)
const res: (IEquipment | undefined)[] = []
Array.from(new Set(items.map((item) => item.itemDetailType))).forEach((type) => {
const weapon = items.filter((item) => item.itemDetailType === type)
res.push(
...weapon.filter((item) => item.categorize === '制式武器'),
undefined,
...weapon.filter((item) => item.categorize === '传世武器'),
)
const remind = rowCount - res.length % rowCount
res.push(...Array(remind).fill(undefined))
})
return res
})
</script> </script>
+8 -1
View File
@@ -2,7 +2,12 @@
<div class="w-100% h-100% main" :class="props.alter.split('.').slice(-1)"> <div class="w-100% h-100% main" :class="props.alter.split('.').slice(-1)">
<div class="header"></div> <div class="header"></div>
<div class="content flex"> <div class="content flex">
<Weapon></Weapon> <div class="flex flex-col gap-20px">
<Weapon ></Weapon>
<Suit></Suit>
<Universal></Universal>
</div>
<div class="h-90vh w-560px overflow-y-auto overflow-x-hidden"> <div class="h-90vh w-560px overflow-y-auto overflow-x-hidden">
<SkillTree></SkillTree> <SkillTree></SkillTree>
@@ -18,7 +23,9 @@
import { useInfoStore } from '@/stores/info' import { useInfoStore } from '@/stores/info'
import { getImageURL } from '@/utils/images' import { getImageURL } from '@/utils/images'
import SkillTree from './Skill/SkillTree.vue'; import SkillTree from './Skill/SkillTree.vue';
import Suit from './Equipment/Suit.vue';
import Weapon from './Equipment/Weapon.vue'; import Weapon from './Equipment/Weapon.vue';
import Universal from './Equipment/Universal.vue';
const props = defineProps<{ const props = defineProps<{
alter: string alter: string
}>() }>()
+4 -3
View File
@@ -4,7 +4,7 @@ import { defineStore } from "pinia"
export const useInfoStore = defineStore("infoStore", ()=>{ export const useInfoStore = defineStore("infoStore", ()=>{
const token = ref<string>("") const alter_token = ref<string>("")
const infos = ref<ICharacterInfo | null>(null) const infos = ref<ICharacterInfo | null>(null)
const skills = computed(()=>infos.value?.skills ?? []) const skills = computed(()=>infos.value?.skills ?? [])
@@ -14,12 +14,13 @@ export const useInfoStore = defineStore("infoStore", ()=>{
const suits = computed(()=>infos.value?.suits ?? []) const suits = computed(()=>infos.value?.suits ?? [])
const createCharacter = async (alter: string, equVersion?: string) => { const createCharacter = async (alter: string, equVersion?: string) => {
token.value = await api.createToken(alter, equVersion) const time = new Date().getTime()
alter_token.value = btoa(JSON.stringify({alter, equVersion, time}))
infos.value = await api.characterInfo() infos.value = await api.characterInfo()
} }
return { return {
token, alter_token,
infos, infos,
skills, skills,
equips, equips,
+4
View File
@@ -13,3 +13,7 @@ export const getImageURL = (image: string) => {
image = image.startsWith('/') ? image : `/${image}` image = image.startsWith('/') ? image : `/${image}`
return `${baseURL}${image}` return `${baseURL}${image}`
} }
export const getLocalImageURL = (url:string)=>{
return new URL(url, import.meta.url).href
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B