🎨合并编译打包

This commit is contained in:
2024-06-04 09:55:48 +08:00
parent e1f168b274
commit 6d4f4b2e02
275 changed files with 17590 additions and 951 deletions

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

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

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

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

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

View 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
}
}
}
]
]
);
}
});

View 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 };

View 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 };

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

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

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

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

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

View 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 };

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

View 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 };

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

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