Merge pull request #2 from LingBrian/master

Master
This commit is contained in:
BrianLing 2024-07-14 15:54:39 +08:00 committed by GitHub
commit 02c7bc9b11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 1661 additions and 145 deletions

View File

@ -0,0 +1,51 @@
<template>
<client-only>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="AppID" width="180" />
<el-table-column prop="applicant" label="申请人" width="180" />
<el-table-column prop="area" label="地区" width="180" />
<el-table-column label="配置" width="300">
<template #default="scope">
<el-icon color="blue"><ElIcon-Cpu /></el-icon>
{{ scope.row.cpu }} Core
<el-icon color="green"><ElIcon-Stopwatch /></el-icon
>{{ scope.row.ram }} GB
<el-icon color="gray"><ElIcon-Memo /></el-icon>
{{ scope.row.disk }} GB
</template>
</el-table-column>
<el-table-column label="状态" width="300">
<template #default="scope">
<el-icon :color="scope.row.deploy ? 'green' : 'orange'"
><ElIcon-Stamp
/></el-icon>
{{ scope.row.deploy ? "通过" : "待审核" }}
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<AdminApplicationShow :desc="scope.row.desc" :name="scope.row.name" />
<el-button
size="small"
type="success"
@click="ElMessage({ message: '同意', type: 'success' })"
>
同意
</el-button>
<el-button
size="small"
type="danger"
@click="ElMessage({ message: '拒绝', type: 'warning' })"
>
拒绝
</el-button>
</template>
</el-table-column>
</el-table>
</client-only>
</template>
<script lang="ts" setup>
import type { Application } from "~/types/Application";
const data: Application[] = await $fetch("/api/admin/applications");
const tableData = ref(data);
</script>

View File

@ -0,0 +1,22 @@
<template>
<el-button plain @click="open">查看</el-button>
</template>
<script setup lang="ts">
import type { Action } from "element-plus";
const props = defineProps(["desc", "name"]);
const open = () => {
ElMessageBox.alert(props.desc, props.name, {
// if you want to disable its autofocus
// autofocus: false,
dangerouslyUseHTMLString: true,
confirmButtonText: "OK",
callback: (action: Action) => {
/* ElMessage({
type: "info",
message: `action: ${action}`,
}); */
},
});
};
</script>

View File

@ -1,24 +1,20 @@
<template>
<client-only>
<client-only>
<el-menu
:default-active="activeIndex"
mode="horizontal"
:ellipsis="false"
@select="handleSelect"
:default-active="activeIndex"
mode="horizontal"
:ellipsis="false"
@select="handleSelect"
>
<el-menu-item index="/">
<img
style="width: 50px;"
src="/logo.svg"
alt="Element logo"
/>
</el-menu-item>
<el-menu-item index="/apply">立即申请</el-menu-item>
<el-menu-item index="/about">关于我们</el-menu-item>
<el-menu-item index="/status" disabled>设备监控</el-menu-item>
<el-sub-menu index="/area" disabled>
<el-menu-item index="/">
<img style="width: 50px" src="/logo.svg" alt="Element logo" />
</el-menu-item>
<el-menu-item index="/apply">立即申请</el-menu-item>
<el-menu-item index="/about" disabled>关于我们</el-menu-item>
<el-menu-item index="/status" disabled>设备监控</el-menu-item>
<el-sub-menu index="/area" disabled>
<template #title>区域服务</template>
<!-- <el-menu-item index="/area/ln1">辽宁一区</el-menu-item>
<!-- <el-menu-item index="/area/ln1">辽宁一区</el-menu-item>
<el-menu-item index="/area/ln2">辽宁二区</el-menu-item>
<el-menu-item index="/area/jx1">江西一区</el-menu-item>
<el-sub-menu index="/area/jx2">
@ -27,27 +23,31 @@
<el-menu-item index="/area/jx2/web">网页服务器</el-menu-item>
<el-menu-item index="/area/jx2/game">游戏服务器</el-menu-item>
</el-sub-menu> -->
</el-sub-menu>
<el-menu-item index="/forum" disabled>论坛</el-menu-item>
<el-menu-item index="/host" disabled>托管</el-menu-item>
<div class="flex-grow" />
<el-menu-item index="/login">登录</el-menu-item>
<el-menu-item index="register">注册</el-menu-item>
</el-sub-menu>
<el-menu-item index="forum" disabled>论坛</el-menu-item>
<el-menu-item index="host" disabled>托管</el-menu-item>
<div class="flex-grow" />
<el-menu-item v-if="auth" index="/user/login">登录</el-menu-item>
<el-menu-item v-if="auth" index="/user/register">注册</el-menu-item>
<el-menu-item v-if="!auth" index="/admin">管理界面</el-menu-item>
<el-menu-item v-if="!auth" @click="logoutNow">注销</el-menu-item>
</el-menu>
</client-only>
</client-only>
</template>
<script lang="ts" setup>
const activeIndex = ref('1')
const activeIndex = ref("1");
const handleSelect = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
navigateTo(key)
navigateTo(key);
};
const auth = ref(useCookie("auth").value == undefined);
function logoutNow() {
logout();
auth.value = true;
}
</script>
<style>
.flex-grow {
flex-grow: 1;
}
</style>
</style>

View File

