✨ init Project
This commit is contained in:
67
src/components/arrayCtrl/arrayCtrl.vue
Normal file
67
src/components/arrayCtrl/arrayCtrl.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<el-tag
|
||||
v-for="tag in modelValue"
|
||||
:key="tag"
|
||||
:closable="editable"
|
||||
:disable-transitions="false"
|
||||
@close="handleClose(tag)"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
<template v-if="editable">
|
||||
<el-input
|
||||
v-if="inputVisible"
|
||||
ref="InputRef"
|
||||
v-model="inputValue"
|
||||
class="w-20"
|
||||
size="small"
|
||||
@keyup.enter="handleInputConfirm"
|
||||
@blur="handleInputConfirm"
|
||||
/>
|
||||
<el-button v-else class="button-new-tag" size="small" @click="showInput">
|
||||
+ 新增
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'ArrayCtrl'
|
||||
})
|
||||
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
const inputValue = ref('')
|
||||
const inputVisible = ref(false)
|
||||
const InputRef = ref(null)
|
||||
|
||||
const modelValue = defineModel()
|
||||
|
||||
defineProps({
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
}
|
||||
})
|
||||
|
||||
const handleClose = (tag) => {
|
||||
modelValue.value.splice(modelValue.value.indexOf(tag), 1)
|
||||
}
|
||||
|
||||
const showInput = () => {
|
||||
inputVisible.value = true
|
||||
nextTick(() => {
|
||||
InputRef.value?.input?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const handleInputConfirm = () => {
|
||||
if (inputValue.value) {
|
||||
modelValue.value.push(inputValue.value)
|
||||
}
|
||||
inputVisible.value = false
|
||||
inputValue.value = ''
|
||||
}
|
||||
</script>
|
||||
44
src/components/bottomInfo/bottomInfo.vue
Normal file
44
src/components/bottomInfo/bottomInfo.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<!--
|
||||
此文件受版权保护,未经授权禁止修改!如果您尚未获得授权,请通过微信(shouzi_1994)联系我们以购买授权。在未授权状态下,只需保留此代码,不会影响任何正常使用。
|
||||
未经授权的商用使用可能会被我们的资产搜索引擎爬取,并可能导致后续索赔。索赔金额将不低于高级授权费的十倍。请您遵守版权法律法规,尊重知识产权。
|
||||
-->
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col md:flex-row gap-2 items-center text-sm text-slate-700 dark:text-slate-500 justify-center py-2"
|
||||
>
|
||||
<div class="text-center">
|
||||
<span class="mr-1">Powered by</span>
|
||||
<span>
|
||||
<a
|
||||
class="font-bold text-active"
|
||||
href="https://github.com/flipped-aurora/gin-vue-admin"
|
||||
>Gin-Vue-Admin</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<slot />
|
||||
<div class="text-center">
|
||||
<span class="mr-1">Copyright</span>
|
||||
<span>
|
||||
<a
|
||||
class="font-bold text-active"
|
||||
href="https://github.com/flipped-aurora"
|
||||
>flipped-aurora团队</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'BottomInfo'
|
||||
})
|
||||
|
||||
console.log(
|
||||
`%c powered by %c flipped-aurorae %c`,
|
||||
'background:#0081ff; padding: 1px; border-radius: 3px 0 0 3px; color: #fff',
|
||||
'background:#354855; padding: 1px 5px; border-radius: 0 3px 3px 0; color: #fff; font-weight: bold;',
|
||||
'background:transparent'
|
||||
)
|
||||
</script>
|
||||
54
src/components/charts/index.vue
Normal file
54
src/components/charts/index.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<!--
|
||||
本组件参考 arco-pro 的实现
|
||||
https://github.com/arco-design/arco-design-pro-vue/blob/main/arco-design-pro-vite/src/components/chart/index.vue
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/8
|
||||
!-->
|
||||
|
||||
<template>
|
||||
<VCharts
|
||||
v-if="renderChart"
|
||||
:option="options"
|
||||
:autoresize="autoResize"
|
||||
:style="{ width, height }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, nextTick } from 'vue'
|
||||
import VCharts from 'vue-echarts'
|
||||
import { useWindowResize } from '@/hooks/use-windows-resize'
|
||||
|
||||
defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
autoResize: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
}
|
||||
})
|
||||
const renderChart = ref(false)
|
||||
nextTick(() => {
|
||||
renderChart.value = true
|
||||
})
|
||||
useWindowResize(() => {
|
||||
renderChart.value = false
|
||||
nextTick(() => {
|
||||
renderChart.value = true
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
196
src/components/commandMenu/index.vue
Normal file
196
src/components/commandMenu/index.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
width="30%"
|
||||
class="overlay"
|
||||
:show-close="false"
|
||||
>
|
||||
<template #header>
|
||||
<input
|
||||
v-model="searchInput"
|
||||
class="quick-input"
|
||||
placeholder="请输入你需要快捷到达的功能"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div v-for="(option, index) in options" :key="index">
|
||||
<div v-if="option.children.length" class="quick-title">
|
||||
{{ option.label }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, key) in option.children"
|
||||
:key="index + '-' + key"
|
||||
class="quick-item"
|
||||
@click="item.func"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="close">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
import { useAppStore, useUserStore } from '@/pinia'
|
||||
defineOptions({
|
||||
name: 'CommandMenu'
|
||||
})
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRouter()
|
||||
const routerStore = useRouterStore()
|
||||
const dialogVisible = ref(false)
|
||||
const searchInput = ref('')
|
||||
const options = reactive([])
|
||||
const deepMenus = (menus) => {
|
||||
const arr = []
|
||||
menus?.forEach((menu) => {
|
||||
if (!menu?.children) return
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
arr.push(...deepMenus(menu.children))
|
||||
} else {
|
||||
if (
|
||||
menu.meta.title &&
|
||||
menu.meta.title.indexOf(searchInput.value) > -1
|
||||
) {
|
||||
arr.push({
|
||||
label: menu.meta.title,
|
||||
func: () => changeRouter(menu)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
return arr
|
||||
}
|
||||
|
||||
const addQuickMenu = () => {
|
||||
const option = {
|
||||
label: '跳转',
|
||||
children: []
|
||||
}
|
||||
const menus = deepMenus(routerStore.asyncRouters[0]?.children || [])
|
||||
option.children.push(...menus)
|
||||
options.push(option)
|
||||
}
|
||||
|
||||
const addQuickOption = () => {
|
||||
const option = {
|
||||
label: '操作',
|
||||
children: []
|
||||
}
|
||||
const quickArr = [
|
||||
{
|
||||
label: '亮色主题',
|
||||
func: () => changeMode(false)
|
||||
},
|
||||
{
|
||||
label: '暗色主题',
|
||||
func: () => changeMode(true)
|
||||
},
|
||||
{
|
||||
label: '退出登录',
|
||||
func: () => userStore.LoginOut()
|
||||
}
|
||||
]
|
||||
option.children.push(
|
||||
...quickArr.filter((item) => item.label.indexOf(searchInput.value) > -1)
|
||||
)
|
||||
options.push(option)
|
||||
}
|
||||
|
||||
addQuickMenu()
|
||||
addQuickOption()
|
||||
|
||||
const open = () => {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const changeRouter = (e) => {
|
||||
const index = e.name
|
||||
const query = {}
|
||||
const params = {}
|
||||
routerStore.routeMap[index]?.parameters &&
|
||||
routerStore.routeMap[index]?.parameters.forEach((item) => {
|
||||
if (item.type === 'query') {
|
||||
query[item.key] = item.value
|
||||
} else {
|
||||
params[item.key] = item.value
|
||||
}
|
||||
})
|
||||
if (index === route.name) return
|
||||
if (e.name.indexOf('http://') > -1 || e.name.indexOf('https://') > -1) {
|
||||
window.open(e.name)
|
||||
} else {
|
||||
router.push({ name: index, query, params })
|
||||
}
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
const changeMode = (darkMode) => {
|
||||
appStore.toggleTheme(darkMode)
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
|
||||
watch(searchInput, () => {
|
||||
options.length = 0
|
||||
addQuickMenu()
|
||||
addQuickOption()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.overlay {
|
||||
border-radius: 4px;
|
||||
.el-dialog__header {
|
||||
padding: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.el-dialog__body {
|
||||
padding: 12px !important;
|
||||
height: 50vh;
|
||||
overflow: auto !important;
|
||||
}
|
||||
.quick-title {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
}
|
||||
.quick-input {
|
||||
@apply bg-gray-50 dark:bg-gray-800;
|
||||
color: #666;
|
||||
border-radius: 4px 4px 0 0;
|
||||
border: none;
|
||||
padding: 12px 16px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.quick-item {
|
||||
font-size: 14px;
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
&:hover {
|
||||
@apply bg-gray-200 dark:bg-slate-500;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
90
src/components/customPic/index.vue
Normal file
90
src/components/customPic/index.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<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>
|
||||
75
src/components/exportExcel/exportExcel.vue
Normal file
75
src/components/exportExcel/exportExcel.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<el-button type="primary" icon="download" @click="exportExcelFunc"
|
||||
>导出</el-button
|
||||
>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import { exportExcel } from '@/api/exportTemplate'
|
||||
|
||||
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
|
||||
}
|
||||
let baseUrl = import.meta.env.VITE_BASE_API
|
||||
if (baseUrl === "/"){
|
||||
baseUrl = ""
|
||||
}
|
||||
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 res = await exportExcel({
|
||||
templateID: props.templateId,
|
||||
params
|
||||
})
|
||||
|
||||
if(res.code === 0){
|
||||
ElMessage.success('创建导出任务成功,开始下载')
|
||||
const url = `${baseUrl}${res.data}`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
40
src/components/exportExcel/exportTemplate.vue
Normal file
40
src/components/exportExcel/exportTemplate.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<el-button type="primary" icon="download" @click="exportTemplateFunc"
|
||||
>下载模板</el-button
|
||||
>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {exportTemplate} from "@/api/exportTemplate";
|
||||
|
||||
const props = defineProps({
|
||||
templateId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const exportTemplateFunc = async () => {
|
||||
if (props.templateId === '') {
|
||||
ElMessage.error('组件未设置模板ID')
|
||||
return
|
||||
}
|
||||
let baseUrl = import.meta.env.VITE_BASE_API
|
||||
if (baseUrl === "/"){
|
||||
baseUrl = ""
|
||||
}
|
||||
|
||||
const res = await exportTemplate({
|
||||
templateID: props.templateId
|
||||
})
|
||||
|
||||
if(res.code === 0){
|
||||
ElMessage.success('创建导出任务成功,开始下载')
|
||||
const url = `${baseUrl}${res.data}`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
45
src/components/exportExcel/importExcel.vue
Normal file
45
src/components/exportExcel/importExcel.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<el-upload
|
||||
:action="url"
|
||||
:show-file-list="false"
|
||||
:on-success="handleSuccess"
|
||||
:multiple="false"
|
||||
:headers="{'x-token': token}"
|
||||
>
|
||||
<el-button type="primary" icon="upload" class="ml-3"> 导入 </el-button>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from "@/pinia";
|
||||
|
||||
let baseUrl = import.meta.env.VITE_BASE_API
|
||||
if (baseUrl === "/"){
|
||||
baseUrl = ""
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
templateId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const token = userStore.token
|
||||
|
||||
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>
|
||||
31
src/components/office/docx.vue
Normal file
31
src/components/office/docx.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<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>
|
||||
36
src/components/office/excel.vue
Normal file
36
src/components/office/excel.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<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>
|
||||
49
src/components/office/index.vue
Normal file
49
src/components/office/index.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<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?.toLowerCase()) ? 'image' : fileExt
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
const fullFileURL = computed(() => {
|
||||
return path.value + '/' + fileUrl.value
|
||||
})
|
||||
</script>
|
||||
39
src/components/office/pdf.vue
Normal file
39
src/components/office/pdf.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<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>
|
||||
86
src/components/richtext/rich-edit.vue
Normal file
86
src/components/richtext/rich-edit.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<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 { ElMessage } from 'element-plus'
|
||||
import { getUrl } from '@/utils/image'
|
||||
|
||||
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>
|
||||
58
src/components/richtext/rich-view.vue
Normal file
58
src/components/richtext/rich-view.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<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'
|
||||
|
||||
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>
|
||||
87
src/components/selectFile/selectFile.vue
Normal file
87
src/components/selectFile/selectFile.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
multiple
|
||||
:action="`${getBaseUrl()}/fileUploadAndDownload/upload?noSave=1`"
|
||||
:on-error="uploadError"
|
||||
:on-success="uploadSuccess"
|
||||
:on-remove="uploadRemove"
|
||||
:show-file-list="true"
|
||||
:limit="limit"
|
||||
:accept="accept"
|
||||
class="upload-btn"
|
||||
:headers="{'x-token': token}"
|
||||
>
|
||||
<el-button type="primary"> 上传文件 </el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getBaseUrl } from '@/utils/format'
|
||||
import { useUserStore } from "@/pinia";
|
||||
|
||||
defineOptions({
|
||||
name: 'UploadCommon'
|
||||
})
|
||||
|
||||
defineProps({
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const token = userStore.token
|
||||
|
||||
const fullscreenLoading = ref(false)
|
||||
|
||||
const model = defineModel({ type: Array })
|
||||
|
||||
const fileList = ref(model.value)
|
||||
|
||||
const emits = defineEmits(['on-success', 'on-error'])
|
||||
|
||||
const uploadSuccess = (res) => {
|
||||
const { data, code } = res
|
||||
if (code !== 0) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '上传失败' + res.msg
|
||||
})
|
||||
fileList.value.pop()
|
||||
return
|
||||
}
|
||||
model.value.push({
|
||||
name: data.file.name,
|
||||
url: data.file.url
|
||||
})
|
||||
emits('on-success', res)
|
||||
}
|
||||
|
||||
const uploadRemove = (file) => {
|
||||
const index = model.value.indexOf(file)
|
||||
if (index > -1) {
|
||||
model.value.splice(index, 1)
|
||||
fileList.value = model.value
|
||||
}
|
||||
}
|
||||
|
||||
const uploadError = (err) => {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '上传失败'
|
||||
})
|
||||
fullscreenLoading.value = false
|
||||
emits('on-error', err)
|
||||
}
|
||||
</script>
|
||||
86
src/components/selectImage/selectComponent.vue
Normal file
86
src/components/selectImage/selectComponent.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-40 h-40 relative rounded border border-dashed border-gray-300 cursor-pointer group"
|
||||
:class="rounded ? 'rounded-full' : ''"
|
||||
>
|
||||
<div class="w-full h-full overflow-hidden" :class="rounded ? 'rounded-full' : ''">
|
||||
<el-icon
|
||||
v-if="isVideoExt(model || '')"
|
||||
:size="32"
|
||||
class="absolute top-[calc(50%-16px)] left-[calc(50%-16px)]"
|
||||
>
|
||||
<VideoPlay />
|
||||
</el-icon>
|
||||
<video
|
||||
v-if="isVideoExt(model || '')"
|
||||
class="w-full h-full object-cover"
|
||||
muted
|
||||
preload="metadata"
|
||||
>
|
||||
<source :src="getUrl(model) + '#t=1'" />
|
||||
</video>
|
||||
|
||||
<el-image
|
||||
v-if="model && !isVideoExt(model)"
|
||||
class="w-full h-full"
|
||||
:src="imgUrl"
|
||||
:preview-src-list="srcList"
|
||||
fit="cover"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="text-gray-600 group-hover:bg-gray-200 group-hover:opacity-60 w-full h-full flex justify-center items-center"
|
||||
@click="chooseItem"
|
||||
>
|
||||
<el-icon>
|
||||
<plus />
|
||||
</el-icon>
|
||||
上传
|
||||
</div>
|
||||
</div>
|
||||
<!-- 删除按钮在外层容器中 -->
|
||||
<div
|
||||
v-if="model"
|
||||
class="right-0 top-0 hidden text-gray-400 group-hover:flex justify-center items-center absolute z-10"
|
||||
@click="deleteItem"
|
||||
>
|
||||
<el-icon :size="24">
|
||||
<CircleCloseFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { getUrl, isVideoExt } from '@/utils/image'
|
||||
import { CircleCloseFilled, Plus } from '@element-plus/icons-vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
model: {
|
||||
default: '',
|
||||
type: String
|
||||
},
|
||||
rounded: {
|
||||
default: false,
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['chooseItem', 'deleteItem'])
|
||||
|
||||
const chooseItem = () => {
|
||||
emits('chooseItem')
|
||||
}
|
||||
|
||||
const deleteItem = () => {
|
||||
emits('deleteItem')
|
||||
}
|
||||
|
||||
const imgUrl = computed(() => {
|
||||
return getUrl(props.model)
|
||||
})
|
||||
|
||||
const srcList = computed(() => {
|
||||
return imgUrl.value ? [imgUrl.value] : []
|
||||
})
|
||||
</script>
|
||||
453
src/components/selectImage/selectImage.vue
Normal file
453
src/components/selectImage/selectImage.vue
Normal file
@@ -0,0 +1,453 @@
|
||||
<template>
|
||||
<div>
|
||||
<selectComponent :rounded="rounded" v-if="!props.multiple" :model="model" @chooseItem="openChooseImg" @deleteItem="openChooseImg" />
|
||||
<div v-else class="w-full gap-4 flex flex-wrap">
|
||||
<selectComponent :rounded="rounded" v-for="(item, index) in model" :key="index" :model="item" @chooseItem="openChooseImg"
|
||||
@deleteItem="deleteImg(index)"
|
||||
/>
|
||||
<selectComponent :rounded="rounded" v-if="model?.length < props.maxUpdateCount || props.maxUpdateCount === 0"
|
||||
@chooseItem="openChooseImg" @deleteItem="openChooseImg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="drawer" title="媒体库 | 点击“文件名”可以编辑,选择的类别即是上传的类别" :size="880">
|
||||
<div class="flex">
|
||||
<div class="w-64" style="border-right: solid 1px var(--el-border-color);">
|
||||
<el-scrollbar style="height: calc(100vh - 110px)">
|
||||
<el-tree
|
||||
:data="categories"
|
||||
node-key="id"
|
||||
:props="defaultProps"
|
||||
@node-click="handleNodeClick"
|
||||
default-expand-all
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="w-36" :class="search.classId === data.ID ? 'text-blue-500 font-bold' : ''">{{ data.name }}
|
||||
</div>
|
||||
<el-dropdown>
|
||||
<el-icon class="ml-3 text-right" v-if="data.ID > 0"><MoreFilled /></el-icon>
|
||||
<el-icon class="ml-3 text-right mt-1" v-else><Plus /></el-icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="addCategoryFun(data)">添加分类</el-dropdown-item>
|
||||
<el-dropdown-item @click="editCategory(data)" v-if="data.ID > 0">编辑分类</el-dropdown-item>
|
||||
<el-dropdown-item @click="deleteCategoryFun(data.ID)" v-if="data.ID > 0">删除分类</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="ml-4 w-[605px]">
|
||||
<div class="gva-btn-list gap-2">
|
||||
<el-input v-model.trim="search.keyword" class="w-96" placeholder="请输入文件名或备注" clearable />
|
||||
<el-button type="primary" icon="search" @click="onSubmit"></el-button>
|
||||
</div>
|
||||
<div class="gva-btn-list gap-2">
|
||||
<el-button @click="useSelectedImages" type="danger" :disabled="selectedImages.length === 0" :icon="ArrowLeftBold">选定</el-button>
|
||||
<upload-common :image-common="imageCommon" :classId="search.classId" @on-success="onSuccess" />
|
||||
<cropper-image :classId="search.classId" @on-success="onSuccess" />
|
||||
<QRCodeUpload :classId="search.classId" @on-success="onSuccess" />
|
||||
<upload-image :image-url="imageUrl" :file-size="2048" :max-w-h="1080" :classId="search.classId" @on-success="onSuccess" />
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div v-for="(item,key) in picList" :key="key" class="w-40">
|
||||
<div class="w-40 h-40 border rounded overflow-hidden border-dashed border-gray-300 cursor-pointer relative group">
|
||||
<el-image :key="key" :src="getUrl(item.url)" fit="cover" class="w-full h-full relative" @click="toggleImageSelection(item)" :class="{ selected: isSelected(item) }">
|
||||
<template #error>
|
||||
<el-icon v-if="isVideoExt(item.url || '')" :size="32" class="absolute top-[calc(50%-16px)] left-[calc(50%-16px)]">
|
||||
<VideoPlay />
|
||||
</el-icon>
|
||||
<video v-if="isVideoExt(item.url || '')"
|
||||
class="w-full h-full object-cover"
|
||||
muted
|
||||
preload="metadata"
|
||||
@click="toggleImageSelection(item)"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
>
|
||||
<source :src="getUrl(item.url) + '#t=1'">
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<div v-else class="w-full h-full object-cover flex items-center justify-center">
|
||||
<el-icon :size="32">
|
||||
<icon-picture />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div class="absolute -right-1 top-1 w-8 h-8 group-hover:inline-block hidden" @click="deleteCheck(item)">
|
||||
<el-icon :size="18">
|
||||
<CloseBold />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-hidden text-nowrap overflow-ellipsis text-center w-full cursor-pointer" @click="editFileNameFunc(item)">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
class="justify-center"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
|
||||
<!-- 添加分类弹窗 -->
|
||||
<el-dialog v-model="categoryDialogVisible" @close="closeAddCategoryDialog" width="520"
|
||||
:title="(categoryFormData.ID === 0 ? '添加' : '编辑') + '分类'"
|
||||
draggable
|
||||
>
|
||||
<el-form ref="categoryForm" :rules="rules" :model="categoryFormData" label-width="80px">
|
||||
<el-form-item label="上级分类">
|
||||
<el-tree-select
|
||||
v-model="categoryFormData.pid"
|
||||
:data="categories"
|
||||
check-strictly
|
||||
:props="defaultProps"
|
||||
:render-after-expand="false"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类名称" prop="name">
|
||||
<el-input v-model.trim="categoryFormData.name" placeholder="分类名称"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="closeAddCategoryDialog">取消</el-button>
|
||||
<el-button type="primary" @click="confirmAddCategory">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getUrl, isVideoExt } from '@/utils/image'
|
||||
import { ref } from 'vue'
|
||||
import { getFileList, editFileName, deleteFile } 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 {
|
||||
ArrowLeftBold,
|
||||
CloseBold,
|
||||
MoreFilled,
|
||||
Picture as IconPicture,
|
||||
Plus,
|
||||
VideoPlay
|
||||
} from '@element-plus/icons-vue'
|
||||
import selectComponent from '@/components/selectImage/selectComponent.vue'
|
||||
import { addCategory, deleteCategory, getCategoryList } from '@/api/attachmentCategory'
|
||||
import CropperImage from "@/components/upload/cropper.vue";
|
||||
import QRCodeUpload from "@/components/upload/QR-code.vue";
|
||||
|
||||
const imageUrl = ref('')
|
||||
const imageCommon = ref('')
|
||||
|
||||
const search = ref({
|
||||
keyword: null,
|
||||
classId: 0
|
||||
})
|
||||
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
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const deleteImg = (index) => {
|
||||
model.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getImageList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getImageList()
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
search.value.classId = 0
|
||||
page.value = 1
|
||||
getImageList()
|
||||
}
|
||||
|
||||
const editFileNameFunc = async(row) => {
|
||||
ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /\S/,
|
||||
inputErrorMessage: '不能为空',
|
||||
inputValue: row.name
|
||||
}).then(async({ value }) => {
|
||||
row.name = value
|
||||
const res = await editFileName(row)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '编辑成功!'
|
||||
})
|
||||
await getImageList()
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '取消修改'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const drawer = ref(false)
|
||||
const picList = ref([])
|
||||
|
||||
const imageTypeList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg']
|
||||
const videoTypeList = ['mp4', 'avi', 'rmvb', 'rm', 'asf', 'divx', 'mpg', 'mpeg', 'mpe', 'wmv', 'mkv', 'vob']
|
||||
|
||||
const listObj = {
|
||||
image: imageTypeList,
|
||||
video: videoTypeList
|
||||
}
|
||||
|
||||
const chooseImg = (url) => {
|
||||
if (props.fileType) {
|
||||
const typeSuccess = listObj[props.fileType].some(item => {
|
||||
if (url?.toLowerCase().includes(item)) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
if (!typeSuccess) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '当前类型不支持使用'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
//if (props.multiple) {
|
||||
// model.value.push(url)
|
||||
//} else {
|
||||
model.value = url
|
||||
//}
|
||||
drawer.value = false
|
||||
}
|
||||
|
||||
const openChooseImg = async() => {
|
||||
if (model.value && !props.multiple) {
|
||||
model.value = ''
|
||||
return
|
||||
}
|
||||
await getImageList()
|
||||
await fetchCategories()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
const deleteCheck = (item) => {
|
||||
ElMessageBox.confirm('是否删除该文件', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async() => {
|
||||
const res = await deleteFile(item)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功!'
|
||||
})
|
||||
await getImageList()
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '已取消删除'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: 'name',
|
||||
value: 'ID'
|
||||
}
|
||||
|
||||
const categories = ref([])
|
||||
const fetchCategories = async() => {
|
||||
const res = await getCategoryList()
|
||||
let data = {
|
||||
name: '全部分类',
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
children:[]
|
||||
}
|
||||
if (res.code === 0) {
|
||||
categories.value = res.data || []
|
||||
categories.value.unshift(data)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNodeClick = (node) => {
|
||||
search.value.keyword = null
|
||||
search.value.classId = node.ID
|
||||
page.value = 1
|
||||
getImageList()
|
||||
}
|
||||
|
||||
const onSuccess = () => {
|
||||
search.value.keyword = null
|
||||
page.value = 1
|
||||
getImageList()
|
||||
}
|
||||
|
||||
const categoryDialogVisible = ref(false)
|
||||
const categoryFormData = ref({
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
name: ''
|
||||
})
|
||||
|
||||
const categoryForm = ref(null)
|
||||
const rules = ref({
|
||||
name: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ max: 20, message: '最多20位字符', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const addCategoryFun = (category) => {
|
||||
categoryDialogVisible.value = true
|
||||
categoryFormData.value.ID = 0
|
||||
categoryFormData.value.pid = category.ID
|
||||
}
|
||||
|
||||
const editCategory = (category) => {
|
||||
categoryFormData.value = {
|
||||
ID: category.ID,
|
||||
pid: category.pid,
|
||||
name: category.name
|
||||
}
|
||||
categoryDialogVisible.value = true
|
||||
}
|
||||
|
||||
const deleteCategoryFun = async(id) => {
|
||||
const res = await deleteCategory({ id: id })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success({ type: 'success', message: '删除成功' })
|
||||
await fetchCategories()
|
||||
}
|
||||
}
|
||||
|
||||
const confirmAddCategory = async() => {
|
||||
categoryForm.value.validate(async valid => {
|
||||
if (valid) {
|
||||
const res = await addCategory(categoryFormData.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '操作成功' })
|
||||
await fetchCategories()
|
||||
closeAddCategoryDialog()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const closeAddCategoryDialog = () => {
|
||||
categoryDialogVisible.value = false
|
||||
categoryFormData.value = {
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
name: ''
|
||||
}
|
||||
}
|
||||
|
||||
const selectedImages = ref([])
|
||||
|
||||
const toggleImageSelection = (item) => {
|
||||
if (props.multiple === false) {
|
||||
chooseImg(item.url)
|
||||
return
|
||||
}
|
||||
const index = selectedImages.value.findIndex(img => img.ID === item.ID)
|
||||
if (index > -1) {
|
||||
selectedImages.value.splice(index, 1)
|
||||
} else {
|
||||
selectedImages.value.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
const isSelected = (item) => {
|
||||
return selectedImages.value.some(img => img.ID === item.ID)
|
||||
}
|
||||
|
||||
const useSelectedImages = () => {
|
||||
selectedImages.value.forEach((item) => {
|
||||
model.value.push(item.url)
|
||||
})
|
||||
drawer.value = false
|
||||
selectedImages.value = []
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.selected {
|
||||
border: 3px solid #409eff;
|
||||
}
|
||||
|
||||
.selected:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border: 10px solid #409eff;
|
||||
}
|
||||
|
||||
.selected:after {
|
||||
content: "";
|
||||
width: 9px;
|
||||
height: 14px;
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 0;
|
||||
border: 3px solid #fff;
|
||||
border-top-color: transparent;
|
||||
border-left-color: transparent;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
</style>
|
||||
32
src/components/svgIcon/svgIcon.vue
Normal file
32
src/components/svgIcon/svgIcon.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<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>
|
||||
65
src/components/upload/QR-code.vue
Normal file
65
src/components/upload/QR-code.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button type="primary" icon="iphone" @click="createQrCode"> 扫码上传</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="扫码上传" width="320px" :show-close="false" append-to-body :close-on-click-modal="false"
|
||||
draggable
|
||||
>
|
||||
<div class="m-2">
|
||||
<vue-qr :logoSrc="logoSrc"
|
||||
:size="291"
|
||||
:margin="0"
|
||||
:autoColor="true"
|
||||
:dotScale="1"
|
||||
:text="codeUrl"
|
||||
colorDark="green"
|
||||
colorLight="white"
|
||||
ref="qrcode"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="onFinished">完成上传</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import logoSrc from '@/assets/logo.png'
|
||||
import vueQr from 'vue-qr/src/packages/vue-qr.vue'
|
||||
import { ref } from 'vue'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
|
||||
defineOptions({
|
||||
name: 'QRCodeUpload'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['on-success'])
|
||||
|
||||
const props = defineProps({
|
||||
classId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const userStore = useUserStore()
|
||||
const codeUrl = ref('')
|
||||
|
||||
const createQrCode = () => {
|
||||
const local = window.location
|
||||
codeUrl.value = local.protocol + '//' + local.host + '/#/scanUpload?id=' + props.classId + '&token=' + userStore.token + '&t=' + Date.now()
|
||||
dialogVisible.value = true
|
||||
console.log(codeUrl.value)
|
||||
}
|
||||
|
||||
const onFinished = () => {
|
||||
dialogVisible.value = false
|
||||
codeUrl.value = ''
|
||||
emit('on-success', '')
|
||||
}
|
||||
</script>
|
||||
90
src/components/upload/common.vue
Normal file
90
src/components/upload/common.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-upload
|
||||
:action="`${getBaseUrl()}/fileUploadAndDownload/upload`"
|
||||
:before-upload="checkFile"
|
||||
:on-error="uploadError"
|
||||
:on-success="uploadSuccess"
|
||||
:show-file-list="false"
|
||||
:data="{'classId': props.classId}"
|
||||
:headers="{'x-token': token}"
|
||||
multiple
|
||||
class="upload-btn"
|
||||
>
|
||||
<el-button type="primary" :icon="Upload">普通上传</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { isVideoMime, isImageMime } from '@/utils/image'
|
||||
import { getBaseUrl } from '@/utils/format'
|
||||
import { Upload } from "@element-plus/icons-vue";
|
||||
import { useUserStore } from "@/pinia";
|
||||
|
||||
defineOptions({
|
||||
name: 'UploadCommon'
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const token = userStore.token
|
||||
|
||||
const props = defineProps({
|
||||
classId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['on-success'])
|
||||
|
||||
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>
|
||||
237
src/components/upload/cropper.vue
Normal file
237
src/components/upload/cropper.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:action="`${getBaseUrl()}/fileUploadAndDownload/upload`"
|
||||
accept="image/*"
|
||||
:show-file-list="false"
|
||||
:auto-upload="false"
|
||||
:data="{'classId': props.classId}"
|
||||
:on-success="handleImageSuccess"
|
||||
:on-change="handleFileChange"
|
||||
:headers="{'x-token': token}"
|
||||
>
|
||||
<el-button type="primary" icon="crop"> 裁剪上传</el-button>
|
||||
</el-upload>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="图片裁剪" width="1200px" append-to-body @close="dialogVisible = false" :close-on-click-modal="false" draggable>
|
||||
<div class="flex gap-[30px] h-[600px]">
|
||||
<!-- 左侧编辑区 -->
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex-1 bg-[#f8f8f8] rounded-lg overflow-hidden">
|
||||
<VueCropper
|
||||
ref="cropperRef"
|
||||
:img="imgSrc"
|
||||
outputType="jpeg"
|
||||
:autoCrop="true"
|
||||
:autoCropWidth="cropWidth"
|
||||
:autoCropHeight="cropHeight"
|
||||
:fixedBox="false"
|
||||
:fixed="fixedRatio"
|
||||
:fixedNumber="fixedNumber"
|
||||
:centerBox="true"
|
||||
:canMoveBox="true"
|
||||
:full="false"
|
||||
:maxImgSize="1200"
|
||||
:original="true"
|
||||
@realTime="handleRealTime"
|
||||
></VueCropper>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="mt-[20px] flex items-center p-[10px] bg-white rounded-lg shadow-[0_2px_12px_rgba(0,0,0,0.1)]">
|
||||
<el-button-group>
|
||||
<el-tooltip content="向左旋转">
|
||||
<el-button @click="rotate(-90)" :icon="RefreshLeft" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="向右旋转">
|
||||
<el-button @click="rotate(90)" :icon="RefreshRight" />
|
||||
</el-tooltip>
|
||||
<el-button :icon="Plus" @click="changeScale(1)"></el-button>
|
||||
<el-button :icon="Minus" @click="changeScale(-1)"></el-button>
|
||||
</el-button-group>
|
||||
|
||||
|
||||
<el-select v-model="currentRatio" placeholder="选择比例" class="w-32 ml-4" @change="onCurrentRatio">
|
||||
<el-option v-for="(item, index) in ratioOptions" :key="index" :label="item.label" :value="index" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧预览区 -->
|
||||
<div class="w-[340px]">
|
||||
<div class="bg-white p-5 rounded-lg shadow-[0_2px_12px_rgba(0,0,0,0.1)]">
|
||||
<div class="mb-[15px] text-gray-600">裁剪预览</div>
|
||||
<div class="bg-white p-5 rounded-lg shadow-[0_2px_12px_rgba(0,0,0,0.1)]"
|
||||
:style="{'width': previews.w + 'px', 'height': previews.h + 'px'}"
|
||||
>
|
||||
<div class="w-full h-full relative overflow-hidden">
|
||||
<img :src="previews.url" :style="previews.img" alt="" class="max-w-none absolute transition-all duration-300 ease-in-out image-render-pixelated origin-[0_0]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="handleUpload" :loading="uploading"> {{ uploading ? '上传中...' : '上 传' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, getCurrentInstance } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { RefreshLeft, RefreshRight, Plus, Minus } from '@element-plus/icons-vue'
|
||||
import 'vue-cropper/dist/index.css'
|
||||
import { VueCropper } from 'vue-cropper'
|
||||
import { getBaseUrl } from '@/utils/format'
|
||||
import { useUserStore } from "@/pinia";
|
||||
|
||||
defineOptions({
|
||||
name: 'CropperImage'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['on-success'])
|
||||
|
||||
const props = defineProps({
|
||||
classId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const uploadRef = ref(null)
|
||||
// 响应式数据
|
||||
const dialogVisible = ref(false)
|
||||
const imgSrc = ref('')
|
||||
const cropperRef = ref(null)
|
||||
const { proxy } = getCurrentInstance()
|
||||
const previews = ref({})
|
||||
const uploading = ref(false)
|
||||
|
||||
// 缩放控制
|
||||
const changeScale = (value) => {
|
||||
proxy.$refs.cropperRef.changeScale(value)
|
||||
}
|
||||
|
||||
// 比例预设
|
||||
const ratioOptions = ref([
|
||||
{ label: '1:1', value: [1, 1] },
|
||||
{ label: '16:9', value: [16, 9] },
|
||||
{ label: '9:16', value: [9, 16] },
|
||||
{ label: '4:3', value: [4, 3] },
|
||||
{ label: '自由比例', value: [] }
|
||||
])
|
||||
|
||||
const fixedNumber = ref([1, 1])
|
||||
const cropWidth = ref(300)
|
||||
const cropHeight = ref(300)
|
||||
|
||||
const fixedRatio = ref(false)
|
||||
const currentRatio = ref(4)
|
||||
const onCurrentRatio = () => {
|
||||
fixedNumber.value = ratioOptions.value[currentRatio.value].value
|
||||
switch (currentRatio.value) {
|
||||
case 0:
|
||||
cropWidth.value = 300
|
||||
cropHeight.value = 300
|
||||
fixedRatio.value = true
|
||||
break
|
||||
case 1:
|
||||
cropWidth.value = 300
|
||||
cropHeight.value = 300 * 9 / 16
|
||||
fixedRatio.value = true
|
||||
break
|
||||
case 2:
|
||||
cropWidth.value = 300 * 9 / 16
|
||||
cropHeight.value = 300
|
||||
fixedRatio.value = true
|
||||
break
|
||||
case 3:
|
||||
cropWidth.value = 300
|
||||
cropHeight.value = 300 * 3 / 4
|
||||
fixedRatio.value = true
|
||||
break
|
||||
default:
|
||||
cropWidth.value = 300
|
||||
cropHeight.value = 300
|
||||
fixedRatio.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 文件处理
|
||||
const handleFileChange = (file) => {
|
||||
const isImage = file.raw.type.includes('image')
|
||||
if (!isImage) {
|
||||
ElMessage.error('请选择图片文件')
|
||||
return
|
||||
}
|
||||
|
||||
if (file.raw.size / 1024 / 1024 > 8) {
|
||||
ElMessage.error('文件大小不能超过8MB!')
|
||||
return false
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
imgSrc.value = e.target.result
|
||||
dialogVisible.value = true
|
||||
}
|
||||
reader.readAsDataURL(file.raw)
|
||||
}
|
||||
|
||||
// 旋转控制
|
||||
const rotate = (degree) => {
|
||||
if (degree === -90) {
|
||||
proxy.$refs.cropperRef.rotateLeft()
|
||||
} else {
|
||||
proxy.$refs.cropperRef.rotateRight()
|
||||
}
|
||||
}
|
||||
|
||||
// 实时预览
|
||||
const handleRealTime = (data) => {
|
||||
previews.value = data
|
||||
//console.log(data)
|
||||
}
|
||||
|
||||
// 上传处理
|
||||
const handleUpload = () => {
|
||||
uploading.value = true
|
||||
proxy.$refs.cropperRef.getCropBlob((blob) => {
|
||||
try {
|
||||
const file = new File([blob], `${Date.now()}.jpg`, { type: 'image/jpeg' })
|
||||
uploadRef.value.clearFiles()
|
||||
uploadRef.value.handleStart(file)
|
||||
uploadRef.value.submit()
|
||||
|
||||
} catch (error) {
|
||||
uploading.value = false
|
||||
ElMessage.error('上传失败: ' + error.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleImageSuccess = (res) => {
|
||||
const { data } = res
|
||||
if (data) {
|
||||
setTimeout(() => {
|
||||
uploading.value = false
|
||||
dialogVisible.value = false
|
||||
previews.value = {}
|
||||
ElMessage.success('上传成功')
|
||||
emit('on-success', data.url)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.vue-cropper) {
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
102
src/components/upload/image.vue
Normal file
102
src/components/upload/image.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-upload
|
||||
:action="`${getBaseUrl()}/fileUploadAndDownload/upload`"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
:before-upload="beforeImageUpload"
|
||||
:multiple="false"
|
||||
:data="{'classId': props.classId}"
|
||||
:headers="{'x-token': token}"
|
||||
>
|
||||
<el-button type="primary" :icon="Upload">压缩上传</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ImageCompress from '@/utils/image'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getBaseUrl } from '@/utils/format'
|
||||
import { Upload } from "@element-plus/icons-vue";
|
||||
import { useUserStore } from "@/pinia";
|
||||
|
||||
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 // 图片长宽上限
|
||||
},
|
||||
classId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const token = userStore.token
|
||||
|
||||
const beforeImageUpload = (file) => {
|
||||
const isJPG = file.type?.toLowerCase() === 'image/jpeg'
|
||||
const isPng = file.type?.toLowerCase() === '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>
|
||||
33
src/components/warningBar/warningBar.vue
Normal file
33
src/components/warningBar/warningBar.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div
|
||||
class="px-1.5 py-2 flex items-center rounded-sm mt-2 bg-amber-50 gap-2 mb-3 text-amber-500 dark:bg-amber-700 dark:text-gray-200"
|
||||
: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>
|
||||
Reference in New Issue
Block a user