This commit is contained in:
jacky 2024-04-09 09:09:14 +08:00
parent b8eebd1d17
commit 30b524a25d
37 changed files with 2785 additions and 19 deletions

View File

@ -0,0 +1,104 @@
<template>
<span class="headerAvatar">
<template v-if="picType === 'avatar'">
<el-avatar
v-if="userStore.userInfo.headerImg"
:size="30"
:src="avatar"
/>
<el-avatar
v-else
:size="30"
:src="noAvatar"
/>
</template>
<template v-if="picType === 'img'">
<img
v-if="userStore.userInfo.headerImg"
:src="avatar"
class="avatar"
>
<img
v-else
:src="noAvatar"
class="avatar"
>
</template>
<template v-if="picType === 'file'">
<el-image
:src="file"
class="file"
:preview-src-list="previewSrcList"
:preview-teleported="true"
/>
</template>
</span>
</template>
<script setup>
import noAvatarPng from '@/assets/noBody.png'
import { useUserStore } from '@/pinia/modules/user'
import { computed, ref } from 'vue'
defineOptions({
name: 'CustomPic'
})
const props = defineProps({
picType: {
type: String,
required: false,
default: 'avatar'
},
picSrc: {
type: String,
required: false,
default: ''
},
preview: {
type: Boolean,
default: false
}
})
const path = ref(import.meta.env.VITE_BASE_API + '/')
const noAvatar = ref(noAvatarPng)
const userStore = useUserStore()
const avatar = computed(() => {
if (props.picSrc === '') {
if (userStore.userInfo.headerImg !== '' && userStore.userInfo.headerImg.slice(0, 4) === 'http') {
return userStore.userInfo.headerImg
}
return path.value + userStore.userInfo.headerImg
} else {
if (props.picSrc !== '' && props.picSrc.slice(0, 4) === 'http') {
return props.picSrc
}
return path.value + props.picSrc
}
})
const file = computed(() => {
if (props.picSrc && props.picSrc.slice(0, 4) !== 'http') {
return path.value + props.picSrc
}
return props.picSrc
})
const previewSrcList = computed(() => props.preview ? [file.value] : [])
</script>
<style scoped>
.headerAvatar{
display: flex;
justify-content: center;
align-items: center;
margin-right: 8px;
}
.file{
width: 80px;
height: 80px;
position: relative;
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<el-button
type="primary"
icon="download"
@click="exportExcelFunc"
>导出</el-button>
</template>
<script setup>
const props = defineProps({
templateId: {
type: String,
required: true
},
condition: {
type: Object,
default: () => ({})
},
limit: {
type: Number,
default: 0
},
offset: {
type: Number,
default: 0
},
order: {
type: String,
default: ''
}
})
import { ElMessage } from 'element-plus'
const exportExcelFunc = async() => {
if (props.templateId === '') {
ElMessage.error('组件未设置模板ID')
return
}
const baseUrl = import.meta.env.VITE_BASE_API
const paramsCopy = JSON.parse(JSON.stringify(props.condition))
if (props.limit) {
paramsCopy.limit = props.limit
}
if (props.offset) {
paramsCopy.offset = props.offset
}
if (props.order) {
paramsCopy.order = props.order
}
const params = Object.entries(paramsCopy)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
const url = `${baseUrl}/sysExportTemplate/exportExcel?templateID=${props.templateId}${params ? '&' + params : ''}`
window.open(url, '_blank')
}
</script>

View File

@ -0,0 +1,28 @@
<template>
<el-button
type="primary"
icon="download"
@click="exportTemplate"
>下载模板</el-button>
</template>
<script setup>
const props = defineProps({
templateId: {
type: String,
required: true
}
})
import { ElMessage } from 'element-plus'
const exportTemplate = async() => {
if (props.templateId === '') {
ElMessage.error('组件未设置模板ID')
return
}
const baseUrl = import.meta.env.VITE_BASE_API
const url = `${baseUrl}/sysExportTemplate/exportTemplate?templateID=${props.templateId}`
window.open(url, '_blank')
}
</script>

View File

@ -0,0 +1,40 @@
<template>
<el-upload
:action="url"
:show-file-list="false"
:on-success="handleSuccess"
:multiple="false"
>
<el-button
type="primary"
icon="upload"
>导入</el-button>
</el-upload>
</template>
<script setup>
import { ElMessage } from 'element-plus'
const baseUrl = import.meta.env.VITE_BASE_API
const props = defineProps({
templateId: {
type: String,
required: true
}
})
const emit = defineEmits(['on-success'])
const url = `${baseUrl}/sysExportTemplate/importExcel?templateID=${props.templateId}`
const handleSuccess = (res) => {
if (res.code === 0) {
ElMessage.success('导入成功')
emit('on-success')
} else {
ElMessage.error(res.msg)
}
}
</script>

View File

@ -0,0 +1,35 @@
<template>
<vue-office-docx
:src="docx"
@rendered="rendered"
/>
</template>
<script>
export default {
name: 'Docx'
}
</script>
<script setup>
import { ref, watch } from 'vue'
// VueOfficeDocx
import VueOfficeDocx from '@vue-office/docx'
//
import '@vue-office/docx/lib/index.css'
const model = defineModel({
type: String,
})
const docx = ref(null)
watch(
() => model,
value => { docx.value = value },
{ immediate: true }
)
const rendered = () => {
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,33 @@
<template>
<VueOfficeExcel :src="excel" @rendered="renderedHandler" @error="errorHandler" style="height: 100vh;width: 100vh"/>
</template>
<script>
export default {
name: 'Excel'
}
</script>
<script setup>
//VueOfficeExcel
import VueOfficeExcel from '@vue-office/excel'
//
import '@vue-office/excel/lib/index.css'
import {ref, watch} from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: () => ""
}
})
const excel = ref('')
watch(() => props.modelValue, val => excel.value = val, {immediate: true})
const renderedHandler = () => {
}
const errorHandler = () => {
}
</script>
<style>
</style>

View File