@ -0,0 +1,115 @@
<template>
<div class="silder-range" :class="rangeStatus ? 'success' : ''">
<i
@mousedown="rangeMove"
:class="rangeStatus ? successIcon : startIcon"
></i>
{{ rangeStatus ? successText : startText }}
</div>
</template>
<script>
export default {
props: {
//
successIcon: {
type: String,
default: "el-icon-success",
},
//
successText: {
type: String,
default: "验证成功",
},
//
startIcon: {
type: String,
default: "el-icon-d-arrow-right",
},
//
startText: {
type: String,
default: "拖动滑块到最右侧",
},
},
name: "SilderVerify",
data() {
return {
rangeStatus: "",
};
},
methods: {
rangeMove(e) {
let ele = e.target;
let startX = e.clientX;
let eleWidth = ele.offsetWidth;
let parentWidth = ele.parentElement.offsetWidth;
let MaxX = parentWidth - eleWidth;
if (this.rangeStatus) {
//
return false;
}
document.onmousemove = (e) => {
let endX = e.clientX;
this.disX = endX - startX;
if (this.disX <= 0) {
this.disX = 0;
}
if (this.disX >= MaxX - eleWidth) {
//,
this.disX = MaxX;
}
ele.style.transition = ".1s all";
ele.style.transform = "translateX(" + this.disX + "px)";
e.preventDefault();
};
document.onmouseup = () => {
if (this.disX !== MaxX) {
ele.style.transition = ".5s all";
ele.style.transform = "translateX(0)";
this.$emit("failed", this.rangeStatus);
} else {
this.rangeStatus = true;
this.$emit("success", this.rangeStatus);
}
document.onmousemove = null;
document.onmouseup = null;
};
},
},
};
</script>
<style scoped>
.silder-range {
background-color: #e3e4e6;
position: relative;
transition: 1s all;
user-select: none;
color: #333;
display: flex;
justify-content: center;
align-items: center;
height: 45px; /*no*/
}
.silder-range i {
position: absolute;
left: 0;
width: 60px; /*no*/
height: 100%;
color: #919191;
background-color: #fff;
border: 1px solid #bbb;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
.silder-range.success {
background-color: #7ac23c;
color: #fff;
}
.silder-range.success i {
color: #7ac23c;
}
</style>

14
composables/logout.ts Normal file
View File

@ -0,0 +1,14 @@
// 它将作为 useFoo() 可用(文件名的驼峰形式,不包括扩展名)
export default function () {
const auth = useCookie("auth");
$fetch("/api/user/logout", {
method: "POST",
body: {
auth: auth.value,
},
});
auth.value = undefined;
navigateTo("/");
return 1;
}

11
composables/user.ts Normal file
View File

@ -0,0 +1,11 @@
export async function getUserId() {
const auth = useCookie("auth");
return await $fetch("/api/user/auth", {
method: "GET",
query: {
auth: auth.value,
},
}).then((res: number) => {
return res;
});
}

6
middleware/admin.ts Normal file
View File

@ -0,0 +1,6 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
if ((await getUserId()) != 1) {
ElMessage("禁止访问");
return navigateTo("/", { replace: true });
}
});

27
middleware/auth.ts Normal file
View File

@ -0,0 +1,27 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
const auth = useCookie("auth");
if (auth.value === undefined) {
ElMessage("未登录或cookie未开启");
return navigateTo("/user/login", { replace: true });
} else {
const result = await $fetch("/api/user/auth", {
method: "post",
body: {
auth: auth.value,
},
});
if (!result.login && to.path !== "/user/test") {
if (result.code == 0) {
ElMessage("未登录");
} else if (result.code == 2) {
ElMessage("登录超时,请重新登录");
auth.value = undefined;
} else {
ElMessage("error" + result.code);
}
return navigateTo("/user/login");
} else {
//console.log(auth.value);
}
}
});

7
middleware/unauth.ts Normal file
View File

@ -0,0 +1,7 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
const auth = useCookie("auth");
if (auth.value === undefined) {
} else {
return navigateTo("/");
}
});

View File

@ -1,8 +1,11 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
modules: [
"@element-plus/nuxt",
'@vueform/nuxt'
],
})
runtimeConfig: {
apiSecret: "",
apiOpenid: "",
apiApikey: "",
apiSmsId: 0,
},
modules: ["@element-plus/nuxt", "@vueform/nuxt"],
});

View File

@ -17,6 +17,7 @@
"element-plus": "^2.7.4",
"nuxt": "^3.11.2",
"prisma": "^5.15.0",
"quanmsms": "^1.0.2",
"typescript": "^5.4.5",
"vue": "^3.4.27",
"vue-router": "^4.3.2"

53
pages/admin/index.vue Normal file
View File

@ -0,0 +1,53 @@
<template>
<Head>
<Title>管理界面</Title>
<Meta name="description" />
</Head>
<client-only>
<el-container>
<el-aside width="64px">
<el-menu default-active="2" class="el-menu-vertical-demo" collapse>
<el-sub-menu index="1">
<template #title>
<el-icon><location /></el-icon>
<span>Navigator One</span>
</template>
<el-menu-item-group>
<template #title><span>Group One</span></template>
<el-menu-item index="1-1">item one</el-menu-item>
<el-menu-item index="1-2">item two</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group Two">
<el-menu-item index="1-3">item three</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title><span>item four</span></template>
<el-menu-item index="1-4-1">item one</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="2">
<el-icon><Stamp /></el-icon>
<template #title>申请审批</template>
</el-menu-item>
<el-menu-item index="3" @click="navigateTo('/admin/loginlogs')">
<el-icon><TakeawayBox /></el-icon>
<template #title>日志</template>
</el-menu-item>
<el-menu-item index="4">
<el-icon><setting /></el-icon>
<template #title>Navigator Four</template>
</el-menu-item>
</el-menu>
</el-aside>
<el-main>
<div class="area">
<AdminApplicationDataTable />
</div>
</el-main>
</el-container>
</client-only>
</template>
<script lang="ts" setup>
import { Location, Setting, Stamp, TakeawayBox } from "@element-plus/icons-vue";
</script>
<style></style>

View File

