大屏与公共页面

This commit is contained in:
chenlinlin 2025-02-21 11:27:20 +08:00
parent 53bd29677b
commit 4af02c6599
7 changed files with 8817 additions and 0 deletions

87
client/src/views/App.vue Normal file
View File

@ -0,0 +1,87 @@
<template>
<!-- 全局进度条组件 -->
<ProgressBar />
<router-view v-if="isReady"></router-view>
<div v-else class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">路由加载中...</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import { useUserStore } from '@/stores/modules/user'
import { useRouteStore } from '@/stores/modules/router'
import { addDynamicRoutes } from '@/router'
import { useRouter, useRoute } from 'vue-router'
import ProgressBar from '@/components/ProgressBar.vue'
const isReady = ref(false)
const userStore = useUserStore()
const routerStore = useRouteStore()
const router = useRouter()
onMounted(async () => {
console.log('开始初始化应用...')
//
await userStore.init()
//
// if (routerStore.defaultRoutes.length === 0) {
// routerStore.initDefaultRoutes()
// //
// addDynamicRoutes()
// }
//
await new Promise((resolve) => setTimeout(resolve, 500))
//
console.log(
'当前路由表:',
router.getRoutes().map((r) => r.path)
)
//
await nextTick()
isReady.value = true
console.log('应用初始化完成,准备渲染路由视图')
//
router.push(router)
})
</script>
<style lang="scss">
.loading-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: #409eff;
animation: spin 1s ease-in-out infinite;
}
.loading-text {
margin-top: 16px;
font-size: 16px;
color: #606266;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,180 @@
<template>
<el-card :class="mergeParameters.cardClass">
<div class="clearfix" style="display: flex; align-items: center; justify-content: space-between;">
<span><span style="color: red;" v-if="mergeParameters.required">*</span>{{ mergeParameters.title }}</span>
<el-upload
ref="upload"
:show-file-list="false"
:action="mergeParameters.url"
:multiple="mergeParameters.multiple"
:auto-upload="mergeParameters.autoUpload"
:on-error="mergeFileActions.error"
:on-change="mergeFileActions.change"
:on-success="mergeFileActions.success"
:headers="mergeParameters.headers">
<el-button v-if="mergeParameters.mode !== 'detail' && mergeParameters.isShowBtn" slot="trigger" type="primary">
<template #icon>
<font-awesome-icon icon="upload"/>
</template>
选择文件
</el-button>
</el-upload>
</div>
<div>
<el-table :data="tableData" border style="width: 100%; padding-top: 5px;">
<el-table-column fixed prop="name" :label="mergeParameters.tableTitle + '名称'"/>
<el-table-column v-if="mergeParameters.isShowType" fixed prop="type"
:label="mergeParameters.tableTitle + '文件类型'" width="200" align="center"></el-table-column>
<el-table-column v-if="mergeParameters.autoUpload" fixed prop="uploadStatus" width="100" label="上传状态"></el-table-column>
<el-table-column fixed="right" label="操作" width="120" align="center" class-name="control-column">
<template #default="scope">
<el-space>
<el-link type="primary" v-if="isDowload(scope.row)" size="small" @click="downloadFile(scope.row)">
{{ isPicture(scope.row.name) ? '查看' : '下载' }}
</el-link>
<el-link type="primary" size="small" v-if="mergeParameters.mode !== 'detail'"
@click="deleteRow(scope.$index, tableData,scope.row)">
删除
</el-link>
</el-space>
</template>
</el-table-column>
</el-table>
</div>
<PictureView ref="pictureViewRef"/>
</el-card>
</template>
<script setup>
import {computed, defineEmits, defineProps, reactive, ref} from "vue"
import {merge} from "lodash"
import {ElMessage} from "element-plus"
import PictureView from "@pages/common/PictureView.vue";
const pictureViewRef = ref(null)
const props = defineProps({
parameters: {
type: Object,
default: () => ({})
},
fileActions: {
type: Object,
},
filesData: {
type: Array,
default: () => []
}
})
const onError = (err, file, fileList) => { //
ElMessage.error('上传失败!' + err)
}
const deleteRow = (index, fileList ,row) => { //
const source = Object.assign([], fileList)
let id = fileList[index].id// ID
fileList.splice(index, 1)
emit('on-remove', fileList, id, {index: index, source: source},row)
}
const viewPicture = (scope) => { //
}
const isPicture = (fileName) => { //
return /\.(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(fileName)
}
const isDowload = (file) => { //
return mergeParameters.mode !== 'add' && file.url
}
const downloadFile = (row) => { //
if (row && row.downloadUrl) {
if (/(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(row.type) || /\.(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(row.name)) {
pictureViewRef.value.viewPicture(row.downloadUrl)
} else {
window.open(row.downloadUrl)
}
}
}
const onChange = (file, fileList) => { //
emit('on-change', file)
}
const onSuccess = (response, file, fileList) => { //
let data = response.data
data.bz = mergeParameters.autoUpload ? mergeParameters.title : fileMap[file.uid] ? fileMap[file.uid] : '' //
data.fileType = mergeParameters.fileType
if (response.result === 'success') {
data.uploadStatus = '上传成功'
if (mergeParameters.autoUpload) { //
emit('on-success', data, file)
} else { //
data.uid = file.uid
emit('on-manual-success', data)
}
} else {
ElMessage.error('上传失败!:'+response.msg)
}
}
const defaultParameters = {
headers: {},
url: null,
title: '文件上传',
tableTitle: '材料',
autoUpload: false,
required: true,
multiple: true,
mode: 'add',
isShowBtn: true,
isShowType: true,
fileType: '',
cardClass: 'box-card'
}
const defaultFileActions = {
error: onError,
change: onChange,
success: onSuccess
}
const fileMap = reactive(merge({}))
const mergeParameters = reactive(merge({}, defaultParameters, props.parameters))
const mergeFileActions = reactive(merge({}, defaultFileActions, props.fileActions))
const emit = defineEmits(['on-remove', 'on-success', 'on-manual-success', 'update:actions'])
const mergeActions = reactive(merge({}, defaultFileActions, props.actions))
emit('update:actions', mergeActions)
const tableData = computed(() => {
let arr = []
for (let data of props.filesData) {
arr.push(data)
}
return arr
})
</script>
<style scoped lang="scss">
.el-card {
width: 100%;
}
.linkFix {
.el-link {
margin-right: 10px !important;
}
}
:deep(.el-table) {
padding-top: 0px !important;
}
:deep(.el-card__body) {
padding: 10px 20px;
}
</style>

View File

@ -0,0 +1,177 @@
<template>
<div class="pic-div">
<el-upload v-if="mergeParameters.mode !== 'detail'"
:action="mergeParameters.url"
list-type="picture-card"
:multiple="mergeParameters.multiple"
:headers="mergeParameters.headers"
:auto-upload="mergeParameters.autoUpload"
:limit="mergeParameters.limit"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
:on-change="handleChange"
:on-success="onSuccess"
:on-error="onError"
:on-exceed="handleExceed"
:file-list="pictureList"
accept="image/gif,image/jpg,image/jpeg,image/png,image/GIF,image/JPG">
<el-icon>
<Plus/>
</el-icon>
</el-upload>
<el-upload v-if="mergeParameters.mode === 'detail'"
class="noUpOnly"
action="#"
list-type="picture-card"
:auto-upload="mergeParameters.autoUpload"
:on-preview="handlePictureCardPreview"
:file-list="pictureList">
<template #file="{ file }">
<img style="width: 148px; height: 148px;" class="el-upload-list__item-thumbnail" :src="file.url">
<div v-if="file.name">
<el-tooltip effect="dark" :content="file.name">
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
<el-icon><zoom-in/></el-icon>
</span>
<span class="el-upload-list__item-delete" @click="handleDownload(file)">
<el-icon><Download/></el-icon>
</span>
</span>
</el-tooltip>
</div>
<div v-else>
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
<el-icon><zoom-in/></el-icon>
</span>
<span class="el-upload-list__item-delete" @click="handleDownload(file)">
<el-icon><Download/></el-icon>
</span>
</span>
</div>
</template>
</el-upload>
<el-dialog :visible.sync="dialogVisible" append-to-body>
<img width="100%" :src="dialogImageUrl" alt="">
</el-dialog>
<PictureView ref="pictureViewRef"/>
</div>
</template>
<script setup>
import {ElMessage} from "element-plus"
import {defineEmits, defineProps, reactive, toRefs, ref, watch, toRaw} from "vue"
import {merge} from "lodash"
import PictureView from "@pages/common/PictureView.vue"
import {Delete, Download, Plus, ZoomIn} from '@element-plus/icons-vue'
import {useUserStore} from "@/stores/modules/user.js"
const userStore = useUserStore()
const props = defineProps({
parameters: {
type: Object,
default: () => ({})
},
formData: {
type: Object,
default: () => ({})
}
})
const state = reactive({
dialogImageUrl: '',
dialogVisible: false,
pictureList: [],
disabled: false,
mpNum: 0,
modifyFileList: []
})
const pictureViewRef = ref(null)
const {
dialogImageUrl,
dialogVisible,
pictureList,
disabled
} = toRefs(state)
const defaultParameters = {
mode: 'add',
isAddCallback: true,
isModifyCallback: true,
multiple: true,
autoUpload: false,
limit: 5,
limitSize: 2,
industryCategory: 'PUB',
displayMsg: false,
specialTreatment: false,
backPic: false,
headers: {'industry-category': 'PUB', 'Authorization': `Bearer ${userStore.token}`},
url: '/api/uploads'
}
const mergeParameters = reactive(merge({}, defaultParameters, props.parameters))
const emit = defineEmits(['on-remove', 'on-success', 'del-picture-id', 'on-change', 'on-pic-view', 'on-remove'])
function onSuccess(res, file, fileList) {
if (mergeParameters.autoUpload) {
emit('on-success', res.data, file, props.formData)
} else
emit('on-success', fileList)
}
function onError(res) {
ElMessage.error('上传失败:', res)
}
function handleChange(file, fileList) {
file.status = 'success'
emit('on-change', file, fileList, props.formData)
if(!mergeParameters.autoUpload)
pictureList.value = fileList
}
function handleRemove(file, fileList) { // file.id
pictureList.value = fileList
if (file.id) {
emit('del-picture', file, props.formData)
}
emit('on-remove', file, props.formData)
}
function handlePictureCardPreview(file) {
if (mergeParameters.backPic)
emit('on-pic-view', file.url, pictureList.value.map(o => o.url))
else
pictureViewRef.value.viewPicture(file.url, pictureList.value.map(o => o.url))
}
function handleDownload(file) { //
if (file && file.url) {
window.open(file.url)
}
}
function handleExceed(files, fileList) {
ElMessage.warning(`当前限制选择 ${mergeParameters.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`)
}
pictureList.value = props.formData?.urls || []
</script>
<style>
.pic-div {
.el-upload-list__item-thumbnail, .el-upload-list__item-actions, .el-upload-list__item, .el-upload--picture-card {
object-fit: initial;
}
}
.noUpOnly div.el-upload--picture-card {
display: none;
}
</style>

View File

@ -0,0 +1,39 @@
<template>
<div class="pic_image__preview">
<el-image ref="imageRef"
style="width: 0px; height: 0px"
:src="src"
:preview-src-list="srcList">
</el-image>
</div>
</template>
<script setup>
import {reactive, ref, toRefs} from "vue"
import defPic from '@/assets/image/pic.gif'
const state = reactive({
src: defPic,
srcList: [defPic]
})
const {src, srcList} = toRefs(state)
const imageRef = ref(null)
function viewPicture(url, urls) {
if (urls && urls.length > 1) srcList.value = urls
else if (url) srcList.value = [url]
imageRef.value.$el.querySelector('img').click()
}
defineExpose({
viewPicture
})
</script>
<style>
.el-icon-circle-close {
color: red !important;
}
</style>

View File

@ -0,0 +1,135 @@
<template>
<el-dialog :open="modelValue" title="在线签名" :show-footer="false" type="medium" @update:open="handleClose">
<div class="signature-container">
<canvas
ref="canvasRef"
class="signature-canvas"
width="800"
height="300"
@mousedown="startDrawing"
@mousemove="draw"
@mouseup="stopDrawing"
@mouseleave="stopDrawing"/>
<div class="signature-actions">
<el-button type="danger" @click="clearCanvas">清除</el-button>
<el-button type="primary" @click="saveSignature">保存签名</el-button>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import ElDialog from "@/components/ElDialog/index.vue"
import { ref, onMounted, watch } from 'vue'
const props = defineProps<{
modelValue: boolean
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'save', dataUrl: string): void
}>()
const canvasRef = ref<HTMLCanvasElement | null>(null)
let isDrawing = false
let lastX = 0
let lastY = 0
let ctx: CanvasRenderingContext2D | null = null
//
const initCanvas = () => {
if (canvasRef.value) {
const canvas = canvasRef.value
ctx = canvas.getContext('2d')
if (ctx) {
ctx.strokeStyle = '#000'
ctx.lineWidth = 4
ctx.lineJoin = 'round'
ctx.lineCap = 'round'
clearCanvas()
}
}
}
//
const startDrawing = (e: MouseEvent) => {
if (!ctx) return
isDrawing = true
;[lastX, lastY] = [e.offsetX, e.offsetY]
}
//
const draw = (e: MouseEvent) => {
if (!isDrawing || !ctx) return
ctx.beginPath()
ctx.moveTo(lastX, lastY)
ctx.lineTo(e.offsetX, e.offsetY)
ctx.stroke()
;[lastX, lastY] = [e.offsetX, e.offsetY]
}
//
const stopDrawing = () => {
isDrawing = false
}
//
const clearCanvas = () => {
if (canvasRef.value && ctx) {
ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, canvasRef.value.width, canvasRef.value.height)
}
}
//
const saveSignature = () => {
if (canvasRef.value) {
const dataUrl = canvasRef.value.toDataURL('image/png')
emit('save', dataUrl)
handleClose()
}
}
//
const handleClose = () => {
emit('update:modelValue', false)
}
//
watch(
() => props.modelValue,
(val) => {
if (val) {
setTimeout(() => {
initCanvas()
}, 100)
}
}
)
onMounted(() => {
initCanvas()
})
</script>
<style scoped lang="scss">
.signature-container {
display: flex;
flex-direction: column;
align-items: center;
}
.signature-canvas {
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: crosshair;
}
.signature-actions {
margin-top: 20px;
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>