mirror of
https://github.com/zclzone/vue-naive-admin.git
synced 2026-01-22 23:50:22 +08:00
init
This commit is contained in:
115
src/views/base/index.vue
Normal file
115
src/views/base/index.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/04 22:46:57
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<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">
|
||||
<i class="i-material-symbols:add mr-4 text-18" />
|
||||
新增
|
||||
</n-button>
|
||||
<n-button type="error">
|
||||
<i class="i-material-symbols:delete-outline mr-4 text-18" />
|
||||
删除
|
||||
</n-button>
|
||||
<n-button type="warning">
|
||||
<i class="i-material-symbols:edit-outline mr-4 text-18" />
|
||||
编辑
|
||||
</n-button>
|
||||
<n-button type="primary">
|
||||
<i class="i-majesticons:eye-line mr-4 text-18" />
|
||||
查看
|
||||
</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">
|
||||
<i class="i-mi:delete mr-4" />
|
||||
删除
|
||||
</n-button>
|
||||
</n-card>
|
||||
|
||||
<n-card min-w-340 title="消息提醒 Message">
|
||||
<n-space>
|
||||
<n-button :loading="loading" type="primary" @click="handleLogin">
|
||||
<i v-show="!loading" class="i-mdi:login mr-4" />
|
||||
登陆
|
||||
</n-button>
|
||||
<n-button type="error" @click="handleMultiMessage">多个错误提醒</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { sleep } from '@/utils'
|
||||
const handleDelete = function () {
|
||||
$dialog.confirm({
|
||||
content: '确认删除?',
|
||||
confirm() {
|
||||
$message.success('删除成功')
|
||||
},
|
||||
cancel() {
|
||||
$message.warning('已取消')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
async function handleLogin() {
|
||||
loading.value = true
|
||||
$message.loading('登陆中...', { key: 'login' })
|
||||
await sleep(2000)
|
||||
$message.error('登陆失败', { key: 'login' })
|
||||
await sleep(500)
|
||||
$message.loading('正在尝试重新登陆...', { key: 'login' })
|
||||
await sleep(2000)
|
||||
$message.success('登陆成功', { key: 'login' })
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function handleMultiMessage() {
|
||||
$message.error(['用户名不能为空!', '密码不能为空!', '密码必须大于6位!'])
|
||||
}
|
||||
|
||||
function notify(type) {
|
||||
$notification[type]({
|
||||
content: '说点啥呢',
|
||||
meta: '想不出来',
|
||||
duration: 2500,
|
||||
keepAliveOnHover: true,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
24
src/views/base/keep-alive.vue
Normal file
24
src/views/base/keep-alive.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:27:37
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<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)
|
||||
</script>
|
||||
79
src/views/base/test-modal.vue
Normal file
79
src/views/base/test-modal.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:27:43
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<CommonPage show-footer>
|
||||
<n-button type="primary" @click="openModal1">打开第一个弹个窗</n-button>
|
||||
<MeModal ref="$modal1">
|
||||
<n-input v-model:value="text" />
|
||||
</MeModal>
|
||||
<MeModal ref="$modal2" title="上一个弹窗提交的内容">
|
||||
<h2>{{ text }}</h2>
|
||||
</MeModal>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MeModal } from '@/components'
|
||||
import { sleep } from '@/utils'
|
||||
import { useModal } from '@/composables'
|
||||
|
||||
const text = ref('')
|
||||
const [$modal1, okLoading1] = useModal()
|
||||
function openModal1() {
|
||||
$modal1.value?.open({
|
||||
title: '第一个弹窗',
|
||||
width: '600px',
|
||||
okText: '再弹个窗',
|
||||
cancelText: '关闭',
|
||||
async onOk() {
|
||||
okLoading1.value = true
|
||||
$message.loading('正在提交...', { key: 'modal1' })
|
||||
await sleep(1000)
|
||||
okLoading1.value = false
|
||||
$message.success('提交成功', { key: 'modal1' })
|
||||
openModal2()
|
||||
return false // 默认关闭弹窗,返回false可让弹窗不关闭
|
||||
},
|
||||
onCancel(message) {
|
||||
$message.info(message ?? '已取消')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const [$modal2, okLoading2] = useModal()
|
||||
function openModal2() {
|
||||
// modal的options都是可变的
|
||||
if ($modal1.value) {
|
||||
$modal1.value.options.style.top = '-100px'
|
||||
$modal1.value.options.title = '我走了'
|
||||
}
|
||||
|
||||
$modal2.value?.open({
|
||||
cancelText: '关闭当前',
|
||||
okText: '关闭所有弹窗',
|
||||
style: { width: '320px', padding: '12px', top: '100px' },
|
||||
async onOk() {
|
||||
okLoading2.value = true
|
||||
$message.loading('正在关闭...', { key: 'modal2' })
|
||||
await sleep(1000)
|
||||
okLoading2.value = false
|
||||
|
||||
// 把modal1也关了
|
||||
$modal1.value?.close()
|
||||
$message.success('已关闭', { key: 'modal2' })
|
||||
},
|
||||
onCancel() {
|
||||
if ($modal1.value) {
|
||||
$modal1.value.options.style.top = '0'
|
||||
$modal1.value.options.title = '我又回来了'
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
49
src/views/base/unocss-icon.vue
Normal file
49
src/views/base/unocss-icon.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:27:49
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<CommonPage show-footer>
|
||||
<template #title-suffix>
|
||||
<n-tag class="ml-12" type="primary">feather图标集 + isme自定义图标</n-tag>
|
||||
</template>
|
||||
<ul class="flex flex-wrap justify-between">
|
||||
<li
|
||||
v-for="item in icons"
|
||||
:key="item"
|
||||
class="m-16 w-160 f-c-c flex-col cursor-pointer rounded-12 px-12 py-24 card-border auto-bg"
|
||||
@click="copy(`<i class="${item}" />`)"
|
||||
>
|
||||
<i :class="item + '?mask'" class="text-28 text-gray-600 hover:bg-primary" />
|
||||
<span
|
||||
class="mt-16 text-center text-14 text-gray-400 hover:color-primary"
|
||||
@click.stop="copy(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="mx-16 h-0 w-160"></li>
|
||||
<li class="mx-16 h-0 w-160"></li>
|
||||
<li class="mx-16 h-0 w-160"></li>
|
||||
<li class="mx-16 h-0 w-160"></li>
|
||||
<li class="mx-16 h-0 w-160"></li>
|
||||
<li class="mx-16 h-0 w-160"></li>
|
||||
<li class="mx-16 h-0 w-160"></li>
|
||||
</ul>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import icons from 'isme:icons'
|
||||
|
||||
const { copy, copied } = useClipboard()
|
||||
|
||||
watch(copied, (val) => {
|
||||
val && $message.success('已复制到剪切板')
|
||||
})
|
||||
</script>
|
||||
76
src/views/base/unocss.vue
Normal file
76
src/views/base/unocss.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:27:55
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<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 card-border>
|
||||
<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>
|
||||
</div>
|
||||
</CommonPage>
|
||||
</template>
|
||||
92
src/views/demo/upload/index.vue
Normal file
92
src/views/demo/upload/index.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:28:02
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<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">
|
||||
<i class="i-mdi:upload mb-12 text-68 color-primary" />
|
||||
<n-text class="text-14 color-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(``)">
|
||||
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: 'ImgUpload' })
|
||||
|
||||
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>
|
||||
41
src/views/error-page/403.vue
Normal file
41
src/views/error-page/403.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:28:09
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<CommonPage :show-header="false">
|
||||
<div class="wh-full flex">
|
||||
<n-result
|
||||
class="m-auto"
|
||||
status="403"
|
||||
title="403 禁止访问"
|
||||
description="抱歉,您暂无权限访问,请联系管理员开通权限。"
|
||||
size="large"
|
||||
>
|
||||
<template #footer>
|
||||
<n-button v-if="back" type="primary" ghost @click="router.replace(back)">
|
||||
返回上一页
|
||||
</n-button>
|
||||
<n-button type="primary" class="ml-20" @click="router.replace('/')">返回首页</n-button>
|
||||
</template>
|
||||
</n-result>
|
||||
</div>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const back = history.state.back
|
||||
|
||||
if (history.state.from === 'permission-guard') {
|
||||
delete history.state.from
|
||||
} else if (route.query.path) {
|
||||
router.replace(route.query.path)
|
||||
}
|
||||
</script>
|
||||
29
src/views/error-page/404.vue
Normal file
29
src/views/error-page/404.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:28:15
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<CommonPage :show-header="false">
|
||||
<div class="wh-full flex">
|
||||
<n-result
|
||||
m-auto
|
||||
status="404"
|
||||
title="404 您访问的页面不存在"
|
||||
description="生活总归带点荒谬"
|
||||
size="large"
|
||||
>
|
||||
<template #footer>
|
||||
<n-button type="primary" ghost @click="replace('/')">返回首页</n-button>
|
||||
</template>
|
||||
</n-result>
|
||||
</div>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { replace } = useRouter()
|
||||
</script>
|
||||
283
src/views/home/index.vue
Normal file
283
src/views/home/index.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:28:22
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<AppPage show-footer>
|
||||
<div class="flex">
|
||||
<n-card class="min-w-200 w-30%">
|
||||
<div class="flex items-center">
|
||||
<n-avatar round :size="60" :src="userStore.avatar" class="flex-shrink-0" />
|
||||
<div class="ml-20 flex-col">
|
||||
<span class="text-20 opacity-80">
|
||||
Hello, {{ userStore.nickName ?? userStore.username }}
|
||||
</span>
|
||||
<span class="mt-4 opacity-50">当前角色:{{ userStore.currentRole?.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-28 text-14 opacity-60">一个人几乎可以在任何他怀有无限热忱的事情上成功。</p>
|
||||
<p class="mt-12 text-right text-12 opacity-40">—— 查尔斯·史考伯</p>
|
||||
</n-card>
|
||||
<n-card class="ml-12 w-70%" title="✨ 欢迎使用 Vue Naive Admin 2.0">
|
||||
<template #header-extra>
|
||||
<a
|
||||
class="text-14 text-primary text-highlight hover:underline hover:opacity-80"
|
||||
href="https://isme.top"
|
||||
target="_blank"
|
||||
@click.prevent="message?.info('官网正在火速开发中...')"
|
||||
>
|
||||
isme.top
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<p class="opacity-60">
|
||||
这是一款极简风格的后台管理模板,包含前后端解决方案,前端使用 Vite + Vue3 + Pinia +
|
||||
Unocss,后端使用 Nestjs + TypeOrm +
|
||||
MySql,简单易用,赏心悦目,历经十几次重构和细节打磨,诚意满满!!
|
||||
</p>
|
||||
<footer class="mt-12 flex items-center justify-end">
|
||||
<n-button
|
||||
type="primary"
|
||||
ghost
|
||||
tag="a"
|
||||
href="https://docs.isme.top/web/#/624306705/188522224"
|
||||
target="__blank"
|
||||
>
|
||||
开发文档
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
class="ml-12"
|
||||
tag="a"
|
||||
href="https://github.com/zclzone/vue-naive-admin/tree/2.x-beta"
|
||||
target="__blank"
|
||||
>
|
||||
代码仓库
|
||||
</n-button>
|
||||
</footer>
|
||||
</n-card>
|
||||
</div>
|
||||
<div class="mt-12 flex">
|
||||
<n-card class="w-50%" title="💯 特性" segmented>
|
||||
<template #header-extra>
|
||||
<span class="opacity-90 text-highlight">👏 历经十几次重构和细节打磨</span>
|
||||
</template>
|
||||
|
||||
<ul class="opacity-90">
|
||||
<li class="py-4">
|
||||
🆒 使用
|
||||
<b>Vue3</b>
|
||||
主流技术栈:
|
||||
<span class="text-highlight">Vite + Vue3 + Pinia</span>
|
||||
</li>
|
||||
<li class="py-4">
|
||||
🍇 使用
|
||||
<b>原子CSS</b>
|
||||
框架:
|
||||
<span class="text-highlight">Unocss</span>
|
||||
,优雅、轻量、易用
|
||||
</li>
|
||||
<li class="py-4">
|
||||
🤹 使用主流的
|
||||
<span class="text-highlight">iconify + unocss</span>
|
||||
图标方案,支持自定义图标,支持动态渲染
|
||||
</li>
|
||||
<li class="py-4">
|
||||
🎨 使用 Naive UI,
|
||||
<span class="text-highlight">极致简洁的代码风格和清爽的页面设计</span>
|
||||
,审美在线,主题轻松定制
|
||||
</li>
|
||||
<li class="py-4">
|
||||
👏 先进且易于理解的文件结构设计,多个模块之间
|
||||
<b>零耦合</b>
|
||||
,单个业务模块删除不影响其他模块
|
||||
</li>
|
||||
<li class="py-4">
|
||||
🚀
|
||||
<span class="text-highlight">扁平化路由</span>
|
||||
设计,每一个组件都可以是一个页面,告别多级路由 KeepAlive 难实现问题
|
||||
</li>
|
||||
|
||||
<li class="py-4">
|
||||
🍒
|
||||
<span class="text-highlight">基于权限动态生成路由</span>
|
||||
,无需额外定义路由,
|
||||
<span class="text-highlight">403和404可区分</span>
|
||||
,而不是无权限也跳404
|
||||
</li>
|
||||
<li class="py-4">
|
||||
🔐 集成主流的双token
|
||||
<span class="text-highlight">无感刷新</span>
|
||||
,安全与体验缺一不可
|
||||
</li>
|
||||
<li class="py-4">
|
||||
✨ 基于 Naive UI 封装
|
||||
<span class="text-highlight">message</span>
|
||||
全局工具方法,支持批量提醒,支持跨页面共享实例
|
||||
</li>
|
||||
<li class="py-4">
|
||||
⚡️ 基于 Naive UI 封装常用的业务组件,包含
|
||||
<span class="text-highlight">Page</span>
|
||||
组件、
|
||||
<span class="text-highlight">CRUD</span>
|
||||
表格组件及
|
||||
<span class="text-highlight">Modal</span>
|
||||
组件,减少大量重复性工作
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<n-divider class="mb-0! mt-12!">
|
||||
<p class="text-14 opacity-60">
|
||||
👉点击
|
||||
<b class="mx-2 transition hover:text-primary">
|
||||
<a href="https://isme.top" target="_blank">更多</a>
|
||||
</b>
|
||||
查看更多实用功能,持续开发中...
|
||||
</p>
|
||||
</n-divider>
|
||||
</n-card>
|
||||
|
||||
<n-card class="ml-12 w-50%" title="🛠️ 技术栈" segmented>
|
||||
<VChart :option="skillOption" autoresize />
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<n-card class="mt-12" title="⚡️ 趋势" segmented>
|
||||
<VChart :option="trendOption" :init-options="{ height: 400 }" 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 skillOption = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter({ name, value }) {
|
||||
return `${name} ${value}%`
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
left: 'center',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
top: '12%',
|
||||
type: 'pie',
|
||||
radius: ['35%', '90%'],
|
||||
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' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const message = $message
|
||||
</script>
|
||||
15
src/views/login/api.js
Normal file
15
src/views/login/api.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/**********************************
|
||||
* @Author: Ronnie Zhang
|
||||
* @LastEditor: Ronnie Zhang
|
||||
* @LastEditTime: 2023/12/05 21:28:30
|
||||
* @Email: zclzone@outlook.com
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
toggleRole: (data) => request.post('/auth/role/toggle', data),
|
||||
login: (data) => request.post('/auth/login', data, { noNeedToken: true }),
|
||||
getUser: () => request.get('/user/detail'),
|
||||
}
|
||||
190
src/views/login/index.vue
Normal file
190
src/views/login/index.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:28:36
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<div class="wh-full flex-col bg-[url(@/assets/images/login_bg.webp)] bg-cover">
|
||||
<div
|
||||
class="m-auto max-w-700 min-w-345 f-c-c rounded-8 bg-opacity-20 bg-cover p-12 card-shadow auto-bg"
|
||||
>
|
||||
<div class="hidden w-380 px-20 py-35 md:block">
|
||||
<img src="@/assets/images/login_banner.webp" class="w-full" alt="login_banner" />
|
||||
</div>
|
||||
|
||||
<div class="w-320 flex-col px-20 py-32">
|
||||
<h2 class="f-c-c text-24 font-normal text-#6a6a6a">
|
||||
<img src="@/assets/images/logo.png" height="50" class="mr-12" />
|
||||
{{ title }}
|
||||
</h2>
|
||||
<n-input
|
||||
v-model:value="loginInfo.username"
|
||||
autofocus
|
||||
class="mt-32 h-40 items-center"
|
||||
placeholder="请输入用户名"
|
||||
:maxlength="20"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-fe:user mr-12 opacity-20" />
|
||||
</template>
|
||||
</n-input>
|
||||
<n-input
|
||||
v-model:value="loginInfo.password"
|
||||
class="mt-20 h-40 items-center"
|
||||
type="password"
|
||||
show-password-on="mousedown"
|
||||
placeholder="请输入密码"
|
||||
:maxlength="20"
|
||||
@keydown.enter="handleLogin"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-fe:lock mr-12 opacity-20" />
|
||||
</template>
|
||||
</n-input>
|
||||
|
||||
<div class="mt-20 flex items-center">
|
||||
<n-input
|
||||
v-model:value="loginInfo.captcha"
|
||||
class="h-40 items-center"
|
||||
palceholder="请输入验证码"
|
||||
:maxlength="4"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-fe:key mr-12 opacity-20" />
|
||||
</template>
|
||||
</n-input>
|
||||
<img
|
||||
v-if="captchaUrl"
|
||||
:src="captchaUrl"
|
||||
alt="验证码"
|
||||
height="40"
|
||||
class="ml-12 w-80 cursor-pointer"
|
||||
@click="initCaptcha"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<n-checkbox
|
||||
class="mt-20"
|
||||
:checked="isRemember"
|
||||
label="记住我"
|
||||
:on-update:checked="(val) => (isRemember = val)"
|
||||
/>
|
||||
|
||||
<div class="mt-20 flex items-center">
|
||||
<n-button class="h-40 flex-1 rounded-5 text-16" type="primary" ghost @click="quickLogin">
|
||||
一键体验
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
class="ml-32 h-40 flex-1 rounded-5 text-16"
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TheFooter class="py-12" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { throttle, lStorage } from '@/utils'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import api from './api'
|
||||
import { useUserStore, useAuthStore } from '@/store'
|
||||
import { initUserAndPermissions } from '@/router'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const title = import.meta.env.VITE_TITLE
|
||||
|
||||
const isLogined = computed(() => {
|
||||
return authStore.accessToken && userStore.roles
|
||||
})
|
||||
|
||||
const loginInfo = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
function initLoginInfo() {
|
||||
const localLoginInfo = lStorage.get('loginInfo')
|
||||
if (localLoginInfo) {
|
||||
loginInfo.value.username = localLoginInfo.username || ''
|
||||
loginInfo.value.password = localLoginInfo.password || ''
|
||||
}
|
||||
}
|
||||
|
||||
const captchaUrl = ref('')
|
||||
const initCaptcha = throttle(() => {
|
||||
captchaUrl.value = '/api/auth/captcha?' + Date.now()
|
||||
}, 500)
|
||||
|
||||
if (isLogined.value) {
|
||||
router.push({ path: '/role-select', query: route.query })
|
||||
} else {
|
||||
initLoginInfo()
|
||||
initCaptcha()
|
||||
}
|
||||
|
||||
function quickLogin() {
|
||||
loginInfo.value.username = 'admin'
|
||||
loginInfo.value.password = '123456'
|
||||
handleLogin(true)
|
||||
}
|
||||
|
||||
const isRemember = useStorage('isRemember', true)
|
||||
const loading = ref(false)
|
||||
async function handleLogin(isQuick) {
|
||||
const { username, password, captcha } = loginInfo.value
|
||||
if (!username || !password) return $message.warning('请输入用户名和密码')
|
||||
if (!isQuick && !captcha) return $message.warning('请输入验证码')
|
||||
try {
|
||||
loading.value = true
|
||||
$message.loading('正在验证,请稍后...', { key: 'login' })
|
||||
const { data } = await api.login({ username, password: password.toString(), captcha, isQuick })
|
||||
if (isRemember.value) {
|
||||
lStorage.set('loginInfo', { username, password })
|
||||
} else {
|
||||
lStorage.remove('loginInfo')
|
||||
}
|
||||
onLoginSuccess(data)
|
||||
} catch (error) {
|
||||
// 10003为验证码错误专属业务码
|
||||
if (error?.code === 10003) {
|
||||
// 为防止爆破,验证码错误则刷新验证码
|
||||
initCaptcha()
|
||||
}
|
||||
$message.destroy('login')
|
||||
console.error(error)
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
async function onLoginSuccess(data = {}) {
|
||||
authStore.setToken(data)
|
||||
$message.loading('登录中...', { key: 'login' })
|
||||
try {
|
||||
await initUserAndPermissions()
|
||||
$message.success('登录成功', { key: 'login' })
|
||||
if (route.query.redirect) {
|
||||
const path = route.query.redirect
|
||||
delete route.query.redirect
|
||||
router.push({ path, query: route.query })
|
||||
} else {
|
||||
router.push('/')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$message.destroy('login')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
19
src/views/pms/resource/api.js
Normal file
19
src/views/pms/resource/api.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**********************************
|
||||
* @Author: Ronnie Zhang
|
||||
* @LastEditor: Ronnie Zhang
|
||||
* @LastEditTime: 2023/12/05 21:28:47
|
||||
* @Email: zclzone@outlook.com
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import axios from 'axios'
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getMenuTree: () => request.get('/permission/menu/tree'),
|
||||
getButtonAndApi: (parentId) => request.get(`/permission/button-and-api/${parentId}`),
|
||||
getComponents: () => axios.get(`${import.meta.env.VITE_PUBLIC_PATH}components.json`),
|
||||
addPermission: (data) => request.post('/permission', data),
|
||||
savePermission: (id, data) => request.patch(`/permission/${id}`, data),
|
||||
deletePermission: (id) => request.delete(`permission/${id}`),
|
||||
}
|
||||
116
src/views/pms/resource/components/MenuTree.vue
Normal file
116
src/views/pms/resource/components/MenuTree.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:28:59
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-space vertical :size="12">
|
||||
<h3>菜单</h3>
|
||||
<div class="flex">
|
||||
<n-input v-model:value="pattern" placeholder="搜索" clearable />
|
||||
<n-button class="ml-12" type="primary" @click="handleAdd()">
|
||||
<i class="i-material-symbols:add mr-4 text-14" />
|
||||
新增
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<n-tree
|
||||
:show-irrelevant-nodes="false"
|
||||
:pattern="pattern"
|
||||
:data="treeData"
|
||||
:render-prefix="renderPrefix"
|
||||
:render-suffix="renderSuffix"
|
||||
:on-update:selected-keys="onSelect"
|
||||
key-field="code"
|
||||
label-field="name"
|
||||
block-line
|
||||
default-expand-all
|
||||
/>
|
||||
</n-space>
|
||||
|
||||
<ResAddOrEdit ref="modalRef" :menus="treeData" @refresh="(data) => emit('refresh', data)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { withModifiers } from 'vue'
|
||||
import ResAddOrEdit from './ResAddOrEdit.vue'
|
||||
import { NButton } from 'naive-ui'
|
||||
import api from '../api'
|
||||
|
||||
defineProps({
|
||||
treeData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
currentMenu: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['refresh', 'update:currentMenu'])
|
||||
|
||||
const pattern = ref('')
|
||||
|
||||
const modalRef = ref(null)
|
||||
async function handleAdd(data = {}) {
|
||||
modalRef.value?.handleOpen({
|
||||
action: 'add',
|
||||
title: '新增菜单',
|
||||
row: { type: 'MENU', ...data },
|
||||
okText: '保存',
|
||||
})
|
||||
}
|
||||
|
||||
function onSelect(keys, option, { action, node }) {
|
||||
emit('update:currentMenu', action === 'select' ? node : null)
|
||||
}
|
||||
|
||||
function renderPrefix({ option }) {
|
||||
return h('i', { class: `${option.icon}?mask text-16` })
|
||||
}
|
||||
|
||||
function renderSuffix({ option }) {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: 'primary',
|
||||
title: '新增下级菜单',
|
||||
size: 'tiny',
|
||||
onClick: withModifiers(() => handleAdd({ parentId: option.id }), ['stop']),
|
||||
},
|
||||
{ default: () => '新增' }
|
||||
),
|
||||
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: 'error',
|
||||
size: 'tiny',
|
||||
style: 'margin-left: 12px;',
|
||||
onClick: withModifiers(() => handleDelete(option), ['stop']),
|
||||
},
|
||||
{ default: () => '删除' }
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
function handleDelete(item) {
|
||||
$dialog.confirm({
|
||||
content: `确认删除【${item.name}】?`,
|
||||
async confirm() {
|
||||
$message.loading('正在删除', { key: 'deleteMenu' })
|
||||
await api.deletePermission(item.id)
|
||||
$message.success('删除成功', { key: 'deleteMenu' })
|
||||
emit('refresh')
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
32
src/views/pms/resource/components/QuestionLabel.vue
Normal file
32
src/views/pms/resource/components/QuestionLabel.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:29:05
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<span class="flex items-center">
|
||||
<n-popover v-if="content" trigger="hover">
|
||||
<template #trigger>
|
||||
<i class="i-material-symbols:help mr-4" />
|
||||
</template>
|
||||
<span>{{ content }}</span>
|
||||
</n-popover>
|
||||
{{ label }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
201
src/views/pms/resource/components/ResAddOrEdit.vue
Normal file
201
src/views/pms/resource/components/ResAddOrEdit.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:29:12
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<MeModal ref="modalRef">
|
||||
<n-form
|
||||
ref="modalFormRef"
|
||||
label-placement="left"
|
||||
require-mark-placement="left"
|
||||
:label-width="100"
|
||||
:model="modalForm"
|
||||
>
|
||||
<n-grid :cols="24" :x-gap="24">
|
||||
<n-form-item-gi :span="12" label="所属菜单" path="parentId">
|
||||
<n-tree-select
|
||||
v-model:value="modalForm.parentId"
|
||||
:options="menuOptions"
|
||||
label-field="name"
|
||||
key-field="id"
|
||||
placeholder="根菜单"
|
||||
clearable
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" path="name" :rule="required">
|
||||
<template #label>
|
||||
<QuestionLabel label="名称" content="标题" />
|
||||
</template>
|
||||
<n-input v-model:value="modalForm.name" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" path="code" :rule="required">
|
||||
<template #label>
|
||||
<QuestionLabel label="编码" content="如果是菜单则对应前端路由的name,使用大驼峰" />
|
||||
</template>
|
||||
<n-input v-model:value="modalForm.code" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" path="path">
|
||||
<template #label>
|
||||
<QuestionLabel label="路由地址" content="父级菜单可不填" />
|
||||
</template>
|
||||
<n-input v-model:value="modalForm.path" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" path="icon">
|
||||
<template #label>
|
||||
<QuestionLabel
|
||||
label="菜单图标"
|
||||
content="如material-symbols:help,图标库地址: https://icones.js.org/collection/all"
|
||||
/>
|
||||
</template>
|
||||
<n-select v-model:value="modalForm.icon" :options="iconOptions" clearable filterable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" path="layout">
|
||||
<template #label>
|
||||
<QuestionLabel
|
||||
label="layout"
|
||||
content="对应layouts文件夹下的目录名, 为空则默认为 default"
|
||||
/>
|
||||
</template>
|
||||
<n-select v-model:value="modalForm.layout" :options="layoutOptions" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="24" path="component">
|
||||
<template #label>
|
||||
<QuestionLabel
|
||||
label="组件路径"
|
||||
content="前端组件的路径,以 /src 开头,父级菜单可不填"
|
||||
/>
|
||||
</template>
|
||||
<n-select
|
||||
v-model:value="modalForm.component"
|
||||
:options="componentOptions"
|
||||
clearable
|
||||
filterable
|
||||
tag
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<n-form-item-gi :span="12" path="show">
|
||||
<template #label>
|
||||
<QuestionLabel label="显示状态" content="控制是否在菜单栏显示,不影响路由注册" />
|
||||
</template>
|
||||
<n-switch v-model:value="modalForm.show">
|
||||
<template #checked>显示</template>
|
||||
<template #unchecked>隐藏</template>
|
||||
</n-switch>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" path="enable">
|
||||
<template #label>
|
||||
<QuestionLabel
|
||||
label="状态"
|
||||
content="如果是菜单,禁用后将不添加到路由表,无法进入此页面"
|
||||
/>
|
||||
</template>
|
||||
<n-switch v-model:value="modalForm.enable">
|
||||
<template #checked>启用</template>
|
||||
<template #unchecked>禁用</template>
|
||||
</n-switch>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" path="enable">
|
||||
<template #label>
|
||||
<QuestionLabel
|
||||
label="KeepAlive"
|
||||
content="设置keepAlive需将组件的name设置成当前菜单的code"
|
||||
/>
|
||||
</template>
|
||||
<n-switch v-model:value="modalForm.keepAlive">
|
||||
<template #checked>是</template>
|
||||
<template #unchecked>否</template>
|
||||
</n-switch>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi
|
||||
:span="12"
|
||||
label="排序"
|
||||
path="order"
|
||||
:rule="{
|
||||
type: 'number',
|
||||
required: true,
|
||||
message: '此为必填项',
|
||||
trigger: ['blur', 'change'],
|
||||
}"
|
||||
>
|
||||
<n-input-number v-model:value="modalForm.order" />
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</MeModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MeModal } from '@/components'
|
||||
import QuestionLabel from './QuestionLabel.vue'
|
||||
import { useForm, useModal } from '@/composables'
|
||||
import api from '../api'
|
||||
import pagePathes from 'isme:page-pathes'
|
||||
import icons from 'isme:icons'
|
||||
|
||||
const props = defineProps({
|
||||
menus: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const menuOptions = computed(() => {
|
||||
return [{ name: '根菜单', id: '', children: props.menus || [] }]
|
||||
})
|
||||
const componentOptions = pagePathes.map((path) => ({ label: path, value: path }))
|
||||
const iconOptions = icons.map((item) => ({
|
||||
label: () =>
|
||||
h('span', { class: 'flex items-center' }, [h('i', { class: item + ' text-18 mr-8' }), item]),
|
||||
value: item,
|
||||
}))
|
||||
const layoutOptions = [
|
||||
{ label: '默认-default', value: 'default' },
|
||||
{ label: '空白-empty', value: 'empty' },
|
||||
]
|
||||
const required = {
|
||||
required: true,
|
||||
message: '此为必填项',
|
||||
trigger: ['blur', 'change'],
|
||||
}
|
||||
|
||||
const defaultForm = { enable: true, show: true }
|
||||
const [modalFormRef, modalForm, validation] = useForm(defaultForm)
|
||||
const [modalRef, okLoading] = useModal()
|
||||
|
||||
const modalAction = ref('')
|
||||
function handleOpen(options = {}) {
|
||||
const { action, row = {}, ...rest } = options
|
||||
modalAction.value = action
|
||||
modalForm.value = { ...row }
|
||||
modalRef.value.open({ ...rest, onOk: onSave })
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (!(await validation())) return
|
||||
okLoading.value = true
|
||||
try {
|
||||
if (!modalForm.value.parentId) modalForm.value.parentId = null
|
||||
if (modalAction.value === 'add') {
|
||||
await api.addPermission(modalForm.value)
|
||||
} else if (modalAction.value === 'edit') {
|
||||
await api.savePermission(modalForm.value.id, modalForm.value)
|
||||
}
|
||||
okLoading.value = false
|
||||
$message.success('保存成功')
|
||||
emit('refresh', modalForm.value)
|
||||
} catch (error) {
|
||||
okLoading.value = false
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handleOpen,
|
||||
})
|
||||
</script>
|
||||
103
src/views/pms/resource/index.vue
Normal file
103
src/views/pms/resource/index.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:28:53
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<CommonPage>
|
||||
<div class="flex">
|
||||
<n-spin size="small" :show="treeLoading">
|
||||
<MenuTree
|
||||
v-model:currentMenu="currentMenu"
|
||||
class="w-320 shrink-0"
|
||||
:tree-data="treeData"
|
||||
@refresh="initData"
|
||||
/>
|
||||
</n-spin>
|
||||
|
||||
<div class="ml-40 w-0 flex-1">
|
||||
<template v-if="currentMenu">
|
||||
<div class="flex justify-between">
|
||||
<h3 class="mb-12">{{ currentMenu.name }}</h3>
|
||||
<n-button
|
||||
:disabled="!currentMenu"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleEdit(currentMenu)"
|
||||
>
|
||||
<i class="i-material-symbols:edit-outline mr-4 text-14" />
|
||||
编辑
|
||||
</n-button>
|
||||
</div>
|
||||
<n-descriptions label-placement="left" bordered :column="2">
|
||||
<n-descriptions-item label="编码">{{ currentMenu.code }}</n-descriptions-item>
|
||||
<n-descriptions-item label="名称">{{ currentMenu.name }}</n-descriptions-item>
|
||||
<n-descriptions-item label="路由地址">
|
||||
{{ currentMenu.path ?? '--' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="组件路径">
|
||||
{{ currentMenu.component ?? '--' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="菜单图标">
|
||||
<span v-if="currentMenu.icon" class="flex items-center">
|
||||
<i :class="`${currentMenu.icon}?mask text-22 mr-8`" />
|
||||
<span class="opacity-50">{{ currentMenu.icon }}</span>
|
||||
</span>
|
||||
<span v-else>无</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="layout">
|
||||
{{ currentMenu.layout ?? 'default' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="是否显示">
|
||||
{{ currentMenu.show ? '是' : '否' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="是否启用">
|
||||
{{ currentMenu.enable ? '是' : '否' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="KeepAlive">
|
||||
{{ currentMenu.keepAlive ? '是' : '否' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="排序">
|
||||
{{ currentMenu.order ?? '--' }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</template>
|
||||
<n-empty v-else class="h-450 f-c-c" size="large" description="请选择菜单查看详情" />
|
||||
</div>
|
||||
</div>
|
||||
<ResAddOrEdit ref="modalRef" :menus="treeData" @refresh="initData" />
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MenuTree from './components/MenuTree.vue'
|
||||
import ResAddOrEdit from './components/ResAddOrEdit.vue'
|
||||
import api from './api'
|
||||
|
||||
const treeData = ref([])
|
||||
const treeLoading = ref(false)
|
||||
async function initData(data) {
|
||||
treeLoading.value = true
|
||||
const res = await api.getMenuTree()
|
||||
treeData.value = res?.data || []
|
||||
treeLoading.value = false
|
||||
|
||||
if (data) currentMenu.value = data
|
||||
}
|
||||
initData()
|
||||
|
||||
const currentMenu = ref(null)
|
||||
|
||||
const modalRef = ref(null)
|
||||
function handleEdit(item = {}) {
|
||||
modalRef.value?.handleOpen({
|
||||
action: 'edit',
|
||||
title: '编辑菜单 - ' + item.name,
|
||||
row: item,
|
||||
okText: '保存',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
21
src/views/pms/role/api.js
Normal file
21
src/views/pms/role/api.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**********************************
|
||||
* @Author: Ronnie Zhang
|
||||
* @LastEditor: Ronnie Zhang
|
||||
* @LastEditTime: 2023/12/05 21:29:27
|
||||
* @Email: zclzone@outlook.com
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
create: (data) => request.post('/role', data),
|
||||
read: (params = {}) => request.get('/role/page', { params }),
|
||||
update: (data) => request.patch(`/role/${data.id}`, data),
|
||||
delete: (id) => request.delete(`/role/${id}`),
|
||||
|
||||
getAllPermissionTree: () => request.get('/permission/tree'),
|
||||
getAllUsers: (params = {}) => request.get('/user', { params }),
|
||||
addRoleUsers: (roleId, data) => request.patch(`/role/users/add/${roleId}`, data),
|
||||
removeRoleUsers: (roleId, data) => request.patch(`/role/users/remove/${roleId}`, data),
|
||||
}
|
||||
85
src/views/pms/role/components/CascadeTree.vue
Normal file
85
src/views/pms/role/components/CascadeTree.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:29:32
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<n-tree
|
||||
:key-field="keyField"
|
||||
:label-field="labelField"
|
||||
:selectable="false"
|
||||
block-line
|
||||
default-expand-all
|
||||
checkable
|
||||
check-on-click
|
||||
cascade
|
||||
:data="treeData"
|
||||
:checked-keys="checkedKeys"
|
||||
:on-update:checked-keys="(keys) => (checkedKeys = keys)"
|
||||
:on-update:indeterminate-keys="(keys) => (halfCheckedKeys = keys)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
treeData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
labelField: {
|
||||
type: String,
|
||||
default: 'label',
|
||||
},
|
||||
keyField: {
|
||||
type: String,
|
||||
default: 'value',
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['update:value'])
|
||||
|
||||
const halfCheckedKeys = ref([])
|
||||
const checkedKeys = ref([])
|
||||
watch([halfCheckedKeys, checkedKeys], ([v1, v2]) => {
|
||||
emit('update:value', new Array(...new Set([...v1, ...v2])))
|
||||
})
|
||||
onMounted(() => {
|
||||
halfCheckedKeys.value = getHalfCheckedValues(props.value, props.treeData)
|
||||
checkedKeys.value = props.value.filter((item) => !halfCheckedKeys.value.includes(item))
|
||||
})
|
||||
|
||||
// 获取半选状态的值
|
||||
function getHalfCheckedValues(selectedValues, treeData, halfCheckedValues = []) {
|
||||
function isHalfChecked(node) {
|
||||
// 如果存在子节点没有选中或者子节点是半选状态
|
||||
return node.children.some(
|
||||
(item) =>
|
||||
!selectedValues.includes(item[props.keyField]) ||
|
||||
halfCheckedValues.includes(item[props.keyField])
|
||||
)
|
||||
}
|
||||
|
||||
function hasGrandson(node) {
|
||||
return node.children.some((item) => !!item.children?.length)
|
||||
}
|
||||
|
||||
for (const item of treeData) {
|
||||
if (!item.children?.length) continue
|
||||
if (hasGrandson(item)) {
|
||||
// 根据孙节点判断子节点是否是半选
|
||||
getHalfCheckedValues(selectedValues, item.children, halfCheckedValues)
|
||||
isHalfChecked(item) && halfCheckedValues.push(item[props.keyField])
|
||||
} else {
|
||||
isHalfChecked(item) && halfCheckedValues.push(item[props.keyField])
|
||||
}
|
||||
}
|
||||
|
||||
return halfCheckedValues
|
||||
}
|
||||
</script>
|
||||
243
src/views/pms/role/index.vue
Normal file
243
src/views/pms/role/index.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:29:38
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<CommonPage>
|
||||
<template #action>
|
||||
<n-button type="primary" @click="handleAdd()">
|
||||
<i class="i-material-symbols:add mr-4 text-18" />
|
||||
新增角色
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<MeCrud
|
||||
ref="$table"
|
||||
v-model:query-items="queryItems"
|
||||
:scroll-x="1200"
|
||||
:columns="columns"
|
||||
:get-data="api.read"
|
||||
>
|
||||
<MeQueryItem label="角色名" :label-width="50">
|
||||
<n-input
|
||||
v-model:value="queryItems.name"
|
||||
type="text"
|
||||
placeholder="请输入角色名"
|
||||
clearable
|
||||
@keydown.enter="$table?.handleSearch"
|
||||
/>
|
||||
</MeQueryItem>
|
||||
<MeQueryItem label="状态" :label-width="50">
|
||||
<n-select
|
||||
v-model:value="queryItems.enable"
|
||||
clearable
|
||||
:options="[
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '停用', value: 0 },
|
||||
]"
|
||||
/>
|
||||
</MeQueryItem>
|
||||
</MeCrud>
|
||||
<MeModal ref="modalRef" width="520px">
|
||||
<n-form
|
||||
ref="modalFormRef"
|
||||
label-placement="left"
|
||||
label-align="left"
|
||||
:label-width="80"
|
||||
:model="modalForm"
|
||||
>
|
||||
<n-form-item
|
||||
label="角色名"
|
||||
path="name"
|
||||
:rule="{
|
||||
required: true,
|
||||
message: '请输入角色名',
|
||||
trigger: ['input', 'blur'],
|
||||
}"
|
||||
>
|
||||
<n-input v-model:value="modalForm.name" />
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="角色编码"
|
||||
path="code"
|
||||
:rule="{
|
||||
required: true,
|
||||
message: '请输入角色编码',
|
||||
trigger: ['input', 'blur'],
|
||||
}"
|
||||
>
|
||||
<n-input v-model:value="modalForm.code" :disabled="modalAction !== 'add'" />
|
||||
</n-form-item>
|
||||
<n-form-item label="权限" path="permissionIds">
|
||||
<CascadeTree
|
||||
v-model:value="modalForm.permissionIds"
|
||||
:tree-data="permissionTree"
|
||||
label-field="name"
|
||||
key-field="id"
|
||||
class="cus-scroll max-h-200 w-full"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="状态" path="enable">
|
||||
<n-switch v-model:value="modalForm.enable">
|
||||
<template #checked>启用</template>
|
||||
<template #unchecked>停用</template>
|
||||
</n-switch>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</MeModal>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NButton, NSwitch } from 'naive-ui'
|
||||
import { MeCrud, MeQueryItem, MeModal } from '@/components'
|
||||
import { useCrud } from '@/composables'
|
||||
import api from './api'
|
||||
import CascadeTree from './components/CascadeTree.vue'
|
||||
|
||||
defineOptions({ name: 'RoleMgt' })
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const $table = ref(null)
|
||||
/** QueryBar筛选参数(可选) */
|
||||
const queryItems = ref({})
|
||||
|
||||
onMounted(() => {
|
||||
$table.value?.handleSearch()
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '角色名', key: 'name' },
|
||||
{ title: '角色编码', key: 'code' },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'enable',
|
||||
render: (row) =>
|
||||
h(
|
||||
NSwitch,
|
||||
{
|
||||
size: 'small',
|
||||
rubberBand: false,
|
||||
value: row.enable,
|
||||
loading: !!row.enableLoading,
|
||||
disabled: row.code === 'SUPER_ADMIN',
|
||||
onUpdateValue: () => handleEnable(row),
|
||||
},
|
||||
{
|
||||
checked: () => '启用',
|
||||
unchecked: () => '停用',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 320,
|
||||
align: 'right',
|
||||
fixed: 'right',
|
||||
render(row) {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
secondary: true,
|
||||
onClick: () =>
|
||||
router.push({ path: `/pms/role/user/${row.id}`, query: { roleName: row.name } }),
|
||||
},
|
||||
{
|
||||
default: () => '分配用户',
|
||||
icon: () => h('i', { class: 'i-fe:user-plus text-14' }),
|
||||
}
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
style: 'margin-left: 12px;',
|
||||
disabled: row.code === 'SUPER_ADMIN',
|
||||
onClick: () => handleEdit(row),
|
||||
},
|
||||
{
|
||||
default: () => '编辑',
|
||||
icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
|
||||
}
|
||||
),
|
||||
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 12px;',
|
||||
disabled: row.code === 'SUPER_ADMIN',
|
||||
onClick: () => handleDelete(row.id),
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
||||
}
|
||||
),
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
async function handleEnable(row) {
|
||||
row.enableLoading = true
|
||||
try {
|
||||
await api.update({ id: row.id, enable: !row.enable })
|
||||
row.enableLoading = false
|
||||
$message.success('操作成功')
|
||||
$table.value?.handleSearch()
|
||||
} catch (error) {
|
||||
row.enableLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
modalRef,
|
||||
modalFormRef,
|
||||
modalAction,
|
||||
modalForm,
|
||||
okLoading,
|
||||
handleAdd,
|
||||
handleDelete,
|
||||
handleOpen,
|
||||
} = useCrud({
|
||||
name: '角色',
|
||||
doCreate: api.create,
|
||||
doDelete: api.delete,
|
||||
doUpdate: api.update,
|
||||
initForm: { enable: true },
|
||||
refresh: () => $table.value?.handleSearch(),
|
||||
})
|
||||
|
||||
function handleEdit(row) {
|
||||
handleOpen({ action: 'edit', title: '修改角色', row, onSave })
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
await modalFormRef.value?.validate()
|
||||
okLoading.value = true
|
||||
try {
|
||||
await api.update(modalForm.value)
|
||||
okLoading.value = false
|
||||
$message.success('保存成功')
|
||||
$table.value?.handleSearch()
|
||||
} catch (error) {
|
||||
okLoading.value = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const permissionTree = ref([])
|
||||
api.getAllPermissionTree().then(({ data = [] }) => (permissionTree.value = data))
|
||||
</script>
|
||||
223
src/views/pms/role/role-user.vue
Normal file
223
src/views/pms/role/role-user.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:29:43
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<CommonPage back>
|
||||
<template #title-suffix>
|
||||
<n-tag class="ml-12" type="warning">{{ route.query.roleName }}</n-tag>
|
||||
</template>
|
||||
<template #action>
|
||||
<div class="flex items-center">
|
||||
<n-button :disabled="!userIds.length" type="error" @click="handleBatchRemove()">
|
||||
<i v-if="userIds.length" class="i-material-symbols:delete-outline mr-4 text-18" />
|
||||
批量取消授权
|
||||
</n-button>
|
||||
<n-button
|
||||
class="ml-12"
|
||||
:disabled="!userIds.length"
|
||||
type="primary"
|
||||
@click="handleBatchAdd()"
|
||||
>
|
||||
<i v-if="userIds.length" class="i-line-md:confirm-circle mr-4 text-18" />
|
||||
批量授权
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<MeCrud
|
||||
ref="$table"
|
||||
v-model:query-items="queryItems"
|
||||
:scroll-x="1200"
|
||||
:columns="columns"
|
||||
:get-data="api.getAllUsers"
|
||||
@on-checked="onChecked"
|
||||
>
|
||||
<MeQueryItem label="用户名" :label-width="50">
|
||||
<n-input
|
||||
v-model:value="queryItems.username"
|
||||
type="text"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
@keydown.enter="$table?.handleSearch"
|
||||
/>
|
||||
</MeQueryItem>
|
||||
|
||||
<MeQueryItem label="性别" :label-width="50">
|
||||
<n-select v-model:value="queryItems.gender" clearable :options="genders" />
|
||||
</MeQueryItem>
|
||||
|
||||
<MeQueryItem label="状态" :label-width="50">
|
||||
<n-select
|
||||
v-model:value="queryItems.enable"
|
||||
clearable
|
||||
:options="[
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '停用', value: 0 },
|
||||
]"
|
||||
/>
|
||||
</MeQueryItem>
|
||||
</MeCrud>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
|
||||
import { MeCrud, MeQueryItem } from '@/components'
|
||||
import { formatDateTime } from '@/utils'
|
||||
import api from './api'
|
||||
import { h } from 'vue'
|
||||
|
||||
defineOptions({ name: 'RoleUser' })
|
||||
const route = useRoute()
|
||||
|
||||
const $table = ref(null)
|
||||
/** QueryBar筛选参数(可选) */
|
||||
const queryItems = ref({})
|
||||
|
||||
onMounted(() => {
|
||||
$table.value?.handleSearch()
|
||||
})
|
||||
|
||||
const genders = [
|
||||
{ label: '男', value: 1 },
|
||||
{ label: '女', value: 2 },
|
||||
]
|
||||
|
||||
const columns = [
|
||||
{ type: 'selection', fixed: 'left' },
|
||||
{
|
||||
title: '头像',
|
||||
key: 'avatar',
|
||||
width: 80,
|
||||
render: ({ avatar }) =>
|
||||
h(NAvatar, {
|
||||
size: 'medium',
|
||||
src: avatar,
|
||||
}),
|
||||
},
|
||||
{ title: '用户名', key: 'username', width: 150, ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: '角色',
|
||||
key: 'roles',
|
||||
width: 200,
|
||||
ellipsis: { tooltip: true },
|
||||
render: ({ roles }) => {
|
||||
if (roles?.length) {
|
||||
return roles.map((item, index) =>
|
||||
h(
|
||||
NTag,
|
||||
{ type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' },
|
||||
{ default: () => item.name }
|
||||
)
|
||||
)
|
||||
}
|
||||
return '暂无角色'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
key: 'gender',
|
||||
width: 80,
|
||||
render: ({ gender }) => genders.find((item) => gender === item.value)?.label ?? '',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createDate',
|
||||
width: 180,
|
||||
render(row) {
|
||||
return h('span', formatDateTime(row['createTime']))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'enable',
|
||||
width: 100,
|
||||
|
||||
render: (row) =>
|
||||
h(
|
||||
NSwitch,
|
||||
{
|
||||
size: 'small',
|
||||
rubberBand: false,
|
||||
value: row.enable,
|
||||
},
|
||||
{
|
||||
checked: () => '启用',
|
||||
unchecked: () => '停用',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 100,
|
||||
align: 'right',
|
||||
fixed: 'right',
|
||||
hideInExcel: true,
|
||||
render(row) {
|
||||
return row.roles?.some((item) => item.id === +route.params.roleId)
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
secondary: true,
|
||||
onClick: () => handleBatchRemove([row.id]),
|
||||
},
|
||||
{
|
||||
default: () => '取消授权',
|
||||
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
||||
}
|
||||
)
|
||||
: h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
secondary: true,
|
||||
onClick: () => handleBatchAdd([row.id]),
|
||||
},
|
||||
{
|
||||
default: () => '授权',
|
||||
icon: () => h('i', { class: 'i-line-md:confirm-circle text-14' }),
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const userIds = ref([])
|
||||
function onChecked(rowKeys) {
|
||||
userIds.value = rowKeys || []
|
||||
}
|
||||
|
||||
function handleBatchAdd(ids = userIds.value) {
|
||||
const roleId = route.params.roleId
|
||||
if (!roleId) return $message.error('角色异常,请重新选择角色')
|
||||
if (!ids.length) return $message.error('请先选择用户')
|
||||
$dialog.confirm({
|
||||
content: `确认分配【${route.query.roleName}】?`,
|
||||
async confirm() {
|
||||
await api.addRoleUsers(roleId, { userIds: ids })
|
||||
$table.value?.handleSearch()
|
||||
},
|
||||
})
|
||||
}
|
||||
function handleBatchRemove(ids = userIds.value) {
|
||||
const roleId = route.params.roleId
|
||||
if (!roleId) return $message.error('角色异常,请重新选择角色')
|
||||
if (!ids.length) return $message.error('请先选择用户')
|
||||
$dialog.confirm({
|
||||
content: `确认取消分配【${route.query.roleName}】?`,
|
||||
async confirm() {
|
||||
await api.removeRoleUsers(roleId, { userIds: ids })
|
||||
$table.value?.handleSearch()
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
19
src/views/pms/user/api.js
Normal file
19
src/views/pms/user/api.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**********************************
|
||||
* @Author: Ronnie Zhang
|
||||
* @LastEditor: Ronnie Zhang
|
||||
* @LastEditTime: 2023/12/05 21:29:51
|
||||
* @Email: zclzone@outlook.com
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
create: (data) => request.post('/user', data),
|
||||
read: (params = {}) => request.get('/user', { params }),
|
||||
update: (data) => request.patch(`/user/${data.id}`, data),
|
||||
delete: (id) => request.delete(`/user/${id}`),
|
||||
resetPwd: (id, data) => request.patch(`/user/password/reset/${id}`, data),
|
||||
|
||||
getAllRoles: () => request.get('/role?enable=1'),
|
||||
}
|
||||
306
src/views/pms/user/index.vue
Normal file
306
src/views/pms/user/index.vue
Normal file
@@ -0,0 +1,306 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:29:56
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<CommonPage>
|
||||
<template #action>
|
||||
<n-button v-permission="'AddUser'" type="primary" @click="handleAdd()">
|
||||
<i class="i-material-symbols:add mr-4 text-18" />
|
||||
创建新用户
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<MeCrud
|
||||
ref="$table"
|
||||
v-model:query-items="queryItems"
|
||||
:scroll-x="1200"
|
||||
:columns="columns"
|
||||
:get-data="api.read"
|
||||
>
|
||||
<MeQueryItem label="用户名" :label-width="50">
|
||||
<n-input
|
||||
v-model:value="queryItems.username"
|
||||
type="text"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
@keydown.enter="$table?.handleSearch"
|
||||
/>
|
||||
</MeQueryItem>
|
||||
|
||||
<MeQueryItem label="性别" :label-width="50">
|
||||
<n-select v-model:value="queryItems.gender" clearable :options="genders" />
|
||||
</MeQueryItem>
|
||||
|
||||
<MeQueryItem label="状态" :label-width="50">
|
||||
<n-select
|
||||
v-model:value="queryItems.enable"
|
||||
clearable
|
||||
:options="[
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '停用', value: 0 },
|
||||
]"
|
||||
/>
|
||||
</MeQueryItem>
|
||||
</MeCrud>
|
||||
|
||||
<MeModal ref="modalRef" width="520px">
|
||||
<n-form
|
||||
ref="modalFormRef"
|
||||
label-placement="left"
|
||||
label-align="left"
|
||||
:label-width="80"
|
||||
:model="modalForm"
|
||||
:disabled="modalAction === 'view'"
|
||||
>
|
||||
<n-form-item
|
||||
label="用户名"
|
||||
path="username"
|
||||
:rule="{
|
||||
required: true,
|
||||
message: '请输入用户名',
|
||||
trigger: ['input', 'blur'],
|
||||
}"
|
||||
>
|
||||
<n-input v-model:value="modalForm.username" :disabled="modalAction !== 'add'" />
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
v-if="['add', 'reset'].includes(modalAction)"
|
||||
:label="modalAction === 'reset' ? '重置密码' : '初始密码'"
|
||||
path="password"
|
||||
:rule="{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: ['input', 'blur'],
|
||||
}"
|
||||
>
|
||||
<n-input v-model:value="modalForm.password" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item v-if="['add', 'setRole'].includes(modalAction)" label="角色" path="roleIds">
|
||||
<n-select
|
||||
v-model:value="modalForm.roleIds"
|
||||
:options="roles"
|
||||
label-field="name"
|
||||
value-field="id"
|
||||
clearable
|
||||
filterable
|
||||
multiple
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="modalAction === 'add'" label="状态" path="enable">
|
||||
<n-switch v-model:value="modalForm.enable">
|
||||
<template #checked>启用</template>
|
||||
<template #unchecked>停用</template>
|
||||
</n-switch>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-alert v-if="modalAction === 'add'" type="warning" closable>
|
||||
详细信息需由用户本人补充修改
|
||||
</n-alert>
|
||||
</MeModal>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
|
||||
import { formatDateTime } from '@/utils'
|
||||
import { MeCrud, MeQueryItem, MeModal } from '@/components'
|
||||
import { useCrud } from '@/composables'
|
||||
import api from './api'
|
||||
|
||||
defineOptions({ name: 'UserMgt' })
|
||||
|
||||
const $table = ref(null)
|
||||
/** QueryBar筛选参数(可选) */
|
||||
const queryItems = ref({})
|
||||
|
||||
onMounted(() => {
|
||||
$table.value?.handleSearch()
|
||||
})
|
||||
|
||||
const genders = [
|
||||
{ label: '男', value: 1 },
|
||||
{ label: '女', value: 2 },
|
||||
]
|
||||
const roles = ref([])
|
||||
api.getAllRoles().then(({ data = [] }) => (roles.value = data))
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '头像',
|
||||
key: 'avatar',
|
||||
width: 80,
|
||||
render: ({ avatar }) =>
|
||||
h(NAvatar, {
|
||||
size: 'medium',
|
||||
src: avatar,
|
||||
}),
|
||||
},
|
||||
{ title: '用户名', key: 'username', width: 150, ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: '角色',
|
||||
key: 'roles',
|
||||
width: 200,
|
||||
ellipsis: { tooltip: true },
|
||||
render: ({ roles }) => {
|
||||
if (roles?.length) {
|
||||
return roles.map((item, index) =>
|
||||
h(
|
||||
NTag,
|
||||
{ type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' },
|
||||
{ default: () => item.name }
|
||||
)
|
||||
)
|
||||
}
|
||||
return '暂无角色'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
key: 'gender',
|
||||
width: 80,
|
||||
render: ({ gender }) => genders.find((item) => gender === item.value)?.label ?? '',
|
||||
},
|
||||
{ title: '邮箱', key: 'email', width: 150, ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createDate',
|
||||
width: 180,
|
||||
render(row) {
|
||||
return h('span', formatDateTime(row['createTime']))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'enable',
|
||||
width: 120,
|
||||
render: (row) =>
|
||||
h(
|
||||
NSwitch,
|
||||
{
|
||||
size: 'small',
|
||||
rubberBand: false,
|
||||
value: row.enable,
|
||||
loading: !!row.enableLoading,
|
||||
onUpdateValue: () => handleEnable(row),
|
||||
},
|
||||
{
|
||||
checked: () => '启用',
|
||||
unchecked: () => '停用',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 320,
|
||||
align: 'right',
|
||||
fixed: 'right',
|
||||
hideInExcel: true,
|
||||
render(row) {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
secondary: true,
|
||||
onClick: () => handleOpenRolesSet(row),
|
||||
},
|
||||
{
|
||||
default: () => '分配角色',
|
||||
icon: () => h('i', { class: 'i-carbon:user-role text-14' }),
|
||||
}
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
style: 'margin-left: 12px;',
|
||||
onClick: () => handleOpen({ action: 'reset', title: '重置密码', row, onSave }),
|
||||
},
|
||||
{
|
||||
default: () => '重置密码',
|
||||
icon: () => h('i', { class: 'i-radix-icons:reset text-14' }),
|
||||
}
|
||||
),
|
||||
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 12px;',
|
||||
onClick: () => handleDelete(row.id),
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
||||
}
|
||||
),
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
async function handleEnable(row) {
|
||||
row.enableLoading = true
|
||||
try {
|
||||
await api.update({ id: row.id, enable: !row.enable })
|
||||
row.enableLoading = false
|
||||
$message.success('操作成功')
|
||||
$table.value?.handleSearch()
|
||||
} catch (error) {
|
||||
row.enableLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenRolesSet(row) {
|
||||
const roleIds = row.roles.map((item) => item.id)
|
||||
handleOpen({
|
||||
action: 'setRole',
|
||||
title: '分配角色',
|
||||
row: { id: row.id, username: row.username, roleIds },
|
||||
onSave,
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
modalRef,
|
||||
modalFormRef,
|
||||
modalForm,
|
||||
modalAction,
|
||||
handleAdd,
|
||||
handleDelete,
|
||||
handleOpen,
|
||||
handleSave,
|
||||
} = useCrud({
|
||||
name: '用户',
|
||||
initForm: { enable: true },
|
||||
doCreate: api.create,
|
||||
doDelete: api.delete,
|
||||
doUpdate: api.update,
|
||||
refresh: () => $table.value?.handleSearch(),
|
||||
})
|
||||
|
||||
function onSave() {
|
||||
if (modalAction.value === 'setRole') {
|
||||
return handleSave({
|
||||
api: () => api.update(modalForm.value),
|
||||
cb: () => $message.success('分配成功'),
|
||||
})
|
||||
} else if (modalAction.value === 'reset') {
|
||||
return handleSave({
|
||||
api: () => api.resetPwd(modalForm.value.id, modalForm.value),
|
||||
cb: () => $message.success('密码重置成功'),
|
||||
})
|
||||
}
|
||||
handleSave()
|
||||
}
|
||||
</script>
|
||||
14
src/views/profile/api.js
Normal file
14
src/views/profile/api.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**********************************
|
||||
* @Author: Ronnie Zhang
|
||||
* @LastEditor: Ronnie Zhang
|
||||
* @LastEditTime: 2023/12/05 21:30:03
|
||||
* @Email: zclzone@outlook.com
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
changePassword: (data) => request.post('/auth/password', data),
|
||||
updateProfile: (data) => request.patch(`/user/profile/${data.id}`, data),
|
||||
}
|
||||
153
src/views/profile/index.vue
Normal file
153
src/views/profile/index.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:30:11
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<AppPage show-footer>
|
||||
<n-card>
|
||||
<n-space align="center">
|
||||
<n-avatar round :size="100" :src="userStore.avatar" />
|
||||
<div class="ml-20">
|
||||
<div class="flex items-center text-16">
|
||||
<span>用户名:</span>
|
||||
<span class="ml-12 opacity-80">{{ userStore.username }}</span>
|
||||
<n-button class="ml-32" type="primary" text @click="pwdModalRef.open()">
|
||||
<i class="i-fe:edit mr-4" />
|
||||
修改密码
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="mt-16 flex items-center">
|
||||
<n-button type="primary" ghost @click="avatarModalRef.open()">更改头像</n-button>
|
||||
<span class="ml-12 opacity-60">
|
||||
修改头像只支持在线链接,不提供上传图片功能,如有需要可自行对接!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card class="mt-20" title="个人资料信息">
|
||||
<template #header-extra>
|
||||
<n-button type="primary" text @click="profileModalRef.open()">
|
||||
<i class="i-fe:edit mr-4" />
|
||||
修改资料
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<n-descriptions
|
||||
label-placement="left"
|
||||
:label-style="{ width: '200px', textAlign: 'center' }"
|
||||
:column="1"
|
||||
bordered
|
||||
>
|
||||
<n-descriptions-item label="昵称">{{ userStore.nickName }}</n-descriptions-item>
|
||||
<n-descriptions-item label="性别">
|
||||
{{ genders.find((item) => item.value === userStore.userInfo?.gender)?.label ?? '未知' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="地址">{{ userStore.userInfo?.address }}</n-descriptions-item>
|
||||
<n-descriptions-item label="邮箱">{{ userStore.userInfo?.email }}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-card>
|
||||
|
||||
<MeModal ref="avatarModalRef" width="420px" title="更改头像" @ok="handleAvatarSave()">
|
||||
<n-input v-model:value="newAvatar" />
|
||||
</MeModal>
|
||||
|
||||
<MeModal ref="pwdModalRef" title="修改密码" width="420px" @ok="handlePwdSave()">
|
||||
<n-form
|
||||
ref="pwdFormRef"
|
||||
:model="pwdForm"
|
||||
label-placement="left"
|
||||
require-mark-placement="left"
|
||||
>
|
||||
<n-form-item label="原密码" path="oldPassword" :rule="required">
|
||||
<n-input v-model:value="pwdForm.oldPassword" type="password" placeholder="请输入原密码" />
|
||||
</n-form-item>
|
||||
<n-form-item label="新密码" path="newPassword" :rule="required">
|
||||
<n-input v-model:value="pwdForm.newPassword" type="password" placeholder="请输入新密码" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</MeModal>
|
||||
|
||||
<MeModal ref="profileModalRef" title="修改资料" width="420px" @ok="handleProfileSave()">
|
||||
<n-form ref="profileFormRef" :model="profileForm" label-placement="left">
|
||||
<n-form-item label="昵称" path="nickName">
|
||||
<n-input v-model:value="profileForm.nickName" placeholder="请输入昵称" />
|
||||
</n-form-item>
|
||||
<n-form-item label="性别" path="gender">
|
||||
<n-select
|
||||
v-model:value="profileForm.gender"
|
||||
:options="genders"
|
||||
placeholder="请选择性别"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="地址" path="address">
|
||||
<n-input v-model:value="profileForm.address" placeholder="请输入地址" />
|
||||
</n-form-item>
|
||||
<n-form-item label="邮箱" path="email">
|
||||
<n-input v-model:value="profileForm.email" placeholder="请输入邮箱" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</MeModal>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MeModal } from '@/components'
|
||||
import { useForm, useModal } from '@/composables'
|
||||
import { useUserStore } from '@/store'
|
||||
import api from './api'
|
||||
const userStore = useUserStore()
|
||||
|
||||
const required = {
|
||||
required: true,
|
||||
message: '此为必填项',
|
||||
trigger: ['blur', 'change'],
|
||||
}
|
||||
|
||||
const [pwdModalRef] = useModal()
|
||||
const [pwdFormRef, pwdForm, pwdValidation] = useForm()
|
||||
|
||||
async function handlePwdSave() {
|
||||
if (!(await pwdValidation())) return false
|
||||
await api.changePassword(pwdForm.value)
|
||||
$message.success('密码修改成功')
|
||||
userStore.getUserInfo()
|
||||
}
|
||||
|
||||
const newAvatar = ref(userStore.avatar)
|
||||
const [avatarModalRef] = useModal()
|
||||
async function handleAvatarSave() {
|
||||
if (!newAvatar.value) {
|
||||
$message.error('请输入头像地址')
|
||||
return false
|
||||
}
|
||||
await api.updateProfile({ id: userStore.userId, avatar: newAvatar.value })
|
||||
$message.success('头像修改成功')
|
||||
userStore.getUserInfo()
|
||||
}
|
||||
|
||||
const genders = [
|
||||
{ label: '保密', value: 0 },
|
||||
{ label: '男', value: 1 },
|
||||
{ label: '女', value: 2 },
|
||||
]
|
||||
const [profileModalRef] = useModal()
|
||||
const [profileFormRef, profileForm, profileValidation] = useForm({
|
||||
id: userStore.userId,
|
||||
nickName: userStore.nickName,
|
||||
gender: userStore.userInfo?.gender ?? 0,
|
||||
address: userStore.userInfo?.address,
|
||||
email: userStore.userInfo?.email,
|
||||
})
|
||||
async function handleProfileSave() {
|
||||
if (!(await profileValidation())) return false
|
||||
await api.updateProfile(profileForm.value)
|
||||
$message.success('资料修改成功')
|
||||
userStore.getUserInfo()
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user