等待开发组件布局

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",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.3",
"sortablejs": "^1.15.3",
"ua-parser-js": "^1.0.38",
"uuid": "^10.0.0",
"v-viewer": "^3.0.13",
@ -34,6 +35,7 @@
"@tsconfig/node20": "^20.1.4",
"@types/ali-oss": "^6.16.11",
"@types/node": "^20.14.5",
"@types/sortablejs": "^1.15.8",
"@types/ua-parser-js": "^0.7.39",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-vue": "^5.0.5",

View File

@ -8,11 +8,15 @@ import SettingsOverlay from './settings/SettingsOverlay'
import Sider from './layout/sider'
import LoginModal from './user/LoginModal'
import { computed } from 'vue'
import asyncLoader from './utils/asyncLoader'
import useLayoutStore from './layout/useLayoutStore'
const Grid = asyncLoader(() => import('./layout/grid'))
const settings = useSettingsStore()
const blockSize = computed(() => settings.state.blockSize + 'rem')
const blockPadding = computed(() => settings.state.blockPadding + 'rem')
const mainWidth = computed(() => settings.state.mainWidth + '%')
const blockRadius = computed(() => settings.state.blockRadius + 'rem')
const blockRadius = computed(() => settings.state.blockRadius)
const layout = useLayoutStore()
</script>
<template>
<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 />
<Sider />
<LoginModal />
<Grid v-if="layout.ready" />
</div>
</template>

View File