@ -0,0 +1,23 @@
<template>
<Head>
<Title>登录日志</Title>
<Meta name="description" />
</Head>
<client-only>
<el-table
:default-sort="{ prop: 'id', order: 'descending' }"
:data="tableData"
style="width: 100%"
>
<el-table-column prop="id" label="id" width="50" />
<el-table-column prop="username" label="用户" width="180" />
<el-table-column prop="date" label="登录时间" width="180" />
<el-table-column prop="ip" label="登录ip" width="180" />
</el-table>
</client-only>
</template>
<script lang="ts" setup>
import type { LoginLog } from "~/types/Log";
const data: LoginLog[] = await $fetch("/api/admin/loginlogs");
const tableData = ref(data);
</script>

View File

@ -1,70 +1,143 @@
<template>
<!--立即申请页面-->
<Head>
<Title>立即申请</Title>
<Meta name="description"/>
</Head>
<div style="width:50%;margin:auto;" :style="{
boxShadow: `var(--el-box-shadow-dark)`,
}">
<ClientOnly >
<Vueform view="tabs" style="padding:20px" >
<TextElement name="username" label="昵称" placeholder="昵称" :columns="{ container: 4, label: 3, wrapper: 12 }"/>
<PhoneElement name="phone" allow-incomplete unmask default="+86" :include="['cn']" label="手机号" placeholder="+86" :columns="{ container: 12, label: 1, wrapper: 3 }" />
<TextElement name="code" label="验证码" placeholder="xxxxxx" :columns="{ container: 3, label: 4, wrapper: 12 }" />
<ButtonElement name="button" :columns="{ container: 2, label: 3, wrapper: 12 }" button-label="发送验证码"/>
<SelectElement
name="select"
default="辽宁一区"
label="地区"
:native="false"
:items="[
'辽宁一区',
'辽宁二区',
'江西一区',
]"
:columns="{ container: 4, label: 3, wrapper: 12 }"
/>
<RadiogroupElement
default="2 Core"
label="CPU核心数"
name="cpu"
:items="['2 Core', '4 Core', '6 Core','More']"
view="tabs"
/>
<RadiogroupElement
default="2 GB"
<!--立即申请页面-->
<Head>
<Title>立即申请</Title>
<Meta name="description" />
</Head>
<div
style="width: 50%; margin: auto"
:style="{
boxShadow: `var(--el-box-shadow-light)`,
}"
>
<ClientOnly>
<Vueform
@success="AppSuccess"
endpoint="/api/test"
method="POST"
view="tabs"
style="padding: 30px"
>
<StaticElement
tag="h4"
align="center"
content="项目详情"
name="static"
/>
<TextElement
name="name"
label="项目名称"
placeholder="项目名称"
:columns="{ container: 4, label: 4, wrapper: 12 }"
:rules="['required', isCreate]"
/>
<SelectElement
name="area"
default="1"
label="地区"
:native="false"
:items="{ 1: '辽宁一区', 2: '辽宁二区', 3: '江西一区' }"
:columns="{ container: 4, label: 3, wrapper: 12 }"
rules="required"
/>
<RadiogroupElement
default="2"
label="CPU核心数"
name="cpu"
:items="{ 2: '2 Core', 4: '4 Core', 6: '6 Core', 10: 'More' }"
view="tabs"
/>
<RadiogroupElement
default="2"
label="RAM容量"
name="ram"
:items="['2 GB', '4 GB', '6 GB','More']"
:items="{ 2: '2 GB', 4: '4 GB', 6: '6 GB', 10: 'More' }"
view="tabs"
/>
<SliderElement
name="hhd"
label="磁盘容量"
:default="5"
:min="1"
:max="40"
:format="(v: number) => v > 1 ? `${Math.round(v)} GB` : '1 GB'"
:add-classes="{
ElementLayout: {
innerWrapper: 'mt-12'
}
}"
/>
<EditorElement
name="usage"
label="用途说明"
rules="required|max:500"
sync
name="disk"
label="磁盘容量"
:default="5"
:min="1"
:max="40"
:format="(v: number) => v > 1 ? `${Math.round(v)} GB` : '1 GB'"
:columns="{ container: 12, label: 12, wrapper: 12 }"
:add-classes="{
ElementLayout: {
innerWrapper: 'mt-12',
},
}"
/>
<ButtonElement name="submit" button-label="提交申请" align="center" size="lg" submits/>
</Vueform>
</ClientOnly>
</div>
<EditorElement
name="desc"
label="用途说明"
rules="required|max:500"
:hide-tools="['attach']"
/>
<StaticElement
label="人机验证"
name="static"
:columns="{ container: 12, label: 12, wrapper: 12 }"
>
<DefaultSilderVerify
@success="sliderHandleSuccess"
@failed="sliderHandleError"
/>
</StaticElement>
<HiddenElement :default="uid" name="uid" />
<HiddenElement :default="auth" name="auth" />
<ButtonElement
:disabled="isBot"
name="submit"
button-label="提交申请"
align="center"
size="lg"
submits
/>
</Vueform>
</ClientOnly>
</div>
</template>
<script setup lang="ts">
import PhoneElemen from '@vueform/vueform'
</script>
import { Validator } from "@vueform/vueform";
definePageMeta({
middleware: ["auth"],
});
const uid: number = await getUserId();
const isBot = ref(true);
const auth = useCookie("auth");
function sliderHandleSuccess() {
ElMessage({ message: "人机验证已通过", type: "success" });
isBot.value = false;
}
function sliderHandleError() {
ElMessage({ message: "人机验证未通过", type: "warning" });
}
const isCreate = class extends Validator {
get msg() {
return "项目名已存在";
}
check(value: string) {
if (value == "") {
return 0;
}
return $fetch("/api/application", {
method: "POST",
body: { name: value },
}).then((res) => {
return res.name;
});
}
};
const form = ref();
function AppSuccess(response: { data: { code: number; msg: string } }) {
if (response.data.code) {
ElMessage({ message: response.data.msg, type: "success" });
navigateTo("/");
} else {
ElMessage({ message: response.data.msg, type: "warning" });
}
}
</script>