@ -0,0 +1,53 @@
<template>
<div class="border border-solid border-gray-100 h-full w-full">
<el-row>
<div v-if="ext==='docx'">
<Docx v-model="fullFileURL" />
</div>
<div v-else-if="ext==='pdf'">
<Pdf v-model="fullFileURL" />
</div>
<div v-else-if="ext==='xlsx'">
<Excel v-model="fullFileURL" />
</div>
<div v-else-if="ext==='image'">
<el-image
:src="fullFileURL"
lazy
/>
</div>
</el-row>
</div>
</template>
<script>
export default {
name: 'Office'
}
</script>
<script setup>
import { ref, watch, computed } from 'vue'
import Docx from '@/components/office/docx.vue'
import Pdf from '@/components/office/pdf.vue'
import Excel from '@/components/office/excel.vue'
const path = ref(import.meta.env.VITE_BASE_API)
const model = defineModel({ type: String})
const fileUrl = ref('')
const ext = ref('')
watch(
() => model,
val => {
fileUrl.value = val
const fileExt = val.split('.')[1] || ''
const image = ['png', 'jpg', 'jpeg', 'gif']
ext.value = image.includes(fileExt) ? 'image' : fileExt
},
{ immediate: true }
)
const fullFileURL = computed(() => {
return path.value + '/' + fileUrl.value
})
</script>

View File

@ -0,0 +1,36 @@
<template>
<vue-office-pdf
:src="pdf"
@rendered="renderedHandler"
@error="errorHandler"
/>
</template>
<script>
export default {
name: "Pdf"
}
</script>
<script setup>
import {ref, watch} from "vue"
//VueOfficeDocx
import VueOfficePdf from "@vue-office/pdf";
//
import '@vue-office/docx/lib/index.css'
console.log("pdf===>")
const props = defineProps({
modelValue: {
type: String,
default: () => ""
}
})
const pdf = ref(null)
watch(() => props.modelValue, val => pdf.value = val, {immediate: true})
const renderedHandler = () => {
console.log("pdf 加载成功")
}
const errorHandler = () => {
console.log("pdf 错误")
}
</script>

View File

@ -0,0 +1,90 @@
<template>
<div class="border border-solid border-gray-100 h-full">
<Toolbar
:editor="editorRef"
:default-config="toolbarConfig"
mode="default"
/>
<Editor
v-model="valueHtml"
class="overflow-y-hidden mt-0.5"
style="height: 18rem;"
:default-config="editorConfig"
mode="default"
@onCreated="handleCreated"
@onChange="change"
/>
</div>
</template>
<script setup>
import '@wangeditor/editor/dist/css/style.css' // css
const basePath = import.meta.env.VITE_BASE_API
import { onBeforeUnmount, ref, shallowRef, watch } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { useUserStore } from '@/pinia/modules/user'
import { ElMessage } from 'element-plus'
import { getUrl } from '@/utils/image'
const userStore = useUserStore()
const emits = defineEmits(['change', 'update:modelValue'])
const change = (editor) => {
emits('change', editor)
emits('update:modelValue', valueHtml.value)
}
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
const editorRef = shallowRef()
const valueHtml = ref('')
const toolbarConfig = {}
const editorConfig = {
placeholder: '请输入内容...',
MENU_CONF: {}
}
editorConfig.MENU_CONF['uploadImage'] = {
fieldName: 'file',
server: basePath + '/fileUploadAndDownload/upload?noSave=1',
customInsert(res, insertFn) {
if (res.code === 0) {
const urlPath = getUrl(res.data.file.url)
insertFn(urlPath, res.data.file.name)
return
}
ElMessage.error(res.msg)
}
}
//
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const handleCreated = (editor) => {
editorRef.value = editor
valueHtml.value = props.modelValue
}
watch(() => props.modelValue, () => {
valueHtml.value = props.modelValue
})
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,62 @@
<template>
<div class="border border-solid border-gray-100 h-full">
<Editor
v-model="valueHtml"
class="overflow-y-hidden mt-0.5"
:default-config="editorConfig"
mode="default"
@onCreated="handleCreated"
@onChange="change"
/>
</div>
</template>
<script setup>
import '@wangeditor/editor/dist/css/style.css' // css
import { onBeforeUnmount, ref, shallowRef, watch } from 'vue'
import { Editor } from '@wangeditor/editor-for-vue'
import { useUserStore } from '@/pinia/modules/user'
const userStore = useUserStore()
const emits = defineEmits(['change', 'update:modelValue'])
const editorConfig = ref({
readOnly: true
})
const change = (editor) => {
emits('change', editor)
emits('update:modelValue', valueHtml.value)
}
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
const editorRef = shallowRef()
const valueHtml = ref('')
//
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const handleCreated = (editor) => {
editorRef.value = editor
valueHtml.value = props.modelValue
}
watch(() => props.modelValue, () => {
valueHtml.value = props.modelValue
})
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,85 @@
<template>
<div>
<el-upload
multiple
:action="`${path}/fileUploadAndDownload/upload?noSave=1`"
:on-error="uploadError"
:on-success="uploadSuccess"
:show-file-list="true"
:file-list="fileList"
:limit="limit"
:accept="accept"
class="upload-btn"
>
<el-button type="primary">上传文件</el-button>
</el-upload>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/pinia/modules/user'
defineOptions({
name: 'UploadCommon',
})
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
limit: {
type: Number,
default: 3
},
accept: {
type: String,
default: ''
},
})
const path = ref(import.meta.env.VITE_BASE_API)
const userStore = useUserStore()
const fullscreenLoading = ref(false)
const fileList = ref(props.modelValue)
const emits = defineEmits(['update:modelValue'])
watch(fileList.value, (val) => {
console.log(val)
emits('update:modelValue', val)
})
watch(
() => props.modelValue,
value => {
fileList.value = value
},
{ immediate: true }
)
const uploadSuccess = (res) => {
const { data } = res
if (data.file) {
fileList.value.push({
name: data.file.name,
url: data.file.url
})
fullscreenLoading.value = false
}
}
const uploadError = () => {
ElMessage({
type: 'error',
message: '上传失败'
})
fullscreenLoading.value = false
}
</script>

View File

