Skip to content

Commit

Permalink
feat(mobile): ✨ 增加ios下拉刷新功能 (#189)
Browse files Browse the repository at this point in the history
替换github更新源链接|增加gitee国内更新源(官方新支持)
  • Loading branch information
nongyehong authored Jan 16, 2025
1 parent cd03444 commit 837d2b3
Show file tree
Hide file tree
Showing 20 changed files with 343 additions and 112 deletions.
18 changes: 0 additions & 18 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,10 @@

<!--引入iconpark图标库-->
<script defer src="/icon.js"></script>

<!-- PageSpy SDK -->
<script crossorigin="anonymous" src="http://localhost:6752/page-spy/index.min.js"></script>

<!-- 插件(非必须,但建议使用) -->
<script crossorigin="anonymous" src="http://localhost:6752/plugin/data-harbor/index.min.js"></script>
<script crossorigin="anonymous" src="http://localhost:6752/plugin/rrweb/index.min.js"></script>
</head>

<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>

<script>
window.$harbor = new DataHarborPlugin();
window.$rrweb = new RRWebPlugin();

[window.$harbor, window.$rrweb].forEach(p => {
PageSpy.registerPlugin(p)
})

window.$pageSpy = new PageSpy();
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion public/icon.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"installMode": "passive"
},
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDk1NkNENEZFNzg1MjVFMEEKUldRS1hsSjQvdFJzbGJXcnNPNXBYZ2RlTmlRRFZYYVI3YXhiWGpYZXFwVUtucThZUnJHUGw5dVUK",
"endpoints": ["https://github.com/HuLaSpark/HuLa/releases/latest/download/latest.json"]
"endpoints": ["https://gitee.com/HuLaSpark/HuLa/releases/download/latest/latest.json"]
}
}
}
2 changes: 1 addition & 1 deletion src/components/common/SystemNotification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { NButton, NAvatar } from 'naive-ui'
import { useNoticeStore } from '@/stores/notice.ts'

const { systemNotice } = storeToRefs(useNoticeStore())
const SysNTF: any = null
const SysNTF = null
if (!systemNotice.value) {
const SysNTF = window.$notification.create({
title: () => <p class="text-14px font-bold pl-10px">系统提示</p>,
Expand Down
158 changes: 158 additions & 0 deletions src/components/mobile/PullToRefresh.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<template>
<div
ref="containerRef"
class="pull-refresh relative overflow-auto"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd">
<!-- 下拉指示器 -->
<div
class="refresh-indicator absolute left-0 right-0 z-10 w-full flex-center transform transition-transform duration-300 bg-white/80 backdrop-blur-sm"
:class="[isRefreshing ? 'text-primary-500' : 'text-gray-400', { 'opacity-0': distance === 0 }]"
:style="{
top: '0',
height: `${indicatorHeight}px`,
transform: `translateY(-${indicatorHeight - distance}px)`
}">
<template v-if="isRefreshing">
<n-spin size="small" />
<span class="ml-2 text-sm">正在刷新...</span>
</template>
<template v-else>
<div class="flex-center flex-col">
<svg
:class="[
'color-#333 size-14px transition-transform duration-300',
{ 'rotate-180': distance >= threshold }
]">
<use href="#arrow-down"></use>
</svg>

<span class="text-xs mt-1">
{{ distance >= threshold ? '释放立即刷新' : '下拉可以刷新' }}
</span>
</div>
</template>
</div>

<!-- 内容区域 -->
<div
ref="contentRef"
class="transform transition-transform duration-300"
:style="{
transform: `translateY(${distance}px)`,
minHeight: '100%'
}">
<slot />
</div>
</div>
</template>

<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
type Props = {
threshold?: number // 触发刷新的阈值
indicatorHeight?: number // 指示器高度
disabled?: boolean // 是否禁用下拉刷新
}
const props = withDefaults(defineProps<Props>(), {
threshold: 60,
indicatorHeight: 50,
disabled: false
})
const emit = defineEmits<{
(e: 'refresh'): void
}>()
const containerRef = ref<HTMLElement>()
const contentRef = ref<HTMLElement>()
const distance = ref(0)
const startY = ref(0)
const isRefreshing = ref(false)
// 处理触摸开始
const handleTouchStart = (e: TouchEvent) => {
if (props.disabled || isRefreshing.value) return
const scrollTop = containerRef.value?.scrollTop ?? 0
// 只有在顶部才能下拉
if (scrollTop <= 0) {
startY.value = e.touches[0].clientY
}
}
// 处理触摸移动
const handleTouchMove = (e: TouchEvent) => {
if (props.disabled || !startY.value || isRefreshing.value) return
const scrollTop = containerRef.value?.scrollTop ?? 0
if (scrollTop > 0) return
const currentY = e.touches[0].clientY
const diff = currentY - startY.value
if (diff > 0) {
e.preventDefault()
// 使用阻尼系数让下拉变得越来越困难
distance.value = Math.min(diff * 0.4, props.threshold * 1.5)
}
}
// 处理触摸结束
const handleTouchEnd = () => {
if (props.disabled || !startY.value) return
if (distance.value >= props.threshold) {
isRefreshing.value = true
distance.value = props.indicatorHeight
emit('refresh')
} else {
distance.value = 0
}
startY.value = 0
}
// 完成刷新
const finishRefresh = () => {
isRefreshing.value = false
distance.value = 0
}
// 暴露方法给父组件
defineExpose({
finishRefresh
})
// 防止iOS橡皮筋效果
onMounted(() => {
useEventListener(
containerRef.value,
'touchmove',
(e: TouchEvent) => {
if (!containerRef.value) return
if (containerRef.value?.scrollTop <= 0 && e.touches[0].clientY > startY.value) {
e.preventDefault()
}
},
{ passive: false }
)
})
</script>

