1
0
mirror of https://github.com/zclzone/vue-naive-admin.git synced 2025-12-28 04:00:22 +08:00

24 Commits

Author SHA1 Message Date
张传龙
83b42bf6b8 chore(projects): add husky and commitlint 2022-07-10 14:02:02 +08:00
张传龙
fd08d25ccf perf: optimize ScrooX component. 2022-07-09 15:03:39 +08:00
张传龙
76c3f0b64c perf: optimize ScrooX component. 2022-07-09 14:38:38 +08:00
张传龙
a1db8273f5 chore: update unocss dependencies. 2022-07-08 22:35:55 +08:00
张传龙
f5ab04112f chore: update settings.json 2022-07-08 17:43:57 +08:00
张传龙
805b2e066f docs: update readme 2022-07-06 10:10:23 +08:00
Ronnie Zhang
6979b245a9 Merge pull request #19 from sean3112/main
微调一下demo
2022-07-05 20:15:27 +08:00
Sean Huang
dff8862c75 feat: Add response code 400.
fix: Change the parameter naming of the get method to params.
2022-07-05 18:35:05 +08:00
Sean Huang
1da5e8d573 Merge remote-tracking branch 'origin/main' 2022-07-05 18:12:01 +08:00
张传龙
7f97dd2f5a style: update prettier format rules 2022-07-03 14:52:49 +08:00
Sean Huang
1f69f07100 Merge remote-tracking branch 'origin/main' 2022-07-03 00:15:38 +08:00
张传龙
f97beeb54b perf: add remember me feature 2022-07-02 00:03:34 +08:00
张传龙
57bc68e7b0 refactor: simplify wrapper storage 2022-07-01 23:27:05 +08:00
Ronnie Zhang
90aa54d4a4 Merge pull request #18 from sean3112/patch-1
Breakpoints issues, depends on 'vite-plugin-vue-setup-extend-plus' instead of 'vite-plugin-vue-setup-extend'
2022-07-01 15:24:32 +08:00
Sean Huang
7564f115d6 Breakpoints issues
Solved the problem that the breakpoint is not in the source code location when debugging.
2022-07-01 15:07:01 +08:00
Sean Huang
8d3753a80e Update package.json
Breakpoints are not in the source code location during debugging
2022-07-01 12:58:43 +08:00
Sean Huang
a816028560 调试时,断点不在源码位置处,更新插件依赖vite-plugin-vue-setup-extend为vite-plugin-vue-setup-extend-plus即可。 2022-07-01 12:38:38 +08:00
张传龙
acde2c1004 feat: Breadcrumb add Icon 2022-06-30 18:29:26 +08:00
张传龙
cb5dd34e17 refactor: simplify mock setup 2022-06-26 18:42:07 +08:00
张传龙
73c82520ca mod: use unocss rewrite the demo page 2022-06-26 18:25:14 +08:00
张传龙
e465ee50bf mod: use unocss rewrite the 404 page 2022-06-26 15:39:44 +08:00
张传龙
2be3f095aa mod: delete extra code 2022-06-26 15:37:57 +08:00
张传龙
26ecafffdc docs: update readme 2022-06-26 15:26:52 +08:00
张传龙
7150d93394 docs: update readme 2022-06-26 15:09:00 +08:00
40 changed files with 1689 additions and 287 deletions

42
.cz-config.js Normal file
View 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: '|'
}

View File