@ -0,0 +1,485 @@
<template>
<div>
<div
v-if="!multiple"
class="update-image"
:style="{
'background-image': `url(${getUrl(model)})`,
'position': 'relative',
}"
>
<el-icon
v-if="isVideoExt(model || '')"
:size="32"
class="video video-icon"
style=""
>
<VideoPlay />
</el-icon>
<video
v-if="isVideoExt(model || '')"
class="avatar video-avatar video"
muted
preload="metadata"
style=""
@click="openChooseImg"
>
<source :src="getUrl(model) + '#t=1'">
</video>
<span
v-if="model"
class="update"
style="position: absolute;"
@click="openChooseImg"
>
<el-icon>
<delete />
</el-icon>
删除</span>
<span
v-else
class="update text-gray-600"
@click="openChooseImg"
>
<el-icon>
<plus />
</el-icon>
上传</span>
</div>
<div
v-else
class="multiple-img"
>
<div
v-for="(item, index) in multipleValue"
:key="index"
class="update-image"
:style="{
'background-image': `url(${getUrl(item)})`,
'position': 'relative',
}"
>
<el-icon
v-if="isVideoExt(item || '')"
:size="32"
class="video video-icon"
>
<VideoPlay />
</el-icon>
<video
v-if="isVideoExt(item || '')"
class="avatar video-avatar video"
muted
preload="metadata"
@click="deleteImg(index)"
>
<source :src="getUrl(item) + '#t=1'">
</video>
<span
class="update"
style="position: absolute;"
@click="deleteImg(index)"
>
<el-icon>
<delete />
</el-icon>
删除</span>
</div>
<div
v-if="!maxUpdateCount || maxUpdateCount>multipleValue.length"
class="add-image"
>
<span
class="update text-gray-600"
@click="openChooseImg"
>
<el-icon>
<Plus />
</el-icon>
上传</span>
</div>
</div>
<el-drawer
v-model="drawer"
title="媒体库"
size="650px"
>
<warning-bar
title="点击“文件名/备注”可以编辑文件名或者备注内容。"
/>
<div class="gva-btn-list">
<upload-common
:image-common="imageCommon"
class="upload-btn-media-library"
@on-success="getImageList"
/>
<upload-image
:image-url="imageUrl"
:file-size="512"
:max-w-h="1080"
class="upload-btn-media-library"
@on-success="getImageList"
/>
<el-form
ref="searchForm"
:inline="true"
:model="search"
>
<el-form-item label="">
<el-input
v-model="search.keyword"
class="keyword"
placeholder="请输入文件名或备注"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
icon="search"
@click="getImageList"
>查询
</el-button>
</el-form-item>
</el-form>
</div>
<div class="media">
<div
v-for="(item,key) in picList"
:key="key"
class="media-box"
>
<div class="header-img-box-list">
<el-image
:key="key"
:src="getUrl(item.url)"
fit="cover"
style="width: 100%;height: 100%;"
@click="chooseImg(item.url)"
>
<template #error>
<el-icon
v-if="isVideoExt(item.url || '')"
:size="32"
class="video video-icon"
>
<VideoPlay />
</el-icon>
<video
v-if="isVideoExt(item.url || '')"
class="avatar video-avatar video"
muted
preload="metadata"
@click="chooseImg(item.url)"
>
<source :src="getUrl(item.url) + '#t=1'">
您的浏览器不支持视频播放
</video>
<div
v-else
class="header-img-box-list"
>
<el-icon class="lost-image">
<icon-picture />
</el-icon>
</div>
</template>
</el-image>
</div>
<div
class="img-title"
@click="editFileNameFunc(item)"
>{{ item.name }}
</div>
</div>
</div>
<el-pagination
:current-page="page"
:page-size="pageSize"
:total="total"
:style="{'justify-content':'center'}"
layout="total, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</el-drawer>
</div>
</template>
<script setup>
import { getUrl, isVideoExt } from '@/utils/image'
import { onMounted, ref } from 'vue'
import { getFileList, editFileName } from '@/api/fileUploadAndDownload'
import UploadImage from '@/components/upload/image.vue'
import UploadCommon from '@/components/upload/common.vue'
import WarningBar from '@/components/warningBar/warningBar.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Plus, Picture as IconPicture } from '@element-plus/icons-vue'
const imageUrl = ref('')
const imageCommon = ref('')
const search = ref({})
const page = ref(1)
const total = ref(0)
const pageSize = ref(20)
const model = defineModel({ type: [String, Array] })
const props = defineProps({
multiple: {
type: Boolean,
default: false
},
fileType: {
type: String,
default: ''
},
maxUpdateCount: {
type: Number,
default: 0
}
})
const multipleValue = ref([])
onMounted(() => {
if (props.multiple) {
multipleValue.value = model.value
}
})
const deleteImg = (index) => {
multipleValue.value.splice(index, 1)
model.value = multipleValue.value
}
//
const handleSizeChange = (val) => {
pageSize.value = val
getImageList()
}
const handleCurrentChange = (val) => {
page.value = val
getImageList()
}
const editFileNameFunc = async(row) => {
ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /\S/,
inputErrorMessage: '不能为空',
inputValue: row.name
}).then(async({ value }) => {
row.name = value
// console.log(row)
const res = await editFileName(row)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '编辑成功!',
})
getImageList()
}
}).catch(() => {
ElMessage({
type: 'info',
message: '取消修改'
})
})
}
const drawer = ref(false)
const picList = ref([])
const imageTypeList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']
const videoTyteList = ['mp4', 'avi', 'rmvb', 'rm', 'asf', 'divx', 'mpg', 'mpeg', 'mpe', 'wmv', 'mkv', 'vob']
const listObj = {
image: imageTypeList,
video: videoTyteList
}
const chooseImg = (url) => {
console.log(url)
if (props.fileType) {
const typeSuccess = listObj[props.fileType].some(item => {
if (url.includes(item)) {
return true
}
})
if (!typeSuccess) {
ElMessage({
type: 'error',
message: '当前类型不支持使用'
})
return
}
}
if (props.multiple) {
multipleValue.value.push(url)
model.value = multipleValue.value
} else {
model.value = url
}
drawer.value = false
}
const openChooseImg = async() => {
if (model.value && !props.multiple) {
model.value = ''
return
}
await getImageList()
drawer.value = true
}
const getImageList = async() => {
const res = await getFileList({ page: page.value, pageSize: pageSize.value, ...search.value })
if (res.code === 0) {
picList.value = res.data.list
total.value = res.data.total
page.value = res.data.page
pageSize.value = res.data.pageSize
}
}
</script>
<style scoped lang="scss">
.multiple-img {
display: flex;
gap: 8px;
width: 100%;
flex-wrap: wrap;
}
.add-image {
width: 120px;
height: 120px;
line-height: 120px;
display: flex;
justify-content: center;
border-radius: 20px;
border: 1px dashed #ccc;
background-size: cover;
cursor: pointer;
}
.update-image {
cursor: pointer;
width: 120px;
height: 120px;
line-height: 120px;
display: flex;
justify-content: center;
border-radius: 20px;
border: 1px dashed #ccc;
background-repeat: no-repeat;
background-size: cover;
position: relative;
&:hover {
color: #fff;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.15) 0%,
rgba(0, 0, 0, 0.15) 100%
),
radial-gradient(
at top center,
rgba(255, 255, 255, 0.4) 0%,
rgba(0, 0, 0, 0.4) 120%
) #989898;
background-blend-mode: multiply, multiply;
background-size: cover;
.update {
color: #fff;
}
.video {
opacity: 0.2;
}
}
.video-icon {
position: absolute;
left: calc(50% - 16px);
top: calc(50% - 16px);
}
video {
object-fit: cover;
max-width: 100%;
border-radius: 20px;
}
.update {
height: 120px;
width: 120px;
text-align: center;
color: transparent;
position: absolute;
}
}
.upload-btn-media-library {
margin-left: 20px;
}
.media {
display: flex;
flex-wrap: wrap;
.media-box {
width: 120px;
margin-left: 20px;
.img-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 36px;
text-align: center;
cursor: pointer;
}
.header-img-box-list {
width: 120px;
height: 120px;
border: 1px dashed #ccc;
border-radius: 8px;
text-align: center;
line-height: 120px;
cursor: pointer;
overflow: hidden;
.el-image__inner {
max-width: 120px;
max-height: 120px;
vertical-align: middle;
width: unset;
height: unset;
}
.el-image {
position: relative;
}
.video-icon {
position: absolute;
left: calc(50% - 16px);
top: calc(50% - 16px);
}
video {
object-fit: cover;
max-width: 100%;
min-height: 100%;
border-radius: 8px;
}
}
}
}
</style>

