🎨合并编译打包
This commit is contained in:
230
web-src/src/views/dashboard/index.vue
Normal file
230
web-src/src/views/dashboard/index.vue
Normal file
@@ -0,0 +1,230 @@
|
||||
<script setup lang="ts">
|
||||
import { useServerStoreHook } from "@/store/modules/server";
|
||||
import { getSystemLog } from "@/api/dashboard";
|
||||
import { reactive } from "vue";
|
||||
import { getClientConnects } from "@/api/clients";
|
||||
|
||||
defineOptions({
|
||||
name: "Dashboard"
|
||||
});
|
||||
|
||||
const systemLogForm = reactive({
|
||||
current: 1,
|
||||
size: 5
|
||||
});
|
||||
|
||||
const systemLogTableData = reactive({
|
||||
total: 0,
|
||||
data: []
|
||||
});
|
||||
|
||||
const clientConnectsData = reactive({
|
||||
data: []
|
||||
});
|
||||
|
||||
// 请求操作日志记录
|
||||
const getSystemLogHandler = () => {
|
||||
getSystemLog(systemLogForm).then(res => {
|
||||
if (res.code === 200) {
|
||||
systemLogTableData.data = res.data.records;
|
||||
systemLogTableData.total = res.data.total;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 定义分页相关事件
|
||||
const pageChange = (page: number, size: number) => {
|
||||
systemLogForm.size = size;
|
||||
systemLogForm.current = page;
|
||||
getSystemLogHandler();
|
||||
};
|
||||
|
||||
const getClientsStatus = () => {
|
||||
getClientConnects().then(res => {
|
||||
if (res.code === 200) {
|
||||
clientConnectsData.data = res.data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const initServerInfo = () => {
|
||||
useServerStoreHook().getServerApi();
|
||||
};
|
||||
|
||||
initServerInfo();
|
||||
getSystemLogHandler();
|
||||
getClientsStatus();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-card style="max-width: 100%">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>操作日志</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
:data="systemLogTableData.data"
|
||||
align="center"
|
||||
:border="true"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="用户名称"
|
||||
min-width="80"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="clientIP"
|
||||
label="客户端IP"
|
||||
min-width="80"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag effect="dark">{{ scope.row.clientIP }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="method"
|
||||
label="操作方法"
|
||||
min-width="80"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.method === 'POST'" effect="dark">{{
|
||||
scope.row.method
|
||||
}}</el-tag>
|
||||
<el-tag
|
||||
v-if="scope.row.method === 'PUT'"
|
||||
type="warning"
|
||||
effect="dark"
|
||||
>{{ scope.row.method }}</el-tag
|
||||
>
|
||||
<el-tag
|
||||
v-if="scope.row.method === 'DELETE'"
|
||||
type="danger"
|
||||
effect="dark"
|
||||
>{{ scope.row.method }}</el-tag
|
||||
>
|
||||
<el-tag
|
||||
v-if="scope.row.method === 'GET'"
|
||||
type="info"
|
||||
effect="dark"
|
||||
>{{ scope.row.method }}</el-tag
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="host"
|
||||
label="请求主机"
|
||||
min-width="80"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="uri"
|
||||
label="操作路径"
|
||||
min-width="80"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="statusCode"
|
||||
label="响应状态码"
|
||||
min-width="80"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
v-if="scope.row.statusCode === 200"
|
||||
type="success"
|
||||
effect="dark"
|
||||
>{{ scope.row.statusCode }}</el-tag
|
||||
>
|
||||
<el-tag v-else type="danger" effect="dark">{{
|
||||
scope.row.statusCode
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
label="操作时间"
|
||||
min-width="80"
|
||||
align="center"
|
||||
/>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-pagination
|
||||
small
|
||||
background
|
||||
layout="total,prev,pager,next"
|
||||
:page-size="systemLogForm.size"
|
||||
:total="systemLogTableData.total"
|
||||
@change="pageChange"
|
||||
/>
|
||||
</template>
|
||||
</el-card>
|
||||
<el-card style="max-width: 100%; margin-top: 10px">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>客户端链接状态</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
:data="clientConnectsData.data"
|
||||
align="center"
|
||||
:border="true"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="客户端名称"
|
||||
min-width="80"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="email"
|
||||
label="联系邮箱"
|
||||
min-width="80"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="ipAllocation"
|
||||
label="客户端IP"
|
||||
min-width="80"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="endpoint"
|
||||
label="端点"
|
||||
min-width="80"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="received"
|
||||
label="接受流量"
|
||||
min-width="80"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="transmitted"
|
||||
label="传输流量"
|
||||
min-width="80"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="isOnline"
|
||||
label="是否在线"
|
||||
min-width="80"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="lastHandShake"
|
||||
label="最后握手时间"
|
||||
min-width="80"
|
||||
align="center"
|
||||
/>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
70
web-src/src/views/error/403.vue
Normal file
70
web-src/src/views/error/403.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import noAccess from "@/assets/status/403.svg?component";
|
||||
|
||||
defineOptions({
|
||||
name: "403"
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-center items-center h-[640px]">
|
||||
<noAccess />
|
||||
<div class="ml-12">
|
||||
<p
|
||||
v-motion
|
||||
class="font-medium text-4xl mb-4 dark:text-white"
|
||||
:initial="{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: 80
|
||||
}
|
||||
}"
|
||||
>
|
||||
403
|
||||
</p>
|
||||
<p
|
||||
v-motion
|
||||
class="mb-4 text-gray-500"
|
||||
:initial="{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: 120
|
||||
}
|
||||
}"
|
||||
>
|
||||
抱歉,你无权访问该页面
|
||||
</p>
|
||||
<el-button
|
||||
v-motion
|
||||
type="primary"
|
||||
:initial="{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: 160
|
||||
}
|
||||
}"
|
||||
@click="router.push('/')"
|
||||
>
|
||||
返回首页
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
70
web-src/src/views/error/404.vue
Normal file
70
web-src/src/views/error/404.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import noExist from "@/assets/status/404.svg?component";
|
||||
|
||||
defineOptions({
|
||||
name: "404"
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-center items-center h-[640px]">
|
||||
<noExist />
|
||||
<div class="ml-12">
|
||||
<p
|
||||
v-motion
|
||||
class="font-medium text-4xl mb-4 dark:text-white"
|
||||
:initial="{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: 80
|
||||
}
|
||||
}"
|
||||
>
|
||||
404
|
||||
</p>
|
||||
<p
|
||||
v-motion
|
||||
class="mb-4 text-gray-500"
|
||||
:initial="{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: 120
|
||||
}
|
||||
}"
|
||||
>
|
||||
抱歉,你访问的页面不存在
|
||||
</p>
|
||||
<el-button
|
||||
v-motion
|
||||
type="primary"
|
||||
:initial="{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: 160
|
||||
}
|
||||
}"
|
||||
@click="router.push('/')"
|
||||
>
|
||||
返回首页
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
70
web-src/src/views/error/500.vue
Normal file
70
web-src/src/views/error/500.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import noServer from "@/assets/status/500.svg?component";
|
||||
|
||||
defineOptions({
|
||||
name: "500"
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-center items-center h-[640px]">
|
||||
<noServer />
|
||||
<div class="ml-12">
|
||||
<p
|
||||
v-motion
|
||||
class="font-medium text-4xl mb-4 dark:text-white"
|
||||
:initial="{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: 80
|
||||
}
|
||||
}"
|
||||
>
|
||||
500
|
||||
</p>
|
||||
<p
|
||||
v-motion
|
||||
class="mb-4 text-gray-500"
|
||||
:initial="{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: 120
|
||||
}
|
||||
}"
|
||||
>
|
||||
抱歉,服务器出错了
|
||||
</p>
|
||||
<el-button
|
||||
v-motion
|
||||
type="primary"
|
||||
:initial="{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: 160
|
||||
}
|
||||
}"
|
||||
@click="router.push('/')"
|
||||
>
|
||||
返回首页
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
188
web-src/src/views/login/index.vue
Normal file
188
web-src/src/views/login/index.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<script setup lang="ts">
|
||||
import Motion from "./utils/motion";
|
||||
import { useRouter } from "vue-router";
|
||||
import { message } from "@/utils/message";
|
||||
import { loginRules } from "./utils/rule";
|
||||
import { useNav } from "@/layout/hooks/useNav";
|
||||
import type { FormInstance } from "element-plus";
|
||||
import { useLayout } from "@/layout/hooks/useLayout";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
import { addPathMatch } from "@/router/utils";
|
||||
import { bg, avatar, illustration } from "./utils/static";
|
||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
import { ref, reactive, toRaw, onMounted, onBeforeUnmount } from "vue";
|
||||
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
|
||||
|
||||
import dayIcon from "@/assets/svg/day.svg?component";
|
||||
import darkIcon from "@/assets/svg/dark.svg?component";
|
||||
import Lock from "@iconify-icons/ri/lock-fill";
|
||||
import User from "@iconify-icons/ri/user-3-fill";
|
||||
import ReImageVerify from "@/components/ReImageVerify/src/index.vue";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
import useGetGlobalProperties from "@/hooks/useGetGlobalProperties";
|
||||
|
||||
defineOptions({
|
||||
name: "Login"
|
||||
});
|
||||
const { $bus } = useGetGlobalProperties();
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const loginRuleFormRef = ref<FormInstance>();
|
||||
|
||||
const { initStorage } = useLayout();
|
||||
initStorage();
|
||||
|
||||
const { dataTheme, overallStyle, dataThemeChange } = useDataThemeChange();
|
||||
dataThemeChange(overallStyle.value);
|
||||
const { title } = useNav();
|
||||
|
||||
const imgCode = ref("");
|
||||
const imgCodeId = ref("");
|
||||
const loginRuleForm = reactive({
|
||||
account: "", // 账号
|
||||
password: "", // 密码
|
||||
captchaId: "", // 验证码id
|
||||
captchaAnswer: "" // 验证码
|
||||
});
|
||||
|
||||
// 登陆接口
|
||||
const onLogin = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
loginRuleForm.captchaId = imgCodeId.value;
|
||||
useUserStoreHook()
|
||||
.loginByUsername(loginRuleForm)
|
||||
.then(res => {
|
||||
if (res.code === 200) {
|
||||
// 获取后端路由
|
||||
usePermissionStoreHook().handleWholeMenus([]);
|
||||
addPathMatch();
|
||||
router.push("/dashboard");
|
||||
message("登录成功", { type: "success" });
|
||||
} else {
|
||||
message("登录失败", { type: "error" });
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
$bus.emit("refreshCode", true);
|
||||
})
|
||||
.finally(() => (loading.value = false));
|
||||
} else {
|
||||
return fields;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 使用公共函数,避免`removeEventListener`失效 */
|
||||
function onkeypress({ code }: KeyboardEvent) {
|
||||
if (code === "Enter") {
|
||||
onLogin(loginRuleFormRef.value);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.document.addEventListener("keypress", onkeypress);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.document.removeEventListener("keypress", onkeypress);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="select-none">
|
||||
<img :src="bg" class="wave" />
|
||||
<div class="flex-c absolute right-5 top-3">
|
||||
<!-- 主题 -->
|
||||
<el-switch
|
||||
v-model="dataTheme"
|
||||
inline-prompt
|
||||
:active-icon="dayIcon"
|
||||
:inactive-icon="darkIcon"
|
||||
@change="dataThemeChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="login-container">
|
||||
<div class="img">
|
||||
<component :is="toRaw(illustration)" />
|
||||
</div>
|
||||
<div class="login-box">
|
||||
<div class="login-form">
|
||||
<avatar class="avatar" />
|
||||
<Motion>
|
||||
<h2 class="outline-none">{{ title }} LOGIN</h2>
|
||||
</Motion>
|
||||
|
||||
<el-form
|
||||
ref="loginRuleFormRef"
|
||||
:model="loginRuleForm"
|
||||
:rules="loginRules"
|
||||
size="large"
|
||||
>
|
||||
<Motion :delay="100">
|
||||
<el-form-item prop="account">
|
||||
<el-input
|
||||
v-model="loginRuleForm.account"
|
||||
clearable
|
||||
placeholder="账号"
|
||||
:prefix-icon="useRenderIcon(User)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</Motion>
|
||||
|
||||
<Motion :delay="150">
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginRuleForm.password"
|
||||
clearable
|
||||
show-password
|
||||
placeholder="密码"
|
||||
:prefix-icon="useRenderIcon(Lock)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</Motion>
|
||||
<Motion :delay="200">
|
||||
<el-form-item prop="captchaAnswer">
|
||||
<el-input
|
||||
v-model="loginRuleForm.captchaAnswer"
|
||||
clearable
|
||||
:prefix-icon="useRenderIcon('ri:shield-keyhole-line')"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<ReImageVerify
|
||||
v-model:code="imgCode"
|
||||
v-model:codeId="imgCodeId"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</Motion>
|
||||
<Motion :delay="250">
|
||||
<el-button
|
||||
class="w-full mt-4"
|
||||
size="default"
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
@click="onLogin(loginRuleFormRef)"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</Motion>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import url("@/style/login.css");
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-input-group__append, .el-input-group__prepend) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
40
web-src/src/views/login/utils/motion.ts
Normal file
40
web-src/src/views/login/utils/motion.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { h, defineComponent, withDirectives, resolveDirective } from "vue";
|
||||
|
||||
/** 封装@vueuse/motion动画库中的自定义指令v-motion */
|
||||
export default defineComponent({
|
||||
name: "Motion",
|
||||
props: {
|
||||
delay: {
|
||||
type: Number,
|
||||
default: 50
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const { delay } = this;
|
||||
const motion = resolveDirective("motion");
|
||||
return withDirectives(
|
||||
h(
|
||||
"div",
|
||||
{},
|
||||
{
|
||||
default: () => [this.$slots.default()]
|
||||
}
|
||||
),
|
||||
[
|
||||
[
|
||||
motion,
|
||||
{
|
||||
initial: { opacity: 0, y: 100 },
|
||||
enter: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
39
web-src/src/views/login/utils/rule.ts
Normal file
39
web-src/src/views/login/utils/rule.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { reactive } from "vue";
|
||||
import type { FormRules } from "element-plus";
|
||||
|
||||
/** 登录校验 */
|
||||
const loginRules = reactive(<FormRules>{
|
||||
account: [
|
||||
{
|
||||
required: true,
|
||||
message: "账号不能为空",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: "密码不能为空",
|
||||
trigger: "blur"
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
message: "密码长度最少6位",
|
||||
trigger: "blur"
|
||||
},
|
||||
{
|
||||
max: 32,
|
||||
message: "密码长度最长32位",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
captchaAnswer: [
|
||||
{
|
||||
required: true,
|
||||
message: "验证码不能为空",
|
||||
trigger: "blur"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export { loginRules };
|
5
web-src/src/views/login/utils/static.ts
Normal file
5
web-src/src/views/login/utils/static.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import bg from "@/assets/login/bg.png";
|
||||
import avatar from "@/assets/login/avatar.svg?component";
|
||||
import illustration from "@/assets/login/illustration.svg?component";
|
||||
|
||||
export { bg, avatar, illustration };
|
99
web-src/src/views/permission/button/index.vue
Normal file
99
web-src/src/views/permission/button/index.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { hasAuth, getAuths } from "@/router/utils";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionButton"
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-2">当前拥有的code列表:{{ getAuths() }}</p>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">组件方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<Auth value="permission:btn:add">
|
||||
<el-button plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
<Auth :value="['permission:btn:edit']">
|
||||
<el-button plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
<Auth
|
||||
:value="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
>
|
||||
<el-button plain type="danger">
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">函数方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-if="hasAuth('permission:btn:add')" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button v-if="hasAuth(['permission:btn:edit'])" plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="
|
||||
hasAuth([
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
])
|
||||
"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
指令方式判断权限(该方式不能动态修改权限)
|
||||
</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-auth="'permission:btn:add'" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button v-auth="['permission:btn:edit']" plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-auth="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
66
web-src/src/views/permission/page/index.vue
Normal file
66
web-src/src/views/permission/page/index.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { initRouter } from "@/router/utils";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { type CSSProperties, ref, computed } from "vue";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionPage"
|
||||
});
|
||||
|
||||
const elStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
width: "85vw",
|
||||
justifyContent: "start"
|
||||
};
|
||||
});
|
||||
|
||||
const username = ref(useUserStoreHook()?.username);
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: "user",
|
||||
label: "管理员角色"
|
||||
},
|
||||
{
|
||||
value: "common",
|
||||
label: "普通角色"
|
||||
}
|
||||
];
|
||||
|
||||
function onChange() {
|
||||
useUserStoreHook()
|
||||
.loginByUsername({ username: username.value, password: "admin123" })
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
storageLocal().removeItem("async-routes");
|
||||
usePermissionStoreHook().clearAllCachePage();
|
||||
initRouter();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-2">
|
||||
模拟后台根据不同角色返回对应路由,观察左侧菜单变化(管理员角色可查看系统管理菜单、普通角色不可查看系统管理菜单)
|
||||
</p>
|
||||
<el-card shadow="never" :style="elStyle">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>当前角色:{{ username }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-select v-model="username" class="!w-[160px]" @change="onChange">
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
442
web-src/src/views/server/clients.vue
Normal file
442
web-src/src/views/server/clients.vue
Normal file
@@ -0,0 +1,442 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
deleteClient,
|
||||
downloadClient,
|
||||
getClients,
|
||||
saveClient
|
||||
} from "@/api/clients";
|
||||
import { h, reactive, ref } from "vue";
|
||||
import { addDialog } from "@/components/ReDialog/index";
|
||||
import qrCodeForms from "./component/qrCode.vue";
|
||||
import editClientForms from "./component/detail.vue";
|
||||
import { type PlusColumn, PlusSearch } from "plus-pro-components";
|
||||
import "plus-pro-components/es/components/search/style/css";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
|
||||
defineOptions({
|
||||
// name 作为一种规范最好必须写上并且和路由的name保持一致
|
||||
name: "Clients"
|
||||
});
|
||||
|
||||
const editClientFormRef = ref();
|
||||
|
||||
const clientSearchForm = ref({
|
||||
name: "",
|
||||
email: "",
|
||||
ip: "",
|
||||
createUser: "",
|
||||
enabled: undefined,
|
||||
current: 1,
|
||||
size: 9
|
||||
});
|
||||
|
||||
const clientSearchProps = {
|
||||
gutter: 0
|
||||
};
|
||||
|
||||
const clientSearchFormColumns: PlusColumn[] = [
|
||||
{
|
||||
label: "名称",
|
||||
prop: "name",
|
||||
valueType: "copy",
|
||||
fieldProps: {
|
||||
placeholder: "请输入"
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "邮箱",
|
||||
prop: "email",
|
||||
valueType: "copy",
|
||||
fieldProps: {
|
||||
placeholder: "请输入"
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "IP",
|
||||
prop: "ip",
|
||||
valueType: "copy",
|
||||
fieldProps: {
|
||||
placeholder: "请输入"
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "创建人",
|
||||
prop: "createUser",
|
||||
valueType: "copy",
|
||||
fieldProps: {
|
||||
placeholder: "请输入"
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "状态",
|
||||
prop: "enabled",
|
||||
valueType: "select",
|
||||
options: [
|
||||
{
|
||||
label: "启用",
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: "禁用",
|
||||
value: 0
|
||||
}
|
||||
],
|
||||
fieldProps: {
|
||||
placeholder: "请选择"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const clientsList = reactive({
|
||||
data: [],
|
||||
total: 0
|
||||
});
|
||||
|
||||
const getClientsApi = (data?: object) => {
|
||||
getClients(data).then(clients => {
|
||||
if (clients.code === 200) {
|
||||
clientsList.data = clients.data.records;
|
||||
clientsList.total = clients.data.total;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 下载客户端配置文件
|
||||
const downloadClientConfig = (id: string, clientName: string) => {
|
||||
downloadClient(id).then(response => {
|
||||
if (response) {
|
||||
const blob = new Blob([response], {
|
||||
type: "text/plain"
|
||||
});
|
||||
const link = document.createElement("a"); // 创建a标签
|
||||
link.download = clientName + ".conf"; // a标签添加属性
|
||||
link.style.display = "none";
|
||||
link.href = URL.createObjectURL(blob);
|
||||
document.body.appendChild(link);
|
||||
link.click(); // 执行下载
|
||||
URL.revokeObjectURL(link.href); // 释放url
|
||||
document.body.removeChild(link); // 释放标签
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 打开二维码模态框
|
||||
const openQrCodeDialog = (clientName: string, id: string) => {
|
||||
addDialog({
|
||||
width: "20%",
|
||||
title: clientName,
|
||||
contentRenderer: () => h(qrCodeForms),
|
||||
props: {
|
||||
formInline: {
|
||||
id: id
|
||||
}
|
||||
},
|
||||
hideFooter: true
|
||||
});
|
||||
};
|
||||
|
||||
// 打开新增客户端弹窗
|
||||
const openAddClientDialog = () => {
|
||||
const serverInfo = storageLocal().getItem("server-info");
|
||||
addDialog({
|
||||
width: "40%",
|
||||
title: "新增",
|
||||
contentRenderer: () => h(editClientForms, { ref: editClientFormRef }),
|
||||
props: {
|
||||
formInline: {
|
||||
id: "",
|
||||
serverId: serverInfo.id,
|
||||
name: "",
|
||||
email: "",
|
||||
subnetRange: "",
|
||||
ipAllocation: "",
|
||||
allowedIPS: "",
|
||||
extraAllowedIPS: "",
|
||||
endpoint: "",
|
||||
useServerDNS: 0,
|
||||
enableAfterCreation: 0,
|
||||
keys: null,
|
||||
enabled: 1
|
||||
}
|
||||
},
|
||||
beforeSure: (done, { options }) => {
|
||||
const FormRef = editClientFormRef.value.getDetailFormRef();
|
||||
FormRef.validate(valid => {
|
||||
if (!valid) return;
|
||||
saveClient(options.props.formInline).then(res => {
|
||||
if (res.code === 200) {
|
||||
done();
|
||||
getClientsApi(clientSearchForm.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 打开编辑客户端信息模态框
|
||||
const openEditClientDialog = (client?: any) => {
|
||||
const serverInfo = storageLocal().getItem("server-info");
|
||||
addDialog({
|
||||
width: "40%",
|
||||
title: client.name,
|
||||
contentRenderer: () => h(editClientForms, { ref: editClientFormRef }),
|
||||
props: {
|
||||
formInline: {
|
||||
id: client.id,
|
||||
serverId: serverInfo.id,
|
||||
name: client.name,
|
||||
email: client.email,
|
||||
subnetRange: client.subnetRange,
|
||||
ipAllocation: client.ipAllocation,
|
||||
allowedIPS: client.allowedIPS,
|
||||
extraAllowedIPS: client.extraAllowedIPS,
|
||||
endpoint: client.endpoint,
|
||||
useServerDNS: client.useServerDNS,
|
||||
enableAfterCreation: client.enableAfterCreation,
|
||||
keys: client.keys,
|
||||
enabled: Number(client.enabled)
|
||||
}
|
||||
},
|
||||
beforeSure: (done, { options }) => {
|
||||
const FormRef = editClientFormRef.value.getDetailFormRef();
|
||||
FormRef.validate(valid => {
|
||||
if (!valid) return;
|
||||
saveClient(options.props.formInline).then(res => {
|
||||
if (res.code === 200) {
|
||||
done();
|
||||
getClientsApi(clientSearchForm.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 打开删除弹窗
|
||||
const openDeleteMessageBox = (clientName: string, clientId: string) => {
|
||||
ElMessageBox.confirm("是否删除:" + clientName, "删除", {
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
center: true
|
||||
}).then(() => {
|
||||
deleteClient(clientId).then(res => {
|
||||
if (res.code === 200) {
|
||||
getClientsApi(clientSearchForm.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 搜索
|
||||
const searchHandler = (value: object) => {
|
||||
getClientsApi(value);
|
||||
};
|
||||
|
||||
// 重置搜索表单
|
||||
const resetSearchHandler = (value: object) => {
|
||||
getClientsApi({
|
||||
current: 1,
|
||||
size: 9
|
||||
});
|
||||
clientSearchForm.value.current = 1;
|
||||
clientSearchForm.value.size = 9;
|
||||
};
|
||||
|
||||
// 定义分页相关事件
|
||||
const pageChange = (page: number, size: number) => {
|
||||
clientSearchForm.value.size = size;
|
||||
clientSearchForm.value.current = page;
|
||||
getClientsApi(clientSearchForm.value);
|
||||
};
|
||||
|
||||
// 超长省略号显示
|
||||
const ellipsis = (str: string) => {
|
||||
if (!str) return "";
|
||||
if (str.length >= 10) {
|
||||
return str.slice(0, 10) + "...";
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
getClientsApi(clientSearchForm.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="search-header" style="padding-bottom: 5px">
|
||||
<el-card>
|
||||
<PlusSearch
|
||||
v-model="clientSearchForm"
|
||||
resetText="重置"
|
||||
searchText="搜索"
|
||||
:hasUnfold="false"
|
||||
:columns="clientSearchFormColumns"
|
||||
:rowProps="clientSearchProps"
|
||||
label-width="80"
|
||||
label-position="right"
|
||||
@search="searchHandler"
|
||||
@reset="resetSearchHandler"
|
||||
/>
|
||||
</el-card>
|
||||
<div style="margin-top: 5px">
|
||||
<el-card>
|
||||
<el-button type="primary" @click="openAddClientDialog"
|
||||
>新增客户端</el-button
|
||||
>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-card body-style="padding: inherit" shadow="hover">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<el-card
|
||||
v-for="val in clientsList.data"
|
||||
style="width: 540px"
|
||||
shadow="hover"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-tooltip :content="val.name" placement="top">
|
||||
<el-tag size="large">
|
||||
{{ ellipsis(val.name) }}
|
||||
</el-tag>
|
||||
</el-tooltip>
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
type="primary"
|
||||
split-button
|
||||
style="float: right"
|
||||
>
|
||||
更多
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
<el-button
|
||||
type="warning"
|
||||
@click="downloadClientConfig(val.id, val.name)"
|
||||
>下载</el-button
|
||||
>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="openDeleteMessageBox(val.name, val.id)"
|
||||
>删除</el-button
|
||||
>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button
|
||||
type="success"
|
||||
style="float: right; margin-right: 5px"
|
||||
@click="openQrCodeDialog(val.name, val.id)"
|
||||
>二维码</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
style="float: right; margin-right: 5px"
|
||||
@click="openEditClientDialog(val)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-form label-position="top">
|
||||
<el-form-item prop="name" label="名称">
|
||||
<el-input v-model="val.name" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="email" label="邮箱">
|
||||
<el-input v-model="val.email" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="ipAllocation" label="客户端IP">
|
||||
<el-select
|
||||
v-model="val.ipAllocation"
|
||||
:clearable="true"
|
||||
:reserve-keyword="false"
|
||||
suffix-icon=""
|
||||
tag-type="primary"
|
||||
popper-class="options-class"
|
||||
placeholder=""
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="allowedIPS" label="允许的IP范围">
|
||||
<el-select
|
||||
v-model="val.allowedIPS"
|
||||
:clearable="true"
|
||||
:reserve-keyword="false"
|
||||
suffix-icon=""
|
||||
tag-type="danger"
|
||||
popper-class="options-class"
|
||||
placeholder=""
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建人">
|
||||
<el-tag effect="dark" type="primary">{{
|
||||
val.createUser
|
||||
}}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户端状态">
|
||||
<el-tag v-if="val.enabled" effect="dark" type="success"
|
||||
>启用</el-tag
|
||||
>
|
||||
<el-tag v-else effect="dark" type="warning">禁用</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item class="timeItem">
|
||||
<p>创建时间: {{ val.createdAt }}</p>
|
||||
<p>更新时间: {{ val.updatedAt }}</p>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
<div class="paginate" style="background-color: #ffffff; margin-top: 5px">
|
||||
<el-card>
|
||||
<el-pagination
|
||||
small
|
||||
background
|
||||
layout="total,prev,pager,next"
|
||||
:page-size="clientSearchForm.size"
|
||||
:total="clientsList.total"
|
||||
@change="pageChange"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.timeItem {
|
||||
.el-form-item__content {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.options-class {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.example-showcase .el-dropdown + .el-dropdown {
|
||||
margin-left: 15px;
|
||||
}
|
||||
.example-showcase .el-dropdown-link {
|
||||
cursor: pointer;
|
||||
color: var(--el-color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
159
web-src/src/views/server/component/detail.vue
Normal file
159
web-src/src/views/server/component/detail.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { FormInstance } from "element-plus";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { userKey } from "@/utils/auth";
|
||||
import {clientFormRules} from "@/views/server/component/rules";
|
||||
|
||||
// 声明 props 类型
|
||||
export interface DetailFormProps {
|
||||
formInline: {
|
||||
id: string;
|
||||
serverId: string;
|
||||
name: string;
|
||||
email: string;
|
||||
subnetRange: string;
|
||||
ipAllocation: [];
|
||||
allowedIPS: [];
|
||||
extraAllowedIPS: [];
|
||||
endpoint: string;
|
||||
useServerDNS: number;
|
||||
enableAfterCreation: number;
|
||||
keys: {
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
presharedKey: string;
|
||||
};
|
||||
enabled: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 声明 props 默认值
|
||||
// 推荐阅读:https://cn.vuejs.org/guide/typescript/composition-api.html#typing-component-props
|
||||
const props = withDefaults(defineProps<DetailFormProps>(), {
|
||||
formInline: () => ({
|
||||
id: "",
|
||||
serverId: "",
|
||||
name: "",
|
||||
email: "",
|
||||
subnetRange: "",
|
||||
ipAllocation: [],
|
||||
allowedIPS: [],
|
||||
extraAllowedIPS: [],
|
||||
endpoint: "",
|
||||
useServerDNS: 0,
|
||||
enableAfterCreation: 0,
|
||||
keys: {
|
||||
privateKey: "",
|
||||
publicKey: "",
|
||||
presharedKey: ""
|
||||
},
|
||||
enabled: 1
|
||||
})
|
||||
});
|
||||
|
||||
// vue 规定所有的 prop 都遵循着单向绑定原则,直接修改 prop 时,Vue 会抛出警告。此处的写法仅仅是为了消除警告。
|
||||
// 因为对一个 reactive 对象执行 ref,返回 Ref 对象的 value 值仍为传入的 reactive 对象,
|
||||
// 即 newFormInline === props.formInline 为 true,所以此处代码的实际效果,仍是直接修改 props.formInline。
|
||||
// 但该写法仅适用于 props.formInline 是一个对象类型的情况,原始类型需抛出事件
|
||||
// 推荐阅读:https://cn.vuejs.org/guide/components/props.html#one-way-data-flow
|
||||
const detailForm = ref(props.formInline);
|
||||
const detailFormRef = ref<FormInstance>();
|
||||
|
||||
function getDetailFormRef() {
|
||||
return detailFormRef.value;
|
||||
}
|
||||
|
||||
defineExpose({ getDetailFormRef });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form
|
||||
ref="detailFormRef"
|
||||
:model="detailForm"
|
||||
:rules="clientFormRules"
|
||||
label-width="20%"
|
||||
label-position="right"
|
||||
>
|
||||
<el-form-item prop="name" label="名称">
|
||||
<el-input v-model="detailForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="email" label="邮箱">
|
||||
<el-input v-model="detailForm.email" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="subnetRange" label="子网范围">
|
||||
<el-input v-model="detailForm.subnetRange" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="ipAllocation" label="客户端IP">
|
||||
<el-select
|
||||
v-model="detailForm.ipAllocation"
|
||||
:clearable="true"
|
||||
:reserve-keyword="false"
|
||||
suffix-icon=""
|
||||
tag-type="warning"
|
||||
popper-class="options-class"
|
||||
placeholder=""
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="allowedIPS" label="允许访问的IP段">
|
||||
<el-select
|
||||
v-model="detailForm.allowedIPS"
|
||||
:clearable="true"
|
||||
:reserve-keyword="false"
|
||||
suffix-icon=""
|
||||
tag-type="warning"
|
||||
popper-class="options-class"
|
||||
placeholder=""
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="extraAllowedIPS" label="其他允许访问的IP段">
|
||||
<el-select
|
||||
v-model="detailForm.extraAllowedIPS"
|
||||
:clearable="true"
|
||||
:reserve-keyword="false"
|
||||
suffix-icon=""
|
||||
tag-type="warning"
|
||||
popper-class="options-class"
|
||||
placeholder=""
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="endpoint" label="链接端点">
|
||||
<el-input v-model="detailForm.endpoint" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="useServerDNS" label="是否使用服务端DNS">
|
||||
<el-radio-group v-model="detailForm.useServerDNS">
|
||||
<el-radio :value="1">是</el-radio>
|
||||
<el-radio :value="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item prop="enableAfterCreation" label="确认后创建">
|
||||
<el-radio-group v-model="detailForm.enableAfterCreation">
|
||||
<el-radio :value="1">是</el-radio>
|
||||
<el-radio :value="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item prop="enabled" label="状态">
|
||||
<el-radio-group v-model="detailForm.enabled">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.options-class {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
45
web-src/src/views/server/component/qrCode.vue
Normal file
45
web-src/src/views/server/component/qrCode.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { FormInstance } from "element-plus";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { userKey } from "@/utils/auth";
|
||||
import {clientQrCode} from "@/api/clients";
|
||||
|
||||
// 声明 props 类型
|
||||
export interface DetailFormProps {
|
||||
formInline: {
|
||||
id: string;
|
||||
qrCodeStr: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 声明 props 默认值
|
||||
// 推荐阅读:https://cn.vuejs.org/guide/typescript/composition-api.html#typing-component-props
|
||||
const props = withDefaults(defineProps<DetailFormProps>(), {
|
||||
formInline: () => ({
|
||||
id: "",
|
||||
qrCodeStr: ""
|
||||
})
|
||||
});
|
||||
|
||||
const qrCodeData = ref(props.formInline);
|
||||
|
||||
const getQrCode = (id: string) => {
|
||||
clientQrCode(id).then(res => {
|
||||
if (res.code === 200) {
|
||||
qrCodeData.value.qrCodeStr = res.data.qrCode;
|
||||
}
|
||||
});
|
||||
};
|
||||
getQrCode(props.formInline.id);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card body-style="padding: inherit" shadow="hover">
|
||||
<el-image
|
||||
:src="qrCodeData.qrCodeStr"
|
||||
:preview-teleported="true"
|
||||
style="width: 100%; height: 100%"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
36
web-src/src/views/server/component/rules.ts
Normal file
36
web-src/src/views/server/component/rules.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { reactive } from "vue";
|
||||
import type { FormRules } from "element-plus";
|
||||
|
||||
/** 登录校验 */
|
||||
const clientFormRules = reactive(<FormRules>{
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: "名称不能为空",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
ipAllocation: [
|
||||
{
|
||||
required: true,
|
||||
message: "客户端IP不能为空",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
allowedIPS: [
|
||||
{
|
||||
required: true,
|
||||
message: "允许链接IP不能为空",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
enabled: [
|
||||
{
|
||||
required: true,
|
||||
message: "状态不能为空",
|
||||
trigger: "blur"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export { clientFormRules };
|
261
web-src/src/views/server/server.vue
Normal file
261
web-src/src/views/server/server.vue
Normal file
@@ -0,0 +1,261 @@
|
||||
<script setup lang="tsx">
|
||||
import { reactive, ref } from "vue";
|
||||
import type { FormInstance } from "element-plus";
|
||||
import { useServerStoreHook } from "@/store/modules/server";
|
||||
import { serverFormRules } from "@/views/server/utils/rules";
|
||||
import { useGlobalSettingStoreHook } from "@/store/modules/globalSetting";
|
||||
import { getPublicIP } from "@/api/server";
|
||||
|
||||
defineOptions({
|
||||
// name 作为一种规范最好必须写上并且和路由的name保持一致
|
||||
name: "Server"
|
||||
});
|
||||
|
||||
const serverFormRef = ref<FormInstance>();
|
||||
|
||||
let serverForm = reactive({
|
||||
id: "",
|
||||
ipScope: "",
|
||||
listenPort: 0,
|
||||
privateKey: "",
|
||||
publicKey: "",
|
||||
postUpScript: "",
|
||||
preDownScript: "",
|
||||
postDownScript: ""
|
||||
});
|
||||
|
||||
const configFormRef = ref<FormInstance>();
|
||||
let configForm = reactive({
|
||||
endpointAddress: "",
|
||||
dnsServer: [],
|
||||
MTU: 0,
|
||||
persistentKeepalive: 0,
|
||||
firewallMark: "",
|
||||
table: "",
|
||||
configFilePath: ""
|
||||
});
|
||||
|
||||
// 获取服务端信息接口
|
||||
const getServerApi = () => {
|
||||
useServerStoreHook()
|
||||
.getServerApi()
|
||||
.then(res => {
|
||||
if (res.code === 200) {
|
||||
const data = useServerStoreHook().getServerInfo;
|
||||
serverForm.id = data.id || "";
|
||||
serverForm.ipScope = data.ipScope || "";
|
||||
serverForm.listenPort = data.listenPort || 0;
|
||||
serverForm.privateKey = data.privateKey || "";
|
||||
serverForm.publicKey = data.publicKey || "";
|
||||
serverForm.postUpScript = data.postUpScript || "";
|
||||
serverForm.preDownScript = data.preDownScript || "";
|
||||
serverForm.postDownScript = data.postDownScript || "";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 更新服务端信息
|
||||
const updateServerApi = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate((valid, fields) => {
|
||||
if (!valid) return fields;
|
||||
useServerStoreHook()
|
||||
.updateServerApi(serverForm)
|
||||
.then(res => {
|
||||
if (res.code === 200) {
|
||||
getServerApi();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 获取全局配置
|
||||
const getGlobalSettingApi = () => {
|
||||
useGlobalSettingStoreHook()
|
||||
.getGlobalSettingApi()
|
||||
.then(res => {
|
||||
if (res.code === 200) {
|
||||
const data = useGlobalSettingStoreHook().getGlobalSetting;
|
||||
configForm.endpointAddress = data.endpointAddress || "";
|
||||
configForm.dnsServer = data.dnsServer || [];
|
||||
configForm.MTU = data.MTU || 0;
|
||||
configForm.persistentKeepalive = data.persistentKeepalive || 0;
|
||||
configForm.firewallMark = data.firewallMark || "";
|
||||
configForm.table = data.table || "";
|
||||
configForm.configFilePath = data.configFilePath || "";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 更新全局配置
|
||||
const updateGlobalSettingApi = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate((valid, fields) => {
|
||||
if (!valid) return fields;
|
||||
useGlobalSettingStoreHook()
|
||||
.updateGlobalSettingApi(configForm)
|
||||
.then(res => {
|
||||
if (res.code === 200) {
|
||||
getGlobalSettingApi();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 获取当前主机的公网IP
|
||||
const getPublicIPApi = () => {
|
||||
getPublicIP().then(res => {
|
||||
if (res.code === 200) {
|
||||
useGlobalSettingStoreHook().SET_ENDPOINT_ADDRESS(res.data.IP);
|
||||
configForm.endpointAddress =
|
||||
useGlobalSettingStoreHook().getEndpointAddress;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
getServerApi();
|
||||
getGlobalSettingApi();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="10" style="padding-left: 50px">
|
||||
<el-card class="left-card" style="max-width: 800px">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>服务端信息</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="server-form">
|
||||
<el-form
|
||||
ref="serverFormRef"
|
||||
:model="serverForm"
|
||||
:rules="serverFormRules"
|
||||
label-position="top"
|
||||
>
|
||||
<el-form-item prop="ipScope" label="子网IP段">
|
||||
<el-input v-model="serverForm.ipScope" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="listenPort" label="监听端口">
|
||||
<el-input-number
|
||||
v-model="serverForm.listenPort"
|
||||
:min="49152"
|
||||
:max="65535"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="privateKey" label="私钥">
|
||||
<el-input v-model="serverForm.privateKey" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="publicKey" label="公钥">
|
||||
<el-input v-model="serverForm.publicKey" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="postUpScript" label="postUpScript">
|
||||
<el-input v-model="serverForm.postUpScript" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="postDownScript" label="postDownScript">
|
||||
<el-input v-model="serverForm.preDownScript" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="postDownScript" label="postDownScript">
|
||||
<el-input v-model="serverForm.postDownScript" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="updateServerApi(serverFormRef)"
|
||||
>确认</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="10" style="padding-left: 50px">
|
||||
<el-card style="max-width: 800px">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>全局配置信息</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form ref="configFormRef" :model="configForm" label-position="top">
|
||||
<el-form-item prop="endpointAddress" label="endpointAddress">
|
||||
<el-input v-model="configForm.endpointAddress" />
|
||||
<el-button
|
||||
size="small"
|
||||
type="warning"
|
||||
class="getIp"
|
||||
@click="getPublicIPApi"
|
||||
>获取IP</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
<el-form-item prop="dnsServer" label="dnsServer">
|
||||
<el-select
|
||||
v-model="configForm.dnsServer"
|
||||
:clearable="true"
|
||||
:reserve-keyword="false"
|
||||
suffix-icon=""
|
||||
tag-type="success"
|
||||
popper-class="options-class"
|
||||
placeholder=""
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="MTU" label="MTU">
|
||||
<el-input v-model="configForm.MTU" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="persistentKeepalive" label="persistentKeepalive">
|
||||
<el-input v-model="configForm.persistentKeepalive" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="firewallMark" label="firewallMark">
|
||||
<el-input v-model="configForm.firewallMark" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="table" label="table">
|
||||
<el-input v-model="configForm.table" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="configFilePath" label="configFilePath">
|
||||
<el-input v-model="configForm.configFilePath" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="updateGlobalSettingApi(configFormRef)"
|
||||
>确认</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.options-class {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.el-row {
|
||||
margin-bottom: 30px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.el-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.el-col {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.grid-content {
|
||||
min-height: 36px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.options-class {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.getIp {
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
22
web-src/src/views/server/utils/rules.ts
Normal file
22
web-src/src/views/server/utils/rules.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { reactive } from "vue";
|
||||
import type { FormRules } from "element-plus";
|
||||
|
||||
/** 登录校验 */
|
||||
const serverFormRules = reactive(<FormRules>{
|
||||
ipScope: [
|
||||
{
|
||||
required: true,
|
||||
message: "不能为空",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
listenPort: [
|
||||
{
|
||||
required: true,
|
||||
message: "不能为空",
|
||||
trigger: "blur"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export { serverFormRules };
|
119
web-src/src/views/user/component/form.vue
Normal file
119
web-src/src/views/user/component/form.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { FormInstance } from "element-plus";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { userKey } from "@/utils/auth";
|
||||
|
||||
// 声明 props 类型
|
||||
export interface FormProps {
|
||||
formInline: {
|
||||
id: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
account: string;
|
||||
email: string;
|
||||
isAdmin: number;
|
||||
status: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 声明 props 默认值
|
||||
// 推荐阅读:https://cn.vuejs.org/guide/typescript/composition-api.html#typing-component-props
|
||||
const props = withDefaults(defineProps<FormProps>(), {
|
||||
formInline: () => ({
|
||||
id: "",
|
||||
avatar: "",
|
||||
name: "",
|
||||
account: "",
|
||||
email: "",
|
||||
isAdmin: 0,
|
||||
status: 1
|
||||
})
|
||||
});
|
||||
|
||||
// vue 规定所有的 prop 都遵循着单向绑定原则,直接修改 prop 时,Vue 会抛出警告。此处的写法仅仅是为了消除警告。
|
||||
// 因为对一个 reactive 对象执行 ref,返回 Ref 对象的 value 值仍为传入的 reactive 对象,
|
||||
// 即 newFormInline === props.formInline 为 true,所以此处代码的实际效果,仍是直接修改 props.formInline。
|
||||
// 但该写法仅适用于 props.formInline 是一个对象类型的情况,原始类型需抛出事件
|
||||
// 推荐阅读:https://cn.vuejs.org/guide/components/props.html#one-way-data-flow
|
||||
const userEditForm = ref(props.formInline);
|
||||
const userEditFormRef = ref<FormInstance>();
|
||||
|
||||
function getUserEditFormRef() {
|
||||
return userEditFormRef.value;
|
||||
}
|
||||
|
||||
// 账号输入框是否禁用
|
||||
function isAccountDisabled() {
|
||||
return userEditForm.value.id !== "" && userEditForm.value.id !== undefined;
|
||||
}
|
||||
|
||||
// 是否为超管处理
|
||||
function isAdminDisabled() {
|
||||
// 只有当前用户是admin才能
|
||||
return storageLocal().getItem(userKey).account !== "admin";
|
||||
}
|
||||
|
||||
defineExpose({ getUserEditFormRef });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form ref="userEditFormRef" :model="userEditForm" label-width="20%">
|
||||
<el-form-item
|
||||
prop="name"
|
||||
label="名称"
|
||||
:rules="[{ required: true, message: '名称不能为空', trigger: 'blur' }]"
|
||||
>
|
||||
<el-input
|
||||
v-model="userEditForm.name"
|
||||
class="!w-[220px]"
|
||||
placeholder="名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
prop="account"
|
||||
label="账号"
|
||||
:rules="[{ required: true, message: '账号不能为空', trigger: 'blur' }]"
|
||||
>
|
||||
<el-input
|
||||
v-model="userEditForm.account"
|
||||
:disabled="isAccountDisabled()"
|
||||
class="!w-[220px]"
|
||||
placeholder="账号"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="email" label="邮箱">
|
||||
<el-input
|
||||
v-model="userEditForm.email"
|
||||
class="!w-[220px]"
|
||||
placeholder="邮箱"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
prop="isAdmin"
|
||||
label="超管"
|
||||
:rules="[
|
||||
{ required: true, message: '是否为超管不能为空', trigger: 'blur' }
|
||||
]"
|
||||
>
|
||||
<el-select
|
||||
v-model="userEditForm.isAdmin"
|
||||
:disabled="isAdminDisabled()"
|
||||
class="!w-[220px]"
|
||||
>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
prop="status"
|
||||
label="状态"
|
||||
:rules="[{ required: true, message: '状态不能为空', trigger: 'blur' }]"
|
||||
>
|
||||
<el-select v-model="userEditForm.status" class="!w-[220px]">
|
||||
<el-option label="禁用" :value="0" />
|
||||
<el-option label="启用" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
301
web-src/src/views/user/list.vue
Normal file
301
web-src/src/views/user/list.vue
Normal file
@@ -0,0 +1,301 @@
|
||||
<script setup lang="tsx">
|
||||
import {
|
||||
userList as userListApi,
|
||||
changeUserStatus as changeUserStatusApi,
|
||||
editUser as editUserApi,
|
||||
deleteUser as deleteUserApi
|
||||
} from "@/api/user";
|
||||
import { h, reactive, ref } from "vue";
|
||||
import { Delete, Edit } from "@element-plus/icons-vue";
|
||||
import { addDialog } from "@/components/ReDialog/index";
|
||||
import forms, { type FormProps } from "./component/form.vue";
|
||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
import AddFill from "@iconify-icons/ri/add-circle-line";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { userKey } from "@/utils/auth";
|
||||
import { message } from "@/utils/message";
|
||||
import useGetGlobalProperties from "@/hooks/useGetGlobalProperties";
|
||||
|
||||
defineOptions({
|
||||
// name 作为一种规范最好必须写上并且和路由的name保持一致
|
||||
name: "UserList"
|
||||
});
|
||||
|
||||
const { $bus } = useGetGlobalProperties();
|
||||
|
||||
// 编辑-模态框
|
||||
const userEditFormRef = ref();
|
||||
|
||||
// 查询表单
|
||||
const userListForm = {
|
||||
current: 1,
|
||||
size: 10
|
||||
};
|
||||
|
||||
let userListData = reactive({
|
||||
data: [],
|
||||
total: 0
|
||||
}); // 表格数据
|
||||
|
||||
// 定义用户列表接口方法
|
||||
const userList = () => {
|
||||
userListApi(userListForm).then(res => {
|
||||
if (res.code === 200) {
|
||||
userListData.data = res.data.records;
|
||||
userListData.total = res.data.total;
|
||||
userListForm.current = res.data.current;
|
||||
userListForm.size = res.data.size;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 定义分页相关事件
|
||||
const pageChange = (page: number, size: number) => {
|
||||
userListForm.size = size;
|
||||
userListForm.current = page;
|
||||
userList();
|
||||
};
|
||||
|
||||
// 定义用户状态变化接口
|
||||
const changeUserStatus = (status: number, userId: string) => {
|
||||
changeUserStatusApi({
|
||||
id: userId,
|
||||
status: status.toString()
|
||||
}).then(res => {
|
||||
if (res.code === 200) {
|
||||
userList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 打开编辑模态框
|
||||
const openEditDialog = (userInfo?: any) => {
|
||||
addDialog({
|
||||
width: "20%",
|
||||
title: userInfo.name,
|
||||
contentRenderer: () => h(forms, { ref: userEditFormRef }),
|
||||
props: {
|
||||
formInline: {
|
||||
id: userInfo.id,
|
||||
name: userInfo.name,
|
||||
avatar: userInfo.avatar,
|
||||
account: userInfo.account,
|
||||
email: userInfo.email,
|
||||
isAdmin: userInfo.isAdmin,
|
||||
status: userInfo.status
|
||||
}
|
||||
},
|
||||
beforeSure: (done, { options }) => {
|
||||
const FormRef = userEditFormRef.value.getUserEditFormRef();
|
||||
FormRef.validate(valid => {
|
||||
if (!valid) return;
|
||||
editUserApi(options.props.formInline).then(res => {
|
||||
if (res.code === 200) {
|
||||
done();
|
||||
userList();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 打开添加模态框
|
||||
const openAddDialog = () => {
|
||||
addDialog({
|
||||
width: "20%",
|
||||
title: "添加管理员",
|
||||
contentRenderer: () => h(forms, { ref: userEditFormRef }),
|
||||
props: {
|
||||
formInline: {
|
||||
isAdmin: 0,
|
||||
status: 1
|
||||
}
|
||||
},
|
||||
beforeSure: (done, { options }) => {
|
||||
const FormRef = userEditFormRef.value.getUserEditFormRef();
|
||||
FormRef.validate(valid => {
|
||||
if (!valid) return;
|
||||
editUserApi(options.props.formInline).then(res => {
|
||||
if (res.code === 200) {
|
||||
done();
|
||||
userList();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 删除用户
|
||||
const userDelete = (userId: string) => {
|
||||
if (userId !== "" || userId !== undefined) {
|
||||
deleteUserApi(userId).then(res => {
|
||||
if (res.code === 200) {
|
||||
userList();
|
||||
} else {
|
||||
message(res.message, { type: "error" });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑和删除按钮的显示与否
|
||||
const deleteOrEditBtnDisable = (userInfo?: object) => {
|
||||
const loginUser = storageLocal().getItem(userKey);
|
||||
// 登陆用户是否为超级管理员
|
||||
if (loginUser.isAdmin !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 是否删除的自身
|
||||
if (loginUser.id === userInfo.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果当前被删除用户是超管则需要登陆用户是宇宙无敌管理员
|
||||
return !(userInfo.isAdmin === 1 && loginUser.account !== "admin");
|
||||
};
|
||||
|
||||
$bus.on("userListData", value => {
|
||||
userListData.data = value.data.records;
|
||||
userListData.total = value.data.total;
|
||||
userListForm.current = value.data.current;
|
||||
userListForm.size = value.data.size;
|
||||
});
|
||||
// 调用接口
|
||||
userList(); // 用户列表接口
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user-list-table">
|
||||
<div class="user-list-table-header">
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="useRenderIcon(AddFill)"
|
||||
@click="openAddDialog"
|
||||
>
|
||||
添加
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="list">
|
||||
<el-table :data="userListData.data" :border="true" style="width: 100%">
|
||||
<el-table-column prop="id" label="id" min-width="125" align="center" />
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="名称"
|
||||
min-width="80"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="avatar"
|
||||
label="头像"
|
||||
min-width="35"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<img class="table-avatar" :src="scope.row.avatar" alt="头像" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="account" label="账号" align="center" />
|
||||
<el-table-column prop="email" label="邮箱" align="center" />
|
||||
<el-table-column
|
||||
prop="isAdmin"
|
||||
label="是否为超级管理员"
|
||||
min-width="60"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.isAdmin === 1" effect="dark">是</el-tag>
|
||||
<el-tag v-else effect="dark" type="warning">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="status"
|
||||
label="状态"
|
||||
min-width="70"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.status"
|
||||
:disabled="!deleteOrEditBtnDisable(scope.row)"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="changeUserStatus(scope.row.status, scope.row.id)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" align="center" />
|
||||
<el-table-column prop="updatedAt" label="更新时间" align="center" />
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="deleteOrEditBtnDisable(scope.row)"
|
||||
size="small"
|
||||
type="primary"
|
||||
:icon="Edit"
|
||||
@click="openEditDialog(scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-popconfirm
|
||||
v-if="deleteOrEditBtnDisable(scope.row)"
|
||||
width="220"
|
||||
confirm-button-text="确认"
|
||||
cancel-button-text="取消"
|
||||
icon-color="#626AEF"
|
||||
title="是否删除?"
|
||||
@confirm="userDelete(scope.row.id)"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger" :icon="Delete">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="div-page">
|
||||
<el-pagination
|
||||
small
|
||||
background
|
||||
layout="total,prev,pager,next"
|
||||
:page-size="userListForm.size"
|
||||
:total="userListData.total"
|
||||
@change="pageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.table-avatar {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-list-form .el-input {
|
||||
--el-input-width: 220px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.user-list-table-header {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.div-page {
|
||||
padding: 20px;
|
||||
text-align: right;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user