等待开发组件布局

This commit is contained in:
plightfield 2024-09-11 16:03:17 +08:00
parent 1e9a05ca33
commit ceb05a4f62
13 changed files with 285 additions and 136 deletions

View File

@ -23,6 +23,7 @@
"oh-vue-icons": "^1.0.0-rc3", "oh-vue-icons": "^1.0.0-rc3",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.3", "pinia-plugin-persistedstate": "^3.2.3",
"sortablejs": "^1.15.3",
"ua-parser-js": "^1.0.38", "ua-parser-js": "^1.0.38",
"uuid": "^10.0.0", "uuid": "^10.0.0",
"v-viewer": "^3.0.13", "v-viewer": "^3.0.13",
@ -34,6 +35,7 @@
"@tsconfig/node20": "^20.1.4", "@tsconfig/node20": "^20.1.4",
"@types/ali-oss": "^6.16.11", "@types/ali-oss": "^6.16.11",
"@types/node": "^20.14.5", "@types/node": "^20.14.5",
"@types/sortablejs": "^1.15.8",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vitejs/plugin-vue": "^5.0.5", "@vitejs/plugin-vue": "^5.0.5",

View File

@ -8,11 +8,15 @@ import SettingsOverlay from './settings/SettingsOverlay'
import Sider from './layout/sider' import Sider from './layout/sider'
import LoginModal from './user/LoginModal' import LoginModal from './user/LoginModal'
import { computed } from 'vue' import { computed } from 'vue'
import asyncLoader from './utils/asyncLoader'
import useLayoutStore from './layout/useLayoutStore'
const Grid = asyncLoader(() => import('./layout/grid'))
const settings = useSettingsStore() const settings = useSettingsStore()
const blockSize = computed(() => settings.state.blockSize + 'rem') const blockSize = computed(() => settings.state.blockSize + 'rem')
const blockPadding = computed(() => settings.state.blockPadding + 'rem') const blockPadding = computed(() => settings.state.blockPadding + 'rem')
const mainWidth = computed(() => settings.state.mainWidth + '%') const mainWidth = computed(() => settings.state.mainWidth + '%')
const blockRadius = computed(() => settings.state.blockRadius + 'rem') const blockRadius = computed(() => settings.state.blockRadius)
const layout = useLayoutStore()
</script> </script>
<template> <template>
<div class="fixed left-0 top-0 w-full h-screen style-root" @contextmenu.prevent> <div class="fixed left-0 top-0 w-full h-screen style-root" @contextmenu.prevent>
@ -23,6 +27,7 @@ const blockRadius = computed(() => settings.state.blockRadius + 'rem')
<SettingsButton /> <SettingsButton />
<Sider /> <Sider />
<LoginModal /> <LoginModal />
<Grid v-if="layout.ready" />
</div> </div>
</template> </template>

View File