View File

@ -0,0 +1,39 @@
<template>
<svg
:class="svgClass"
v-bind="$attrs"
:color="color"
>
<use
:xlink:href="'#'+name"
rel="external nofollow"
/>
</svg>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
name: {
type: String,
required: true
},
color: {
type: String,
default: 'currentColor'
}
})
const svgClass = computed(() => {
if (props.name) {
return `svg-icon ${props.name}`
}
return 'svg-icon'
})
</script>
<style scoped>
.svg-icon {
@apply w-4 h-4;
fill: currentColor;
vertical-align: middle;
}
</style>

View File

@ -0,0 +1,75 @@
<template>
<div>
<el-upload
:action="`${path}/fileUploadAndDownload/upload`"
:before-upload="checkFile"
:on-error="uploadError"
:on-success="uploadSuccess"
:show-file-list="false"
class="upload-btn"
>
<el-button type="primary">普通上传</el-button>
</el-upload>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { isVideoMime, isImageMime } from '@/utils/image'
defineOptions({
name: 'UploadCommon',
})
const emit = defineEmits(['on-success'])
const path = ref(import.meta.env.VITE_BASE_API)
const fullscreenLoading = ref(false)
const checkFile = (file) => {
fullscreenLoading.value = true
const isLt500K = file.size / 1024 / 1024 < 0.5 // 500K, @todo
const isLt5M = file.size / 1024 / 1024 < 5 // 5MB, @todo
const isVideo = isVideoMime(file.type)
const isImage = isImageMime(file.type)
let pass = true
if (!isVideo && !isImage) {
ElMessage.error('上传图片只能是 jpg,png,svg,webp 格式, 上传视频只能是 mp4,webm 格式!')
fullscreenLoading.value = false
pass = false
}
if (!isLt5M && isVideo) {
ElMessage.error('上传视频大小不能超过 5MB')
fullscreenLoading.value = false
pass = false
}
if (!isLt500K && isImage) {
ElMessage.error('未压缩的上传图片大小不能超过 500KB请使用压缩上传')
fullscreenLoading.value = false
pass = false
}
console.log('upload file check result: ', pass)
return pass
}
const uploadSuccess = (res) => {
const { data } = res
if (data.file) {
emit('on-success', data.file.url)
}
}
const uploadError = () => {
ElMessage({
type: 'error',
message: '上传失败'
})
fullscreenLoading.value = false
}
</script>

View File