View File

@ -1,14 +1,15 @@
<template slot-scope><!--主页面-->
<Head>
<Title>FreePotato Server</Title>
<Meta name="description" content="免费服务器~"/>
</Head>
<ElRow :gutter="10" align="middle" style="height: 900px;">
<ElCol :span="24"><IndexNewsCarouel/></ElCol>
<ElCol :span="24"><IndexNewsStatus/></ElCol>
</ElRow>
<template slot-scope>
<!--主页面-->
<Head>
<Title>FreePotato Server</Title>
<Meta name="description" content="免费服务器~" />
</Head>
<ElRow :gutter="10" align="middle" style="height: 900px">
<ElCol :span="24"><IndexNewsCarouel /></ElCol>
<ElCol :span="24"><IndexNewsStatus /></ElCol>
</ElRow>
</template>
<script setup lang="ts">
</script>
const a = await getUserId();
console.info(a);
</script>

View File

@ -1,15 +0,0 @@
<template>
<!--登录界面-->
<Head>
<Title>登录界面</Title>
<Meta name="description"/>
</Head>
<div>
<el-text class="mx-1" type="primary" >
Primary
</el-text>
</div>
</template>
<script setup lang="ts">
</script>

View File

@ -1,15 +0,0 @@
<template>
<!--注册页面-->
<Head>
<Title>注册页面</Title>
<Meta name="description"/>
</Head>
<div>
<el-text class="mx-1" type="primary" >
Primary
</el-text>
</div>
</template>
<script setup lang="ts">
</script>

63
pages/user/index.vue Normal file
View File

@ -0,0 +1,63 @@
<template>
<Head>
<Title>个人界面</Title>
<Meta name="description" />
</Head>
<client-only>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="AppID" width="180" />
<el-table-column prop="applicant" label="申请人" width="180" />
<el-table-column prop="area" label="地区" width="180" />
<el-table-column label="配置" width="300">
<template #default="scope">
<el-icon color="blue"><ElIcon-Cpu /></el-icon>
{{ scope.row.cpu }} Core
<el-icon color="green"><ElIcon-Stopwatch /></el-icon
>{{ scope.row.ram }} GB
<el-icon color="gray"><ElIcon-Memo /></el-icon>
{{ scope.row.disk }} GB
</template>
</el-table-column>
<el-table-column prop="desc" label="用途" width="360" />
<el-table-column label="状态" width="300">
<template #default="scope">
<el-icon :color="scope.row.deploy ? 'green' : 'orange'"
><ElIcon-Stamp
/></el-icon>
{{ scope.row.deploy ? "通过" : "待审核" }}
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button
size="small"
type="success"
@click="ElMessage({ message: '查看', type: 'success' })"
>
查看
</el-button>
<el-button
size="small"
type="danger"
@click="ElMessage({ message: '取消', type: 'warning' })"
>
取消
</el-button>
</template>
</el-table-column>
</el-table>
</client-only>
</template>
<script lang="ts" setup>
import type { Application } from "~/types/Application";
definePageMeta({
middleware: ["auth"],
});
const data: Application[] = await $fetch("/api/user/application", {
method: "POST",
body: {
uid: await getUserId(),
},
});
const tableData = ref(data);
</script>

111
pages/user/login/index.vue Normal file
View File

@ -0,0 +1,111 @@
<template>
<!--登录界面-->
<Head>
<Title>登录界面</Title>
<Meta name="description" />
</Head>
<div
class="formbox"
:style="{
boxShadow: `var(--el-box-shadow-light)`,
}"
>
<ClientOnly>
<Vueform
endpoint="/api/user/login"
method="POST"
view="tabs"
style="padding: 30px"
@success="PostSuccess"
>
<StaticElement tag="h4" align="center" content="登录" name="static" />
<TextElement
ref="phone$"
name="phone"
allow-incomplete
label="手机号"
placeholder="+86"
:columns="{ container: 12, label: 3, wrapper: 10 }"
rules="required|min:11|max:11"
/>
<TextElement
input-type="password"
name="password"
allow-incomplete
label="密码"
rules="required|min:8|max:16"
:columns="{ container: 12, label: 3, wrapper: 10 }"
/>
<HiddenElement :default="ip" name="client_ip" />
<CheckboxElement
name="policy"
label="用户协议"
rules="required"
message="您必须同意用户政策才能继续"
>
已阅读并同意<ElLink href="/" target="_blank">用户协议</ElLink>
</CheckboxElement>
<ButtonElement
name="submit"
button-label="登录"
align="center"
size="lg"
submits
/>
</Vueform>
</ClientOnly>
</div>
</template>
<script setup lang="ts">
import PhoneElemen from "@vueform/vueform";
definePageMeta({
middleware: ["unauth"],
});
const form = ref();
const data: { query: string } = await $fetch("http://ip-api.com/json");
const ip = data.query;
function PostSuccess(response: {
data: {
code: number;
msg: string;
token: string;
};
}) {
if (response.data.code == 1) {
ElMessage({ message: response.data.msg, type: "success" });
const auth = useCookie("auth", { maxAge: 60 * 30 });
auth.value = response.data.token;
location.reload();
navigateTo("/", { replace: true });
} else if (response.data.code == -1) {
ElMessage({ message: response.data.msg, type: "warning" });
navigateTo("/user/register");
} else {
ElMessage({ message: response.data.msg, type: "warning" });
}
}
</script>
<style>
.formbox {
width: 25%;
margin: auto;
}
@media screen and (min-width: 901px) and (max-width: 1200px) {
.formbox {
width: 50%;
margin: auto;
}
}
@media screen and (min-width: 601px) and (max-width: 900px) {
.formbox {
width: 75%;
margin: auto;
}
}
@media screen and (max-width: 600px) {
.formbox {
width: 88%;
margin: auto;
}
}
</style>

