侧边栏和登录框

This commit is contained in:
plightfield 2024-09-11 13:46:40 +08:00
parent 226c2a92c7
commit 1e9a05ca33
19 changed files with 772 additions and 141 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",
"ua-parser-js": "^1.0.38",
"uuid": "^10.0.0", "uuid": "^10.0.0",
"v-viewer": "^3.0.13", "v-viewer": "^3.0.13",
"viewerjs": "^1.11.6", "viewerjs": "^1.11.6",
@ -33,6 +34,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/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",
"@vitejs/plugin-vue-jsx": "^4.0.0", "@vitejs/plugin-vue-jsx": "^4.0.0",

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -5,6 +5,8 @@ import Background from './layout/background'
import GLobalModal from './GlobalModal' import GLobalModal from './GlobalModal'
import SettingsButton from './settings/SettingsButton' import SettingsButton from './settings/SettingsButton'
import SettingsOverlay from './settings/SettingsOverlay' import SettingsOverlay from './settings/SettingsOverlay'
import Sider from './layout/sider'
import LoginModal from './user/LoginModal'
import { computed } from 'vue' import { computed } from 'vue'
const settings = useSettingsStore() const settings = useSettingsStore()
const blockSize = computed(() => settings.state.blockSize + 'rem') const blockSize = computed(() => settings.state.blockSize + 'rem')
@ -19,6 +21,8 @@ const blockRadius = computed(() => settings.state.blockRadius + 'rem')
<GLobalModal /> <GLobalModal />
<SettingsOverlay /> <SettingsOverlay />
<SettingsButton /> <SettingsButton />
<Sider />
<LoginModal />
</div> </div>
</template> </template>

View File

@ -8,3 +8,9 @@ export const ossKeyUrl = import.meta.env.PROD
export const ossBase = import.meta.env.PROD export const ossBase = import.meta.env.PROD
? 'http://btab.oss-cn-hangzhou.aliyuncs.com' ? 'http://btab.oss-cn-hangzhou.aliyuncs.com'
: 'http://btab.oss-cn-hangzhou.aliyuncs.com' : 'http://btab.oss-cn-hangzhou.aliyuncs.com'
export const apiBase = import.meta.env.PROD
? 'http://192.168.110.28:8300'
: 'http://192.168.110.28:8300'
export const cdnBase = import.meta.env.PROD ? apiBase : apiBase

View File

@ -1,45 +1,8 @@
import { createInstance, INDEXEDDB } from 'localforage' import { createInstance, INDEXEDDB } from 'localforage'
import { reactive, toRaw, watch, type WatchStopHandle } from 'vue'
const db = createInstance({ export default createInstance({
driver: INDEXEDDB, driver: INDEXEDDB,
name: 'fatfox', name: 'fatfox',
version: 1.0, version: 1.0,
storeName: 'fat_fox_key_value_pairs' storeName: 'fat_fox_key_value_pairs'
}) })
export function useForageStore<T extends { [key: string]: any; loading: boolean }>(
name: string,
defaultData: T,
partialWrite: (res: T) => T = (res) => res
) {
const state = reactive(defaultData)
const writeWatch = () =>
watch(
state,
(d) => {
db.setItem(name, partialWrite(toRaw(d) as T))
},
{ deep: true }
)
let stopWatch: WatchStopHandle = () => {}
const refresh = () => {
stopWatch()
state.loading = true
db.getItem<{ data: T }>(name).then((res) => {
if (res?.data) {
Object.assign(state, res.data)
}
state.loading = false
stopWatch = writeWatch()
})
}
refresh()
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
refresh()
}
})
}
export default db

View File