<style scoped>
.pull-refresh {
overscroll-behavior-y: contain;
-webkit-overflow-scrolling: touch;
}
.flex-center {
@apply flex items-center justify-center;
}
.refresh-indicator {
pointer-events: none;
}
</style>
2 changes: 1 addition & 1 deletion src/hooks/useMockMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useUserInfo } from '@/hooks/useCached.ts'
export const useMockMessage = () => {
const globalStore = useGlobalStore()
// 获取本地存储的用户信息
const userInfo = computed(() => JSON.parse(localStorage.getItem('USER_INFO') || '{}'))
const userInfo = computed(() => JSON.parse(localStorage.getItem('user') || '{}'))
const currentRoomId = computed(() => globalStore.currentSession.roomId)

/**
Expand Down
2 changes: 1 addition & 1 deletion src/layout/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ useMitt.on(WsResponseMessageType.TOKEN_EXPIRED, async (wsTokenExpire: WsTokenExp
await logout()
userStore.isSign = false
userStore.userInfo = {}
localStorage.removeItem('USER_INFO')
localStorage.removeItem('user')
localStorage.removeItem('TOKEN')
loginStore.loginStatus = LoginStatus.Init
}
Expand Down
4 changes: 1 addition & 3 deletions src/layout/left/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,13 @@ const moreList = ref<OPT.L.MoreList[]>([
label: '设置',
icon: 'settings',
click: async () => {
// todo 设置
await createWebviewWindow('设置', 'settings', 840, 840)
}
},
{
label: '关于',
icon: 'info',
click: async () => {
// todo 关于
await createWebviewWindow('关于', 'about', 360, 480)
}
},
Expand All @@ -108,7 +106,7 @@ const moreList = ref<OPT.L.MoreList[]>([
await logout()
// 如果没有设置自动登录,则清除用户信息
userStore.userInfo = {}
localStorage.removeItem('USER_INFO')
localStorage.removeItem('user')
localStorage.removeItem('TOKEN')
const headers = new Headers()
headers.append('Authorization', '')
Expand Down
44 changes: 42 additions & 2 deletions src/mobile/layout/index.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,52 @@
<template>
<div class="h-100vh">
<NavBar />
<RouterView />
<NavBar>
<template #left>
<n-flex align="center" :size="6" class="w-full">
<n-avatar
:size="38"
:src="AvatarUtils.getAvatarUrl(userStore.userInfo.avatar!)"
fallback-src="/logo.png"
round />

<n-flex vertical justify="center" :size="6">
<p
style="
font-weight: bold !important;
font-family:
system-ui,
-apple-system,
sans-serif;
"
class="text-(16px [--text-color])">
{{ userStore.userInfo.name }}
</p>
<p class="text-(10px [--text-color])">☁️ 柳州鱼峰</p>
</n-flex>
</n-flex>
</template>

<template #right>
<svg class="size-28px"><use href="#plus"></use></svg>
</template>
</NavBar>
<RouterView class="center" />
<TabBar />
</div>
</template>

<script setup lang="ts">
import TabBar from './tabBar/index.vue'
import NavBar from './navBar/index.vue'
import { useUserStore } from '@/stores/user.ts'
import { AvatarUtils } from '@/utils/avatarUtils.ts'
const userStore = useUserStore()
</script>
<style lang="scss">
.center {
height: calc(100vh - calc(env(safe-area-inset-top) + 44px) - calc(50px + env(safe-area-inset-bottom)));
padding-top: calc(env(safe-area-inset-top) + 44px);
padding-bottom: calc(max(50px, 20px + env(safe-area-inset-bottom)));
}
</style>
63 changes: 15 additions & 48 deletions src/mobile/layout/navBar/index.vue
Original file line number Diff line number Diff line change
@@ -1,65 +1,32 @@
<template>
<nav class="safe-area-nav">
<div class="flex items-center justify-between px-16px h-46px">
<!-- 左侧返回按钮 -->
<div v-if="showBack" class="flex items-center justify-center w-8 h-8 -ml-2 cursor-pointer" @click="handleBack">
<svg
class="w-5 h-5 text-gray-700"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
<div class="flex items-center justify-between w-full box-border px-16px h-full">
<!-- 左侧插槽 -->
<div class="flex-1 min-w-0">
<slot name="left"></slot>
</div>
<div v-else class="w-8"></div>

<!-- 标题 -->
<h1 class="text-center text-base font-medium text-gray-800 flex-1 truncate px-2">
{{ title }}
</h1>
<!-- 中间内容 -->
<div v-if="$slots.center" class="flex-1 flex justify-center">
<div class="flex justify-center">
<slot name="center"></slot>
</div>
</div>

<!-- 右侧插槽 -->
<div class="w-8">
<slot name="right"></slot>
<div class="flex-1 min-w-0 flex justify-end">
<div class="ml-auto">
<slot name="right"></slot>
</div>
</div>
</div>
</nav>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
interface NavBarProps {
title?: string
showBack?: boolean
onBack?: () => void
}
const props = withDefaults(defineProps<NavBarProps>(), {
title: '',
showBack: true
})

const emit = defineEmits<{
(event: 'back'): void
}>()
const router = useRouter()
const handleBack = (): void => {
if (props.onBack) {
props.onBack()
} else {
emit('back')
router.back()
}
}
</script>
<script setup lang="ts"></script>
<style scoped lang="scss">
.safe-area-nav {
@apply z-99999 fixed top-0 w-full bg-#e3e3e396 backdrop-blur-md;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding-top: env(safe-area-inset-top);
height: 44px;
}
Expand Down
Loading

0 comments on commit 837d2b3

Please sign in to comment.