9
pages/user/logout.ts Normal file
View File

@ -0,0 +1,9 @@
const auth = useCookie("auth");
$fetch("/api/user/logout", {
method: "POST",
body: {
auth: auth.value,
},
});
auth.value = undefined;
navigateTo("/");

View File

@ -0,0 +1,255 @@
<template slot-scope>
<!--注册页面-->
<Head>
<Title>注册页面</Title>
<Meta name="description" />
</Head>
<div
class="formbox"
:style="{
boxShadow: `var(--el-box-shadow-light)`,
}"
>
<ClientOnly>
<Vueform
endpoint="/api/user/create"
method="POST"
view="tabs"
:model-value="form"
@update:model-value="form = $event"
@success="RegSuccess"
validate-on="change"
style="padding: 5%"
>
<StaticElement tag="h4" align="center" content="注册" name="static" />
<TextElement
ref="username$"
name="username"
allow-incomplete
label="昵称"
placeholder="昵称"
:columns="{ container: 12, label: 3, wrapper: 8 }"
rules="required|min:3|max:64"
@input="Check()"
/>
<TextElement
ref="phone$"
name="phone"
allow-incomplete
label="手机号"
placeholder="+86"
:columns="{ container: 12, label: 3, wrapper: 10 }"
rules="required|min:11|max:11"
@input="Check()"
:disabled="phoneChecked"
/>
<StaticElement
name="static"
:columns="{ container: 12, label: 3, wrapper: 12 }"
>
<DefaultSilderVerify
@success="sliderHandleSuccess"
@failed="sliderHandleError"
/>
</StaticElement>
<TextElement
name="code"
label="验证码"
placeholder="xxxxxx"
rules="required|max:6|min:6"
:columns="{ container: 8, label: 5, wrapper: 12 }"
@blur="CheckCode()"
:disabled="!codeBtnRef.disabled || phoneChecked"
ref="code$"
/>
<ButtonElement
name="button"
:columns="{ container: 4, label: 4, wrapper: 12 }"
@click="Send()"
:button-label="codeBtnRef.text"
:disabled="codeBtnRef.disabled || phoneChecked"
/>
<TextElement
input-type="password"
name="password"
label="密码"
rules="required|min:8|max:16|confirmed"
:columns="{ container: 12, label: 3, wrapper: 10 }"
/>
<TextElement
input-type="password"
name="password_confirmation"
label="确认密码"
rules="required|min:8|max:16"
:columns="{ container: 12, label: 3, wrapper: 10 }"
/>
<CheckboxElement
name="policy"
label="用户协议"
rules="required"
message="您必须同意用户政策才能继续"
>
已阅读并同意<ElLink href="/" target="_blank">用户协议</ElLink>
</CheckboxElement>
<ButtonElement
:disabled="canSubmit"
name="submit"
button-label="注册"
align="center"
size="lg"
submits
/><ButtonElement
name="reset"
danger
button-label="重置"
resets
size="sm"
/>
</Vueform>
</ClientOnly>
</div>
</template>
<script setup lang="ts">
import PhoneElemen from "@vueform/vueform";
definePageMeta({
middleware: ["unauth"],
});
const form = ref({});
const phoneChecked = ref(false);
const codeBtnRef = ref({ text: "发送验证码", color: "", disabled: false });
const username$ = ref(null);
const phone$ = ref(null);
const code$ = ref(null);
const canSubmit = ref(false);
let isBot = true;
function sliderHandleSuccess() {
ElMessage({ message: "人机验证已通过", type: "success" });
isBot = false;
}
function sliderHandleError() {
ElMessage({ message: "人机验证未通过", type: "warning" });
}
function Check() {
const query = {
username: form.value.username,
phone: form.value.phone || "",
};
if (!/^(?!1(4|7)\d{9})1[3-9]\d{9}$/.test(query.phone) && query.phone != "") {
username$.value.messageBag.clear("errors");
username$.value.messageBag.append("手机号格式错误" + query.phone);
canSubmit.value = true;
return;
} else {
username$.value.messageBag.clear("errors");
}
$fetch("/api/reg", {
method: "POST",
body: query,
}).then((res) => {
if (!res.phone) {
username$.value.messageBag.append("手机号已经注册");
canSubmit.value = true;
return 0;
} else {
username$.value.messageBag.clear("errors");
canSubmit.value = false;
}
if (!res.username) {
username$.value.messageBag.append("昵称已存在");
canSubmit.value = true;
} else {
username$.value.messageBag.clear("errors");
canSubmit.value = false;
}
});
}
function Send() {
if (isBot) {
ElMessage({ message: "人机验证未通过", type: "warning" });
return;
}
Check();
if (!canSubmit.value) {
$fetch("/api/reg/sms", {
method: "POST",
body: {
phone: form.value.phone,
},
}).then((res) => {
switch (res.code) {
case 0:
ElMessage({ message: res.msg + "\n" + res.error, type: "error" });
break;
case 1:
ElMessage({ message: res.msg, type: "success" });
codeBtnRef.value.disabled = true;
codeBtnRef.value.text = "请查收";
break;
case 2:
ElMessage({ message: res.msg, type: "warning" });
codeBtnRef.value.disabled = true;
codeBtnRef.value.text = "请查收";
break;
default:
break;
}
});
}
console.info(form.value.phone.replace("+86", ""));
}
function CheckCode() {
if (!code$.value.invalid) {
$fetch("/api/reg/code", {
method: "POST",
body: {
phone: form.value.phone.replace("+86", ""),
code: form.value.code,
},
}).then((res) => {
switch (res.code) {
case 0:
ElMessage({
message: res.msg,
type: "error",
});
break;
case 1:
ElMessage({ message: res.msg, type: "success" });
codeBtnRef.value.disabled = true;
codeBtnRef.value.text = "已验证";
phoneChecked.value = true;
break;
case 2:
ElMessage({ message: res.msg, type: "warning" });
codeBtnRef.value.disabled = false;
codeBtnRef.value.text = "发送验证码";
break;
default:
ElMessage({ message: res.msg, type: "warning" });
break;
}
});
}
}
function RegSuccess(response: { data: { code: number; msg: string } }, form$) {
if (response.data.code == 1) {
ElMessage({ message: response.data.msg, type: "success" });
navigateTo("/user/login");
} else {
ElMessage({ message: response.data.msg, type: "warning" });
}
}
</script>
<style>
.formbox {
width: 25%;
margin: auto;
}
@media screen and (max-width: 1200px) {
.formbox {
width: 100%;
}
}
</style>

