feat: 登录Dialog

This commit is contained in:
lanyeeee
2025-08-06 05:49:49 +08:00
parent 003c50ba13
commit 04ed15a196
7 changed files with 251 additions and 4 deletions

5
components.d.ts vendored
View File

@@ -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']

View File

@@ -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>

View 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>

View File

@@ -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
View 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>

View File

@@ -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
View File

@@ -0,0 +1,6 @@
export function ensureHttps(url: string): string {
if (url.startsWith('http://')) {
return url.replace('http://', 'https://')
}
return url
}