This commit is contained in:
parent
3ff4fa97a6
commit
89e792dcca
22
package.json
22
package.json
|
|
@ -10,27 +10,31 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"axios": "^1.9.0",
|
"axios": "1.12.0",
|
||||||
"cropperjs": "1.5.13",
|
"cropperjs": "^1.6.2",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"element-plus": "^2.9.10",
|
"echarts": "^6.0.0",
|
||||||
|
"element-plus": "2.11.3",
|
||||||
|
"image-conversion": "^2.1.1",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.3.0",
|
"pinia-plugin-persistedstate": "^4.3.0",
|
||||||
"sortablejs": "^1.15.6",
|
"sortablejs": "^1.15.6",
|
||||||
"vue": "^3.5.14",
|
"vue": "^3.5.22",
|
||||||
"vue-i18n": "^11.1.5",
|
"vue-i18n": "^11.1.12",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vuedraggable": "^2.24.3"
|
"vuedraggable": "^4.1.0",
|
||||||
|
"xgplayer": "^3.0.22",
|
||||||
|
"xgplayer-hls": "^3.0.22"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.15.21",
|
"@types/node": "^24.9.1",
|
||||||
"@vitejs/plugin-vue": "^5.2.4",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"eslint": "^9.27.0",
|
"eslint": "^9.27.0",
|
||||||
"sass": "^1.89.0",
|
"sass": "^1.89.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.5"
|
"vite": "7.1.5"
|
||||||
},
|
},
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,4 +89,17 @@ export default {
|
||||||
upload: async function (data, config = {}) {
|
upload: async function (data, config = {}) {
|
||||||
return await http.post("upload", data, config);
|
return await http.post("upload", data, config);
|
||||||
},
|
},
|
||||||
|
monitor: {
|
||||||
|
server: async function (data, config = {}) {
|
||||||
|
return await http.get("monitor/server", data, config);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
online: {
|
||||||
|
list: async function (data, config = {}) {
|
||||||
|
return await http.get("online/list", data, config);
|
||||||
|
},
|
||||||
|
quit: async function (data, config = {}) {
|
||||||
|
return await http.get("online/quit", data, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<svg t="1761031307486" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8433" width="256" height="256"><path d="M512 800m-96 0a96 96 0 1 0 192 0 96 96 0 1 0-192 0Z" p-id="8434"></path><path d="M748.096 501.44a324.608 324.608 0 0 0-230.272-95.36c-87.52 0-169.6 34.72-230.272 95.36a326.72 326.72 0 0 0-42.464 52.256 32 32 0 1 0 53.568 35.008 263.616 263.616 0 0 1 34.144-41.984 260.704 260.704 0 0 1 185.024-76.64c70.368 0 136.224 27.872 184.992 76.64 12.8 12.8 24.256 26.88 34.144 42.016a32 32 0 0 0 53.568-35.008 325.024 325.024 0 0 0-42.432-52.288z" p-id="8435"></path><path d="M991.776 384.576a569.824 569.824 0 0 0-73.792-90.816C812.544 188.32 669.92 128 517.824 128S223.072 188.32 117.664 293.76a568.416 568.416 0 0 0-73.792 90.816 32 32 0 0 0 53.568 35.008 505.76 505.76 0 0 1 65.472-80.608C256.448 245.472 382.88 192 517.824 192s261.344 53.472 354.912 147.008a502.944 502.944 0 0 1 65.472 80.608 32 32 0 1 0 53.568-35.04z" p-id="8436"></path></svg>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
<template>
|
||||||
|
<div class="pi-asset">
|
||||||
|
<div class="file-item" v-for="(item, index) in value" :key="index" :style="style">
|
||||||
|
<el-image v-if="obj.isImg(item)" :src="item" :style="style" :preview-src-list="[item]" fit="cover"
|
||||||
|
preview-teleported :z-index="9999"></el-image>
|
||||||
|
<pi-player v-else-if="obj.isVideo(item)" :src="item" :options="videoOptions"></pi-player>
|
||||||
|
<el-icon v-else :style="style" style="color: #409eff;" :size="32">
|
||||||
|
<component :is="'pi-icon-task'"/>
|
||||||
|
</el-icon>
|
||||||
|
<div class="file-item-delete" @click="del(index)">
|
||||||
|
<el-icon>
|
||||||
|
<component :is="'el-icon-delete'" color="#ffffff"/>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="pi-upload" :style="style" v-if="limit > value.length" @click="openDialog">
|
||||||
|
<el-icon><component :is="'el-icon-plus'"/></el-icon>
|
||||||
|
</span>
|
||||||
|
<picker-dialog v-if="dialogVisible" ref="pickerRef" @success="saveSuccess" @closed="dialogVisible=false"
|
||||||
|
:max="limit-value.length" :multiple="limit>1" :type="type"></picker-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import pickerDialog from "./picker.vue";
|
||||||
|
import assetConfig from "@/config/asset"
|
||||||
|
import piPlayer from '@/components/piPlayer'
|
||||||
|
import {nextTick, ref, watch} from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: null,
|
||||||
|
limit: {type: Number, default: 1},
|
||||||
|
height: {type: Number, default: 148},
|
||||||
|
width: {type: Number, default: 148},
|
||||||
|
type: {type: String, default: "image"}
|
||||||
|
})
|
||||||
|
|
||||||
|
let dialogVisible = ref(false)
|
||||||
|
const pickerRef = ref(null)
|
||||||
|
let value = ref([])
|
||||||
|
let style = ref({
|
||||||
|
width: props.width + "px",
|
||||||
|
height: props.height + "px"
|
||||||
|
})
|
||||||
|
let obj = ref(assetConfig)
|
||||||
|
let videoOptions = ref({
|
||||||
|
pip: true
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(value, (val) => {
|
||||||
|
if (props.limit > 1) {
|
||||||
|
emit('update:modelValue', val)
|
||||||
|
} else {
|
||||||
|
emit('update:modelValue', val[0])
|
||||||
|
}
|
||||||
|
}, {deep: true, immediate: false})
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
if (props.limit > 1) {
|
||||||
|
if (!val || val.length <= 0) return;
|
||||||
|
value.value = val
|
||||||
|
} else {
|
||||||
|
if (!val) return;
|
||||||
|
value.value = val ? [val] : []
|
||||||
|
}
|
||||||
|
}, {deep: true, immediate: true})
|
||||||
|
|
||||||
|
function saveSuccess(val) {
|
||||||
|
if (props.limit > 1) {
|
||||||
|
value.value = value.value.concat(val)
|
||||||
|
} else {
|
||||||
|
value.value = [val]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function del(index) {
|
||||||
|
value.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDialog() {
|
||||||
|
dialogVisible.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
pickerRef.value.open()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pi-asset {
|
||||||
|
width: 100%;
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-asset .file-item {
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-asset .file-item:hover .file-item-delete {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-asset .file-item .file-item-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #F56C6C;
|
||||||
|
line-height: initial;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
display: none;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-asset .pi-upload {
|
||||||
|
border: 1px dashed var(--el-border-color-darker);
|
||||||
|
background-color: var(--el-fill-color-lighter);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-asset .pi-upload:hover {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<el-dialog title="移动文件" v-model="visible" :width="500" destroy-on-close @closed="$emit('closed')">
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||||
|
<el-form-item label="选择" prop="category_id">
|
||||||
|
<el-cascader ref="categoryRef" v-model="form.category_id" :options="menu" :props="menuProps" clearable
|
||||||
|
style="width: 100%;"></el-cascader>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible=false">取 消</el-button>
|
||||||
|
<el-button type="primary" :loading="isSaveing" @click="submit()">保 存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {ref} from "vue";
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['success', 'closed'])
|
||||||
|
const categoryRef = ref(null)
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
let visible = ref(false)
|
||||||
|
let isSaveing = ref(false)
|
||||||
|
let form = ref({
|
||||||
|
category_id: 0
|
||||||
|
})
|
||||||
|
let rules = ref({
|
||||||
|
category_name: [
|
||||||
|
{required: true, message: '请输入分类名称'}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
let menu = ref([])
|
||||||
|
let menuProps = ref({
|
||||||
|
value: "category_id",
|
||||||
|
label: "category_name",
|
||||||
|
checkStrictly: true
|
||||||
|
})
|
||||||
|
|
||||||
|
function open(data) {
|
||||||
|
visible.value = true
|
||||||
|
if (data) {
|
||||||
|
menu.value = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (categoryRef.value.getCheckedNodes().length > 0) {
|
||||||
|
form.value.category_id = categoryRef.value.getCheckedNodes()[0].value
|
||||||
|
}
|
||||||
|
emit('success', form.value.category_id)
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,646 @@
|
||||||
|
<template>
|
||||||
|
<el-dialog title="选择文件" v-model="visible" :width="900" destroy-on-close @closed="$emit('closed')"
|
||||||
|
append-to-body>
|
||||||
|
<div class="pi-file-select">
|
||||||
|
<div class="pi-file-select__side" v-loading="menuLoading">
|
||||||
|
<div class="pi-file-select__side-menu">
|
||||||
|
<el-tree ref="menuRef" class="menu" :data="menu" :node-key="treeProps.key" :props="treeProps"
|
||||||
|
:expand-on-click-node="false" check-strictly show-checkbox
|
||||||
|
:current-node-key="menu.length>0?menu[0][treeProps.key]:''" highlight-current
|
||||||
|
@node-click="groupClick" :check-on-click-leaf="false">
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span class="custom-tree-node el-tree-node__label">
|
||||||
|
<span class="label">
|
||||||
|
{{ node.label }}
|
||||||
|
</span>
|
||||||
|
<span class="do">
|
||||||
|
<el-icon @click.stop="editCategory(data)"><el-icon-edit/></el-icon>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</div>
|
||||||
|
<div class="pi-file-select__side-msg">
|
||||||
|
<el-button type="primary" size="small" icon="el-icon-plus" @click="addCategory"></el-button>
|
||||||
|
<el-button type="danger" size="small" plain icon="el-icon-delete" @click="delCategory"></el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pi-file-select__files" v-loading="listLoading">
|
||||||
|
<div class="pi-file-select__top">
|
||||||
|
<div class="upload" v-if="!hideUpload">
|
||||||
|
<el-upload class="pi-file-select__upload" action="" multiple :show-file-list="false"
|
||||||
|
:accept="accept" :on-change="uploadChange" :before-upload="uploadBefore"
|
||||||
|
:on-progress="uploadProcess" :on-success="uploadSuccess" :on-error="uploadError"
|
||||||
|
:http-request="uploadRequest">
|
||||||
|
<el-button type="primary" icon="el-icon-upload">本地上传</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<el-button type="danger" icon="el-icon-delete" @click="delFile">删除</el-button>
|
||||||
|
<el-button type="primary" icon="el-icon-rank" @click="moveFile">移动</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="keyword">
|
||||||
|
<el-input v-model="file_name" style="width: 220px;" prefix-icon="el-icon-search"
|
||||||
|
placeholder="文件名搜索" clearable @keyup.enter="search" @input="search"
|
||||||
|
@clear="search"></el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pi-file-select__list">
|
||||||
|
<el-scrollbar ref="scrollbarRef">
|
||||||
|
<el-empty v-if="fileList.length==0 && data.length==0" description="无数据"
|
||||||
|
:image-size="80"></el-empty>
|
||||||
|
<div v-for="(file, index) in fileList" :key="index" class="pi-file-select__item">
|
||||||
|
<div class="pi-file-select__item__file">
|
||||||
|
<div class="pi-file-select__item__upload">
|
||||||
|
<el-progress type="circle" :percentage="file.progress" :width="70"></el-progress>
|
||||||
|
</div>
|
||||||
|
<el-image :src="file.tempImg" fit="contain"></el-image>
|
||||||
|
</div>
|
||||||
|
<p>{{ file.name }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-for="item in data" :key="item[fileProps.fileName]" class="pi-file-select__item"
|
||||||
|
:class="{active: value.includes(item[fileProps.url]) }" @click="select(item)">
|
||||||
|
<div class="pi-file-select__item__file">
|
||||||
|
<div class="pi-file-select__item__checkbox" v-if="multiple">
|
||||||
|
<el-icon>
|
||||||
|
<el-icon-check/>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="pi-file-select__item__select" v-else>
|
||||||
|
<el-icon>
|
||||||
|
<el-icon-check/>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="pi-file-select__item__box"></div>
|
||||||
|
<el-image v-if="obj.isImg(item[fileProps.url])" :src="item[fileProps.url]" fit="contain"
|
||||||
|
lazy></el-image>
|
||||||
|
<div v-else-if="obj.isVideo(item[fileProps.url])" class="item-video">
|
||||||
|
<el-icon size="32px">
|
||||||
|
<component :is="'pi-icon-video'"/>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div v-else class="item-file item-file-doc">
|
||||||
|
<el-icon size="32px">
|
||||||
|
<component :is="'pi-icon-task'"/>
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p :title="item[fileProps.fileName]">{{ item[fileProps.fileName] }}</p>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
<div class="pi-file-select__pagination">
|
||||||
|
<el-pagination size="small" background layout="prev, pager, next" :total="total" :page-size="pageSize"
|
||||||
|
v-model:currentPage="currentPage" @current-change="reload"></el-pagination>
|
||||||
|
</div>
|
||||||
|
<div class="pi-file-select__do">
|
||||||
|
<div><span v-if="multiple">已选择 <b>{{ value.length }}</b> / <b>{{ max }}</b> 项</span></div>
|
||||||
|
<el-button type="primary" :disabled="value.length<=0" @click="submit">确 定</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<save-dialog v-if="dialog.save" ref="saveRef" @success="getMenu" @closed="dialog.save=false"></save-dialog>
|
||||||
|
<move-dialog v-if="dialog.move" ref="moveRef" @success="moveSuccess"
|
||||||
|
@closed="dialog.move=false"></move-dialog>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import assetConfig from "@/config/asset"
|
||||||
|
import saveDialog from "./save.vue";
|
||||||
|
import moveDialog from "./move.vue";
|
||||||
|
import * as imageConversion from 'image-conversion';
|
||||||
|
import {ref, onMounted, nextTick, getCurrentInstance} from "vue"
|
||||||
|
import tools from "@/utils/tools"
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['success', 'closed'])
|
||||||
|
const props = defineProps({
|
||||||
|
hideUpload: {type: Boolean, default: false},
|
||||||
|
multiple: {type: Boolean, default: false},
|
||||||
|
max: {type: Number, default: assetConfig.max},
|
||||||
|
maxSize: {type: Number, default: assetConfig.maxSize},
|
||||||
|
type: {type: String, default: "image"}
|
||||||
|
})
|
||||||
|
const {proxy} = getCurrentInstance()
|
||||||
|
const saveRef = ref(null)
|
||||||
|
const moveRef = ref(null)
|
||||||
|
const scrollbarRef = ref(null)
|
||||||
|
const menuRef = ref(null)
|
||||||
|
|
||||||
|
let dialog = ref({
|
||||||
|
save: false,
|
||||||
|
move: false
|
||||||
|
})
|
||||||
|
let file_name = ref(null)
|
||||||
|
let pageSize = ref(20)
|
||||||
|
let total = ref(0)
|
||||||
|
let currentPage = ref(1)
|
||||||
|
let data = ref([])
|
||||||
|
let menu = ref([])
|
||||||
|
let category_id = ref(0)
|
||||||
|
let value = ref(props.multiple ? [] : '')
|
||||||
|
let fileList = ref([])
|
||||||
|
let accept = ref(assetConfig.fileType(props.type)['accept'])
|
||||||
|
let listLoading = ref(false)
|
||||||
|
let menuLoading = ref(false)
|
||||||
|
let treeProps = ref(assetConfig.menuProps)
|
||||||
|
let fileProps = ref(assetConfig.fileProps)
|
||||||
|
let visible = ref(false)
|
||||||
|
let obj = ref(assetConfig)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getMenu()
|
||||||
|
getData()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function open() {
|
||||||
|
visible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMenu() {
|
||||||
|
menuLoading.value = true
|
||||||
|
const res = await assetConfig.menuListObj();
|
||||||
|
menu.value = tools.makeTreeData(res.data, 0, "category_id")
|
||||||
|
menu.value.unshift({
|
||||||
|
'category_id': '',
|
||||||
|
'category_name': '未分类',
|
||||||
|
'pid': 0
|
||||||
|
})
|
||||||
|
menuLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
listLoading.value = true
|
||||||
|
const reqData = {
|
||||||
|
[assetConfig.request.menuKey]: category_id.value,
|
||||||
|
[assetConfig.request.page]: currentPage.value,
|
||||||
|
[assetConfig.request.pageSize]: pageSize.value,
|
||||||
|
};
|
||||||
|
reqData.type = props.type
|
||||||
|
reqData.file_name = file_name.value
|
||||||
|
const res = await assetConfig.fileListObj(reqData);
|
||||||
|
const parseData = assetConfig.listParseData(res);
|
||||||
|
data.value = parseData.rows
|
||||||
|
total.value = parseData.total
|
||||||
|
listLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupClick(data) {
|
||||||
|
category_id.value = data.category_id
|
||||||
|
currentPage.value = 1
|
||||||
|
file_name.value = null
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function search() {
|
||||||
|
currentPage.value = 1
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function select(item) {
|
||||||
|
const itemUrl = item[fileProps.value.url]
|
||||||
|
if (props.multiple) {
|
||||||
|
if (value.value.length >= props.max) {
|
||||||
|
proxy.$message.error("选择文件过多")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value.value.includes(itemUrl)) {
|
||||||
|
value.value.splice(value.value.findIndex(f => f == itemUrl), 1)
|
||||||
|
} else {
|
||||||
|
value.value.push(itemUrl)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (value.value.includes(itemUrl)) {
|
||||||
|
value.value = ''
|
||||||
|
} else {
|
||||||
|
value.value = itemUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
emit('success', value.value);
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function delFile() {
|
||||||
|
const _value = JSON.parse(JSON.stringify(value.value))
|
||||||
|
const url = props.multiple ? _value.toString() : _value;
|
||||||
|
if (!url) return;
|
||||||
|
const res = await assetConfig.delFileObj({url: url});
|
||||||
|
proxy.$message.success(res.msg)
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveFile() {
|
||||||
|
dialog.value.move = true
|
||||||
|
nextTick(() => {
|
||||||
|
moveRef.value.open(menu.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveSuccess(category_id) {
|
||||||
|
console.log(category_id)
|
||||||
|
const _value = JSON.parse(JSON.stringify(value.value))
|
||||||
|
const url = props.multiple ? _value.toString() : _value;
|
||||||
|
if (!url) return;
|
||||||
|
const res = await assetConfig.moveFileObj({url: url, category_id: category_id});
|
||||||
|
proxy.$message.success(res.msg)
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadChange(file, _fileList) {
|
||||||
|
file.tempImg = URL.createObjectURL(file.raw);
|
||||||
|
fileList.value = _fileList
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadBefore(file) {
|
||||||
|
const fileSize = file.size / 1024;
|
||||||
|
if (assetConfig.isImg(file.name) && fileSize > 800) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
imageConversion.compressAccurately(file, 800).then((res) => {
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (fileSize > props.maxSize * 1024) {
|
||||||
|
proxy.$message.warning(`上传文件大小不能超过 ${props.maxSize}MB!`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadRequest(param) {
|
||||||
|
// 获取文件类型
|
||||||
|
const _type = assetConfig.getType(param.file.name)
|
||||||
|
if (props.type !== "" && proxy.type !== _type) {
|
||||||
|
proxy.$message.warning(`不允许的文件类型!`);
|
||||||
|
clearFiles(param.file)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const data = new FormData();
|
||||||
|
data.append("file", param.file);
|
||||||
|
if (category_id.value) {
|
||||||
|
data.append(assetConfig.request.menuKey, category_id.value);
|
||||||
|
}
|
||||||
|
assetConfig.uploadObj(data, {
|
||||||
|
onUploadProgress: e => {
|
||||||
|
param.onProgress(e)
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
param.onSuccess(res)
|
||||||
|
}).catch(err => {
|
||||||
|
param.onError(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadProcess(event, file) {
|
||||||
|
file.progress = Number((event.loaded / event.total * 100).toFixed(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFiles(file) {
|
||||||
|
fileList.value.splice(fileList.value.findIndex(f => f.uid == file.uid), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadSuccess(res, file) {
|
||||||
|
fileList.value.splice(fileList.value.findIndex(f => f.uid == file.uid), 1)
|
||||||
|
var response = assetConfig.uploadParseData(res);
|
||||||
|
data.value.unshift({
|
||||||
|
[fileProps.value.fileName]: response.fileName,
|
||||||
|
[fileProps.value.url]: response.url
|
||||||
|
})
|
||||||
|
if (!props.multiple) {
|
||||||
|
value.value = response.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadError(err) {
|
||||||
|
proxy.$notify.error({
|
||||||
|
title: '上传文件错误',
|
||||||
|
message: err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function editCategory(data) {
|
||||||
|
dialog.value.save = true
|
||||||
|
nextTick(() => {
|
||||||
|
saveRef.value.open('edit', menu.value, data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCategory() {
|
||||||
|
dialog.value.save = true
|
||||||
|
nextTick(() => {
|
||||||
|
saveRef.value.open('add',menu.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function delCategory() {
|
||||||
|
var CheckedNodes = menuRef.value.getCheckedNodes()
|
||||||
|
if (CheckedNodes.length == 0) {
|
||||||
|
proxy.$message.warning("请选择需要删除的项")
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var confirm = await proxy.$confirm('确认删除已选择的菜单吗?', '提示', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '删除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
confirmButtonClass: 'el-button--danger'
|
||||||
|
}).catch(() => {
|
||||||
|
})
|
||||||
|
if (confirm != 'confirm') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
menuLoading.value = true
|
||||||
|
var ids = CheckedNodes.map(item => item.category_id)
|
||||||
|
var res = await assetConfig.menuDelObj({ids: ids.toString()})
|
||||||
|
menuLoading.value = false
|
||||||
|
proxy.$message.success(res.msg)
|
||||||
|
CheckedNodes.forEach(item => {
|
||||||
|
var node = menuRef.value.getNode(item)
|
||||||
|
if (node == null) {
|
||||||
|
reload()
|
||||||
|
} else if (node.isCurrent) {
|
||||||
|
saveRef.value.setData({})
|
||||||
|
}
|
||||||
|
menuRef.value.remove(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pi-file-select {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__files {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__list {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item {
|
||||||
|
display: inline-block;
|
||||||
|
float: left;
|
||||||
|
margin: 0 15px 25px 0;
|
||||||
|
width: 110px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item__file {
|
||||||
|
width: 110px;
|
||||||
|
height: 110px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item__file .el-image {
|
||||||
|
width: 110px;
|
||||||
|
height: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item__box {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
border: 2px solid var(--el-color-success);
|
||||||
|
z-index: 1;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item__box::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background: var(--el-color-success);
|
||||||
|
opacity: 0.2;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item:hover .pi-file-select__item__box {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item.active .pi-file-select__item__box {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item.active .pi-file-select__item__box::before {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item p {
|
||||||
|
margin-top: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-text-overflow: ellipsis;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item__checkbox {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
top: 7px;
|
||||||
|
right: 7px;
|
||||||
|
z-index: 2;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item__checkbox i {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item__select {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
z-index: 2;
|
||||||
|
background: var(--el-color-success);
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item__select i {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item.active .pi-file-select__item__checkbox {
|
||||||
|
background: var(--el-color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item.active .pi-file-select__item__checkbox i {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item.active .pi-file-select__item__select {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item__file .item-file {
|
||||||
|
width: 110px;
|
||||||
|
height: 110px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item__file .item-file i {
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item__file .item-file.item-file-doc {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item .item-video {
|
||||||
|
width: 110px;
|
||||||
|
height: 110px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item .item-video video {
|
||||||
|
width: 110px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__item__upload {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__side {
|
||||||
|
width: 200px;
|
||||||
|
margin-right: 15px;
|
||||||
|
border-right: 1px solid rgba(128, 128, 128, 0.2);
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__side-menu {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__side-msg {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__top {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__upload {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__top .tips {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__top .tips i {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 5px;
|
||||||
|
position: relative;
|
||||||
|
bottom: -0.125em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__pagination {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-file-select__do {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 14px;
|
||||||
|
padding-right: 24px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node .label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node .label .el-tag {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node .do {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node .do i {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node .do i:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node:hover .do {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload) {
|
||||||
|
display: block;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<el-dialog :title="titleMap[mode]" v-model="visible" :width="500" destroy-on-close @closed="$emit('closed')">
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||||
|
<el-form-item label="上级" prop="pid">
|
||||||
|
<el-cascader ref="categoryRef" v-model="form.pid" :options="menu" :props="menuProps" clearable
|
||||||
|
style="width: 100%;"></el-cascader>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="名称" prop="category_name">
|
||||||
|
<el-input v-model="form.category_name" placeholder="请输入分类名称" clearable></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序" prop="rank">
|
||||||
|
<el-input-number v-model="form.rank" controls-position="right" :min="1" :max="9999"
|
||||||
|
style="width: 100%;"></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible=false">取 消</el-button>
|
||||||
|
<el-button type="primary" :loading="isSaveing" @click="submit()">保 存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import assetConfig from "@/config/asset"
|
||||||
|
import {getCurrentInstance, ref} from "vue"
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['success', 'closed'])
|
||||||
|
const formRef = ref(null)
|
||||||
|
const categoryRef = ref(null)
|
||||||
|
const {proxy} = getCurrentInstance()
|
||||||
|
|
||||||
|
let mode = ref("add")
|
||||||
|
const titleMap = ref({
|
||||||
|
add: '新增',
|
||||||
|
edit: '编辑'
|
||||||
|
})
|
||||||
|
let visible = ref(false)
|
||||||
|
let isSaveing = ref(false)
|
||||||
|
|
||||||
|
let form = ref({
|
||||||
|
category_id: null,
|
||||||
|
category_name: "",
|
||||||
|
pid: 0,
|
||||||
|
rank: 1
|
||||||
|
})
|
||||||
|
let rules = ref({
|
||||||
|
category_name: [
|
||||||
|
{required: true, message: '请输入分类名称'}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
let menu = ref([])
|
||||||
|
let menuProps = ref({
|
||||||
|
value: "category_id",
|
||||||
|
label: "category_name",
|
||||||
|
checkStrictly: true
|
||||||
|
})
|
||||||
|
|
||||||
|
function open(_mode = 'add', _menu = null, data = null) {
|
||||||
|
mode.value = _mode;
|
||||||
|
visible.value = true;
|
||||||
|
if (_menu) {
|
||||||
|
menu.value = _menu
|
||||||
|
}
|
||||||
|
if (data) {
|
||||||
|
Object.assign(form.value, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
// 校验登录
|
||||||
|
const validate = await formRef.value.validate().catch(() => {
|
||||||
|
});
|
||||||
|
if (!validate) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let data = Object.assign({}, form.value)
|
||||||
|
// 部门获取最后一项
|
||||||
|
if (categoryRef.value.getCheckedNodes().length > 0) {
|
||||||
|
data.pid = categoryRef.value.getCheckedNodes()[0].value
|
||||||
|
}
|
||||||
|
isSaveing.value = true;
|
||||||
|
const res = data.category_id ? await assetConfig.menuEditObj(data) : await assetConfig.menuAddObj(data);
|
||||||
|
isSaveing.value = false;
|
||||||
|
emit('success', data, mode.value)
|
||||||
|
visible.value = false;
|
||||||
|
proxy.$message.success(res.msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
const T = {
|
||||||
|
"color": [
|
||||||
|
"#409EFF",
|
||||||
|
"#36CE9E",
|
||||||
|
"#f56e6a",
|
||||||
|
"#626c91",
|
||||||
|
"#edb00d",
|
||||||
|
"#909399"
|
||||||
|
],
|
||||||
|
'grid': {
|
||||||
|
'left': '3%',
|
||||||
|
'right': '3%',
|
||||||
|
'bottom': '10',
|
||||||
|
'top': '40',
|
||||||
|
'containLabel': true
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"textStyle": {
|
||||||
|
"color": "#999"
|
||||||
|
},
|
||||||
|
"inactiveColor": "rgba(128,128,128,0.4)"
|
||||||
|
},
|
||||||
|
"categoryAxis": {
|
||||||
|
"axisLine": {
|
||||||
|
"show": true,
|
||||||
|
"lineStyle": {
|
||||||
|
"color": "rgba(128,128,128,0.2)",
|
||||||
|
"width": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"axisTick": {
|
||||||
|
"show": false,
|
||||||
|
"lineStyle": {
|
||||||
|
"color": "#333"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"axisLabel": {
|
||||||
|
"color": "#999"
|
||||||
|
},
|
||||||
|
"splitLine": {
|
||||||
|
"show": false,
|
||||||
|
"lineStyle": {
|
||||||
|
"color": [
|
||||||
|
"#eee"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"splitArea": {
|
||||||
|
"show": false,
|
||||||
|
"areaStyle": {
|
||||||
|
"color": [
|
||||||
|
"rgba(255,255,255,0.01)",
|
||||||
|
"rgba(0,0,0,0.01)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"valueAxis": {
|
||||||
|
"axisLine": {
|
||||||
|
"show": false,
|
||||||
|
"lineStyle": {
|
||||||
|
"color": "#999"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"splitLine": {
|
||||||
|
"show": true,
|
||||||
|
"lineStyle": {
|
||||||
|
"color": "rgba(128,128,128,0.2)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default T
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<div ref="chartsRef" :style="{height:height, width:width}"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
import T from './echarts-theme-T.js';
|
||||||
|
import {ref, watch, computed, onActivated, onDeactivated, nextTick, onMounted} from "vue";
|
||||||
|
echarts.registerTheme('T', T);
|
||||||
|
|
||||||
|
const unwarp = (obj) => obj && (obj.__v_raw || obj.valueOf() || obj);
|
||||||
|
const chartsRef = ref(null)
|
||||||
|
const props = defineProps({
|
||||||
|
height: {type: String, default: "100%"},
|
||||||
|
width: {type: String, default: "100%"},
|
||||||
|
nodata: {type: Boolean, default: false},
|
||||||
|
option: {
|
||||||
|
type: Object, default: () => {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let isActive = ref(false)
|
||||||
|
let chart = ref(null)
|
||||||
|
|
||||||
|
watch(() => props.option, (v) => {
|
||||||
|
unwarp(chart.value).setOption(v);
|
||||||
|
}, {deep: true})
|
||||||
|
|
||||||
|
const options = computed(() => {
|
||||||
|
return props.option || {}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
if (!isActive.value) {
|
||||||
|
nextTick(() => {
|
||||||
|
chart.value.resize()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
isActive.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
isActive.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
draw()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
var myChart = echarts.init(chartsRef.value, 'T');
|
||||||
|
myChart.setOption(options.value);
|
||||||
|
chart.value = myChart;
|
||||||
|
window.addEventListener('resize', () => myChart.resize());
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
<template>
|
||||||
|
<el-dialog title="请选择" v-model="visible" :width="800" destroy-on-close @closed="$emit('closed')"
|
||||||
|
:close-on-click-modal="false" top="8vh">
|
||||||
|
<div v-loading="loading">
|
||||||
|
<div class="pi-table-select__header">
|
||||||
|
<slot name="header" :form="formData" :submit="formSubmit"></slot>
|
||||||
|
</div>
|
||||||
|
<el-table ref="tableRef" :data="tableData" :height="480" :highlight-current-row="!multiple"
|
||||||
|
@row-click="click"
|
||||||
|
@select="select" @select-all="selectAll">
|
||||||
|
<el-table-column v-if="multiple" type="selection" width="45"></el-table-column>
|
||||||
|
<el-table-column v-else type="index" width="45">
|
||||||
|
<template #default="scope"><span>{{ scope.$index + (currentPage - 1) * pageSize + 1 }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<slot></slot>
|
||||||
|
</el-table>
|
||||||
|
<div class="pi-table-select__page">
|
||||||
|
<el-pagination size="small" background layout="prev, pager, next" :total="total" :page-size="pageSize"
|
||||||
|
v-model:currentPage="formData.page" @current-change="reload"></el-pagination>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer v-if="multiple">
|
||||||
|
<el-button @click="visible=false">取 消</el-button>
|
||||||
|
<el-button @click="confirm()">确 定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import config from "@/config/select"
|
||||||
|
import {ref, onMounted, nextTick} from "vue"
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(["success", "closed"])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {type: String, default: '请选择'},
|
||||||
|
apiObj: {
|
||||||
|
type: Function, default: () => {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
limit: {type: Number, default: 0},
|
||||||
|
multiple: {type: Boolean, default: false},
|
||||||
|
params: {
|
||||||
|
type: Object, default: () => {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
type: Object, default: () => {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const tableRef = ref(null)
|
||||||
|
|
||||||
|
let loading = ref(false)
|
||||||
|
let visible = ref(false)
|
||||||
|
let tableData = ref([])
|
||||||
|
let pageSize = ref(config.pageSize)
|
||||||
|
let currentPage = ref(1)
|
||||||
|
let total = ref(0)
|
||||||
|
let formData = ref({})
|
||||||
|
let checkData = ref([])
|
||||||
|
let defaultProps = ref({
|
||||||
|
label: config.props.label,
|
||||||
|
value: config.props.value,
|
||||||
|
page: config.request.page,
|
||||||
|
pageSize: config.request.pageSize,
|
||||||
|
keyword: config.request.keyword,
|
||||||
|
fields: []
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
defaultProps.value = Object.assign(defaultProps.value, props.props)
|
||||||
|
})
|
||||||
|
|
||||||
|
//显示
|
||||||
|
function open() {
|
||||||
|
visible.value = true;
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
var reqData = {
|
||||||
|
[config.request.page]: currentPage.value,
|
||||||
|
[config.request.pageSize]: pageSize.value,
|
||||||
|
}
|
||||||
|
Object.assign(reqData, props.params, formData.value)
|
||||||
|
const res = await props.apiObj(reqData)
|
||||||
|
tableData.value = res.data
|
||||||
|
total.value = res.count
|
||||||
|
initCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirm() {
|
||||||
|
visible.value = false
|
||||||
|
emit('success', checkData.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formSubmit() {
|
||||||
|
currentPage.value = 1
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
//分页刷新表格
|
||||||
|
function reload() {
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
//表格勾选事件
|
||||||
|
function select(rows, row) {
|
||||||
|
var isSelect = rows.length && rows.indexOf(row) !== -1
|
||||||
|
if (isSelect) {
|
||||||
|
checkData.value.push(row)
|
||||||
|
} else {
|
||||||
|
checkData.value.splice(checkData.value.findIndex(item => item[props.value] == row[props.value]), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//表格全选事件
|
||||||
|
function selectAll(rows) {
|
||||||
|
var isAllSelect = rows.length > 0
|
||||||
|
if (isAllSelect) {
|
||||||
|
rows.forEach(row => {
|
||||||
|
var isHas = false
|
||||||
|
isHas = checkData.value.find(item => item[props.value] == row[props.value])
|
||||||
|
if (!isHas) {
|
||||||
|
checkData.value.push(row)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
tableData.value.forEach(row => {
|
||||||
|
var isHas = false
|
||||||
|
isHas = checkData.value.find(item => item[props.value] == row[props.value])
|
||||||
|
if (isHas) {
|
||||||
|
checkData.value.splice(checkData.value.findIndex(item => item[props.value] == row[props.value]), 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function click(row) {
|
||||||
|
if (!props.multiple) {
|
||||||
|
visible.value = false
|
||||||
|
emit('success', row);
|
||||||
|
}
|
||||||
|
var isSelect = checkData.value.length && checkData.value.indexOf(row) !== -1
|
||||||
|
tableRef.value.toggleRowSelection(row)
|
||||||
|
if (!isSelect) {
|
||||||
|
checkData.value.push(row)
|
||||||
|
} else {
|
||||||
|
checkData.value.splice(checkData.value.findIndex(item => item[props.value] == row[props.value]), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCheck() {
|
||||||
|
if (!props.multiple) return;
|
||||||
|
nextTick(() => {
|
||||||
|
tableData.value.forEach((row) => {
|
||||||
|
var isHas = false
|
||||||
|
isHas = checkData.value.find(item => item[props.value] == row[props.value])
|
||||||
|
if (isHas) {
|
||||||
|
tableRef.value.toggleRowSelection(row, true)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pi-table-select__table {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-table-select__page {
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import {TextStyle} from '@tiptap/extension-text-style'
|
||||||
|
|
||||||
|
export default TextStyle.extend({
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
fontSize: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: element => element.style.fontSize || null,
|
||||||
|
renderHTML: attributes => {
|
||||||
|
if (!attributes.fontSize) return {}
|
||||||
|
return {
|
||||||
|
style: `font-size: ${attributes.fontSize}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
<template>
|
||||||
|
<NodeViewWrapper as="p" class="image-wrapper">
|
||||||
|
<div class="image-box" ref="boxRef" @click="handleClick">
|
||||||
|
<img :src="node.attrs.src" :alt="node.attrs.alt" :width="node.attrs.width" :height="node.attrs.height" :draggable="false"/>
|
||||||
|
<!-- 操作菜单,仅在选中时显示 -->
|
||||||
|
<div v-if="selected" class="image-menu">
|
||||||
|
<button @click="deleteImage">删除</button>
|
||||||
|
<button @click="replaceImage">配置</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-dialog v-model="settingShow" title="配置" width="500" align-center>
|
||||||
|
<el-form v-model="form" label-position="top">
|
||||||
|
<el-form-item label="地址" prop="src">
|
||||||
|
<el-input v-model="form.src"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="描述" prop="alt">
|
||||||
|
<el-input v-model="form.alt"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="宽高" prop="width">
|
||||||
|
<el-row :gutter="10" style="margin: 10px 0; width: 100%;">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-input v-model="form.width">
|
||||||
|
<template #suffix>宽 </template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-input v-model="form.height">
|
||||||
|
<template #suffix>高 </template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="settingShow = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submit">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</NodeViewWrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {NodeViewWrapper, NodeViewProps} from '@tiptap/vue-3'
|
||||||
|
import {computed, ref} from "vue"
|
||||||
|
|
||||||
|
const props = defineProps<NodeViewProps>();
|
||||||
|
const selected = computed(() => props.selected)
|
||||||
|
|
||||||
|
let settingShow = ref(false)
|
||||||
|
let form = ref({
|
||||||
|
src: '',
|
||||||
|
width: '',
|
||||||
|
height: '',
|
||||||
|
alt: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
function deleteImage() {
|
||||||
|
props.deleteNode()
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceImage() {
|
||||||
|
form.value.src = props.node.attrs.src
|
||||||
|
form.value.width = props.node.attrs.width
|
||||||
|
form.value.height = props.node.attrs.height
|
||||||
|
form.value.alt = props.node.attrs.alt
|
||||||
|
settingShow.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
props.editor.commands.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
settingShow.value = false
|
||||||
|
props.updateAttributes(form.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.image-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-box img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作菜单 */
|
||||||
|
.image-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-menu button {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
border: none;
|
||||||
|
padding: 4px 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import Action from "./Action.vue"
|
||||||
|
import {VueNodeViewRenderer} from "@tiptap/vue-3"
|
||||||
|
import { Image } from '@tiptap/extension-image'
|
||||||
|
|
||||||
|
export default Image.extend({
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
class: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addNodeView() {
|
||||||
|
return VueNodeViewRenderer(Action)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import {Extension, Command} from '@tiptap/core'
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
indent: {
|
||||||
|
increaseIndent: () => ReturnType
|
||||||
|
decreaseIndent: () => ReturnType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Extension.create({
|
||||||
|
name: 'indent',
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
maxIndent: 50,
|
||||||
|
types: ['paragraph', 'heading', 'code_block', 'bullet_list', 'ordered_list'],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addGlobalAttributes() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
types: this.options.types,
|
||||||
|
attributes: {
|
||||||
|
'indent': {
|
||||||
|
default: 0,
|
||||||
|
parseHTML: element => parseInt(element.getAttribute('indent') || '0', 10),
|
||||||
|
renderHTML: attributes => {
|
||||||
|
const level = attributes['indent']
|
||||||
|
return level > 0 && {
|
||||||
|
'indent': level,
|
||||||
|
style: `text-indent: ${level * 2}em;`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
increaseIndent:
|
||||||
|
(): Command =>
|
||||||
|
({tr, state, dispatch}) => {
|
||||||
|
const {from, to} = state.selection
|
||||||
|
let modified = false
|
||||||
|
state.doc.nodesBetween(from, to, (node, pos) => {
|
||||||
|
if (this.options.types.includes(node.type.name)) {
|
||||||
|
const indent = node.attrs['indent'] || 0
|
||||||
|
if (indent + 1 > this.options.maxIndent) return
|
||||||
|
tr.setNodeMarkup(pos, undefined, {
|
||||||
|
...node.attrs,
|
||||||
|
'indent': indent + 1,
|
||||||
|
})
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (modified) {
|
||||||
|
dispatch && dispatch(tr)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
decreaseIndent:
|
||||||
|
(): Command =>
|
||||||
|
({tr, state, dispatch}) => {
|
||||||
|
const {from, to} = state.selection
|
||||||
|
let modified = false
|
||||||
|
state.doc.nodesBetween(from, to, (node, pos) => {
|
||||||
|
if (this.options.types.includes(node.type.name)) {
|
||||||
|
const indent = node.attrs['indent'] || 0
|
||||||
|
if (indent - 1 < 0) return
|
||||||
|
const newIndent = Math.max(indent - 1, 0)
|
||||||
|
tr.setNodeMarkup(pos, undefined, {
|
||||||
|
...node.attrs,
|
||||||
|
'indent': newIndent,
|
||||||
|
})
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (modified) {
|
||||||
|
dispatch && dispatch(tr)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
Tab: () => this.editor.commands.increaseIndent(),
|
||||||
|
'Shift-Tab': () => this.editor.commands.decreaseIndent(),
|
||||||
|
Backspace: () => {
|
||||||
|
const {state} = this.editor
|
||||||
|
const {selection} = state
|
||||||
|
const {$from} = selection
|
||||||
|
if ($from.parentOffset !== 0) return false
|
||||||
|
const node = $from.node()
|
||||||
|
if (!this.options.types.includes(node.type.name)) return false
|
||||||
|
const indent = node.attrs['indent'] || 0
|
||||||
|
if (indent > 0) {
|
||||||
|
return this.editor.commands.decreaseIndent()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import {Extension} from '@tiptap/core'
|
||||||
|
import {Plugin, PluginKey} from '@tiptap/pm/state'
|
||||||
|
import {Decoration, DecorationSet} from '@tiptap/pm/view'
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
selection: {
|
||||||
|
setSelection: () => ReturnType
|
||||||
|
clearSelection: () => ReturnType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Extension.create({
|
||||||
|
name: 'selection',
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
class: 'selection',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addStorage() {
|
||||||
|
return {
|
||||||
|
fakeRange: null as { from: number, to: number } | null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey('selection'),
|
||||||
|
props: {
|
||||||
|
decorations: (state) => {
|
||||||
|
const {fakeRange} = this.storage
|
||||||
|
if (!fakeRange) return null
|
||||||
|
const deco = Decoration.inline(
|
||||||
|
fakeRange.from,
|
||||||
|
fakeRange.to,
|
||||||
|
{class: this.options.class},
|
||||||
|
)
|
||||||
|
return DecorationSet.create(state.doc, [deco])
|
||||||
|
},
|
||||||
|
handleClick: (view, pos, event) => {
|
||||||
|
const { fakeRange } = this.storage
|
||||||
|
if (!fakeRange) return false
|
||||||
|
|
||||||
|
const clickedInsideFake =
|
||||||
|
pos >= fakeRange.from && pos <= fakeRange.to
|
||||||
|
|
||||||
|
if (!clickedInsideFake) {
|
||||||
|
// 点击了 fake selection 外部,清除它
|
||||||
|
this.storage.fakeRange = null
|
||||||
|
view.dispatch(view.state.tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setSelection: () => ({state, view}) => {
|
||||||
|
const {from, to, empty} = state.selection
|
||||||
|
if (!empty && from !== to) {
|
||||||
|
this.storage.fakeRange = {from, to}
|
||||||
|
view.dispatch(state.tr)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
clearSelection: () => ({state, view}) => {
|
||||||
|
if (this.storage.fakeRange) {
|
||||||
|
this.storage.fakeRange = null
|
||||||
|
view.dispatch(state.tr)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
<template>
|
||||||
|
<NodeViewWrapper class="table-wrapper" @contextmenu.prevent="showMenu($event)">
|
||||||
|
<NodeViewContent as="table" v-bind="props.node.attrs" :class="isSelected ? 'ProseMirror-selectednode' : ''"/>
|
||||||
|
<el-dropdown ref="tableDropdownRef" trigger="contextmenu" :teleported="false">
|
||||||
|
<span></span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu ref="tableMenu" class="dropdown">
|
||||||
|
<el-dropdown-item @click="editor.commands.mergeCells()" :disabled="!editor.can().mergeCells()">
|
||||||
|
合并单元格
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="editor.commands.addRowBefore()" :disabled="!editor.can().addRowBefore()" divided>
|
||||||
|
上加一行
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="editor.commands.addRowAfter()" :disabled="!editor.can().addRowAfter()">
|
||||||
|
下加一行
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="editor.commands.addColumnBefore()"
|
||||||
|
:disabled="!editor.can().addColumnBefore()">左加一列
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="editor.commands.addColumnAfter()"
|
||||||
|
:disabled="!editor.can().addColumnAfter()">右加一列
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="editor.commands.deleteRow()" :disabled="!editor.can().deleteRow()" divided>删除行
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="editor.commands.deleteColumn()" :disabled="!editor.can().deleteColumn()">
|
||||||
|
删除列
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</NodeViewWrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {NodeViewWrapper, NodeViewContent, NodeViewProps} from '@tiptap/vue-3'
|
||||||
|
import {ref, computed} from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<NodeViewProps>()
|
||||||
|
import {NodeSelection} from 'prosemirror-state'
|
||||||
|
|
||||||
|
const tableDropdownRef = ref(null)
|
||||||
|
const tableMenu = ref(null)
|
||||||
|
|
||||||
|
const isSelected = computed(() => {
|
||||||
|
const {selection} = props.editor.state
|
||||||
|
|
||||||
|
if (selection instanceof NodeSelection && selection.node.type.name === 'table') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const $from = selection.$from
|
||||||
|
let inTable = false
|
||||||
|
for (let depth = $from.depth; depth > 0; depth--) {
|
||||||
|
const node = $from.node(depth)
|
||||||
|
if (node.type.name === 'table') {
|
||||||
|
inTable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inTable
|
||||||
|
})
|
||||||
|
|
||||||
|
function showMenu(e) {
|
||||||
|
const _menu = tableDropdownRef.value.$el
|
||||||
|
_menu.style.position = 'absolute';
|
||||||
|
// 点位置+边框
|
||||||
|
_menu.style.left = e.layerX + 'px'
|
||||||
|
// 组件高度 + 工具栏高度 + 偏移
|
||||||
|
_menu.style.top = e.layerY + 'px'
|
||||||
|
tableDropdownRef.value.handleOpen()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.table-wrapper {
|
||||||
|
position: relative;
|
||||||
|
:deep(.el-dropdown-menu) {
|
||||||
|
list-style-type: disc;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-popper) {
|
||||||
|
line-height: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
import {VueNodeViewRenderer} from "@tiptap/vue-3";
|
||||||
|
import {NodeSelection} from 'prosemirror-state'
|
||||||
|
import {Table} from '@tiptap/extension-table'
|
||||||
|
import Action from "./Action.vue"
|
||||||
|
|
||||||
|
export default Table.extend({
|
||||||
|
selectable: true,
|
||||||
|
atom: true,
|
||||||
|
// 添加属性配置
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
width: {
|
||||||
|
default: '100%',
|
||||||
|
renderHTML: attributes => {
|
||||||
|
return {
|
||||||
|
width: attributes.width,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
default: null,
|
||||||
|
renderHTML: attributes => {
|
||||||
|
return {
|
||||||
|
class: attributes.class,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
default: 'table-layout: fixed;border-collapse: collapse;',
|
||||||
|
renderHTML: attributes => {
|
||||||
|
if (!attributes.style) return {}
|
||||||
|
return {
|
||||||
|
style: attributes.style,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderHTML({node, HTMLAttributes}) {
|
||||||
|
return ['table', HTMLAttributes, ['tbody', 0]]
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
Backspace: ({editor}) => {
|
||||||
|
const {state, view} = editor
|
||||||
|
const {selection} = state
|
||||||
|
|
||||||
|
// 没有深度直接删除
|
||||||
|
const {$from} = selection
|
||||||
|
if ($from.depth == 0) {
|
||||||
|
editor.commands.deleteSelection()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中整个表格删除
|
||||||
|
if (selection instanceof NodeSelection && selection.node.type.name === 'table') {
|
||||||
|
const tr = state.tr.delete(selection.from, selection.to)
|
||||||
|
view.dispatch(tr)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const pos = $from.before($from.depth) // 当前段落的开始位置
|
||||||
|
const index = $from.index($from.depth - 1)
|
||||||
|
|
||||||
|
if (index == 0) return false
|
||||||
|
const parent = $from.node($from.depth - 1)
|
||||||
|
|
||||||
|
// 查前一个节点是不是 table
|
||||||
|
const beforeNode = parent.child(index - 1)
|
||||||
|
|
||||||
|
if (beforeNode?.type.name === 'table') {
|
||||||
|
const deletePos = pos - beforeNode.nodeSize
|
||||||
|
view.dispatch(
|
||||||
|
state.tr
|
||||||
|
.setSelection(NodeSelection.create(state.doc, deletePos))
|
||||||
|
.deleteSelection()
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
Delete: ({editor}) => {
|
||||||
|
const {state, view} = editor
|
||||||
|
const {selection} = state
|
||||||
|
|
||||||
|
// 没有深度直接删除
|
||||||
|
const {$from} = selection
|
||||||
|
if ($from.depth == 0) {
|
||||||
|
editor.commands.deleteSelection()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
//选中整个表格删除
|
||||||
|
if (selection instanceof NodeSelection && selection.node.type.name === 'table') {
|
||||||
|
const tr = state.tr.delete(selection.from, selection.to)
|
||||||
|
view.dispatch(tr)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在表格前删除
|
||||||
|
const pos = $from.before($from.depth) // 当前段落的开始位置
|
||||||
|
const index = $from.index($from.depth - 1)
|
||||||
|
const parent = $from.node($from.depth - 1)
|
||||||
|
|
||||||
|
if (index < parent.childCount - 1) {
|
||||||
|
const afterNode = parent.child(index + 1)
|
||||||
|
if (afterNode?.type.name === 'table') {
|
||||||
|
const deletePos = pos + $from.node().nodeSize
|
||||||
|
view.dispatch(
|
||||||
|
state.tr
|
||||||
|
.setSelection(NodeSelection.create(state.doc, deletePos))
|
||||||
|
.deleteSelection()
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index == 0) return false
|
||||||
|
const beforeNode = parent.child(index - 1)
|
||||||
|
if (beforeNode?.type.name === 'table') {
|
||||||
|
const deletePos = pos - beforeNode.nodeSize
|
||||||
|
view.dispatch(
|
||||||
|
state.tr
|
||||||
|
.setSelection(NodeSelection.create(state.doc, deletePos))
|
||||||
|
.deleteSelection()
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addNodeView() {
|
||||||
|
return VueNodeViewRenderer(Action)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
<template>
|
||||||
|
<NodeViewWrapper as="p" class="video-wrapper">
|
||||||
|
<div class="video-box" ref="boxRef" @click="handleClick">
|
||||||
|
<NodeViewContent as="video" v-bind="props.node.attrs" controls/>
|
||||||
|
<div v-if="selected" class="video-menu">
|
||||||
|
<button @click="deleteVideo">删除</button>
|
||||||
|
<button @click="replaceVideo">配置</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-dialog v-model="settingShow" title="配置" width="500" align-center>
|
||||||
|
<el-form v-model="form" label-position="top">
|
||||||
|
<el-form-item label="地址" prop="src">
|
||||||
|
<el-input v-model="form.src"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="宽高" prop="width">
|
||||||
|
<el-row :gutter="10" style="margin: 10px 0; width: 100%;">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-input v-model="form.width">
|
||||||
|
<template #suffix>宽 </template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-input v-model="form.height">
|
||||||
|
<template #suffix>高 </template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="settingShow = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submit">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</NodeViewWrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {NodeViewWrapper, NodeViewProps, NodeViewContent} from '@tiptap/vue-3'
|
||||||
|
import {computed, ref} from "vue"
|
||||||
|
|
||||||
|
const props = defineProps<NodeViewProps>();
|
||||||
|
const selected = computed(() => props.selected)
|
||||||
|
|
||||||
|
let settingShow = ref(false)
|
||||||
|
let form = ref({
|
||||||
|
src: '',
|
||||||
|
width: '',
|
||||||
|
height: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
function deleteVideo() {
|
||||||
|
props.deleteNode()
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceVideo() {
|
||||||
|
form.value.src = props.node.attrs.src
|
||||||
|
form.value.width = props.node.attrs.width
|
||||||
|
form.value.height = props.node.attrs.height
|
||||||
|
settingShow.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
props.editor.commands.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
settingShow.value = false
|
||||||
|
props.updateAttributes(form.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.video-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-box video {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作菜单 */
|
||||||
|
.video-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-menu button {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
border: none;
|
||||||
|
padding: 4px 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
// extensions/Video.ts
|
||||||
|
import {Node, mergeAttributes, Command, CommandProps} from '@tiptap/core'
|
||||||
|
import {VueNodeViewRenderer} from "@tiptap/vue-3";
|
||||||
|
import Action from "./Action.vue";
|
||||||
|
|
||||||
|
export interface VideoOptions {
|
||||||
|
HTMLAttributes: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
video: {
|
||||||
|
setVideo: (options: { src: string }) => ReturnType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Node.create<VideoOptions>({
|
||||||
|
name: 'video',
|
||||||
|
group: 'block',
|
||||||
|
atom: true,
|
||||||
|
selectable: true,
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
src: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
default: '100%',
|
||||||
|
renderHTML: attributes => {
|
||||||
|
return {
|
||||||
|
width: attributes.width,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
default: null,
|
||||||
|
renderHTML: attributes => {
|
||||||
|
return {
|
||||||
|
class: attributes.class,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
default: '',
|
||||||
|
renderHTML: attributes => {
|
||||||
|
if (!attributes.style) return {}
|
||||||
|
return {
|
||||||
|
style: attributes.style,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'video',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
renderHTML({HTMLAttributes}) {
|
||||||
|
return [
|
||||||
|
'video',
|
||||||
|
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||||
|
controls: true,
|
||||||
|
}),
|
||||||
|
['source', {src: HTMLAttributes.src, type: 'video/mp4'}],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setVideo: (attrs: { src: string }): Command => ({commands}: CommandProps) => {
|
||||||
|
return commands.insertContent({
|
||||||
|
type: this.name,
|
||||||
|
attrs,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addNodeView() {
|
||||||
|
return VueNodeViewRenderer(Action)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,569 @@
|
||||||
|
<template>
|
||||||
|
<div class="pi-editor">
|
||||||
|
<div class="toolbar" ref="toolbarRef">
|
||||||
|
<div class="box">
|
||||||
|
<div class="group">
|
||||||
|
<el-button text circle title="撤销" class="item"
|
||||||
|
@click="editor.chain().focus().undo().run()"
|
||||||
|
:disabled="!editor.can().undo()">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-arrow-go-back'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button text circle title="重做" class="item"
|
||||||
|
@click="editor.chain().focus().redo().run()"
|
||||||
|
:disabled="!editor.can().redo()">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-arrow-go-forward'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="group">
|
||||||
|
<el-color-picker v-model="fontColor" ref="colorPickerRef"
|
||||||
|
@change="editor.chain().focus().setColor(fontColor).run()"
|
||||||
|
:predefine="predefineColors" :teleported="false" :show-alpha="true"
|
||||||
|
class="color-picker"></el-color-picker>
|
||||||
|
<el-color-picker v-model="bgColor" ref="bgPickerRef"
|
||||||
|
@change="bgColor ? editor.chain().focus().toggleHighlight({ color: bgColor }).run() : editor.chain().focus().unsetHighlight().run()"
|
||||||
|
:predefine="predefineColors" :teleported="false" :show-alpha="true"
|
||||||
|
class="color-picker"></el-color-picker>
|
||||||
|
<el-button text circle title="文字颜色" class="item"
|
||||||
|
@click="colorPickerRef.show()">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-font-color'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button text circle title="背景色" class="item"
|
||||||
|
@click="bgPickerRef.show()">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-background-color'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button text circle title="粗体" class="item"
|
||||||
|
@click="editor.chain().focus().toggleBold().run()"
|
||||||
|
:class="{ 'active': editor.isActive('bold') }">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-bold'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button text circle title="斜体" class="item"
|
||||||
|
@click="editor.chain().focus().toggleItalic().run()"
|
||||||
|
:class="{ 'active': editor.isActive('italic') }">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-italic'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button text circle title="下划线" class="item"
|
||||||
|
@click="editor.chain().focus().toggleUnderline().run()"
|
||||||
|
:class="{ 'active': editor.isActive('underline') }">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-underline'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button text circle title="删除线" class="item"
|
||||||
|
@click="editor.chain().focus().toggleStrike().run()"
|
||||||
|
:class="{ 'active': editor.isActive('strike') }">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-strikethrough'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="group">
|
||||||
|
<el-button text circle title="左对齐" class="item"
|
||||||
|
:class="{ 'active': editor.isActive({ textAlign: 'left' }) }"
|
||||||
|
@click="editor.isActive({ textAlign: 'left' }) ? editor.chain().focus().unsetTextAlign().run() : editor.chain().focus().setTextAlign('left').run()">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-align-left'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button text circle title="居中对齐" class="item"
|
||||||
|
:class="{ 'active': editor.isActive({ textAlign: 'center' }) }"
|
||||||
|
@click="editor.isActive({ textAlign: 'center' }) ? editor.chain().focus().unsetTextAlign().run() : editor.chain().focus().setTextAlign('center').run()">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-align-center'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button text circle title="右对齐" class="item"
|
||||||
|
:class="{ 'active': editor.isActive({ textAlign: 'right' }) }"
|
||||||
|
@click="editor.isActive({ textAlign: 'right' }) ? editor.chain().focus().unsetTextAlign().run() : editor.chain().focus().setTextAlign('right').run()">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-align-right'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button text circle title="两端对齐" class="item"
|
||||||
|
:class="{ 'active': editor.isActive({ textAlign: 'justify' }) }"
|
||||||
|
@click="editor.isActive({ textAlign: 'justify' }) ? editor.chain().focus().unsetTextAlign().run() : editor.chain().focus().setTextAlign('justify').run()">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-align-justify'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="group">
|
||||||
|
<el-button text circle title="减少缩进" class="item"
|
||||||
|
@click="editor.chain().focus().decreaseIndent().run()"
|
||||||
|
:disabled="editor.can().decreaseIndent()">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-indent-decrease'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button text circle title="增加缩进" class="item"
|
||||||
|
@click="editor.chain().focus().increaseIndent().run()"
|
||||||
|
:disabled="editor.can().increaseIndent()">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-indent-increase'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="group">
|
||||||
|
<el-button text circle title="有序列表" class="item"
|
||||||
|
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||||
|
:class="{ 'active': editor.isActive('orderedList') }">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-list-ordered'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button text circle title="无序列表" class="item"
|
||||||
|
@click="editor.chain().focus().toggleBulletList().run()"
|
||||||
|
:class="{ 'active': editor.isActive('bulletList') }">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-list-unordered'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="group">
|
||||||
|
<el-popover :visible="tableVisible" :width="220">
|
||||||
|
<el-row style="margin: 10px 0;" :gutter="10">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-input v-model="tableRow">
|
||||||
|
<template #suffix>行 </template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-input v-model="tableCol">
|
||||||
|
<template #suffix>列 </template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<div style="text-align: right; margin: 0">
|
||||||
|
<el-button size="small" text @click="tableVisible = false">取消</el-button>
|
||||||
|
<el-button size="small" type="primary" @click="insertTable">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<template #reference>
|
||||||
|
<el-button text circle title="表格" class="item"
|
||||||
|
@click="tableVisible = true">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-table'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
<el-button text circle title="图片" class="item"
|
||||||
|
@click="showPicker1">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-image'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button text circle title="视频" class="item"
|
||||||
|
@click="showPicker2">
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-video'"/>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="group">
|
||||||
|
<el-select v-model="tag" class="item" style="width:130px" clearable @change="tagChange">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-heading'"/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
<el-option label="段落" value="p">
|
||||||
|
<template #default>
|
||||||
|
<p>段落</p>
|
||||||
|
</template>
|
||||||
|
</el-option>
|
||||||
|
<el-option label="H1" value="h1">
|
||||||
|
<template #default>
|
||||||
|
<h1>H1</h1>
|
||||||
|
</template>
|
||||||
|
</el-option>
|
||||||
|
<el-option label="H2" value="h2">
|
||||||
|
<template #default>
|
||||||
|
<h2>H2</h2>
|
||||||
|
</template>
|
||||||
|
</el-option>
|
||||||
|
<el-option label="H3" value="h3">
|
||||||
|
<template #default>
|
||||||
|
<h3>H3</h3>
|
||||||
|
</template>
|
||||||
|
</el-option>
|
||||||
|
<el-option label="H4" value="h4">
|
||||||
|
<template #default>
|
||||||
|
<h4>H4</h4>
|
||||||
|
</template>
|
||||||
|
</el-option>
|
||||||
|
<el-option label="H5" value="h5">
|
||||||
|
<template #default>
|
||||||
|
<h5>H5</h5>
|
||||||
|
</template>
|
||||||
|
</el-option>
|
||||||
|
<el-option label="H6" value="h6">
|
||||||
|
<template #default>
|
||||||
|
<h6>H6</h6>
|
||||||
|
</template>
|
||||||
|
</el-option>
|
||||||
|
<el-option label="预格式化" value="pre">
|
||||||
|
<template #default>
|
||||||
|
<pre>预格式化</pre>
|
||||||
|
</template>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="fontSize" class="item" style="width:130px" clearable allow-create filterable
|
||||||
|
@change="editor.chain().focus().setMark('textStyle', { fontSize: fontSize }).run()">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon size="20px">
|
||||||
|
<component :is="'pi-icon-font-size'"/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
<el-option label="12px" value="12px"/>
|
||||||
|
<el-option label="14px" value="14px"/>
|
||||||
|
<el-option label="16px" value="16px"/>
|
||||||
|
<el-option label="18px" value="18px"/>
|
||||||
|
<el-option label="22px" value="22px"/>
|
||||||
|
<el-option label="24px" value="24px"/>
|
||||||
|
<el-option label="36px" value="36px"/>
|
||||||
|
<el-option label="72px" value="72px"/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<editor-content class="body" :editor="editor"/>
|
||||||
|
<pi-asset-picker ref="pickerRef1" v-if="pickerVisible1" @success="saveSuccess1" @closed="pickerVisible1=false"
|
||||||
|
type="image" :max="30" :multiple="true"></pi-asset-picker>
|
||||||
|
<pi-asset-picker ref="pickerRef2" v-if="pickerVisible2" @success="saveSuccess2" @closed="pickerVisible2=false"
|
||||||
|
type="video" :max="1"></pi-asset-picker>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import piAssetPicker from '@/components/piAsset/picker'
|
||||||
|
import {Editor, EditorContent} from "@tiptap/vue-3";
|
||||||
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
|
import TextAlign from "@tiptap/extension-text-align"
|
||||||
|
import Underline from '@tiptap/extension-underline'
|
||||||
|
import Highlight from '@tiptap/extension-highlight'
|
||||||
|
import Color from '@tiptap/extension-color'
|
||||||
|
import TableRow from '@tiptap/extension-table-row'
|
||||||
|
import TableCell from '@tiptap/extension-table-cell'
|
||||||
|
import TableHeader from '@tiptap/extension-table-header'
|
||||||
|
import Image from './ext/Image'
|
||||||
|
import Video from './ext/Video'
|
||||||
|
import Indent from './ext/Indent'
|
||||||
|
import FontSize from './ext/FontSize'
|
||||||
|
import Table from './ext/Table'
|
||||||
|
import Selection from "./ext/Selection";
|
||||||
|
|
||||||
|
import {ref, onUnmounted, watch, nextTick, onMounted} from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {type: String, default: ''},
|
||||||
|
})
|
||||||
|
|
||||||
|
const pickerRef1 = ref(null)
|
||||||
|
const pickerRef2 = ref(null)
|
||||||
|
const colorPickerRef = ref(null)
|
||||||
|
const bgPickerRef = ref(null)
|
||||||
|
const toolbarRef = ref(null)
|
||||||
|
const pickerVisible1 = ref(false)
|
||||||
|
const pickerVisible2 = ref(false)
|
||||||
|
|
||||||
|
const predefineColors = ref([
|
||||||
|
'#FFFFFF',
|
||||||
|
'#000000',
|
||||||
|
'#409EFF',
|
||||||
|
'#67C23A',
|
||||||
|
'#E6A23C',
|
||||||
|
'#F56C6C',
|
||||||
|
'#909399',
|
||||||
|
'#303133',
|
||||||
|
'#CDD0D6',
|
||||||
|
'#E6E8EB',
|
||||||
|
'#606266',
|
||||||
|
'#EBEDF0'
|
||||||
|
])
|
||||||
|
let editor = ref(null)
|
||||||
|
let fontSize = ref('12px')
|
||||||
|
let fontColor = ref('')
|
||||||
|
let bgColor = ref('')
|
||||||
|
let tag = ref('')
|
||||||
|
let tableVisible = ref(false)
|
||||||
|
let tableRow = ref(2)
|
||||||
|
let tableCol = ref(3)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (value) => {
|
||||||
|
const isSame = editor.value.getHTML() === value
|
||||||
|
if (isSame) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
editor.value.commands.setContent(value, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 保存 fakeSelection
|
||||||
|
editor.value?.on('selectionUpdate', () => {
|
||||||
|
editor.value?.commands.setSelection()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 点击外部仍保留背景
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const el = document.querySelector('.ProseMirror')
|
||||||
|
if (!el?.contains(e.target)){
|
||||||
|
// 只是触发 view 更新,让装饰生效
|
||||||
|
editor.value?.view.dispatch(editor.value.state.tr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 点击内部清除假选中
|
||||||
|
document.querySelector('.ProseMirror')?.addEventListener('click', () => {
|
||||||
|
editor.value?.commands.clearSelection()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
editor.value.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.value = new Editor({
|
||||||
|
content: props.modelValue,
|
||||||
|
extensions: [
|
||||||
|
StarterKit,
|
||||||
|
Underline,
|
||||||
|
TextAlign.configure({
|
||||||
|
types: ['heading', 'paragraph'],
|
||||||
|
}),
|
||||||
|
Indent,
|
||||||
|
Highlight.configure({
|
||||||
|
multicolor: true, // 支持多种背景色
|
||||||
|
}),
|
||||||
|
Color,
|
||||||
|
FontSize,
|
||||||
|
Table,
|
||||||
|
TableRow,
|
||||||
|
TableCell.configure({
|
||||||
|
HTMLAttributes: {
|
||||||
|
style: 'border: 1px solid #ccc;padding: 0.4rem;'
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
TableHeader.configure({
|
||||||
|
HTMLAttributes: {
|
||||||
|
style: 'border: 1px solid #ccc;padding: 0.4rem;'
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Image.configure({
|
||||||
|
inline: false,
|
||||||
|
allowBase64: true,
|
||||||
|
}),
|
||||||
|
Video.configure({
|
||||||
|
inline: false,
|
||||||
|
allowBase64: true,
|
||||||
|
}),
|
||||||
|
Selection
|
||||||
|
],
|
||||||
|
onUpdate: () => {
|
||||||
|
emit('update:modelValue', editor.value.getHTML())
|
||||||
|
},
|
||||||
|
onSelectionUpdate: () => {
|
||||||
|
// 字体大小回显
|
||||||
|
let _fontSize = editor.value.getAttributes('textStyle').fontSize
|
||||||
|
fontSize.value = _fontSize ? _fontSize : '12px'
|
||||||
|
|
||||||
|
// 标签回显
|
||||||
|
if (editor.value.isActive('paragraph')) {
|
||||||
|
tag.value = 'p'
|
||||||
|
} else if (editor.value.isActive('heading', {level: 1})) {
|
||||||
|
tag.value = 'h1'
|
||||||
|
} else if (editor.value.isActive('heading', {level: 2})) {
|
||||||
|
tag.value = 'h2'
|
||||||
|
} else if (editor.value.isActive('heading', {level: 3})) {
|
||||||
|
tag.value = 'h3'
|
||||||
|
} else if (editor.value.isActive('heading', {level: 4})) {
|
||||||
|
tag.value = 'h4'
|
||||||
|
} else if (editor.value.isActive('heading', {level: 5})) {
|
||||||
|
tag.value = 'h5'
|
||||||
|
} else if (editor.value.isActive('heading', {level: 6})) {
|
||||||
|
tag.value = 'h6'
|
||||||
|
} else if (editor.value.isActive('codeBlock')) {
|
||||||
|
tag.value = 'pre'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCreate: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// 初始化光标
|
||||||
|
const firstNode = editor.value.state.doc.content.firstChild
|
||||||
|
const from1 = firstNode?.content?.content?.[0]?.nodeSize ? 1 : 0
|
||||||
|
const to1 = from1 + (firstNode?.nodeSize || 0) - 2
|
||||||
|
editor.value.commands.setTextSelection({from1, to1})
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function tagChange() {
|
||||||
|
if (tag.value === 'p') {
|
||||||
|
editor.value.chain().focus().setParagraph().run()
|
||||||
|
} else if (tag.value === 'h1') {
|
||||||
|
editor.value.chain().focus().setHeading({level: 1}).run()
|
||||||
|
} else if (tag.value === 'h2') {
|
||||||
|
editor.value.chain().focus().setHeading({level: 2}).run()
|
||||||
|
} else if (tag.value === 'h3') {
|
||||||
|
editor.value.chain().focus().setHeading({level: 3}).run()
|
||||||
|
} else if (tag.value === 'h4') {
|
||||||
|
editor.value.chain().focus().setHeading({level: 4}).run()
|
||||||
|
} else if (tag.value === 'h5') {
|
||||||
|
editor.value.chain().focus().setHeading({level: 5}).run()
|
||||||
|
} else if (tag.value === 'h6') {
|
||||||
|
editor.value.chain().focus().setHeading({level: 6}).run()
|
||||||
|
} else if (tag.value === 'pre') {
|
||||||
|
editor.value.chain().focus().setCodeBlock().run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertTable() {
|
||||||
|
editor.value.chain().focus().insertTable({rows: tableRow.value, cols: tableCol.value}).run()
|
||||||
|
tableVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPicker1() {
|
||||||
|
pickerVisible1.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
pickerRef1.value.open()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPicker2() {
|
||||||
|
pickerVisible2.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
pickerRef2.value.open()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSuccess1(val) {
|
||||||
|
val.forEach(src => {
|
||||||
|
editor.value.chain().focus().setImage({
|
||||||
|
src: src,
|
||||||
|
alt: ''
|
||||||
|
}).run()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSuccess2(val) {
|
||||||
|
editor.value.chain().focus().setVideo({src: val}).run()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pi-editor {
|
||||||
|
border: 2px solid #eee;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
visibility: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-editor .toolbar .box {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: none;
|
||||||
|
box-shadow: 0 2px 2px -2px rgba(34, 47, 62, .1), 0 8px 8px -4px rgba(34, 47, 62, .07);
|
||||||
|
padding: 4px 0;
|
||||||
|
transition: box-shadow .5s;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-editor .toolbar .box .group {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-editor .toolbar .box .group .item + .item {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.toolbar .box .group .item.color .el-color-picker__trigger) {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.group .color-picker) {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-editor .toolbar .box .group .el-button.active {
|
||||||
|
background: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-editor .toolbar .box .group .el-button.active .el-icon {
|
||||||
|
color: var(--el-fill-color-blank);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-editor .toolbar .box .group .el-button .el-icon {
|
||||||
|
color: var(--el-color-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-editor .toolbar .box .group .el-button.is-disabled .el-icon {
|
||||||
|
color: var(--el-button-disabled-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-editor .body {
|
||||||
|
margin: 1em;
|
||||||
|
height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
resize: vertical;
|
||||||
|
|
||||||
|
:deep(ul) {
|
||||||
|
list-style-type: disc;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(ol) {
|
||||||
|
list-style-type: decimal;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(li) {
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ProseMirror-selectednode) {
|
||||||
|
border: 3px solid #b4d7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.selectedCell) {
|
||||||
|
background-color: rgba(66, 185, 131, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.selection) {
|
||||||
|
background-color: rgba(180, 213, 255, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<div class="pi-icon-select__wrapper" :class="{'hasValue':value}" @click="open">
|
<div class="pi-icon-select__wrapper" :class="{'hasValue':value}" @click="open">
|
||||||
<el-input :prefix-icon="value||'el-icon-plus'" v-model="value" :disabled="disabled" readonly></el-input>
|
<el-input :prefix-icon="value||'el-icon-plus'" v-model="value" :disabled="disabled" readonly></el-input>
|
||||||
</div>
|
</div>
|
||||||
|
<el-text style="margin-left: 5px;">{{value}}</el-text>
|
||||||
<el-dialog title="图标选择器" v-model="dialogVisible" :width="760" destroy-on-close append-to-body>
|
<el-dialog title="图标选择器" v-model="dialogVisible" :width="760" destroy-on-close append-to-body>
|
||||||
<div class="pi-icon-select__dialog">
|
<div class="pi-icon-select__dialog">
|
||||||
<el-form :rules="{}">
|
<el-form :rules="{}">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
<template>
|
||||||
|
<el-dialog title="经纬度选择" v-model="visible" :width="850" destroy-on-close @closed="$emit('closed')" :close-on-click-modal="true">
|
||||||
|
<div class="map">
|
||||||
|
<el-input v-model="address" placeholder="搜索地名" @change="searchMapHandle" style="width: 300px;"/>
|
||||||
|
<el-button type="primary" style="margin-left: 20px" @click="searchMapHandle">搜索</el-button>
|
||||||
|
<el-button type="primary" style="margin-left: 10px" @click="submit">确定</el-button>
|
||||||
|
<div id="map-container">
|
||||||
|
<div class="input">
|
||||||
|
<el-input v-model="location.lng" size="small" placeholder="经度"></el-input>
|
||||||
|
<el-input v-model="location.lat" size="small" placeholder="纬度"></el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import AMapLoader from '@amap/amap-jsapi-loader';
|
||||||
|
import {ref} from "vue";
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open,
|
||||||
|
setData
|
||||||
|
})
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "piMap"
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['success', 'closed'])
|
||||||
|
let AMap = ref(null)
|
||||||
|
let location = ref({
|
||||||
|
lat: '',
|
||||||
|
lng: ''
|
||||||
|
})
|
||||||
|
let markers = ref([])
|
||||||
|
let map = ref(null)
|
||||||
|
let visible = ref(false)
|
||||||
|
let address = ref("")
|
||||||
|
|
||||||
|
//显示
|
||||||
|
function open(){
|
||||||
|
visible.value = true;
|
||||||
|
initMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMap(){
|
||||||
|
window._AMapSecurityConfig = {
|
||||||
|
securityJsCode: "1d4ec5360562473b31ffd4925995fe5c"
|
||||||
|
}
|
||||||
|
AMapLoader.load({
|
||||||
|
key: "90960616a710a55eebb2cdfb0a959ede",
|
||||||
|
version:"2.0",
|
||||||
|
plugins: ['AMap.Scale', 'AMap.PlaceSearch', 'AMap.AutoComplete']
|
||||||
|
}).then((a)=>{
|
||||||
|
map.value = new a.Map("map-container",{
|
||||||
|
viewMode: "3D",
|
||||||
|
zoom: 15
|
||||||
|
})
|
||||||
|
// 地图点击事件
|
||||||
|
map.value.on('click', clickMapHandler)
|
||||||
|
AMap.value = a
|
||||||
|
searchMapHandle()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击地图事件
|
||||||
|
function clickMapHandler(e) {
|
||||||
|
location.value.lng = e.lnglat.getLng()
|
||||||
|
location.value.lat = e.lnglat.getLat()
|
||||||
|
map.value.remove(markers.value)
|
||||||
|
let marker = new AMap.value.Marker({
|
||||||
|
position: [location.value.lng, location.value.lat],
|
||||||
|
});
|
||||||
|
marker.setMap(map.value)
|
||||||
|
markers.value.push(marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchMapHandle() {
|
||||||
|
AMap.value.plugin('AMap.PlaceSearch', function () {
|
||||||
|
const autoOptions = {
|
||||||
|
map: map.value,
|
||||||
|
pageSize: 1,
|
||||||
|
pageIndex: 1,
|
||||||
|
autoFitView: true
|
||||||
|
};
|
||||||
|
const placeSearch = new AMap.value.PlaceSearch(autoOptions);
|
||||||
|
placeSearch.search(address.value, function (status, result) {
|
||||||
|
if (status == 'complete') {
|
||||||
|
if (result.poiList.pois.length > 0) {
|
||||||
|
location.value.lng = result.poiList.pois[0].location.lng;
|
||||||
|
location.value.lat = result.poiList.pois[0].location.lat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
emit('success', location.value)
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setData(a) {
|
||||||
|
address.value = a
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.map #map-container {
|
||||||
|
padding: 0;
|
||||||
|
margin: 20px auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input {
|
||||||
|
width: 150px;
|
||||||
|
margin: 10px 0 0 10px;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
<template>
|
||||||
|
<div class="pi-video" ref="videoRef"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Player from 'xgplayer'
|
||||||
|
import HlsPlayer from 'xgplayer-hls'
|
||||||
|
import {ref, watch, onMounted} from "vue";
|
||||||
|
|
||||||
|
const videoRef = ref(null)
|
||||||
|
const props = defineProps({
|
||||||
|
src: {type: String, required: true, default: ""},
|
||||||
|
autoplay: {type: Boolean, default: false},
|
||||||
|
controls: {type: Boolean, default: true},
|
||||||
|
loop: {type: Boolean, default: false},
|
||||||
|
isLive: {type: Boolean, default: false},
|
||||||
|
options: {
|
||||||
|
type: Object, default: () => {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let player = ref(null)
|
||||||
|
|
||||||
|
watch(() => props.src, (val) => {
|
||||||
|
if (player.value.hasStart) {
|
||||||
|
player.value.src = val
|
||||||
|
} else {
|
||||||
|
player.value.start(val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
props.isLive ? initHls() : init()
|
||||||
|
})
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
player.value = new Player({
|
||||||
|
el: videoRef.value,
|
||||||
|
url: props.src,
|
||||||
|
autoplay: props.autoplay,
|
||||||
|
loop: props.loop,
|
||||||
|
controls: props.controls,
|
||||||
|
fluid: true,
|
||||||
|
lang: 'zh-cn',
|
||||||
|
...props.options
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function initHls() {
|
||||||
|
player.value = new HlsPlayer({
|
||||||
|
el: videoRef.value,
|
||||||
|
url: props.src,
|
||||||
|
autoplay: props.autoplay,
|
||||||
|
loop: props.loop,
|
||||||
|
controls: props.controls,
|
||||||
|
fluid: true,
|
||||||
|
isLive: true,
|
||||||
|
ignores: ['time', 'progress'],
|
||||||
|
lang: 'zh-cn',
|
||||||
|
...props.options
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pi-video:deep(.danmu) > * {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 1px 1px 0 #000, -1px -1px 0 #000, -1px 1px 0 #000, 1px -1px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-video:deep(.xgplayer-controls) {
|
||||||
|
background-image: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-video:deep(.xgplayer-progress-tip) {
|
||||||
|
border: 0;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0, 0, 0, .5);
|
||||||
|
line-height: 25px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-video:deep(.xgplayer-enter-spinner) {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<div class="pi-section">
|
||||||
|
<div class="title"><el-text size="large">{{title}}</el-text></div>
|
||||||
|
<el-alert v-if="tip" class="tip" show-icon type="info" :title="tip" :closable="false"></el-alert>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const prop = defineProps({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
tip: { type: String}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pi-section {margin-bottom: 20px;}
|
||||||
|
.pi-section .title {color: #333;font-weight: 700;font-size: 18px;position: relative;padding-left: 12px;margin-bottom: 20px;}
|
||||||
|
.pi-section .title:before { content: ""; width: 4px; height: 80%; background-color: var(--el-color-primary); position: absolute; left: 0; top: 50%; transform: translateY(-50%);}
|
||||||
|
.pi-section .tip {margin-bottom: 20px;}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
<template>
|
||||||
|
<el-select ref="selectRef" v-model="defaultValue" :size="size" :clearable="clearable" :multiple="multiple"
|
||||||
|
:collapse-tags="collapseTags" :collapse-tags-tooltip="collapseTagsTooltip" :filterable="filterable"
|
||||||
|
:placeholder="placeholder" :disabled="disabled" :filter-method="filterMethod" @remove-tag="removeTag"
|
||||||
|
@visible-change="visibleChange" @clear="clear">
|
||||||
|
<template #empty>
|
||||||
|
<div class="pi-table-select__table" :style="{width: tableWidth+'px'}" v-loading="loading">
|
||||||
|
<div class="pi-table-select__header">
|
||||||
|
<slot name="header" :form="formData" :submit="formSubmit"></slot>
|
||||||
|
</div>
|
||||||
|
<el-table ref="tableRef" :data="tableData" :height="245" :highlight-current-row="!multiple"
|
||||||
|
@row-click="click" @select="select" @select-all="selectAll">
|
||||||
|
<el-table-column v-if="multiple" type="selection" width="45"></el-table-column>
|
||||||
|
<el-table-column v-else type="index" width="45">
|
||||||
|
<template #default="scope"><span>{{ scope.$index + (currentPage - 1) * pageSize + 1 }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<slot></slot>
|
||||||
|
</el-table>
|
||||||
|
<div class="pi-table-select__page">
|
||||||
|
<el-pagination size="small" background layout="prev, pager, next" :total="total" :page-size="pageSize"
|
||||||
|
v-model:currentPage="currentPage" @current-change="reload"></el-pagination>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import config from '@/config/select'
|
||||||
|
import {ref, watch, onMounted, nextTick} from 'vue'
|
||||||
|
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: null,
|
||||||
|
apiObj: {
|
||||||
|
type: Function, default: () => {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
type: Object, default: () => {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
placeholder: {type: String, default: "请选择"},
|
||||||
|
size: {type: String, default: "default"},
|
||||||
|
clearable: {type: Boolean, default: false},
|
||||||
|
multiple: {type: Boolean, default: false},
|
||||||
|
filterable: {type: Boolean, default: false},
|
||||||
|
collapseTags: {type: Boolean, default: false},
|
||||||
|
collapseTagsTooltip: {type: Boolean, default: false},
|
||||||
|
disabled: {type: Boolean, default: false},
|
||||||
|
tableWidth: {type: Number, default: 400},
|
||||||
|
mode: {type: String, default: "popover"},
|
||||||
|
props: {
|
||||||
|
type: Object, default: () => {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableRef = ref(null)
|
||||||
|
const selectRef = ref(null)
|
||||||
|
let loading = ref(false)
|
||||||
|
let keyword = ref(null)
|
||||||
|
let defaultValue = ref([])
|
||||||
|
let tableData = ref([])
|
||||||
|
let pageSize = ref(config.pageSize)
|
||||||
|
let total = ref(0)
|
||||||
|
let currentPage = ref(1)
|
||||||
|
let defaultProps = ref({
|
||||||
|
label: config.props.label,
|
||||||
|
value: config.props.value,
|
||||||
|
page: config.request.page,
|
||||||
|
pageSize: config.request.pageSize,
|
||||||
|
keyword: config.request.keyword,
|
||||||
|
fields: []
|
||||||
|
})
|
||||||
|
let formData = ref({})
|
||||||
|
|
||||||
|
watch(() => props.modelValue, () => {
|
||||||
|
defaultValue.value = props.modelValue
|
||||||
|
autoCurrentLabel()
|
||||||
|
}, {deep: true})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
defaultProps.value = Object.assign(defaultProps.value, props.props);
|
||||||
|
defaultValue.value = props.modelValue
|
||||||
|
autoCurrentLabel()
|
||||||
|
})
|
||||||
|
|
||||||
|
//表格显示隐藏回调
|
||||||
|
function visibleChange(visible) {
|
||||||
|
if (visible) {
|
||||||
|
currentPage.value = 1
|
||||||
|
keyword.value = null
|
||||||
|
formData.value = {}
|
||||||
|
getData()
|
||||||
|
} else {
|
||||||
|
autoCurrentLabel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
loading.value = true;
|
||||||
|
var reqData = {
|
||||||
|
[defaultProps.value.page]: currentPage.value,
|
||||||
|
[defaultProps.value.pageSize]: pageSize.value,
|
||||||
|
[defaultProps.value.keyword]: keyword.value
|
||||||
|
}
|
||||||
|
Object.assign(reqData, props.params, formData.value)
|
||||||
|
var res = await props.apiObj(reqData);
|
||||||
|
var parseData = config.parseData(res)
|
||||||
|
tableData.value = parseData.rows;
|
||||||
|
total.value = parseData.total;
|
||||||
|
loading.value = false;
|
||||||
|
//表格默认赋值
|
||||||
|
nextTick(() => {
|
||||||
|
if (props.multiple) {
|
||||||
|
defaultValue.value.forEach(row => {
|
||||||
|
var setrow = tableData.value.filter(item => item[defaultProps.value.value] === row[defaultProps.value.value])
|
||||||
|
if (setrow.length > 0) {
|
||||||
|
tableRef.value.toggleRowSelection(setrow[0], true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
var setrow = tableData.value.filter(item => item[defaultProps.value.value] === defaultValue.value[defaultProps.value.value])
|
||||||
|
tableRef.value.setCurrentRow(setrow[0]);
|
||||||
|
}
|
||||||
|
tableRef.value.setScrollTop(0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//插糟表单提交
|
||||||
|
function formSubmit() {
|
||||||
|
currentPage.value = 1
|
||||||
|
keyword.value = null
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
//分页刷新表格
|
||||||
|
function reload() {
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
//自动模拟options赋值
|
||||||
|
function autoCurrentLabel() {
|
||||||
|
// nextTick(() => {
|
||||||
|
// if (props.multiple) {
|
||||||
|
// selectRef.value.selected.forEach(item => {
|
||||||
|
// item.currentLabel = item.value[defaultProps.value.label]
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// selectRef.value.selectedLabel = defaultValue.value[defaultProps.value.label]
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
|
||||||
|
//表格勾选事件
|
||||||
|
function select(rows, row) {
|
||||||
|
var isSelect = rows.length && rows.indexOf(row) !== -1
|
||||||
|
if (isSelect) {
|
||||||
|
defaultValue.value.push(row)
|
||||||
|
} else {
|
||||||
|
defaultValue.value.splice(defaultValue.value.findIndex(item => item[defaultProps.value.value] == row[defaultProps.value.value]), 1)
|
||||||
|
}
|
||||||
|
autoCurrentLabel()
|
||||||
|
// 解析数据
|
||||||
|
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
|
||||||
|
emit('change', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
|
||||||
|
}
|
||||||
|
|
||||||
|
//表格全选事件
|
||||||
|
function selectAll(rows) {
|
||||||
|
var isAllSelect = rows.length > 0
|
||||||
|
if (isAllSelect) {
|
||||||
|
rows.forEach(row => {
|
||||||
|
var isHas = defaultValue.value.find(item => item[defaultProps.value.value] == row[defaultProps.value.value])
|
||||||
|
if (!isHas) {
|
||||||
|
defaultValue.value.push(row)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.tableData.forEach(row => {
|
||||||
|
var isHas = defaultValue.value.find(item => item[defaultProps.value.value] == row[defaultProps.value.value])
|
||||||
|
if (isHas) {
|
||||||
|
defaultValue.value.splice(defaultValue.value.findIndex(item => item[defaultProps.value.value] == row[defaultProps.value.value]), 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
autoCurrentLabel()
|
||||||
|
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
|
||||||
|
emit('change', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
|
||||||
|
}
|
||||||
|
|
||||||
|
function click(row) {
|
||||||
|
if (props.multiple) {
|
||||||
|
//处理多选点击行
|
||||||
|
} else {
|
||||||
|
defaultValue.value = row
|
||||||
|
selectRef.value.blur()
|
||||||
|
autoCurrentLabel()
|
||||||
|
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
|
||||||
|
emit('change', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//tags删除后回调
|
||||||
|
function removeTag(tag) {
|
||||||
|
var row = findRowByKey(tag[defaultProps.value.value])
|
||||||
|
tableRef.value.toggleRowSelection(row, false);
|
||||||
|
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
|
||||||
|
}
|
||||||
|
|
||||||
|
//清空后的回调
|
||||||
|
function clear() {
|
||||||
|
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关键值查询表格数据行
|
||||||
|
function findRowByKey(value) {
|
||||||
|
return tableData.value.find(item => item[defaultProps.value.value] === value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterMethod(k) {
|
||||||
|
if (!k) {
|
||||||
|
keyword.value = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
keyword.value = k;
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发select隐藏
|
||||||
|
function blur() {
|
||||||
|
selectRef.value.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发select显示
|
||||||
|
function focus() {
|
||||||
|
selectRef.value.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pi-table-select__table {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-table-select__page {
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header v-if="!hideAct">
|
<div v-if="(hasSearchSlot || hasExtendSearchSlot) && show" class="extend">
|
||||||
|
<el-collapse-transition>
|
||||||
|
<div class="extend-panel">
|
||||||
|
<slot name="extend"></slot>
|
||||||
|
</div>
|
||||||
|
</el-collapse-transition>
|
||||||
|
</div>
|
||||||
|
<el-header v-if="hasSearchSlot || hasExtendSearchSlot || hasDoSlot">
|
||||||
<div class="left-panel">
|
<div class="left-panel">
|
||||||
<slot name="do"></slot>
|
<slot name="do"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-panel">
|
<div class="right-panel">
|
||||||
<slot name="search"></slot>
|
<slot name="search"></slot>
|
||||||
|
<el-button v-if="hasExtendSearchSlot" type="primary" icon="el-icon-filter" @click="show = !show" :plain="show"/>
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main class="nopadding">
|
<el-main class="nopadding">
|
||||||
|
|
@ -50,7 +58,8 @@
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-button icon="el-icon-set-up" circle style="margin-left:15px"></el-button>
|
<el-button icon="el-icon-set-up" circle style="margin-left:15px"></el-button>
|
||||||
</template>
|
</template>
|
||||||
<columnSetting v-if="customColumnShow" ref="columnSettingRef" @userChange="columnSettingChange"
|
<columnSetting v-if="customColumnShow" ref="columnSettingRef"
|
||||||
|
@userChange="columnSettingChange"
|
||||||
@save="columnSettingSave" @back="columnSettingBack"
|
@save="columnSettingSave" @back="columnSettingBack"
|
||||||
:column="userColumn"></columnSetting>
|
:column="userColumn"></columnSetting>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
|
|
@ -83,7 +92,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import columnSetting from './columnSetting'
|
import columnSetting from './columnSetting'
|
||||||
import config from "@/config/table"
|
import config from "@/config/table"
|
||||||
import {ref, watch, computed, onMounted, onActivated, onDeactivated, getCurrentInstance} from "vue"
|
import {ref, watch, computed, onMounted, onActivated, onDeactivated, getCurrentInstance, useSlots} from "vue"
|
||||||
import tools from "@/utils/tools.js";
|
import tools from "@/utils/tools.js";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|
@ -99,15 +108,22 @@ defineExpose({
|
||||||
const {proxy} = getCurrentInstance()
|
const {proxy} = getCurrentInstance()
|
||||||
const emit = defineEmits(['dataChange'])
|
const emit = defineEmits(['dataChange'])
|
||||||
|
|
||||||
|
const slots = useSlots()
|
||||||
|
const hasSearchSlot = computed(() => !!slots.search)
|
||||||
|
const hasExtendSearchSlot = computed(() => !!slots.extend)
|
||||||
|
const hasDoSlot = computed(() => !!slots.do)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
tableName: {type: String, default: ""},
|
tableName: {type: String, default: ""},
|
||||||
apiObj: {
|
apiObj: {
|
||||||
type: Function, default: () => {}
|
type: Function, default: () => {
|
||||||
|
}
|
||||||
},
|
},
|
||||||
workbench: {type: Boolean, default: false},
|
workbench: {type: Boolean, default: false},
|
||||||
params: {type: Object, default: () => ({})},
|
params: {type: Object, default: () => ({})},
|
||||||
data: {
|
data: {
|
||||||
type: Object, default: () => {}
|
type: Object, default: () => {
|
||||||
|
}
|
||||||
},
|
},
|
||||||
height: {type: [String, Number], default: "100%"},
|
height: {type: [String, Number], default: "100%"},
|
||||||
size: {type: String, default: "default"},
|
size: {type: String, default: "default"},
|
||||||
|
|
@ -118,14 +134,14 @@ const props = defineProps({
|
||||||
rowKey: {type: String, default: ""},
|
rowKey: {type: String, default: ""},
|
||||||
summaryMethod: {type: Function, default: null},
|
summaryMethod: {type: Function, default: null},
|
||||||
column: {
|
column: {
|
||||||
type: Object, default: () => {}
|
type: Object, default: () => {
|
||||||
|
}
|
||||||
},
|
},
|
||||||
remoteSort: {type: Boolean, default: false},
|
remoteSort: {type: Boolean, default: false},
|
||||||
remoteFilter: {type: Boolean, default: false},
|
remoteFilter: {type: Boolean, default: false},
|
||||||
remoteSummary: {type: Boolean, default: false},
|
remoteSummary: {type: Boolean, default: false},
|
||||||
hidePagination: {type: Boolean, default: false},
|
hidePagination: {type: Boolean, default: false},
|
||||||
hideDo: {type: Boolean, default: false},
|
hideDo: {type: Boolean, default: false},
|
||||||
hideAct: {type: Boolean, default: false},
|
|
||||||
hideRefresh: {type: Boolean, default: false},
|
hideRefresh: {type: Boolean, default: false},
|
||||||
hideSetting: {type: Boolean, default: false},
|
hideSetting: {type: Boolean, default: false},
|
||||||
paginationLayout: {type: String, default: config.paginationLayout},
|
paginationLayout: {type: String, default: config.paginationLayout},
|
||||||
|
|
@ -140,7 +156,7 @@ let emptyText = ref("暂无数据")
|
||||||
let toggleIndex = ref(0)
|
let toggleIndex = ref(0)
|
||||||
let tableData = ref([])
|
let tableData = ref([])
|
||||||
let total = ref(0)
|
let total = ref(0)
|
||||||
let currentPage = ref(0)
|
let currentPage = ref(1)
|
||||||
let prop = ref(null)
|
let prop = ref(null)
|
||||||
let order = ref(null)
|
let order = ref(null)
|
||||||
let loading = ref(false)
|
let loading = ref(false)
|
||||||
|
|
@ -155,6 +171,7 @@ let _config = ref({
|
||||||
stripe: props.stripe
|
stripe: props.stripe
|
||||||
})
|
})
|
||||||
let nowWork = ref(null)
|
let nowWork = ref(null)
|
||||||
|
let show = ref(false)
|
||||||
|
|
||||||
watch(() => props.data, () => {
|
watch(() => props.data, () => {
|
||||||
tableData.value = props.data;
|
tableData.value = props.data;
|
||||||
|
|
@ -253,30 +270,32 @@ async function getData() {
|
||||||
}
|
}
|
||||||
|
|
||||||
//分页点击
|
//分页点击
|
||||||
function paginationChange(){
|
function paginationChange() {
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
//条数变化
|
//条数变化
|
||||||
function pageSizeChange(size){
|
function pageSizeChange(size) {
|
||||||
piPageSize.value = size
|
piPageSize.value = size
|
||||||
getData()
|
getData()
|
||||||
}
|
}
|
||||||
|
|
||||||
//刷新数据
|
//刷新数据
|
||||||
function refresh(){
|
function refresh() {
|
||||||
piTableRef.value.clearSelection()
|
piTableRef.value.clearSelection()
|
||||||
getData()
|
getData()
|
||||||
}
|
}
|
||||||
|
|
||||||
//更新数据 合并上一次params
|
//更新数据 合并上一次params
|
||||||
function upData(params, page=1){
|
function upData(params, page = 1) {
|
||||||
currentPage.value = page;
|
currentPage.value = page;
|
||||||
piTableRef.value.clearSelection();
|
piTableRef.value.clearSelection();
|
||||||
Object.assign(tableParams.value, params || {})
|
Object.assign(tableParams.value, params || {})
|
||||||
getData()
|
getData()
|
||||||
}
|
}
|
||||||
|
|
||||||
//重载数据 替换params
|
//重载数据 替换params
|
||||||
function reload(params, page=1){
|
function reload(params, page = 1) {
|
||||||
currentPage.value = page;
|
currentPage.value = page;
|
||||||
tableParams.value = params || {}
|
tableParams.value = params || {}
|
||||||
piTableRef.value.clearSelection();
|
piTableRef.value.clearSelection();
|
||||||
|
|
@ -284,58 +303,64 @@ function reload(params, page=1){
|
||||||
piTableRef.value.clearFilter()
|
piTableRef.value.clearFilter()
|
||||||
getData()
|
getData()
|
||||||
}
|
}
|
||||||
|
|
||||||
//自定义变化事件
|
//自定义变化事件
|
||||||
function columnSettingChange(column){
|
function columnSettingChange(column) {
|
||||||
userColumn.value = column;
|
userColumn.value = column;
|
||||||
toggleIndex.value += 1;
|
toggleIndex.value += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
//自定义列保存
|
//自定义列保存
|
||||||
async function columnSettingSave(column){
|
async function columnSettingSave(column) {
|
||||||
columnSettingRef.value.isSave = true
|
columnSettingRef.value.isSave = true
|
||||||
try {
|
try {
|
||||||
await config.columnSettingSave(props.tableName, column)
|
await config.columnSettingSave(props.tableName, column)
|
||||||
}catch(error){
|
} catch (error) {
|
||||||
proxy.$message.error('保存失败')
|
proxy.$message.error('保存失败')
|
||||||
columnSettingRef.value.isSave = false
|
columnSettingRef.value.isSave = false
|
||||||
}
|
}
|
||||||
proxy.$message.success('保存成功')
|
proxy.$message.success('保存成功')
|
||||||
columnSettingRef.value.isSave = false
|
columnSettingRef.value.isSave = false
|
||||||
}
|
}
|
||||||
|
|
||||||
//自定义列重置
|
//自定义列重置
|
||||||
async function columnSettingBack(){
|
async function columnSettingBack() {
|
||||||
columnSettingRef.value.isSave = true
|
columnSettingRef.value.isSave = true
|
||||||
try {
|
try {
|
||||||
const column = await config.columnSettingReset(props.tableName, props.column)
|
const column = await config.columnSettingReset(props.tableName, props.column)
|
||||||
userColumn.value = column
|
userColumn.value = column
|
||||||
columnSettingRef.value.usercolumn = tools.objCopy(userColumn.value)
|
columnSettingRef.value.usercolumn = tools.objCopy(userColumn.value)
|
||||||
}catch(error){
|
} catch (error) {
|
||||||
proxy.$message.error('重置失败')
|
proxy.$message.error('重置失败')
|
||||||
columnSettingRef.value.isSave = false
|
columnSettingRef.value.isSave = false
|
||||||
}
|
}
|
||||||
columnSettingRef.value.isSave = false
|
columnSettingRef.value.isSave = false
|
||||||
}
|
}
|
||||||
|
|
||||||
//排序事件
|
//排序事件
|
||||||
function sortChange(obj){
|
function sortChange(obj) {
|
||||||
if(!proxy.remoteSort){
|
if (!proxy.remoteSort) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if(obj.column && obj.prop){
|
if (obj.column && obj.prop) {
|
||||||
prop.value = obj.prop
|
prop.value = obj.prop
|
||||||
order.value = obj.order
|
order.value = obj.order
|
||||||
}else{
|
} else {
|
||||||
prop.value = null
|
prop.value = null
|
||||||
order.value = null
|
order.value = null
|
||||||
}
|
}
|
||||||
getData()
|
getData()
|
||||||
}
|
}
|
||||||
|
|
||||||
//本地过滤
|
//本地过滤
|
||||||
function filterHandler(value, row, column){
|
function filterHandler(value, row, column) {
|
||||||
const property = column.property;
|
const property = column.property;
|
||||||
return row[property] === value;
|
return row[property] === value;
|
||||||
}
|
}
|
||||||
|
|
||||||
//过滤事件
|
//过滤事件
|
||||||
function filterChange(filters){
|
function filterChange(filters) {
|
||||||
if(!props.remoteFilter){
|
if (!props.remoteFilter) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
Object.keys(filters).forEach(key => {
|
Object.keys(filters).forEach(key => {
|
||||||
|
|
@ -343,104 +368,116 @@ function filterChange(filters){
|
||||||
})
|
})
|
||||||
upData(filters)
|
upData(filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
//远程合计行处理
|
//远程合计行处理
|
||||||
function remoteSummaryMethod(param){
|
function remoteSummaryMethod(param) {
|
||||||
const {columns} = param
|
const {columns} = param
|
||||||
const sums = []
|
const sums = []
|
||||||
columns.forEach((column, index) => {
|
columns.forEach((column, index) => {
|
||||||
if(index === 0) {
|
if (index === 0) {
|
||||||
sums[index] = '合计'
|
sums[index] = '合计'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const values = summary.value[column.property]
|
const values = summary.value[column.property]
|
||||||
if(values){
|
if (values) {
|
||||||
sums[index] = values
|
sums[index] = values
|
||||||
}else{
|
} else {
|
||||||
sums[index] = ''
|
sums[index] = ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return sums
|
return sums
|
||||||
}
|
}
|
||||||
function configSizeChange(){
|
|
||||||
|
function configSizeChange() {
|
||||||
piTableRef.value.doLayout()
|
piTableRef.value.doLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
//插入行 unshiftRow
|
//插入行 unshiftRow
|
||||||
function unshiftRow(row){
|
function unshiftRow(row) {
|
||||||
tableData.value.unshift(row)
|
tableData.value.unshift(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
//插入行 pushRow
|
//插入行 pushRow
|
||||||
function pushRow(row){
|
function pushRow(row) {
|
||||||
tableData.value.push(row)
|
tableData.value.push(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
//根据key覆盖数据
|
//根据key覆盖数据
|
||||||
function updateKey(row, rowKey=props.rowKey){
|
function updateKey(row, rowKey = props.rowKey) {
|
||||||
tableData.value.filter(item => item[rowKey]===row[rowKey] ).forEach(item => {
|
tableData.value.filter(item => item[rowKey] === row[rowKey]).forEach(item => {
|
||||||
Object.assign(item, row)
|
Object.assign(item, row)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//根据index覆盖数据
|
//根据index覆盖数据
|
||||||
function updateIndex(row, index){
|
function updateIndex(row, index) {
|
||||||
Object.assign(tableData.value[index], row)
|
Object.assign(tableData.value[index], row)
|
||||||
}
|
}
|
||||||
|
|
||||||
//根据index删除
|
//根据index删除
|
||||||
function removeIndex(index){
|
function removeIndex(index) {
|
||||||
tableData.value.splice(index, 1)
|
tableData.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
//根据index批量删除
|
//根据index批量删除
|
||||||
function removeIndexes(indexes=[]){
|
function removeIndexes(indexes = []) {
|
||||||
indexes.forEach(index => {
|
indexes.forEach(index => {
|
||||||
tableData.value.splice(index, 1)
|
tableData.value.splice(index, 1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//根据key删除
|
//根据key删除
|
||||||
function removeKey(key, rowKey=props.rowKey){
|
function removeKey(key, rowKey = props.rowKey) {
|
||||||
tableData.value.splice(tableData.value.findIndex(item => item[rowKey]===key), 1)
|
tableData.value.splice(tableData.value.findIndex(item => item[rowKey] === key), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
//根据keys批量删除
|
//根据keys批量删除
|
||||||
function removeKeys(keys=[], rowKey=props.rowKey){
|
function removeKeys(keys = [], rowKey = props.rowKey) {
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
tableData.value.splice(tableData.value.findIndex(item => item[rowKey]===key), 1)
|
tableData.value.splice(tableData.value.findIndex(item => item[rowKey] === key), 1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//原生方法转发
|
//原生方法转发
|
||||||
function clearSelection(){
|
function clearSelection() {
|
||||||
piTableRef.value.clearSelection()
|
piTableRef.value.clearSelection()
|
||||||
}
|
}
|
||||||
function toggleRowSelection(row, selected){
|
|
||||||
|
function toggleRowSelection(row, selected) {
|
||||||
piTableRef.value.toggleRowSelection(row, selected)
|
piTableRef.value.toggleRowSelection(row, selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function toggleAllSelection(){
|
function toggleAllSelection() {
|
||||||
piTableRef.value.toggleAllSelection()
|
piTableRef.value.toggleAllSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleRowExpansion(row, expanded){
|
function toggleRowExpansion(row, expanded) {
|
||||||
piTableRef.value.toggleRowExpansion(row, expanded)
|
piTableRef.value.toggleRowExpansion(row, expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCurrentRow(row){
|
function setCurrentRow(row) {
|
||||||
piTableRef.value.setCurrentRow(row)
|
piTableRef.value.setCurrentRow(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSort(){
|
function clearSort() {
|
||||||
piTableRef.value.clearSort()
|
piTableRef.value.clearSort()
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearFilter(columnKey){
|
function clearFilter(columnKey) {
|
||||||
piTableRef.value.clearFilter(columnKey)
|
piTableRef.value.clearFilter(columnKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
function doLayout(){
|
function doLayout() {
|
||||||
piTableRef.value.doLayout()
|
piTableRef.value.doLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
function sort(prop, order){
|
function sort(prop, order) {
|
||||||
piTableRef.value.sort(prop, order)
|
piTableRef.value.sort(prop, order)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
.pi-table {
|
.pi-table {
|
||||||
height: calc(100% - 50px);
|
height: calc(100% - 50px);
|
||||||
}
|
}
|
||||||
|
|
@ -470,4 +507,31 @@ function sort(prop, order){
|
||||||
width: 12px;
|
width: 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pi-extend {
|
||||||
|
background: var(--el-bg-color-overlay);
|
||||||
|
border-color: var(--el-border-color-light);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-header) {
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extend {
|
||||||
|
background: var(--el-bg-color-overlay);
|
||||||
|
border-color: var(--el-border-color-light);
|
||||||
|
padding: 15px 15px 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extend-panel {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel > * + *, .left-panel > * + *, .extend-panel > * + * {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
<template>
|
||||||
|
<div class="sc-form-table">
|
||||||
|
<el-table :data="data" ref="tableRef" :key="toggleIndex" border stripe>
|
||||||
|
<el-table-column type="index" width="50" fixed="left">
|
||||||
|
<template #header>
|
||||||
|
<el-button :disabled="(limit > 0 && data.length >= limit) || disabledAdd" type="primary"
|
||||||
|
icon="el-icon-plus" size="small" circle @click="rowAdd"></el-button>
|
||||||
|
</template>
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="sc-form-table-handle">
|
||||||
|
<span>{{ scope.$index + 1 }}</span>
|
||||||
|
<el-button type="danger" :disabled="scope.$index == 0 && !delFirst" icon="el-icon-delete"
|
||||||
|
size="small" plain circle @click="rowDel(scope.row, scope.$index)"></el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="" width="58" v-if="dragSort">
|
||||||
|
<template #default>
|
||||||
|
<el-tag class="move" style="cursor: move;">
|
||||||
|
<el-icon-d-caret style="width: 1em; height: 1em;"/>
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<slot></slot>
|
||||||
|
<template #empty>
|
||||||
|
{{ placeholder }}
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Sortable from 'sortablejs'
|
||||||
|
import {ref, onMounted, watch, nextTick} from 'vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['add', 'del', 'update:modelValue'])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {type: Array, default: () => []},
|
||||||
|
addTemplate: {type: Object, default: () => {}},
|
||||||
|
placeholder: {type: String, default: "暂无数据"},
|
||||||
|
dragSort: {type: Boolean, default: false},
|
||||||
|
delFirst: {type: Boolean, default: true},
|
||||||
|
limit: {type: Number, default: 0},
|
||||||
|
disabledAdd: {type: Boolean, default: false},
|
||||||
|
// 魔术模式 使用将仅触发添加事件
|
||||||
|
magic: {type: Boolean, default: false},
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableRef = ref(null)
|
||||||
|
|
||||||
|
let data = ref([])
|
||||||
|
let toggleIndex = ref(0)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.modelValue.length > 0) {
|
||||||
|
data.value = props.modelValue
|
||||||
|
}
|
||||||
|
if (props.dragSort) {
|
||||||
|
rowDrop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.modelValue, () => {
|
||||||
|
data.value = props.modelValue
|
||||||
|
}, {deep: true})
|
||||||
|
|
||||||
|
watch(data, () => {
|
||||||
|
emit('update:modelValue', data.value)
|
||||||
|
}, {deep: true})
|
||||||
|
|
||||||
|
function rowDrop() {
|
||||||
|
const tbody = tableRef.value.$el.querySelector('.el-table__body-wrapper tbody')
|
||||||
|
Sortable.create(tbody, {
|
||||||
|
handle: ".move",
|
||||||
|
animation: 300,
|
||||||
|
ghostClass: "ghost",
|
||||||
|
onEnd({newIndex, oldIndex}) {
|
||||||
|
const tableData = data.value
|
||||||
|
const currRow = tableData.splice(oldIndex, 1)[0]
|
||||||
|
tableData.splice(newIndex, 0, currRow)
|
||||||
|
toggleIndex.value += 1
|
||||||
|
nextTick(() => {
|
||||||
|
rowDrop()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowAdd() {
|
||||||
|
if (props.magic) {
|
||||||
|
emit('add')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const temp = JSON.parse(JSON.stringify(props.addTemplate))
|
||||||
|
data.value.push(temp)
|
||||||
|
emit('add', data.value.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowDel(row, index) {
|
||||||
|
data.value.splice(index, 1)
|
||||||
|
emit('del')
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sc-form-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-form-table .sc-form-table-handle {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-form-table .sc-form-table-handle span {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-form-table .sc-form-table-handle button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-form-table .hover-row .sc-form-table-handle span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-form-table .hover-row .sc-form-table-handle button {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
import api from "@/api";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
uploadObj: api.system.file.upload,
|
||||||
|
fileListObj: api.system.file.list,
|
||||||
|
moveFileObj: api.system.file.move,
|
||||||
|
delFileObj: api.system.file.del,
|
||||||
|
menuListObj: api.system.category.list,
|
||||||
|
menuAddObj: api.system.category.add,
|
||||||
|
menuEditObj: api.system.category.edit,
|
||||||
|
menuDelObj: api.system.category.del,
|
||||||
|
successCode: 200,
|
||||||
|
maxSize: 20,
|
||||||
|
max: 99,
|
||||||
|
uploadParseData: function (res) {
|
||||||
|
return {
|
||||||
|
fileName: res.data.fileName,
|
||||||
|
url: res.data.filePath
|
||||||
|
}
|
||||||
|
},
|
||||||
|
listParseData: function (res) {
|
||||||
|
return {
|
||||||
|
rows: res.data,
|
||||||
|
total: res.count,
|
||||||
|
msg: res.msg,
|
||||||
|
code: res.code
|
||||||
|
}
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
keyword: 'file_name',
|
||||||
|
menuKey: 'category_id'
|
||||||
|
},
|
||||||
|
menuProps: {
|
||||||
|
key: 'category_id',
|
||||||
|
label: 'category_name',
|
||||||
|
children: 'children'
|
||||||
|
},
|
||||||
|
fileProps: {
|
||||||
|
fileName: 'file_name',
|
||||||
|
url: 'url'
|
||||||
|
},
|
||||||
|
fileType(type: string) {
|
||||||
|
switch (type) {
|
||||||
|
case 'word':
|
||||||
|
return {
|
||||||
|
"accept": "application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"ext": ['doc', 'docx']
|
||||||
|
}
|
||||||
|
case 'excel':
|
||||||
|
return {
|
||||||
|
"accept": "application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"ext": ['xls', 'xlsx']
|
||||||
|
}
|
||||||
|
case 'ppt':
|
||||||
|
return {
|
||||||
|
"accept": "application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
"ext": ['ppt', 'pptx']
|
||||||
|
}
|
||||||
|
case 'image':
|
||||||
|
return {
|
||||||
|
"accept": "image/x-ms-bmp, image/jpeg, image/gif, image/png",
|
||||||
|
"ext": ['png', 'jpg', 'bmp', 'jpeg', 'gif']
|
||||||
|
}
|
||||||
|
case 'video':
|
||||||
|
return {
|
||||||
|
"accept": "video/mp4",
|
||||||
|
"ext": ['mp4']
|
||||||
|
}
|
||||||
|
case 'audio':
|
||||||
|
return {
|
||||||
|
"accept": "audio/mpeg, audio/wav",
|
||||||
|
"ext": ['mp3', 'wav']
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
"accept": "",
|
||||||
|
"ext": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 图片
|
||||||
|
isImg(fileUrl: string) {
|
||||||
|
const fileExt = fileUrl.substring(fileUrl.lastIndexOf(".") + 1)
|
||||||
|
return this.fileType('image')['ext'].indexOf(fileExt) != -1
|
||||||
|
},
|
||||||
|
// 视频
|
||||||
|
isVideo(fileUrl: string) {
|
||||||
|
const fileExt = fileUrl.substring(fileUrl.lastIndexOf(".") + 1)
|
||||||
|
return this.fileType('video')['ext'].indexOf(fileExt) != -1
|
||||||
|
},
|
||||||
|
getExt(fileUrl: string) {
|
||||||
|
return fileUrl.substring(fileUrl.lastIndexOf(".") + 1)
|
||||||
|
},
|
||||||
|
getType(name: string) {
|
||||||
|
switch (this.getExt(name)) {
|
||||||
|
case 'mp3':
|
||||||
|
case 'wav':
|
||||||
|
return 'audio';
|
||||||
|
case 'mp4':
|
||||||
|
return 'video';
|
||||||
|
case 'png':
|
||||||
|
case 'jpg':
|
||||||
|
case 'jpeg':
|
||||||
|
case 'bmp':
|
||||||
|
case 'gif':
|
||||||
|
return 'image';
|
||||||
|
case 'ppt':
|
||||||
|
case 'pptx':
|
||||||
|
return 'ppt';
|
||||||
|
case 'xls':
|
||||||
|
case 'xlsx':
|
||||||
|
return 'excel';
|
||||||
|
case 'doc':
|
||||||
|
case 'docx':
|
||||||
|
return 'word';
|
||||||
|
default:
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
export default {
|
||||||
|
pageSize: 10, //表格每一页条数
|
||||||
|
parseData: function (res) {
|
||||||
|
return {
|
||||||
|
data: res,
|
||||||
|
rows: res.data, //分析行数据字段结构
|
||||||
|
total: res.count, //分析总数字段结构
|
||||||
|
msg: res.msg, //分析描述字段结构
|
||||||
|
code: res.code //分析状态字段结构
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filter: function (data, fields, multiple = true) {
|
||||||
|
if (multiple) {
|
||||||
|
const value = [];
|
||||||
|
if (data.length > 0 && fields.length > 0) {
|
||||||
|
for (const key in data) {
|
||||||
|
const item = {};
|
||||||
|
for (const field of fields) {
|
||||||
|
item[field] = data[key][field]
|
||||||
|
}
|
||||||
|
value.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}else {
|
||||||
|
const item = {};
|
||||||
|
for (const field of fields) {
|
||||||
|
item[field] = data[field]
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
page: 'page', //规定当前分页字段
|
||||||
|
pageSize: 'limit', //规定一页条数字段
|
||||||
|
keyword: 'keyword' //规定搜索字段
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
label: 'label', //映射label显示字段
|
||||||
|
value: 'value', //映射value值字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,10 +46,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<el-scrollbar height="220px" :view-style="{ 'overflow-x': 'hidden' }">
|
<el-scrollbar height="220px" :view-style="{ 'overflow-x': 'hidden' }">
|
||||||
<el-link v-for="item in msgList" :key="item" href="/#/message" underline="never" :title="item.time + ' ' + item.title">
|
<el-link v-for="item in msgList" :key="item" href="/#/message" underline="never" :title="item.time + ' ' + item.title" style="display: block; width: 100%;">
|
||||||
<template #default>
|
<template #default>
|
||||||
<el-text type="primary" v-time.tip="item.time"></el-text>
|
<el-text truncated style="width: 100%;"><el-text type="primary" v-time.tip="item.time" style="margin-right: 5px;"></el-text>{{ item.title }}</el-text>
|
||||||
<div>{{ item.title }}</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-link>
|
</el-link>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
|
|
@ -372,14 +371,10 @@ async function loadData() {
|
||||||
|
|
||||||
.msg-content .el-link {
|
.msg-content .el-link {
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-content .el-link .el-text{
|
:deep(.el-link__inner) {
|
||||||
margin-right: 5px;
|
display: flex;
|
||||||
width: 48px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-more {
|
.msg-more {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import errorHandler from "@/utils/errorHandler";
|
||||||
|
|
||||||
import piDialog from "@/components/piDialog"
|
import piDialog from "@/components/piDialog"
|
||||||
import piTable from "@/components/piTable"
|
import piTable from "@/components/piTable"
|
||||||
import piPage from "@/components/piPage"
|
|
||||||
import piUpload from "@/components/piUpload"
|
import piUpload from "@/components/piUpload"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -19,7 +18,6 @@ export default {
|
||||||
// 注册全局组件
|
// 注册全局组件
|
||||||
app.component('piDialog', piDialog)
|
app.component('piDialog', piDialog)
|
||||||
app.component('piTable', piTable)
|
app.component('piTable', piTable)
|
||||||
app.component('piPage', piPage)
|
|
||||||
app.component('piUpload', piUpload)
|
app.component('piUpload', piUpload)
|
||||||
|
|
||||||
//注册全局指令
|
//注册全局指令
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,12 @@ axios.interceptors.request.use((config) => {
|
||||||
})
|
})
|
||||||
//响应拦截
|
//响应拦截
|
||||||
axios.interceptors.response.use((response) => {
|
axios.interceptors.response.use((response) => {
|
||||||
|
// 续签逻辑
|
||||||
|
const newToken = response.headers['x-token-refresh']
|
||||||
|
const ttl = response.headers['x-token-expire']
|
||||||
|
if (newToken && ttl > 0) {
|
||||||
|
tools.data.set("TOKEN", newToken, ttl)
|
||||||
|
}
|
||||||
let res = response.data
|
let res = response.data
|
||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
return Promise.resolve(res)
|
return Promise.resolve(res)
|
||||||
|
|
|
||||||
|
|
@ -108,34 +108,34 @@ const tools = {
|
||||||
return JSON.parse(JSON.stringify(obj || []));
|
return JSON.parse(JSON.stringify(obj || []));
|
||||||
},
|
},
|
||||||
/* 日期格式化 */
|
/* 日期格式化 */
|
||||||
dateFormat: function (date, fmt='yyyy-MM-dd hh:mm:ss') {
|
dateFormat: function (date, fmt = 'yyyy-MM-dd hh:mm:ss') {
|
||||||
date = new Date(date)
|
date = new Date(date)
|
||||||
var o = {
|
var o = {
|
||||||
"M+" : date.getMonth()+1, //月份
|
"M+": date.getMonth() + 1, //月份
|
||||||
"d+" : date.getDate(), //日
|
"d+": date.getDate(), //日
|
||||||
"h+" : date.getHours(), //小时
|
"h+": date.getHours(), //小时
|
||||||
"m+" : date.getMinutes(), //分
|
"m+": date.getMinutes(), //分
|
||||||
"s+" : date.getSeconds(), //秒
|
"s+": date.getSeconds(), //秒
|
||||||
"q+" : Math.floor((date.getMonth()+3)/3), //季度
|
"q+": Math.floor((date.getMonth() + 3) / 3), //季度
|
||||||
"S" : date.getMilliseconds() //毫秒
|
"S": date.getMilliseconds() //毫秒
|
||||||
};
|
};
|
||||||
if(/(y+)/.test(fmt)) {
|
if (/(y+)/.test(fmt)) {
|
||||||
fmt=fmt.replace(RegExp.$1, (date.getFullYear()+"").substr(4 - RegExp.$1.length));
|
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
|
||||||
}
|
}
|
||||||
for(var k in o) {
|
for (var k in o) {
|
||||||
if(new RegExp("("+ k +")").test(fmt)){
|
if (new RegExp("(" + k + ")").test(fmt)) {
|
||||||
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length==1) ? (o[k]) : (("00"+ o[k]).substr((""+ o[k]).length)));
|
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt;
|
return fmt;
|
||||||
},
|
},
|
||||||
makeTreeData: function (data, pid = 0, key = "id") {
|
makeTreeData: function (data, pid = 0, key = "id", parent = "pid") {
|
||||||
const arr = [];
|
const arr = [];
|
||||||
for (let item of data) {
|
for (let item of data) {
|
||||||
if(item.pid == pid){
|
if (item[parent] == pid) {
|
||||||
// 数据格式处理
|
// 数据格式处理
|
||||||
const tmp = item;
|
const tmp = item;
|
||||||
const children = tools.makeTreeData(data, item[key], key);
|
const children = tools.makeTreeData(data, item[key], key, parent);
|
||||||
if (children.length > 0) {
|
if (children.length > 0) {
|
||||||
tmp['children'] = children
|
tmp['children'] = children
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<template>
|
||||||
|
<pi-table ref="tableRef" :apiObj="api.system.online.list" @selection-change="selectionChange">
|
||||||
|
<template #do>
|
||||||
|
<el-input v-model="search.username" placeholder="用户名" clearable style="width: 200px;" @keyup.enter="upsearch"></el-input>
|
||||||
|
<el-button type="primary" icon="el-icon-search" @click="upsearch"></el-button>
|
||||||
|
</template>
|
||||||
|
<el-table-column type="selection" width="50"></el-table-column>
|
||||||
|
<el-table-column label="#" type="index" width="50"></el-table-column>
|
||||||
|
<el-table-column label="会话编号" prop="session_id"></el-table-column>
|
||||||
|
<el-table-column label="用户名" prop="username"></el-table-column>
|
||||||
|
<el-table-column label="部门名称" prop="account.dept.dept_name"></el-table-column>
|
||||||
|
<el-table-column label="主机" prop="ip"></el-table-column>
|
||||||
|
<el-table-column label="登录地点" prop="location"></el-table-column>
|
||||||
|
<el-table-column label="浏览器" prop="os.browser"></el-table-column>
|
||||||
|
<el-table-column label="操作系统" prop="os.os"></el-table-column>
|
||||||
|
<el-table-column label="登录时间" prop="online_time"></el-table-column>
|
||||||
|
<el-table-column label="操作" fixed="right" align="right" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button-group>
|
||||||
|
<el-popconfirm title="确定删除吗?" @confirm="table_del(scope.row, scope.$index)">
|
||||||
|
<template #reference>
|
||||||
|
<el-button v-auth="'online:quit'" text type="primary" size="small">强退</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</el-button-group>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</pi-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import api from "@/api/index";
|
||||||
|
import {getCurrentInstance, nextTick, ref} from "vue";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "monitorOnline"
|
||||||
|
})
|
||||||
|
|
||||||
|
const {proxy} = getCurrentInstance()
|
||||||
|
const tableRef = ref(null)
|
||||||
|
const dialogRef = ref(null)
|
||||||
|
|
||||||
|
let dialogShow = ref(false)
|
||||||
|
let selection = ref([])
|
||||||
|
let search = ref({
|
||||||
|
username: null
|
||||||
|
})
|
||||||
|
|
||||||
|
//删除
|
||||||
|
async function table_del(row) {
|
||||||
|
const loading = proxy.$loading();
|
||||||
|
const res = await api.system.post.del({ids: row.post_id});
|
||||||
|
tableRef.value.refresh()
|
||||||
|
loading.close();
|
||||||
|
proxy.$message.success(res.msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
//批量删除
|
||||||
|
async function batch_del() {
|
||||||
|
proxy.$confirm(`确定删除选中的 ${selection.value.length} 项吗?如果删除项中含有子集将会被一并删除`, '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
const loading = proxy.$loading();
|
||||||
|
const res = await api.system.post.del({ids: selection.value.map(item => item.post_id).toString()});
|
||||||
|
tableRef.value.refresh()
|
||||||
|
loading.close();
|
||||||
|
proxy.$message.success(res.msg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//表格选择后回调事件
|
||||||
|
function selectionChange(e) {
|
||||||
|
selection.value = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
//搜索
|
||||||
|
function upsearch() {
|
||||||
|
tableRef.value.upData(search.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,328 @@
|
||||||
|
<template>
|
||||||
|
<el-main>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<el-text>
|
||||||
|
<el-icon>
|
||||||
|
<component :is="'el-icon-cpu'"/>
|
||||||
|
</el-icon>
|
||||||
|
<span>CPU</span>
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">核心数</el-text>
|
||||||
|
<el-text>{{ cpu.cpu_cores }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">用户使用率</el-text>
|
||||||
|
<el-text>{{ cpu.user_usage }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">系统使用率</el-text>
|
||||||
|
<el-text>{{ cpu.system_usage }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">当前空闲率</el-text>
|
||||||
|
<el-text>{{ cpu.idle }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<el-text>
|
||||||
|
<el-icon>
|
||||||
|
<component :is="'el-icon-document'"/>
|
||||||
|
</el-icon>
|
||||||
|
<span>内存</span>
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">总内存</el-text>
|
||||||
|
<el-text>{{ mem.total }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">已用内存</el-text>
|
||||||
|
<el-text>{{ mem.used }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">剩余内存</el-text>
|
||||||
|
<el-text>{{ mem.free }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">使用率</el-text>
|
||||||
|
<el-text>{{ mem.usage }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<el-text>
|
||||||
|
<el-icon>
|
||||||
|
<component :is="'el-icon-monitor'"/>
|
||||||
|
</el-icon>
|
||||||
|
<span>服务器信息</span>
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">服务器名称</el-text>
|
||||||
|
<el-text>{{ sys.server_name }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">操作系统</el-text>
|
||||||
|
<el-text>{{ sys.os }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">服务器IP</el-text>
|
||||||
|
<el-text>{{ sys.server_ip }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">系统架构</el-text>
|
||||||
|
<el-text>{{ sys.architecture }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">PHP版本</el-text>
|
||||||
|
<el-text>{{ sys.php_version }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<el-text>
|
||||||
|
<el-icon>
|
||||||
|
<component :is="'el-icon-mostly-cloudy'"/>
|
||||||
|
</el-icon>
|
||||||
|
<span>环境信息</span>
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">SWOOLE版本</el-text>
|
||||||
|
<el-text>{{ app.swoole_version }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">HYPERF版本</el-text>
|
||||||
|
<el-text>{{ app.hyperf_version }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">项目路径</el-text>
|
||||||
|
<el-text>{{ app.project_path }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">启动时间</el-text>
|
||||||
|
<el-text>{{ app.start_time }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">运行时长</el-text>
|
||||||
|
<el-text>{{ app.uptime }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<el-text>
|
||||||
|
<el-icon>
|
||||||
|
<component :is="'el-icon-box'"/>
|
||||||
|
</el-icon>
|
||||||
|
<span>缓存信息</span>
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">Redis版本</el-text>
|
||||||
|
<el-text>{{ redis.version }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">运行模式</el-text>
|
||||||
|
<el-text>{{ redis.mode }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">端口</el-text>
|
||||||
|
<el-text>{{ redis.port }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">客户端数</el-text>
|
||||||
|
<el-text>{{ redis.clients }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">运行时间(天)</el-text>
|
||||||
|
<el-text>{{ redis.uptime_days }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">使用内存</el-text>
|
||||||
|
<el-text>{{ redis.used_memory }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">使用CPU</el-text>
|
||||||
|
<el-text>{{ redis.used_cpu }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">内存配置</el-text>
|
||||||
|
<el-text>{{ redis.maxmemory }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">AOF是否开启</el-text>
|
||||||
|
<el-text>{{ redis.aof_enabled }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">RDB是否成功</el-text>
|
||||||
|
<el-text>{{ redis.rdb_last_bgsave_status }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">Key数量</el-text>
|
||||||
|
<el-text>{{ redis.keys }}</el-text>
|
||||||
|
</el-text>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-text class="pi-title">
|
||||||
|
<el-text type="primary">网络入口/出口</el-text>
|
||||||
|
<el-text>{{ redis.net_input_bytes }}/{{redis.net_output_bytes}}</el-text>
|
||||||
|
</el-text>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<el-text>
|
||||||
|
<el-icon>
|
||||||
|
<component :is="'el-icon-message-box'"/>
|
||||||
|
</el-icon>
|
||||||
|
<span>磁盘信息</span>
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<pi-table :data="disks" hide-pagination hide-do hide-refresh>
|
||||||
|
<el-table-column label="盘符路径" prop="path"></el-table-column>
|
||||||
|
<el-table-column label="文件系统" prop="filesystem"></el-table-column>
|
||||||
|
<el-table-column label="盘符类型" prop="type"></el-table-column>
|
||||||
|
<el-table-column label="总大小" prop="total"></el-table-column>
|
||||||
|
<el-table-column label="可用大小" prop="used"></el-table-column>
|
||||||
|
<el-table-column label="已用大小" prop="available"></el-table-column>
|
||||||
|
<el-table-column label="已用百分比" prop="used_percent"></el-table-column>
|
||||||
|
</pi-table>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import api from "@/api"
|
||||||
|
import {ref, onMounted} from "vue";
|
||||||
|
import PiTable from "@/components/piTable/index.vue";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "monitorServer"
|
||||||
|
})
|
||||||
|
|
||||||
|
let cpu = ref({
|
||||||
|
'cpu_cores': 0,
|
||||||
|
'user_usage': '0%',
|
||||||
|
'system_usage': '0%',
|
||||||
|
'idle': '0%'
|
||||||
|
})
|
||||||
|
|
||||||
|
let mem = ref({
|
||||||
|
'total': 0,
|
||||||
|
'used': '0',
|
||||||
|
'free': '0',
|
||||||
|
'usage': '0%'
|
||||||
|
})
|
||||||
|
|
||||||
|
let sys = ref({
|
||||||
|
'server_name': '',
|
||||||
|
'server_ip': '',
|
||||||
|
'os': '',
|
||||||
|
'architecture': '',
|
||||||
|
'php_version': ''
|
||||||
|
})
|
||||||
|
|
||||||
|
let app = ref({
|
||||||
|
'hyperf_version': '',
|
||||||
|
'swoole_version': '',
|
||||||
|
'project_path': '',
|
||||||
|
'start_time': '',
|
||||||
|
'uptime': ''
|
||||||
|
})
|
||||||
|
|
||||||
|
let disks = ref([])
|
||||||
|
let redis = ref([])
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const res = await api.system.monitor.server()
|
||||||
|
cpu.value = res.data.cpu
|
||||||
|
mem.value = res.data.mem
|
||||||
|
sys.value = res.data.sys
|
||||||
|
app.value = res.data.app
|
||||||
|
redis.value = res.data.redis
|
||||||
|
disks.value = res.data.disks
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pi-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<el-dialog :title="title" v-model="visible" :width="500" destroy-on-close @closed="emit('closed')" :close-on-click-modal="false">
|
<el-dialog title="快速添加页面和权限" v-model="visible" :width="500" destroy-on-close @closed="emit('closed')" :close-on-click-modal="false">
|
||||||
<el-form :model="form" ref="dialogForm" label-width="100px" label-position="right">
|
<el-form :model="form" ref="dialogForm" label-width="100px" label-position="right">
|
||||||
<el-form-item label="菜单名称" prop="title">
|
<el-form-item label="菜单名称" prop="title">
|
||||||
<el-input v-model="form.title" clearable></el-input>
|
<el-input v-model="form.title" placeholder="测试页面" clearable></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="菜单图标" prop="icon">
|
<el-form-item label="菜单图标" prop="icon">
|
||||||
<pi-icon v-model="form.icon" clearable></pi-icon>
|
<pi-icon v-model="form.icon" clearable></pi-icon>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="页面名称" prop="name">
|
<el-form-item label="页面名称" prop="name">
|
||||||
<el-input v-model="form.name" clearable></el-input>
|
<el-input v-model="form.name" placeholder="systemDemo" clearable></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="路由地址" prop="path">
|
<el-form-item label="路由地址" prop="path">
|
||||||
<el-input v-model="form.path" clearable></el-input>
|
<el-input v-model="form.path" placeholder="system/demo" clearable></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="权限标识" prop="flag">
|
<el-form-item label="权限标识" prop="flag">
|
||||||
<el-input v-model="form.flag" clearable></el-input>
|
<el-input v-model="form.flag" placeholder="demo" clearable></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import piIcon from '@/components/piIcon'
|
import piIcon from '@/components/piIcon'
|
||||||
import {ref, getCurrentInstance, defineEmits} from "vue"
|
import {ref, getCurrentInstance} from "vue"
|
||||||
import api from "@/api"
|
import api from "@/api"
|
||||||
const emit = defineEmits(['closed', 'success']);
|
const emit = defineEmits(['closed', 'success']);
|
||||||
|
|
||||||
|
|
@ -35,7 +35,6 @@ defineExpose({
|
||||||
})
|
})
|
||||||
|
|
||||||
const {proxy} = getCurrentInstance()
|
const {proxy} = getCurrentInstance()
|
||||||
const title = ref("快速添加")
|
|
||||||
let visible = ref(false)
|
let visible = ref(false)
|
||||||
let isSaveing = ref(false)
|
let isSaveing = ref(false)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,6 @@ function selectionChange(e) {
|
||||||
|
|
||||||
//搜索
|
//搜索
|
||||||
function upsearch() {
|
function upsearch() {
|
||||||
tableRef.value.upData(search)
|
tableRef.value.upData(search.value)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -242,10 +242,6 @@ async function saveInfo() {
|
||||||
isSaveing.value = false
|
isSaveing.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function sexOption(v) {
|
|
||||||
console.log(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function savePass() {
|
async function savePass() {
|
||||||
const validate = await passRef.value.validate().catch(() => {});
|
const validate = await passRef.value.validate().catch(() => {});
|
||||||
if(!validate){ return false }
|
if(!validate){ return false }
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import {defineConfig, loadEnv} from 'vite'
|
import {defineConfig, loadEnv} from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import {fileURLToPath} from 'url'
|
||||||
|
|
||||||
export default defineConfig(({mode, command}) => {
|
export default defineConfig(({mode, command}) => {
|
||||||
const env = loadEnv(mode, process.cwd())
|
const env = loadEnv(mode, process.cwd())
|
||||||
|
|
@ -23,14 +23,14 @@ export default defineConfig(({mode, command}) => {
|
||||||
server: {
|
server: {
|
||||||
port: 8611,
|
port: 8611,
|
||||||
host: true,
|
host: true,
|
||||||
// open: true,
|
open: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: VITE_API_BASE,
|
target: VITE_API_BASE,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (p) => p.replace(/^\/api/, '')
|
rewrite: (p) => p.replace(/^\/api/, '')
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
|
|
@ -47,6 +47,16 @@ export default defineConfig(({mode, command}) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [vue()]
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
{
|
||||||
|
name: 'full-reload',
|
||||||
|
handleHotUpdate({file, server}) {
|
||||||
|
if (file.endsWith('.vue')) {
|
||||||
|
server.ws.send({type: 'full-reload'});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue