commit
02c7bc9b11
51
components/Admin/Application/DataTable.vue
Normal file
51
components/Admin/Application/DataTable.vue
Normal 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>
|
22
components/Admin/Application/Show.vue
Normal file
22
components/Admin/Application/Show.vue
Normal 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>
|
@ -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>
|
||||
|
115
components/Default/SilderVerify.vue
Normal file
115
components/Default/SilderVerify.vue
Normal 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
14
composables/logout.ts
Normal 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
11
composables/user.ts
Normal 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
6
middleware/admin.ts
Normal 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
27
middleware/auth.ts
Normal 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
7
middleware/unauth.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
const auth = useCookie("auth");
|
||||
if (auth.value === undefined) {
|
||||
} else {
|
||||
return navigateTo("/");
|
||||
}
|
||||
});
|
@ -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"],
|
||||
});
|
||||
|
@ -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
53
pages/admin/index.vue
Normal 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>
|
23
pages/admin/loginlogs/index.vue
Normal file
23
pages/admin/loginlogs/index.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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
63
pages/user/index.vue
Normal 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
111
pages/user/login/index.vue
Normal 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
9
pages/user/logout.ts
Normal file
@ -0,0 +1,9 @@
|
||||
const auth = useCookie("auth");
|
||||
$fetch("/api/user/logout", {
|
||||
method: "POST",
|
||||
body: {
|
||||
auth: auth.value,
|
||||
},
|
||||
});
|
||||
auth.value = undefined;
|
||||
navigateTo("/");
|
255
pages/user/register/index.vue
Normal file
255
pages/user/register/index.vue
Normal 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>
|
@ -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
113
prisma/test.prisma
Normal 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[]
|
||||
}
|
51
server/api/admin/applications.ts
Normal file
51
server/api/admin/applications.ts
Normal 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);
|
||||
});
|
38
server/api/admin/loginlogs.ts
Normal file
38
server/api/admin/loginlogs.ts
Normal 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);
|
||||
});
|
6
server/api/application/create.post.ts
Normal file
6
server/api/application/create.post.ts
Normal 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;
|
||||
});
|
15
server/api/application/index.ts
Normal file
15
server/api/application/index.ts
Normal 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
30
server/api/reg/code.ts
Normal 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
21
server/api/reg/index.ts
Normal 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
96
server/api/reg/sms.ts
Normal 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);
|
||||
}
|
||||
});
|
@ -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' } */
|
||||
|
55
server/api/user/application.post.ts
Normal file
55
server/api/user/application.post.ts
Normal 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);
|
||||
});
|
23
server/api/user/auth.get.ts
Normal file
23
server/api/user/auth.get.ts
Normal 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
35
server/api/user/auth.ts
Normal 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
47
server/api/user/create.ts
Normal 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
47
server/api/user/login.ts
Normal 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
15
server/api/user/logout.ts
Normal 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;
|
||||
});
|
@ -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
1
types/Application/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export { Application } from "./Application.ts";
|
6
types/Log/LoginLog.ts
Normal file
6
types/Log/LoginLog.ts
Normal 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
1
types/Log/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export { LoginLog } from "./LoginLog.ts";
|
@ -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,
|
||||
]
|
||||
})
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user