mirror of
https://github.com/zclzone/vue-naive-admin.git
synced 2025-12-26 19:20:21 +08:00
init
This commit is contained in:
19
src/components/common/AppCard.vue
Normal file
19
src/components/common/AppCard.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/04 22:51:04
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<div class="auto-bg" :class="{ 'card-border': bordered }">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
bordered: Boolean,
|
||||
})
|
||||
</script>
|
||||
31
src/components/common/AppPage.vue
Normal file
31
src/components/common/AppPage.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/04 22:51:12
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<main class="cus-scroll h-full flex-col flex-1 bg-#f5f6fb dark:bg-#121212">
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<main :class="{ 'flex-1': full }" class="m-12"><slot /></main>
|
||||
</transition>
|
||||
<slot v-if="$slots.footer" name="footer" />
|
||||
<TheFooter v-else-if="showFooter" class="mb-12 mt-auto" />
|
||||
<n-back-top :bottom="20" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
full: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
74
src/components/common/CommonPage.vue
Normal file
74
src/components/common/CommonPage.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/04 22:51:21
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<main class="h-full flex-1 overflow-hidden bg-#f5f6fb dark:bg-#121212">
|
||||
<div class="h-full flex-col">
|
||||
<AppCard
|
||||
v-if="showHeader"
|
||||
class="sticky top-0 z-1 min-h-60 flex items-center justify-between px-24"
|
||||
border-b="1px solid light_border dark:dark_border"
|
||||
>
|
||||
<slot v-if="$slots.header" name="header" />
|
||||
<template v-else>
|
||||
<div class="flex items-center">
|
||||
<slot v-if="$slots['title-prefix']" name="title-prefix" />
|
||||
<template v-else-if="back">
|
||||
<div
|
||||
class="mr-16 flex cursor-pointer items-center text-16 opacity-60 transition-all-300 hover:opacity-40"
|
||||
@click="router.back()"
|
||||
>
|
||||
<i class="i-material-symbols:arrow-left-alt" />
|
||||
<span class="ml-4">返回</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="mr-12 h-16 w-4 rounded-l-2 bg-primary"></div>
|
||||
<h2 class="font-normal">{{ title ?? route.meta?.title }}</h2>
|
||||
<slot name="title-suffix" />
|
||||
</div>
|
||||
<slot name="action" />
|
||||
</template>
|
||||
</AppCard>
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<AppCard class="cus-scroll m-12 h-0 flex-1 rounded-8 p-24" bordered>
|
||||
<slot />
|
||||
</AppCard>
|
||||
</transition>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="bg-#f5f6fb dark:bg-#121212">
|
||||
<slot v-if="$slots.footer" name="footer" />
|
||||
<AppCard v-else-if="showFooter" class="py-12">
|
||||
<TheFooter />
|
||||
</AppCard>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
back: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
</script>
|
||||
24
src/components/common/TheFooter.vue
Normal file
24
src/components/common/TheFooter.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<!--------------------------------
|
||||
- @Description: 底部
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/04 22:42:33
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<footer class="f-c-c text-14 text-gray-500">
|
||||
<p>
|
||||
Copyright © 2023
|
||||
<a
|
||||
href="https://github.com/zclzone"
|
||||
target="__blank"
|
||||
class="transition"
|
||||
hover="decoration-underline color-primary"
|
||||
>
|
||||
Ronnie Zhang(大脸怪)
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</template>
|
||||
4
src/components/common/index.js
Normal file
4
src/components/common/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as AppCard } from './AppCard.vue'
|
||||
export { default as TheFooter } from './TheFooter.vue'
|
||||
export { default as AppPage } from './AppPage.vue'
|
||||
export { default as CommonPage } from './CommonPage.vue'
|
||||
2
src/components/index.js
Normal file
2
src/components/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './common'
|
||||
export * from './me'
|
||||
35
src/components/me/crud/QueryItem.vue
Normal file
35
src/components/me/crud/QueryItem.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/04 22:51:48
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<label v-if="label || label === 0" class="flex-shrink-0" :style="{ width: labelWidth + 'px' }">
|
||||
{{ label }}
|
||||
</label>
|
||||
<div :style="{ width: contentWidth + 'px' }" class="flex-shrink-0">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
labelWidth: {
|
||||
type: Number,
|
||||
default: 80,
|
||||
},
|
||||
contentWidth: {
|
||||
type: Number,
|
||||
default: 220,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
167
src/components/me/crud/index.vue
Normal file
167
src/components/me/crud/index.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/04 22:51:42
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<AppCard
|
||||
v-if="$slots.default"
|
||||
bordered
|
||||
bg="#fafafc dark:black"
|
||||
class="mb-30 min-h-60 flex justify-between rounded-4 p-16"
|
||||
>
|
||||
<n-space wrap :size="[32, 16]">
|
||||
<slot />
|
||||
</n-space>
|
||||
<div class="flex-shrink-0">
|
||||
<n-button ghost type="primary" @click="handleReset">
|
||||
<i class="i-fe:rotate-ccw mr-4" />
|
||||
重置
|
||||
</n-button>
|
||||
<n-button class="ml-20" type="primary" @click="handleSearch">
|
||||
<i class="i-fe:search mr-4" />
|
||||
搜索
|
||||
</n-button>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<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>
|
||||
import { NDataTable } from 'naive-ui'
|
||||
import { utils, writeFile } from 'xlsx'
|
||||
|
||||
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 {}
|
||||
},
|
||||
},
|
||||
/**
|
||||
* ! 约定接口入参出参
|
||||
* * 分页模式需约定分页接口入参
|
||||
* @pageSize 分页参数:一页展示多少条,默认10
|
||||
* @pageNo 分页参数:页码,默认1
|
||||
* * 需约定接口出参
|
||||
* @pageData 分页模式必须,非分页模式如果没有pageData则取上一层data
|
||||
* @total 分页模式必须,非分页模式如果没有total则取上一层data.length
|
||||
*/
|
||||
getData: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange'])
|
||||
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,
|
||||
...paginationParams,
|
||||
})
|
||||
tableData.value = data?.pageData || data
|
||||
pagination.itemCount = data.total ?? data.length
|
||||
} catch (error) {
|
||||
tableData.value = []
|
||||
pagination.itemCount = 0
|
||||
} finally {
|
||||
emit('onDataChange', tableData.value)
|
||||
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)
|
||||
}
|
||||
}
|
||||
function handleExport(columns = props.columns, data = tableData.value) {
|
||||
if (!data?.length) return $message.warning('没有数据')
|
||||
const columnsData = columns.filter((item) => !!item.title && !item.hideInExcel)
|
||||
const thKeys = columnsData.map((item) => item.key)
|
||||
const thData = columnsData.map((item) => item.title)
|
||||
const trData = data.map((item) => thKeys.map((key) => item[key]))
|
||||
const sheet = utils.aoa_to_sheet([thData, ...trData])
|
||||
const workBook = utils.book_new()
|
||||
utils.book_append_sheet(workBook, sheet, '数据报表')
|
||||
writeFile(workBook, '数据报表.xlsx')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handleExport,
|
||||
})
|
||||
</script>
|
||||
3
src/components/me/index.js
Normal file
3
src/components/me/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as MeModal } from './modal/index.vue'
|
||||
export { default as MeCrud } from './crud/index.vue'
|
||||
export { default as MeQueryItem } from './crud/QueryItem.vue'
|
||||
171
src/components/me/modal/index.vue
Normal file
171
src/components/me/modal/index.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/04 22:51:58
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
:style="{ width: modalOptions.width, ...modalOptions.style }"
|
||||
:preset="undefined"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
>
|
||||
<n-card
|
||||
:title="modalOptions.title"
|
||||
:style="modalOptions.contentStyle"
|
||||
:closable="modalOptions.closable"
|
||||
@close="close()"
|
||||
>
|
||||
<slot></slot>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<template #footer>
|
||||
<slot v-if="$slots.footer" name="footer" />
|
||||
<footer v-else-if="modalOptions.showFooter" class="flex justify-end">
|
||||
<n-button v-if="modalOptions.showCancel" @click="handleCancel()">
|
||||
{{ modalOptions.cancelText }}
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="modalOptions.showOk"
|
||||
type="primary"
|
||||
:loading="modalOptions.okLoading"
|
||||
class="ml-20"
|
||||
@click="handleOk()"
|
||||
>
|
||||
{{ modalOptions.okText }}
|
||||
</n-button>
|
||||
</footer>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
width: {
|
||||
type: String,
|
||||
default: '800px',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消',
|
||||
},
|
||||
okText: {
|
||||
type: String,
|
||||
default: '确定',
|
||||
},
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showOk: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
style: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
contentStyle: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
onOk: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onCancel: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
// 声明一个show变量,用于控制模态框的显示与隐藏
|
||||
const show = ref(false)
|
||||
// 声明一个modalOptions变量,用于存储模态框的配置信息
|
||||
const modalOptions = ref({})
|
||||
|
||||
// 打开模态框
|
||||
function open(options = {}) {
|
||||
// 将props和options合并赋值给modalOptions
|
||||
modalOptions.value = { ...props, ...options }
|
||||
// 将show的值设置为true
|
||||
show.value = true
|
||||
}
|
||||
|
||||
// 定义一个close函数,用于关闭模态框
|
||||
function close() {
|
||||
show.value = false
|
||||
}
|
||||
|
||||
// 定义一个handleOk函数,用于处理模态框确定操作
|
||||
async function handleOk(data) {
|
||||
// 如果modalOptions中没有onOk函数,则直接关闭模态框
|
||||
if (typeof modalOptions.value.onOk !== 'function') {
|
||||
return close()
|
||||
}
|
||||
try {
|
||||
// 调用onOk函数,传入data参数
|
||||
const res = await modalOptions.value.onOk(data)
|
||||
// 如果onOk函数的返回值不为false,则关闭模态框
|
||||
res !== false && close()
|
||||
} catch (error) {
|
||||
// 如果出现异常,则打印错误信息
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 定义一个handleCancel函数,用于处理模态框取消操作
|
||||
async function handleCancel(data) {
|
||||
// 如果modalOptions中没有onCancel函数,则直接关闭模态框
|
||||
if (typeof modalOptions.value.onCancel !== 'function') {
|
||||
return close()
|
||||
}
|
||||
try {
|
||||
// 调用onCancel函数,传入data参数
|
||||
const res = await modalOptions.value.onCancel(data)
|
||||
|
||||
// 如果onCancel函数的返回值不为false,则关闭模态框
|
||||
res !== false && close()
|
||||
} catch (error) {
|
||||
// 如果出现异常,则打印错误信息
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const okLoading = computed({
|
||||
get() {
|
||||
return !!modalOptions.value?.okLoading
|
||||
},
|
||||
set(v) {
|
||||
if (modalOptions.value) {
|
||||
modalOptions.value.okLoading = v
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// 定义一个defineExpose函数,用于暴露open、close、handleOk、handleCancel函数
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
handleOk,
|
||||
handleCancel,
|
||||
okLoading,
|
||||
options: modalOptions,
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user