@ -6,7 +6,7 @@ import asyncLoader from './utils/asyncLoader'
addIcons(MdClose, MdOpeninfull, MdClosefullscreen) addIcons(MdClose, MdOpeninfull, MdClosefullscreen)
const SearchPage = asyncLoader(() => import('@/layout/header/search/SearchPage')) const SearchPage = asyncLoader(() => import('@/layout/header/search/SearchPage'))
const noFullList: RouteStr[] = ['global-search'] const noFullList: RouteStr[] = ['global-search', 'global-adder']
export default defineComponent(() => { export default defineComponent(() => {
const router = useRouterStore() const router = useRouterStore()
@ -21,7 +21,7 @@ export default defineComponent(() => {
full.value = false full.value = false
}) })
return () => ( return () => (
<div class="fixed left-0 top-0 z-20 w-full"> <div class="fixed left-0 top-0 z-50 w-full">
{/* 背景遮罩 */} {/* 背景遮罩 */}
<Transition> <Transition>
{show.value && ( {show.value && (

81
src/layout/grid/index.tsx Normal file
View File

@ -0,0 +1,81 @@
import { defineComponent, onMounted } from 'vue'
import useLayoutStore from '../useLayoutStore'
import { OhVueIcon, addIcons } from 'oh-vue-icons'
import { MdAdd } from 'oh-vue-icons/icons'
import useRouterStore from '@/useRouterStore'
import Sortable from 'sortablejs'
addIcons(MdAdd)
export default defineComponent(() => {
const layout = useLayoutStore()
const router = useRouterStore()
let container: HTMLDivElement | null = null
onMounted(() => {
if (!container) return
new Sortable(container, {
animation: 400,
scroll: true,
scrollSensitivity: 200,
scrollSpeed: 10,
invertSwap: true,
invertedSwapThreshold: 1,
filter: '.operation-button',
ghostClass: 'opacity-20',
dragClass: 'dragging-block',
onStart: (e) => {
// layout.moving.id = e.item.id
// layout.moving.type = e.item.dataset?.type || ''
},
onMove: (e) => {
if (e.related.className.includes('operation-button') && e.willInsertAfter) return false
},
onEnd: (e) => {
// const oldPath = e.from.dataset.path
// const newPath = e.to.dataset.path
// if (e.oldIndex === undefined || e.newIndex === undefined || !oldPath || !newPath) return
// const oldIndex = e.oldIndex
// const newIndex = e.newIndex
// const oldList = useSortList(oldPath as any)
// const newList = useSortList(newPath as any)
// const item = oldList[oldIndex]
// if (!item || !isReactive(oldList) || !isReactive(newList)) return
// oldList.splice(oldIndex, 1)
// newList.splice(newIndex, 0, item)
// layout.moving.id = ''
// layout.moving.type = ''
}
})
})
return () => (
<div
class="absolute left-0 top-0 w-full h-screen overflow-y-auto no-scrollbar pt-[240px] px-[calc((100%_-_var(--main-width))_/_2)]"
onScroll={(e) => {
const h = (e.target as any).scrollTop
if (h > 60) {
// 需要移动搜索框和时间
layout.isCompact = true
} else {
layout.isCompact = false
}
}}
>
<div
class="w-full grid justify-center grid-flow-row-dense pb-[200px]"
style="grid-template-columns:repeat(auto-fill, var(--block-size));grid-auto-rows:var(--block-size);grid-template-rows:var(--block-size)"
ref={(el) => (container = el as any)}
>
<div class="w-full h-full flex justify-center items-center p-[var(--block-padding)] relative operation-button">
<div
class="w-full h-full overflow-hidden rounded-[calc(var(--block-radius)_*_var(--block-size))] bg-white/80 backdrop-blur flex justify-center items-center cursor-pointer hover:scale-105 transition-all"
onClick={() => {
router.path = 'global-adder'
}}
>
<OhVueIcon name="md-add" fill="white" scale={2} />
</div>
</div>
</div>
</div>
)
})

View File

@ -2,6 +2,7 @@ import useSettingsStore from '@/settings/useSettingsStore'
import useTimeStore from '@/utils/useTimeStore' import useTimeStore from '@/utils/useTimeStore'
import { Lunar } from 'lunar-typescript' import { Lunar } from 'lunar-typescript'
import { computed, defineComponent, Transition } from 'vue' import { computed, defineComponent, Transition } from 'vue'
import useLayoutStore from '../useLayoutStore'
export default defineComponent({ export default defineComponent({
setup() { setup() {
@ -28,21 +29,39 @@ export default defineComponent({
dayWeek dayWeek
} }
}) })
const settings = useSettingsStore() const settings = useSettingsStore()
const layout = useLayoutStore()
return () => ( return () => (
<div class="shadow-text tracking-widest font-mono text-white/80 font-bold"> <div
class="absolute z-20 shadow-text tracking-widest font-mono font-bold h-[110px] transition-all"
style={{
color: layout.isCompact ? 'white' : 'rgba(255,255,255,.8)',
transitionDuration: '.4s',
left: layout.isCompact ? '20px' : '50%',
top: layout.isCompact ? '20px' : '50px',
transform: layout.isCompact ? '' : 'translate(-50%,0)'
}}
>
<Transition> <Transition>
{settings.state.showDate && ( {settings.state.showDate && (
<div class="text-[4rem] leading-[4rem]">{text.value.timeStr}</div> <div
class={
'transition-all ' +
(layout.isCompact ? 'text-[1.4rem] leading-[1.4rem]' : 'text-[4rem] leading-[4rem]')
}
>
{text.value.timeStr}
</div>
)} )}
</Transition> </Transition>
<Transition> <Transition>
{settings.state.showTime && ( {settings.state.showTime && (
<div class="flex justify-center items-center gap-4 mt-4"> <div
class={'flex items-center gap-4 mt-4 ' + (layout.isCompact ? '' : 'justify-center')}
>
<div>{text.value.dateStr}</div> <div>{text.value.dateStr}</div>
<div>{info.value.dayWeek}</div> <Transition>{!layout.isCompact && <div>{info.value.dayWeek}</div>}</Transition>
<div>{info.value.day}</div> <Transition>{!layout.isCompact && <div>{info.value.day}</div>}</Transition>
</div> </div>
)} )}
</Transition> </Transition>

View File

@ -9,25 +9,8 @@ export default defineComponent({
setup() { setup() {
return () => ( return () => (
<> <>
<div
class="absolute z-20"
style={{
left: '50%',
top: '4rem',
transform: 'translate(-50%,0)'
}}
>
<GlobalTime /> <GlobalTime />
</div>
<div
class="absolute left-1/2 -translate-x-1/2 z-20"
style={{
top: '12rem'
}}
>
<Search /> <Search />
</div>
</> </>
) )
} }

View File

@ -5,15 +5,18 @@ import useSearchConfigStore from './useSearchConfigStore'
import SearchConfig from './SearchConfig' import SearchConfig from './SearchConfig'
import SearchHistory from './SearchHistory' import SearchHistory from './SearchHistory'
import SearchSuggestion from './SearchSuggestion' import SearchSuggestion from './SearchSuggestion'
import useLayoutStore from '@/layout/useLayoutStore'
export default defineComponent(() => { export default defineComponent(() => {
const settings = useSettingsStore() const settings = useSettingsStore()
const search = useSearchStore() const search = useSearchStore()
const searchConfig = useSearchConfigStore() const searchConfig = useSearchConfigStore()
const layout = useLayoutStore()
return () => ( return () => (
<div <div
class="relative" class="absolute left-1/2 -translate-x-1/2 z-20 transition-all"
style={{ style={{
top: layout.isCompact ? '40px' : '172px',
width: settings.state.searchWidth + 'rem' width: settings.state.searchWidth + 'rem'
}} }}
> >

View File

@ -42,5 +42,4 @@ export interface Layout {
Block | null Block | null
] ]
simple: boolean simple: boolean
loading: boolean
} }

View File

@ -5,6 +5,7 @@ import { OhVueIcon, addIcons } from 'oh-vue-icons'
import { PxHeadset, PxAddBox, PxCheck } from 'oh-vue-icons/icons' import { PxHeadset, PxAddBox, PxCheck } from 'oh-vue-icons/icons'
import useRouterStore from '@/useRouterStore' import useRouterStore from '@/useRouterStore'
import useLayoutStore from '../useLayoutStore' import useLayoutStore from '../useLayoutStore'
import useUserStore from '@/user/useUserStore'
initIcons() initIcons()
addIcons(PxHeadset, PxAddBox, PxCheck) addIcons(PxHeadset, PxAddBox, PxCheck)
@ -57,6 +58,7 @@ export default defineComponent(() => {
const selected = ref(icons[0]) const selected = ref(icons[0])
const router = useRouterStore() const router = useRouterStore()
const layout = useLayoutStore() const layout = useLayoutStore()
const user = useUserStore()
const label = ref('') const label = ref('')
watch( watch(
selected, selected,
@ -66,6 +68,8 @@ export default defineComponent(() => {
{ immediate: true } { immediate: true }
) )
return () => ( return () => (
<Transition>
{layout.ready && (
<div class="fixed left-6 top-1/2 -translate-y-1/2 h-[600px] z-30"> <div class="fixed left-6 top-1/2 -translate-y-1/2 h-[600px] z-30">
<div class="w-[56px] h-full rounded-[28px] bg-black/70 backdrop-blur flex flex-col justify-between items-center"> <div class="w-[56px] h-full rounded-[28px] bg-black/70 backdrop-blur flex flex-col justify-between items-center">
<ModeSwitch /> <ModeSwitch />
@ -115,7 +119,11 @@ export default defineComponent(() => {
<div <div
class="w-[56px] h-[56px] bg-white/40 rounded-full border-white border-[2px] border-solid overflow-hidden cursor-pointer" class="w-[56px] h-[56px] bg-white/40 rounded-full border-white border-[2px] border-solid overflow-hidden cursor-pointer"
onClick={() => { onClick={() => {
if (user.isLogin) {
router.path = 'settings-user' router.path = 'settings-user'
} else {
router.path = 'global-login'
}
}} }}
></div> ></div>
</div> </div>
@ -167,5 +175,7 @@ export default defineComponent(() => {
)} )}
</Transition> </Transition>
</div> </div>
)}
</Transition>
) )
}) })

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { Layout } from './layout.types' import type { Layout } from './layout.types'
import { reactive, ref, toRaw, watch } from 'vue' import { computed, reactive, ref, toRaw, watch } from 'vue'
import db from '@/db' import db from '@/db'
const defaultLayout: Layout = { const defaultLayout: Layout = {
@ -9,8 +9,7 @@ const defaultLayout: Layout = {
currentPage: 0, currentPage: 0,
dir: {}, dir: {},
dock: [null, null, null, null, null, null, null, null, null, null], dock: [null, null, null, null, null, null, null, null, null, null],
simple: false, simple: false
loading: true
} }
export default defineStore('layout', () => { export default defineStore('layout', () => {
@ -36,8 +35,15 @@ export default defineStore('layout', () => {
state.currentPage = 0 state.currentPage = 0
} }
) )
const currentPageList = computed(() =>
state.simple ? [] : state.content[state.current]?.[state.currentPage]?.list || []
)
// 是让时间和搜索改变位置,使画面更紧凑 —— @/layout/grid
const isCompact = ref(false)
return { return {
state, state,
ready ready,
currentPageList,
isCompact
} }
}) })

View File

@ -18,7 +18,7 @@ export default defineStore(
blockSize: 6, blockSize: 6,
blockPadding: 1, blockPadding: 1,
mainWidth: 70, mainWidth: 70,
blockRadius: 1, blockRadius: 0.2,
// 搜索 // 搜索
searchWidth: 30, searchWidth: 30,
searchRadius: 24 searchRadius: 24

View File

@ -1,15 +1,17 @@
import { defineComponent, reactive, Transition } from 'vue' import { defineComponent, reactive, Transition } from 'vue'
import useRouterStore from '@/useRouterStore' import useRouterStore from '@/useRouterStore'
import request from '@/utils/request' import request from '@/utils/request'
import useUserStore from './useUserStore'
export default defineComponent(() => { export default defineComponent(() => {
const router = useRouterStore() const router = useRouterStore()
const user = useUserStore()
const form = reactive({ const form = reactive({
email: '', email: '',
password: '' password: ''
}) })
return () => ( return () => (
<div class="fixed left-0 top-0 z-20 w-full"> <div class="fixed left-0 top-0 z-50 w-full">
<Transition> <Transition>
{router.path === 'global-login' && ( {router.path === 'global-login' && (
<div <div
@ -36,10 +38,11 @@ export default defineComponent(() => {
<input placeholder="密码" type="password" v-model={form.password} /> <input placeholder="密码" type="password" v-model={form.password} />
<button <button
onClick={() => { onClick={() => {
request('POST', '/api/user/login', { request<string>('POST', '/api/user/login', {
data: form data: form,
returnType: 'text'
}).then((res) => { }).then((res) => {
console.log(res) user.token = res
}) })
}} }}
> >

View File

@ -1,5 +1,43 @@
import request from '@/utils/request'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
interface UserInfo {
id: string
username: string
created: string
vipUntil: string
gender: number
birthday: string
brief: string
email: string
password: string
type: string
updated: string
avatar: string
openId: string
}
export default defineStore('user', () => { export default defineStore('user', () => {
return {} const token = ref(localStorage.getItem('token') || '')
watch(token, (val) => {
localStorage.setItem('token', val)
})
const profile = ref<null | UserInfo>(null)
watch(
token,
(val) => {
if (!val) return
request<UserInfo>('GET', '/api/profile').then((res) => {
profile.value = res
})
},
{ immediate: true }
)
const isLogin = computed(() => !!token.value && !!profile.value)
return {
token,
profile,
isLogin
}
}) })