naive-ui.
This commit is contained in:
@@ -4,7 +4,12 @@ import { logout } from "../api"
|
||||
import { useUserStore } from "../store/user"
|
||||
import { isDark, toggleDark } from "~/shared/composables/dark"
|
||||
import { toggleLogin, toggleSignup } from "~/shared/composables/modal"
|
||||
import { isDesktop } from "../composables/breakpoints"
|
||||
import type {
|
||||
MenuOption,
|
||||
DropdownOption,
|
||||
DropdownDividerOption,
|
||||
} from "naive-ui"
|
||||
import { RouterLink } from "vue-router"
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
@@ -15,8 +20,8 @@ async function handleLogout() {
|
||||
router.replace("/")
|
||||
}
|
||||
|
||||
function handleDropdown(command: string) {
|
||||
switch (command) {
|
||||
function handleDropdown(key: string) {
|
||||
switch (key) {
|
||||
case "logout":
|
||||
handleLogout()
|
||||
break
|
||||
@@ -24,57 +29,66 @@ function handleDropdown(command: string) {
|
||||
}
|
||||
|
||||
onMounted(userStore.getMyProfile)
|
||||
|
||||
const menus: MenuOption[] = [
|
||||
{
|
||||
label: () =>
|
||||
h(RouterLink, { to: "/learn#step-1" }, { default: () => "自学" }),
|
||||
key: "learn",
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: "/" }, { default: () => "题库" }),
|
||||
key: "problem",
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: "/contest" }, { default: () => "比赛" }),
|
||||
key: "contest",
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: "/status" }, { default: () => "提交" }),
|
||||
key: "status",
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: "/rank" }, { default: () => "排名" }),
|
||||
key: "rank",
|
||||
},
|
||||
]
|
||||
|
||||
const options = computed<Array<DropdownOption | DropdownDividerOption>>(() => [
|
||||
{ label: "我的主页", key: "home" },
|
||||
{ label: "我的提交", key: "status" },
|
||||
{ label: "我的设置", key: "setting" },
|
||||
{ label: "后台管理", key: "admin", show: userStore.isAdminRole },
|
||||
{ type: "divider" },
|
||||
{ label: "退出", key: "logout" },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-menu
|
||||
v-if="isDesktop"
|
||||
router
|
||||
mode="horizontal"
|
||||
:default-active="$route.path"
|
||||
>
|
||||
<el-menu-item index="/learn#step-1">自学</el-menu-item>
|
||||
<el-menu-item index="/">题库</el-menu-item>
|
||||
<el-menu-item index="/contest">比赛</el-menu-item>
|
||||
<el-menu-item index="/status">提交</el-menu-item>
|
||||
<el-menu-item index="/rank">排名</el-menu-item>
|
||||
</el-menu>
|
||||
<el-space v-if="isDesktop" class="actions">
|
||||
<el-button
|
||||
circle
|
||||
:icon="isDark ? Sunny : Moon"
|
||||
@click="toggleDark()"
|
||||
></el-button>
|
||||
<div v-if="userStore.isFinished && !userStore.isAuthed">
|
||||
<el-button @click="toggleLogin(true)">登录</el-button>
|
||||
<el-button @click="toggleSignup(true)">注册</el-button>
|
||||
</div>
|
||||
<div v-if="userStore.isFinished && userStore.isAuthed">
|
||||
<el-dropdown @command="handleDropdown">
|
||||
<el-button>{{ userStore.user.username }}</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>我的主页</el-dropdown-item>
|
||||
<el-dropdown-item>我的提交</el-dropdown-item>
|
||||
<el-dropdown-item>我的设置</el-dropdown-item>
|
||||
<el-dropdown-item v-if="userStore.isAdminRole">
|
||||
后台管理
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">退出</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
<n-space justify="space-between" align="center">
|
||||
<n-menu mode="horizontal" :options="menus" default-value="problem"></n-menu>
|
||||
<n-space>
|
||||
<n-button circle @click="toggleDark()">
|
||||
<template #icon>
|
||||
<n-icon v-if="isDark"><Sunny /></n-icon>
|
||||
<n-icon v-else><Moon /></n-icon>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-space>
|
||||
</n-button>
|
||||
<div v-if="userStore.isFinished">
|
||||
<n-dropdown
|
||||
v-if="userStore.isAuthed"
|
||||
:options="options"
|
||||
@select="handleDropdown"
|
||||
>
|
||||
<n-button>{{ userStore.user.username }}</n-button>
|
||||
</n-dropdown>
|
||||
<n-space v-else>
|
||||
<n-button @click="toggleLogin(true)">登录</n-button>
|
||||
<n-button @click="toggleSignup(true)">注册</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.el-menu {
|
||||
flex: 1;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: solid 1px var(--el-menu-border-color);
|
||||
}
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { FormInstance } from "element-plus"
|
||||
import { login } from "../api"
|
||||
import { loginModal, toggleLogin, toggleSignup } from "../composables/modal"
|
||||
import { useUserStore } from "../store/user"
|
||||
import type { FormRules } from "naive-ui"
|
||||
|
||||
const userStore = useUserStore()
|
||||
const loginRef = ref<FormInstance>()
|
||||
const loginRef = ref()
|
||||
const form = reactive({
|
||||
username: "",
|
||||
password: "",
|
||||
})
|
||||
const rules = reactive({
|
||||
const rules: FormRules = {
|
||||
username: [{ required: true, message: "用户名必填", trigger: "blur" }],
|
||||
password: [
|
||||
{ required: true, message: "密码必填", trigger: "blur" },
|
||||
{ min: 6, max: 20, message: "长度在6到20位之间", trigger: "change" },
|
||||
{ min: 6, max: 20, message: "长度在6到20位之间", trigger: "input" },
|
||||
],
|
||||
})
|
||||
}
|
||||
const { isLoading, error, execute } = login(form)
|
||||
const msg = computed(() => error.value && "用户名或密码不正确")
|
||||
|
||||
async function submit() {
|
||||
if (!loginRef.value) return
|
||||
const valid = await loginRef.value.validate()
|
||||
if (valid) {
|
||||
await execute()
|
||||
if (!error.value) {
|
||||
toggleLogin(false)
|
||||
userStore.getMyProfile()
|
||||
loginRef.value?.validate(async (errors: FormRules | undefined) => {
|
||||
if (!errors) {
|
||||
await execute()
|
||||
if (!error.value) {
|
||||
toggleLogin(false)
|
||||
userStore.getMyProfile()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function goSignup() {
|
||||
@@ -39,41 +39,43 @@ function goSignup() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
style="max-width: 400px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
v-model="loginModal"
|
||||
<n-modal
|
||||
:mask-closable="false"
|
||||
v-model:show="loginModal"
|
||||
preset="card"
|
||||
title="登录"
|
||||
style="width: 400px"
|
||||
:auto-focus="false"
|
||||
>
|
||||
<el-form
|
||||
ref="loginRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="right"
|
||||
label-width="70px"
|
||||
>
|
||||
<el-form-item label="用户名" required prop="username">
|
||||
<el-input v-model="form.username" name="username"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" required prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
<n-form ref="loginRef" :model="form" :rules="rules" show-require-mark>
|
||||
<n-form-item label="用户名" path="username">
|
||||
<n-input
|
||||
v-model:value="form.username"
|
||||
autofocus
|
||||
clearable
|
||||
name="username"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="密码" path="password">
|
||||
<n-input
|
||||
v-model:value="form.password"
|
||||
clearable
|
||||
type="password"
|
||||
show-password
|
||||
@change="submit"
|
||||
name="password"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="isLoading" @click="submit">
|
||||
登录
|
||||
</el-button>
|
||||
<el-button @click="goSignup">没有账号,立即注册</el-button>
|
||||
</el-form-item>
|
||||
<el-alert v-if="msg" :title="msg" show-icon type="error" />
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
@change="submit"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-alert v-if="msg" type="error" :show-icon="false"> {{ msg }}</n-alert>
|
||||
<n-form-item>
|
||||
<n-space>
|
||||
<n-button type="primary" :loading="isLoading" @click="submit">
|
||||
登录
|
||||
</n-button>
|
||||
<n-button @click="goSignup">没有账号,立即注册</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { isDesktop } from "~/shared/composables/breakpoints"
|
||||
|
||||
interface Props {
|
||||
total: number
|
||||
limit: number
|
||||
@@ -21,16 +22,15 @@ watch(page, () => emit("update:page", page))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-pagination
|
||||
<n-pagination
|
||||
v-if="props.total"
|
||||
class="right margin"
|
||||
:layout="isDesktop ? 'prev,pager,next,sizes' : 'prev,next,sizes'"
|
||||
background
|
||||
:total="props.total"
|
||||
:page-sizes="[10, 20, 30]"
|
||||
:pager-count="5"
|
||||
:item-count="props.total"
|
||||
v-model:page="page"
|
||||
v-model:page-size="limit"
|
||||
v-model:current-page="page"
|
||||
:page-sizes="[10, 20, 30]"
|
||||
:page-slot="isDesktop ? 7 : 5"
|
||||
show-size-picker
|
||||
/>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
@@ -1,15 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { signupModal } from "../composables/modal"
|
||||
import type { FormRules } from "naive-ui"
|
||||
import { signupModal, toggleLogin, toggleSignup } from "../composables/modal"
|
||||
|
||||
const form = reactive({
|
||||
username: "",
|
||||
password: "",
|
||||
passwordAgain: "",
|
||||
email: "",
|
||||
})
|
||||
const rules: FormRules = {}
|
||||
|
||||
const [isLoading] = useToggle()
|
||||
const msg = ref("")
|
||||
|
||||
function goLogin() {
|
||||
toggleLogin(true)
|
||||
toggleSignup(false)
|
||||
}
|
||||
|
||||
function submit() {}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
v-model="signupModal"
|
||||
<n-modal
|
||||
:mask-closable="false"
|
||||
v-model:show="signupModal"
|
||||
preset="card"
|
||||
title="注册"
|
||||
style="width: 400px"
|
||||
:auto-focus="false"
|
||||
>
|
||||
</el-dialog>
|
||||
<n-form ref="signupRef" :model="form" :rules="rules" show-require-mark>
|
||||
<n-form-item label="用户名" path="username">
|
||||
<n-input
|
||||
v-model:value="form.username"
|
||||
autofocus
|
||||
clearable
|
||||
name="username"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="邮箱" path="email">
|
||||
<n-input
|
||||
v-model:value="form.email"
|
||||
clearable
|
||||
name="email"
|
||||
@change="submit"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="密码" path="password">
|
||||
<n-input
|
||||
v-model:value="form.password"
|
||||
clearable
|
||||
type="password"
|
||||
name="password"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="确认密码" path="password">
|
||||
<n-input
|
||||
v-model:value="form.passwordAgain"
|
||||
clearable
|
||||
type="password"
|
||||
name="passwordAgain"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-alert v-if="msg" type="error" :show-icon="false"> {{ msg }}</n-alert>
|
||||
<n-form-item>
|
||||
<n-space>
|
||||
<n-button type="primary" :loading="isLoading" @click="submit">
|
||||
登录
|
||||
</n-button>
|
||||
<n-button @click="goLogin">已经注册?现在登录</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
|
||||
interface Props {
|
||||
split: "horizontal" | "vertical"
|
||||
className?: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
split: "horizontal",
|
||||
className: "",
|
||||
})
|
||||
|
||||
const classes = computed(() => [props.split, props.className].join(" "))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.splitter-pane.vertical.splitter-paneL {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
padding-right: 3px;
|
||||
}
|
||||
.splitter-pane.vertical.splitter-paneR {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
height: 100%;
|
||||
padding-left: 3px;
|
||||
}
|
||||
.splitter-pane.horizontal.splitter-paneL {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
.splitter-pane.horizontal.splitter-paneR {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
padding-top: 3px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,47 +0,0 @@
|
||||
<template>
|
||||
<div :class="classes"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
|
||||
interface Props {
|
||||
split: "horizontal" | "vertical"
|
||||
className?: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
split: "horizontal",
|
||||
className: "",
|
||||
})
|
||||
|
||||
const classes = computed(() =>
|
||||
["splitter-pane-resizer", props.split, props.className].join(" ")
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.splitter-pane-resizer {
|
||||
box-sizing: border-box;
|
||||
background: #000;
|
||||
position: absolute;
|
||||
opacity: 0.2;
|
||||
z-index: 1;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
.splitter-pane-resizer.horizontal {
|
||||
height: 11px;
|
||||
margin: -5px 0;
|
||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: row-resize;
|
||||
width: 100%;
|
||||
}
|
||||
.splitter-pane-resizer.vertical {
|
||||
width: 11px;
|
||||
height: 100%;
|
||||
margin-left: -5px;
|
||||
border-left: 5px solid rgba(255, 255, 255, 0);
|
||||
border-right: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: col-resize;
|
||||
}
|
||||
</style>
|
||||
@@ -1,141 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
:style="{ cursor, userSelect }"
|
||||
class="vue-splitter-container clearfix"
|
||||
@mouseup="onMouseUp"
|
||||
@mousemove="onMouseMove"
|
||||
>
|
||||
<Pane
|
||||
class="splitter-pane splitter-paneL"
|
||||
:split="split"
|
||||
:style="{ [type]: percent + '%' }"
|
||||
>
|
||||
<slot name="panel"></slot>
|
||||
</Pane>
|
||||
|
||||
<Resizer
|
||||
:className="className"
|
||||
:style="{ [resizeType]: percent + '%' }"
|
||||
:split="split"
|
||||
@mousedown.native="onMouseDown"
|
||||
@click.native="onClick"
|
||||
></Resizer>
|
||||
|
||||
<Pane
|
||||
class="splitter-pane splitter-paneR"
|
||||
:split="split"
|
||||
:style="{ [type]: 100 - percent + '%' }"
|
||||
>
|
||||
<slot name="paner"></slot>
|
||||
</Pane>
|
||||
<div class="vue-splitter-container-mask" v-if="active"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Resizer from "./Resizer.vue"
|
||||
import Pane from "./Pane.vue"
|
||||
import { computed, ref } from "vue"
|
||||
|
||||
interface Props {
|
||||
minPercent?: number
|
||||
defaultPercent?: number
|
||||
split: "vertical" | "horizontal"
|
||||
className?: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
minPercent: 10,
|
||||
defaultPercent: 50,
|
||||
split: "horizontal",
|
||||
className: "",
|
||||
})
|
||||
|
||||
const emit = defineEmits(["resize"])
|
||||
|
||||
const active = ref(false)
|
||||
const hasMoved = ref(false)
|
||||
const percent = ref(props.defaultPercent)
|
||||
const type = ref(props.split === "vertical" ? "width" : "height")
|
||||
const resizeType = ref(props.split === "vertical" ? "left" : "top")
|
||||
|
||||
const userSelect = computed(() => (active.value ? "none" : "auto"))
|
||||
const cursor = computed(() =>
|
||||
active.value ? (props.split === "vertical" ? "col-resize" : "row-resize") : ""
|
||||
)
|
||||
|
||||
// watch(
|
||||
// () => defaultPercent,
|
||||
// (newValue) => {
|
||||
// percent.value = newValue
|
||||
// }
|
||||
// )
|
||||
|
||||
function onClick() {
|
||||
if (!hasMoved.value) {
|
||||
percent.value = 50
|
||||
emit("resize", percent.value)
|
||||
}
|
||||
}
|
||||
function onMouseDown() {
|
||||
active.value = true
|
||||
hasMoved.value = false
|
||||
}
|
||||
function onMouseUp() {
|
||||
active.value = false
|
||||
}
|
||||
function onMouseMove(e: any) {
|
||||
if (e.buttons === 0) {
|
||||
active.value = false
|
||||
}
|
||||
if (active.value) {
|
||||
let offset = 0
|
||||
let target = e.currentTarget
|
||||
if (props.split === "vertical") {
|
||||
while (target) {
|
||||
offset += target.offsetLeft
|
||||
target = target.offsetParent
|
||||
}
|
||||
} else {
|
||||
while (target) {
|
||||
offset += target.offsetTop
|
||||
target = target.offsetParent
|
||||
}
|
||||
}
|
||||
const currentPage = props.split === "vertical" ? e.pageX : e.pageY
|
||||
const targetOffset =
|
||||
props.split === "vertical"
|
||||
? e.currentTarget.offsetWidth
|
||||
: e.currentTarget.offsetHeight
|
||||
const newPercent =
|
||||
Math.floor(((currentPage - offset) / targetOffset) * 10000) / 100
|
||||
if (newPercent > props.minPercent && newPercent < 100 - props.minPercent) {
|
||||
percent.value = newPercent
|
||||
}
|
||||
emit("resize", newPercent)
|
||||
hasMoved.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.clearfix:after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
content: " ";
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
.vue-splitter-container {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.vue-splitter-container-mask {
|
||||
z-index: 9999;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,2 +1,2 @@
|
||||
export const isDark = useDark({ storageKey: "theme-appearance" })
|
||||
export const isDark = useLocalStorage("theme-appearance", false)
|
||||
export const toggleDark = useToggle(isDark)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<el-container>
|
||||
<el-main>
|
||||
<n-layout>
|
||||
<n-layout-content bordered>
|
||||
<router-view></router-view>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -5,20 +5,24 @@ import Header from "../Header/index.vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container>
|
||||
<el-header class="header">
|
||||
<n-layout>
|
||||
<n-layout-header bordered class="header">
|
||||
<Header />
|
||||
</el-header>
|
||||
<el-main>
|
||||
</n-layout-header>
|
||||
<n-layout-content class="content">
|
||||
<router-view></router-view>
|
||||
</el-main>
|
||||
</n-layout-content>
|
||||
<Login />
|
||||
<Signup />
|
||||
</el-container>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
:style="{ cursor, userSelect }"
|
||||
class="vue-splitter-container clearfix"
|
||||
@mouseup="onMouseUp"
|
||||
@mousemove="onMouseMove"
|
||||
>
|
||||
<Pane
|
||||
class="splitter-pane splitter-paneL"
|
||||
:split="split"
|
||||
:style="{ [type]: percent + '%' }"
|
||||
>
|
||||
<slot name="panel"></slot>
|
||||
</Pane>
|
||||
|
||||
<Resizer
|
||||
:className="className"
|
||||
:style="{ [resizeType]: percent + '%' }"
|
||||
:split="split"
|
||||
@mousedown.native="onMouseDown"
|
||||
@click.native="onClick"
|
||||
></Resizer>
|
||||
|
||||
<Pane
|
||||
class="splitter-pane splitter-paneR"
|
||||
:split="split"
|
||||
:style="{ [type]: 100 - percent + '%' }"
|
||||
>
|
||||
<slot name="paner"></slot>
|
||||
</Pane>
|
||||
<div class="vue-splitter-container-mask" v-if="active"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Resizer from "./Resizer.vue"
|
||||
import Pane from "./Pane.vue"
|
||||
import { computed, ref } from "vue"
|
||||
|
||||
interface Props {
|
||||
minPercent?: number
|
||||
defaultPercent?: number
|
||||
split: "vertical" | "horizontal"
|
||||
className?: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
minPercent: 10,
|
||||
defaultPercent: 50,
|
||||
split: "horizontal",
|
||||
className: "",
|
||||
})
|
||||
|
||||
const emit = defineEmits(["resize"])
|
||||
|
||||
const active = ref(false)
|
||||
const hasMoved = ref(false)
|
||||
const percent = ref(props.defaultPercent)
|
||||
const type = ref(props.split === "vertical" ? "width" : "height")
|
||||
const resizeType = ref(props.split === "vertical" ? "left" : "top")
|
||||
|
||||
const userSelect = computed(() => (active.value ? "none" : "auto"))
|
||||
const cursor = computed(() =>
|
||||
active.value ? (props.split === "vertical" ? "col-resize" : "row-resize") : ""
|
||||
)
|
||||
|
||||
// watch(
|
||||
// () => defaultPercent,
|
||||
// (newValue) => {
|
||||
// percent.value = newValue
|
||||
// }
|
||||
// )
|
||||
|
||||
function onClick() {
|
||||
if (!hasMoved.value) {
|
||||
percent.value = 50
|
||||
emit("resize", percent.value)
|
||||
}
|
||||
}
|
||||
function onMouseDown() {
|
||||
active.value = true
|
||||
hasMoved.value = false
|
||||
}
|
||||
function onMouseUp() {
|
||||
active.value = false
|
||||
}
|
||||
function onMouseMove(e: any) {
|
||||
if (e.buttons === 0) {
|
||||
active.value = false
|
||||
}
|
||||
if (active.value) {
|
||||
let offset = 0
|
||||
let target = e.currentTarget
|
||||
if (props.split === "vertical") {
|
||||
while (target) {
|
||||
offset += target.offsetLeft
|
||||
target = target.offsetParent
|
||||
}
|
||||
} else {
|
||||
while (target) {
|
||||
offset += target.offsetTop
|
||||
target = target.offsetParent
|
||||
}
|
||||
}
|
||||
const currentPage = props.split === "vertical" ? e.pageX : e.pageY
|
||||
const targetOffset =
|
||||
props.split === "vertical"
|
||||
? e.currentTarget.offsetWidth
|
||||
: e.currentTarget.offsetHeight
|
||||
const newPercent =
|
||||
Math.floor(((currentPage - offset) / targetOffset) * 10000) / 100
|
||||
if (newPercent > props.minPercent && newPercent < 100 - props.minPercent) {
|
||||
percent.value = newPercent
|
||||
}
|
||||
emit("resize", newPercent)
|
||||
hasMoved.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.clearfix:after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
content: " ";
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
.vue-splitter-container {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.vue-splitter-container-mask {
|
||||
z-index: 9999;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user