@ -6,7 +6,7 @@ import asyncLoader from './utils/asyncLoader'
addIcons(MdClose, MdOpeninfull, MdClosefullscreen)
const SearchPage = asyncLoader(() => import('@/layout/header/search/SearchPage'))
const noFullList: RouteStr[] = ['global-search']
const noFullList: RouteStr[] = ['global-search', 'global-adder']
export default defineComponent(() => {
const router = useRouterStore()
@ -21,7 +21,7 @@ export default defineComponent(() => {
full.value = false
})
return () => (
<div class="fixed left-0 top-0 z-20 w-full">
<div class="fixed left-0 top-0 z-50 w-full">
{/* 背景遮罩 */}
<Transition>
{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 { Lunar } from 'lunar-typescript'
import { computed, defineComponent, Transition } from 'vue'
import useLayoutStore from '../useLayoutStore'
export default defineComponent({
setup() {
@ -28,21 +29,39 @@ export default defineComponent({
dayWeek
}
})
const settings = useSettingsStore()
const layout = useLayoutStore()
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>
{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>
{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>{info.value.dayWeek}</div>
<div>{info.value.day}</div>
<Transition>{!layout.isCompact && <div>{info.value.dayWeek}</div>}</Transition>
<Transition>{!layout.isCompact && <div>{info.value.day}</div>}</Transition>
</div>
)}
</Transition>

View File

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

View File

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

View File

@ -42,5 +42,4 @@ export interface Layout {
Block | null
]
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 useRouterStore from '@/useRouterStore'
import useLayoutStore from '../useLayoutStore'
import useUserStore from '@/user/useUserStore'
initIcons()
addIcons(PxHeadset, PxAddBox, PxCheck)
@ -57,6 +58,7 @@ export default defineComponent(() => {
const selected = ref(icons[0])
const router = useRouterStore()
const layout = useLayoutStore()
const user = useUserStore()
const label = ref('')
watch(
selected,
@ -66,106 +68,114 @@ export default defineComponent(() => {
{ immediate: true }
)
return () => (
<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">
<ModeSwitch />
<div class="w-full h-[64px]" />
<div class="w-full h-0 flex-grow overflow-auto relative no-scrollbar">
<TransitionGroup>
{layout.state.content[layout.state.current].map((el, idx) => (
<Item
key={idx}
name={el.name}
label={el.label}
idx={idx}
active={layout.state.currentPage === idx}
onClick={() => {
layout.state.currentPage = idx
}}
/>
))}
</TransitionGroup>
<Transition>
{layout.state.content[layout.state.current]?.length > 0 && (
<Transition>
{layout.ready && (
<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">
<ModeSwitch />
<div class="w-full h-[64px]" />
<div class="w-full h-0 flex-grow overflow-auto relative no-scrollbar">
<TransitionGroup>
{layout.state.content[layout.state.current].map((el, idx) => (
<Item
key={idx}
name={el.name}
label={el.label}
idx={idx}
active={layout.state.currentPage === idx}
onClick={() => {
layout.state.currentPage = idx
}}
/>
))}
</TransitionGroup>
<Transition>
{layout.state.content[layout.state.current]?.length > 0 && (
<div
class="absolute w-full h-[56px] rounded-lg bg-white/40 left-0 transition-all"
style={{
transitionDuration: '.3s',
top: `${layout.state.currentPage * 56}px`
}}
/>
)}
</Transition>
</div>
<div class="w-full h-4" />
<Item
name="px-add-box"
label="添加"
onClick={() => {
showEdit.value = true
}}
/>
<Item
name="px-headset"
label="反馈"
onClick={() => {
router.path = 'settings-fallback'
}}
/>
<div
class="w-[56px] h-[56px] bg-white/40 rounded-full border-white border-[2px] border-solid overflow-hidden cursor-pointer"
onClick={() => {
if (user.isLogin) {
router.path = 'settings-user'
} else {
router.path = 'global-login'
}
}}
></div>
</div>
{/* 添加页面 */}
<Transition name="page-adder">
{showEdit.value && (
<div
class="absolute w-full h-[56px] rounded-lg bg-white/40 left-0 transition-all"
style={{
transitionDuration: '.3s',
top: `${layout.state.currentPage * 56}px`
class="absolute left-[70px] bottom-0 w-56 rounded-lg p-4 bg-white/40 backdrop-blur shadow-lg"
v-outside-click={() => {
showEdit.value = false
}}
/>
>
<input
class="rounded bg-black/10 text-center text-sm w-full py-1"
v-model={label.value}
maxlength={2}
/>
<div class="flex flex-wrap gap-1 mt-2">
{icons.map((el) => (
<div
class={
'p-1 rounded cursor-pointer transition-all ' +
(selected.value.name === el.name
? 'text-black/80 bg-white shadow'
: 'text-black/60 hover:shadow hover:text-black/80 hover:bg-white')
}
onClick={() => {
selected.value = { ...el }
}}
>
<OhVueIcon name={el.name} fill="black" scale={1.3} />
</div>
))}
</div>
<div
class="w-full mt-2 py-1 rounded-lg bg-white text-center text-sm shadow hover:shadow-lg transition-all cursor-pointer"
onClick={() => {
layout.state.content[layout.state.current].push({
list: [],
label: label.value,
name: selected.value.name
})
}}
>
<OhVueIcon name="px-check" />
</div>
</div>
)}
</Transition>
</div>
<div class="w-full h-4" />
<Item
name="px-add-box"
label="添加"
onClick={() => {
showEdit.value = true
}}
/>
<Item
name="px-headset"
label="反馈"
onClick={() => {
router.path = 'settings-fallback'
}}
/>
<div
class="w-[56px] h-[56px] bg-white/40 rounded-full border-white border-[2px] border-solid overflow-hidden cursor-pointer"
onClick={() => {
router.path = 'settings-user'
}}
></div>
</div>
{/* 添加页面 */}
<Transition name="page-adder">
{showEdit.value && (
<div
class="absolute left-[70px] bottom-0 w-56 rounded-lg p-4 bg-white/40 backdrop-blur shadow-lg"
v-outside-click={() => {
showEdit.value = false
}}
>
<input
class="rounded bg-black/10 text-center text-sm w-full py-1"
v-model={label.value}
maxlength={2}
/>
<div class="flex flex-wrap gap-1 mt-2">
{icons.map((el) => (
<div
class={
'p-1 rounded cursor-pointer transition-all ' +
(selected.value.name === el.name
? 'text-black/80 bg-white shadow'
: 'text-black/60 hover:shadow hover:text-black/80 hover:bg-white')
}
onClick={() => {
selected.value = { ...el }
}}
>
<OhVueIcon name={el.name} fill="black" scale={1.3} />
</div>
))}
</div>
<div
class="w-full mt-2 py-1 rounded-lg bg-white text-center text-sm shadow hover:shadow-lg transition-all cursor-pointer"
onClick={() => {
layout.state.content[layout.state.current].push({
list: [],
label: label.value,
name: selected.value.name
})
}}
>
<OhVueIcon name="px-check" />
</div>
</div>
)}
</Transition>
</div>
)}
</Transition>
)
})

View File

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

View File

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

View File

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

View File

@ -1,5 +1,43 @@
import request from '@/utils/request'
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', () => {
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
}
})