View File

@ -3,6 +3,7 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-3.0.x"]
}
datasource db {
@ -16,6 +17,12 @@ model User {
username String?
password String
applications Application[]
loginlogs Loginlogs[]
}
model Adminer {
id Int @id @default(autoincrement())
adminId Int
}
model Application {
@ -30,3 +37,17 @@ model Application {
applicant User @relation(fields: [applicantId], references: [id])
applicantId Int
}
model Register {
id Int @id @default(autoincrement())
phone String
deadline DateTime
code String
}
model Loginlogs {
id Int @id @default(autoincrement())
outtime DateTime
ip String
loginer User @relation(fields: [userid], references: [id])
userid Int
token String @unique
}

113
prisma/test.prisma Normal file
View File

@ -0,0 +1,113 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-3.0.x"]
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
phone String @unique
username String?
password String
applications Application[]
loginlogs Loginlogs[]
Vm Vm[]
}
model Adminer {
id Int @id @default(autoincrement())
adminId Int
}
model Application {
id Int @id @default(autoincrement())
name String
area String
cpu Int
ram Int
disk Int
desc String
deploy Boolean @default(false)
applicant User @relation(fields: [applicantId], references: [id])
applicantId Int
}
model Register {
id Int @id @default(autoincrement())
phone String
deadline DateTime
code String
}
model Loginlogs {
id Int @id @default(autoincrement())
outtime DateTime
ip String
loginer User @relation(fields: [userid], references: [id])
userid Int
token String @unique
}
//Config
model Web {
ConfigId Int @id
ConfigName String
ConfigValue String
}
model Cluster {
ClusterId Int @id
Name String
Ip String
Username String
Password String
Gateway String
Resource String
Status String
Nodes Node[]
}
model Node {
NodeId Int @id
Cluster Cluster @relation(fields: [ClusterId], references: [ClusterId])
ClusterId Int
Resource String
Status String
Vms Vm[]
}
model Template {
TemplateId Int @id @default(autoincrement())
OS String
Type String
Path String
Cpu String
Ram String
Disk String
Ports String
Vm Vm[]
}
//datasource
model Ip {
Adress String @id
Vmid Int
Vm Vm @relation(fields: [Vmid], references: [Vmid])
}
model Port {
Port Int @id
Vmid Int
Vm Vm @relation(fields: [Vmid], references: [Vmid])
}
model Vm {
Vmid Int @id @default(autoincrement())
NodeId Int
Node Node @relation(fields: [NodeId], references: [NodeId])
TemplateId Int
Template Template @relation(fields: [TemplateId], references: [TemplateId])
UserId Int
User User @relation(fields: [UserId], references: [id])
Ip Ip[]
SshPort Int
Ports Port[]
}

View File

@ -0,0 +1,51 @@
import { PrismaClient } from "@prisma/client";
import type { Application } from "~/types/Application";
const db = new PrismaClient();
type app = {
applicant: {
username: string | null;
};
} & {
id: number;
name: string;
area: string;
cpu: number;
ram: number;
disk: number;
desc: string;
deploy: boolean;
applicantId: number;
};
function formatApplication(raw: app[]): Application[] {
var logs: Application[] = [];
raw.forEach((element: app) => {
logs.push({
id: element.id,
area: element.area,
name: element.name,
applicant: element.applicant.username || "none",
cpu: element.cpu,
ram: element.ram,
disk: element.disk,
desc: element.desc,
deploy: element.deploy,
});
});
return logs;
}
export default defineEventHandler(async (event) => {
const applications = await db.application.findMany({
orderBy: {
id: "desc", // 'asc' 表示升序,'desc' 表示降序
},
include: {
applicant: {
select: {
username: true,
},
},
},
});
await db.$disconnect();
return formatApplication(applications);
});

View File

@ -0,0 +1,38 @@
import { PrismaClient } from "@prisma/client";
import type { LoginLog } from "~/types/Log";
const db = new PrismaClient();
type log = { loginer: { username: string | null } } & {
id: number;
outtime: Date;
ip: string;
userid: number;
token: string;
};
function formatLogs(raw: log[]): LoginLog[] {
var logs: LoginLog[] = [];
raw.forEach((element: log) => {
logs.push({
id: element.id,
username: element.loginer.username || "none",
date: new Date(element.outtime.getTime() - 1800000).toLocaleTimeString(),
ip: element.ip,
});
});
return logs;
}
export default defineEventHandler(async (event) => {
const loginlogs = await db.loginlogs.findMany({
orderBy: {
outtime: "desc", // 'asc' 表示升序,'desc' 表示降序
},
include: {
loginer: {
select: {
username: true,
},
},
},
});
await db.$disconnect();
return formatLogs(loginlogs);
});

View File

@ -0,0 +1,6 @@
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
export default defineEventHandler(async (event) => {
const body = await readBody(event);
return body;
});

View File

@ -0,0 +1,15 @@
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const res = {
name:
(await db.application.findFirst({
where: {
name: body.name,
},
})) == null,
};
await db.$disconnect();
return res;
});

