mirror of
https://github.com/zclzone/vue-naive-admin.git
synced 2025-12-29 04:20:22 +08:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdbe9b8483 | ||
|
|
30211e14ea | ||
|
|
e7b1896d9e | ||
|
|
a5c1046e67 | ||
|
|
31670cd671 | ||
|
|
b0e3a94e12 | ||
|
|
2b2a324a62 | ||
|
|
40483e09e6 | ||
|
|
a5a3472486 | ||
|
|
fd1752693a | ||
|
|
2f3a83758a | ||
|
|
738212c84b | ||
|
|
a4f3e16007 | ||
|
|
5b2d1c68dd | ||
|
|
7b8b50322c | ||
|
|
bb171866b6 | ||
|
|
f1bc9edbac | ||
|
|
3a38adc71e | ||
|
|
b760cc34dd | ||
|
|
b59e47b5dd | ||
|
|
d1dd58215d | ||
|
|
661aed1a94 | ||
|
|
f2e2fc6819 | ||
|
|
9ea8ffd7fd | ||
|
|
af983d16b9 | ||
|
|
079761b6fd |
@@ -1,11 +1,5 @@
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
/**
|
||||
* * 扩展setup插件,支持在script标签中使用name属性
|
||||
* usage: <script setup name="MyComp"></script>
|
||||
*/
|
||||
import vueSetupExtend from 'vite-plugin-vue-setup-extend-plus'
|
||||
|
||||
/**
|
||||
* * unocss插件,原子css
|
||||
* https://github.com/antfu/unocss
|
||||
@@ -22,7 +16,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(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
|
||||
|
||||
if (viteEnv?.VITE_USE_MOCK) {
|
||||
plugins.push(configMockPlugin(isBuild))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { resolve } from 'path'
|
||||
import DefineOptions from 'unplugin-vue-define-options/vite'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
@@ -18,6 +19,7 @@ import { getSrcPath } from '../utils'
|
||||
const customIconPath = resolve(getSrcPath(), 'assets/svg')
|
||||
|
||||
export default [
|
||||
DefineOptions(),
|
||||
AutoImport({
|
||||
imports: ['vue', 'vue-router'],
|
||||
dts: false,
|
||||
|
||||
187
mock/api/post.js
187
mock/api/post.js
@@ -1,74 +1,133 @@
|
||||
const posts = [
|
||||
{
|
||||
title: '使用纯css优雅配置移动端rem布局',
|
||||
author: '大脸怪',
|
||||
category: 'Css',
|
||||
description: '通常配置rem布局会使用js进行处理,比如750的设计稿会这样...',
|
||||
content: '通常配置rem布局会使用js进行处理,比如750的设计稿会这样',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-11-04T04:03:36.000Z',
|
||||
updateDate: '2021-11-04T04:03:36.000Z',
|
||||
},
|
||||
{
|
||||
title: 'Vue2&Vue3项目风格指南',
|
||||
author: 'Ronnie',
|
||||
category: 'Vue',
|
||||
description: '总结的Vue2和Vue3的项目风格',
|
||||
content: '### 1. 命名风格\n\n> 文件夹如果是由多个单词组成,应该始终是横线连接 ',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-10-25T08:57:47.000Z',
|
||||
updateDate: '2022-02-28T04:02:39.000Z',
|
||||
},
|
||||
{
|
||||
title: '如何优雅的给图片添加水印',
|
||||
author: '大脸怪',
|
||||
category: 'JavaScript',
|
||||
description: '优雅的给图片添加水印',
|
||||
content: '我之前写过一篇文章记录了一次上传图片的优化史',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-06-24T18:46:19.000Z',
|
||||
updateDate: '2021-09-23T07:51:22.000Z',
|
||||
},
|
||||
|
||||
{
|
||||
title: '前端缓存的理解',
|
||||
author: '大脸怪',
|
||||
category: 'Http',
|
||||
description: '谈谈前端缓存的理解',
|
||||
content: '> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-06-10T18:51:19.000Z',
|
||||
updateDate: '2021-09-17T09:33:24.000Z',
|
||||
},
|
||||
{
|
||||
title: 'Promise的五个静态方法',
|
||||
author: '大脸怪',
|
||||
category: 'JavaScript',
|
||||
description: '简单介绍下在 Promise 类中,有5 种静态方法及它们的使用场景',
|
||||
content: '## 1. Promise.all\n\n并行执行多个 promise,并等待所有 promise 都准备就绪。再对它们进行处理。',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-02-22T22:37:06.000Z',
|
||||
updateDate: '2021-09-17T09:33:24.000Z',
|
||||
},
|
||||
]
|
||||
|
||||
export default [
|
||||
{
|
||||
url: '/api/posts',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
response: (data = {}) => {
|
||||
const { title, pageNo, pageSize } = data.query
|
||||
let pageData = []
|
||||
let total = 60
|
||||
const filterData = posts.filter((item) => item.title.includes(title) || (!title && title !== 0))
|
||||
if (filterData.length) {
|
||||
if (pageSize) {
|
||||
while (pageData.length < pageSize) {
|
||||
pageData.push(filterData[Math.round(Math.random() * (filterData.length - 1))])
|
||||
}
|
||||
} else {
|
||||
pageData = filterData
|
||||
}
|
||||
pageData = pageData.map((item, index) => ({
|
||||
id: pageSize * (pageNo - 1) + index + 1,
|
||||
...item,
|
||||
}))
|
||||
} else {
|
||||
total = 0
|
||||
}
|
||||
return {
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: [
|
||||
{
|
||||
id: 36,
|
||||
title: '使用纯css优雅配置移动端rem布局',
|
||||
author: 'Ronnie',
|
||||
category: '移动端,Css',
|
||||
description: '通常配置rem布局会使用js进行处理,比如750的设计稿会这样...',
|
||||
content: '通常配置rem布局会使用js进行处理,比如750的设计稿会这样',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-11-04T04:03:36.000Z',
|
||||
updateDate: '2021-11-04T04:03:36.000Z',
|
||||
},
|
||||
{
|
||||
id: 35,
|
||||
title: 'Vue2&Vue3项目风格指南',
|
||||
author: 'Ronnie',
|
||||
category: 'Vue',
|
||||
description: '总结的Vue2和Vue3的项目风格',
|
||||
content: '### 1. 命名风格\n\n> 文件夹如果是由多个单词组成,应该始终是横线连接 ',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-10-25T08:57:47.000Z',
|
||||
updateDate: '2022-02-28T04:02:39.000Z',
|
||||
},
|
||||
{
|
||||
id: 28,
|
||||
title: '如何优雅的给图片添加水印',
|
||||
author: '大脸怪',
|
||||
category: 'JavaScript',
|
||||
description: '优雅的给图片添加水印',
|
||||
content: '我之前写过一篇文章记录了一次上传图片的优化史',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-06-24T18:46:19.000Z',
|
||||
updateDate: '2021-09-23T07:51:22.000Z',
|
||||
},
|
||||
|
||||
{
|
||||
id: 26,
|
||||
title: '前端缓存的理解',
|
||||
author: '大脸怪',
|
||||
category: 'Http',
|
||||
description: '谈谈前端缓存的理解',
|
||||
content: '> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-06-10T18:51:19.000Z',
|
||||
updateDate: '2021-09-17T09:33:24.000Z',
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
title: 'Promise的五个静态方法',
|
||||
author: '大脸怪',
|
||||
category: 'JavaScript',
|
||||
description: '简单介绍下在 Promise 类中,有5 种静态方法及它们的使用场景',
|
||||
content: '## 1. Promise.all\n\n并行执行多个 promise,并等待所有 promise 都准备就绪。再对它们进行处理。',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-02-22T22:37:06.000Z',
|
||||
updateDate: '2021-09-17T09:33:24.000Z',
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageData,
|
||||
total,
|
||||
pageNo,
|
||||
pageSize,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/api/post',
|
||||
method: 'post',
|
||||
response: ({ body }) => {
|
||||
return {
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: body,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/api/post/:id',
|
||||
method: 'put',
|
||||
response: ({ query, body }) => {
|
||||
return {
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: {
|
||||
id: query.id,
|
||||
body,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/api/post/:id',
|
||||
method: 'delete',
|
||||
response: ({ query }) => {
|
||||
return {
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: {
|
||||
id: query.id,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
14
package.json
14
package.json
@@ -21,8 +21,8 @@
|
||||
"md-editor-v3": "^1.11.4",
|
||||
"mockjs": "^1.1.0",
|
||||
"pinia": "^2.0.13",
|
||||
"vue": "^3.2.31",
|
||||
"vue-router": "^4.0.15"
|
||||
"vue": "^3.2.39",
|
||||
"vue-router": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.3",
|
||||
@@ -44,20 +44,20 @@
|
||||
"fs-extra": "^10.0.1",
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"naive-ui": "^2.32.1",
|
||||
"naive-ui": "^2.33.3",
|
||||
"prettier": "^2.6.1",
|
||||
"rollup-plugin-visualizer": "^5.6.0",
|
||||
"sass": "^1.49.10",
|
||||
"unocss": "^0.43.2",
|
||||
"unplugin-auto-import": "^0.9.2",
|
||||
"unplugin-icons": "^0.14.1",
|
||||
"unplugin-icons": "^0.14.9",
|
||||
"unplugin-vue-components": "^0.17.21",
|
||||
"vite": "^3.0.9",
|
||||
"unplugin-vue-define-options": "^0.11.2",
|
||||
"vite": "^3.1.1",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-mock": "^2.9.6",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-setup-extend-plus": "^0.1.0"
|
||||
"vite-plugin-svg-icons": "^2.0.1"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
|
||||
714
pnpm-lock.yaml
generated
714
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,5 +2,5 @@ import request from '@/utils/http'
|
||||
|
||||
export default {
|
||||
getUser: () => request.get('/user'),
|
||||
refreshToken: () => request.post('/auth/refreshToken'),
|
||||
refreshToken: () => request.post('/auth/refreshToken', null, { noNeedTip: true }),
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script setup>
|
||||
import { renderCustomIcon } from '@/utils/icon'
|
||||
|
||||
/** 自定义图标 */
|
||||
const props = defineProps({
|
||||
/** 图标名称(图片的文件名) */
|
||||
/** 图标名称(assets/svg下的文件名) */
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -16,10 +15,8 @@ const props = defineProps({
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const iconCom = computed(() => renderCustomIcon(props.icon, props))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="iconCom" />
|
||||
<TheIcon type="custom" v-bind="props" />
|
||||
</template>
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup name="SvgIcon">
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
33
src/components/icon/TheIcon.vue
Normal file
33
src/components/icon/TheIcon.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
import { renderIcon, renderCustomIcon } from '@/utils/icon'
|
||||
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 14,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
/** iconify | custom */
|
||||
type: {
|
||||
type: String,
|
||||
default: 'iconify',
|
||||
},
|
||||
})
|
||||
|
||||
const iconCom = computed(() =>
|
||||
props.type === 'iconify'
|
||||
? renderIcon(props.icon, { size: props.size, color: props.color })
|
||||
: renderCustomIcon(props.icon, { size: props.size, color: props.color })
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="iconCom" />
|
||||
</template>
|
||||
17
src/components/page/AppPage.vue
Normal file
17
src/components/page/AppPage.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<section class="cus-scroll-y wh-full p-15 flex-col bg-[#f5f6fb]">
|
||||
<slot />
|
||||
<AppFooter v-if="showFooter" mt-15 />
|
||||
</section>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
33
src/components/page/CommonPage.vue
Normal file
33
src/components/page/CommonPage.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<AppPage :show-footer="showFooter">
|
||||
<header v-if="showHeader" px-15 mb-15 min-h-45 flex justify-between items-center>
|
||||
<slot v-if="$slots.header" name="header" />
|
||||
<template v-else>
|
||||
<h2 color="#333" text-22 font-normal>{{ title || route.meta?.title }}</h2>
|
||||
<slot name="action" />
|
||||
</template>
|
||||
</header>
|
||||
|
||||
<n-card rounded-10 flex-1>
|
||||
<slot />
|
||||
</n-card>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
const route = useRoute()
|
||||
</script>
|
||||
16
src/components/query-bar/QueryBar.vue
Normal file
16
src/components/query-bar/QueryBar.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div min-h-60 p-15 flex items-start justify-between b-1 bc-ccc rounded-8 bg="#fafafc">
|
||||
<n-space wrap :size="[35, 15]">
|
||||
<slot />
|
||||
</n-space>
|
||||
|
||||
<div flex-shrink-0>
|
||||
<n-button secondary type="primary" @click="emit('reset')">重置</n-button>
|
||||
<n-button ml-20 type="primary" @click="emit('search')">搜索</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const emit = defineEmits(['search', 'reset'])
|
||||
</script>
|
||||
29
src/components/query-bar/QueryBarItem.vue
Normal file
29
src/components/query-bar/QueryBarItem.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div flex items-center>
|
||||
<label v-if="!isNullOrWhitespace(label)" w-80 flex-shrink-0 :style="{ width: labelWidth + 'px' }">
|
||||
{{ label }}
|
||||
</label>
|
||||
<div :style="{ width: contentWidth + 'px' }" flex-shrink-0>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { isNullOrWhitespace } from '@/utils/is'
|
||||
|
||||
defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
labelWidth: {
|
||||
type: Number,
|
||||
default: 80,
|
||||
},
|
||||
contentWidth: {
|
||||
type: Number,
|
||||
default: 220,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
48
src/components/table/CrudModal.vue
Normal file
48
src/components/table/CrudModal.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<n-modal v-model:show="show" :style="{ width }" preset="card" :title="title" size="huge" :bordered="false">
|
||||
<slot />
|
||||
<template v-if="showFooter" #footer>
|
||||
<footer flex justify-end>
|
||||
<slot name="footer">
|
||||
<n-button @click="show = false">取消</n-button>
|
||||
<n-button :loading="loading" ml-20 type="primary" @click="emit('onSave')">保存</n-button>
|
||||
</slot>
|
||||
</footer>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
width: {
|
||||
type: String,
|
||||
default: '600px',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'onSave'])
|
||||
const show = computed({
|
||||
get() {
|
||||
return props.visible
|
||||
},
|
||||
set(v) {
|
||||
emit('update:visible', v)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
130
src/components/table/CrudTable.vue
Normal file
130
src/components/table/CrudTable.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<QueryBar v-if="$slots.queryBar" mb-30 @search="handleSearch" @reset="handleReset">
|
||||
<slot name="queryBar" />
|
||||
</QueryBar>
|
||||
|
||||
<n-data-table
|
||||
:remote="remote"
|
||||
:loading="loading"
|
||||
:scroll-x="scrollX"
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:row-key="(row) => row[rowKey]"
|
||||
:pagination="isPagination ? pagination : false"
|
||||
@update:checked-row-keys="onChecked"
|
||||
@update:page="onPageChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
/**
|
||||
* @remote true: 后端分页 false: 前端分页
|
||||
*/
|
||||
remote: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* @remote 是否分页
|
||||
*/
|
||||
isPagination: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
scrollX: {
|
||||
type: Number,
|
||||
default: 1200,
|
||||
},
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
/** queryBar中的参数 */
|
||||
queryItems: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
/** 补充参数(可选) */
|
||||
extraParams: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
/**
|
||||
* ! 约定接口入参出参
|
||||
* * 分页模式需约定分页接口入参
|
||||
* @pageSize 分页参数:一页展示多少条,默认10
|
||||
* @pageNo 分页参数:页码,默认1
|
||||
* * 需约定接口出参
|
||||
* @pageData 分页模式必须,非分页模式如果没有pageData则取上一层data
|
||||
* @total 分页模式必须,非分页模式如果没有total则取上一层data.length
|
||||
*/
|
||||
getData: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:queryItems', 'onChecked'])
|
||||
const loading = ref(false)
|
||||
const initQuery = { ...props.queryItems }
|
||||
const tableData = ref([])
|
||||
const pagination = reactive({ page: 1, pageSize: 10 })
|
||||
|
||||
async function handleQuery() {
|
||||
try {
|
||||
loading.value = true
|
||||
let paginationParams = {}
|
||||
// 如果非分页模式或者使用前端分页,则无需传分页参数
|
||||
if (props.isPagination && props.remote) {
|
||||
paginationParams = { pageNo: pagination.page, pageSize: pagination.pageSize }
|
||||
}
|
||||
const { data } = await props.getData({ ...props.queryItems, ...props.extraParams, ...paginationParams })
|
||||
tableData.value = data?.pageData || data
|
||||
pagination.itemCount = data.total ?? data.length
|
||||
} catch (error) {
|
||||
tableData.value = []
|
||||
pagination.itemCount = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
handleQuery()
|
||||
}
|
||||
async function handleReset() {
|
||||
const queryItems = { ...props.queryItems }
|
||||
for (const key in queryItems) {
|
||||
queryItems[key] = ''
|
||||
}
|
||||
emit('update:queryItems', { ...queryItems, ...initQuery })
|
||||
await nextTick()
|
||||
pagination.page = 1
|
||||
handleQuery()
|
||||
}
|
||||
function onPageChange(currentPage) {
|
||||
pagination.page = currentPage
|
||||
if (props.remote) {
|
||||
handleQuery()
|
||||
}
|
||||
}
|
||||
function onChecked(rowKeys) {
|
||||
if (props.columns.some((item) => item.type === 'selection')) {
|
||||
emit('onChecked', rowKeys)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handleSearch,
|
||||
handleReset,
|
||||
})
|
||||
</script>
|
||||
1
src/composables/index.js
Normal file
1
src/composables/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as useCRUD } from './useCRUD'
|
||||
103
src/composables/useCRUD.js
Normal file
103
src/composables/useCRUD.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { isNullOrWhitespace } from '@/utils/is'
|
||||
|
||||
const ACTIONS = {
|
||||
view: '查看',
|
||||
edit: '编辑',
|
||||
add: '新增',
|
||||
}
|
||||
|
||||
export default function ({ name, initForm = {}, doCreate, doDelete, doUpdate, refresh }) {
|
||||
const modalVisible = ref(false)
|
||||
const modalAction = ref('')
|
||||
const modalTitle = computed(() => ACTIONS[modalAction.value] + name)
|
||||
const modalLoading = ref(false)
|
||||
const modalFormRef = ref(null)
|
||||
const modalForm = ref({ ...initForm })
|
||||
|
||||
/** 新增 */
|
||||
function handleAdd() {
|
||||
modalAction.value = 'add'
|
||||
modalVisible.value = true
|
||||
modalForm.value = { ...initForm }
|
||||
}
|
||||
|
||||
/** 修改 */
|
||||
function handleEdit(row) {
|
||||
modalAction.value = 'edit'
|
||||
modalVisible.value = true
|
||||
modalForm.value = { ...row }
|
||||
}
|
||||
|
||||
/** 查看 */
|
||||
function handleView(row) {
|
||||
modalAction.value = 'view'
|
||||
modalVisible.value = true
|
||||
modalForm.value = { ...row }
|
||||
}
|
||||
|
||||
/** 保存 */
|
||||
function handleSave() {
|
||||
if (!['edit', 'add'].includes(modalAction.value)) {
|
||||
modalVisible.value = false
|
||||
return
|
||||
}
|
||||
modalFormRef.value?.validate(async (err) => {
|
||||
if (err) return
|
||||
const actions = {
|
||||
add: {
|
||||
api: () => doCreate(modalForm.value),
|
||||
cb: () => $message.success('新增成功'),
|
||||
},
|
||||
edit: {
|
||||
api: () => doUpdate(modalForm.value),
|
||||
cb: () => $message.success('编辑成功'),
|
||||
},
|
||||
}
|
||||
const action = actions[modalAction.value]
|
||||
|
||||
try {
|
||||
modalLoading.value = true
|
||||
const data = await action.api()
|
||||
action.cb()
|
||||
modalLoading.value = modalVisible.value = false
|
||||
data && refresh(data)
|
||||
} catch (error) {
|
||||
modalLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete(id, confirmOptions) {
|
||||
if (isNullOrWhitespace(id)) return
|
||||
$dialog.confirm({
|
||||
content: '确定删除?',
|
||||
async confirm() {
|
||||
try {
|
||||
modalLoading.value = true
|
||||
const data = await doDelete(id)
|
||||
$message.success('删除成功')
|
||||
modalLoading.value = false
|
||||
refresh(data)
|
||||
} catch (error) {
|
||||
modalLoading.value = false
|
||||
}
|
||||
},
|
||||
...confirmOptions,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
modalVisible,
|
||||
modalAction,
|
||||
modalTitle,
|
||||
modalLoading,
|
||||
handleAdd,
|
||||
handleDelete,
|
||||
handleEdit,
|
||||
handleView,
|
||||
handleSave,
|
||||
modalForm,
|
||||
modalFormRef,
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<keep-alive :include="keepAliveRouteNames">
|
||||
<component :is="Component" v-if="appStore.reloadFlag" :key="route.path" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
<KeepAlive :include="keepAliveRouteNames">
|
||||
<component :is="Component" v-if="appStore.reloadFlag" :key="route.meta?.key || route.fullPath" />
|
||||
</KeepAlive>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
|
||||
import { useRouter } from 'vue-router'
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
const allRoutes = router.getRoutes()
|
||||
const keepAliveRouteNames = computed(() => {
|
||||
return allRoutes.filter((route) => route.meta?.keepAlive).map((route) => route.name)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
<template>
|
||||
<header px-15 h-full flex items-center>
|
||||
<div flex items-center>
|
||||
<MenuCollapse />
|
||||
<BreadCrumb ml-15 />
|
||||
</div>
|
||||
<div ml-auto flex items-center>
|
||||
<GithubSite />
|
||||
<FullScreen />
|
||||
<UserAvatar />
|
||||
</div>
|
||||
</header>
|
||||
<div flex items-center>
|
||||
<MenuCollapse />
|
||||
<BreadCrumb ml-15 />
|
||||
</div>
|
||||
<div ml-auto flex items-center>
|
||||
<GithubSite />
|
||||
<FullScreen />
|
||||
<UserAvatar />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:collapsed-icon-size="22"
|
||||
:collapsed-width="64"
|
||||
:options="menuOptions"
|
||||
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
|
||||
:value="curRoute.meta?.activeMenu || curRoute.name"
|
||||
@update:value="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
@@ -19,9 +19,9 @@ import { useAppStore } from '@/store/modules/app'
|
||||
import { renderCustomIcon, renderIcon } from '@/utils/icon'
|
||||
|
||||
const router = useRouter()
|
||||
const curRoute = useRoute()
|
||||
const permissionStore = usePermissionStore()
|
||||
const appStore = useAppStore()
|
||||
const { currentRoute } = router
|
||||
|
||||
const menuOptions = computed(() => {
|
||||
return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.order - b.order)
|
||||
@@ -87,7 +87,7 @@ function handleMenuSelect(key, item) {
|
||||
if (isExternal(item.path)) {
|
||||
window.open(item.path)
|
||||
} else {
|
||||
if (item.path === currentRoute.value.path && !currentRoute.value.meta?.keepAlive) {
|
||||
if (item.path === curRoute.path) {
|
||||
appStore.reloadPage()
|
||||
} else {
|
||||
router.push(item.path)
|
||||
@@ -106,7 +106,7 @@ function handleMenuSelect(key, item) {
|
||||
&.n-menu-item-content--selected,
|
||||
&:hover {
|
||||
&::before {
|
||||
border-left: 4px solid var(--primaryColor);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +72,15 @@ const options = computed(() => [
|
||||
},
|
||||
])
|
||||
|
||||
const route = useRoute()
|
||||
const actionMap = new Map([
|
||||
[
|
||||
'reload',
|
||||
() => {
|
||||
if (route.meta?.keepAlive) {
|
||||
// 重置keepAlive
|
||||
route.meta.key = +new Date()
|
||||
}
|
||||
appStore.reloadPage()
|
||||
},
|
||||
],
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</ScrollX>
|
||||
</template>
|
||||
|
||||
<script setup name="Tags">
|
||||
<script setup>
|
||||
import ContextMenu from './ContextMenu.vue'
|
||||
import { useTagsStore } from '@/store/modules/tags'
|
||||
import ScrollX from '@/components/common/ScrollX.vue'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<n-layout has-sider h-full>
|
||||
<n-layout has-sider wh-full>
|
||||
<n-layout-sider
|
||||
bordered
|
||||
collapse-mode="width"
|
||||
@@ -10,19 +10,18 @@
|
||||
>
|
||||
<SideBar />
|
||||
</n-layout-sider>
|
||||
<n-layout>
|
||||
<n-layout-header bg-white border-b bc-eee :style="`height: ${header.height ?? 60}px`">
|
||||
<AppHeader />
|
||||
</n-layout-header>
|
||||
|
||||
<n-layout bg="#f5f6fb" :style="`height: calc(100% - ${header.height ?? 60}px)`">
|
||||
<AppTags v-if="tags.visible" :style="`height: ${tags.height ?? 50}px`" />
|
||||
<AppMain
|
||||
class="cus-scroll border-t bc-eee overflow-auto"
|
||||
:style="{ height: `calc(100% - ${tags.visible ? tags.height ?? 50 : 0}px)` }"
|
||||
/>
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
<article flex-1 flex-col overflow-hidden>
|
||||
<header bg-white px-15 border-b bc-eee flex items-center :style="`height: ${header.height}px`">
|
||||
<AppHeader />
|
||||
</header>
|
||||
<section v-if="tags.visible" border-b bc-eee>
|
||||
<AppTags :style="{ height: `${tags.height}px` }" />
|
||||
</section>
|
||||
<section flex-1 overflow-hidden>
|
||||
<AppMain />
|
||||
</section>
|
||||
</article>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import { setupRouter } from '@/router'
|
||||
import { setupStore } from '@/store'
|
||||
import App from './App.vue'
|
||||
|
||||
function setupApp() {
|
||||
async function setupApp() {
|
||||
const app = createApp(App)
|
||||
|
||||
setupStore(app)
|
||||
|
||||
setupRouter(app)
|
||||
await setupRouter(app)
|
||||
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { NOT_FOUND_ROUTE } from '@/router/routes'
|
||||
import { getToken, refreshAccessToken, removeToken } from '@/utils/token'
|
||||
import { toLogin } from '@/utils/auth'
|
||||
import { getToken, refreshAccessToken } from '@/utils/token'
|
||||
import { isNullOrWhitespace } from '@/utils/is'
|
||||
|
||||
const WHITE_LIST = ['/login']
|
||||
const WHITE_LIST = ['/login', '/404']
|
||||
export function createPermissionGuard(router) {
|
||||
const userStore = useUserStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
router.beforeEach(async (to) => {
|
||||
const token = getToken()
|
||||
|
||||
@@ -19,24 +13,9 @@ export function createPermissionGuard(router) {
|
||||
}
|
||||
|
||||
/** 有token的情况 */
|
||||
if (to.path === '/login') return { path: '/' }
|
||||
|
||||
if (to.path === '/login') return { path: '/', replace: true }
|
||||
|
||||
// 已经拿到用户信息
|
||||
if (userStore.userId) {
|
||||
refreshAccessToken()
|
||||
return true
|
||||
}
|
||||
await userStore.getUserInfo().catch((error) => {
|
||||
removeToken()
|
||||
toLogin()
|
||||
$message.error(error.message || '获取用户信息失败!')
|
||||
})
|
||||
const accessRoutes = permissionStore.generateRoutes(userStore.role)
|
||||
accessRoutes.forEach((route) => {
|
||||
!router.hasRoute(route.name) && router.addRoute(route)
|
||||
})
|
||||
router.addRoute(NOT_FOUND_ROUTE)
|
||||
return { ...to, replace: true }
|
||||
refreshAccessToken()
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,28 +1,53 @@
|
||||
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||
import { setupRouterGuard } from './guard'
|
||||
import { basicRoutes } from './routes'
|
||||
import { basicRoutes as routes, EMPTY_ROUTE, NOT_FOUND_ROUTE } from './routes'
|
||||
import { getToken } from '@/utils/token'
|
||||
import { isNullOrWhitespace } from '@/utils/is'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
|
||||
const isHash = import.meta.env.VITE_USE_HASH === 'true'
|
||||
export const router = createRouter({
|
||||
history: isHash ? createWebHashHistory('/') : createWebHistory('/'),
|
||||
routes: [],
|
||||
routes,
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
})
|
||||
|
||||
export function resetRouter() {
|
||||
export async function resetRouter() {
|
||||
router.getRoutes().forEach((route) => {
|
||||
const { name } = route
|
||||
router.hasRoute(name) && router.removeRoute(name)
|
||||
})
|
||||
basicRoutes.forEach((route) => {
|
||||
!router.hasRoute(route.name) && router.addRoute(route)
|
||||
})
|
||||
await addDynamicRoutes()
|
||||
}
|
||||
|
||||
export function setupRouter(app) {
|
||||
basicRoutes.forEach((route) => {
|
||||
!router.hasRoute(route.name) && router.addRoute(route)
|
||||
})
|
||||
app.use(router)
|
||||
setupRouterGuard(router)
|
||||
export async function addDynamicRoutes() {
|
||||
const token = getToken()
|
||||
|
||||
// 没有token情况
|
||||
if (isNullOrWhitespace(token)) {
|
||||
router.addRoute(EMPTY_ROUTE)
|
||||
return
|
||||
}
|
||||
|
||||
// 有token的情况
|
||||
try {
|
||||
const userStore = useUserStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
!userStore.userId && (await userStore.getUserInfo())
|
||||
const accessRoutes = permissionStore.generateRoutes(userStore.role)
|
||||
accessRoutes.forEach((route) => {
|
||||
!router.hasRoute(route.name) && router.addRoute(route)
|
||||
})
|
||||
router.hasRoute(EMPTY_ROUTE.name) && router.removeRoute(EMPTY_ROUTE.name)
|
||||
router.addRoute(NOT_FOUND_ROUTE)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupRouter(app) {
|
||||
await addDynamicRoutes()
|
||||
setupRouterGuard(router)
|
||||
app.use(router)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export const basicRoutes = [
|
||||
meta: {
|
||||
title: '外部链接',
|
||||
icon: 'mdi:link-variant',
|
||||
order: 2,
|
||||
order: 4,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
@@ -63,6 +63,12 @@ export const NOT_FOUND_ROUTE = {
|
||||
isHidden: true,
|
||||
}
|
||||
|
||||
export const EMPTY_ROUTE = {
|
||||
name: 'Empty',
|
||||
path: '/:pathMatch(.*)*',
|
||||
component: null,
|
||||
}
|
||||
|
||||
const modules = import.meta.glob('@/views/**/route.js', { eager: true })
|
||||
const asyncRoutes = []
|
||||
Object.keys(modules).forEach((key) => {
|
||||
|
||||
@@ -27,13 +27,9 @@ export const useUserStore = defineStore('user', {
|
||||
async getUserInfo() {
|
||||
try {
|
||||
const res = await api.getUser()
|
||||
if (res.code === 0) {
|
||||
const { id, name, avatar, role } = res.data
|
||||
this.userInfo = { id, name, avatar, role }
|
||||
return Promise.resolve(res.data)
|
||||
} else {
|
||||
return Promise.reject(res)
|
||||
}
|
||||
const { id, name, avatar, role } = res.data
|
||||
this.userInfo = { id, name, avatar, role }
|
||||
return Promise.resolve(res.data)
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
@@ -32,25 +32,42 @@ body {
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
/* 自定义滚动条样式 */
|
||||
.cus-scroll {
|
||||
overflow: auto;
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
.cus-scroll-x {
|
||||
overflow-x: auto;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
.cus-scroll-y {
|
||||
overflow-y: auto;
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
.cus-scroll,
|
||||
.cus-scroll-x,
|
||||
.cus-scroll-y {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: #f6f6f6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #bfbfbf;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: #999999;
|
||||
background: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,3 +6,9 @@ export function toLogin() {
|
||||
query: { ...router.currentRoute.value.query, redirect: router.currentRoute.value.path },
|
||||
})
|
||||
}
|
||||
|
||||
export function toFourZeroFour() {
|
||||
router.replace({
|
||||
path: '/404',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export function setupMessage(NMessage) {
|
||||
* * loading message不会自动清除,除非被替换成非loading message,非loading message默认2秒后自动清除
|
||||
*/
|
||||
|
||||
removeMessage(message, duration = 2000) {
|
||||
removeMessage(message = loadingMessage, duration = 2000) {
|
||||
setTimeout(() => {
|
||||
if (message) {
|
||||
message.destroy()
|
||||
|
||||
@@ -1,13 +1,43 @@
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
const WITHOUT_TOKEN_API = [{ url: '/auth/login', method: 'POST' }]
|
||||
|
||||
export function isWithoutToken({ url, method = '' }) {
|
||||
return WITHOUT_TOKEN_API.some((item) => item.url === url && item.method === method.toUpperCase())
|
||||
}
|
||||
import { isNullOrUndef } from '@/utils/is'
|
||||
import { removeToken } from '@/utils/token'
|
||||
import { toLogin } from '@/utils/auth'
|
||||
|
||||
export function addBaseParams(params) {
|
||||
if (!params.userId) {
|
||||
params.userId = useUserStore().userId
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveResError(error = {}) {
|
||||
let { code, message } = error
|
||||
if (isNullOrUndef(code)) {
|
||||
// 未知错误
|
||||
code = -1
|
||||
message = message ?? '接口未知异常!'
|
||||
} else {
|
||||
switch (code) {
|
||||
case 400:
|
||||
message = message ?? '请求参数错误'
|
||||
break
|
||||
case 401:
|
||||
message = message ?? '登录已过期'
|
||||
removeToken()
|
||||
toLogin()
|
||||
break
|
||||
case 403:
|
||||
message = message ?? '没有权限'
|
||||
break
|
||||
case 404:
|
||||
message = message ?? '资源或接口不存在'
|
||||
break
|
||||
case 500:
|
||||
message = message ?? '服务器异常'
|
||||
break
|
||||
default:
|
||||
message = message ?? '操作异常!'
|
||||
break
|
||||
}
|
||||
}
|
||||
return { code, message }
|
||||
}
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { getToken } from '@/utils/token'
|
||||
import { toLogin } from '@/utils/auth'
|
||||
import { isNullOrUndef } from '@/utils/is'
|
||||
import { isWithoutToken } from './helpers'
|
||||
import { resolveResError } from './helpers'
|
||||
|
||||
export function reqResolve(config) {
|
||||
// 防止缓存,给get请求加上时间戳
|
||||
if (config.method === 'get') {
|
||||
config.params = { ...config.params, t: new Date().getTime() }
|
||||
}
|
||||
|
||||
// 处理不需要token的请求
|
||||
if (isWithoutToken(config)) {
|
||||
if (config.noNeedToken) {
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -35,37 +29,18 @@ export function reqReject(error) {
|
||||
}
|
||||
|
||||
export function repResolve(response) {
|
||||
return response?.data
|
||||
const { noNeedTip } = response.config
|
||||
if (response.data?.code !== 0) {
|
||||
const { code, message } = resolveResError(response?.data)
|
||||
!noNeedTip && $message.error(message)
|
||||
return Promise.reject({ code, message, error: response?.data })
|
||||
}
|
||||
return Promise.resolve(response?.data)
|
||||
}
|
||||
|
||||
export function repReject(error) {
|
||||
let { code, message } = error.response?.data || {}
|
||||
if (isNullOrUndef(code)) {
|
||||
// 未知错误
|
||||
code = -1
|
||||
message = '接口异常!'
|
||||
} else {
|
||||
/**
|
||||
* TODO 此处可以根据后端返回的错误码自定义框架层面的错误处理
|
||||
*/
|
||||
switch (code) {
|
||||
case 400:
|
||||
message = message || '请求参数错误'
|
||||
break
|
||||
case 401:
|
||||
message = message || '登录已过期'
|
||||
break
|
||||
case 403:
|
||||
message = message || '没有权限'
|
||||
break
|
||||
case 404:
|
||||
message = message || '资源或接口不存在'
|
||||
break
|
||||
default:
|
||||
message = message || '未知异常'
|
||||
break
|
||||
}
|
||||
}
|
||||
console.error(`【${code}】 ${error}`)
|
||||
return Promise.resolve({ code, message, error })
|
||||
const { noNeedTip } = error.response?.config || error.config
|
||||
const { code, message } = resolveResError(error.response?.data)
|
||||
!noNeedTip && $message.error(message)
|
||||
return Promise.reject({ code, message, error })
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { h } from 'vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import SvgIcon from '@/components/custom/SvgIcon.vue'
|
||||
import SvgIcon from '@/components/icon/SvgIcon.vue'
|
||||
|
||||
export function renderIcon(icon, props = { size: 12 }) {
|
||||
return () => h(NIcon, props, { default: () => h(Icon, { icon }) })
|
||||
|
||||
@@ -72,6 +72,7 @@ export function isNullOrWhitespace(val) {
|
||||
return isNullOrUndef(val) || isWhitespace(val)
|
||||
}
|
||||
|
||||
/** 空数组 | 空字符串 | 空对象 | 空Map | 空Set */
|
||||
export function isEmpty(val) {
|
||||
if (isArray(val) || isString(val)) {
|
||||
return val.length === 0
|
||||
|
||||
@@ -26,8 +26,6 @@ export async function refreshAccessToken() {
|
||||
if (new Date().getTime() - time <= 1000 * 60 * 30) return
|
||||
try {
|
||||
const res = await api.refreshToken()
|
||||
if (res.code === 0) {
|
||||
setToken(res.data.token)
|
||||
}
|
||||
setToken(res.data.token)
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
89
src/views/demo/base/index.vue
Normal file
89
src/views/demo/base/index.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<CommonPage show-footer>
|
||||
<n-space size="large">
|
||||
<n-card title="按钮 Button">
|
||||
<n-space>
|
||||
<n-button>Default</n-button>
|
||||
<n-button type="tertiary"> Tertiary </n-button>
|
||||
<n-button type="primary"> Primary </n-button>
|
||||
<n-button type="info"> Info </n-button>
|
||||
<n-button type="success"> Success </n-button>
|
||||
<n-button type="warning"> Warning </n-button>
|
||||
<n-button type="error"> Error </n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card title="带 Icon 的按钮">
|
||||
<n-space>
|
||||
<n-button type="info"> <TheIcon icon="material-symbols:add" :size="18" class="mr-5" /> 新增 </n-button>
|
||||
<n-button type="error">
|
||||
<TheIcon icon="material-symbols:delete-outline" :size="18" class="mr-5" /> 删除
|
||||
</n-button>
|
||||
<n-button type="warning">
|
||||
<TheIcon icon="material-symbols:edit-outline" :size="18" class="mr-5" /> 编辑
|
||||
</n-button>
|
||||
<n-button type="primary"> <TheIcon icon="majesticons:eye-line" :size="18" class="mr-5" /> 查看 </n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
|
||||
<n-space size="large" mt-30>
|
||||
<n-card min-w-340 title="通知 Notification">
|
||||
<n-space>
|
||||
<n-button @click="notify('info')"> 信息 </n-button>
|
||||
<n-button @click="notify('success')"> 成功 </n-button>
|
||||
<n-button @click="notify('warning')"> 警告 </n-button>
|
||||
<n-button @click="notify('error')"> 错误 </n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card min-w-340 title="确认弹窗 Dialog">
|
||||
<n-button type="error" @click="handleDelete"> <icon-mi:delete mr-5 />删除</n-button>
|
||||
</n-card>
|
||||
|
||||
<n-card min-w-340 title="消息提醒 Message">
|
||||
<n-button :loading="loading" type="primary" @click="handleLogin">
|
||||
<icon-mdi:login v-show="!loading" mr-5 />
|
||||
登陆
|
||||
</n-button>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const handleDelete = function () {
|
||||
$dialog.confirm({
|
||||
content: '确认删除?',
|
||||
confirm() {
|
||||
$message.success('删除成功')
|
||||
},
|
||||
cancel() {
|
||||
$message.warning('已取消')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
function handleLogin() {
|
||||
loading.value = true
|
||||
$message.loading('登陆中...')
|
||||
setTimeout(() => {
|
||||
$message.error('登陆失败')
|
||||
$message.loading('正在尝试重新登陆...')
|
||||
setTimeout(() => {
|
||||
$message.success('登陆成功')
|
||||
loading.value = false
|
||||
}, 2000)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
function notify(type) {
|
||||
$notification[type]({
|
||||
content: '说点啥呢',
|
||||
meta: '想不出来',
|
||||
duration: 2500,
|
||||
keepAliveOnHover: true,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
31
src/views/demo/base/keep-alive/index.vue
Normal file
31
src/views/demo/base/keep-alive/index.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<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 color-gray text-14>注:右击标签重新加载可重置keep-alive</p>
|
||||
</div>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({ name: 'KeepAlive' })
|
||||
|
||||
const inputVal = ref('')
|
||||
const number = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
$message.success('onMounted')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
$message.error('onUnmounted')
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
$message.info('onActivated')
|
||||
})
|
||||
onDeactivated(() => {
|
||||
$message.warning('onDeactivated')
|
||||
})
|
||||
</script>
|
||||
@@ -2,50 +2,41 @@ const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'Test',
|
||||
path: '/test',
|
||||
path: '/base',
|
||||
component: Layout,
|
||||
redirect: '/test/unocss',
|
||||
redirect: '/base/index',
|
||||
meta: {
|
||||
title: '基础功能测试',
|
||||
customIcon: 'logo',
|
||||
title: '基础功能',
|
||||
icon: 'majesticons:compass-line',
|
||||
order: 1,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'BaseComponents',
|
||||
path: 'idnex',
|
||||
component: () => import('./index.vue'),
|
||||
meta: {
|
||||
title: '基础组件',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Unocss',
|
||||
path: 'unocss',
|
||||
component: () => import('./unocss/index.vue'),
|
||||
meta: {
|
||||
title: '测试unocss',
|
||||
title: 'Unocss',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Message',
|
||||
path: 'message',
|
||||
component: () => import('./message/index.vue'),
|
||||
meta: {
|
||||
title: '测试Message',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Dialog',
|
||||
path: 'dialog',
|
||||
component: () => import('./dialog/index.vue'),
|
||||
meta: {
|
||||
title: '测试Dialog',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'TestKeepAlive',
|
||||
name: 'KeepAlive',
|
||||
path: 'keep-alive',
|
||||
component: () => import('./keep-alive/index.vue'),
|
||||
meta: {
|
||||
title: '测试Keep-Alive',
|
||||
keepAlive: true,
|
||||
title: 'KeepAlive',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
66
src/views/demo/base/unocss/index.vue
Normal file
66
src/views/demo/base/unocss/index.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<CommonPage show-footer>
|
||||
<p>
|
||||
文档:<a hover-decoration-underline c-blue 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 f-c-c mt-20 w-350 rounded-10 b-1 bc-ccc>
|
||||
<div flex w-360 flex-wrap justify-around p-10>
|
||||
<div w-50 h-50 b-1 rounded-5 f-c-c p-10 m-20>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black self-end></span>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black self-center></span>
|
||||
<span w-6 h-6 rounded-3 bg-black self-end></span>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20>
|
||||
<div flex-col justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
<div flex-col justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 f-c-c flex-col p-10 m-20>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex-col justify-between p-10 m-20>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 font-normal text-14 text-center w-350 mt-10 color-gray>Flex 骰子</h2>
|
||||
</CommonPage>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div p-24>
|
||||
<CommonPage :show-header="false" title="32323">
|
||||
<div h-60 pl-20 pr-20 flex items-center bg-white>
|
||||
<input
|
||||
v-model="post.title"
|
||||
@@ -7,17 +7,19 @@
|
||||
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">
|
||||
<TheIcon v-if="!btnLoading" icon="line-md:confirm-circle" class="mr-5" :size="18" /> 保存
|
||||
</n-button>
|
||||
</div>
|
||||
<MdEditor v-model="post.content" style="height: calc(100vh - 220px)" />
|
||||
</div>
|
||||
<MdEditor v-model="post.content" style="height: calc(100vh - 250px)" />
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MdEditor from 'md-editor-v3'
|
||||
import 'md-editor-v3/lib/style.css'
|
||||
|
||||
const router = useRouter()
|
||||
defineOptions({ name: 'MDEditor' })
|
||||
|
||||
// refs
|
||||
let post = ref({})
|
||||
@@ -29,7 +31,6 @@ function handleSavePost(e) {
|
||||
setTimeout(() => {
|
||||
$message.success('保存成功')
|
||||
btnLoading.value = false
|
||||
router.push('/example/table/post')
|
||||
}, 2000)
|
||||
}
|
||||
</script>
|
||||
41
src/views/demo/route.js
Normal file
41
src/views/demo/route.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'Demo',
|
||||
path: '/demo',
|
||||
component: Layout,
|
||||
redirect: '/demo/crud-table',
|
||||
meta: {
|
||||
title: '示例页面',
|
||||
customIcon: 'logo',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
order: 3,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'CrudTable',
|
||||
path: 'crud-table',
|
||||
component: () => import('./table/index.vue'),
|
||||
meta: {
|
||||
title: 'CRUD表格',
|
||||
icon: 'ic:baseline-table-view',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'MDEditor',
|
||||
path: 'md-editor',
|
||||
component: () => import('./editor/md-editor.vue'),
|
||||
meta: {
|
||||
title: 'MD编辑器',
|
||||
icon: 'ri:markdown-line',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -3,11 +3,7 @@ import request from '@/utils/http'
|
||||
export default {
|
||||
getPosts: (params = {}) => request.get('posts', { params }),
|
||||
getPostById: (id) => request.get(`/post/${id}`),
|
||||
savePost: (id, data = {}) => {
|
||||
if (id) {
|
||||
return request.put(`/post/${id}`, data)
|
||||
}
|
||||
return request.post('/post', data)
|
||||
},
|
||||
addPost: (data) => request.post('/post', data),
|
||||
updatePost: (data) => request.put(`/post/${data.id}`, data),
|
||||
deletePost: (id) => request.delete(`/post/${id}`),
|
||||
}
|
||||
221
src/views/demo/table/index.vue
Normal file
221
src/views/demo/table/index.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<CommonPage show-footer title="文章">
|
||||
<template #action>
|
||||
<n-button type="primary" @click="handleAdd">
|
||||
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" /> 新建文章
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<CrudTable
|
||||
ref="$table"
|
||||
v-model:query-items="queryItems"
|
||||
:extra-params="extraParams"
|
||||
:scroll-x="1200"
|
||||
:columns="columns"
|
||||
:get-data="api.getPosts"
|
||||
@on-checked="onChecked"
|
||||
>
|
||||
<template #queryBar>
|
||||
<QueryBarItem label="标题" :label-width="50">
|
||||
<n-input
|
||||
v-model:value="queryItems.title"
|
||||
type="text"
|
||||
placeholder="请输入标题"
|
||||
@keydown.enter="$table?.handleSearch"
|
||||
/>
|
||||
</QueryBarItem>
|
||||
</template>
|
||||
</CrudTable>
|
||||
<!-- 新增/编辑/查看 -->
|
||||
<CrudModal
|
||||
v-model:visible="modalVisible"
|
||||
:title="modalTitle"
|
||||
:loading="modalLoading"
|
||||
:show-footer="modalAction !== 'view'"
|
||||
@on-save="handleSave"
|
||||
>
|
||||
<n-form
|
||||
ref="modalFormRef"
|
||||
label-placement="left"
|
||||
label-align="left"
|
||||
:label-width="80"
|
||||
:model="modalForm"
|
||||
:disabled="modalAction === 'view'"
|
||||
>
|
||||
<n-form-item label="作者" path="author">
|
||||
<n-input v-model:value="modalForm.author" disabled />
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="文章标题"
|
||||
path="title"
|
||||
:rule="{
|
||||
required: true,
|
||||
message: '请输入文章标题',
|
||||
trigger: ['input', 'blur'],
|
||||
}"
|
||||
>
|
||||
<n-input v-model:value="modalForm.title" placeholder="请输入文章标题" />
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="文章内容"
|
||||
path="content"
|
||||
:rule="{
|
||||
required: true,
|
||||
message: '请输入文章内容',
|
||||
trigger: ['input', 'blur'],
|
||||
}"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="modalForm.content"
|
||||
placeholder="请输入文章内容"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 3,
|
||||
maxRows: 5,
|
||||
}"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</CrudModal>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NButton, NSwitch } from 'naive-ui'
|
||||
import { formatDateTime } from '@/utils'
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
import { useCRUD } from '@/composables'
|
||||
import api from './api'
|
||||
import { isNullOrUndef } from '@/utils/is'
|
||||
|
||||
defineOptions({ name: 'CrudTable' })
|
||||
|
||||
const $table = ref(null)
|
||||
/** QueryBar筛选参数(可选) */
|
||||
const queryItems = ref({})
|
||||
/** 补充参数(可选) */
|
||||
const extraParams = ref({})
|
||||
|
||||
onMounted(() => {
|
||||
$table.value?.handleSearch()
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ type: 'selection', fixed: 'left' },
|
||||
{
|
||||
title: '发布',
|
||||
key: 'isPublish',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
render(row) {
|
||||
return h(NSwitch, {
|
||||
size: 'small',
|
||||
rubberBand: false,
|
||||
value: row['isPublish'],
|
||||
loading: !!row.publishing,
|
||||
onUpdateValue: () => handlePublish(row),
|
||||
})
|
||||
},
|
||||
},
|
||||
{ title: '标题', key: 'title', width: 150, ellipsis: { tooltip: true } },
|
||||
{ title: '分类', key: 'category', width: 80, ellipsis: { tooltip: true } },
|
||||
{ title: '创建人', key: 'author', width: 80 },
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createDate',
|
||||
width: 150,
|
||||
render(row) {
|
||||
return h('span', formatDateTime(row['createDate']))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最后更新时间',
|
||||
key: 'updateDate',
|
||||
width: 150,
|
||||
render(row) {
|
||||
return h('span', formatDateTime(row['updateDate']))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 240,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
render(row) {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
secondary: true,
|
||||
onClick: () => handleView(row),
|
||||
},
|
||||
{ default: () => '查看', icon: renderIcon('majesticons:eye-line', { size: 14 }) }
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
style: 'margin-left: 15px;',
|
||||
onClick: () => handleEdit(row),
|
||||
},
|
||||
{ default: () => '编辑', icon: renderIcon('material-symbols:edit-outline', { size: 14 }) }
|
||||
),
|
||||
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 15px;',
|
||||
onClick: () => handleDelete(row.id),
|
||||
},
|
||||
{ default: () => '删除', icon: renderIcon('material-symbols:delete-outline', { size: 14 }) }
|
||||
),
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// 选中事件
|
||||
function onChecked(rowKeys) {
|
||||
if (rowKeys.length) $message.info(`选中${rowKeys.join(' ')}`)
|
||||
}
|
||||
|
||||
// 发布
|
||||
function handlePublish(row) {
|
||||
if (isNullOrUndef(row.id)) return
|
||||
|
||||
row.publishing = true
|
||||
setTimeout(() => {
|
||||
row.isPublish = !row.isPublish
|
||||
row.publishing = false
|
||||
$message?.success(row.isPublish ? '已发布' : '已取消发布')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const {
|
||||
modalVisible,
|
||||
modalAction,
|
||||
modalTitle,
|
||||
modalLoading,
|
||||
handleAdd,
|
||||
handleDelete,
|
||||
handleEdit,
|
||||
handleView,
|
||||
handleSave,
|
||||
modalForm,
|
||||
modalFormRef,
|
||||
} = useCRUD({
|
||||
name: '文章',
|
||||
initForm: { author: '大脸怪' },
|
||||
doCreate: api.addPost,
|
||||
doDelete: api.deletePost,
|
||||
doUpdate: api.updatePost,
|
||||
refresh: () => $table.value?.handleSearch(),
|
||||
})
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div h-full flex>
|
||||
<AppPage>
|
||||
<n-result m-auto status="404" description="抱歉,您访问的页面不存在。">
|
||||
<template #icon>
|
||||
<img src="@/assets/images/404.webp" width="500" />
|
||||
@@ -8,7 +8,7 @@
|
||||
<n-button @click="replace('/')">返回首页</n-button>
|
||||
</template>
|
||||
</n-result>
|
||||
</div>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
@@ -1,39 +0,0 @@
|
||||
<template>
|
||||
<div p-24>
|
||||
<div flex>
|
||||
<n-button size="small" type="primary" @click="handleCreate">新建文章</n-button>
|
||||
</div>
|
||||
<n-data-table
|
||||
mt-20
|
||||
:loading="loading"
|
||||
:scroll-x="1600"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
:row-key="(row) => row.id"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { usePostTable } from './usePostTable'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const pagination = ref({ pageSize: 10 })
|
||||
const { loading, columns, tableData, initColumns, initTableData } = usePostTable()
|
||||
|
||||
onBeforeMount(() => {
|
||||
initColumns()
|
||||
initTableData()
|
||||
})
|
||||
|
||||
function handleCreate() {
|
||||
router.push('/example/table/post-create')
|
||||
}
|
||||
|
||||
function handleCheck(rowKeys) {
|
||||
if (rowKeys.length) $message.info(`选中${rowKeys.join(' ')}`)
|
||||
}
|
||||
</script>
|
||||
@@ -1,161 +0,0 @@
|
||||
import { h } from 'vue'
|
||||
import { NButton, NSwitch } from 'naive-ui'
|
||||
import { formatDateTime } from '@/utils'
|
||||
import api from './api'
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
|
||||
export const usePostTable = () => {
|
||||
// refs
|
||||
const loading = ref(false)
|
||||
const tableData = ref([])
|
||||
const columns = ref([])
|
||||
|
||||
async function initTableData() {
|
||||
loading.value = true
|
||||
tableData.value = await getTableData()
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function handleDelete(row) {
|
||||
if (row && row.id) {
|
||||
$dialog.confirm({
|
||||
content: '确定删除?',
|
||||
confirm() {
|
||||
$message.success('删除成功')
|
||||
initTableData()
|
||||
},
|
||||
cancel() {
|
||||
$message.success('已取消')
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRecommend(row) {
|
||||
if (row && row.id) {
|
||||
row.recommending = true
|
||||
setTimeout(() => {
|
||||
row.isRecommend = !row.isRecommend
|
||||
row.recommending = false
|
||||
$message.success(row.isRecommend ? '已推荐' : '已取消推荐')
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePublish(row) {
|
||||
if (row && row.id) {
|
||||
row.publishing = true
|
||||
setTimeout(() => {
|
||||
row.isPublish = !row.isPublish
|
||||
row.publishing = false
|
||||
$message.success(row.isPublish ? '已发布' : '已取消发布')
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
|
||||
function initColumns() {
|
||||
columns.value = [
|
||||
{ type: 'selection' },
|
||||
{ title: '标题', key: 'title', width: 150, ellipsis: { tooltip: true } },
|
||||
{ title: '分类', key: 'category', width: 80, ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: '描述',
|
||||
key: 'description',
|
||||
width: 200,
|
||||
ellipsis: { tooltip: true },
|
||||
},
|
||||
{ title: '创建人', key: 'author', width: 80 },
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createDate',
|
||||
width: 150,
|
||||
render(row) {
|
||||
return h('span', formatDateTime(row['createDate']))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最后更新时间',
|
||||
key: 'updateDate',
|
||||
width: 150,
|
||||
render(row) {
|
||||
return h('span', formatDateTime(row['updateDate']))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '推荐',
|
||||
key: 'isRecommend',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
render(row) {
|
||||
return h(NSwitch, {
|
||||
size: 'small',
|
||||
value: row['isRecommend'],
|
||||
rubberBand: false,
|
||||
loading: !!row.recommending,
|
||||
onUpdateValue: () => handleRecommend(row),
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '发布',
|
||||
key: 'isPublish',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
render(row) {
|
||||
return h(NSwitch, {
|
||||
size: 'small',
|
||||
rubberBand: false,
|
||||
value: row['isPublish'],
|
||||
loading: !!row.publishing,
|
||||
onUpdateValue: () => handlePublish(row),
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
render(row) {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 15px;',
|
||||
onClick: () => handleDelete(row),
|
||||
},
|
||||
{ default: () => '删除', icon: renderIcon('material-symbols:delete-outline', { size: 14 }) }
|
||||
),
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async function getTableData() {
|
||||
try {
|
||||
const res = await api.getPosts()
|
||||
if (res.code === 0) {
|
||||
return res.data
|
||||
}
|
||||
console.warn(res.message)
|
||||
return []
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
columns,
|
||||
tableData,
|
||||
initColumns,
|
||||
initTableData,
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'Example',
|
||||
path: '/example',
|
||||
component: Layout,
|
||||
redirect: '/example/table',
|
||||
meta: {
|
||||
title: '组件示例',
|
||||
icon: 'mdi:menu',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
order: 3,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Table',
|
||||
path: 'table',
|
||||
component: () => import('./index.vue'),
|
||||
redirect: '/example/table/post',
|
||||
meta: {
|
||||
title: '表格',
|
||||
icon: 'mdi:table',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'PostList',
|
||||
path: 'post',
|
||||
component: () => import('./post/index.vue'),
|
||||
meta: {
|
||||
title: '文章列表',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'PostCreate',
|
||||
path: 'post-create',
|
||||
component: () => import('./post/PostCreate.vue'),
|
||||
meta: {
|
||||
title: '创建文章',
|
||||
icon: 'material-symbols:auto-awesome-outline-rounded',
|
||||
role: ['admin'],
|
||||
requireAuth: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export default {
|
||||
login: (data) => request.post('/auth/login', data),
|
||||
login: (data) => request.post('/auth/login', data, { noNeedToken: true }),
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<template>
|
||||
<div class="cus-scroll h-full py-15 flex-col overflow-auto bg-cover" :style="{ backgroundImage: `url(${bgImg})` }">
|
||||
<div m-auto p-15 f-c-c min-w-345 rounded-10 card-shadow bg-white dark:bg-dark bg-opacity-60>
|
||||
<AppPage :show-footer="true" bg-cover :style="{ backgroundImage: `url(${bgImg})` }">
|
||||
<div
|
||||
style="transform: translateY(25px)"
|
||||
class="m-auto p-15 f-c-c min-w-345 max-w-700 rounded-10 card-shadow bg-white bg-opacity-60"
|
||||
>
|
||||
<div w-380 hidden md:block px-20 py-35>
|
||||
<img src="@/assets/images/login_banner.webp" w-full alt="login_banner" />
|
||||
</div>
|
||||
|
||||
<div w-320 flex-col px-20 py-35>
|
||||
<h5 f-c-c text-24 font-normal color="#6a6a6a"><icon-custom-logo mr-30 text-50 color-primary />{{ title }}</h5>
|
||||
<h5 f-c-c text-24 font-normal color="#6a6a6a"><icon-custom-logo mr-10 text-50 color-primary />{{ title }}</h5>
|
||||
<div mt-30>
|
||||
<n-input
|
||||
v-model:value="loginInfo.name"
|
||||
@@ -33,14 +36,13 @@
|
||||
</div>
|
||||
|
||||
<div mt-20>
|
||||
<n-button w-full h-50 rounded-5 text-16 type="primary" :loading="loging" @click="handleLogin">
|
||||
<n-button w-full h-50 rounded-5 text-16 type="primary" :loading="loading" @click="handleLogin">
|
||||
登录
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AppFooter />
|
||||
</div>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -49,6 +51,7 @@ import { setToken } from '@/utils/token'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import bgImg from '@/assets/images/login_bg.webp'
|
||||
import api from './api'
|
||||
import { addDynamicRoutes } from '@/router'
|
||||
|
||||
const title = import.meta.env.VITE_TITLE
|
||||
|
||||
@@ -71,7 +74,7 @@ function initLoginInfo() {
|
||||
}
|
||||
|
||||
const isRemember = useStorage('isRemember', false)
|
||||
const loging = ref(false)
|
||||
const loading = ref(false)
|
||||
async function handleLogin() {
|
||||
const { name, password } = loginInfo.value
|
||||
if (!name || !password) {
|
||||
@@ -79,30 +82,28 @@ async function handleLogin() {
|
||||
return
|
||||
}
|
||||
try {
|
||||
loading.value = true
|
||||
$message.loading('正在验证...')
|
||||
loging.value = true
|
||||
const res = await api.login({ name, password: password.toString() })
|
||||
if (res.code === 0) {
|
||||
$message.success('登录成功')
|
||||
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')
|
||||
router.push({ path, query })
|
||||
} else {
|
||||
router.push('/')
|
||||
}
|
||||
$message.success('登录成功')
|
||||
setToken(res.data.token)
|
||||
if (isRemember.value) {
|
||||
lStorage.set('loginInfo', { name, password })
|
||||
} else {
|
||||
$message.warning(res.message)
|
||||
lStorage.remove('loginInfo')
|
||||
}
|
||||
await addDynamicRoutes()
|
||||
if (query.redirect) {
|
||||
const path = query.redirect
|
||||
Reflect.deleteProperty(query, 'redirect')
|
||||
router.push({ path, query })
|
||||
} else {
|
||||
router.push('/')
|
||||
}
|
||||
} catch (error) {
|
||||
$message.error(error.message)
|
||||
console.error(error)
|
||||
$message.removeMessage()
|
||||
}
|
||||
loging.value = false
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<div p24>
|
||||
<n-button type="error" @click="handleDelete"> <icon-mi:delete mr-5 />删除</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="TestDialog">
|
||||
const handleDelete = function () {
|
||||
$dialog.confirm({
|
||||
content: '确认删除?',
|
||||
confirm() {
|
||||
$message.success('删除成功')
|
||||
},
|
||||
cancel() {
|
||||
$message.warning('已取消')
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -1,20 +0,0 @@
|
||||
<template>
|
||||
<div p24>
|
||||
<n-gradient-text gradient="linear-gradient(90deg, red 0%, green 50%, blue 100%)"> 注意查看提示语 </n-gradient-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!--使用keep-alive须设置name,注意请与对应的路由的name保持一致,方便统一处理-->
|
||||
<script setup name="TestKeepAlive">
|
||||
onMounted(() => {
|
||||
$message.success('触发onMounted')
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
$message.success('触发onActivated')
|
||||
})
|
||||
|
||||
onDeactivated(() => {
|
||||
$message.success('触发onDeactivated')
|
||||
})
|
||||
</script>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<div p24>
|
||||
<n-button type="primary" @click="handleLogin">
|
||||
<icon-mdi:login mr-5 />
|
||||
登陆
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
function handleLogin() {
|
||||
$message.loading('登陆中...')
|
||||
setTimeout(() => {
|
||||
$message.error('登陆失败')
|
||||
$message.loading('正在尝试重新登陆...')
|
||||
setTimeout(() => {
|
||||
$message.success('登陆成功')
|
||||
}, 2000)
|
||||
}, 2000)
|
||||
}
|
||||
</script>
|
||||
@@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<div p-24>
|
||||
<p>
|
||||
文档:<a hover-decoration-underline c-blue 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 flex mt-20>
|
||||
<div flex p-20 rounded-5 bg="#fff">
|
||||
<div text-20 font-600>Flex布局</div>
|
||||
<div flex w-360 flex-wrap justify-around ml-15 p-10>
|
||||
<div w-50 h-50 b-1 rounded-5 f-c-c p-10 m-20>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black self-end></span>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black self-center></span>
|
||||
<span w-6 h-6 rounded-3 bg-black self-end></span>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20>
|
||||
<div flex-col justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
<div flex-col justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 f-c-c flex-col p-10 m-20>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
</div>
|
||||
<div w-50 h-50 b-1 rounded-5 flex-col justify-between p-10 m-20>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
<div flex w-full justify-between>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
<span w-6 h-6 rounded-3 bg-black></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,52 +1,54 @@
|
||||
<template>
|
||||
<div p-15>
|
||||
<n-card rounded-10>
|
||||
<div flex items-center>
|
||||
<img rounded-full width="60" :src="userStore.avatar" />
|
||||
<div ml-20>
|
||||
<p text-16>Hello, {{ userStore.name }}</p>
|
||||
<p mt-5 text-12 op-60>今天又是元气满满的一天</p>
|
||||
<AppPage :show-footer="true">
|
||||
<div flex-1>
|
||||
<n-card rounded-10>
|
||||
<div flex items-center>
|
||||
<img rounded-full width="60" :src="userStore.avatar" />
|
||||
<div ml-20>
|
||||
<p text-16>Hello, {{ userStore.name }}</p>
|
||||
<p mt-5 text-12 op-60>今天又是元气满满的一天</p>
|
||||
</div>
|
||||
<div ml-auto flex items-center>
|
||||
<n-statistic label="待办" :value="4">
|
||||
<template #suffix> / 10 </template>
|
||||
</n-statistic>
|
||||
<n-statistic label="Stars" w-100 ml-80>
|
||||
<a href="https://github.com/zclzone/vue-naive-admin">
|
||||
<img allt="stars" src="https://badgen.net/github/stars/zclzone/vue-naive-admin" />
|
||||
</a>
|
||||
</n-statistic>
|
||||
<n-statistic label="Forks" w-100 ml-80>
|
||||
<a href="https://github.com/zclzone/vue-naive-admin">
|
||||
<img allt="forks" src="https://badgen.net/github/forks/zclzone/vue-naive-admin" />
|
||||
</a>
|
||||
</n-statistic>
|
||||
</div>
|
||||
</div>
|
||||
<div ml-auto flex items-center>
|
||||
<n-statistic label="待办" :value="4">
|
||||
<template #suffix> / 10 </template>
|
||||
</n-statistic>
|
||||
<n-statistic label="Stars" w-100 ml-80>
|
||||
<a href="https://github.com/zclzone/vue-naive-admin">
|
||||
<img allt="stars" src="https://badgen.net/github/stars/zclzone/vue-naive-admin" />
|
||||
</a>
|
||||
</n-statistic>
|
||||
<n-statistic label="Forks" w-100 ml-80>
|
||||
<a href="https://github.com/zclzone/vue-naive-admin">
|
||||
<img allt="forks" src="https://badgen.net/github/forks/zclzone/vue-naive-admin" />
|
||||
</a>
|
||||
</n-statistic>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-card>
|
||||
|
||||
<n-card title="项目" size="small" :segmented="true" mt-15 rounded-10>
|
||||
<template #header-extra>
|
||||
<n-button text type="primary">更多</n-button>
|
||||
</template>
|
||||
<div flex flex-wrap justify-between>
|
||||
<n-card
|
||||
v-for="i in 10"
|
||||
:key="i"
|
||||
class="w-300 flex-shrink-0 mt-10 mb-10 cursor-pointer"
|
||||
hover:card-shadow
|
||||
title="Vue Naive Admin"
|
||||
size="small"
|
||||
>
|
||||
<p op-60>一个基于 Vue3.0、Vite、Naive UI 的轻量级后台管理模板</p>
|
||||
</n-card>
|
||||
<div w-300 h-0></div>
|
||||
<div w-300 h-0></div>
|
||||
<div w-300 h-0></div>
|
||||
<div w-300 h-0></div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
<n-card title="项目" size="small" :segmented="true" mt-15 rounded-10>
|
||||
<template #header-extra>
|
||||
<n-button text type="primary">更多</n-button>
|
||||
</template>
|
||||
<div flex flex-wrap justify-between>
|
||||
<n-card
|
||||
v-for="i in 10"
|
||||
:key="i"
|
||||
class="w-300 flex-shrink-0 mt-10 mb-10 cursor-pointer"
|
||||
hover:card-shadow
|
||||
title="Vue Naive Admin"
|
||||
size="small"
|
||||
>
|
||||
<p op-60>一个基于 Vue3.0、Vite、Naive UI 的轻量级后台管理模板</p>
|
||||
</n-card>
|
||||
<div w-300 h-0></div>
|
||||
<div w-300 h-0></div>
|
||||
<div w-300 h-0></div>
|
||||
<div w-300 h-0></div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
Reference in New Issue
Block a user