@ -0,0 +1,97 @@
<template>
<div>
<el-upload
:action="`${path}/fileUploadAndDownload/upload`"
:show-file-list="false"
:on-success="handleImageSuccess"
:before-upload="beforeImageUpload"
:multiple="false"
>
<el-button type="primary">压缩上传</el-button>
</el-upload>
</div>
</template>
<script setup>
import ImageCompress from '@/utils/image'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/pinia/modules/user'
defineOptions({
name: 'UploadImage',
})
const emit = defineEmits(['on-success'])
const props = defineProps({
imageUrl: {
type: String,
default: ''
},
fileSize: {
type: Number,
default: 2048 // 2M
},
maxWH: {
type: Number,
default: 1920 //
}
})
const path = ref(import.meta.env.VITE_BASE_API)
const userStore = useUserStore()
const beforeImageUpload = (file) => {
const isJPG = file.type === 'image/jpeg'
const isPng = file.type === 'image/png'
if (!isJPG && !isPng) {
ElMessage.error('上传头像图片只能是 jpg或png 格式!')
return false
}
const isRightSize = file.size / 1024 < props.fileSize
if (!isRightSize) {
//
const compress = new ImageCompress(file, props.fileSize, props.maxWH)
return compress.compress()
}
return isRightSize
}
const handleImageSuccess = (res) => {
const { data } = res
if (data.file) {
emit('on-success', data.file.url)
}
}
</script>
<style lang="scss" scoped>
.image-uploader {
border: 1px dashed #d9d9d9;
width: 180px;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.image-uploader {
border-color: #409eff;
}
.image-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.image {
width: 178px;
height: 178px;
display: block;
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<div
class="px-1.5 py-2 flex items-center bg-amber-50 rounded gap-2 mb-3 text-amber-500"
:class="href&&'cursor-pointer'"
@click="open"
>
<el-icon class="text-xl">
<warning-filled />
</el-icon>
<span>
{{ title }}
</span>
</div>
</template>
<script setup>
import { WarningFilled } from '@element-plus/icons-vue'
const prop = defineProps({
title: {
type: String,
default: ''
},
href: {
type: String,
default: ''
}
})
const open = () => {
if (prop.href) {
window.open(prop.href)
}
}
</script>

12
src/core/admin.js Normal file
View File

@ -0,0 +1,12 @@
/*
* web框架组
*
* */
// 加载网站配置文件夹
import { register } from './global'
export default {
install: (app) => {
register(app)
}
}

16
src/core/config.js Normal file
View File

@ -0,0 +1,16 @@
/**
* 网站配置文件
*/
const config = {
appName: '利农天下',
appLogo: '',
showViteLogo: true,
logs: [],
}
export const viteLogo = (env) => {
console.log('哈哈哈')
}
export default config

44
src/core/global.js Normal file
View File

@ -0,0 +1,44 @@
import config from './config'
import { h } from 'vue'
// 统一导入el-icon图标
import * as ElIconModules from '@element-plus/icons-vue'
import svgIcon from '@/components/svgIcon/svgIcon.vue'
// 导入转换图标名称的函数
const createIconComponent = (name) => ({
name: 'SvgIcon',
render() {
return h(svgIcon, {
name: name,
})
},
})
const registerIcons = async (app) => {
const iconModules = import.meta.glob('@/assets/icons/**/*.svg')
for (const path in iconModules) {
const iconName = path.split('/').pop().replace(/\.svg$/, '')
// 如果iconName带空格则不加入到图标库中并且提示名称不合法
if (iconName.indexOf(' ') !== -1) {
console.error(`icon ${iconName}.svg includes whitespace`)
continue
}
const iconComponent = createIconComponent(iconName)
config.logs.push({
'key': iconName,
'label': iconName,
})
app.component(iconName, iconComponent)
}
}
export const register = (app) => {
// 统一注册el-icon图标
for (const iconName in ElIconModules) {
app.component(iconName, ElIconModules[iconName])
}
app.component('SvgIcon', svgIcon)
registerIcons(app)
app.config.globalProperties.$GIN_VUE_ADMIN = config
}

41
src/directive/auth.js Normal file
View File

@ -0,0 +1,41 @@
// 权限按钮展示指令
import { useUserStore } from '@/pinia/modules/user'
export default {
install: (app) => {
const userStore = useUserStore()
app.directive('auth', {
// 当被绑定的元素插入到 DOM 中时……
mounted: function(el, binding) {
const userInfo = userStore.userInfo
let type = ''
switch (Object.prototype.toString.call(binding.value)) {
case '[object Array]':
type = 'Array'
break
case '[object String]':
type = 'String'
break
case '[object Number]':
type = 'Number'
break
default:
type = ''
break
}
if (type === '') {
el.parentNode.removeChild(el)
return
}
const waitUse = binding.value.toString().split(',')
let flag = waitUse.some(item => Number(item) === userInfo.authorityId)
if (binding.modifiers.not) {
flag = !flag
}
if (!flag) {
el.parentNode.removeChild(el)
}
}
})
}
}

View File

@ -0,0 +1,23 @@
// 监听 window 的 resize 事件,返回当前窗口的宽高
import { shallowRef } from 'vue'
import { tryOnMounted, useEventListener } from '@vueuse/core'
const width = shallowRef(0)
const height = shallowRef(0)
export const useWindowResize = (cb) => {
const onResize = () => {
width.value = window.innerWidth
height.value = window.innerHeight
if (cb && typeof cb === 'function') {
cb(width.value, height.value)
}
}
tryOnMounted(onResize)
useEventListener('resize', onResize, { passive: true })
return {
width,
height,
}
}

7
src/pinia/index.js Normal file
View File

@ -0,0 +1,7 @@
import { createPinia } from 'pinia'
const store = createPinia()
export {
store
}

View File

@ -0,0 +1,40 @@
import { findSysDictionary } from '@/api/sysDictionary'
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useDictionaryStore = defineStore('dictionary', () => {
const dictionaryMap = ref({})
const setDictionaryMap = (dictionaryRes) => {
dictionaryMap.value = { ...dictionaryMap.value, ...dictionaryRes }
}
const getDictionary = async(type) => {
if (dictionaryMap.value[type] && dictionaryMap.value[type].length) {
return dictionaryMap.value[type]
} else {
const res = await findSysDictionary({ type })
if (res.code === 0) {
const dictionaryRes = {}
const dict = []
res.data.resysDictionary.sysDictionaryDetails && res.data.resysDictionary.sysDictionaryDetails.forEach(item => {
dict.push({
label: item.label,
value: item.value,
extend: item.extend
})
})
dictionaryRes[res.data.resysDictionary.type] = dict
setDictionaryMap(dictionaryRes)
return dictionaryMap.value[type]
}
}
}
return {
dictionaryMap,
setDictionaryMap,
getDictionary
}
})

103
src/pinia/modules/router.js Normal file
View File

@ -0,0 +1,103 @@
import { asyncRouterHandle } from '@/utils/asyncRouter'
import { emitter } from '@/utils/bus.js'
import { asyncMenu } from '@/api/menu'
import { defineStore } from 'pinia'
import { ref } from 'vue'
const notLayoutRouterArr = []
const keepAliveRoutersArr = []
const nameMap = {}
const formatRouter = (routes, routeMap, parent) => {
routes && routes.forEach(item => {
item.parent = parent
item.meta.btns = item.btns
item.meta.hidden = item.hidden
if (item.meta.defaultMenu === true) {
if (!parent) {
item = { ...item, path: `/${item.path}` }
notLayoutRouterArr.push(item)
}
}
routeMap[item.name] = item
if (item.children && item.children.length > 0) {
formatRouter(item.children, routeMap, item)
}
})
}
const KeepAliveFilter = (routes) => {
routes && routes.forEach(item => {
// 子菜单中有 keep-alive 的,父菜单也必须 keep-alive否则无效。这里将子菜单中有 keep-alive 的父菜单也加入。
if ((item.children && item.children.some(ch => ch.meta.keepAlive) || item.meta.keepAlive)) {
item.component && item.component().then(val => {
keepAliveRoutersArr.push(val.default.name)
nameMap[item.name] = val.default.name
})
}
if (item.children && item.children.length > 0) {
KeepAliveFilter(item.children)
}
})
}
export const useRouterStore = defineStore('router', () => {
const keepAliveRouters = ref([])
const asyncRouterFlag = ref(0)
const setKeepAliveRouters = (history) => {
const keepArrTemp = []
history.forEach(item => {
if (nameMap[item.name]) {
keepArrTemp.push(nameMap[item.name])
}
})
keepAliveRouters.value = Array.from(new Set(keepArrTemp))
}
emitter.on('setKeepAlive', setKeepAliveRouters)
const asyncRouters = ref([])
const routeMap = ({})
// 从后台获取动态路由
const SetAsyncRouter = async() => {
asyncRouterFlag.value++
const baseRouter = [{
path: '/layout',
name: 'layout',
component: 'view/layout/index.vue',
meta: {
title: '底层layout'
},
children: []
}]
const asyncRouterRes = await asyncMenu()
const asyncRouter = asyncRouterRes.data.menus
asyncRouter && asyncRouter.push({
path: 'reload',
name: 'Reload',
hidden: true,
meta: {
title: '',
closeTab: true,
},
component: 'view/error/reload.vue'
})
formatRouter(asyncRouter, routeMap)
baseRouter[0].children = asyncRouter
if (notLayoutRouterArr.length !== 0) {
baseRouter.push(...notLayoutRouterArr)
}
asyncRouterHandle(baseRouter)
KeepAliveFilter(asyncRouter)
asyncRouters.value = baseRouter
return true
}
return {
asyncRouters,
keepAliveRouters,
asyncRouterFlag,
SetAsyncRouter,
routeMap
}
})

162
src/pinia/modules/user.js Normal file
View File

@ -0,0 +1,162 @@
import { login, getUserInfo, setSelfInfo } from '@/api/user'
import { jsonInBlacklist } from '@/api/jwt'
import router from '@/router/index'
import { ElLoading, ElMessage } from 'element-plus'
import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'
import { useRouterStore } from './router'
import cookie from 'js-cookie'
export const useUserStore = defineStore('user', () => {
const loadingInstance = ref(null)
const userInfo = ref({
uuid: '',
nickName: '',
headerImg: '',
authority: {},
sideMode: 'dark',
activeColor: 'var(--el-color-primary)',
baseColor: '#fff'
})
const token = ref(window.localStorage.getItem('token') || cookie.get('x-token') || '')
const setUserInfo = (val) => {
userInfo.value = val
}
const setToken = (val) => {
token.value = val
}
const NeedInit = () => {
token.value = ''
window.localStorage.removeItem('token')
router.push({ name: 'Init', replace: true })
}
const ResetUserInfo = (value = {}) => {
userInfo.value = {
...userInfo.value,
...value
}
}
/* 获取用户信息*/
const GetUserInfo = async() => {
const res = await getUserInfo()
if (res.code === 0) {
setUserInfo(res.data.userInfo)
}
return res
}
/* 登录*/
const LoginIn = async(loginInfo) => {
loadingInstance.value = ElLoading.service({
fullscreen: true,
text: '登录中,请稍候...',
})
try {
const res = await login(loginInfo)
if (res.code === 0) {
setUserInfo(res.data.user)
setToken(res.data.token)
const routerStore = useRouterStore()
await routerStore.SetAsyncRouter()
const asyncRouters = routerStore.asyncRouters
asyncRouters.forEach(asyncRouter => {
router.addRoute(asyncRouter)
})
if (!router.hasRoute(userInfo.value.authority.defaultRouter)) {
ElMessage.error('请联系管理员进行授权')
} else {
await router.replace({ name: userInfo.value.authority.defaultRouter })
}
loadingInstance.value.close()
const isWin = ref(/windows/i.test(navigator.userAgent))
if (isWin.value) {
window.localStorage.setItem('osType', 'WIN')
} else {
window.localStorage.setItem('osType', 'MAC')
}
return true
}
} catch (e) {
loadingInstance.value.close()
}
loadingInstance.value.close()
}
/* 登出*/
const LoginOut = async() => {
const res = await jsonInBlacklist()
if (res.code === 0) {
await ClearStorage()
router.push({ name: 'Login', replace: true })
window.location.reload()
}
}
/* 清理数据 */
const ClearStorage = async() => {
token.value = ''
sessionStorage.clear()
window.localStorage.removeItem('token')
cookie.remove('x-token')
}
/* 设置侧边栏模式*/
const changeSideMode = async(data) => {
const res = await setSelfInfo({ sideMode: data })
if (res.code === 0) {
userInfo.value.sideMode = data
ElMessage({
type: 'success',
message: '设置成功'
})
}
}
const mode = computed(() => userInfo.value.sideMode)
const sideMode = computed(() => {
if (userInfo.value.sideMode === 'dark') {
return '#191a23'
} else if (userInfo.value.sideMode === 'light') {
return '#fff'
} else {
return userInfo.value.sideMode
}
})
const baseColor = computed(() => {
if (userInfo.value.sideMode === 'dark') {
return '#fff'
} else if (userInfo.value.sideMode === 'light') {
return '#191a23'
} else {
return userInfo.value.baseColor
}
})
const activeColor = computed(() => {
return 'var(--el-color-primary)'
})
watch(() => token.value, () => {
window.localStorage.setItem('token', token.value)
})
return {
userInfo,
token,
NeedInit,
ResetUserInfo,
GetUserInfo,
LoginIn,
LoginOut,
changeSideMode,
mode,
sideMode,
setToken,
baseColor,
activeColor,
loadingInstance,
ClearStorage
}
})

View File

@ -0,0 +1,30 @@
import service from '@/utils/request'
// @Tags System
// @Summary 发送测试邮件
// @Security ApiKeyAuth
// @Produce application/json
// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}"
// @Router /email/emailTest [post]
export const emailTest = (data) => {
return service({
url: '/email/emailTest',
method: 'post',
data
})
}
// @Tags System
// @Summary 发送邮件
// @Security ApiKeyAuth
// @Produce application/json
// @Param data body email_response.Email true "发送邮件必须的参数"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}"
// @Router /email/sendEmail [post]
export const sendEmail = (data) => {
return service({
url: '/email/sendEmail',
method: 'post',
data
})
}

View File

@ -0,0 +1,63 @@
<template>
<div>
<warning-bar title="需要提前配置email配置文件为防止不必要的垃圾邮件在线体验功能不开放此功能体验。" />
<div class="gva-form-box">
<el-form
ref="emailForm"
label-position="right"
label-width="80px"
:model="form"
>
<el-form-item label="目标邮箱">
<el-input v-model="form.to" />
</el-form-item>
<el-form-item label="邮件">
<el-input v-model="form.subject" />
</el-form-item>
<el-form-item label="邮件内容">
<el-input
v-model="form.body"
type="textarea"
/>
</el-form-item>
<el-form-item>
<el-button @click="sendTestEmail">发送测试邮件</el-button>
<el-button @click="sendEmail">发送邮件</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup>
import WarningBar from '@/components/warningBar/warningBar.vue'
import { emailTest } from '@/plugin/email/api/email.js'
import { ElMessage } from 'element-plus'
import { reactive, ref } from 'vue'
defineOptions({
name: 'Email',
})
const emailForm = ref(null)
const form = reactive({
to: '',
subject: '',
body: '',
})
const sendTestEmail = async() => {
const res = await emailTest()
if (res.code === 0) {
ElMessage.success('发送成功')
}
}
const sendEmail = async() => {
const res = await emailTest()
if (res.code === 0) {
ElMessage.success('发送成功,请查收')
}
}
</script>

31
src/router/index.js Normal file
View File

@ -0,0 +1,31 @@
import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [{
path: '/',
redirect: '/login'
},
{
path: '/init',
name: 'Init',
component: () => import('@/view/init/index.vue')
},
{
path: '/login',
name: 'Login',
component: () => import('@/view/login/index.vue')
},
{
path: '/:catchAll(.*)',
meta: {
closeTab: true,
},
component: () => import('@/view/error/index.vue')
}
]
const router = createRouter({
history: createWebHashHistory(),
routes,
})
export default router

View File

@ -0,0 +1,24 @@
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'white': #ffffff,
'black': #000000,
'primary': (
'base': #4d70ff,
),
'success': (
'base': #67c23a,
),
'warning': (
'base': #e6a23c,
),
'danger': (
'base': #f56c6c,
),
'error': (
'base': #f56c6c,
),
'info': (
'base': #909399,
),
)
);