30
server/api/reg/code.ts Normal file
View File

@ -0,0 +1,30 @@
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const register = (await db.register.findFirst({
where: {
phone: body.phone,
},
orderBy: {
deadline: "desc", // 'asc' 表示升序,'desc' 表示降序
},
})) || {
phone: "",
code: "",
deadline: new Date(),
};
await db.$disconnect();
const deadlineDate = new Date(register.deadline);
const now = new Date();
if (now > deadlineDate) {
return { code: 2, msg: "验证码超时了,请重新发送" };
} else if (register.code != body.code) {
console.log(body.code + " phone " + register.phone + " " + register.code);
return { code: 0, msg: "验证码错误" };
} else if (register.code == body.code) {
return { code: 1, msg: "验证码正确" };
} else {
return { code: -1, msg: "未知错误" };
}
});

21
server/api/reg/index.ts Normal file
View File

@ -0,0 +1,21 @@
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const res = {
username:
(await db.user.findFirst({
where: {
username: body.username,
},
})) == null,
phone:
(await db.user.findFirst({
where: {
phone: body.phone,
},
})) == null,
};
await db.$disconnect();
return res;
});

96
server/api/reg/sms.ts Normal file
View File

@ -0,0 +1,96 @@
import { PrismaClient } from "@prisma/client";
import qmAPI from "quanmsms";
const config = useRuntimeConfig();
const smsApi = new qmAPI(config.apiOpenid.toString(), {
sms: {
apiKey: config.apiApikey,
},
});
const db = new PrismaClient();
function generateRandomCode(length: number) {
let code = "";
//const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';复杂验证码
const possible = "0123456789";
for (let i = 0; i < length; i++) {
code += possible.charAt(Math.floor(Math.random() * possible.length));
}
return code;
}
function generateDeadline() {
const now = new Date(); // 获取当前时间
const threeMinutesLater = new Date(now.getTime() + 3.5 * 60 * 1000); // 加上3分钟
return threeMinutesLater.toISOString(); // 转换为ISO格式字符串
}
function send(telstr: string) {
let tel = parseInt(telstr);
const code = generateRandomCode(6);
let param = {
tel: tel,
model_id: config.apiSmsId.toString(),
model_args: {
//模板id对应的变量key值,object格式内部自动处理成平台要求的str
code: code,
// 如果你的模板没变量,该值可为空
},
};
return smsApi
.sendSMS(param)
.then((response: { code: number; state: number }) => {
if (response.state == 200) {
db.register
.create({
data: {
phone: telstr,
deadline: generateDeadline(),
code: code,
},
})
.then((reg) => {
if (reg.phone == telstr) {
console.info(telstr + "发送成功");
}
});
return { code: 1, msg: "发送成功" };
} else {
console.info(tel);
return { code: 0, msg: "发送失败", error: response };
}
// 你的业务代码
});
}
export default defineEventHandler(async (event) => {
const body = await readBody(event);
let isSend =
(await db.register.findFirst({
where: {
phone: body.phone,
},
})) != null;
if (isSend) {
const register = (await db.register.findFirst({
where: {
phone: body.phone,
},
orderBy: {
deadline: "desc", // 'asc' 表示升序,'desc' 表示降序
},
})) || {
deadline: new Date(),
};
const deadlineDate = new Date(register.deadline);
const now = new Date();
if (now <= deadlineDate) {
await db.$disconnect();
return { code: 2, msg: "三分钟内请勿重发送" };
} else {
await db.$disconnect();
return send(body.phone);
}
} else {
await db.$disconnect();
return send(body.phone);
}
});

View File

@ -1,7 +1,66 @@
import { PrismaClient } from "@prisma/client"
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
export default defineEventHandler(async (event) => {
const body: {
name: string;
area: string;
cpu: string;
ram: string;
disk: string;
desc: string;
uid: string;
auth: string;
} = await readBody(event);
const isAuth: { login: boolean; code: number } = await $fetch(
"/api/user/auth",
{
method: "POST",
body: {
auth: body.auth,
},
}
);
if (isAuth.login) {
await db.application.create({
data: {
name: body.name,
area: body.area,
cpu: parseInt(body.cpu),
ram: parseInt(body.ram),
disk: parseInt(body.disk),
desc: body.desc,
applicantId: parseInt(body.uid),
},
});
const db = new PrismaClient()
await db.$disconnect();
return {
code: 1,
msg: "申请提交成功",
};
} else {
if (isAuth.code == 0) {
console.error(isAuth);
console.error(JSON.stringify(body));
return {
code: 0,
msg: "未登录",
};
} else {
return {
code: 0,
msg: "登陆超时",
};
}
}
return body;
});
export default defineEventHandler((event) => {
return 'Hello World!'
})
/* { name: '1231',
area: '1',
cpu: '2',
ram: '2',
disk: '5',
desc: '<div>12313</div>',
uid: '1',
auth: 'a19705902b2ba12fbe52930b34802ab1' } */

View File

