侧边栏和登录框
This commit is contained in:
parent
226c2a92c7
commit
1e9a05ca33
|
@ -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",
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
39
src/db.ts
39
src/db.ts
|
@ -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
|
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
})
|
|
@ -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: '邮箱'
|
||||||
|
}
|
||||||
|
]
|
|
@ -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>
|
||||||
|
)
|
||||||
|
})
|
|
@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
24
src/main.css
24
src/main.css
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
})
|
|
@ -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>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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>
|
||||||
|
}
|
Loading…
Reference in New Issue