View File

@ -0,0 +1,42 @@
@import '@/style/main.scss';
.el-button {
font-weight: 400;
border-radius: 2px;
}
::-webkit-scrollbar {
@apply hidden;
}
.gva-search-box {
@apply p-6 pb-0.5 bg-white rounded mb-3;
}
.gva-form-box {
@apply p-6 bg-white rounded;
}
.gva-pagination {
@apply flex justify-end;
.el-pagination__editor {
.el-input__inner {
@apply h-8;
}
}
.is-active {
@apply rounded text-white;
background: var(--el-color-primary);
color: #ffffff !important;
}
}
.el-drawer__header{
margin-bottom: 0 !important;
padding-top: 16px !important;
padding-bottom: 16px !important;
@apply border-0 border-b border-solid border-gray-200;
}

47
src/style/iconfont.css Normal file
View File

@ -0,0 +1,47 @@
@font-face {
font-family: 'gvaIcon';
src: url('data:font/ttf;charset=utf-8;base64,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AAAAAAALABMBkAADAAEECQAAACYAAAADAAEECQABABAAPAADAAEECQACAA4AVwADAAEECQADABAAbwADAAEECQAEABAAigADAAEECQAFABYApQADAAEECQAGABAAyQADAAEECQAKAFYA5AADAAEECQALACYBaABDAHIAZQBhAHQAZQBkACAAYgB5ACAAaQBjAG8AbgBmAG8AbgB0AABDcmVhdGVkIGJ5IGljb25mb250AABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABSAGUAZwB1AGwAYQByAABSZWd1bGFyAABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABWAGUAcgBzAGkAbwBuACAAMQAuADAAAFZlcnNpb24gMS4wAABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABHAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAHMAdgBnADIAdAB0AGYAIABmAHIAbwBtACAARgBvAG4AdABlAGwAbABvACAAcAByAG8AagBlAGMAdAAuAABHZW5lcmF0ZWQgYnkgc3ZnMnR0ZiBmcm9tIEZvbnRlbGxvIHByb2plY3QuAABoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAABodHRwOi8vZm9udGVsbG8uY29tAAAAAAIAAAAAAAAACgAAAAAAAQAAAAAAAAAAAAAAAAAAAAAACwAAAAEAAgECAQMBBAEFAQYBBwEIAQkRYXJyb3ctZG91YmxlLWxlZnQSYXJyb3ctZG91YmxlLXJpZ2h0EGN1c3RvbWVyLXNlcnZpY2URZnVsbHNjcmVlbi1leHBhbmQRZnVsbHNjcmVlbi1zaHJpbmsGcHJvbXB0B3JlZnJlc2gGc2VhcmNoAAAAAf//AAIAAQAAAAwAAAAWAAAAAgABAAMACgABAAQAAAACAAAAAAAAAAEAAAAA1aQnCAAAAADeWFEjAAAAAN5YUSM=') format('truetype');
font-weight: 600;
font-style: normal;
font-display: swap;
}
.gvaIcon {
font-family: "gvaIcon" !important;
font-size: 16px;
font-style: normal;
font-weight: 800;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.gvaIcon-arrow-double-left:before {
content: "\e665";
}
.gvaIcon-arrow-double-right:before {
content: "\e666";
}
.gvaIcon-fullscreen-shrink:before {
content: "\e676";
}
.gvaIcon-customer-service:before {
content: "\e66a";
}
.gvaIcon-fullscreen-expand:before {
content: "\e675";
}
.gvaIcon-prompt:before {
content: "\e67b";
}
.gvaIcon-refresh:before {
content: "\e67c";
}
.gvaIcon-search:before {
content: "\e67d";
}

701
src/style/main.scss Normal file
View File

@ -0,0 +1,701 @@
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
@import '@/style/iconfont.css';
html {
line-height: 1.15;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box;
/* 1 */
height: 0;
/* 1 */
overflow: visible;
/* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none;
/* 1 */
text-decoration: underline;
/* 2 */
text-decoration: underline dotted;
/* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
line-height: 1.15;
/* 1 */
margin: 0;
/* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box;
/* 1 */
color: inherit;
/* 2 */
display: table;
/* 1 */
max-width: 100%;
/* 1 */
padding: 0;
/* 3 */
white-space: normal;
/* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}
HTML,
body,
div,
ul,
ol,
dl,
li,
dt,
dd,
p,
blockquote,
pre,
form,
fieldset,
table,
th,
td {
border: none;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
font-size: 14px;
margin: 0px;
padding: 0px;
}
html,
body {
height: 100%;
width: 100%;
}
address,
caption,
cite,
code,
th,
var {
font-style: normal;
font-weight: normal;
}
a {
text-decoration: none;
}
input::-ms-clear {
display: none;
}
input::-ms-reveal {
display: none;
}
input {
-webkit-appearance: none;
margin: 0;
outline: none;
padding: 0;
}
input::-webkit-input-placeholder {
color: #ccc;
}
input::-ms-input-placeholder {
color: #ccc;
}
input::-moz-placeholder {
color: #ccc;
}
input[type=submit],
input[type=button] {
cursor: pointer;
}
button[disabled],
input[disabled] {
cursor: default;
}
img {
border: none;
}
ul,
ol,
li {
list-style-type: none;
}
// 导航
#app {
.el-container {
@apply relative h-full w-full;
}
.el-container.mobile.openside {
@apply fixed top-0;
}
.gva-aside {
@apply fixed top-0 left-0 z-[1001] overflow-hidden;
.el-menu {
@apply border-r-0;
}
}
.aside {
.el-menu--collapse {
>.el-menu-item {
display: flex;
justify-content: center;
}
}
.el-sub-menu {
.el-menu {
.is-active {
// 关闭三级菜单二级菜单样式
ul {
border: none;
}
}
// 关闭三级菜单二级菜单样式
.is-active.is-opened {
ul {
border: none;
}
}
}
}
}
.hideside {
.aside {
@apply w-[54px]
}
}
.mobile {
.gva-aside {
@apply w-[54px];
}
}
.hideside {
.main-cont.el-main {
@apply ml-[54px];
}
}
.mobile {
.main-cont.el-main {
@apply ml-0;
}
}
}
// layout
.admin-box {
@apply min-h-[calc(100vh-200px)] px-3 py-4 mt-28 mb-4 mx-1;
.el-table {
th {
@apply px-0 py-2;
.cell {
@apply leading-[40px] text-gray-700;
}
}
td {
@apply px-0 py-2;
.cell {
@apply leading-[40px] text-gray-600;
}
}
.is-leaf {
@apply border-b border-t-0 border-l-0 border-solid border-gray-50;
border-right:var(--el-table-border);
background: #F7FBFF !important;
}
}
}
// table
.el-pagination {
@apply mt-8;
.btn-prev,
.btn-next {
@apply border border-solid border-gray-300 rounded;
}
.el-pager {
li {
@apply border border-solid border-gray-300 rounded text-gray-600 text-sm mx-1;
}
}
}
.el-container.layout-cont {
.header-cont {
@apply px-4 h-16 bg-white;
}
.main-cont {
@apply h-screen overflow-visible;
&.el-main {
@apply min-h-full ml-[220px] bg-main p-0 overflow-auto;
}
.breadcrumb {
@apply h-16 flex items-center p-0 ml-12 text-lg;
.el-breadcrumb__item {
.el-breadcrumb__inner {
@apply text-gray-600;
}
}
.el-breadcrumb__item:nth-last-child(1) {
.el-breadcrumb__inner {
@apply text-gray-600;
}
}
}
.router-history {
@apply bg-white p-0 border-t border-l-0 border-r-0 border-b-0 border-solid border-gray-100;
.el-tabs__header {
@apply m-0;
.el-tabs__item{
@apply border-solid border-r border-t-0 border-gray-100 border-b-0 border-l-0;
}
.el-tabs__item.is-active {
@apply bg-blue-500 bg-opacity-5;
}
.el-tabs__nav {
@apply border-0;
}
}
}
.aside {
@apply overflow-auto;
}
.el-menu-vertical {
@apply h-[calc(100vh-60px)];
&:not(.el-menu--collapse) {
@apply w-[220px];
}
}
.el-menu--collapse {
@apply w-[54px];
li {
.el-tooltip,
.el-sub-menu__title {
@apply px-4;
}
}
}
}
}
.el-dropdown {
@apply overflow-hidden
}
.gva-table-box {
@apply p-6 bg-white rounded;
}
.gva-btn-list {
@apply mb-3 flex gap-3 items-center;
}
#nprogress .bar {
background: #29d !important;
}
.gva-customer-icon{
@apply w-4 h-4;
}
.el-form--inline {
.el-form-item {
& > .el-input, .el-cascader, .el-select, .el-date-editor, .el-autocomplete {
@apply w-52;
}
}
}

31
src/utils/asyncRouter.js Normal file
View File

@ -0,0 +1,31 @@
const viewModules = import.meta.glob('../view/**/*.vue')
const pluginModules = import.meta.glob('../plugin/**/*.vue')
export const asyncRouterHandle = (asyncRouter) => {
asyncRouter.forEach(item => {
if (item.component && typeof item.component === 'string') {
if (item.component.split('/')[0] === 'view') {
item.component = dynamicImport(viewModules, item.component)
} else if (item.component.split('/')[0] === 'plugin') {
item.component = dynamicImport(pluginModules, item.component)
}
}
if (item.children) {
asyncRouterHandle(item.children)
}
})
}
function dynamicImport(
dynamicViewsModules,
component
) {
const keys = Object.keys(dynamicViewsModules)
const matchKeys = keys.filter((key) => {
const k = key.replace('../', '')
return k === component
})
const matchKey = matchKeys[0]
return dynamicViewsModules[matchKey]
}

6
src/utils/btnAuth.js Normal file
View File

@ -0,0 +1,6 @@
import { useRoute } from 'vue-router'
import { reactive } from 'vue'
export const useBtnAuth = () => {
const route = useRoute()
return route.meta.btns || reactive({})
}

View File

@ -24,12 +24,6 @@ export const formatOnlyDate = (time) => {
return ''
}
}
export const formatPriceType = (priceType) => {
switch (priceType) {
case 1: return '出厂价'
default: '未定义'
}
}
export const filterDict = (value, options) => {
const rowLabel = options && options.filter(item => item.value === value)

View File

@ -6,14 +6,14 @@
<div class="gva-top-card-left-title">早安请开始一天的工作吧</div>
<div class="gva-top-card-left-dot">{{ weatherInfo }}</div>
<div>
<!-- <div class="gva-top-card-left-item">
<div class="gva-top-card-left-item">
插件仓库
<a
style="color:#409EFF"
target="view_window"
href="https://plugin.gin-vue-admin.com/#/layout/home"
>https://plugin.gin-vue-admin.com</a>
</div> -->
</div>
</div>
</div>
<img

View File

@ -3,7 +3,7 @@ import axios from 'axios'
import { ref } from 'vue'
const weatherInfo = ref('今日晴0℃ - 10℃天气寒冷注意添加衣物。')
const amapKey = '1f0c8b27920dc41d204800793d629d8e'
const amapKey = '8e8baa8a7317586c29ec694895de6e0a'
export const useWeatherInfo = () => {
ip()
@ -16,20 +16,16 @@ export const ip = async () => {
return false
}
const res = await axios.get('https://restapi.amap.com/v3/ip?key=' + amapKey)
var cityCode
if (res.data.status !== '0') {
cityCode = res.data.adcode
} else {
cityCode = '100000'
if (res.data.adcode) {
getWeather(res.data.adcode)
}
getWeather(cityCode)
}
const getWeather = async(code) => {
const url = 'https://restapi.amap.com/v3/weather/weatherInfo?key=' + amapKey + '&extensions=base&city=' + code
const response = await axios.get(url)
const response = await axios.get('https://restapi.amap.com/v3/weather/weatherInfo?key=' + amapKey + '&extensions=base&city=' + code)
if (response.data.status === '1') {
const s = response.data.lives[0]
weatherInfo.value = s.city + ' 天气:' + s.weather + ' 温度:' + s.temperature + '摄氏度 风向:' + s.winddirection + ' 风力:' + s.windpower + '级 空气湿度:' + s.humidity
}
}