naive-ui.

This commit is contained in:
2023-01-17 10:42:50 +08:00
parent 5d0f34c144
commit 01760b8eaa
29 changed files with 4864 additions and 2263 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,2 +1,2 @@
export const isDark = useDark({ storageKey: "theme-appearance" })
export const isDark = useLocalStorage("theme-appearance", false)
export const toggleDark = useToggle(isDark)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>