@ -1,17 +1,7 @@
import { defineComponent, reactive, ref } from 'vue' import { defineComponent, ref } from 'vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN' import { Button, Checkbox, Divider, message, Modal } from 'ant-design-vue'
import { import useSearchConfigStore from './useSearchConfigStore'
Button, import { EditOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'
Checkbox,
ConfigProvider,
Divider,
Form,
Input,
message,
Modal
} from 'ant-design-vue'
import useSearchConfigStore, { type SearchInfo } from './useSearchConfigStore'
import { EditOutlined, DeleteOutlined, PlusOutlined, CheckOutlined } from '@ant-design/icons-vue'
import asyncLoader from '@/utils/asyncLoader' import asyncLoader from '@/utils/asyncLoader'
const SearchAdder = asyncLoader(() => import('./SearchAdder')) const SearchAdder = asyncLoader(() => import('./SearchAdder'))
const SearchItem = defineComponent({ const SearchItem = defineComponent({
@ -125,69 +115,61 @@ export default defineComponent(() => {
const showAdder = ref<{ [key: string]: any } | null | undefined>(undefined) const showAdder = ref<{ [key: string]: any } | null | undefined>(undefined)
return () => ( return () => (
<div <div
class="w-full h-full bg-white/90 backdrop-blur p-4 flex flex-col select-text" class="w-full h-full bg-white/80 backdrop-blur p-4 flex flex-col select-text"
onContextmenu={(e) => e.stopPropagation()} onContextmenu={(e) => e.stopPropagation()}
> >
<ConfigProvider locale={zhCN}> <h2 class="text-center tracking-widest font-bold text-xl text-black/80"></h2>
<h2 class="text-center tracking-widest font-bold text-xl text-black/80"></h2> <div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-2 gap-4"> <Divider></Divider>
<Divider></Divider> <Divider></Divider>
<Divider></Divider> </div>
<div class="w-full h-0 flex-grow grid grid-cols-2 gap-4">
<div class="w-full h-full overflow-auto">
{searchConfig.defaultList.map((el) => (
<SearchItem key={el.url} url={el.url} icon={el.icon} name={el.name} v-model={el.show} />
))}
</div> </div>
<div class="w-full h-0 flex-grow grid grid-cols-2 gap-4"> <div class="w-full h-full flex flex-col">
<div class="w-full h-full overflow-auto"> <div class="w-full h-0 flex-grow overflow-y-auto">
{searchConfig.defaultList.map((el) => ( {searchConfig.customList.map((el) => (
<SearchItem <SearchItem
key={el.url} key={el.url}
url={el.url} url={el.url}
icon={el.icon} icon={el.icon}
name={el.name} name={el.name}
editable
v-model={el.show} v-model={el.show}
onEdit={(obj) => {
showAdder.value = obj
}}
/> />
))} ))}
</div> </div>
<div class="w-full h-full flex flex-col"> <Button
<div class="w-full h-0 flex-grow overflow-y-auto"> type="primary"
{searchConfig.customList.map((el) => ( class="mt-4"
<SearchItem block
key={el.url} icon={<PlusOutlined />}
url={el.url} onClick={() => {
icon={el.icon} showAdder.value = null
name={el.name} }}
editable >
v-model={el.show}
onEdit={(obj) => { </Button>
showAdder.value = obj
}}
/>
))}
</div>
<Button
type="primary"
class="mt-4"
block
icon={<PlusOutlined />}
onClick={() => {
showAdder.value = null
}}
>
</Button>
</div>
</div> </div>
<Modal </div>
open={showAdder.value !== undefined} <Modal
onUpdate:open={(e) => { open={showAdder.value !== undefined}
if (!e) { onUpdate:open={(e) => {
showAdder.value = undefined if (!e) {
} showAdder.value = undefined
}} }
title="添加搜索引擎" }}
footer={false} title="添加搜索引擎"
> footer={false}
<SearchAdder selected={showAdder.value as any} /> >
</Modal> <SearchAdder selected={showAdder.value as any} />
</ConfigProvider> </Modal>
</div> </div>
) )
}) })

View File

@ -22,24 +22,25 @@ export interface Block {
extra?: any extra?: any
} }
export type LayoutPages = { list: Block[]; icon: string; label: string }[] export type LayoutPages = { list: Block[]; label: string; name: string }[]
export interface Layout { export interface Layout {
content: [LayoutPages, LayoutPages, LayoutPages] content: [LayoutPages, LayoutPages, LayoutPages]
current: 0 | 1 | 2 // 游戏,工作,轻娱 current: 0 | 1 | 2 // 游戏,工作,轻娱
currentPage: number currentPage: number
dir: { [key: string]: Block[] } dir: { [key: string]: Block[] }
dock: { dock: [
q: Block | null Block | null,
w: Block | null Block | null,
e: Block | null Block | null,
r: Block | null Block | null,
a: Block | null Block | null,
s: Block | null Block | null,
d: Block | null Block | null,
f: Block | null Block | null,
b: Block | null Block | null,
} Block | null
]
simple: boolean simple: boolean
loading: boolean loading: boolean
} }

View File

@ -0,0 +1,179 @@
import { defineComponent, ref, watch } from 'vue'
import useLayoutStore from '../useLayoutStore'
import { OhVueIcon, addIcons } from 'oh-vue-icons'
import { MdVideogameassetTwotone, MdWorkhistoryTwotone, MdStarsTwotone } from 'oh-vue-icons/icons'
addIcons(MdVideogameassetTwotone, MdWorkhistoryTwotone, MdStarsTwotone)
export default defineComponent(() => {
const hover = ref(false)
const selected = ref<0 | 1 | 2>(0)
const layout = useLayoutStore()
watch(
() => layout.state.current,
(val) => {
selected.value = val
},
{ immediate: true }
)
return () => (
<div
class="w-[56px] h-[56px] relative cursor-pointer"
onMouseenter={() => (hover.value = true)}
onMouseleave={() => (hover.value = false)}
>
<svg width="56" height="56" viewBox="0 0 1 1">
<circle
class="relative z-10 transition-all"
cx="0.5"
cy="0.5"
r={hover.value ? 0.4 : 0.44}
stroke={hover.value ? 'rgba(255,255,255,.8)' : 'rgba(255,255,255,.4)'}
stroke-width={hover.value ? 0.08 : 0.03}
fill-opacity={hover.value ? 1 : 0}
fill={hover.value ? 'rgba(0,0,0,.4)' : 'transparent'}
/>
<text
x={hover.value ? '0.23' : '0.28'}
y={hover.value ? '0.6' : '0.42'}
font-size={hover.value ? '0.26' : '0.22'}
class="transition-all"
fill="white"
>
{layout.state.current === 0 ? '游戏' : layout.state.current === 1 ? '工作' : '休闲'}
</text>
<text
x="0.28"
y="0.68"
font-size="0.22"
class="transition-all"
fill={hover.value ? 'transparent' : 'white'}
>
</text>
</svg>
{/* 转盘 */}
<div
class={
'w-[200px] h-[200px] absolute -left-[72px] -top-[72px] rounded-full overflow-hidden transition-all ' +
(hover.value ? 'scale-100 rotate-0 opacity-100' : 'scale-0 -rotate-90 opacity-0')
}
style="transform-origin: 54% 54%; filter: drop-shadow(0 0 4px rgba(0,0,0,.4));transition-duration:.3s; transition-timing-function: cubic-bezier(.4,0,.2,1)"
>
<div class="absolute left-[105px] top-[18px] text-white text-[13px] flex flex-col items-center pointer-events-none">
<OhVueIcon name="md-videogameasset-twotone" fill="white" scale={1.3} />
</div>
<div class="absolute left-[150px] top-[80px] text-white text-[13px] flex flex-col items-center pointer-events-none">
<OhVueIcon name="md-workhistory-twotone" fill="white" scale={1.3} />
</div>
<div class="absolute left-[105px] top-[140px] text-white text-[13px] flex flex-col items-center pointer-events-none">
<OhVueIcon name="md-stars-twotone" fill="white" scale={1.3} />
</div>
<svg width="200" height="200" class="w-full h-full" viewBox="0 0 240 240">
<g>
<path
fill="rgba(0,0,0,.6)"
d="M155.29,94.09c-7.97-10.81-20.68-17.24-34.1-17.24-4.48,0-8.91,.71-13.17,2.1L85.85,11.74c11.31-3.68,23.12-5.54,35.1-5.54,35.71,0,69.72,17.1,91.07,45.78l-56.72,42.12Z"
/>
<path
fill="rgba(0,0,0,.6)"
d="M156.16,143.1c4.83-7.06,7.39-15.31,7.39-23.89s-2.66-17.18-7.7-24.35l56.73-42.13c14.29,19.53,21.84,42.66,21.84,66.93s-7.3,46.62-21.12,65.94l-57.13-42.5Z"
/>
<path
fill="rgba(0,0,0,.6)"
d="M120.94,233.14c-12.09,0-24-1.9-35.41-5.64l22.25-68.1c4.33,1.44,8.83,2.17,13.4,2.17,13.64,0,26.47-6.6,34.43-17.69l57.12,42.5c-21.32,29.29-55.6,46.76-91.79,46.76Z"
/>
</g>
{/* 判定区块,无颜色 */}
<g class="relative z-10">
<path
fill="transparent"
d="M155.29,94.09c-7.97-10.81-20.68-17.24-34.1-17.24-4.48,0-8.91,.71-13.17,2.1L85.85,11.74c11.31-3.68,23.12-5.54,35.1-5.54,35.71,0,69.72,17.1,91.07,45.78l-56.72,42.12Z"
onMouseenter={() => {
selected.value = 0
}}
/>
<path
fill="transparent"
d="M156.16,143.1c4.83-7.06,7.39-15.31,7.39-23.89s-2.66-17.18-7.7-24.35l56.73-42.13c14.29,19.53,21.84,42.66,21.84,66.93s-7.3,46.62-21.12,65.94l-57.13-42.5Z"
onMouseenter={() => {
selected.value = 1
}}
/>
<path
fill="transparent"
d="M120.94,233.14c-12.09,0-24-1.9-35.41-5.64l22.25-68.1c4.33,1.44,8.83,2.17,13.4,2.17,13.64,0,26.47-6.6,34.43-17.69l57.12,42.5c-21.32,29.29-55.6,46.76-91.79,46.76Z"
onMouseenter={() => {
selected.value = 2
}}
/>
</g>
<defs>
<linearGradient
id="mode-switch-selected"
x1="154.46"
y1="119.31"
x2="233.16"
y2="119.31"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#fff" />
<stop offset="1" stop-color="#fff" stop-opacity=".2" />
</linearGradient>
</defs>
<g>
<path
class={
'absolute z-30 transition-all opacity-60 ' +
(selected.value === 0
? '-rotate-[72deg]'
: selected.value === 1
? 'rotate-0'
: 'rotate-[72deg]')
}
onClick={() => {
layout.state.current = selected.value
}}
style="transform-origin: 50% 50%;"
fill="url(#mode-switch-selected)"
d="M154.46,95.56c2.19-2.22,56.63-42.87,56.63-42.87,0,0,22.04,26.2,22,64.39,1.48,38.81-20.93,68.85-20.93,68.85,0,0-55.81-40.54-57.3-42.91,2.37-2.59,16.11-22.46-.41-47.46Z"
/>
<path
class={
'relative transition-all ' +
(layout.state.current === 0
? '-rotate-[72deg]'
: layout.state.current === 1
? 'rotate-0'
: 'rotate-[72deg]')
}
style="transform-origin: 50% 50%;"
fill="none"
stroke="rgba(255,255,255,.8)"
stroke-linecap="round"
stroke-miterlimit="10"
stroke-width="3px"
d="M215.65,51.3s24,32.56,22,67.67c-1.33,31.11-10.52,53.19-21.41,68.96"
/>
<path
class={
'relative transition-all ' +
(layout.state.current === 0
? '-rotate-[72deg]'
: layout.state.current === 1
? 'rotate-0'
: 'rotate-[72deg]')
}
style="transform-origin: 50% 50%;"
fill="rgba(255,255,255,.8)"
d="M154.51,95.83l4.36-3.41s7.73,10.66,7.5,24.88c1.11,13.7-6.91,29.57-6.91,29.57l-4.63-3.81s5.47-11.68,5.32-24.42c.89-12-5.64-22.8-5.64-22.8Z"
/>
</g>
</svg>
</div>
</div>
)
})

159
src/layout/sider/icons.ts Normal file
View File

@ -0,0 +1,159 @@
import { addIcons } from 'oh-vue-icons'
import {
PxHome,
PxSpeedFast,
PxCalculator,
PxArticleMultiple,
PxVideo,
PxEdit,
PxCode,
PxImageGallery,
PxGamepad,
PxShoppingBag,
PxCoin,
PxBookOpen,
PxCar,
PxCloud,
PxCommand,
PxHumanHandsup,
PxCameraAlt,
PxDevicePhone,
PxHeart,
PxBus,
PxMusic,
PxComment,
PxTeach,
PxTrending,
PxMailMultiple
} from 'oh-vue-icons/icons'
export function initIcons() {
addIcons(
PxHome,
PxSpeedFast,
PxCalculator,
PxArticleMultiple,
PxVideo,
PxEdit,
PxCode,
PxImageGallery,
PxGamepad,
PxShoppingBag,
PxCoin,
PxBookOpen,
PxCar,
PxCloud,
PxCommand,
PxHumanHandsup,
PxCameraAlt,
PxDevicePhone,
PxHeart,
PxBus,
PxMusic,
PxComment,
PxTeach,
PxTrending,
PxMailMultiple
)
}
export default [
{
name: 'px-home',
label: '首页'
},
{
name: 'px-speed-fast',
label: '效率'
},
{
name: 'px-calculator',
label: '工具'
},
{
name: 'px-article-multiple',
label: '咨询'
},
{
name: 'px-video',
label: '影音'
},
{
name: 'px-edit',
label: '设计'
},
{
name: 'px-code',
label: '编程'
},
{
name: 'px-image-gallery',
label: '图库'
},
{
name: 'px-gamepad',
label: '游戏'
},
{
name: 'px-shopping-bag',
label: '购物'
},
{
name: 'px-coin',
label: '金融'
},
{
name: 'px-book-open',
label: '阅读'
},
{
name: 'px-car',
label: '汽车'
},
{
name: 'px-cloud',
label: '网络'
},
{
name: 'px-command',
label: '产品'
},
{
name: 'px-human-handsup',
label: '创意'
},
{
name: 'px-camera-alt',
label: '摄影'
},
{
name: 'px-device-phone',
label: '科技'
},
{
name: 'px-heart',
label: '健康'
},
{
name: 'px-bus',
label: '旅游'
},
{
name: 'px-music',
label: '音乐'
},
{
name: 'px-comment',
label: '社交'
},
{
name: 'px-teach',
label: '学习'
},
{
name: 'px-trending',
label: '股票'
},
{
name: 'px-mail-multiple',
label: '邮箱'
}
]

171
src/layout/sider/index.tsx Normal file
View File

@ -0,0 +1,171 @@
import { defineComponent, ref, Transition, TransitionGroup, watch } from 'vue'
import ModeSwitch from './ModeSwitch'
import icons, { initIcons } from './icons'
import { OhVueIcon, addIcons } from 'oh-vue-icons'
import { PxHeadset, PxAddBox, PxCheck } from 'oh-vue-icons/icons'
import useRouterStore from '@/useRouterStore'
import useLayoutStore from '../useLayoutStore'
initIcons()
addIcons(PxHeadset, PxAddBox, PxCheck)
const Item = defineComponent({
props: {
name: {
type: String,
default: ''
},
label: {
type: String,
default: ''
},
idx: {
type: Number,
default: 0
},
active: {
type: Boolean,
default: false
}
},
emits: ['click'],
setup(props, ctx) {
const hover = ref(false)
return () => (
<div
class={
'relative z-10 w-full h-[56px] flex flex-col justify-center items-center text-[13px] cursor-pointer transition-all font-bold ' +
(props.active ? 'text-white' : hover.value ? 'text-white' : 'text-white/80')
}
onMouseenter={() => {
hover.value = true
}}
onMouseleave={() => {
hover.value = false
}}
onClick={() => {
ctx.emit('click')
}}
>
<OhVueIcon name={props.name} fill="white" scale={1.2} />
{props.label}
</div>
)
}
})
export default defineComponent(() => {
const showEdit = ref(false)
const selected = ref(icons[0])
const router = useRouterStore()
const layout = useLayoutStore()
const label = ref('')
watch(
selected,
(val) => {
label.value = val.label
},
{ 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 && (
<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={() => {
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>
)
})

View File

@ -1,30 +1,43 @@
import { useForageStore } from '@/db'
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 db from '@/db'
const defaultLayout: Layout = { const defaultLayout: Layout = {
content: [[], [], []], content: [[], [], []],
current: 0, current: 0,
currentPage: 0, currentPage: 0,
dir: {}, dir: {},
dock: { dock: [null, null, null, null, null, null, null, null, null, null],
q: null,
w: null,
e: null,
r: null,
a: null,
s: null,
d: null,
f: null,
b: null
},
simple: false, simple: false,
loading: true loading: true
} }
export default defineStore('layout', () => { export default defineStore('layout', () => {
const state = useForageStore('layout', defaultLayout) const state = reactive(defaultLayout)
const ready = ref(false)
db.getItem<Layout>('layout').then((res) => {
if (res) {
Object.assign(state, res)
}
ready.value = true
})
watch(
[ready, state],
([re, s]) => {
if (!re) return
db.setItem('layout', toRaw(s))
},
{ deep: true }
)
watch(
() => state.current,
() => {
state.currentPage = 0
}
)
return { return {
state state,
ready
} }
}) })

View File

@ -40,7 +40,7 @@ body {
top: -3px; top: -3px;
} }
.ant-modal-content { .ant-modal-content {
background-color: rgba(255, 255, 255, 0.9) !important; background-color: rgba(255, 255, 255, 0.8) !important;
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
} }
.ant-modal-header { .ant-modal-header {
@ -135,3 +135,25 @@ body {
opacity: 0; opacity: 0;
transform: scale(0.4); transform: scale(0.4);
} }
/* 页面添加框动画 */
.page-adder-enter-active,
.page-adder-leave-active {
transform-origin: center bottom;
transition:
transform 0.3s ease-in-out,
left 0.3s ease-in-out,
opacity 0.3s ease-in-out;
}
.page-adder-enter-from,
.page-adder-leave-to {
opacity: 0;
transform: scale(0.4);
left: 0;
}
.no-scrollbar {
scrollbar-width: none;
-ms-overflow-style: none;
}

View File

@ -7,7 +7,7 @@ export default defineComponent(() => {
const router = useRouterStore() const router = useRouterStore()
return () => ( return () => (
<div <div
class="absolute left-8 bottom-8 p-1 z-10 flex justify-center items-center cursor-pointer rounded-lg hover:bg-black/20 transition-all" class="absolute left-10 bottom-8 p-1 z-10 flex justify-center items-center cursor-pointer rounded-lg hover:bg-black/20 transition-all"
style="filter: drop-shadow(0 0 4px rgba(0,0,0,0.2))" style="filter: drop-shadow(0 0 4px rgba(0,0,0,0.2))"
onClick={() => { onClick={() => {
router.path = 'settings-background' router.path = 'settings-background'

View File

@ -2,7 +2,7 @@ import useRouterStore from '@/useRouterStore'
import asyncLoader from '@/utils/asyncLoader' import asyncLoader from '@/utils/asyncLoader'
import { computed, defineComponent, Transition } from 'vue' import { computed, defineComponent, Transition } from 'vue'
const ProfilePage = asyncLoader(() => import('@/user/UserPage')) const UserPage = asyncLoader(() => import('@/user/UserPage'))
const BackgroundPage = asyncLoader(() => import('@/layout/background/BackgroundPage')) const BackgroundPage = asyncLoader(() => import('@/layout/background/BackgroundPage'))
const SettingsTab = defineComponent({ const SettingsTab = defineComponent({
@ -38,11 +38,11 @@ export default defineComponent(() => {
const router = useRouterStore() const router = useRouterStore()
const show = computed(() => router.path.startsWith('settings-')) const show = computed(() => router.path.startsWith('settings-'))
return () => ( return () => (
<div class="fixed left-0 bottom-0 z-20 w-full"> <div class="fixed left-0 bottom-0 z-40 w-full">
{/* 背景遮罩 */} {/* 背景遮罩 */}
{show.value && ( {show.value && (
<div <div
class="w-full h-screen" class="w-full h-screen backdrop-blur-sm"
onClick={() => { onClick={() => {
router.path = '' router.path = ''
}} }}
@ -51,7 +51,7 @@ export default defineComponent(() => {
{/* 弹框主体 */} {/* 弹框主体 */}
<Transition name="settings"> <Transition name="settings">
{show.value && ( {show.value && (
<div class="absolute left-8 bottom-20 w-[600px] h-[480px] rounded-lg overflow-hidden shadow-2xl flex"> <div class="absolute left-6 bottom-20 w-[600px] h-[480px] rounded-lg overflow-hidden shadow-2xl flex">
<div class="w-[200px] p-4 h-full bg-white/60 backdrop-blur flex flex-col"> <div class="w-[200px] p-4 h-full bg-white/60 backdrop-blur flex flex-col">
<div <div
class={ class={
@ -74,10 +74,10 @@ export default defineComponent(() => {
<SettingsTab label="重置" path="settings-reset" /> <SettingsTab label="重置" path="settings-reset" />
<SettingsTab label="问题反馈" path="settings-fallback" /> <SettingsTab label="问题反馈" path="settings-fallback" />
</div> </div>
<div class="w-0 h-full flex-grow bg-white/90 backdrop-blur"> <div class="w-0 h-full flex-grow bg-white/80 backdrop-blur">
<Transition> <Transition>
{router.path === 'settings-user' ? ( {router.path === 'settings-user' ? (
<ProfilePage /> <UserPage />
) : router.path === 'settings-background' ? ( ) : router.path === 'settings-background' ? (
<BackgroundPage /> <BackgroundPage />
) : null} ) : null}

View File

@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
export type WidgetStr = 'ai' | 'calendar' export type WidgetStr = 'ai' | 'calendar'
export type GlobalStr = 'search' | 'block' | 'adder' export type GlobalStr = 'search' | 'block' | 'adder' | 'login'
export type SettingStr = export type SettingStr =
| 'user' | 'user'
| 'background' | 'background'

58
src/user/LoginModal.tsx Normal file
View File

@ -0,0 +1,58 @@
import { defineComponent, reactive, Transition } from 'vue'
import useRouterStore from '@/useRouterStore'
import request from '@/utils/request'
export default defineComponent(() => {
const router = useRouterStore()
const form = reactive({
email: '',
password: ''
})
return () => (
<div class="fixed left-0 top-0 z-20 w-full">
<Transition>
{router.path === 'global-login' && (
<div
class="w-full h-screen bg-black/20 backdrop-blur"
onClick={() => {
router.path = ''
}}
></div>
)}
</Transition>
<Transition name="modal">
{router.path === 'global-login' && (
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 overflow-hidden transition-all w-[540px] max-w-[90%] rounded-lg p-2 bg-white/40 backdrop-blur">
<div class="w-full h-full rounded-lg bg-white/80 overflow-hidden">
<div class="flex justify-center py-4">
<img src="/logo.png" alt="logo" class="w-1/3" />
</div>
<div class="text-center text-lg text-black/60 font-bold tracking-widest">
Fatfox
</div>
<div class="flex flex-col">
<input placeholder="邮箱" v-model={form.email} />
<input placeholder="密码" type="password" v-model={form.password} />
<button
onClick={() => {
request('POST', '/api/user/login', {
data: form
}).then((res) => {
console.log(res)
})
}}
>
</button>
</div>
<div class="flex justify-end py-4">
<img src="/logo.png" alt="logo" class="w-1/2 relative top-8 left-4 opacity-10" />
</div>
</div>
</div>
)}
</Transition>
</div>
)
})

View File

@ -1,5 +1,19 @@
import useRouterStore from '@/useRouterStore'
import { Button } from 'ant-design-vue'
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
export default defineComponent(() => () => ( export default defineComponent(() => {
<div class="absolute left-0 top-0 w-full h-full p-4">this is user</div> const router = useRouterStore()
)) return () => (
<div class="absolute left-0 top-0 w-full h-full p-4">
this is user
<Button
onClick={() => {
router.path = 'global-login'
}}
>
</Button>
</div>
)
})

View File

@ -56,7 +56,7 @@ export default defineComponent({
}} }}
/> />
<div <div
class="flex justify-center items-center rounded bg-slate-200 hover:bg-slate-300 transition-all bg-cover bg-no-repeat bg-center cursor-pointer" class="flex justify-center items-center rounded bg-slate-200 hover:bg-slate-300 transition-all bg-cover bg-no-repeat bg-center cursor-pointer shadow"
style={{ style={{
width: props.width + 'px', width: props.width + 'px',
height: props.width / props.ratio + 'px', height: props.width / props.ratio + 'px',

57
src/utils/request.ts Normal file
View File

@ -0,0 +1,57 @@
import { apiBase, cdnBase } from '@/config'
import { UAParser } from 'ua-parser-js'
const ua = new UAParser(navigator.userAgent)
// 设备型号:例如 Mac Os-Chrome
const device = ua.getOS().name + '-' + ua.getBrowser().name
/**
*
* @param method
* @param url
* @param requestConfig
*/
export default function request<R>(
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
url: `${'/api' | '/cdn'}${string}`,
requestConfig?: {
data?: any
dataType?: 'json' | 'form'
returnType?: 'json' | 'text' | 'blob'
}
) {
const { data, dataType = 'json', returnType = 'json' } = requestConfig || {}
const fp = localStorage.getItem('fp') || ''
const config: any = {
method,
headers: {
Authorization: 'Bearer ' + localStorage.getItem('token'),
device,
fp
}
}
if (!['GET', 'DELETE'].includes(method)) {
if (dataType === 'json') {
config.body = JSON.stringify(data)
config.headers['Content-Type'] = 'application/json'
} else {
config.body = new FormData()
for (const key in data) {
config.body.append(key, data[key])
}
}
}
const path = url.replace(/^\/api/, apiBase).replace(/^\/cdn/, cdnBase)
return fetch(path, config).then((res) => {
if (res.status >= 200 && res.status < 400) {
return res[returnType]()
} else {
return res.text().then((err) => {
if (res.status === 401) {
localStorage.setItem('token', '')
return Promise.reject('登录已过期,请重新登录')
}
return Promise.reject(err)
})
}
}) as Promise<R>
}