mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-05-06 20:02:57 +08:00
feat: 登录Dialog
This commit is contained in:
5
components.d.ts
vendored
5
components.d.ts
vendored
@@ -8,12 +8,14 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
FloatLabelInput: typeof import('./src/components/FloatLabelInput.vue')['default']
|
||||
NA: typeof import('naive-ui')['NA']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDialog: typeof import('naive-ui')['NDialog']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NEl: typeof import('naive-ui')['NEl']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||
@@ -21,7 +23,10 @@ declare module 'vue' {
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NModalProvider: typeof import('naive-ui')['NModalProvider']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
TitleBar: typeof import('./src/components/TitleBar.vue')['default']
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<title>Tauri + Vue + Typescript App</title>
|
||||
</head>
|
||||
|
||||
|
||||
82
src/components/FloatLabelInput.vue
Normal file
82
src/components/FloatLabelInput.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { InputInst, InputProps } from 'naive-ui'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label: string
|
||||
size?: InputProps['size']
|
||||
type?: InputProps['type']
|
||||
clearable?: InputProps['clearable']
|
||||
}>(),
|
||||
{
|
||||
size: 'medium',
|
||||
type: 'text',
|
||||
clearable: false,
|
||||
},
|
||||
)
|
||||
|
||||
const value = defineModel<InputProps['value']>('value', { required: true })
|
||||
|
||||
const focused = ref(false)
|
||||
const NInputRef = ref<InputInst>()
|
||||
|
||||
const floating = computed(() => value.value !== '' || focused.value)
|
||||
|
||||
const translateY = computed(() => {
|
||||
if (props.size === 'tiny') {
|
||||
return 'translate-y-[-90%]'
|
||||
} else if (props.size === 'small') {
|
||||
return 'translate-y-[-120%]'
|
||||
} else if (props.size === 'medium') {
|
||||
return 'translate-y-[-140%]'
|
||||
} else if (props.size === 'large') {
|
||||
return 'translate-y-[-160%]'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
defineExpose({ NInputRef })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-input
|
||||
ref="NInputRef"
|
||||
:size="size"
|
||||
:type="type"
|
||||
:clearable="clearable"
|
||||
placeholder=""
|
||||
v-model:value="value"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false">
|
||||
<template #prefix>
|
||||
<n-el
|
||||
tag="span"
|
||||
:class="[
|
||||
'float-label bg-white transition-all duration-200 ease-in-out',
|
||||
floating ? `text-0.75rem px-0.5 ${translateY}` : '',
|
||||
]">
|
||||
{{ label }}
|
||||
</n-el>
|
||||
</template>
|
||||
</n-input>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.n-input-wrapper) {
|
||||
@apply overflow-visible flex items-center;
|
||||
}
|
||||
|
||||
:deep(.n-input__prefix) {
|
||||
@apply absolute leading-none z-2;
|
||||
}
|
||||
|
||||
:deep(.n-input__input-el) {
|
||||
@apply relative z-3;
|
||||
}
|
||||
|
||||
.n-input--focus .float-label,
|
||||
.n-input:hover .float-label {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,12 @@ import icon from '../../src-tauri/icons/128x128.png'
|
||||
import { PhCopySimple, PhMinus, PhSquare, PhX } from '@phosphor-icons/vue'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useStore } from '../store.ts'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { ensureHttps } from '../utils.tsx'
|
||||
import LoginDialog from '../dialogs/LoginDialog.vue'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const appWindow = getCurrentWindow()
|
||||
const windowMaximised = ref<boolean>(false)
|
||||
@@ -15,7 +20,6 @@ const loginDialogShowing = ref<boolean>(false)
|
||||
|
||||
onMounted(async () => {
|
||||
windowMaximised.value = await appWindow.isMaximized()
|
||||
windowFullscreen.value = await appWindow.isFullscreen()
|
||||
|
||||
await appWindow.onResized(async () => {
|
||||
windowMaximised.value = await appWindow.isMaximized()
|
||||
@@ -32,7 +36,24 @@ onMounted(async () => {
|
||||
<img data-tauri-drag-region :src="icon" alt="icon" class="ml-2 mr-2 w-6 h-6" draggable="false" />
|
||||
<span data-tauri-drag-region class="text-base select-none">哔哩哔哩视频下载器</span>
|
||||
|
||||
<div class="ml-auto" />
|
||||
<div class="flex whitespace-nowrap ml-auto mr-4" @click="loginDialogShowing = true">
|
||||
<div v-if="store.userInfo !== undefined" class="flex items-center whitespace-nowrap cursor-pointer">
|
||||
<img
|
||||
class="w-8 h-8 rounded-full"
|
||||
:src="`${ensureHttps(store.userInfo.face)}@128w_128h`"
|
||||
alt=""
|
||||
draggable="false" />
|
||||
<div class="line-clamp-1">{{ store.userInfo.uname }}</div>
|
||||
</div>
|
||||
<div v-else class="flex items-center whitespace-nowrap cursor-pointer" @click="loginDialogShowing = true">
|
||||
<img
|
||||
class="w-8 h-8 rounded-full"
|
||||
src="https://i0.hdslb.com/bfs/face/member/noface.jpg@128w_128h"
|
||||
alt=""
|
||||
draggable="false" />
|
||||
<div class="line-clamp-1">未登录</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentPlatform !== 'macos'" class="flex items-center select-none">
|
||||
<div
|
||||
|
||||
131
src/dialogs/LoginDialog.vue
Normal file
131
src/dialogs/LoginDialog.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import { commands, QrcodeData, QrcodeStatus } from '../bindings.ts'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useStore } from '../store.ts'
|
||||
import icon from '../../src-tauri/icons/128x128.png'
|
||||
import FloatLabelInput from '../components/FloatLabelInput.vue'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const showing = defineModel<boolean>('showing', { required: true })
|
||||
|
||||
const currentTabName = ref<'cookie' | 'qrcode'>('cookie')
|
||||
|
||||
// 只要showing有任何变动,currentTabName就改为cookie
|
||||
watch(showing, () => {
|
||||
currentTabName.value = 'cookie'
|
||||
})
|
||||
|
||||
watch(
|
||||
() => store.config?.sessdata,
|
||||
async (value, oldValue) => {
|
||||
if (store.config === undefined) {
|
||||
return
|
||||
}
|
||||
if (oldValue !== undefined && oldValue !== '' && value === '') {
|
||||
// 如果旧的 sessdata 不为空,新的 sessdata 为空,相当于退出登录
|
||||
store.userInfo = undefined
|
||||
message.success('已退出登录')
|
||||
return
|
||||
} else if (value === undefined || value === '') {
|
||||
// 如果 sessdata 为空,说明用户没有登录
|
||||
return
|
||||
}
|
||||
const result = await commands.getUserInfo(value)
|
||||
if (result.status === 'error') {
|
||||
console.error(result.error)
|
||||
store.userInfo = undefined
|
||||
return
|
||||
}
|
||||
store.userInfo = result.data
|
||||
message.success('获取用户信息成功')
|
||||
showing.value = false
|
||||
},
|
||||
)
|
||||
|
||||
watch([showing, currentTabName], async () => {
|
||||
if (currentTabName.value !== 'qrcode' || !showing.value) {
|
||||
return
|
||||
}
|
||||
// 如果当前选项卡是二维码,并且dialog正在显示,则生成二维码
|
||||
await generateQrcode()
|
||||
})
|
||||
|
||||
const qrcodeData = ref<QrcodeData>()
|
||||
const qrcodeStatus = ref<QrcodeStatus>()
|
||||
|
||||
async function generateQrcode() {
|
||||
const result = await commands.generateQrcode()
|
||||
if (result.status === 'error') {
|
||||
console.error(result.error)
|
||||
return
|
||||
}
|
||||
qrcodeData.value = result.data
|
||||
// 每隔一秒获取一次二维码状态,直到showing为false
|
||||
const interval = setInterval(async () => {
|
||||
if (!showing.value) {
|
||||
clearInterval(interval)
|
||||
return
|
||||
}
|
||||
await getQrcodeStatus()
|
||||
handleQrcodeStatus()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function getQrcodeStatus() {
|
||||
if (qrcodeData.value === undefined) {
|
||||
return
|
||||
}
|
||||
const result = await commands.getQrcodeStatus(qrcodeData.value?.qrcode_key)
|
||||
if (result.status === 'error') {
|
||||
console.error(result.error)
|
||||
return
|
||||
}
|
||||
qrcodeStatus.value = result.data
|
||||
}
|
||||
|
||||
function handleQrcodeStatus() {
|
||||
if (qrcodeStatus.value === undefined || store.config === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (qrcodeStatus.value.code === 0) {
|
||||
const sessdata = qrcodeStatus.value.url.split('SESSDATA=')[1].split('&')[0]
|
||||
store.config.sessdata = encodeURIComponent(sessdata)
|
||||
showing.value = false
|
||||
message.success('登录成功')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal v-if="store.config !== undefined" v-model:show="showing">
|
||||
<n-dialog :showIcon="false" @close="showing = false" title="登录方式">
|
||||
<div class="flex flex-col">
|
||||
<n-tabs class="h-full" v-model:value="currentTabName" type="line" size="small" animated>
|
||||
<n-tab-pane name="cookie" tab="Cookie">
|
||||
<FloatLabelInput v-model:value="store.config.sessdata" label="SESSDATA" clearable />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="qrcode" tab="二维码" display-directive="show:lazy">
|
||||
<div class="flex flex-col">
|
||||
二维码状态:{{ qrcodeStatus?.message }}
|
||||
<n-qr-code
|
||||
v-if="qrcodeData !== undefined"
|
||||
class="mx-auto my-4"
|
||||
error-correction-level="H"
|
||||
:size="360"
|
||||
:value="qrcodeData.url"
|
||||
:icon-src="icon"
|
||||
icon-background-color="transparent"
|
||||
:icon-size="96" />
|
||||
<div v-else class="w-90 h-90 mx-auto my-4 p-3" />
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</n-dialog>
|
||||
</n-modal>
|
||||
</template>
|
||||
@@ -1,9 +1,10 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { Config } from './bindings.ts'
|
||||
import { Config, UserInfo } from './bindings.ts'
|
||||
|
||||
export const useStore = defineStore('store', () => {
|
||||
const config = ref<Config>()
|
||||
const userInfo = ref<UserInfo>()
|
||||
|
||||
return { config }
|
||||
return { config, userInfo }
|
||||
})
|
||||
|
||||
6
src/utils.tsx
Normal file
6
src/utils.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
export function ensureHttps(url: string): string {
|
||||
if (url.startsWith('http://')) {
|
||||
return url.replace('http://', 'https://')
|
||||
}
|
||||
return url
|
||||
}
|
||||
Reference in New Issue
Block a user