@@ -2,7 +2,7 @@ module.exports = {
root: true, root: true,
extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'], extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'],
rules: { rules: {
'prettier/prettier': 'warn', 'prettier/prettier': 'error',
'vue/valid-template-root': 'off', 'vue/valid-template-root': 'off',
'vue/no-multiple-template-root': 'off', 'vue/no-multiple-template-root': 'off',
'vue/multi-word-component-names': [ 'vue/multi-word-component-names': [

36
.husky/_/husky.sh Normal file
View 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
View 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
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint

View File

@@ -1,8 +1,5 @@
{ {
"files.eol": "\n", "files.eol": "\n",
"path-intellisense.mappings": {
"@/": "${workspaceRoot}/src"
},
"editor.formatOnSave": false, "editor.formatOnSave": false,
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"], "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {

View File

@@ -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` 不确定分类的修改 - `mod` 不确定分类的修改
- `release` 发布 - `release` 发布
### 入群交流
<p>
<p align="center"> <img src="https://assets.qszone.com/image/入群.png" />
<img src="https://assets.qszone.com/image/Snipaste_2022-06-23_19-26-26.png" />
</p> </p>

View File

@@ -1,7 +1,7 @@
import { createHtmlPlugin } from 'vite-plugin-html' import { createHtmlPlugin } from 'vite-plugin-html'
export function configHtmlPlugin(viteEnv, isBuild) { export function configHtmlPlugin(viteEnv, isBuild) {
const { VITE_APP_TITLE, VITE_PUBLIC_PATH } = viteEnv const { VITE_APP_TITLE } = viteEnv
const htmlPlugin = createHtmlPlugin({ const htmlPlugin = createHtmlPlugin({
minify: isBuild, minify: isBuild,

View File

@@ -4,7 +4,7 @@ import vue from '@vitejs/plugin-vue'
* * 扩展setup插件支持在script标签中使用name属性 * * 扩展setup插件支持在script标签中使用name属性
* usage: <script setup name="MyComp"></script> * 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 * * unocss插件原子css
@@ -20,7 +20,7 @@ import { configMockPlugin } from './mock'
import unplugin from './unplugin' import unplugin from './unplugin'
export function createVitePlugins(viteEnv, isBuild) { 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) { if (viteEnv?.VITE_APP_USE_MOCK) {
plugins.push(configMockPlugin(isBuild)) plugins.push(configMockPlugin(isBuild))

View File

@@ -2,12 +2,11 @@ import { viteMockServe } from 'vite-plugin-mock'
export function configMockPlugin(isBuild) { export function configMockPlugin(isBuild) {
return viteMockServe({ return viteMockServe({
ignore: /^\_/, mockPath: 'mock/modules',
mockPath: 'mock',
localEnabled: !isBuild, localEnabled: !isBuild,
prodEnabled: isBuild, prodEnabled: isBuild,
injectCode: ` injectCode: `
import { setupProdMockServer } from '../mock/_create-prod-server'; import { setupProdMockServer } from '../mock';
setupProdMockServer(); setupProdMockServer();
`, `,
}) })

4
commitlint.config.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
ignores: [(commit) => commit.includes('init')],
extends: ['@commitlint/config-conventional'],
}

View File

@@ -1,11 +1,8 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer' import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
const modules = import.meta.globEager('./**/*.js') const modules = import.meta.globEager('./modules/*.js')
const mockModules = [] const mockModules = []
Object.keys(modules).forEach((key) => { Object.keys(modules).forEach((key) => {
if (key.includes('/_')) {
return
}
mockModules.push(...modules[key].default) mockModules.push(...modules[key].default)
}) })

View File

@@ -1,4 +1,4 @@
import { resolveToken } from '../_utils' import { resolveToken } from '../utils'
const token = { const token = {
admin: 'admin', admin: 'admin',

View File

@@ -1,4 +1,4 @@
import { resolveToken } from '../_utils' import { resolveToken } from '../utils'
const users = { const users = {
admin: { admin: {

View File

@@ -8,7 +8,9 @@
"build": "vite build", "build": "vite build",
"build:test": "vite build --mode test", "build:test": "vite build --mode test",
"build:github": "vite build --mode github && esno ./build/script", "build:github": "vite build --mode github && esno ./build/script",
"preview": "vite preview" "preview": "vite preview",
"prepare": "husky install",
"cz": "cz"
}, },
"dependencies": { "dependencies": {
"@vueuse/core": "^8.4.2", "@vueuse/core": "^8.4.2",
@@ -21,11 +23,16 @@
"vue-router": "^4.0.15" "vue-router": "^4.0.15"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"@iconify/json": "^2.1.63", "@iconify/json": "^2.1.63",
"@iconify/vue": "^3.2.1", "@iconify/vue": "^3.2.1",
"@vitejs/plugin-vue": "^1.10.2", "@vitejs/plugin-vue": "^1.10.2",
"@vue/compiler-sfc": "^3.2.31", "@vue/compiler-sfc": "^3.2.31",
"chalk": "^5.0.1", "chalk": "^5.0.1",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^6.9.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"eslint": "^8.12.0", "eslint": "^8.12.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
@@ -33,17 +40,23 @@
"eslint-plugin-vue": "^8.5.0", "eslint-plugin-vue": "^8.5.0",
"esno": "^0.13.0", "esno": "^0.13.0",
"fs-extra": "^10.0.1", "fs-extra": "^10.0.1",
"husky": "^8.0.1",
"naive-ui": "^2.30.3", "naive-ui": "^2.30.3",
"prettier": "^2.6.1", "prettier": "^2.6.1",
"rollup-plugin-visualizer": "^5.6.0", "rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.49.10", "sass": "^1.49.10",
"unocss": "^0.38.2", "unocss": "^0.43.2",
"unplugin-auto-import": "^0.8.8", "unplugin-auto-import": "^0.8.8",
"unplugin-icons": "^0.14.1", "unplugin-icons": "^0.14.1",
"unplugin-vue-components": "^0.17.21", "unplugin-vue-components": "^0.17.21",
"vite": "^2.9.9", "vite": "^2.9.9",
"vite-plugin-html": "^3.2.0", "vite-plugin-html": "^3.2.0",
"vite-plugin-mock": "^2.9.6", "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

File diff suppressed because it is too large Load Diff

View File

@@ -3,4 +3,5 @@ module.exports = {
singleQuote: true, singleQuote: true,
semi: false, semi: false,
endOfLine: 'lf', endOfLine: 'lf',
bracketSameLine: true,
} }

View File

@@ -1,10 +1,10 @@
import { defAxios as request } from '@/utils/http' import { defAxios as request } from '@/utils/http'
export function getPosts(data = {}) { export function getPosts(params = {}) {
return request({ return request({
url: '/posts', url: '/posts',
method: 'get', method: 'get',
data, params,
}) })
} }

View File

@@ -1,10 +1,10 @@
import { defAxios as request } from '@/utils/http' import { defAxios as request } from '@/utils/http'
export function getUsers(data = {}) { export function getUsers(params = {}) {
return request({ return request({
url: '/users', url: '/users',
method: 'get', method: 'get',
data, params,
}) })
} }

View File

@@ -1,23 +1,21 @@
<template> <template>
<div ref="wrapper" class="tags-wrapper" @mousewheel.prevent="handleMouseWheel"> <div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel">
<template v-if="showArrow && isOverflow"> <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 /> <icon-ic:baseline-keyboard-arrow-left />
</div> </div>
<div class="right" @click="handleMouseWheel({ wheelDelta: -50 })"> <div class="right" @click="handleMouseWheel({ wheelDelta: -120 })">
<icon-ic:baseline-keyboard-arrow-right /> <icon-ic:baseline-keyboard-arrow-right />
</div> </div>
</template> </template>
<div <div
ref="content" ref="content"
class="tags-content" class="content"
:class="{ overflow: isOverflow && showArrow }" :class="{ overflow: isOverflow && showArrow }"
:style="{ :style="{
height: height + 'px',
transform: `translateX(${translateX}px)`, transform: `translateX(${translateX}px)`,
}" }">
>
<slot /> <slot />
</div> </div>
</div> </div>
@@ -25,37 +23,26 @@
<script setup> <script setup>
import { debounce } from '@/utils' import { debounce } from '@/utils'
import { isNullOrUndef } from '@/utils/is'
defineProps({ defineProps({
height: {
type: Number,
default: 50,
},
showArrow: { showArrow: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
}) })
onMounted(() => {
refreshIsOverflow()
})
const translateX = ref(0) const translateX = ref(0)
const content = ref(null) const content = ref(null)
const wrapper = ref(null) const wrapper = ref(null)
const isOverflow = ref(false) const isOverflow = ref(false)
function refreshIsOverflow(isIncrease) { const refreshIsOverflow = debounce(() => {
isOverflow.value = content.value.offsetWidth > wrapper.value.offsetWidth const wrapperWidth = wrapper.value.offsetWidth
if (isNullOrUndef(isIncrease)) return const contentWidth = content.value.offsetWidth
if (isOverflow.value) { isOverflow.value = contentWidth > wrapperWidth
handleMouseWheel({ wheelDelta: isIncrease ? -100 : 100 }) resetTranslateX(wrapperWidth, contentWidth)
} else if (!isIncrease && translateX.value < 0) { }, 200)
handleMouseWheel({ wheelDelta: 100 })
}
}
function handleMouseWheel(e) { function handleMouseWheel(e) {
const { wheelDelta } = e const { wheelDelta } = e
const wrapperWidth = wrapper.value.offsetWidth const wrapperWidth = wrapper.value.offsetWidth
@@ -66,15 +53,15 @@ function handleMouseWheel(e) {
* @wrapperWidth 容器的宽度 * @wrapperWidth 容器的宽度
* @contentWidth 内容的宽度 * @contentWidth 内容的宽度
*/ */
if (wheelDelta < 0 && -translateX.value > contentWidth - wrapperWidth + 10) { if (wheelDelta < 0) {
return if (wrapperWidth > contentWidth && translateX.value < -10) return
if (wrapperWidth <= contentWidth && contentWidth + translateX.value - wrapperWidth < -10) return
} }
if (wheelDelta > 0 && translateX.value > 10) { if (wheelDelta > 0 && translateX.value > 10) {
return return
} }
translateX.value += wheelDelta translateX.value += wheelDelta
resetTranslateX(wrapperWidth, contentWidth) resetTranslateX(wrapperWidth, contentWidth)
} }
@@ -88,20 +75,29 @@ const resetTranslateX = debounce(function (wrapperWidth, contentWidth) {
} }
}, 200) }, 200)
defineExpose({ const observer = new MutationObserver(refreshIsOverflow)
refreshIsOverflow, onMounted(() => {
refreshIsOverflow()
window.addEventListener('resize', refreshIsOverflow)
// 监听内容宽度刷新是否超出
observer.observe(content.value, { childList: true })
})
onBeforeUnmount(() => {
window.removeEventListener('resize', refreshIsOverflow)
observer.disconnect()
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.tags-wrapper { .wrapper {
display: flex; display: flex;
background-color: #fff; background-color: #fff;
position: sticky;
top: 0;
z-index: 9; z-index: 9;
overflow: hidden; overflow: hidden;
.tags-content { position: relative;
.content {
padding: 0 10px; padding: 0 10px;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -1,12 +1,15 @@
<template> <template>
<n-breadcrumb> <n-breadcrumb>
<n-breadcrumb-item v-for="item in route.matched" :key="item.path" @click="handleBreadClick(item.path)"> <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 }} {{ item.meta.title }}
</n-breadcrumb-item> </n-breadcrumb-item>
</n-breadcrumb> </n-breadcrumb>
</template> </template>
<script setup> <script setup>
import { renderIcon } from '@/utils/icon'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()

View File

@@ -7,8 +7,7 @@
:collapsed-width="64" :collapsed-width="64"
:options="menuOptions" :options="menuOptions"
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name" :value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
@update:value="handleMenuSelect" @update:value="handleMenuSelect" />
/>
</template> </template>
<script setup> <script setup>

View File

@@ -6,8 +6,7 @@
:y="y" :y="y"
placement="bottom-start" placement="bottom-start"
@clickoutside="handleHideDropdown" @clickoutside="handleHideDropdown"
@select="handleSelect" @select="handleSelect" />
/>
</template> </template>
<script setup> <script setup>

View File

@@ -1,5 +1,5 @@
<template> <template>
<ScrollX ref="scrollX" :height="useTheme.tags.height"> <ScrollX :class="`h-${useTheme.tags.height}`">
<n-tag <n-tag
v-for="tag in tagsStore.tags" v-for="tag in tagsStore.tags"
:key="tag.path" :key="tag.path"
@@ -7,8 +7,7 @@
:closable="tagsStore.tags.length > 1" :closable="tagsStore.tags.length > 1"
@click="handleTagClick(tag.path)" @click="handleTagClick(tag.path)"
@close.stop="tagsStore.removeTag(tag.path)" @close.stop="tagsStore.removeTag(tag.path)"
@contextmenu.prevent="handleContextMenu($event, tag)" @contextmenu.prevent="handleContextMenu($event, tag)">
>
{{ tag.title }} {{ tag.title }}
</n-tag> </n-tag>
</ScrollX> </ScrollX>
@@ -17,8 +16,7 @@
v-model:show="contextMenuOption.show" v-model:show="contextMenuOption.show"
:current-path="contextMenuOption.currentPath" :current-path="contextMenuOption.currentPath"
:x="contextMenuOption.x" :x="contextMenuOption.x"
:y="contextMenuOption.y" :y="contextMenuOption.y" />
/>
</template> </template>
<script setup name="Tags"> <script setup name="Tags">
@@ -49,15 +47,6 @@ watch(
{ immediate: true } { immediate: true }
) )
const scrollX = ref(null)
watch(
() => tagsStore.tags,
async (newVal, oldVal) => {
await nextTick()
scrollX.value?.refreshIsOverflow(newVal.length > oldVal.length)
}
)
const handleTagClick = (path) => { const handleTagClick = (path) => {
tagsStore.setActiveTag(path) tagsStore.setActiveTag(path)
router.push(path) router.push(path)

View File

@@ -6,8 +6,7 @@
:collapsed-width="64" :collapsed-width="64"
:width="220" :width="220"
:native-scrollbar="false" :native-scrollbar="false"
:collapsed="appStore.collapsed" :collapsed="appStore.collapsed">
>
<SideBar /> <SideBar />
</n-layout-sider> </n-layout-sider>
<n-layout> <n-layout>
@@ -22,8 +21,7 @@
:style="{ :style="{
height: `calc(100% - ${useTheme.tags.visible ? useTheme.tags.height : 0}px)`, height: `calc(100% - ${useTheme.tags.visible ? useTheme.tags.height : 0}px)`,
overflow: 'auto', overflow: 'auto',
}" }" />
/>
</n-layout> </n-layout>
</n-layout> </n-layout>
</n-layout> </n-layout>

View File

@@ -9,6 +9,7 @@ export default [
meta: { meta: {
title: '组件示例', title: '组件示例',
role: ['admin'], role: ['admin'],
icon: 'mdi:menu',
}, },
children: [ children: [
{ {

View File

@@ -1,7 +1,6 @@
import { createSessionStorage } from '@/utils/cache' import { sStorage } from '@/utils/cache'
export const tagsSS = createSessionStorage({ prefixKey: 'tag_' }) export const activeTag = sStorage.get('activeTag')
export const activeTag = tagsSS.get('activeTag') export const tags = sStorage.get('tags')
export const tags = tagsSS.get('tags')
export const WITHOUT_TAG_PATHS = ['/404', '/login', '/redirect'] export const WITHOUT_TAG_PATHS = ['/404', '/login', '/redirect']

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia' 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 { router } from '@/router'
import { sStorage } from '@/utils/cache'
export const useTagsStore = defineStore('tag', { export const useTagsStore = defineStore('tag', {
state() { state() {
@@ -12,11 +13,11 @@ export const useTagsStore = defineStore('tag', {
actions: { actions: {
setActiveTag(path) { setActiveTag(path) {
this.activeTag = path this.activeTag = path
tagsSS.set('activeTag', path) sStorage.set('activeTag', path)
}, },
setTags(tags) { setTags(tags) {
this.tags = tags this.tags = tags
tagsSS.set('tags', tags) sStorage.set('tags', tags)
}, },
addTag(tag = {}) { addTag(tag = {}) {
this.setActiveTag(tag.path) this.setActiveTag(tag.path)

View File

@@ -1,15 +1,21 @@
import { createWebStorage } from './web-storage' import { createStorage } from './storage'
const prefixKey = 'Vue_Naive_Admin_'
export const createLocalStorage = function (option = {}) { export const createLocalStorage = function (option = {}) {
return createWebStorage({ return createStorage({
prefixKey: option.prefixKey || '', prefixKey: option.prefixKey || '',
storage: localStorage, storage: localStorage,
}) })
} }
export const createSessionStorage = function (option = {}) { export const createSessionStorage = function (option = {}) {
return createWebStorage({ return createStorage({
prefixKey: option.prefixKey || '', prefixKey: option.prefixKey || '',
storage: sessionStorage, storage: sessionStorage,
}) })
} }
export const lStorage = createLocalStorage({ prefixKey })
export const sStorage = createSessionStorage({ prefixKey })

View File

@@ -1,6 +1,6 @@
import { isNullOrUndef } from '@/utils/is' import { isNullOrUndef } from '@/utils/is'
class WebStorage { class Storage {
constructor(option) { constructor(option) {
this.storage = option.storage this.storage = option.storage
this.prefixKey = option.prefixKey this.prefixKey = option.prefixKey
@@ -50,6 +50,6 @@ class WebStorage {
} }
} }
export function createWebStorage({ prefixKey = '', storage = sessionStorage }) { export function createStorage({ prefixKey = '', storage = sessionStorage }) {
return new WebStorage({ prefixKey, storage }) return new Storage({ prefixKey, storage })
} }

View File

@@ -52,6 +52,9 @@ export function repReject(error) {
* TODO 此处可以根据后端返回的错误码自定义框架层面的错误处理 * TODO 此处可以根据后端返回的错误码自定义框架层面的错误处理
*/ */
switch (code) { switch (code) {
case 400:
message = message || '请求参数错误'
break
case 401: case 401:
message = message || '登录已过期' message = message || '登录已过期'
break break

View File

@@ -1,25 +1,23 @@
import { createLocalStorage } from './cache' import { lStorage } from './cache'
import { refreshToken } from '@/api/auth' import { refreshToken } from '@/api/auth'
const TOKEN_CODE = 'access_token' const TOKEN_CODE = 'access_token'
const DURATION = 6 * 60 * 60 const DURATION = 6 * 60 * 60
export const lsToken = createLocalStorage()
export function getToken() { export function getToken() {
return lsToken.get(TOKEN_CODE) return lStorage.get(TOKEN_CODE)
} }
export function setToken(token) { export function setToken(token) {
lsToken.set(TOKEN_CODE, token, DURATION) lStorage.set(TOKEN_CODE, token, DURATION)
} }
export function removeToken() { export function removeToken() {
lsToken.remove(TOKEN_CODE) lStorage.remove(TOKEN_CODE)
} }
export async function refreshAccessToken() { export async function refreshAccessToken() {
const tokenItem = lsToken.getItem(TOKEN_CODE) const tokenItem = lStorage.getItem(TOKEN_CODE)
if (!tokenItem) { if (!tokenItem) {
return return
} }

View File

@@ -36,8 +36,7 @@
class="w-300 flex-shrink-0 mt-10 mb-10 cursor-pointer" class="w-300 flex-shrink-0 mt-10 mb-10 cursor-pointer"
hover:card-shadow hover:card-shadow
title="Vue Naive Admin" title="Vue Naive Admin"
size="small" size="small">
>
<p op-60>一个基于 Vue3.0ViteNaive UI 的轻量级后台管理模板</p> <p op-60>一个基于 Vue3.0ViteNaive UI 的轻量级后台管理模板</p>
</n-card> </n-card>
<div w-300 h-0></div> <div w-300 h-0></div>

View File

@@ -1,11 +1,11 @@
<template> <template>
<div class="page-404"> <div h-full flex>
<n-result status="404" description="抱歉,您访问的页面不存在。"> <n-result m-auto status="404" description="抱歉,您访问的页面不存在。">
<template #icon> <template #icon>
<img src="@/assets/images/404.png" width="500" /> <img src="@/assets/images/404.png" width="500" />
</template> </template>
<template #footer> <template #footer>
<n-button strong secondary type="primary" @click="replace('/')">返回首页</n-button> <n-button @click="replace('/')">返回首页</n-button>
</template> </template>
</n-result> </n-result>
</div> </div>
@@ -14,12 +14,3 @@
<script setup> <script setup>
const { replace } = useRouter() const { replace } = useRouter()
</script> </script>
<style lang="scss" scoped>
.page-404 {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -1,10 +1,14 @@
<template> <template>
<div p-20> <div p-24>
<div class="header"> <div h-60 pl-20 pr-20 flex items-center bg-white>
<input v-model="post.title" type="text" placeholder="输入文章标题..." class="title" /> <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> <n-button type="primary" style="width: 80px" :loading="btnLoading" @click="handleSavePost">保存</n-button>
</div> </div>
<MdEditor v-model="post.content" style="height: calc(100vh - 210px)" /> <MdEditor v-model="post.content" style="height: calc(100vh - 220px)" />
</div> </div>
</template> </template>
@@ -37,21 +41,3 @@ function handleSavePost(e) {
} }
} }
</style> </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>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div p24> <div p-24>
<div class="action-btns"> <div flex>
<n-button size="small" type="primary" @click="handleCreate">新建文章</n-button> <n-button size="small" type="primary" @click="handleCreate">新建文章</n-button>
</div> </div>
<n-data-table <n-data-table
@@ -11,8 +11,7 @@
:columns="columns" :columns="columns"
:pagination="pagination" :pagination="pagination"
:row-key="(row) => row.id" :row-key="(row) => row.id"
@update:checked-row-keys="handleCheck" @update:checked-row-keys="handleCheck" />
/>
</div> </div>
</template> </template>
@@ -37,9 +36,3 @@ function handleCheck(rowKeys) {
if (rowKeys.length) $message.info(`选中${rowKeys.join(' ')}`) if (rowKeys.length) $message.info(`选中${rowKeys.join(' ')}`)
} }
</script> </script>
<style lang="scss" scoped>
.action-btns {
display: flex;
}
</style>

View File

@@ -10,28 +10,31 @@
<icon-custom-logo mr30 text-50 /> <icon-custom-logo mr30 text-50 />
{{ title }} {{ title }}
</h5> </h5>
<div mt-35 w-full max-w-360> <div mt-30 w-full max-w-360>
<n-input <n-input
v-model:value="loginInfo.name" v-model:value="loginInfo.name"
autofocus autofocus
class="text-16 items-center h-50 pl-10" class="text-16 items-center h-50 pl-10"
placeholder="请输入用户名" placeholder="admin"
:maxlength="20" :maxlength="20">
>
</n-input> </n-input>
</div> </div>
<div mt-35 w-full max-w-360> <div mt-30 w-full max-w-360>
<n-input <n-input
v-model:value="loginInfo.password" v-model:value="loginInfo.password"
class="text-16 items-center h-50 pl-10" class="text-16 items-center h-50 pl-10"
type="password" type="password"
show-password-on="mousedown" show-password-on="mousedown"
placeholder="密码" placeholder="123456"
:maxlength="20" :maxlength="20"
@keydown.enter="handleLogin" @keydown.enter="handleLogin" />
/>
</div> </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> <n-button w-full h-50 rounded-5 text-16 type="primary" @click="handleLogin">登录</n-button>
</div> </div>
</div> </div>
@@ -41,8 +44,9 @@
<script setup> <script setup>
import { login } from '@/api/auth' import { login } from '@/api/auth'
import { createLocalStorage } from '@/utils/cache' import { lStorage } from '@/utils/cache'
import { setToken } from '@/utils/token' import { setToken } from '@/utils/token'
import { useStorage } from '@vueuse/core'
const title = import.meta.env.VITE_APP_TITLE const title = import.meta.env.VITE_APP_TITLE
@@ -50,17 +54,21 @@ const router = useRouter()
const query = unref(router.currentRoute).query const query = unref(router.currentRoute).query
const loginInfo = ref({ const loginInfo = ref({
name: 'admin', name: '',
password: '123456', password: '',
}) })
const ls = createLocalStorage({ prefixKey: 'login_' }) initLoginInfo()
const lsLoginInfo = ls.get('loginInfo')
if (lsLoginInfo) { function initLoginInfo() {
loginInfo.value.name = lsLoginInfo.name || '' const localLoginInfo = lStorage.get('loginInfo')
loginInfo.value.password = lsLoginInfo.password || '' if (localLoginInfo) {
loginInfo.value.name = localLoginInfo.name || ''
loginInfo.value.password = localLoginInfo.password || ''
}
} }
const isRemember = useStorage('isRemember', false)
async function handleLogin() { async function handleLogin() {
const { name, password } = loginInfo.value const { name, password } = loginInfo.value
if (!name || !password) { if (!name || !password) {
@@ -72,9 +80,12 @@ async function handleLogin() {
const res = await login({ name, password: password.toString() }) const res = await login({ name, password: password.toString() })
if (res.code === 0) { if (res.code === 0) {
$message.success('登录成功') $message.success('登录成功')
ls.set('loginInfo', { name, password })
setToken(res.data.token) setToken(res.data.token)
if (isRemember.value) {
lStorage.set('loginInfo', { name, password })
} else {
lStorage.remove('loginInfo')
}
if (query.redirect) { if (query.redirect) {
const path = query.redirect const path = query.redirect
Reflect.deleteProperty(query, 'redirect') Reflect.deleteProperty(query, 'redirect')

View File

@@ -1,4 +1,4 @@
import { defineConfig, presetAttributify, presetIcons, presetUno } from 'unocss' import { defineConfig, presetAttributify, presetUno } from 'unocss'
export default defineConfig({ export default defineConfig({
shortcuts: [ shortcuts: [