🎉新版本页面
This commit is contained in:
parent
00f3d51c1d
commit
89c89a28ee
2
.env
2
.env
@ -1,3 +1,3 @@
|
||||
VITE_TITLE = 'Vue Naive Admin'
|
||||
VITE_TITLE = 'Wireguard-UI'
|
||||
|
||||
VITE_PORT = 3100
|
||||
|
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
65
.idea/codeStyles/Project.xml
generated
Normal file
65
.idea/codeStyles/Project.xml
generated
Normal file
@ -0,0 +1,65 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="LINE_SEPARATOR" value=" " />
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<editorconfig>
|
||||
<option name="ENABLED" value="false" />
|
||||
</editorconfig>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
6
.idea/git_toolbox_blame.xml
generated
Normal file
6
.idea/git_toolbox_blame.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxBlameSettings">
|
||||
<option name="version" value="2" />
|
||||
</component>
|
||||
</project>
|
21
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
21
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,21 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="myValues">
|
||||
<value>
|
||||
<list size="7">
|
||||
<item index="0" class="java.lang.String" itemvalue="nobr" />
|
||||
<item index="1" class="java.lang.String" itemvalue="noembed" />
|
||||
<item index="2" class="java.lang.String" itemvalue="comment" />
|
||||
<item index="3" class="java.lang.String" itemvalue="noscript" />
|
||||
<item index="4" class="java.lang.String" itemvalue="embed" />
|
||||
<item index="5" class="java.lang.String" itemvalue="script" />
|
||||
<item index="6" class="java.lang.String" itemvalue="content" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myCustomValuesEnabled" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/wireguard-ui.iml" filepath="$PROJECT_DIR$/.idea/wireguard-ui.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
12
.idea/wireguard-ui.iml
generated
Normal file
12
.idea/wireguard-ui.iml
generated
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
@ -7,7 +7,7 @@ export const PROXY_CONFIG = {
|
||||
* @转发路径 http://localhost:8080/user
|
||||
*/
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
target: 'http://localhost:6687/api',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(new RegExp('^/api'), ''),
|
||||
},
|
||||
|
@ -66,7 +66,7 @@
|
||||
"fs-extra": "^11.1.1",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^14.0.1",
|
||||
"naive-ui": "^2.35.0",
|
||||
"naive-ui": "^2.39.0",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"sass": "^1.69.0",
|
||||
"unocss": "0.55.3",
|
||||
|
9649
pnpm-lock.yaml
generated
9649
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +0,0 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getUser: () => request.get('/user'),
|
||||
refreshToken: () => request.post('/auth/refreshToken', null, { noNeedTip: true }),
|
||||
}
|
9
src/api/user.js
Normal file
9
src/api/user.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getUser: () => request.get('/user/info'), // 获取当前登陆用户信息
|
||||
logout: () => request.post('/user/logout'), // 退出登陆
|
||||
changePassword: (data) => request.put('/user/change-password',data), // 修改密码
|
||||
generateAvatar: () => request.post('/user/generate-avatar'), // 生成头像
|
||||
addOrUpdateUser: (data) => request.post('/user',data), // 新增/编辑用户信息
|
||||
}
|
@ -2,22 +2,22 @@
|
||||
<footer f-c-c flex-col text-14 color="#6a6a6a">
|
||||
<p>
|
||||
Copyright © 2022-present
|
||||
<a
|
||||
href="https://github.com/zclzone"
|
||||
target="__blank"
|
||||
hover="decoration-underline color-primary"
|
||||
>
|
||||
Ronnie Zhang
|
||||
</a>
|
||||
<!-- <a-->
|
||||
<!-- href="https://github.com/zclzone"-->
|
||||
<!-- target="__blank"-->
|
||||
<!-- hover="decoration-underline color-primary"-->
|
||||
<!-- >-->
|
||||
<!-- Ronnie Zhang-->
|
||||
<!-- </a>-->
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
href="http://beian.miit.gov.cn/"
|
||||
target="__blank"
|
||||
hover="decoration-underline color-primary"
|
||||
>
|
||||
赣ICP备2020015008号-2
|
||||
</a>
|
||||
<!-- <a-->
|
||||
<!-- href="http://beian.miit.gov.cn/"-->
|
||||
<!-- target="__blank"-->
|
||||
<!-- hover="decoration-underline color-primary"-->
|
||||
<!-- >-->
|
||||
<!-- 赣ICP备2020015008号-2-->
|
||||
<!-- </a>-->
|
||||
</p>
|
||||
</footer>
|
||||
</template>
|
||||
|
229
src/components/page/ClientListHeaderPage.vue
Normal file
229
src/components/page/ClientListHeaderPage.vue
Normal file
@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<AppPage :show-footer="showFooter">
|
||||
<header v-if="showHeader" mt-10 mb-20 min-h-45 flex items-center justify-between px-15>
|
||||
<slot v-if="$slots.header" name="header" />
|
||||
<template v-else>
|
||||
<n-card style="margin-left: 1.65%; width: 97.25%">
|
||||
<n-space justify="end">
|
||||
<n-button size="small" style="float: right" type="info" @click="addClient">添加</n-button>
|
||||
<n-button size="small" style="float: right" type="primary" @click="refreshList">刷新</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
<slot name="action" />
|
||||
</template>
|
||||
</header>
|
||||
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</AppPage>
|
||||
<n-modal
|
||||
style="width: 25%"
|
||||
v-model:show="addModalShow"
|
||||
preset="card"
|
||||
title="添加"
|
||||
header-style="margin-left: 40%"
|
||||
>
|
||||
<n-form
|
||||
ref="addModalFormRef"
|
||||
:rules="addModalFormRules"
|
||||
:model="addModalForm"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
label-align="left"
|
||||
require-mark-placement="right"
|
||||
>
|
||||
<n-form-item label="名称" path="name">
|
||||
<n-input v-model:value="addModalForm.name"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="邮箱" path="email">
|
||||
<n-input v-model:value="addModalForm.email"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="IP" path="addModalForm.ipAllocation">
|
||||
<n-select
|
||||
v-model:value="addModalForm.ipAllocation"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
placeholder="输入,按回车确认"
|
||||
:show-arrow="false"
|
||||
:show="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="可访问IP" path="allowedIps">
|
||||
<n-select
|
||||
v-model:value="addModalForm.allowedIps"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
placeholder="输入,按回车确认"
|
||||
:show-arrow="false"
|
||||
:show="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="可访问IP(扩展)" path="extraAllowedIps">
|
||||
<n-select
|
||||
v-model:value="addModalForm.extraAllowedIps"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
placeholder="输入,按回车确认"
|
||||
:show-arrow="false"
|
||||
:show="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="服务端DNS" path="useServerDns">
|
||||
<n-radio value="1" :checked="addModalForm.useServerDns === 1" @change="addModalForm.useServerDns = 1">是</n-radio>
|
||||
<n-radio value="0" :checked="addModalForm.useServerDns === 0" @change="addModalForm.useServerDns = 0">否</n-radio>
|
||||
</n-form-item>
|
||||
<n-form-item label="公钥" path="keys.publicKey">
|
||||
<n-input v-model:value="addModalForm.keys.publicKey"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="私钥" path="keys.privateKey">
|
||||
<n-input v-model:value="addModalForm.keys.privateKey"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="共享密钥" path="keys.presharedKey">
|
||||
<n-input v-model:value="addModalForm.keys.presharedKey"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button style="margin-left: 28%" size="small" type="info" @click="generateKeys">生成密钥对</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item label="状态" path="editModalForm.enabled">
|
||||
<n-radio-group :value="addModalForm.enabled">
|
||||
<n-radio :value="1" :checked="addModalForm.enabled === 1" @change="addModalForm.enabled = 1">启用</n-radio>
|
||||
<n-radio :value="0" :checked="addModalForm.enabled === 0" @change="addModalForm.enabled = 0">禁用</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<n-form-item label="离线监听" path="offlineMonitoring">
|
||||
<n-radio-group :value="addModalForm.offlineMonitoring">
|
||||
<n-radio :value="1" :checked="addModalForm.offlineMonitoring === 1" @change="addModalForm.offlineMonitoring = 1">启用</n-radio>
|
||||
<n-radio :value="0" :checked="addModalForm.offlineMonitoring === 0" @change="addModalForm.offlineMonitoring = 0">禁用</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<n-button type="info" style="margin-left: 40%" @click="confirmAddClient()">确认</n-button>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import clientApi from '@/views/client/api'
|
||||
import { NButton } from 'naive-ui'
|
||||
import { debounce } from '@/utils'
|
||||
defineProps({
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
const route = useRoute()
|
||||
|
||||
const addModalShow = ref(false)
|
||||
|
||||
|
||||
// 添加模态框的form ref
|
||||
const addModalFormRef = ref()
|
||||
|
||||
// 添加模态框的form rules
|
||||
const addModalFormRules = {
|
||||
name: [
|
||||
{ required: true, message: '名称不能为空', trigger: 'blur' }
|
||||
],
|
||||
ipAllocation: [
|
||||
{ required: true, message: '客户端IP不能为空', trigger: 'change' }
|
||||
],
|
||||
keys: {
|
||||
privateKey: [
|
||||
{ required: true, message: '密钥不能为空', trigger: 'blur' }
|
||||
],
|
||||
publicKey: [
|
||||
{ required: true, message: '公钥不能为空', trigger: 'blur' }
|
||||
],
|
||||
presharedKey: [
|
||||
{ required: true, message: '共享密钥不能为空', trigger: 'blur' }
|
||||
],
|
||||
},
|
||||
enabled: [
|
||||
{ required: true, message: '状态不能为空', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 添加模态框的数据集
|
||||
const addModalForm = ref({
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
ipAllocation: [],
|
||||
allowedIps: [],
|
||||
extraAllowedIps: [],
|
||||
useServerDns: 0,
|
||||
keys: {
|
||||
privateKey: '',
|
||||
publicKey: '',
|
||||
presharedKey: ''
|
||||
},
|
||||
enabled: 1,
|
||||
offlineMonitoring: 1
|
||||
})
|
||||
|
||||
// 点击确认按钮
|
||||
async function confirmAddClient() {
|
||||
addModalFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
return valid
|
||||
}
|
||||
|
||||
clientApi.saveClient(addModalForm.value).then((response) => {
|
||||
if (response.data.code === 200) {
|
||||
refreshList()
|
||||
addModalShow.value = false
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 添加客户端
|
||||
const addClient = debounce(() => {
|
||||
generateClientIPS()
|
||||
addModalShow.value = true
|
||||
},300)
|
||||
|
||||
// 生成密钥对
|
||||
function generateKeys() {
|
||||
clientApi.generateClientKeys().then(res => {
|
||||
if (res.data.code === 200) {
|
||||
addModalForm.value.keys.privateKey = res.data.data.privateKey
|
||||
addModalForm.value.keys.publicKey = res.data.data.publicKey
|
||||
addModalForm.value.keys.presharedKey = res.data.data.presharedKey
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生成客户端IP
|
||||
function generateClientIPS() {
|
||||
clientApi.generateClientIP().then(res => {
|
||||
if (res.data.code === 200) {
|
||||
addModalForm.value.ipAllocation = res.data.data.clientIPS
|
||||
addModalForm.value.allowedIps = res.data.data.serverIPS
|
||||
generateKeys()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
function refreshList() {
|
||||
clientApi.clientList({
|
||||
current: 1,
|
||||
size: 8
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<AppPage :show-footer="showFooter">
|
||||
<header v-if="showHeader" mb-15 min-h-45 flex items-center justify-between px-15>
|
||||
<header v-if="showHeader" mt-10 mb-20 min-h-45 flex items-center justify-between px-15>
|
||||
<slot v-if="$slots.header" name="header" />
|
||||
<template v-else>
|
||||
<h2 text-22 font-normal text-hex-333 dark:text-hex-ccc>{{ title || route.meta?.title }}</h2>
|
||||
<h3>{{ title }}</h3>
|
||||
<slot name="action" />
|
||||
</template>
|
||||
</header>
|
||||
|
||||
<n-card flex-1 rounded-10>
|
||||
<div>
|
||||
<slot />
|
||||
</n-card>
|
||||
</div>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
|
@ -10,14 +10,14 @@
|
||||
p-15
|
||||
bc-ccc
|
||||
dark:bg-black
|
||||
h-60
|
||||
>
|
||||
<n-space wrap :size="[35, 15]">
|
||||
<slot />
|
||||
</n-space>
|
||||
|
||||
<div flex-shrink-0>
|
||||
<n-button secondary type="primary" @click="emit('reset')">重置</n-button>
|
||||
<n-button ml-20 type="primary" @click="emit('search')">搜索</n-button>
|
||||
<n-button ml-20 size="small" type="primary" @click="emit('search')">搜索</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -2,36 +2,217 @@
|
||||
<n-dropdown :options="options" @select="handleSelect">
|
||||
<div flex cursor-pointer items-center>
|
||||
<img :src="userStore.avatar" mr10 h-35 w-35 rounded-full />
|
||||
<span>{{ userStore.name }}</span>
|
||||
<span>{{ userStore.nickname }}</span>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
<n-modal
|
||||
v-model:show="showChangePwdModel"
|
||||
transform-origin="center"
|
||||
preset="card"
|
||||
title="修改密码"
|
||||
:bordered="false"
|
||||
size="large"
|
||||
style="width: 400px"
|
||||
header-style="text-align: center"
|
||||
>
|
||||
<n-form label-placement="top"
|
||||
ref="changePasswordFormRef"
|
||||
:rules="changePasswordFormRules"
|
||||
:model="changePasswordFormModel">
|
||||
<n-form-item label="原密码" path="originalPassword">
|
||||
<n-input type="password" show-password-on="click" v-model:value="changePasswordFormModel.originalPassword"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="新密码" path="newPassword">
|
||||
<n-input type="password" show-password-on="click" v-model:value="changePasswordFormModel.newPassword"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="确认密码" path="confirmPassword">
|
||||
<n-input type="password" show-password-on="click" v-model:value="changePasswordFormModel.confirmPassword"></n-input>
|
||||
</n-form-item>
|
||||
<n-button type="primary" @click="changePasswordHandle()">确认</n-button>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
<n-modal
|
||||
v-model:show="showInfoModel"
|
||||
transform-origin="center"
|
||||
preset="card"
|
||||
:title="infoFormModel.nickname || '个人资料'"
|
||||
:bordered="false"
|
||||
size="large"
|
||||
style="width: 400px"
|
||||
header-style="text-align: center"
|
||||
>
|
||||
<n-form label-placement="top"
|
||||
ref="infoFormRef"
|
||||
:rules="infoFormRules"
|
||||
:model="infoFormModel">
|
||||
<n-form-item path="avatar">
|
||||
<n-avatar
|
||||
round
|
||||
style="cursor: pointer;margin-left: 36%"
|
||||
:size="78"
|
||||
:src="showAvatar"
|
||||
@click="changeAvatar()"
|
||||
></n-avatar>
|
||||
</n-form-item>
|
||||
<n-form-item label="账号" path="account">
|
||||
<n-input disabled v-model:value="infoFormModel.account"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="昵称" path="nickname">
|
||||
<n-input v-model:value="infoFormModel.nickname"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="联系方式" path="contact">
|
||||
<n-input v-model:value="infoFormModel.contact"></n-input>
|
||||
</n-form-item>
|
||||
<n-button type="primary" @click="updateInfo()">确认</n-button>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserStore } from '@/store'
|
||||
import { renderIcon } from '@/utils'
|
||||
import api from '@/api/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: "个人信息",
|
||||
key: 'info',
|
||||
icon: renderIcon('streamline:information-desk-solid', { size: '14px' })
|
||||
},
|
||||
{
|
||||
label: "修改密码",
|
||||
key: 'change-password',
|
||||
icon: renderIcon('mdi:lock', { size: '14px' }),
|
||||
},
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'logout',
|
||||
icon: renderIcon('mdi:exit-to-app', { size: '14px' }),
|
||||
},
|
||||
}
|
||||
]
|
||||
const showAvatar = ref('')
|
||||
|
||||
const showChangePwdModel = ref(false)
|
||||
const showInfoModel = ref(false)
|
||||
|
||||
const changePasswordFormRef = ref()
|
||||
const infoFormRef = ref()
|
||||
|
||||
// 表单数据值
|
||||
const changePasswordFormModel = ref({
|
||||
originalPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
// 用户信息表单
|
||||
const infoFormModel = ref({
|
||||
id: '',
|
||||
account: '',
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
contact: '',
|
||||
isAdmin: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
// 表单数据验证规则
|
||||
const changePasswordFormRules = {
|
||||
originalPassword: [
|
||||
{ required: true, message: '原密码不能为空', trigger: 'blur' },
|
||||
{ min: 8, message: '密码最低8位', trigger: 'blur' },
|
||||
{ max: 32, message: '密码最长32位', trigger: 'blur' }
|
||||
],
|
||||
newPassword: [
|
||||
{ required: true, message: '新密码不能为空', trigger: 'blur' },
|
||||
{ min: 8, message: '密码最低8位', trigger: 'blur' },
|
||||
{ max: 32, message: '密码最长32位', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ validator: validatePasswordSame, message: '密码不一致', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 个人信息表单验证规则
|
||||
const infoFormRules = {
|
||||
avatar: [
|
||||
{ required: true, message: '头像不能为空', trigger: 'blur' }
|
||||
],
|
||||
nickname: [
|
||||
{ required: true, message: '昵称不能为空', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 验证密码是否一致
|
||||
function validatePasswordSame(rule, value) {
|
||||
return value === changePasswordFormModel.value.newPassword;
|
||||
}
|
||||
|
||||
// 根据不同点击事件触发不同
|
||||
function handleSelect(key) {
|
||||
if (key === 'logout') {
|
||||
$dialog.confirm({
|
||||
title: '提示',
|
||||
type: 'info',
|
||||
content: '确认退出?',
|
||||
confirm() {
|
||||
userStore.logout()
|
||||
$message.success('已退出登录')
|
||||
},
|
||||
})
|
||||
switch (key) {
|
||||
case 'logout':
|
||||
$dialog.confirm({
|
||||
title: '提示',
|
||||
type: 'info',
|
||||
content: '确认退出?',
|
||||
confirm() {
|
||||
userStore.logout()
|
||||
$message.success('已退出登录')
|
||||
},
|
||||
})
|
||||
break;
|
||||
case 'change-password':
|
||||
showChangePwdModel.value = true
|
||||
break;
|
||||
case 'info':
|
||||
infoFormModel.value = useUserStore().localUserInfo
|
||||
showAvatar.value = infoFormModel.value.avatar
|
||||
showInfoModel.value = true
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 修改密码逻辑
|
||||
async function changePasswordHandle() {
|
||||
changePasswordFormRef.value.validate(async (errors) => {
|
||||
if (errors) {
|
||||
return errors
|
||||
}
|
||||
const res = await api.changePassword(changePasswordFormModel.value)
|
||||
if (res.data.code === 200) {
|
||||
changePasswordFormModel.value = ref(null)
|
||||
showChangePwdModel.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生成头像
|
||||
async function changeAvatar() {
|
||||
const res = await api.generateAvatar()
|
||||
if (res.data.code === 200) {
|
||||
showAvatar.value = res.data.data
|
||||
}
|
||||
}
|
||||
|
||||
async function updateInfo() {
|
||||
infoFormRef.value.validate(async (errors) => {
|
||||
if (errors) {
|
||||
return errors
|
||||
}
|
||||
|
||||
if (showAvatar.value !== infoFormModel.value.avatar) {
|
||||
infoFormModel.value.avatar = showAvatar.value
|
||||
}
|
||||
|
||||
const res = await api.addOrUpdateUser(infoFormModel.value)
|
||||
if (res.data.code === 200) {
|
||||
infoFormModel.value = ref(null)
|
||||
showInfoModel.value = false
|
||||
await useUserStore().getUserInfo()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
@ -4,10 +4,6 @@
|
||||
<BreadCrumb ml-15 hidden sm:block />
|
||||
</div>
|
||||
<div ml-auto flex items-center>
|
||||
<MessageNotification />
|
||||
<ThemeMode />
|
||||
<GiteeSite />
|
||||
<GithubSite />
|
||||
<FullScreen />
|
||||
<UserAvatar />
|
||||
</div>
|
||||
@ -18,8 +14,4 @@ import BreadCrumb from './components/BreadCrumb.vue'
|
||||
import MenuCollapse from './components/MenuCollapse.vue'
|
||||
import FullScreen from './components/FullScreen.vue'
|
||||
import UserAvatar from './components/UserAvatar.vue'
|
||||
import GithubSite from './components/GithubSite.vue'
|
||||
import GiteeSite from './components/GiteeSite.vue'
|
||||
import ThemeMode from './components/ThemeMode.vue'
|
||||
import MessageNotification from './components/MessageNotification.vue'
|
||||
</script>
|
||||
|
@ -4,7 +4,7 @@
|
||||
bordered
|
||||
collapse-mode="width"
|
||||
:collapsed-width="64"
|
||||
:width="220"
|
||||
:width="200"
|
||||
:native-scrollbar="false"
|
||||
:collapsed="appStore.collapsed"
|
||||
>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getToken, refreshAccessToken, isNullOrWhitespace } from '@/utils'
|
||||
import { getToken, isNullOrWhitespace } from '@/utils'
|
||||
|
||||
const WHITE_LIST = ['/login', '/404']
|
||||
export function createPermissionGuard(router) {
|
||||
@ -14,7 +14,6 @@ export function createPermissionGuard(router) {
|
||||
/** 有token的情况 */
|
||||
if (to.path === '/login') return { path: '/' }
|
||||
|
||||
refreshAccessToken()
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
@ -28,41 +28,25 @@ export async function resetRouter() {
|
||||
}
|
||||
|
||||
export async function addDynamicRoutes() {
|
||||
// return Promise.reject('123')
|
||||
const token = getToken()
|
||||
|
||||
// 没有token情况
|
||||
if (isNullOrWhitespace(token)) {
|
||||
router.addRoute(EMPTY_ROUTE)
|
||||
return
|
||||
}
|
||||
|
||||
// 有token的情况
|
||||
const userStore = useUserStore()
|
||||
try {
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
!userStore.userId && (await userStore.getUserInfo())
|
||||
!userStore.id && (await userStore.getUserInfo())
|
||||
|
||||
const accessRoutes = permissionStore.generateRoutes(userStore.role)
|
||||
accessRoutes.forEach((route) => {
|
||||
!router.hasRoute(route.name) && router.addRoute(route)
|
||||
})
|
||||
router.hasRoute(EMPTY_ROUTE.name) && router.removeRoute(EMPTY_ROUTE.name)
|
||||
router.addRoute(NOT_FOUND_ROUTE)
|
||||
|
||||
window.$notification?.success({
|
||||
title: '🎉🎉🎉 2.0 全栈版本开源了!',
|
||||
content: () =>
|
||||
h(
|
||||
'span',
|
||||
{},
|
||||
'2.0为全栈版本,提供前端+后端,全新重构,全面简化,',
|
||||
h(
|
||||
'a',
|
||||
{ href: 'https://admin.isme.top', target: '__blank' },
|
||||
'👉体验 https://admin.isme.top'
|
||||
)
|
||||
),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$message.error('初始化用户信息失败: ' + error)
|
||||
|
@ -1,13 +1,6 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export const basicRoutes = [
|
||||
{
|
||||
name: '404',
|
||||
path: '/404',
|
||||
component: () => import('@/views/error-page/404.vue'),
|
||||
isHidden: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Login',
|
||||
path: '/login',
|
||||
@ -16,44 +9,7 @@ export const basicRoutes = [
|
||||
meta: {
|
||||
title: '登录页',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'ExternalLink',
|
||||
path: '/external-link',
|
||||
component: Layout,
|
||||
meta: {
|
||||
title: '外部链接',
|
||||
icon: 'mdi:link-variant',
|
||||
order: 4,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'LinkGithubSrc',
|
||||
path: 'https://github.com/zclzone/vue-naive-admin',
|
||||
meta: {
|
||||
title: '源码 - github',
|
||||
icon: 'mdi:github',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'LinkGiteeSrc',
|
||||
path: 'https://gitee.com/zclzone/vue-naive-admin',
|
||||
meta: {
|
||||
title: '源码 - gitee',
|
||||
icon: 'simple-icons:gitee',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'LinkDocs',
|
||||
path: 'https://zclzone.github.io/vue-naive-admin-docs',
|
||||
meta: {
|
||||
title: '文档 - vuepress',
|
||||
icon: 'mdi:vuejs',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
export const NOT_FOUND_ROUTE = {
|
||||
|
@ -2,34 +2,54 @@ import { defineStore } from 'pinia'
|
||||
import { resetRouter } from '@/router'
|
||||
import { useTagsStore, usePermissionStore } from '@/store'
|
||||
import { removeToken, toLogin } from '@/utils'
|
||||
import api from '@/api'
|
||||
import api from '@/api/user'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state() {
|
||||
return {
|
||||
userInfo: {},
|
||||
userInfo: {
|
||||
id: '',
|
||||
account: '',
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
contact: '',
|
||||
isAdmin: 0,
|
||||
status: 1
|
||||
},
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
userId() {
|
||||
id() {
|
||||
return this.userInfo?.id
|
||||
},
|
||||
name() {
|
||||
return this.userInfo?.name
|
||||
account() {
|
||||
return this.userInfo?.account
|
||||
},
|
||||
nickname() {
|
||||
return this.userInfo?.nickname
|
||||
},
|
||||
avatar() {
|
||||
return this.userInfo?.avatar
|
||||
},
|
||||
role() {
|
||||
return this.userInfo?.role || []
|
||||
contact() {
|
||||
return this.userInfo?.contact
|
||||
},
|
||||
isAdmin() {
|
||||
return this.userInfo?.isAdmin
|
||||
},
|
||||
status() {
|
||||
return this.userInfo?.status
|
||||
},
|
||||
localUserInfo() {
|
||||
return this.userInfo
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async getUserInfo() {
|
||||
try {
|
||||
const res = await api.getUser()
|
||||
const { id, name, avatar, role } = res.data
|
||||
this.userInfo = { id, name, avatar, role }
|
||||
const { id, account,nickname, avatar,contact,isAdmin,status } = res.data.data
|
||||
this.userInfo = { id, account,nickname, avatar, contact,isAdmin,status }
|
||||
return Promise.resolve(res.data)
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
|
@ -1,33 +1,29 @@
|
||||
import { lStorage } from '@/utils'
|
||||
import api from '@/api'
|
||||
|
||||
const TOKEN_CODE = 'access_token'
|
||||
const DURATION = 6 * 60 * 60
|
||||
const TOKEN_CODE = 'token'
|
||||
const X_TOKEN_CODE = 'X_TOKEN_CODE'
|
||||
|
||||
export function getToken() {
|
||||
return lStorage.get(TOKEN_CODE)
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
lStorage.set(TOKEN_CODE, token, DURATION)
|
||||
export function getTokenAll() {
|
||||
return lStorage.getItem(TOKEN_CODE)
|
||||
}
|
||||
|
||||
export function getXToken() {
|
||||
return lStorage.get(X_TOKEN_CODE)
|
||||
}
|
||||
|
||||
export function setXToken(token) {
|
||||
lStorage.set(X_TOKEN_CODE,token)
|
||||
}
|
||||
|
||||
export function setToken(token,expireAt) {
|
||||
lStorage.set(TOKEN_CODE, token,expireAt)
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
lStorage.remove(TOKEN_CODE)
|
||||
}
|
||||
|
||||
export async function refreshAccessToken() {
|
||||
const tokenItem = lStorage.getItem(TOKEN_CODE)
|
||||
if (!tokenItem) {
|
||||
return
|
||||
}
|
||||
const { time } = tokenItem
|
||||
// token生成或者刷新后30分钟内不执行刷新
|
||||
if (Date.now() - time <= 1000 * 60 * 30) return
|
||||
try {
|
||||
const res = await api.refreshToken()
|
||||
setToken(res.data.token)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
@ -88,3 +88,12 @@ export function useResize(el, cb) {
|
||||
observer.observe(el)
|
||||
return observer
|
||||
}
|
||||
|
||||
// 超长省略号显示
|
||||
export function ellipsis(str) {
|
||||
if (!str) return "";
|
||||
if (str.length >= 10) {
|
||||
return str.slice(0, 10) + "...";
|
||||
}
|
||||
return str;
|
||||
}
|
@ -2,7 +2,7 @@ import { useUserStore } from '@/store'
|
||||
|
||||
export function addBaseParams(params) {
|
||||
if (!params.userId) {
|
||||
params.userId = useUserStore().userId
|
||||
params.userId = useUserStore().id
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ export function resolveResError(code, message) {
|
||||
break
|
||||
case 500:
|
||||
message = message ?? '服务器异常'
|
||||
useUserStore().logout()
|
||||
break
|
||||
default:
|
||||
message = message ?? `【${code}】: 未知异常!`
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getToken } from '@/utils'
|
||||
import { getToken, getTokenAll, getXToken, setToken, setXToken } from '@/utils'
|
||||
import { resolveResError } from './helpers'
|
||||
|
||||
export function reqResolve(config) {
|
||||
@ -12,11 +12,16 @@ export function reqResolve(config) {
|
||||
return Promise.reject({ code: 401, message: '登录已过期,请重新登录!' })
|
||||
}
|
||||
|
||||
// if (expire < Date.now()) {
|
||||
// return Promise.reject({ code: 401, message: '登录已过期,请重新登录!' })
|
||||
// }
|
||||
|
||||
/**
|
||||
* * 加上 token
|
||||
* ! 认证方案: JWT Bearer
|
||||
*/
|
||||
config.headers.Authorization = config.headers.Authorization || 'Bearer ' + token
|
||||
config.headers.Authorization = config.headers.Authorization || token
|
||||
config.headers.set('X-TOKEN', getXToken())
|
||||
|
||||
return config
|
||||
}
|
||||
@ -26,9 +31,15 @@ export function reqReject(error) {
|
||||
}
|
||||
|
||||
export function resResolve(response) {
|
||||
// 设置token
|
||||
if (response.headers.get('Authorization')) {
|
||||
const { expire } = getTokenAll()
|
||||
setToken(response.headers.get('Authorization'),expire / 1000)
|
||||
setXToken(response.headers.get('X-TOKEN'))
|
||||
}
|
||||
// TODO: 处理不同的 response.headers
|
||||
const { data, status, config, statusText } = response
|
||||
if (data?.code !== 0) {
|
||||
if (data?.code !== 200 && status !== 200) {
|
||||
const code = data?.code ?? status
|
||||
|
||||
/** 根据code处理对应的操作,并返回处理后的message */
|
||||
@ -38,7 +49,8 @@ export function resResolve(response) {
|
||||
!config.noNeedTip && window.$message?.error(message)
|
||||
return Promise.reject({ code, message, error: data || response })
|
||||
}
|
||||
return Promise.resolve(data)
|
||||
|
||||
return Promise.resolve(response)
|
||||
}
|
||||
|
||||
export function resReject(error) {
|
||||
@ -49,6 +61,11 @@ export function resReject(error) {
|
||||
window.$message?.error(message)
|
||||
return Promise.reject({ code, message, error })
|
||||
}
|
||||
if (error.response.headers.get('Authorization')) {
|
||||
const { expire } = getTokenAll()
|
||||
setToken(error.response.headers.get('Authorization'),expire / 1000)
|
||||
setXToken(error.response.headers.get('X-TOKEN'))
|
||||
}
|
||||
const { data, status, config } = error.response
|
||||
const code = data?.code ?? status
|
||||
const message = resolveResError(code, data?.message ?? error.message)
|
||||
|
@ -14,7 +14,7 @@ class Storage {
|
||||
const stringData = JSON.stringify({
|
||||
value,
|
||||
time: Date.now(),
|
||||
expire: !isNullOrUndef(expire) ? Date.now() + expire * 1000 : null,
|
||||
expire: !isNullOrUndef(expire) ? expire * 1000 : null,
|
||||
})
|
||||
this.storage.setItem(this.getKey(key), stringData)
|
||||
}
|
||||
@ -31,7 +31,7 @@ class Storage {
|
||||
const data = JSON.parse(val)
|
||||
const { value, time, expire } = data
|
||||
if (isNullOrUndef(expire) || expire > Date.now()) {
|
||||
return { value, time }
|
||||
return { value, time, expire }
|
||||
}
|
||||
this.remove(key)
|
||||
return def
|
||||
|
10
src/views/client/api.js
Normal file
10
src/views/client/api.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
clientList: (params) => request.get('/client/list',{ params }), // 客户端列表
|
||||
downloadClient: (type,id) => request.get(`/client/download/${id}/${type}`), // 下载客户端配置文件
|
||||
deleteClient: (id) => request.delete(`/client/${id}`), // 删除客户端
|
||||
saveClient: (data) => request.post(`/client`, data), // 新增/编辑客户端
|
||||
generateClientKeys: () => request.post(`/client/generate-keys`), // 生成密钥对
|
||||
generateClientIP: () => request.post(`/client/generate-ip`), // 生成客户端IP
|
||||
}
|
669
src/views/client/index.vue
Normal file
669
src/views/client/index.vue
Normal file
@ -0,0 +1,669 @@
|
||||
<template>
|
||||
<AppPage>
|
||||
<header mt-10 mb-35 min-h-190 flex items-center justify-between px-15>
|
||||
<slot v-if="$slots.header" name="header" />
|
||||
<template v-else>
|
||||
<n-card style="margin-left: 1.65%; width: 97.25%">
|
||||
<QueryBar @search="search">
|
||||
<n-form label-placement="left"
|
||||
ref="listQueryRef"
|
||||
:model="searchParam"
|
||||
>
|
||||
<n-space justify="start">
|
||||
<n-form-item style="margin-top: 3px" size="small" label="名称" path="name">
|
||||
<n-input size="small" v-model:value="searchParam.name"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="邮箱" path="email">
|
||||
<n-input size="small" v-model:value="searchParam.email"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="IP" path="ipAllocation">
|
||||
<n-input size="small" v-model:value="searchParam.ipAllocation"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="状态" path="enabled" style="width: 150px">
|
||||
<n-select
|
||||
size="small"
|
||||
v-model:value="searchParam.enabled"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
:options="selOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</QueryBar>
|
||||
<n-divider/>
|
||||
<n-space justify="end">
|
||||
<n-button size="small" style="float: right" type="info" @click="addClient">添加</n-button>
|
||||
<n-button size="small" style="float: right" type="primary" @click="refreshList">刷新</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
<slot name="action" />
|
||||
</template>
|
||||
</header>
|
||||
<div>
|
||||
<n-card
|
||||
v-for="row in listData.data"
|
||||
:hoverable="true"
|
||||
style="margin-left: 2.5%;margin-bottom: 10px;float: left;width: 22%;"
|
||||
>
|
||||
<template #header>
|
||||
<n-popover trigger="hover">
|
||||
<template #trigger>
|
||||
<n-tag type="info" size="small" :bordered="false">
|
||||
{{ ellipsis(row.name) }}
|
||||
</n-tag>
|
||||
</template>
|
||||
<span>{{ row.name }}</span>
|
||||
</n-popover>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<n-button size="small" type="info" @click="openEditModal(row)">详情</n-button>
|
||||
<n-button size="small" ml-5 color="#8F930B" @click="openQrCode(row.id,row.name)">二维码</n-button>
|
||||
<n-dropdown
|
||||
trigger="click"
|
||||
:options="dropMenuOpts"
|
||||
:show-arrow="true"
|
||||
@select="moreSelectHandle"
|
||||
>
|
||||
<n-button size="small" ml-5 type="primary" @click="saveRows(row)">更多</n-button>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
<div>
|
||||
<n-form label-placement="top" label-width="auto">
|
||||
<n-form-item label="名称:">
|
||||
<n-button color="#60688C" dashed size="small">
|
||||
{{ row.name }}
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item label="邮箱:">
|
||||
<n-button color="#60688C" dashed size="small">
|
||||
{{ !row.email ? '--' : row.email }}
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item label="客户端IP:">
|
||||
<n-button mr-3="" color="#0519F8" dashed size="small" v-for="cip in row.ipAllocation">
|
||||
{{ cip }}
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item label="可访问IP:">
|
||||
<n-button v-if="row.allowedIps.length <= 0" dashed size="small">
|
||||
-
|
||||
</n-button>
|
||||
<n-button v-else dashed mr-2 type="warning" v-for="aip in row.allowedIps" size="small">
|
||||
{{ aip }}
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item label="创建人:">
|
||||
<n-button color="#54150F" dashed size="small">
|
||||
{{ row.createUser }}
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item label="状态:">
|
||||
<n-button v-if="row.enabled === 1" color="#067748" round :bordered="false" size="small">
|
||||
启用
|
||||
</n-button>
|
||||
<n-button v-else color="#BA090C" round :bordered="false" size="small">
|
||||
禁用
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item label="离线监听:">
|
||||
<n-button v-if="row.offlineMonitoring === 1" color="#067748" round :bordered="false" size="small">
|
||||
启用
|
||||
</n-button>
|
||||
<n-button v-else color="#BA090C" round :bordered="false" size="small">
|
||||
禁用
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item class="timeItem" label="时间:">
|
||||
<n-space vertical>
|
||||
<span> 创建时间: {{ row.createdAt }}</span>
|
||||
<span> 更新时间: {{ row.updatedAt }}</span>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
<div mt-10 mb-20 min-h-45 flex items-center justify-between px-15>
|
||||
<n-card style="margin-left: 1.65%; width: 97.25%">
|
||||
<n-pagination
|
||||
:item-count="listData.total"
|
||||
v-model:page="search.page"
|
||||
@update-page="pageChange"
|
||||
:default-page-size="8"
|
||||
/>
|
||||
</n-card>
|
||||
</div>
|
||||
</AppPage>
|
||||
<n-modal
|
||||
style="width: 16%"
|
||||
v-model:show="showQrCode"
|
||||
preset="card"
|
||||
:title="qrCodeData.title"
|
||||
header-style="text-align: center"
|
||||
content-style="text-align: center"
|
||||
>
|
||||
<n-image
|
||||
:src="qrCodeData.data"
|
||||
preview-disabled
|
||||
alt="请使用手机Wireguard软件扫码"
|
||||
/>
|
||||
</n-modal>
|
||||
<n-modal
|
||||
style="width: 25%"
|
||||
v-model:show="editModalShow"
|
||||
preset="card"
|
||||
:title="editModalTitle"
|
||||
header-style="text-align: center"
|
||||
>
|
||||
<n-form
|
||||
ref="editModalFormRef"
|
||||
:rules="editModalFormRules"
|
||||
:model="editModalForm"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
label-align="left"
|
||||
require-mark-placement="right"
|
||||
>
|
||||
<n-form-item label="名称" path="name">
|
||||
<n-input v-model:value="editModalForm.name"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="邮箱" path="email">
|
||||
<n-input v-model:value="editModalForm.email"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="IP" path="ipAllocation" :rule="{
|
||||
required: true,
|
||||
type: 'array',
|
||||
message: 'IP不能为空',
|
||||
trigger: ['blur','change']
|
||||
}">
|
||||
<n-select
|
||||
v-model:value="editModalForm.ipAllocation"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
placeholder="输入,按回车确认"
|
||||
:show-arrow="false"
|
||||
:show="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="可访问IP" path="allowedIps">
|
||||
<n-select
|
||||
v-model:value="editModalForm.allowedIps"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
placeholder="输入,按回车确认"
|
||||
:show-arrow="false"
|
||||
:show="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="可访问IP(扩展)" path="extraAllowedIps">
|
||||
<n-select
|
||||
v-model:value="editModalForm.extraAllowedIps"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
placeholder="输入,按回车确认"
|
||||
:show-arrow="false"
|
||||
:show="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="服务端DNS" path="useServerDns">
|
||||
<n-radio value="1" :checked="editModalForm.useServerDns === 1" @change="editModalForm.useServerDns = 1">是</n-radio>
|
||||
<n-radio value="0" :checked="editModalForm.useServerDns === 0" @change="editModalForm.useServerDns = 0">否</n-radio>
|
||||
</n-form-item>
|
||||
<n-form-item label="公钥" path="keys.publicKey">
|
||||
<n-input disabled v-model:value="editModalForm.keys.publicKey"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="私钥" path="keys.privateKey">
|
||||
<n-input disabled v-model:value="editModalForm.keys.privateKey"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="共享密钥" path="keys.presharedKey">
|
||||
<n-input disabled v-model:value="editModalForm.keys.presharedKey"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="状态" path="editModalForm.enabled">
|
||||
<n-radio-group :value="editModalForm.enabled">
|
||||
<n-radio :value="1" :checked="editModalForm.enabled === 1" @change="editModalForm.enabled = 1">启用</n-radio>
|
||||
<n-radio :value="0" :checked="editModalForm.enabled === 0" @change="editModalForm.enabled = 0">禁用</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<n-form-item label="离线监听" path="offlineMonitoring">
|
||||
<n-radio-group :value="editModalForm.offlineMonitoring">
|
||||
<n-radio :value="1" :checked="editModalForm.offlineMonitoring === 1" @change="editModalForm.offlineMonitoring = 1">启用</n-radio>
|
||||
<n-radio :value="0" :checked="editModalForm.offlineMonitoring === 0" @change="editModalForm.offlineMonitoring = 0">禁用</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<n-button type="info" style="margin-left: 40%" @click="updateClient()">确认</n-button>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
<n-modal
|
||||
style="width: 25%"
|
||||
v-model:show="addModalShow"
|
||||
preset="card"
|
||||
title="添加"
|
||||
header-style="text-align: center"
|
||||
>
|
||||
<n-form
|
||||
ref="addModalFormRef"
|
||||
:rules="addModalFormRules"
|
||||
:model="addModalForm"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
label-align="left"
|
||||
require-mark-placement="right"
|
||||
>
|
||||
<n-form-item label="名称" path="name">
|
||||
<n-input v-model:value="addModalForm.name"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="邮箱" path="email">
|
||||
<n-input v-model:value="addModalForm.email"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="IP" path="ipAllocation" :rule="{
|
||||
required: true,
|
||||
type: 'array',
|
||||
message: 'IP不能为空',
|
||||
trigger: ['blur','change']
|
||||
}">
|
||||
<n-select
|
||||
v-model:value="addModalForm.ipAllocation"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
placeholder="输入,按回车确认"
|
||||
:show-arrow="false"
|
||||
:show="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="可访问IP" path="allowedIps">
|
||||
<n-select
|
||||
v-model:value="addModalForm.allowedIps"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
placeholder="输入,按回车确认"
|
||||
:show-arrow="false"
|
||||
:show="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="可访问IP(扩展)" path="extraAllowedIps">
|
||||
<n-select
|
||||
v-model:value="addModalForm.extraAllowedIps"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
placeholder="输入,按回车确认"
|
||||
:show-arrow="false"
|
||||
:show="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="服务端DNS" path="useServerDns">
|
||||
<n-radio value="1" :checked="addModalForm.useServerDns === 1" @change="addModalForm.useServerDns = 1">是</n-radio>
|
||||
<n-radio value="0" :checked="addModalForm.useServerDns === 0" @change="addModalForm.useServerDns = 0">否</n-radio>
|
||||
</n-form-item>
|
||||
<n-form-item label="公钥" path="keys.publicKey">
|
||||
<n-input v-model:value="addModalForm.keys.publicKey"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="私钥" path="keys.privateKey">
|
||||
<n-input v-model:value="addModalForm.keys.privateKey"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="共享密钥" path="keys.presharedKey">
|
||||
<n-input v-model:value="addModalForm.keys.presharedKey"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button style="margin-left: 28%" size="small" type="info" @click="generateKeys">生成密钥对</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item label="状态" path="editModalForm.enabled">
|
||||
<n-radio-group :value="addModalForm.enabled">
|
||||
<n-radio :value="1" :checked="addModalForm.enabled === 1" @change="addModalForm.enabled = 1">启用</n-radio>
|
||||
<n-radio :value="0" :checked="addModalForm.enabled === 0" @change="addModalForm.enabled = 0">禁用</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<n-form-item label="离线监听" path="offlineMonitoring">
|
||||
<n-radio-group :value="addModalForm.offlineMonitoring">
|
||||
<n-radio :value="1" :checked="addModalForm.offlineMonitoring === 1" @change="addModalForm.offlineMonitoring = 1">启用</n-radio>
|
||||
<n-radio :value="0" :checked="addModalForm.offlineMonitoring === 0" @change="addModalForm.offlineMonitoring = 0">禁用</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<n-button type="info" style="margin-left: 40%" @click="confirmAddClient()">确认</n-button>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import api from '@/views/client/api'
|
||||
import { NButton } from 'naive-ui'
|
||||
import { debounce, ellipsis } from '@/utils'
|
||||
import clientApi from '@/views/client/api'
|
||||
import QueryBar from '@/components/query-bar/QueryBar.vue'
|
||||
|
||||
const selOptions = [
|
||||
{
|
||||
label: '启用',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '禁用',
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
|
||||
const listQueryRef = ref()
|
||||
|
||||
// 卡片数据表
|
||||
const listData = reactive({
|
||||
data: [],
|
||||
total: 0,
|
||||
totalPage: 0,
|
||||
})
|
||||
|
||||
// 是否打开二维码模态框
|
||||
const showQrCode = ref(false)
|
||||
|
||||
// 编辑模块框
|
||||
const editModalShow = ref(false)
|
||||
const addModalShow = ref(false)
|
||||
const editModalTitle = ref('')
|
||||
|
||||
// 二维码的数据
|
||||
const qrCodeData = {
|
||||
title: '',
|
||||
data: '',
|
||||
}
|
||||
|
||||
// 点击更多按钮时候保存的当前客户端信息
|
||||
const getMoreRowData = ref({
|
||||
id: '',
|
||||
name: ''
|
||||
})
|
||||
|
||||
// 编辑模态框的form ref
|
||||
const editModalFormRef = ref()
|
||||
const addModalFormRef = ref()
|
||||
|
||||
// 编辑模态框的form rules
|
||||
const editModalFormRules = {
|
||||
name: [
|
||||
{ required: true, message: '名称不能为空', trigger: 'blur' }
|
||||
],
|
||||
keys: {
|
||||
privateKey: [
|
||||
{ required: true, message: '密钥不能为空', trigger: 'blur' }
|
||||
],
|
||||
publicKey: [
|
||||
{ required: true, message: '公钥不能为空', trigger: 'blur' }
|
||||
],
|
||||
presharedKey: [
|
||||
{ required: true, message: '共享密钥不能为空', trigger: 'blur' }
|
||||
],
|
||||
},
|
||||
enabled: [
|
||||
{ required: true, message: '状态不能为空', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 添加模态框的form rules
|
||||
const addModalFormRules = {
|
||||
name: [
|
||||
{ required: true, message: '名称不能为空', trigger: 'blur' }
|
||||
],
|
||||
keys: {
|
||||
privateKey: [
|
||||
{ required: true, message: '密钥不能为空', trigger: 'blur' }
|
||||
],
|
||||
publicKey: [
|
||||
{ required: true, message: '公钥不能为空', trigger: 'blur' }
|
||||
],
|
||||
presharedKey: [
|
||||
{ required: true, message: '共享密钥不能为空', trigger: 'blur' }
|
||||
],
|
||||
},
|
||||
enabled: [
|
||||
{ required: true, message: '状态不能为空', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 编辑模态框的数据集
|
||||
const editModalForm = ref({
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
ipAllocation: [],
|
||||
allowedIps: [],
|
||||
extraAllowedIps: [],
|
||||
useServerDns: 0,
|
||||
keys: {
|
||||
privateKey: '',
|
||||
publicKey: '',
|
||||
presharedKey: ''
|
||||
},
|
||||
enabled: 1,
|
||||
offlineMonitoring: 1
|
||||
})
|
||||
|
||||
// 添加模态框的数据集
|
||||
const addModalForm = ref({
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
ipAllocation: [],
|
||||
allowedIps: [],
|
||||
extraAllowedIps: [],
|
||||
useServerDns: 0,
|
||||
keys: {
|
||||
privateKey: '',
|
||||
publicKey: '',
|
||||
presharedKey: ''
|
||||
},
|
||||
enabled: 1,
|
||||
offlineMonitoring: 1
|
||||
})
|
||||
|
||||
// 下拉菜单选项
|
||||
const dropMenuOpts = [
|
||||
{
|
||||
key: 'download',
|
||||
label: '下载',
|
||||
},
|
||||
{
|
||||
key: 'mail',
|
||||
label: '邮件',
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
}
|
||||
]
|
||||
|
||||
// 页码参数
|
||||
const searchParam = reactive({
|
||||
name: '',
|
||||
email: '',
|
||||
ipAllocation: '',
|
||||
enabled: undefined,
|
||||
current: 1,
|
||||
size: 8,
|
||||
})
|
||||
|
||||
// 获取客户端列表
|
||||
function getClientList() {
|
||||
api.clientList(searchParam).then((res) => {
|
||||
if (res.data.code === 200) {
|
||||
listData.data = res.data.data.records
|
||||
listData.total = res.data.data.total;
|
||||
listData.totalPage = res.data.data.totalPage
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 打开二维码
|
||||
function openQrCode(clientID,title) {
|
||||
api.downloadClient("QRCODE",clientID).then((res) => {
|
||||
if (res.data.code === 200) {
|
||||
qrCodeData.data = res.data.data.qrCode
|
||||
qrCodeData.title = title
|
||||
showQrCode.value = true
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// 更多按钮下的操作逻辑
|
||||
function moreSelectHandle(key) {
|
||||
switch (key) {
|
||||
case "download": // 下载文件
|
||||
api.downloadClient("FILE",getMoreRowData.value.id).then((response) => {
|
||||
if (response && response.status === 200) {
|
||||
const blob = new Blob([response.data], {
|
||||
type: "text/plain"
|
||||
});
|
||||
const link = document.createElement("a"); // 创建a标签
|
||||
link.download = getMoreRowData.value.name + ".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); // 释放标签
|
||||
}
|
||||
})
|
||||
break;
|
||||
case "mail": // 发送邮件
|
||||
api.downloadClient("EMAIL",getMoreRowData.value.id).then((response) => {
|
||||
if (response.data.code === 200) {
|
||||
$message.success("发送邮件成功,请注意查收!")
|
||||
}
|
||||
})
|
||||
break;
|
||||
case "delete": // 删除客户端
|
||||
$dialog.confirm({
|
||||
type: 'warning',
|
||||
title: '删除',
|
||||
content: `是否删除客户端:【${getMoreRowData.value.name}】?`,
|
||||
async confirm() {
|
||||
api.deleteClient(getMoreRowData.value.id).then(res => {
|
||||
if (res.data.code === 200) {
|
||||
$message.success("操作成功!")
|
||||
getClientList()
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 点击更多按钮传递数据
|
||||
function saveRows(row) {
|
||||
getMoreRowData.value = row
|
||||
}
|
||||
|
||||
// 打开编辑模态框
|
||||
function openEditModal(row) {
|
||||
editModalShow.value = true
|
||||
editModalTitle.value = row.name
|
||||
editModalForm.value.id = row.id
|
||||
editModalForm.value.name = row.name
|
||||
editModalForm.value.email = row.email
|
||||
editModalForm.value.ipAllocation = row.ipAllocation
|
||||
editModalForm.value.allowedIps = row.allowedIps
|
||||
editModalForm.value.extraAllowedIps = row.extraAllowedIps
|
||||
editModalForm.value.useServerDns = row.useServerDns
|
||||
editModalForm.value.keys.privateKey = row.keys.privateKey
|
||||
editModalForm.value.keys.publicKey = row.keys.publicKey
|
||||
editModalForm.value.keys.presharedKey = row.keys.presharedKey
|
||||
editModalForm.value.enabled = row.enabled
|
||||
editModalForm.value.offlineMonitoring = row.offlineMonitoring
|
||||
}
|
||||
|
||||
// 更改客户端信息
|
||||
function updateClient() {
|
||||
editModalFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
return valid
|
||||
}
|
||||
|
||||
api.saveClient(editModalForm.value).then((res) => {
|
||||
if (res.data.code === 200) {
|
||||
editModalShow.value = false
|
||||
getClientList()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// 点击确认按钮
|
||||
async function confirmAddClient() {
|
||||
addModalFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
return valid
|
||||
}
|
||||
|
||||
clientApi.saveClient(addModalForm.value).then((response) => {
|
||||
if (response.data.code === 200) {
|
||||
refreshList()
|
||||
addModalShow.value = false
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 添加客户端
|
||||
const addClient = debounce(() => {
|
||||
generateClientIPS()
|
||||
addModalForm.value.name = ""
|
||||
addModalShow.value = true
|
||||
},300)
|
||||
|
||||
// 生成密钥对
|
||||
function generateKeys() {
|
||||
clientApi.generateClientKeys().then(res => {
|
||||
if (res.data.code === 200) {
|
||||
addModalForm.value.keys.privateKey = res.data.data.privateKey
|
||||
addModalForm.value.keys.publicKey = res.data.data.publicKey
|
||||
addModalForm.value.keys.presharedKey = res.data.data.presharedKey
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生成客户端IP
|
||||
function generateClientIPS() {
|
||||
clientApi.generateClientIP().then(res => {
|
||||
if (res.data.code === 200) {
|
||||
addModalForm.value.ipAllocation = res.data.data.clientIPS
|
||||
addModalForm.value.allowedIps = res.data.data.serverIPS
|
||||
generateKeys()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
function refreshList() {
|
||||
searchParam.name = ""
|
||||
searchParam.email = ""
|
||||
searchParam.enabled = undefined
|
||||
searchParam.ipAllocation = ""
|
||||
getClientList()
|
||||
}
|
||||
|
||||
// 页码变化
|
||||
function pageChange(page) {
|
||||
searchParam.current = page
|
||||
getClientList()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function search() {
|
||||
getClientList()
|
||||
}
|
||||
|
||||
getClientList()
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.timeItem {
|
||||
.n-form-item-blank {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
23
src/views/client/route.js
Normal file
23
src/views/client/route.js
Normal file
@ -0,0 +1,23 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'Client',
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/client',
|
||||
meta: {
|
||||
order: 2,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Client',
|
||||
path: 'client',
|
||||
component: () => import('./index.vue'),
|
||||
meta: {
|
||||
title: '客户端',
|
||||
icon: 'ph:network-duotone',
|
||||
order: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
<template>
|
||||
<CommonPage show-footer>
|
||||
<n-space size="large">
|
||||
<n-card title="按钮 Button">
|
||||
<n-space>
|
||||
<n-button>Default</n-button>
|
||||
<n-button type="tertiary">Tertiary</n-button>
|
||||
<n-button type="primary">Primary</n-button>
|
||||
<n-button type="info">Info</n-button>
|
||||
<n-button type="success">Success</n-button>
|
||||
<n-button type="warning">Warning</n-button>
|
||||
<n-button type="error">Error</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card title="带 Icon 的按钮">
|
||||
<n-space>
|
||||
<n-button type="info">
|
||||
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" />
|
||||
新增
|
||||
</n-button>
|
||||
<n-button type="error">
|
||||
<TheIcon icon="material-symbols:delete-outline" :size="18" class="mr-5" />
|
||||
删除
|
||||
</n-button>
|
||||
<n-button type="warning">
|
||||
<TheIcon icon="material-symbols:edit-outline" :size="18" class="mr-5" />
|
||||
编辑
|
||||
</n-button>
|
||||
<n-button type="primary">
|
||||
<TheIcon icon="majesticons:eye-line" :size="18" class="mr-5" />
|
||||
查看
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
|
||||
<n-space size="large" mt-30>
|
||||
<n-card min-w-340 title="通知 Notification">
|
||||
<n-space>
|
||||
<n-button @click="notify('info')">信息</n-button>
|
||||
<n-button @click="notify('success')">成功</n-button>
|
||||
<n-button @click="notify('warning')">警告</n-button>
|
||||
<n-button @click="notify('error')">错误</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card min-w-340 title="确认弹窗 Dialog">
|
||||
<n-button type="error" @click="handleDelete">
|
||||
<icon-mi:delete mr-5 />
|
||||
删除
|
||||
</n-button>
|
||||
</n-card>
|
||||
|
||||
<n-card min-w-340 title="消息提醒 Message">
|
||||
<n-button :loading="loading" type="primary" @click="handleLogin">
|
||||
<icon-mdi:login v-show="!loading" mr-5 />
|
||||
登陆
|
||||
</n-button>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const handleDelete = function () {
|
||||
$dialog.confirm({
|
||||
content: '确认删除?',
|
||||
confirm() {
|
||||
$message.success('删除成功')
|
||||
},
|
||||
cancel() {
|
||||
$message.warning('已取消')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
function handleLogin() {
|
||||
loading.value = true
|
||||
$message.loading('登陆中...')
|
||||
setTimeout(() => {
|
||||
$message.error('登陆失败')
|
||||
$message.loading('正在尝试重新登陆...')
|
||||
setTimeout(() => {
|
||||
$message.success('登陆成功')
|
||||
loading.value = false
|
||||
}, 2000)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
function notify(type) {
|
||||
$notification[type]({
|
||||
content: '说点啥呢',
|
||||
meta: '想不出来',
|
||||
duration: 2500,
|
||||
keepAliveOnHover: true,
|
||||
})
|
||||
}
|
||||
</script>
|
@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<CommonPage show-footer>
|
||||
<div w-350>
|
||||
<n-input v-model:value="inputVal" />
|
||||
<n-input-number v-model:value="number" mt-30 />
|
||||
<p mt-20 text-center text-14 color-gray>注:右击标签重新加载可重置keep-alive</p>
|
||||
</div>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({ name: 'KeepAlive' })
|
||||
|
||||
const inputVal = ref('')
|
||||
const number = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
$message.success('onMounted')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
$message.error('onUnmounted')
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
$message.info('onActivated')
|
||||
})
|
||||
onDeactivated(() => {
|
||||
$message.warning('onDeactivated')
|
||||
})
|
||||
</script>
|
@ -1,43 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'Test',
|
||||
path: '/base',
|
||||
component: Layout,
|
||||
redirect: '/base/index',
|
||||
meta: {
|
||||
title: '基础功能',
|
||||
icon: 'majesticons:compass-line',
|
||||
order: 1,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'BaseComponents',
|
||||
path: 'index',
|
||||
component: () => import('./index.vue'),
|
||||
meta: {
|
||||
title: '基础组件',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Unocss',
|
||||
path: 'unocss',
|
||||
component: () => import('./unocss/index.vue'),
|
||||
meta: {
|
||||
title: 'Unocss',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'KeepAlive',
|
||||
path: 'keep-alive',
|
||||
component: () => import('./keep-alive/index.vue'),
|
||||
meta: {
|
||||
title: 'KeepAlive',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
<template>
|
||||
<CommonPage show-footer>
|
||||
<p>
|
||||
文档:
|
||||
<a c-blue hover-decoration-underline href="https://uno.antfu.me/" target="_blank">
|
||||
https://uno.antfu.me/
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
playground:
|
||||
<a c-blue hover-decoration-underline href="https://unocss.antfu.me/play/" target="_blank">
|
||||
https://unocss.antfu.me/play/
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div mt-20 w-350 f-c-c flex-col>
|
||||
<div flex flex-wrap justify-around rounded-10 p-10 border="1 solid #ccc">
|
||||
<div m-20 h-50 w-50 f-c-c rounded-5 p-10 border="1 solid">
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
<div m-20 h-50 w-50 flex justify-between rounded-5 p-10 border="1 solid">
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 self-end rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
<div m-20 h-50 w-50 flex justify-between rounded-5 p-10 border="1 solid">
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 self-center rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 self-end rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
<div m-20 h-50 w-50 flex justify-between rounded-5 p-10 border="1 solid">
|
||||
<div flex-col justify-between>
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
<div flex-col justify-between>
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
</div>
|
||||
<div m-20 h-50 w-50 flex-col items-center justify-between rounded-5 p-10 border="1 solid">
|
||||
<div w-full flex justify-between>
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
<div h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<div w-full flex justify-between>
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
</div>
|
||||
<div m-20 h-50 w-50 flex-col justify-between rounded-5 p-10 border="1 solid">
|
||||
<div w-full flex justify-between>
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
<div w-full flex justify-between>
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
<div w-full flex justify-between>
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 mt-10 text-14 font-normal color-gray>Flex 骰子</h2>
|
||||
</div>
|
||||
</CommonPage>
|
||||
</template>
|
@ -1,54 +0,0 @@
|
||||
<template>
|
||||
<CommonPage>
|
||||
<div h-60 flex items-center bg-white pl-20 pr-20 dark:bg-dark>
|
||||
<input
|
||||
v-model="post.title"
|
||||
class="mr-20 flex-1 pb-15 pt-15 text-20 font-bold color-primary"
|
||||
dark:bg-dark
|
||||
type="text"
|
||||
placeholder="输入文章标题..."
|
||||
/>
|
||||
<n-button type="primary" style="width: 80px" :loading="btnLoading" @click="handleSavePost">
|
||||
<TheIcon v-if="!btnLoading" icon="line-md:confirm-circle" class="mr-5" :size="18" />
|
||||
保存
|
||||
</n-button>
|
||||
</div>
|
||||
<MdEditor
|
||||
v-model="post.content"
|
||||
:theme="appStore.isDark ? 'dark' : 'light'"
|
||||
style="height: calc(100vh - 305px)"
|
||||
/>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MdEditor } from 'md-editor-v3'
|
||||
import 'md-editor-v3/lib/style.css'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
defineOptions({ name: 'MDEditor' })
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// refs
|
||||
let post = ref({})
|
||||
let btnLoading = ref(false)
|
||||
|
||||
function handleSavePost() {
|
||||
btnLoading.value = true
|
||||
$message.loading('正在保存...')
|
||||
setTimeout(() => {
|
||||
$message.success('保存成功')
|
||||
btnLoading.value = false
|
||||
}, 2000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.md-preview {
|
||||
ul,
|
||||
ol {
|
||||
list-style: revert;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,46 +0,0 @@
|
||||
<template>
|
||||
<AppPage>
|
||||
<div class="h-full flex-col" border="1 solid #ccc" dark:bg-dark>
|
||||
<WangToolbar
|
||||
border-b="1px solid #ccc"
|
||||
:editor="editorRef"
|
||||
:default-config="toolbarConfig"
|
||||
mode="default"
|
||||
/>
|
||||
<WangEditor
|
||||
v-model="valueHtml"
|
||||
style="flex: 1; overflow-y: hidden"
|
||||
:default-config="editorConfig"
|
||||
mode="default"
|
||||
@on-created="handleCreated"
|
||||
/>
|
||||
</div>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
import { Editor as WangEditor, Toolbar as WangToolbar } from '@wangeditor/editor-for-vue'
|
||||
|
||||
defineOptions({ name: 'RichTextEditor' })
|
||||
const editorRef = shallowRef()
|
||||
const toolbarConfig = { excludeKeys: 'fullScreen' }
|
||||
const editorConfig = { placeholder: '请输入内容...', MENU_CONF: {} }
|
||||
const valueHtml = ref('')
|
||||
|
||||
const handleCreated = (editor) => {
|
||||
editorRef.value = editor
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html.dark {
|
||||
--w-e-textarea-bg-color: #333;
|
||||
--w-e-textarea-color: #fff;
|
||||
--w-e-toolbar-bg-color: #333;
|
||||
--w-e-toolbar-color: #fff;
|
||||
--w-e-toolbar-active-bg-color: #666;
|
||||
--w-e-toolbar-active-color: #fff;
|
||||
/* ...其他... */
|
||||
}
|
||||
</style>
|
@ -1,65 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'Demo',
|
||||
path: '/demo',
|
||||
component: Layout,
|
||||
redirect: '/demo/crud',
|
||||
meta: {
|
||||
title: '示例页面',
|
||||
icon: 'uil:pagelines',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
order: 3,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Crud',
|
||||
path: 'crud',
|
||||
component: () => import('./table/index.vue'),
|
||||
meta: {
|
||||
title: 'CRUD表格',
|
||||
icon: 'ic:baseline-table-view',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'MDEditor',
|
||||
path: 'md-editor',
|
||||
component: () => import('./editor/md-editor.vue'),
|
||||
meta: {
|
||||
title: 'MD编辑器',
|
||||
icon: 'ri:markdown-line',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'RichTextEditor',
|
||||
path: 'rich-text',
|
||||
component: () => import('./editor/rich-text.vue'),
|
||||
meta: {
|
||||
title: '富文本编辑器',
|
||||
icon: 'ic:sharp-text-rotation-none',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Upload',
|
||||
path: 'upload',
|
||||
component: () => import('./upload/index.vue'),
|
||||
meta: {
|
||||
title: '图片上传',
|
||||
icon: 'mdi:upload',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getPosts: (params = {}) => request.get('posts', { params }),
|
||||
getPostById: (id) => request.get(`/post/${id}`),
|
||||
addPost: (data) => request.post('/post', data),
|
||||
updatePost: (data) => request.put(`/post/${data.id}`, data),
|
||||
deletePost: (id) => request.delete(`/post/${id}`),
|
||||
}
|
@ -1,233 +0,0 @@
|
||||
<template>
|
||||
<CommonPage show-footer title="文章">
|
||||
<template #action>
|
||||
<div>
|
||||
<n-button type="primary" secondary @click="$table?.handleExport()">
|
||||
<TheIcon icon="mdi:download" :size="18" class="mr-5" />
|
||||
导出
|
||||
</n-button>
|
||||
<n-button type="primary" class="ml-16" @click="handleAdd">
|
||||
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" />
|
||||
新建文章
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<CrudTable
|
||||
ref="$table"
|
||||
v-model:query-items="queryItems"
|
||||
:extra-params="extraParams"
|
||||
:scroll-x="1200"
|
||||
:columns="columns"
|
||||
:get-data="api.getPosts"
|
||||
@on-checked="onChecked"
|
||||
@on-data-change="(data) => (tableData = data)"
|
||||
>
|
||||
<template #queryBar>
|
||||
<QueryBarItem label="标题" :label-width="50">
|
||||
<n-input
|
||||
v-model:value="queryItems.title"
|
||||
type="text"
|
||||
placeholder="请输入标题"
|
||||
@keypress.enter="$table?.handleSearch"
|
||||
/>
|
||||
</QueryBarItem>
|
||||
</template>
|
||||
</CrudTable>
|
||||
<!-- 新增/编辑/查看 -->
|
||||
<CrudModal
|
||||
v-model:visible="modalVisible"
|
||||
:title="modalTitle"
|
||||
:loading="modalLoading"
|
||||
:show-footer="modalAction !== 'view'"
|
||||
@on-save="handleSave"
|
||||
>
|
||||
<n-form
|
||||
ref="modalFormRef"
|
||||
label-placement="left"
|
||||
label-align="left"
|
||||
:label-width="80"
|
||||
:model="modalForm"
|
||||
:disabled="modalAction === 'view'"
|
||||
>
|
||||
<n-form-item label="作者" path="author">
|
||||
<n-input v-model:value="modalForm.author" disabled />
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="文章标题"
|
||||
path="title"
|
||||
:rule="{
|
||||
required: true,
|
||||
message: '请输入文章标题',
|
||||
trigger: ['input', 'blur'],
|
||||
}"
|
||||
>
|
||||
<n-input v-model:value="modalForm.title" placeholder="请输入文章标题" />
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="文章内容"
|
||||
path="content"
|
||||
:rule="{
|
||||
required: true,
|
||||
message: '请输入文章内容',
|
||||
trigger: ['input', 'blur'],
|
||||
}"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="modalForm.content"
|
||||
placeholder="请输入文章内容"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 3,
|
||||
maxRows: 5,
|
||||
}"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</CrudModal>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NButton, NSwitch } from 'naive-ui'
|
||||
import { formatDateTime, renderIcon, isNullOrUndef } from '@/utils'
|
||||
import { useCRUD } from '@/composables'
|
||||
import api from './api'
|
||||
|
||||
defineOptions({ name: 'Crud' })
|
||||
|
||||
const $table = ref(null)
|
||||
/** 表格数据,触发搜索的时候会更新这个值 */
|
||||
const tableData = ref([])
|
||||
/** QueryBar筛选参数(可选) */
|
||||
const queryItems = ref({})
|
||||
/** 补充参数(可选) */
|
||||
const extraParams = ref({})
|
||||
|
||||
onActivated(() => {
|
||||
$table.value?.handleSearch()
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ type: 'selection', fixed: 'left' },
|
||||
{
|
||||
title: '发布',
|
||||
key: 'isPublish',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
render(row) {
|
||||
return h(NSwitch, {
|
||||
size: 'small',
|
||||
rubberBand: false,
|
||||
value: row['isPublish'],
|
||||
loading: !!row.publishing,
|
||||
onUpdateValue: () => handlePublish(row),
|
||||
})
|
||||
},
|
||||
},
|
||||
{ title: '标题', key: 'title', width: 150, ellipsis: { tooltip: true } },
|
||||
{ title: '分类', key: 'category', width: 80, ellipsis: { tooltip: true } },
|
||||
{ title: '创建人', key: 'author', width: 80 },
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createDate',
|
||||
width: 150,
|
||||
render(row) {
|
||||
return h('span', formatDateTime(row['createDate']))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最后更新时间',
|
||||
key: 'updateDate',
|
||||
width: 150,
|
||||
render(row) {
|
||||
return h('span', formatDateTime(row['updateDate']))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 240,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
hideInExcel: true,
|
||||
render(row) {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
secondary: true,
|
||||
onClick: () => handleView(row),
|
||||
},
|
||||
{ default: () => '查看', icon: renderIcon('majesticons:eye-line', { size: 14 }) }
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
style: 'margin-left: 15px;',
|
||||
onClick: () => handleEdit(row),
|
||||
},
|
||||
{ default: () => '编辑', icon: renderIcon('material-symbols:edit-outline', { size: 14 }) }
|
||||
),
|
||||
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 15px;',
|
||||
onClick: () => handleDelete(row.id),
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 }),
|
||||
}
|
||||
),
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// 选中事件
|
||||
function onChecked(rowKeys) {
|
||||
if (rowKeys.length) $message.info(`选中${rowKeys.join(' ')}`)
|
||||
}
|
||||
|
||||
// 发布
|
||||
function handlePublish(row) {
|
||||
if (isNullOrUndef(row.id)) return
|
||||
|
||||
row.publishing = true
|
||||
setTimeout(() => {
|
||||
row.isPublish = !row.isPublish
|
||||
row.publishing = false
|
||||
$message?.success(row.isPublish ? '已发布' : '已取消发布')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const {
|
||||
modalVisible,
|
||||
modalAction,
|
||||
modalTitle,
|
||||
modalLoading,
|
||||
handleAdd,
|
||||
handleDelete,
|
||||
handleEdit,
|
||||
handleView,
|
||||
handleSave,
|
||||
modalForm,
|
||||
modalFormRef,
|
||||
} = useCRUD({
|
||||
name: '文章',
|
||||
initForm: { author: '大脸怪' },
|
||||
doCreate: api.addPost,
|
||||
doDelete: api.deletePost,
|
||||
doUpdate: api.updatePost,
|
||||
refresh: () => $table.value?.handleSearch(),
|
||||
})
|
||||
</script>
|
@ -1,84 +0,0 @@
|
||||
<template>
|
||||
<CommonPage>
|
||||
<n-upload
|
||||
class="mx-auto w-[75%] p-20 text-center"
|
||||
:custom-request="handleUpload"
|
||||
:show-file-list="false"
|
||||
accept=".png,.jpg,.jpeg"
|
||||
@before-upload="onBeforeUpload"
|
||||
>
|
||||
<n-upload-dragger>
|
||||
<div class="h-150 f-c-c flex-col">
|
||||
<TheIcon icon="mdi:upload" :size="68" class="mb-12 c-primary" />
|
||||
<n-text class="text-14 c-gray">点击或者拖动文件到该区域来上传</n-text>
|
||||
</div>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
|
||||
<n-card v-if="imgList && imgList.length" class="mt-16 items-center">
|
||||
<n-image-group>
|
||||
<n-space justify="space-between" align="center">
|
||||
<n-card v-for="(item, index) in imgList" :key="index" class="w-280 hover:card-shadow">
|
||||
<div class="h-160 f-c-c">
|
||||
<n-image width="200" :src="item.url" />
|
||||
</div>
|
||||
<n-space class="mt-16" justify="space-evenly">
|
||||
<n-button dashed type="primary" @click="copy(item.url)">url</n-button>
|
||||
<n-button dashed type="primary" @click="copy(`![${item.fileName}](${item.url})`)">
|
||||
MD
|
||||
</n-button>
|
||||
<n-button
|
||||
dashed
|
||||
type="primary"
|
||||
@click="copy(`<img src="${item.url}" />`)"
|
||||
>
|
||||
img
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
<div v-for="i in 4" :key="i" class="w-280" />
|
||||
</n-space>
|
||||
</n-image-group>
|
||||
</n-card>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
defineOptions({ name: 'Upload' })
|
||||
|
||||
const { copy, copied } = useClipboard()
|
||||
|
||||
const imgList = reactive([
|
||||
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
||||
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
||||
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
||||
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
||||
])
|
||||
|
||||
watch(copied, (val) => {
|
||||
val && $message.success('已复制到剪切板')
|
||||
})
|
||||
|
||||
function onBeforeUpload({ file }) {
|
||||
if (!file.file?.type.startsWith('image/')) {
|
||||
$message.error('只能上传图片')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function handleUpload({ file, onFinish }) {
|
||||
if (!file || !file.type) {
|
||||
$message.error('请选择文件')
|
||||
}
|
||||
|
||||
// 模拟上传
|
||||
$message.loading('上传中...')
|
||||
setTimeout(() => {
|
||||
$message.success('上传成功')
|
||||
imgList.push({ fileName: file.name, url: URL.createObjectURL(file.file) })
|
||||
onFinish()
|
||||
}, 1500)
|
||||
}
|
||||
</script>
|
@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<AppPage>
|
||||
<n-result m-auto status="404" description="抱歉,您访问的页面不存在。">
|
||||
<template #icon>
|
||||
<img src="@/assets/images/404.webp" width="500" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<n-button @click="replace('/')">返回首页</n-button>
|
||||
</template>
|
||||
</n-result>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { replace } = useRouter()
|
||||
</script>
|
@ -1,24 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'ErrorPage',
|
||||
path: '/error-page',
|
||||
component: Layout,
|
||||
redirect: '/error-page/404',
|
||||
meta: {
|
||||
title: '错误页',
|
||||
icon: 'mdi:alert-circle-outline',
|
||||
order: 99,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'ERROR-404',
|
||||
path: '404',
|
||||
component: () => import('./404.vue'),
|
||||
meta: {
|
||||
title: '404',
|
||||
icon: 'tabler:error-404',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
login: (data) => request.post('/auth/login', data, { noNeedToken: true }),
|
||||
// 获取验证码
|
||||
captcha: () => request.get('/login/captcha',{ noNeedToken: true }),
|
||||
login: (data) => request.post('/login', data, { noNeedToken: true }),
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
class="m-auto max-w-700 min-w-345 f-c-c rounded-10 bg-white bg-opacity-60 p-15 card-shadow"
|
||||
dark:bg-dark
|
||||
>
|
||||
<div hidden w-380 px-20 py-35 md:block>
|
||||
<div hidden w-380 flex-col px-20 py-35 md:block>
|
||||
<img src="@/assets/images/login_banner.webp" w-full alt="login_banner" />
|
||||
</div>
|
||||
|
||||
@ -14,12 +14,12 @@
|
||||
<img src="@/assets/images/logo.png" height="50" class="mr-10" />
|
||||
{{ title }}
|
||||
</h5>
|
||||
<div mt-32>
|
||||
<div mt-20>
|
||||
<n-input
|
||||
v-model:value="loginInfo.name"
|
||||
v-model:value="loginInfo.account"
|
||||
autofocus
|
||||
class="h-48 items-center text-16"
|
||||
placeholder="name"
|
||||
class="h-45 items-center text-16"
|
||||
placeholder="账号"
|
||||
:maxlength="20"
|
||||
>
|
||||
<template #prefix>
|
||||
@ -27,13 +27,13 @@
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
<div mt-32>
|
||||
<div mt-20>
|
||||
<n-input
|
||||
v-model:value="loginInfo.password"
|
||||
class="h-48 items-center text-16"
|
||||
class="h-45 items-center text-16"
|
||||
type="password"
|
||||
show-password-on="mousedown"
|
||||
placeholder="password"
|
||||
show-password-on="click"
|
||||
placeholder="密码"
|
||||
:maxlength="20"
|
||||
@keydown.enter="handleLogin"
|
||||
>
|
||||
@ -43,12 +43,26 @@
|
||||
</n-input>
|
||||
</div>
|
||||
|
||||
<div mt-20>
|
||||
<n-checkbox
|
||||
:checked="isRemember"
|
||||
label="记住我"
|
||||
:on-update:checked="(val) => (isRemember = val)"
|
||||
/>
|
||||
<div class="mt-20 flex items-center">
|
||||
<n-input
|
||||
v-model:value="loginInfo.captchaCode"
|
||||
class="h-45 items-center w-8"
|
||||
palceholder="请输入验证码"
|
||||
:maxlength="4"
|
||||
@keydown.enter="handleLogin()"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-material-symbols:keyboard-outline class="mr-8 text-20 opacity-40" />
|
||||
</template>
|
||||
</n-input>
|
||||
<img
|
||||
v-if="captchaUrl"
|
||||
:src="captchaUrl"
|
||||
alt="验证码"
|
||||
height="38"
|
||||
class="ml-12 w-120 cursor-pointer"
|
||||
@click="getCaptcha()"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div mt-20>
|
||||
@ -70,51 +84,54 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { lStorage, setToken } from '@/utils'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { setToken, setXToken } from '@/utils'
|
||||
import bgImg from '@/assets/images/login_bg.webp'
|
||||
import api from './api'
|
||||
import { addDynamicRoutes } from '@/router'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const title = import.meta.env.VITE_TITLE
|
||||
|
||||
const router = useRouter()
|
||||
const { query } = useRoute()
|
||||
|
||||
const captchaUrl = ref('')
|
||||
const loginInfo = ref({
|
||||
name: '',
|
||||
account: '',
|
||||
password: '',
|
||||
captchaId: '',
|
||||
captchaCode: ''
|
||||
})
|
||||
|
||||
initLoginInfo()
|
||||
// 进入系统初始化验证码
|
||||
getCaptcha()
|
||||
|
||||
function initLoginInfo() {
|
||||
const localLoginInfo = lStorage.get('loginInfo')
|
||||
if (localLoginInfo) {
|
||||
loginInfo.value.name = localLoginInfo.name || ''
|
||||
loginInfo.value.password = localLoginInfo.password || ''
|
||||
}
|
||||
// 获取验证码
|
||||
async function getCaptcha() {
|
||||
const res = await api.captcha()
|
||||
captchaUrl.value = res.data.data.captcha
|
||||
loginInfo.value.captchaId = res.data.data.id
|
||||
}
|
||||
|
||||
const isRemember = useStorage('isRemember', false)
|
||||
const loading = ref(false)
|
||||
async function handleLogin() {
|
||||
const { name, password } = loginInfo.value
|
||||
if (!name || !password) {
|
||||
const { account, password, captchaCode } = loginInfo.value
|
||||
if (!account || !password) {
|
||||
$message.warning('请输入用户名和密码')
|
||||
return
|
||||
}
|
||||
if (!captchaCode) {
|
||||
$message.warning('请输入验证码')
|
||||
return
|
||||
}
|
||||
try {
|
||||
loading.value = true
|
||||
$message.loading('正在验证...')
|
||||
const res = await api.login({ name, password: password.toString() })
|
||||
const res = await api.login(loginInfo.value)
|
||||
$message.success('登录成功')
|
||||
setToken(res.data.token)
|
||||
if (isRemember.value) {
|
||||
lStorage.set('loginInfo', { name, password })
|
||||
} else {
|
||||
lStorage.remove('loginInfo')
|
||||
}
|
||||
const tokenStr = `${res.data.data.type} ${res.data.data.token}`
|
||||
setToken(tokenStr,res.data.data.expireAt)
|
||||
setXToken(res.headers.get('X-Token'))
|
||||
await addDynamicRoutes()
|
||||
if (query.redirect) {
|
||||
const path = query.redirect
|
||||
@ -124,6 +141,7 @@ async function handleLogin() {
|
||||
router.push('/')
|
||||
}
|
||||
} catch (error) {
|
||||
await getCaptcha()
|
||||
console.error(error)
|
||||
$message.removeMessage()
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div>a-1-1</div>
|
||||
</template>
|
@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div>a-1-2</div>
|
||||
</template>
|
@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<CommonPage>
|
||||
<div>a-1</div>
|
||||
<div pl-20>
|
||||
<RouterView />
|
||||
</div>
|
||||
</CommonPage>
|
||||
</template>
|
@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div>a-2-1</div>
|
||||
</template>
|
@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<CommonPage>
|
||||
<div>a-2</div>
|
||||
<div pl-20>
|
||||
<RouterView />
|
||||
</div>
|
||||
</CommonPage>
|
||||
</template>
|
@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<CommonPage>
|
||||
<div>a</div>
|
||||
<div pl-20>
|
||||
<RouterView />
|
||||
</div>
|
||||
</CommonPage>
|
||||
</template>
|
@ -1,75 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'MultipleMenu',
|
||||
path: '/multi-menu',
|
||||
component: Layout,
|
||||
meta: {
|
||||
title: '多级菜单',
|
||||
icon: 'ic:baseline-menu',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
order: 4,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'a-1',
|
||||
path: 'multiple-menu',
|
||||
component: () => import('./a-1/index.vue'),
|
||||
meta: {
|
||||
title: 'a-1',
|
||||
icon: 'ic:baseline-menu',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'a-1-1',
|
||||
path: 'a-1-1',
|
||||
component: () => import('./a-1/a-1-1/index.vue'),
|
||||
meta: {
|
||||
title: 'a-1-1',
|
||||
icon: 'ic:baseline-menu',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'a-1-2',
|
||||
path: 'a-1-2',
|
||||
component: () => import('./a-1/a-1-2/index.vue'),
|
||||
meta: {
|
||||
title: 'a-1-2',
|
||||
icon: 'ic:baseline-menu',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'a-2',
|
||||
path: 'a-2',
|
||||
component: () => import('./a-2/index.vue'),
|
||||
meta: {
|
||||
title: 'a-2',
|
||||
icon: 'ic:baseline-menu',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'a-2-1',
|
||||
path: 'a-2-1',
|
||||
component: () => import('./a-2/a-2-1/index.vue'),
|
||||
meta: {
|
||||
title: 'a-2-1(单个子菜单)',
|
||||
icon: 'ic:baseline-menu',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
9
src/views/setting/api.js
Normal file
9
src/views/setting/api.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getSetting: (params) => request.get('/setting',{params}), // 获取配置
|
||||
setSetting: (data) => request.post('/setting', data), // 设置配置
|
||||
delSetting: (code) => request.delete(`/setting/${code}`), // 删除配置
|
||||
allSettings: () => request.get('/setting/all'), // 所有配置
|
||||
publicAddr: () => request.get('/setting/public-addr'), // 获取公网地址
|
||||
}
|
489
src/views/setting/index.vue
Normal file
489
src/views/setting/index.vue
Normal file
@ -0,0 +1,489 @@
|
||||
<template>
|
||||
<AppPage>
|
||||
<n-card>
|
||||
<n-tabs default-value="Server" justify-content="space-evenly" type="line" @update-value="tabChange">
|
||||
<n-tab-pane name="Server" tab="服务端">
|
||||
<n-form
|
||||
ref="serverFormRef"
|
||||
:model="serverFormModel"
|
||||
:rules="serverFormRules"
|
||||
>
|
||||
<n-form-item label="IP段" path="ipScope" :rule="{
|
||||
required: true,
|
||||
type: 'array',
|
||||
message: 'IP段不能为空',
|
||||
trigger: ['change','blur']
|
||||
}">
|
||||
<n-select
|
||||
v-model:value="serverFormModel.ipScope"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
placeholder="输入,按回车确认"
|
||||
:show-arrow="false"
|
||||
:show="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="监听端口" path="listenPort" :rule="[
|
||||
{
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '监听端口不能为空',
|
||||
trigger: ['change','blur']
|
||||
},
|
||||
{
|
||||
min: 1120,
|
||||
type: 'number',
|
||||
message: '端口最低设置为1120',
|
||||
trigger: ['change','blur']
|
||||
},
|
||||
{
|
||||
max: 65535,
|
||||
type: 'number',
|
||||
message: '端口最高设置为65535',
|
||||
trigger: ['change','blur']
|
||||
}
|
||||
]">
|
||||
<n-input-number :min="1120" :max="65535" v-model:value="serverFormModel.listenPort"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="私钥" path="privateKey">
|
||||
<n-input v-model:value="serverFormModel.privateKey"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="公钥" path="publicKey">
|
||||
<n-input v-model:value="serverFormModel.publicKey"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="上行脚本" path="postUpScript">
|
||||
<n-input v-model:value="serverFormModel.postUpScript"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="下行脚本" path="postDownScript">
|
||||
<n-input v-model:value="serverFormModel.postDownScript"/>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="info" @click="updateServerConf">确认</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="Global" tab="全局">
|
||||
<n-form
|
||||
ref="globalFormRef"
|
||||
:model="globalFormModel"
|
||||
:rules="globalFormRules"
|
||||
>
|
||||
<n-form-item label="公网IP" path="endpointAddress" class="pid">
|
||||
<n-input v-model:value="globalFormModel.endpointAddress"/>
|
||||
<n-button style="margin-top: 5px" size="small" type="warning" @click="getPublicAddr">获取地址</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item label="DNS" path="dnsServer" :rule="{
|
||||
required: true,
|
||||
type: 'array',
|
||||
message: 'dns不能为空',
|
||||
trigger: ['change','blur']
|
||||
}">
|
||||
<n-select
|
||||
v-model:value="globalFormModel.dnsServer"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
placeholder="输入,按回车确认"
|
||||
:show-arrow="false"
|
||||
:show="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="MTU" path="MTU" :rule="[
|
||||
{
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: 'MTU不能为空',
|
||||
trigger: ['change','blur']
|
||||
},
|
||||
{
|
||||
min: 1120,
|
||||
type: 'number',
|
||||
message: 'MTU最低设置为100',
|
||||
trigger: ['change','blur']
|
||||
},
|
||||
{
|
||||
max: 3000,
|
||||
type: 'number',
|
||||
message: 'MTU最高设置为3000',
|
||||
trigger: ['change','blur']
|
||||
}
|
||||
]">
|
||||
<n-input-number :min="100" :max="3000" v-model:value="globalFormModel.MTU"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="persistentKeepalive" path="persistentKeepalive" :rule="[
|
||||
{
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: 'persistentKeepalive不能为空',
|
||||
trigger: ['change','blur']
|
||||
},
|
||||
{
|
||||
min: 15,
|
||||
type: 'number',
|
||||
message: 'persistentKeepalive最低设置为15',
|
||||
trigger: ['change','blur']
|
||||
},
|
||||
{
|
||||
max: 300,
|
||||
type: 'number',
|
||||
message: 'persistentKeepalive最高设置为300',
|
||||
trigger: ['change','blur']
|
||||
}
|
||||
]">
|
||||
<n-input-number :min="15" :max="300" v-model:value="globalFormModel.persistentKeepalive"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="firewallMark" path="firewallMark">
|
||||
<n-input v-model:value="globalFormModel.firewallMark"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="table" path="table">
|
||||
<n-input v-model:value="globalFormModel.table"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="configPath" path="configFilePath">
|
||||
<n-input v-model:value="globalFormModel.configFilePath"/>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="info" @click="updateGlobalConf">确认</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="Other" tab="其他">
|
||||
<n-button style="float:right;margin-bottom: 10px" size="small" type="info" @click="showAddModal = !showAddModal">添加</n-button>
|
||||
<n-data-table
|
||||
:columns="tableColumns"
|
||||
:data="taleData.data"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
<n-modal
|
||||
:title="editFormModel.describe"
|
||||
v-model:show="showEditModal"
|
||||
preset="card"
|
||||
style="width: 30%"
|
||||
>
|
||||
<n-form
|
||||
ref="editFormRef"
|
||||
:model="editFormModel"
|
||||
>
|
||||
<n-form-item v-for="(item,index) in editFormModel.data" :label="index">
|
||||
<n-input v-if="typeof item === 'string'" v-model:value="editFormModel.data[index]"/>
|
||||
<n-input-number v-else-if="typeof item === 'number'" v-model:value="editFormModel.data[index]"/>
|
||||
<n-radio-group v-else-if="typeof item === 'boolean'" :value="editFormModel.data[index]">
|
||||
<n-radio :value="true" :checked="editFormModel.data[index] === true" @change="editFormModel.data[index] = true">是</n-radio>
|
||||
<n-radio :value="false" :checked="editFormModel.data[index] === false" @change="editFormModel.data[index] = false">否</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="info" @click="updateSetting">确认</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
<n-modal
|
||||
title="添加"
|
||||
v-model:show="showAddModal"
|
||||
preset="card"
|
||||
style="width: 30%"
|
||||
>
|
||||
<n-form :model="addFormModel" ref="addFormRef">
|
||||
<n-form-item label="Code">
|
||||
<n-input v-model:value="addFormModel.code"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="选项">
|
||||
<n-dynamic-input v-model:value="addFormModel.data" preset="pair" key-placeholder="键" value-placeholder="值"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="描述">
|
||||
<n-input v-model:value="addFormModel.describe"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="info" @click="addSetting">确认</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
</AppPage>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
// 服务端配置表单ref
|
||||
import AppPage from '@/components/page/AppPage.vue'
|
||||
import api from '@/views/setting/api'
|
||||
import { NButton } from 'naive-ui'
|
||||
import { renderIcon } from '@/utils'
|
||||
|
||||
// 表头
|
||||
const tableColumns = [
|
||||
{
|
||||
title: 'code',
|
||||
key: 'code',
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
key: "describe",
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
render: (row) => {
|
||||
return [
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
color: '#4389DA',
|
||||
onClick: () => listBtn(row, "EDIT")
|
||||
}, {
|
||||
icon: renderIcon('mingcute:edit-line', { size: 14 }),
|
||||
}),
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
style: 'margin-left: 5px',
|
||||
color: '#ED1518',
|
||||
onClick: () => listBtn(row, "DELETE")
|
||||
}, {
|
||||
icon: renderIcon('icon-park-outline:delete', { size: 14 }),
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const taleData = ref({
|
||||
data: []
|
||||
})
|
||||
|
||||
// 服务端表单ref
|
||||
const serverFormRef = ref(null)
|
||||
// 全局设置表单ref
|
||||
const globalFormRef = ref(null)
|
||||
// 编辑模态框表单ref
|
||||
const editFormRef = ref(null)
|
||||
// 添加模态框表单ref
|
||||
const addFormRef = ref(null)
|
||||
// 编辑模态框展示
|
||||
const showEditModal = ref(false)
|
||||
// 添加模态框展示
|
||||
const showAddModal = ref(false)
|
||||
|
||||
// 服务端表单数据
|
||||
const serverFormModel = ref({
|
||||
ipScope: [],
|
||||
listenPort: 51820,
|
||||
privateKey: '',
|
||||
publicKey: '',
|
||||
postUpScript: '',
|
||||
postDownScript: ''
|
||||
})
|
||||
// 全局设置表单数据
|
||||
const globalFormModel = ref({
|
||||
MTU: 1450,
|
||||
configFilePath: '',
|
||||
dnsServer: [],
|
||||
endpointAddress: '',
|
||||
firewallMark: '',
|
||||
persistentKeepalive: 15,
|
||||
table: ''
|
||||
})
|
||||
// 编辑表单数据
|
||||
const editFormModel = ref({
|
||||
code: '',
|
||||
data: '',
|
||||
describe: ''
|
||||
})
|
||||
// 添加表单数据
|
||||
const addFormModel = ref({
|
||||
code: '',
|
||||
data: [
|
||||
{
|
||||
key: '',
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
describe: ''
|
||||
})
|
||||
|
||||
// 服务端表单数据验证
|
||||
const serverFormRules = {
|
||||
privateKey: [
|
||||
{ required: true, message: '密钥不能为空', trigger: 'blur' }
|
||||
],
|
||||
publicKey: [
|
||||
{ required: true, message: '公钥不能为空', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
// 全局表单数据验证
|
||||
const globalFormRules = {
|
||||
endpointAddress: [
|
||||
{ required: true, message: '公网IP不能为空', trigger: 'blur' }
|
||||
],
|
||||
configFilePath: [
|
||||
{ required: true, message: '文件地址不能为空', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 获取服务端配置
|
||||
async function getServerConfig () {
|
||||
const res = await api.getSetting({
|
||||
code: "WG_SERVER"
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
serverFormModel.value = JSON.parse(res.data.data)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取全局配置
|
||||
async function getGlobalConfig () {
|
||||
const res = await api.getSetting({
|
||||
code: "WG_SETTING"
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
globalFormModel.value = JSON.parse(res.data.data)
|
||||
}
|
||||
}
|
||||
|
||||
// 更改服务端配置
|
||||
async function updateServerConf() {
|
||||
serverFormRef.value?.validate(async (errors) => {
|
||||
if (errors) {
|
||||
return errors
|
||||
}
|
||||
const res = await api.setSetting({
|
||||
code: "WG_SERVER",
|
||||
data: JSON.stringify(serverFormModel.value)
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
await getServerConfig()
|
||||
$message.success("操作成功")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// 更改全局配置
|
||||
async function updateGlobalConf() {
|
||||
globalFormRef.value?.validate(async (errors) => {
|
||||
if (errors) {
|
||||
return errors
|
||||
}
|
||||
const res = await api.setSetting({
|
||||
code: "WG_SETTING",
|
||||
data: JSON.stringify(globalFormModel.value)
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
await getGlobalConfig()
|
||||
$message.success("操作成功")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取公网IP地址
|
||||
async function getPublicAddr() {
|
||||
const res = await api.publicAddr()
|
||||
if (res.data.code === 200) {
|
||||
globalFormModel.value.endpointAddress = res.data.data
|
||||
}
|
||||
}
|
||||
|
||||
// 获取全部配置
|
||||
function allSetting() {
|
||||
api.allSettings().then(res => {
|
||||
if (res.data.code === 200) {
|
||||
taleData.value.data = res.data.data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// tab切换事件
|
||||
function tabChange(code) {
|
||||
switch (code) {
|
||||
case "Server":
|
||||
getServerConfig()
|
||||
break;
|
||||
case "Global":
|
||||
getGlobalConfig()
|
||||
break;
|
||||
case "Other":
|
||||
allSetting()
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表操作按钮
|
||||
function listBtn(row,code) {
|
||||
switch (code) {
|
||||
case "EDIT":
|
||||
showEditModal.value = true
|
||||
editFormModel.value.describe = row.describe
|
||||
editFormModel.value.data = JSON.parse(row.data)
|
||||
editFormModel.value.code = row.code
|
||||
break;
|
||||
case "DELETE":
|
||||
$dialog.confirm({
|
||||
type: 'warning',
|
||||
title: '删除',
|
||||
content: `是否删除设置:【${row.describe}】?`,
|
||||
async confirm() {
|
||||
const res = await api.delSetting(row.code)
|
||||
if (res.data.code === 200) {
|
||||
$message.success("操作成功")
|
||||
allSetting()
|
||||
}
|
||||
},
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 更改配置信息
|
||||
async function updateSetting() {
|
||||
const res = await api.setSetting({
|
||||
code: editFormModel.value.code,
|
||||
data: JSON.stringify(editFormModel.value.data),
|
||||
describe: editFormModel.value.describe
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
showEditModal.value = false
|
||||
allSetting()
|
||||
}
|
||||
}
|
||||
|
||||
// 添加设置
|
||||
async function addSetting() {
|
||||
const dataObj = {}
|
||||
for (const item of addFormModel.value.data) {
|
||||
dataObj[item.key] = item.value
|
||||
}
|
||||
|
||||
const addFormData = {
|
||||
code: addFormModel.value.code,
|
||||
data: JSON.stringify(dataObj),
|
||||
describe: addFormModel.value.describe
|
||||
}
|
||||
|
||||
const res = await api.setSetting(addFormData)
|
||||
if (res.data.code === 200) {
|
||||
showAddModal.value = false
|
||||
addFormModel.value = {
|
||||
code: '',
|
||||
data: [
|
||||
{
|
||||
key: '',
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
describe: ''
|
||||
}
|
||||
allSetting()
|
||||
}
|
||||
}
|
||||
|
||||
getServerConfig()
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.pid .n-form-item-blank {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
23
src/views/setting/route.js
Normal file
23
src/views/setting/route.js
Normal file
@ -0,0 +1,23 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'Setting',
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/setting',
|
||||
meta: {
|
||||
order: 3,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Setting',
|
||||
path: 'setting',
|
||||
component: () => import('./index.vue'),
|
||||
meta: {
|
||||
title: '设置',
|
||||
icon: 'ant-design:setting-outlined',
|
||||
order: 3,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
7
src/views/user/api.js
Normal file
7
src/views/user/api.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
userList: (params = {}) => request.get('/user/list',{ params }), // 用户列表
|
||||
deleteUser: (id) => request.delete(`/user/${id}`,), // 删除用户
|
||||
resetPassword: (id) => request.put(`/user/reset-password/${id}`), // 重置用户密码
|
||||
}
|
405
src/views/user/index.vue
Normal file
405
src/views/user/index.vue
Normal file
@ -0,0 +1,405 @@
|
||||
<template>
|
||||
<AppPage>
|
||||
<n-card
|
||||
:segmented="{
|
||||
content: true,
|
||||
footer: 'soft'
|
||||
}"
|
||||
header-style="font-size: 15px"
|
||||
:hoverable="true"
|
||||
:bordered="false"
|
||||
>
|
||||
<template #header>
|
||||
{{ $route.meta.title }}
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<n-button v-if="useUserStore().isAdmin === 1" size="small" type="info" @click="addUser()">添加</n-button>
|
||||
<n-button style="margin-left: 5px" size="small" type="primary" @click="getUserList()">刷新</n-button>
|
||||
</template>
|
||||
<n-data-table
|
||||
remote
|
||||
ref="table"
|
||||
:columns="tableColumns"
|
||||
:data="tableData.data"
|
||||
:pagination="paginate"
|
||||
:paginate-single-page="false"
|
||||
></n-data-table>
|
||||
</n-card>
|
||||
</AppPage>
|
||||
<n-modal
|
||||
v-model:show="showInfoModel"
|
||||
transform-origin="center"
|
||||
preset="card"
|
||||
:title="infoFormModel.nickname || '个人资料'"
|
||||
:bordered="false"
|
||||
size="large"
|
||||
style="width: 400px"
|
||||
header-style="text-align: center"
|
||||
>
|
||||
<n-form
|
||||
ref="infoFormRef"
|
||||
:rules="infoFormRules"
|
||||
:model="infoFormModel"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
label-align="right"
|
||||
require-mark-placement="left"
|
||||
>
|
||||
<n-form-item label="账号" path="account">
|
||||
<n-input v-if="infoFormModel.id !== ''" disabled v-model:value="infoFormModel.account"></n-input>
|
||||
<n-input v-else v-model:value="infoFormModel.account"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="infoFormModel.id === ''" label="密码" path="password">
|
||||
<n-input type="password" v-model:value="infoFormModel.password"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="昵称" path="nickname">
|
||||
<n-input v-model:value="infoFormModel.nickname"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="联系方式" path="contact">
|
||||
<n-input v-model:value="infoFormModel.contact"></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="管理员">
|
||||
<n-space>
|
||||
<n-radio
|
||||
:checked="infoFormModel.isAdmin === 1"
|
||||
value="1"
|
||||
@change="infoFormModel.isAdmin = 1"
|
||||
>
|
||||
是
|
||||
</n-radio>
|
||||
<n-radio
|
||||
:checked="infoFormModel.isAdmin === 0"
|
||||
value="0"
|
||||
@change="infoFormModel.isAdmin = 0"
|
||||
>
|
||||
否
|
||||
</n-radio>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
<n-form-item label="状态">
|
||||
<n-space>
|
||||
<n-radio
|
||||
:checked="infoFormModel.status === 1"
|
||||
value="1"
|
||||
@change="infoFormModel.status = 1"
|
||||
>
|
||||
是
|
||||
</n-radio>
|
||||
<n-radio
|
||||
:checked="infoFormModel.status === 0"
|
||||
value="0"
|
||||
@change="infoFormModel.status = 0"
|
||||
>
|
||||
否
|
||||
</n-radio>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
<n-button style="margin-left: 20%" type="primary" @click="SaveUser(infoFormModel)">确认</n-button>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AppPage from '@/components/page/AppPage.vue'
|
||||
import api from '@/views/user/api'
|
||||
import userApi from '@/api/user'
|
||||
import { NAvatar,NTag,NButton } from 'naive-ui'
|
||||
import { renderIcon } from '@/utils'
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
const infoFormRef = ref()
|
||||
|
||||
// 用户信息model的展示控制
|
||||
const showInfoModel = ref(false)
|
||||
// 表单表头
|
||||
const tableColumns = [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'index',
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
render: (_, index) => {
|
||||
return `${index + 1}`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '账号',
|
||||
key: 'account',
|
||||
align: 'center',
|
||||
titleAlign: 'center'
|
||||
|
||||
},
|
||||
{
|
||||
title: '昵称',
|
||||
key: 'nickname',
|
||||
align: 'center',
|
||||
titleAlign: 'center'
|
||||
},
|
||||
{
|
||||
title: '头像',
|
||||
key: 'avatar',
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
render: (row) => {
|
||||
return h(NAvatar,{
|
||||
src: row.avatar,
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '联系方式',
|
||||
key: 'contact',
|
||||
align: 'center',
|
||||
titleAlign: 'center'
|
||||
},
|
||||
{
|
||||
title: '管理员',
|
||||
key: 'isAdmin',
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
render: (row) => {
|
||||
switch (row.isAdmin) {
|
||||
case 1:
|
||||
return h(NTag,{
|
||||
type: 'info',
|
||||
},{
|
||||
default: () => '是'
|
||||
})
|
||||
case 0:
|
||||
return h(NTag,{
|
||||
type: 'success',
|
||||
},{
|
||||
default: () => '否'
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
render: (row) => {
|
||||
switch (row.status) {
|
||||
case 1:
|
||||
return h(NTag,{
|
||||
type: 'info',
|
||||
},{
|
||||
default: () => '启用'
|
||||
})
|
||||
case 0:
|
||||
return h(NTag,{
|
||||
type: 'success',
|
||||
},{
|
||||
default: () => '禁用'
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createdAt',
|
||||
align: 'center',
|
||||
titleAlign: 'center'
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
key: 'updatedAt',
|
||||
align: 'center',
|
||||
titleAlign: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
render: (row) => {
|
||||
let components = [
|
||||
h(NButton,{
|
||||
size: 'small',
|
||||
color: '#4389DA',
|
||||
onClick: () => listBtn(row,"EDIT")
|
||||
},{
|
||||
icon: renderIcon('mingcute:edit-line', { size: 14 }),
|
||||
}),
|
||||
h(NButton,{
|
||||
size: 'small',
|
||||
style: 'margin-left: 5px',
|
||||
color: '#0BE170',
|
||||
onClick: () => listBtn(row,"RESET")
|
||||
},{
|
||||
icon: renderIcon('ic:baseline-lock-reset', { size: 14 }),
|
||||
}),
|
||||
h(NButton,{
|
||||
size: 'small',
|
||||
style: 'margin-left: 5px',
|
||||
color: '#ED1518',
|
||||
onClick: () => listBtn(row,"DELETE")
|
||||
},{
|
||||
icon: renderIcon('icon-park-outline:delete', { size: 14 }),
|
||||
})
|
||||
]
|
||||
const currentLoginUser = useUserStore().localUserInfo
|
||||
if (row.account === 'admin') {
|
||||
components = []
|
||||
}
|
||||
if (currentLoginUser.isAdmin !== 1) {
|
||||
components = []
|
||||
}
|
||||
if (currentLoginUser.id === row.id) {
|
||||
components = []
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 表单数据
|
||||
const tableData = ref({
|
||||
data: []
|
||||
})
|
||||
|
||||
// 页码控件
|
||||
const paginate = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page) => {
|
||||
paginate.page = page;
|
||||
pageParam.current = page
|
||||
pageParam.size = paginate.pageSize
|
||||
getUserList()
|
||||
}
|
||||
})
|
||||
|
||||
// 用户信息的表单数据
|
||||
const infoFormModel = ref({
|
||||
id: '',
|
||||
account: '',
|
||||
password: '',
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
contact: '',
|
||||
isAdmin: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
// 页码参数
|
||||
const pageParam = {
|
||||
current: 1,
|
||||
size: 10
|
||||
}
|
||||
|
||||
// 个人信息表单验证规则
|
||||
const infoFormRules = {
|
||||
account: [
|
||||
{ required: true, message: '账号不能为空', trigger: 'blur' },
|
||||
{ min: 2, message: '账号长度最短2位', trigger: 'blur' },
|
||||
{ max: 20, message: '账号长度最长20位', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ min: 8, message: '密码至少8位', trigger: 'blur' },
|
||||
{ max: 32, message: '密码最多32位', trigger: 'blur' }
|
||||
],
|
||||
nickname: [
|
||||
{ required: true, message: '昵称不能为空', trigger: 'blur' }
|
||||
],
|
||||
isAdmin: [
|
||||
{ required: true, message: '管理员不能为空', trigger: 'blur' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '状态不能为空', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 获取用户列表信息
|
||||
async function getUserList() {
|
||||
const res = await api.userList(pageParam)
|
||||
if (res.data.code === 200) {
|
||||
tableData.value.data = res.data.data.records;
|
||||
paginate.itemCount = res.data.data.total;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表操作按钮
|
||||
async function listBtn(row,type) {
|
||||
switch (type) {
|
||||
case 'EDIT':
|
||||
showInfoModel.value = true
|
||||
infoFormModel.value.id = row.id;
|
||||
infoFormModel.value.account = row.account;
|
||||
infoFormModel.value.nickname = row.nickname;
|
||||
infoFormModel.value.avatar = row.avatar;
|
||||
infoFormModel.value.contact = row.contact;
|
||||
infoFormModel.value.isAdmin = row.isAdmin;
|
||||
infoFormModel.value.status = row.status;
|
||||
break;
|
||||
case 'RESET':
|
||||
$dialog.confirm({
|
||||
type: 'info',
|
||||
title: '重置密码',
|
||||
content: `是否重置用户:【${row.nickname}】的密码?`,
|
||||
async confirm() {
|
||||
const res = await api.resetPassword(row.id)
|
||||
if (res.data.code === 200) {
|
||||
$message.success("操作成功")
|
||||
await getUserList()
|
||||
}
|
||||
},
|
||||
})
|
||||
break;
|
||||
case 'DELETE':
|
||||
$dialog.confirm({
|
||||
type: 'warning',
|
||||
title: '删除',
|
||||
content: `是否删除用户:【${row.nickname}】?`,
|
||||
async confirm() {
|
||||
const res = await api.deleteUser(row.id)
|
||||
if (res.data.code === 200) {
|
||||
$message.success("操作成功")
|
||||
await getUserList()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 新增/编辑用户
|
||||
async function SaveUser(user) {
|
||||
if (!user) {
|
||||
showInfoModel.value = true
|
||||
}
|
||||
infoFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
return valid
|
||||
}
|
||||
const res = await userApi.addOrUpdateUser(user)
|
||||
if (res.data.code === 200) {
|
||||
await getUserList()
|
||||
showInfoModel.value = false
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// 添加用户
|
||||
function addUser() {
|
||||
infoFormModel.value = {
|
||||
id: '',
|
||||
account: '',
|
||||
password: '',
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
contact: '',
|
||||
isAdmin: 0,
|
||||
status: 1
|
||||
}
|
||||
showInfoModel.value = true
|
||||
}
|
||||
|
||||
getUserList()
|
||||
</script>
|
||||
<style></style>
|
23
src/views/user/route.js
Normal file
23
src/views/user/route.js
Normal file
@ -0,0 +1,23 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'User',
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/user',
|
||||
meta: {
|
||||
order: 1,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'User',
|
||||
path: 'user',
|
||||
component: () => import('./index.vue'),
|
||||
meta: {
|
||||
title: '管理员',
|
||||
icon: 'ph:users-bold',
|
||||
order: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
<div class="flex items-center">
|
||||
<n-avatar round :size="60" :src="userStore.avatar" />
|
||||
<div class="ml-20 flex-col">
|
||||
<span class="text-20 opacity-80">Hello, {{ userStore.name }}</span>
|
||||
<span class="text-20 opacity-80">Hello, {{ userStore.nickname }}</span>
|
||||
<span class="mt-4 opacity-50">今日事,今日毕。</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -14,188 +14,14 @@
|
||||
<p class="mt-12 text-right text-12 opacity-40">—— 查尔斯·史考伯</p>
|
||||
</n-card>
|
||||
<n-card class="ml-12 w-70%">
|
||||
<h3 class="text-20 font-normal opacity-90">⚡️欢迎使用 Vue Naive Admin</h3>
|
||||
<p class="mt-8 opacity-60">
|
||||
这是一款基于 Vue3 + Vite + Pinia + Unocss + Naive UI 的轻量级后台管理模板!!
|
||||
</p>
|
||||
<footer class="mt-24 flex items-center justify-end">
|
||||
<n-button
|
||||
tag="a"
|
||||
href="https://zclzone.github.io/vue-naive-admin-docs"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
ghost
|
||||
>
|
||||
开发文档
|
||||
</n-button>
|
||||
<n-button
|
||||
tag="a"
|
||||
href="https://github.com/zclzone/vue-naive-admin"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
class="ml-12"
|
||||
>
|
||||
代码仓库
|
||||
</n-button>
|
||||
</footer>
|
||||
|
||||
</n-card>
|
||||
</div>
|
||||
<div class="mt-12 flex">
|
||||
<n-card title="项目" segmented>
|
||||
<template #header-extra>
|
||||
<n-button text type="primary">更多</n-button>
|
||||
</template>
|
||||
<div class="flex flex-wrap justify-between">
|
||||
<n-card
|
||||
v-for="i in 6"
|
||||
:key="i"
|
||||
size="small"
|
||||
class="my-6 w-320 flex-shrink-0 cursor-pointer hover:card-shadow"
|
||||
title="Vue Naive Admin"
|
||||
>
|
||||
<p class="op-60">一个基于 Vue3.0、Vite、Naive UI 的轻量级后台管理模板</p>
|
||||
</n-card>
|
||||
<div h-0 w-300></div>
|
||||
<div h-0 w-300></div>
|
||||
<div h-0 w-300></div>
|
||||
<div h-0 w-300></div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<n-card class="ml-12" title="技术栈" segmented>
|
||||
<VChart :option="skillsOption" class="wh-full" autoresize />
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<n-card class="mt-12" title="趋势" segmented>
|
||||
<VChart :option="trendOption" class="h-480 w-full" autoresize />
|
||||
</n-card>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserStore } from '@/store'
|
||||
import * as echarts from 'echarts/core'
|
||||
import { TooltipComponent, GridComponent, LegendComponent } from 'echarts/components'
|
||||
import { BarChart, LineChart, PieChart } from 'echarts/charts'
|
||||
import { UniversalTransition } from 'echarts/features'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import VChart from 'vue-echarts'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
echarts.use([
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
BarChart,
|
||||
LineChart,
|
||||
CanvasRenderer,
|
||||
UniversalTransition,
|
||||
PieChart,
|
||||
])
|
||||
|
||||
const trendOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
crossStyle: {
|
||||
color: '#999',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
top: '5%',
|
||||
data: ['star', 'fork'],
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 3000,
|
||||
interval: 500,
|
||||
axisLabel: {
|
||||
formatter: '{value}',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 500,
|
||||
interval: 100,
|
||||
axisLabel: {
|
||||
formatter: '{value}',
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'star',
|
||||
type: 'line',
|
||||
data: [200, 320, 520, 550, 600, 805, 888, 950, 1300, 2503, 2702, 2712],
|
||||
},
|
||||
{
|
||||
name: 'fork',
|
||||
yAxisIndex: 1,
|
||||
type: 'bar',
|
||||
data: [40, 72, 110, 115, 121, 175, 180, 201, 260, 398, 423, 455],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const skillsOption = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter({ name, value }) {
|
||||
return `${name} ${value}%`
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
left: 'center',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
top: '7%',
|
||||
type: 'pie',
|
||||
radius: ['40%', '85%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 36,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [
|
||||
{ value: 38.5, name: 'Vue' },
|
||||
{ value: 37.0, name: 'JavaScript' },
|
||||
{ value: 6.5, name: 'CSS' },
|
||||
{ value: 6.2, name: 'HTML' },
|
||||
{ value: 1.8, name: 'Other' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
</script>
|
||||
|
@ -5,6 +5,9 @@ export default {
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/workbench',
|
||||
meta: {
|
||||
order: 0,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Workbench',
|
||||
|
Loading…
Reference in New Issue
Block a user