mirror of
https://github.com/zclzone/vue-naive-admin.git
synced 2025-12-28 12:10:20 +08:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83b42bf6b8 | ||
|
|
fd08d25ccf | ||
|
|
76c3f0b64c | ||
|
|
a1db8273f5 | ||
|
|
f5ab04112f | ||
|
|
805b2e066f | ||
|
|
6979b245a9 | ||
|
|
dff8862c75 | ||
|
|
1da5e8d573 | ||
|
|
7f97dd2f5a | ||
|
|
1f69f07100 | ||
|
|
f97beeb54b | ||
|
|
57bc68e7b0 | ||
|
|
90aa54d4a4 | ||
|
|
7564f115d6 | ||
|
|
8d3753a80e | ||
|
|
a816028560 | ||
|
|
acde2c1004 | ||
|
|
cb5dd34e17 | ||
|
|
73c82520ca | ||
|
|
e465ee50bf | ||
|
|
2be3f095aa | ||
|
|
26ecafffdc | ||
|
|
7150d93394 |
42
.cz-config.js
Normal file
42
.cz-config.js
Normal file
@@ -0,0 +1,42 @@
|
||||
module.exports = {
|
||||
types: [
|
||||
{ value: 'feat', name:'feat: 新增功能' },
|
||||
{ value: 'fix', name:'fix: 修复bug' },
|
||||
{ value: 'docs', name:'docs: 文档变更' },
|
||||
{ value: 'style', name:'style: 代码格式(不影响功能,例如空格、分号等格式修正)' },
|
||||
{ value: 'refactor', name:'refactor: 代码重构(不包括 bug 修复、功能新增)' },
|
||||
{ value: 'perf', name:'perf: 性能优化' },
|
||||
{ value: 'test', name:'test: 添加、修改测试用例' },
|
||||
{ value: 'build', name:'build: 构建流程、外部依赖变更(如升级 npm 包、修改 脚手架 配置等)' },
|
||||
{ value: 'ci', name:'ci: 修改 CI 配置、脚本' },
|
||||
{ value: 'chore', name:'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' },
|
||||
{ value: 'revert', name:'revert: 回滚 commit' },
|
||||
],
|
||||
scopes: [
|
||||
['custom', '自定义3'],
|
||||
['projects', '项目搭建'],
|
||||
['components', '组件相关'],
|
||||
['utils', 'utils 相关'],
|
||||
['styles', '样式相关'],
|
||||
['deps', '项目依赖'],
|
||||
['other', '其他修改'],
|
||||
].map(([value, description]) => {
|
||||
return {
|
||||
value,
|
||||
name: `${value.padEnd(30)} (${description})`
|
||||
}
|
||||
}),
|
||||
messages: {
|
||||
type: '确保本次提交遵循 Angular 规范!选择你要提交的类型:\n',
|
||||
scope: '选择一个 scope(可选):',
|
||||
customScope: '请输入自定义的 scope:',
|
||||
subject: '填写简短精炼的变更描述:',
|
||||
body: '填写更加详细的变更描述(可选)。使用 "|" 换行:',
|
||||
breaking: '列举非兼容性重大的变更(可选):',
|
||||
footer: '列举出所有变更的 Issues Closed(可选)。 例如: #31, #34:',
|
||||
confirmCommit: '确认提交?'
|
||||
},
|
||||
allowBreakingChanges: ['feat', 'fix'],
|
||||
subjectLimit: 100,
|
||||
breaklineChar: '|'
|
||||
}
|
||||
@@ -2,7 +2,7 @@ module.exports = {
|
||||
root: true,
|
||||
extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'],
|
||||
rules: {
|
||||
'prettier/prettier': 'warn',
|
||||
'prettier/prettier': 'error',
|
||||
'vue/valid-template-root': 'off',
|
||||
'vue/no-multiple-template-root': 'off',
|
||||
'vue/multi-word-component-names': [
|
||||
|
||||
36
.husky/_/husky.sh
Normal file
36
.husky/_/husky.sh
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env sh
|
||||
if [ -z "$husky_skip_init" ]; then
|
||||
debug () {
|
||||
if [ "$HUSKY_DEBUG" = "1" ]; then
|
||||
echo "husky (debug) - $1"
|
||||
fi
|
||||
}
|
||||
|
||||
readonly hook_name="$(basename -- "$0")"
|
||||
debug "starting $hook_name..."
|
||||
|
||||
if [ "$HUSKY" = "0" ]; then
|
||||
debug "HUSKY env variable is set to 0, skipping hook"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -f ~/.huskyrc ]; then
|
||||
debug "sourcing ~/.huskyrc"
|
||||
. ~/.huskyrc
|
||||
fi
|
||||
|
||||
readonly husky_skip_init=1
|
||||
export husky_skip_init
|
||||
sh -e "$0" "$@"
|
||||
exitCode="$?"
|
||||
|
||||
if [ $exitCode != 0 ]; then
|
||||
echo "husky - $hook_name hook exited with code $exitCode (error)"
|
||||
fi
|
||||
|
||||
if [ $exitCode = 127 ]; then
|
||||
echo "husky - command not found in PATH=$PATH"
|
||||
fi
|
||||
|
||||
exit $exitCode
|
||||
fi
|
||||
4
.husky/commit-msg
Normal file
4
.husky/commit-msg
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx --no-install commitlint --edit "$1"
|
||||
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run lint
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"files.eol": "\n",
|
||||
"path-intellisense.mappings": {
|
||||
"@/": "${workspaceRoot}/src"
|
||||
},
|
||||
"editor.formatOnSave": false,
|
||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
|
||||
"editor.codeActionsOnSave": {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
### 简介
|
||||
|
||||
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin),一个基于 Vue3.0、Vite、Naive UI 的后台管理模板,相较于其他比较流行的后台管理模板,此项目相对简洁、轻量,学习成本非常低,对新手极其友好。不过麻雀虽小五脏俱全,权限、Mock、菜单、axios 封装、pinia、项目配置、样式配置、环境配置,以及一些经常用的基础组件封装等等这些该有的都有,非常适用于中小型项目或者个人项目,也可此模板进行二次封装改造用于大型项目。
|
||||
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin)是一个 **完全开源免费且允许商用** 的后台管理模板,基于 Vue3、Vite2、Pinia 和 Naive UI等前端最新技术栈。相较于其他比较流行的后台管理模板,此项目相对简洁、轻量,学习成本非常低。麻雀虽小,五脏俱全,权限、Mock、菜单、axios 封装、pinia、项目配置、样式配置、环境配置,以及一些经常用的基础组件封装等等这些该有的都有,非常适合中小型项目或者个人项目。
|
||||
|
||||
### 为什么要开发这个模板
|
||||
|
||||
@@ -113,10 +113,10 @@ npm run preview
|
||||
- `mod` 不确定分类的修改
|
||||
- `release` 发布
|
||||
|
||||
### 入群交流
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img src="https://assets.qszone.com/image/Snipaste_2022-06-23_19-26-26.png" />
|
||||
<p>
|
||||
<img src="https://assets.qszone.com/image/入群.png" />
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createHtmlPlugin } from 'vite-plugin-html'
|
||||
|
||||
export function configHtmlPlugin(viteEnv, isBuild) {
|
||||
const { VITE_APP_TITLE, VITE_PUBLIC_PATH } = viteEnv
|
||||
const { VITE_APP_TITLE } = viteEnv
|
||||
|
||||
const htmlPlugin = createHtmlPlugin({
|
||||
minify: isBuild,
|
||||
|
||||
@@ -4,7 +4,7 @@ import vue from '@vitejs/plugin-vue'
|
||||
* * 扩展setup插件,支持在script标签中使用name属性
|
||||
* usage: <script setup name="MyComp"></script>
|
||||
*/
|
||||
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
|
||||
import vueSetupExtend from 'vite-plugin-vue-setup-extend-plus'
|
||||
|
||||
/**
|
||||
* * unocss插件,原子css
|
||||
@@ -20,7 +20,7 @@ import { configMockPlugin } from './mock'
|
||||
import unplugin from './unplugin'
|
||||
|
||||
export function createVitePlugins(viteEnv, isBuild) {
|
||||
const plugins = [vue(), VueSetupExtend(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
|
||||
const plugins = [vue(), vueSetupExtend(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
|
||||
|
||||
if (viteEnv?.VITE_APP_USE_MOCK) {
|
||||
plugins.push(configMockPlugin(isBuild))
|
||||
|
||||
@@ -2,12 +2,11 @@ import { viteMockServe } from 'vite-plugin-mock'
|
||||
|
||||
export function configMockPlugin(isBuild) {
|
||||
return viteMockServe({
|
||||
ignore: /^\_/,
|
||||
mockPath: 'mock',
|
||||
mockPath: 'mock/modules',
|
||||
localEnabled: !isBuild,
|
||||
prodEnabled: isBuild,
|
||||
injectCode: `
|
||||
import { setupProdMockServer } from '../mock/_create-prod-server';
|
||||
import { setupProdMockServer } from '../mock';
|
||||
setupProdMockServer();
|
||||
`,
|
||||
})
|
||||
|
||||
4
commitlint.config.js
Normal file
4
commitlint.config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
ignores: [(commit) => commit.includes('init')],
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
|
||||
|
||||
const modules = import.meta.globEager('./**/*.js')
|
||||
const modules = import.meta.globEager('./modules/*.js')
|
||||
const mockModules = []
|
||||
Object.keys(modules).forEach((key) => {
|
||||
if (key.includes('/_')) {
|
||||
return
|
||||
}
|
||||
mockModules.push(...modules[key].default)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { resolveToken } from '../_utils'
|
||||
import { resolveToken } from '../utils'
|
||||
|
||||
const token = {
|
||||
admin: 'admin',
|
||||
@@ -1,4 +1,4 @@
|
||||
import { resolveToken } from '../_utils'
|
||||
import { resolveToken } from '../utils'
|
||||
|
||||
const users = {
|
||||
admin: {
|
||||
19
package.json
19
package.json
@@ -8,7 +8,9 @@
|
||||
"build": "vite build",
|
||||
"build:test": "vite build --mode test",
|
||||
"build:github": "vite build --mode github && esno ./build/script",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"prepare": "husky install",
|
||||
"cz": "cz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^8.4.2",
|
||||
@@ -21,11 +23,16 @@
|
||||
"vue-router": "^4.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.3",
|
||||
"@commitlint/config-conventional": "^17.0.3",
|
||||
"@iconify/json": "^2.1.63",
|
||||
"@iconify/vue": "^3.2.1",
|
||||
"@vitejs/plugin-vue": "^1.10.2",
|
||||
"@vue/compiler-sfc": "^3.2.31",
|
||||
"chalk": "^5.0.1",
|
||||
"commitizen": "^4.2.4",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"cz-customizable": "^6.9.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"eslint": "^8.12.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
@@ -33,17 +40,23 @@
|
||||
"eslint-plugin-vue": "^8.5.0",
|
||||
"esno": "^0.13.0",
|
||||
"fs-extra": "^10.0.1",
|
||||
"husky": "^8.0.1",
|
||||
"naive-ui": "^2.30.3",
|
||||
"prettier": "^2.6.1",
|
||||
"rollup-plugin-visualizer": "^5.6.0",
|
||||
"sass": "^1.49.10",
|
||||
"unocss": "^0.38.2",
|
||||
"unocss": "^0.43.2",
|
||||
"unplugin-auto-import": "^0.8.8",
|
||||
"unplugin-icons": "^0.14.1",
|
||||
"unplugin-vue-components": "^0.17.21",
|
||||
"vite": "^2.9.9",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-mock": "^2.9.6",
|
||||
"vite-plugin-vue-setup-extend": "^0.3.0"
|
||||
"vite-plugin-vue-setup-extend-plus": "^0.1.0"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "node_modules/cz-customizable"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1569
pnpm-lock.yaml
generated
1569
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,4 +3,5 @@ module.exports = {
|
||||
singleQuote: true,
|
||||
semi: false,
|
||||
endOfLine: 'lf',
|
||||
bracketSameLine: true,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { defAxios as request } from '@/utils/http'
|
||||
|
||||
export function getPosts(data = {}) {
|
||||
export function getPosts(params = {}) {
|
||||
return request({
|
||||
url: '/posts',
|
||||
method: 'get',
|
||||
data,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { defAxios as request } from '@/utils/http'
|
||||
|
||||
export function getUsers(data = {}) {
|
||||
export function getUsers(params = {}) {
|
||||
return request({
|
||||
url: '/users',
|
||||
method: 'get',
|
||||
data,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="tags-wrapper" @mousewheel.prevent="handleMouseWheel">
|
||||
<div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel">
|
||||
<template v-if="showArrow && isOverflow">
|
||||
<div class="left" @click="handleMouseWheel({ wheelDelta: 50 })">
|
||||
<div class="left" @click="handleMouseWheel({ wheelDelta: 120 })">
|
||||
<icon-ic:baseline-keyboard-arrow-left />
|
||||
</div>
|
||||
<div class="right" @click="handleMouseWheel({ wheelDelta: -50 })">
|
||||
<div class="right" @click="handleMouseWheel({ wheelDelta: -120 })">
|
||||
<icon-ic:baseline-keyboard-arrow-right />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
ref="content"
|
||||
class="tags-content"
|
||||
class="content"
|
||||
:class="{ overflow: isOverflow && showArrow }"
|
||||
:style="{
|
||||
height: height + 'px',
|
||||
transform: `translateX(${translateX}px)`,
|
||||
}"
|
||||
>
|
||||
}">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,37 +23,26 @@
|
||||
|
||||
<script setup>
|
||||
import { debounce } from '@/utils'
|
||||
import { isNullOrUndef } from '@/utils/is'
|
||||
|
||||
defineProps({
|
||||
height: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
showArrow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
refreshIsOverflow()
|
||||
})
|
||||
|
||||
const translateX = ref(0)
|
||||
const content = ref(null)
|
||||
const wrapper = ref(null)
|
||||
const isOverflow = ref(false)
|
||||
|
||||
function refreshIsOverflow(isIncrease) {
|
||||
isOverflow.value = content.value.offsetWidth > wrapper.value.offsetWidth
|
||||
if (isNullOrUndef(isIncrease)) return
|
||||
if (isOverflow.value) {
|
||||
handleMouseWheel({ wheelDelta: isIncrease ? -100 : 100 })
|
||||
} else if (!isIncrease && translateX.value < 0) {
|
||||
handleMouseWheel({ wheelDelta: 100 })
|
||||
}
|
||||
}
|
||||
const refreshIsOverflow = debounce(() => {
|
||||
const wrapperWidth = wrapper.value.offsetWidth
|
||||
const contentWidth = content.value.offsetWidth
|
||||
isOverflow.value = contentWidth > wrapperWidth
|
||||
resetTranslateX(wrapperWidth, contentWidth)
|
||||
}, 200)
|
||||
|
||||
function handleMouseWheel(e) {
|
||||
const { wheelDelta } = e
|
||||
const wrapperWidth = wrapper.value.offsetWidth
|
||||
@@ -66,15 +53,15 @@ function handleMouseWheel(e) {
|
||||
* @wrapperWidth 容器的宽度
|
||||
* @contentWidth 内容的宽度
|
||||
*/
|
||||
if (wheelDelta < 0 && -translateX.value > contentWidth - wrapperWidth + 10) {
|
||||
return
|
||||
if (wheelDelta < 0) {
|
||||
if (wrapperWidth > contentWidth && translateX.value < -10) return
|
||||
if (wrapperWidth <= contentWidth && contentWidth + translateX.value - wrapperWidth < -10) return
|
||||
}
|
||||
if (wheelDelta > 0 && translateX.value > 10) {
|
||||
return
|
||||
}
|
||||
|
||||
translateX.value += wheelDelta
|
||||
|
||||
resetTranslateX(wrapperWidth, contentWidth)
|
||||
}
|
||||
|
||||
@@ -88,20 +75,29 @@ const resetTranslateX = debounce(function (wrapperWidth, contentWidth) {
|
||||
}
|
||||
}, 200)
|
||||
|
||||
defineExpose({
|
||||
refreshIsOverflow,
|
||||
const observer = new MutationObserver(refreshIsOverflow)
|
||||
onMounted(() => {
|
||||
refreshIsOverflow()
|
||||
|
||||
window.addEventListener('resize', refreshIsOverflow)
|
||||
// 监听内容宽度刷新是否超出
|
||||
observer.observe(content.value, { childList: true })
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', refreshIsOverflow)
|
||||
observer.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-wrapper {
|
||||
.wrapper {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
z-index: 9;
|
||||
overflow: hidden;
|
||||
.tags-content {
|
||||
position: relative;
|
||||
.content {
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<template>
|
||||
<n-breadcrumb>
|
||||
<n-breadcrumb-item v-for="item in route.matched" :key="item.path" @click="handleBreadClick(item.path)">
|
||||
<component :is="renderIcon(item.meta?.icon, { size: 16 })" v-if="item.meta?.icon" />
|
||||
{{ item.meta.title }}
|
||||
</n-breadcrumb-item>
|
||||
</n-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
:collapsed-width="64"
|
||||
:options="menuOptions"
|
||||
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
|
||||
@update:value="handleMenuSelect"
|
||||
/>
|
||||
@update:value="handleMenuSelect" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
:y="y"
|
||||
placement="bottom-start"
|
||||
@clickoutside="handleHideDropdown"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
@select="handleSelect" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ScrollX ref="scrollX" :height="useTheme.tags.height">
|
||||
<ScrollX :class="`h-${useTheme.tags.height}`">
|
||||
<n-tag
|
||||
v-for="tag in tagsStore.tags"
|
||||
:key="tag.path"
|
||||
@@ -7,8 +7,7 @@
|
||||
:closable="tagsStore.tags.length > 1"
|
||||
@click="handleTagClick(tag.path)"
|
||||
@close.stop="tagsStore.removeTag(tag.path)"
|
||||
@contextmenu.prevent="handleContextMenu($event, tag)"
|
||||
>
|
||||
@contextmenu.prevent="handleContextMenu($event, tag)">
|
||||
{{ tag.title }}
|
||||
</n-tag>
|
||||
</ScrollX>
|
||||
@@ -17,8 +16,7 @@
|
||||
v-model:show="contextMenuOption.show"
|
||||
:current-path="contextMenuOption.currentPath"
|
||||
:x="contextMenuOption.x"
|
||||
:y="contextMenuOption.y"
|
||||
/>
|
||||
:y="contextMenuOption.y" />
|
||||
</template>
|
||||
|
||||
<script setup name="Tags">
|
||||
@@ -49,15 +47,6 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const scrollX = ref(null)
|
||||
watch(
|
||||
() => tagsStore.tags,
|
||||
async (newVal, oldVal) => {
|
||||
await nextTick()
|
||||
scrollX.value?.refreshIsOverflow(newVal.length > oldVal.length)
|
||||
}
|
||||
)
|
||||
|
||||
const handleTagClick = (path) => {
|
||||
tagsStore.setActiveTag(path)
|
||||
router.push(path)
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
:collapsed-width="64"
|
||||
:width="220"
|
||||
:native-scrollbar="false"
|
||||
:collapsed="appStore.collapsed"
|
||||
>
|
||||
:collapsed="appStore.collapsed">
|
||||
<SideBar />
|
||||
</n-layout-sider>
|
||||
<n-layout>
|
||||
@@ -22,8 +21,7 @@
|
||||
:style="{
|
||||
height: `calc(100% - ${useTheme.tags.visible ? useTheme.tags.height : 0}px)`,
|
||||
overflow: 'auto',
|
||||
}"
|
||||
/>
|
||||
}" />
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
|
||||
@@ -9,6 +9,7 @@ export default [
|
||||
meta: {
|
||||
title: '组件示例',
|
||||
role: ['admin'],
|
||||
icon: 'mdi:menu',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createSessionStorage } from '@/utils/cache'
|
||||
import { sStorage } from '@/utils/cache'
|
||||
|
||||
export const tagsSS = createSessionStorage({ prefixKey: 'tag_' })
|
||||
export const activeTag = tagsSS.get('activeTag')
|
||||
export const tags = tagsSS.get('tags')
|
||||
export const activeTag = sStorage.get('activeTag')
|
||||
export const tags = sStorage.get('tags')
|
||||
|
||||
export const WITHOUT_TAG_PATHS = ['/404', '/login', '/redirect']
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { tagsSS, activeTag, tags, WITHOUT_TAG_PATHS } from './helpers'
|
||||
import { activeTag, tags, WITHOUT_TAG_PATHS } from './helpers'
|
||||
import { router } from '@/router'
|
||||
import { sStorage } from '@/utils/cache'
|
||||
|
||||
export const useTagsStore = defineStore('tag', {
|
||||
state() {
|
||||
@@ -12,11 +13,11 @@ export const useTagsStore = defineStore('tag', {
|
||||
actions: {
|
||||
setActiveTag(path) {
|
||||
this.activeTag = path
|
||||
tagsSS.set('activeTag', path)
|
||||
sStorage.set('activeTag', path)
|
||||
},
|
||||
setTags(tags) {
|
||||
this.tags = tags
|
||||
tagsSS.set('tags', tags)
|
||||
sStorage.set('tags', tags)
|
||||
},
|
||||
addTag(tag = {}) {
|
||||
this.setActiveTag(tag.path)
|
||||
|
||||
12
src/utils/cache/index.js
vendored
12
src/utils/cache/index.js
vendored
@@ -1,15 +1,21 @@
|
||||
import { createWebStorage } from './web-storage'
|
||||
import { createStorage } from './storage'
|
||||
|
||||
const prefixKey = 'Vue_Naive_Admin_'
|
||||
|
||||
export const createLocalStorage = function (option = {}) {
|
||||
return createWebStorage({
|
||||
return createStorage({
|
||||
prefixKey: option.prefixKey || '',
|
||||
storage: localStorage,
|
||||
})
|
||||
}
|
||||
|
||||
export const createSessionStorage = function (option = {}) {
|
||||
return createWebStorage({
|
||||
return createStorage({
|
||||
prefixKey: option.prefixKey || '',
|
||||
storage: sessionStorage,
|
||||
})
|
||||
}
|
||||
|
||||
export const lStorage = createLocalStorage({ prefixKey })
|
||||
|
||||
export const sStorage = createSessionStorage({ prefixKey })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isNullOrUndef } from '@/utils/is'
|
||||
|
||||
class WebStorage {
|
||||
class Storage {
|
||||
constructor(option) {
|
||||
this.storage = option.storage
|
||||
this.prefixKey = option.prefixKey
|
||||
@@ -50,6 +50,6 @@ class WebStorage {
|
||||
}
|
||||
}
|
||||
|
||||
export function createWebStorage({ prefixKey = '', storage = sessionStorage }) {
|
||||
return new WebStorage({ prefixKey, storage })
|
||||
export function createStorage({ prefixKey = '', storage = sessionStorage }) {
|
||||
return new Storage({ prefixKey, storage })
|
||||
}
|
||||
@@ -52,6 +52,9 @@ export function repReject(error) {
|
||||
* TODO 此处可以根据后端返回的错误码自定义框架层面的错误处理
|
||||
*/
|
||||
switch (code) {
|
||||
case 400:
|
||||
message = message || '请求参数错误'
|
||||
break
|
||||
case 401:
|
||||
message = message || '登录已过期'
|
||||
break
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import { createLocalStorage } from './cache'
|
||||
import { lStorage } from './cache'
|
||||
import { refreshToken } from '@/api/auth'
|
||||
|
||||
const TOKEN_CODE = 'access_token'
|
||||
const DURATION = 6 * 60 * 60
|
||||
|
||||
export const lsToken = createLocalStorage()
|
||||
|
||||
export function getToken() {
|
||||
return lsToken.get(TOKEN_CODE)
|
||||
return lStorage.get(TOKEN_CODE)
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
lsToken.set(TOKEN_CODE, token, DURATION)
|
||||
lStorage.set(TOKEN_CODE, token, DURATION)
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
lsToken.remove(TOKEN_CODE)
|
||||
lStorage.remove(TOKEN_CODE)
|
||||
}
|
||||
|
||||
export async function refreshAccessToken() {
|
||||
const tokenItem = lsToken.getItem(TOKEN_CODE)
|
||||
const tokenItem = lStorage.getItem(TOKEN_CODE)
|
||||
if (!tokenItem) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -36,8 +36,7 @@
|
||||
class="w-300 flex-shrink-0 mt-10 mb-10 cursor-pointer"
|
||||
hover:card-shadow
|
||||
title="Vue Naive Admin"
|
||||
size="small"
|
||||
>
|
||||
size="small">
|
||||
<p op-60>一个基于 Vue3.0、Vite、Naive UI 的轻量级后台管理模板</p>
|
||||
</n-card>
|
||||
<div w-300 h-0></div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="page-404">
|
||||
<n-result status="404" description="抱歉,您访问的页面不存在。">
|
||||
<div h-full flex>
|
||||
<n-result m-auto status="404" description="抱歉,您访问的页面不存在。">
|
||||
<template #icon>
|
||||
<img src="@/assets/images/404.png" width="500" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<n-button strong secondary type="primary" @click="replace('/')">返回首页</n-button>
|
||||
<n-button @click="replace('/')">返回首页</n-button>
|
||||
</template>
|
||||
</n-result>
|
||||
</div>
|
||||
@@ -14,12 +14,3 @@
|
||||
<script setup>
|
||||
const { replace } = useRouter()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-404 {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<template>
|
||||
<div p-20>
|
||||
<div class="header">
|
||||
<input v-model="post.title" type="text" placeholder="输入文章标题..." class="title" />
|
||||
<div p-24>
|
||||
<div h-60 pl-20 pr-20 flex items-center bg-white>
|
||||
<input
|
||||
v-model="post.title"
|
||||
class="flex-1 pt-15 pb-15 mr-20 text-20 font-bold color-primary"
|
||||
type="text"
|
||||
placeholder="输入文章标题..." />
|
||||
<n-button type="primary" style="width: 80px" :loading="btnLoading" @click="handleSavePost">保存</n-button>
|
||||
</div>
|
||||
<MdEditor v-model="post.content" style="height: calc(100vh - 210px)" />
|
||||
<MdEditor v-model="post.content" style="height: calc(100vh - 220px)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,21 +41,3 @@ function handleSavePost(e) {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
background-color: #fff;
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.title {
|
||||
flex: 1;
|
||||
padding: 15px 0;
|
||||
margin-right: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--primaryColor);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div p24>
|
||||
<div class="action-btns">
|
||||
<div p-24>
|
||||
<div flex>
|
||||
<n-button size="small" type="primary" @click="handleCreate">新建文章</n-button>
|
||||
</div>
|
||||
<n-data-table
|
||||
@@ -11,8 +11,7 @@
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
:row-key="(row) => row.id"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
/>
|
||||
@update:checked-row-keys="handleCheck" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,9 +36,3 @@ function handleCheck(rowKeys) {
|
||||
if (rowKeys.length) $message.info(`选中${rowKeys.join(' ')}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.action-btns {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,28 +10,31 @@
|
||||
<icon-custom-logo mr30 text-50 />
|
||||
{{ title }}
|
||||
</h5>
|
||||
<div mt-35 w-full max-w-360>
|
||||
<div mt-30 w-full max-w-360>
|
||||
<n-input
|
||||
v-model:value="loginInfo.name"
|
||||
autofocus
|
||||
class="text-16 items-center h-50 pl-10"
|
||||
placeholder="请输入用户名"
|
||||
:maxlength="20"
|
||||
>
|
||||
placeholder="admin"
|
||||
:maxlength="20">
|
||||
</n-input>
|
||||
</div>
|
||||
<div mt-35 w-full max-w-360>
|
||||
<div mt-30 w-full max-w-360>
|
||||
<n-input
|
||||
v-model:value="loginInfo.password"
|
||||
class="text-16 items-center h-50 pl-10"
|
||||
type="password"
|
||||
show-password-on="mousedown"
|
||||
placeholder="密码"
|
||||
placeholder="123456"
|
||||
:maxlength="20"
|
||||
@keydown.enter="handleLogin"
|
||||
/>
|
||||
@keydown.enter="handleLogin" />
|
||||
</div>
|
||||
<div mt-35 w-full max-w-360>
|
||||
|
||||
<div mt-20 w-full max-w-360>
|
||||
<n-checkbox :checked="isRemember" label="记住我" :on-update:checked="(val) => (isRemember = val)" />
|
||||
</div>
|
||||
|
||||
<div mt-20 w-full max-w-360>
|
||||
<n-button w-full h-50 rounded-5 text-16 type="primary" @click="handleLogin">登录</n-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,8 +44,9 @@
|
||||
|
||||
<script setup>
|
||||
import { login } from '@/api/auth'
|
||||
import { createLocalStorage } from '@/utils/cache'
|
||||
import { lStorage } from '@/utils/cache'
|
||||
import { setToken } from '@/utils/token'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const title = import.meta.env.VITE_APP_TITLE
|
||||
|
||||
@@ -50,17 +54,21 @@ const router = useRouter()
|
||||
const query = unref(router.currentRoute).query
|
||||
|
||||
const loginInfo = ref({
|
||||
name: 'admin',
|
||||
password: '123456',
|
||||
name: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const ls = createLocalStorage({ prefixKey: 'login_' })
|
||||
const lsLoginInfo = ls.get('loginInfo')
|
||||
if (lsLoginInfo) {
|
||||
loginInfo.value.name = lsLoginInfo.name || ''
|
||||
loginInfo.value.password = lsLoginInfo.password || ''
|
||||
initLoginInfo()
|
||||
|
||||
function initLoginInfo() {
|
||||
const localLoginInfo = lStorage.get('loginInfo')
|
||||
if (localLoginInfo) {
|
||||
loginInfo.value.name = localLoginInfo.name || ''
|
||||
loginInfo.value.password = localLoginInfo.password || ''
|
||||
}
|
||||
}
|
||||
|
||||
const isRemember = useStorage('isRemember', false)
|
||||
async function handleLogin() {
|
||||
const { name, password } = loginInfo.value
|
||||
if (!name || !password) {
|
||||
@@ -72,9 +80,12 @@ async function handleLogin() {
|
||||
const res = await login({ name, password: password.toString() })
|
||||
if (res.code === 0) {
|
||||
$message.success('登录成功')
|
||||
ls.set('loginInfo', { name, password })
|
||||
setToken(res.data.token)
|
||||
|
||||
if (isRemember.value) {
|
||||
lStorage.set('loginInfo', { name, password })
|
||||
} else {
|
||||
lStorage.remove('loginInfo')
|
||||
}
|
||||
if (query.redirect) {
|
||||
const path = query.redirect
|
||||
Reflect.deleteProperty(query, 'redirect')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineConfig, presetAttributify, presetIcons, presetUno } from 'unocss'
|
||||
import { defineConfig, presetAttributify, presetUno } from 'unocss'
|
||||
|
||||
export default defineConfig({
|
||||
shortcuts: [
|
||||
|
||||
Reference in New Issue
Block a user