@ -0,0 +1,55 @@
import { PrismaClient } from "@prisma/client";
import type { Application } from "~/types/Application";
const db = new PrismaClient();
type app = {
applicant: {
username: string | null;
};
} & {
id: number;
name: string;
area: string;
cpu: number;
ram: number;
disk: number;
desc: string;
deploy: boolean;
applicantId: number;
};
function formatApplication(raw: app[]): Application[] {
var logs: Application[] = [];
raw.forEach((element: app) => {
logs.push({
id: element.id,
area: element.area,
name: element.name,
applicant: element.applicant.username || "none",
cpu: element.cpu,
ram: element.ram,
disk: element.disk,
desc: element.desc,
deploy: element.deploy,
});
});
return logs;
}
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const applications = await db.application.findMany({
where: {
applicantId: body.uid,
},
orderBy: {
id: "desc", // 'asc' 表示升序,'desc' 表示降序
},
include: {
applicant: {
select: {
username: true,
},
},
},
});
await db.$disconnect();
return formatApplication(applications);
});

View File

@ -0,0 +1,23 @@
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
export default defineEventHandler(async (event) => {
const query: { auth: string } = getQuery(event);
let userid: number = 0;
userid = await db.loginlogs
.findFirst({
where: { token: query.auth },
select: {
userid: true,
},
})
.then((res) => {
if (res != null) {
return res.userid;
} else {
return 0;
}
});
//console.info(userid);
return userid;
});

35
server/api/user/auth.ts Normal file
View File

@ -0,0 +1,35 @@
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
async function auth(auth: string) {
const res = await db.loginlogs.findFirst({
where: { token: auth },
});
//return JSON.stringify((await res).values)
if (res == null) {
return {
login: false,
code: 0, //未登录
};
} else {
const deadlineDate = new Date(res.outtime);
const now = new Date();
if (now < deadlineDate) {
return {
login: true,
code: 1, //登录状态正常
};
} else {
return {
login: false,
code: 2, //登录超时
};
}
}
}
export default defineEventHandler(async (event) => {
const body = await readBody(event);
return auth(body.auth);
});

47
server/api/user/create.ts Normal file
View File

@ -0,0 +1,47 @@
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const register = await db.register.findFirst({
where: {
phone: body.phone,
},
orderBy: {
deadline: "desc", // 'asc' 表示升序,'desc' 表示降序
},
});
if (register != null) {
const deadlineDate = new Date(register.deadline.getTime() + 7 * 60 * 1000);
const now = new Date();
if (now <= deadlineDate) {
if (register.code != body.code) {
return { code: 0, msg: "验证码错误请重新发送" + register };
} else {
await db.user.create({
data: {
username: body.username,
phone: body.phone,
password: (await import("crypto"))
.createHash("md5")
.update(body.password)
.digest("hex"),
},
});
await db.$disconnect();
return { code: 1, msg: "注册成功" };
}
} else {
await db.$disconnect();
return {
code: -1,
msg: "验证码超时",
};
}
} else {
await db.$disconnect();
return {
code: -1,
msg: "验证码超时",
};
}
});

47
server/api/user/login.ts Normal file
View File

@ -0,0 +1,47 @@
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
function generateDeadline() {
const now = new Date(); // 获取当前时间
const threeMinutesLater = new Date(now.getTime() + 30 * 60 * 1000); // 加上30分钟
return threeMinutesLater.toISOString(); // 转换为ISO格式字符串
}
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const loginer = await db.user.findFirst({
where: {
phone: body.phone,
},
});
if (loginer == null) {
return { code: -1, msg: "未注册" };
} else {
if (
loginer.password ==
(await import("crypto"))
.createHash("md5")
.update(body.password)
.digest("hex")
) {
const token = (await import("crypto"))
.createHash("md5")
.update(body.password + new Date().toISOString())
.digest("hex");
await db.loginlogs.create({
data: {
userid: loginer.id,
outtime: generateDeadline(),
token: token,
ip: body.client_ip,
},
});
await db.$disconnect();
return { code: 1, msg: "登录成功", token: token };
} else {
await db.$disconnect();
return {
code: 0,
msg: "用户名、手机号或密码错误" /* + JSON.stringify(body) */,
};
}
}
});

15
server/api/user/logout.ts Normal file
View File

@ -0,0 +1,15 @@
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
export default defineEventHandler(async (event) => {
const body = await readBody(event);
await db.loginlogs.update({
where: {
token: body.auth,
},
data: {
outtime: new Date().toISOString(),
},
});
await db.$disconnect();
return 1;
});

View File

@ -1,3 +1,11 @@
export type Application = {
}
id: number;
name: string;
area: string;
cpu: number;
ram: number;
disk: number;
desc: string;
applicant: string;
deploy: boolean;
};

1
types/Application/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export { Application } from "./Application.ts";

6
types/Log/LoginLog.ts Normal file
View File

@ -0,0 +1,6 @@
export type LoginLog = {
id: number;
username: string;
date: string;
ip: string;
};

1
types/Log/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export { LoginLog } from "./LoginLog.ts";

View File

@ -4,12 +4,20 @@ import en from '@vueform/vueform/locales/en'
import zh_CN from '@vueform/vueform/locales/zh_CN'
import vueform from '@vueform/vueform/dist/vueform'
import { defineConfig } from '@vueform/vueform'
import PluginMask from '@vueform/plugin-mask'
import axios from 'axios'
// You might place these anywhere else in your project
import '@vueform/vueform/dist/vueform.css';
axios.defaults.headers.post = {
'Content-Type': 'application/json'
}
export default defineConfig({
axios,
theme: vueform,
locales: { zh_CN,en },
locale: 'zh_CN',
plugins: [
PluginMask,
]
})

View File

@ -4858,6 +4858,11 @@ proxy-from-env@^1.1.0:
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
quanmsms@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/quanmsms/-/quanmsms-1.0.2.tgz#8e7748329f87fd5f5550fe8eb94b6ab11a54ff90"
integrity sha512-wY4A2tz5ZANlgJw4t3fTgmcRpXO6FkyAxSfipdWa8DZxtfbItjQ4BErlugiEmL0sB2cTwJG9P3w5NT7i56T11Q==
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"