文章目录
前言
为自己搭建一个可以自我思考的平台,其核心为“心想事成”。
一、思考过程?
此时是我已实现过得一个前端案例,其中借鉴网上的拖拽事件的介绍实现的功能。
二、完善
搭建时遇到过的问题。
其中使用图片上传功能,我想着是上传到本地的前端项目的assets上然后可以展示,但实际情况是element展示不出上传到assets文件夹下的图片。
实现页面
1、组件类型页面
<template>
<div>
<div style="margin-bottom: 15px">
<el-autocomplete
v-model="name"
:fetch-suggestions="querySearch"
:clearable="false"
value-key="value"
class="inline-input w-50"
placeholder="请输入组件类型名称"
@select="handleSelect"
@keyup.enter.native="getDataList(true)"
/>
</div>
<div style="margin-bottom: 15px">
<el-button type="primary" plain @click="handleAdd">新增</el-button>
</div>
<el-space
fill
wrap
:fill-ratio="fillRatio"
:direction="direction"
style="width: 100%; margin-bottom: 15px;"
>
<el-table v-loading="loading" :data="tableData" border>
<template slot="empty">
<el-empty :image-size="100" description="暂无数据"></el-empty>
</template>
<el-table-column label="组件类型" align="center" prop="name"/>
<el-table-column label="排序" align="center" prop="sort"/>
<el-table-column
label="操作"
align="center"
class-name="small-padding fixed-width"
>
<template #default="scope">
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button link type="primary" size="small" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-space>
<el-space
fill
wrap
:fill-ratio="fillRatio"
:direction="direction"
>
<el-pagination
v-model:current-page="queryParams.current"
v-model:page-size="queryParams.size"
:page-sizes="[100, 200, 300, 400]"
:small="small"
:disabled="disabled"
:background="background"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-space>
<el-dialog v-model="dialogFormVisible" @close="resetForm(dataFormRef)" :title="title" width="30%" center>
<el-form ref="dataFormRef" :model="dataForm" :rules="rules">
<el-form-item label="组件类型" :label-width="formLabelWidth" prop="name">
<el-input v-model="dataForm.name" placeholder="请输入组件类型" autocomplete="off" />
</el-form-item>
<!-- <el-form-item label="排序" :label-width="formLabelWidth" prop="sort">
<el-input-number v-model="dataForm.sort" :min="1" />
</el-form-item>-->
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeDialog(false, dataFormRef)">取消</el-button>
<el-button type="primary" @click="submitDataForm(dataFormRef)">确认</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import {onMounted, ref} from 'vue'
import {page as keywordPage} from '@/api/keyword'
import {page as keywordRelationPage} from '@/api/keywordRelation'
import {ElMessage, ElMessageBox, FormInstance, FormRules} from "element-plus";
import {page, delById, getById, save, update} from '@/api/componentType'
interface RestaurantItem {
value: string
count: string
}
const restaurants = ref < RestaurantItem[] > ([])
const querySearch = (queryString: string, cb: any) => {
if (queryString && queryString != '') {
let params = {
current: 1,
size: 10,
keyword: queryString,
}
let keywordList = []
params.keyword = queryString
keywordRelationPage(params).then(res => {
if (res.data.total > 0) {
res.data.records.forEach(item => {
let keyword: object = {
key: item.keyword,
value: item.keywordRelationSentence,
count: item.useCount,
createTime: item.createTime,
}
keywordList.push(keyword)
})
}
const results = queryString
? keywordList.filter(createFilter(queryString))
: keywordList
// call callback function to return suggestions
cb(results)
})
} else {
const results = queryString
? restaurants.value.filter(createInitFilter(queryString))
: restaurants.value
// call callback function to return suggestions
cb(results)
}
}
const createFilter = (queryString: string) => {
return (item) => {
return (
item.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
)
}
}
const createInitFilter = (queryString: string) => {
return (restaurant: RestaurantItem) => {
return (
restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
)
}
}
const loadAll = () => {
let params = {
current: 1,
size: 10,
}
let keywordList = []
keywordPage(params).then(res => {
if (res.data.total > 0) {
res.data.records.forEach(item => {
let keyword = {
value: item.keyword,
count: item.useCount,
createTime: item.createTime,
}
keywordList.push(keyword)
})
}
})
return keywordList;
}
const handleSelect = (item: RestaurantItem) => {
console.log(item, 'restaurantItem')
}
onMounted(() => {
restaurants.value = loadAll()
})
const direction = ref('horizontal')
const fillRatio = ref(30)
const tableData = ref([])
const name = ref('')
const total = ref(0)
const loading = ref('')
const queryParams = ref({
current: 1,
size: 10,
name: '',
})
const small = ref(false)
const background = ref(false)
const disabled = ref(false)
const getDataList = (reset) => {
if (reset) {
queryParams.value.current = 1
}
if (name.value) {
queryParams.value.name = name.value
}
page(queryParams.value).then(resp => {
tableData.value = resp.data.records
total.value = resp.data.total
})
}
const handleSizeChange = (val: number) => {
console.log(`${val} items per page`)
queryParams.value.size = val
getDataList(true)
}
const handleCurrentChange = (val: number) => {
console.log(`current page: ${val}`)
queryParams.value.current = val
getDataList(false)
}
onMounted(() => {
getDataList(true)
})
const handleDelete = (row: object) => {
ElMessageBox.confirm(
'请确认是否删除该组件类型?',
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
try {
await delById(row.id)
ElMessage({
type: 'success',
message: '删除成功!',
})
getDataList(false)
} catch (e) {
console.log(e)
}
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
}
import {reactive} from 'vue'
const title = ref('新增组件类型')
const dialogFormVisible = ref(false)
const formLabelWidth = '140px'
interface DataForm {
id: string,
name: string,
// sort: string,
}
const dataFormRef = ref<FormInstance>()
const dataForm = reactive<DataForm>({
id: '',
name: '',
// sort: '',
})
const rules = reactive<FormRules<DataForm>>({
name: [
{ required: true, message: '请输入组件类型名', trigger: 'blur' },
],
/*sort: [
{ required: true, message: '请输入密码', trigger: 'blur' },
],*/
})
const handleAdd = () => {
title.value = '新增组件类型'
dialogFormVisible.value = true
}
const handleEdit = (row: object) => {
title.value = '修改组件类型'
getById(row.id).then(res => {
if (res.code != 200) {
return
}
dialogFormVisible.value = true
dataForm.id = res.data.id
dataForm.name = res.data.name
// dataForm.sort = res.data.sort
})
}
const submitDataForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
if (dataForm.id && dataForm.id != '') {
update(dataForm).then(res => {
if (res.code != 200) {
ElMessage.success("请求错误")
return
}
ElMessage.success("修改成功")
closeDialog(false, formEl)
})
} else {
save(dataForm).then(res => {
if (res.code != 200) {
ElMessage.success("请求错误")
return
}
ElMessage.success("新增成功")
closeDialog(true, formEl)
})
}
} else {
console.log('error submit!', fields)
}
})
}
const closeDialog = (reset: boolean, formEl: FormInstance | undefined) => {
dialogFormVisible.value = false
resetForm(formEl)
getDataList(reset)
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
</script>
<style scoped>
.el-button--text {
margin-right: 15px;
}
.el-select {
width: 300px;
}
.el-input {
width: 300px;
}
.dialog-footer button:first-child {
margin-right: 10px;
}
::v-deep .el-dialog{
display: flex;
flex-direction: column;
margin:0 !important;
position:absolute;
top:50%;
left:50%;
transform:translate(-50%,-50%);
max-height:calc(100% - 30px);
max-width:calc(100% - 30px);
}
::v-deep .el-dialog .el-dialog__body{
flex:1;
overflow: auto;
}
</style>
1.1、接口调用
import axios from '../utils/http'
// 分页查询用户列表
export function page(params) {
return axios('/componentType/page', { params }, "get");
}
// 分页查询组件列表
export function list() {
return axios('/componentType/list', null, "get");
}
// 根据主键ID删除用户信息
export function delById(id) {
return axios(`/componentType/delById/${id}`, null, "delete");
}
// 根据主键ID查询用户详情
export function getById(id) {
return axios(`/componentType/${id}`, null, 'get')
}
// 根据主键ID查询用户详情
export function save(dataForm) {
return axios(`/componentType/save`, { dataForm }, 'post')
}
// 根据主键ID查询用户详情
export function update(dataForm) {
return axios(`/componentType/update`, { dataForm }, 'put')
}
2、组件页面
<template>
<div>
<div style="margin-bottom: 15px">
<el-autocomplete
v-model="name"
:fetch-suggestions="querySearch"
:clearable="false"
value-key="value"
class="inline-input w-50"
placeholder="请输入组件名"
@select="handleSelect"
@keyup.enter.native="getDataList(true)"
/>
</div>
<div style="margin-bottom: 15px">
<el-button type="primary" plain @click="handleAdd">新增</el-button>
</div>
<el-space
fill
wrap
:fill-ratio="fillRatio"
:direction="direction"
style="width: 100%; margin-bottom: 15px;"
>
<el-table v-loading="loading" :data="tableData" border>
<template slot="empty">
<el-empty :image-size="100" description="暂无数据"></el-empty>
</template>
<el-table-column label="组件" align="center" prop="name"/>
<el-table-column label="排序" align="center" prop="sort"/>
<el-table-column label="组件图片" align="center" prop="imgUrl">
<template #default="scope">
<el-image style="width: 100px; height: 100px" :src="scope.row.imgUrl" :fit="'fill'" />
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
class-name="small-padding fixed-width"
>
<template #default="scope">
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button link type="primary" size="small" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-space>
<el-space
fill
wrap
:fill-ratio="fillRatio"
:direction="direction"
>
<el-pagination
:hide-on-single-page="hidePageVisible"
v-model:current-page="queryParams.current"
v-model:page-size="queryParams.size"
:page-sizes="[10, 50, 100, 200]"
:small="small"
:disabled="disabled"
:background="background"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-space>
<el-dialog v-model="dialogFormVisible" @close="resetForm(dataFormRef)" :title="title" width="30%" center>
<el-form ref="dataFormRef" :model="dataForm" :rules="rules">
<el-form-item label="组件" :label-width="formLabelWidth" prop="name">
<el-input v-model="dataForm.name" placeholder="请输入组件名称" autocomplete="off" />
</el-form-item>
<el-form-item label="组件类型" :label-width="formLabelWidth" prop="typeId">
<el-select v-model="dataForm.typeId" class="m-2" placeholder="Select" filterable size="large">
<el-option
v-for="item in componentTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- <el-form-item label="排序" :label-width="formLabelWidth" prop="sort">
<el-input v-model="dataForm.sort" placeholder="请输入排序" autocomplete="off" />
</el-form-item>-->
<el-form-item label="组件图片" :label-width="formLabelWidth" prop="imgUrl">
<my-upload-file :imgUrl="dataForm.imgUrl" @uploaded="getImgUrl"></my-upload-file>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeDialog(false, dataFormRef)">取消</el-button>
<el-button type="primary" @click="submitDataForm(dataFormRef)">确认</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import {onMounted, ref, reactive} from 'vue'
import {page as keywordPage} from '@/api/keyword'
import {page as keywordRelationPage} from '@/api/keywordRelation'
import {ElMessage, ElMessageBox, FormInstance, FormRules, UploadProps, UploadUserFile} from "element-plus";
import {page, delById, getById, save, update} from '@/api/component'
import type { UploadFile } from 'element-plus'
import {list} from "@/api/componentType";
interface RestaurantItem {
value: string
count: string
}
const componentTypeOptions = ref([])
const getComponentTypeList = () => {
list().then(resp => {
if (resp.code != 200) return
let options = []
resp.data.forEach((item, index) => {
let option = {
label: item.name,
value: item.id
}
if (index == 0) {
dataForm.typeId = item.id
}
options.push(option)
})
componentTypeOptions.value = options
})
}
onMounted(() => {
getComponentTypeList()
})
const restaurants = ref < RestaurantItem[] > ([])
const querySearch = (queryString: string, cb: any) => {
if (queryString && queryString != '') {
let params = {
current: 1,
size: 10,
keyword: queryString,
}
let keywordList = []
params.keyword = queryString
keywordRelationPage(params).then(res => {
if (res.data.total > 0) {
res.data.records.forEach(item => {
let keyword: object = {
key: item.keyword,
value: item.keywordRelationSentence,
count: item.useCount,
createTime: item.createTime,
}
keywordList.push(keyword)
})
}
const results = queryString
? keywordList.filter(createFilter(queryString))
: keywordList
// call callback function to return suggestions
cb(results)
})
} else {
const results = queryString
? restaurants.value.filter(createInitFilter(queryString))
: restaurants.value
// call callback function to return suggestions
cb(results)
}
}
const createFilter = (queryString: string) => {
return (item) => {
return (
item.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
)
}
}
const createInitFilter = (queryString: string) => {
return (restaurant: RestaurantItem) => {
return (
restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
)
}
}
const loadAll = () => {
let params = {
current: 1,
size: 10,
}
let keywordList = []
keywordPage(params).then(res => {
if (res.data.total > 0) {
res.data.records.forEach(item => {
let keyword = {
value: item.keyword,
count: item.useCount,
createTime: item.createTime,
}
keywordList.push(keyword)
})
}
})
return keywordList;
}
const handleSelect = (item: RestaurantItem) => {
console.log(item, 'restaurantItem')
}
onMounted(() => {
restaurants.value = loadAll()
})
const direction = ref('horizontal')
const fillRatio = ref(30)
const tableData = ref([])
const name = ref('')
const total = ref(0)
const loading = ref('')
const queryParams = ref({
current: 1,
size: 10,
name: '',
})
const small = ref(false)
const background = ref(false)
const disabled = ref(false)
const hidePageVisible = ref(false)
const getDataList = (reset) => {
if (reset) {
queryParams.value.current = 1
}
if (name.value) {
queryParams.value.name = name.value
}
page(queryParams.value).then(resp => {
tableData.value = resp.data.records
total.value = resp.data.total
if (resp.data.total < queryParams.value.size) {
hidePageVisible.value = true
}
})
}
const handleSizeChange = (val: number) => {
console.log(`${val} items per page`)
queryParams.value.size = val
getDataList(true)
}
const handleCurrentChange = (val: number) => {
console.log(`current page: ${val}`)
queryParams.value.current = val
getDataList(false)
}
onMounted(() => {
getDataList(true)
})
const handleDelete = (row: object) => {
ElMessageBox.confirm(
'请确认是否删除该组件?',
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
try {
await delById(row["id"])
ElMessage({
type: 'success',
message: '删除成功!',
})
getDataList(false)
} catch (e) {
console.log(e)
}
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
}
const title = ref('新增组件')
const dialogFormVisible = ref(false)
const formLabelWidth = '140px'
interface DataForm {
id: string,
name: string,
typeId: Number,
sort: Number,
imgUrl: string
}
const dataFormRef = ref<FormInstance>()
const dataForm = reactive<DataForm>({
id: '',
name: '',
typeId: 0,
sort: 0,
imgUrl: ''
})
const rules = reactive<FormRules<DataForm>>({
name: [
{ required: true, message: '请输入组件名', trigger: 'blur' },
],
typeId: [
{ required: true, message: '请选择组件类型', trigger: 'blur' },
],
sort: [
{ required: true, message: '请输入排序', trigger: 'blur' },
],
})
const handleAdd = () => {
title.value = '新增组件'
dialogFormVisible.value = true
dataForm.id = ''
dataForm.name = ''
}
const handleEdit = (row: object) => {
title.value = '修改组件'
getById(row["id"]).then(res => {
if (res.code != 200) {
return
}
dataForm.id = res.data.id
dataForm.name = res.data.name
dataForm.sort = res.data.sort
dataForm.imgUrl = res.data.imgUrl
dialogFormVisible.value = true
})
}
const submitDataForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
if (dataForm.id && dataForm.id != '') {
update(dataForm).then(res => {
if (res.code != 200) {
ElMessage.success("请求错误")
return
}
ElMessage.success("修改成功")
closeDialog(false, formEl)
})
} else {
save(dataForm).then(res => {
if (res.code != 200) {
ElMessage.success("请求错误")
return
}
ElMessage.success("新增成功")
closeDialog(true, formEl)
})
}
} else {
console.log('error submit!', fields)
}
})
}
const closeDialog = (reset: boolean, formEl: FormInstance | undefined) => {
dialogFormVisible.value = false
resetForm(formEl)
getDataList(reset)
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
dataForm.imgUrl = ''
}
import MyUploadFile from "@/components/MyUploadFile.vue";
const getImgUrl = (url: string) => {
dataForm.imgUrl = url
}
</script>
<style scoped>
.el-button--text {
margin-right: 15px;
}
.el-select {
width: 300px;
}
.el-input {
width: 300px;
}
.dialog-footer button:first-child {
margin-right: 10px;
}
::v-deep .el-dialog{
display: flex;
flex-direction: column;
margin:0 !important;
position:absolute;
top:50%;
left:50%;
transform:translate(-50%,-50%);
max-height:calc(100% - 30px);
max-width:calc(100% - 30px);
}
::v-deep .el-dialog .el-dialog__body{
flex:1;
overflow: auto;
}
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
.none-up /deep/ .el-upload--picture-card {
display: none;
}
::v-deep .el-dialog{
display: flex;
flex-direction: column;
margin:0 !important;
position:absolute;
top:50%;
left:50%;
transform:translate(-50%,-50%);
max-height:calc(100%);
max-width:calc(100%);
}
::v-deep .el-dialog .el-dialog__body{
flex:1;
overflow: auto;
}
</style>
2.1、组件接口
import axios from '../utils/http'
// 分页查询组件页码
export function page(params) {
return axios('/component/page', { params }, "get");
}
// 根据主键ID删除用户信息
export function delById(id) {
return axios(`/component/delById/${id}`, null, "delete");
}
// 根据主键ID查询用户详情
export function getById(id) {
return axios(`/component/${id}`, null, 'get')
}
// 根据主键ID查询用户详情
export function save(dataForm) {
return axios(`/component/save`, { dataForm }, 'post')
}
// 根据主键ID查询用户详情
export function update(dataForm) {
return axios(`/component/update`, { dataForm }, 'put')
}
2.2、组件上传(该功能点是照抄的一个网上的文件上传组件,该组件挺牛逼的,实现的效果挺好)
<template>
<div class="uploader">
<input
type="file"
id="file-input"
style="display: none"
accept="image/*"
@change="onImageAdded"
/>
<div
class="card upload-card"
@click="openFileDialog"
v-if="!isThumbnailVisible"
>
<svg
class="icon"
width="28"
height="28"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="#8c939d"
d="M480 480V128a32 32 0 0164 0v352h352a32 32 0 110 64H544v352a32 32 0 11-64 0V544H128a32 32 0 010-64h352z"
></path>
</svg>
</div>
<div class="card thumbnail-card" v-show="isThumbnailVisible">
<img src="" alt="缩略图" id="thumbnail" />
<label class="success-label" v-show="isSuccessLabelVisible"
><i class="success-icon"
><svg
class="icon"
width="12"
height="12"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="white"
d="M406.656 706.944L195.84 496.256a32 32 0 10-45.248 45.248l256 256 512-512a32 32 0 00-45.248-45.248L406.592 706.944z"
></path></svg
></i>
</label>
<!-- 图标 -->
<div class="thumbnail-actions">
<span class="thumbnail-preview" @click="handleThumbnailPreview">
<svg
class="icon"
width="20"
height="20"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="white"
d="M795.904 750.72l124.992 124.928a32 32 0 01-45.248 45.248L750.656 795.904a416 416 0 1145.248-45.248zM480 832a352 352 0 100-704 352 352 0 000 704zm-32-384v-96a32 32 0 0164 0v96h96a32 32 0 010 64h-96v96a32 32 0 01-64 0v-96h-96a32 32 0 010-64h96z"
></path>
</svg>
</span>
<span class="thumbnail-delete" @click="handleThumbnailRemove">
<svg
class="icon"
width="20"
height="20"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="white"
d="M160 256H96a32 32 0 010-64h256V95.936a32 32 0 0132-32h256a32 32 0 0132 32V192h256a32 32 0 110 64h-64v672a32 32 0 01-32 32H192a32 32 0 01-32-32V256zm448-64v-64H416v64h192zM224 896h576V256H224v640zm192-128a32 32 0 01-32-32V416a32 32 0 0164 0v320a32 32 0 01-32 32zm192 0a32 32 0 01-32-32V416a32 32 0 0164 0v320a32 32 0 01-32 32z"
></path>
</svg>
</span>
</div>
<!-- 进度条 -->
<el-progress
type="circle"
:percentage="progress"
v-show="isProgressVisible"
:width="110"
id="progress"
/>
</div>
<vue-easy-lightbox
moveDisabled
:visible="isLightBoxVisible"
:imgs="localImageUrl"
:index="index"
@hide="handleLightboxHide"
/>
</div>
</template>
<script>
import { ref, computed } from "vue";
import { uploadImage } from "../utils/uploadImg";
import { Plus } from "@element-plus/icons-vue";
import VueEasyLightbox from "vue-easy-lightbox";
import {ElMessage, ElMessageBox, FormInstance, FormRules} from "element-plus";
export default {
name: "MyUploadFile",
emits: ["uploaded", "aboutToUpload", "removed"],
components: { Plus, VueEasyLightbox },
props: {
imgUrl: {
type: String,
default: ''
},
},
setup(props, context) {
let progress = ref(0);
let isLightBoxVisible = ref(false);
let isProgressVisible = ref(false);
let isSuccessLabelVisible = ref(false);
let imageUrl = ref("");
let localImageUrl = ref("");
let index = ref(0);
let isThumbnailVisible = computed(() => localImageUrl.value.length > 0);
if (props.imgUrl) {
imageUrl.value = localImageUrl.value = props.imgUrl
setTimeout(() => {
let thumbnailEl = document.getElementById("thumbnail");
if (thumbnailEl) {
thumbnailEl.src = props.imgUrl
}
}, 1000)
}
function openFileDialog() {
document.getElementById("file-input").click();
}
function onImageAdded() {
let fileInput = document.getElementById("file-input");
if (fileInput.files.length == 0) {
return;
}
context.emit("aboutToUpload");
let file = fileInput.files[0];
setImageUrl(URL.createObjectURL(file));
upload(file);
}
function setImageUrl(url) {
let thumbnailEl = document.getElementById("thumbnail");
thumbnailEl.src = localImageUrl.value = url;
}
function handleThumbnailRemove(file) {
imageUrl.value = "";
localImageUrl.value = "";
context.emit("removed", file);
}
function handleThumbnailPreview() {
isLightBoxVisible.value = true;
}
function handleLightboxHide() {
isLightBoxVisible.value = false;
}
function upload(file) {
progress.value = 0;
isProgressVisible.value = true;
isSuccessLabelVisible.value = false;
uploadImage(file, progress).then(
(url) => {
progress.value = 100;
imageUrl.value = url.url;
document.getElementById("thumbnail").src = url.url;
context.emit("uploaded", url.url);
setTimeout(() => {
isProgressVisible.value = false;
isSuccessLabelVisible.value = true;
}, 200);
},
() => {
isProgressVisible.value = false;
localImageUrl.value = "";
context.emit("uploaded", "");
ElMessage.error("哎呀,图片上传出错啦~")
}
);
}
return {
progress,
imageUrl,
localImageUrl,
index,
isLightBoxVisible,
isThumbnailVisible,
isProgressVisible,
isSuccessLabelVisible,
handleThumbnailRemove,
handleThumbnailPreview,
handleLightboxHide,
openFileDialog,
onImageAdded,
setImageUrl,
};
},
};
</script>
<style lang="less" scoped>
.uploader {
display: flex;
}
.card {
background-color: #fbfdff;
border: 1px dashed #c0ccda;
border-radius: 6px;
width: 148px;
height: 148px;
overflow: hidden;
}
.upload-card {
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s;
cursor: pointer;
&:hover {
border-color: #409eff;
color: #409eff;
}
}
.thumbnail-card {
border: 1px solid #c0ccda;
position: relative;
#thumbnail {
width: 100%;
height: 100%;
object-fit: contain;
display: inline;
}
.success-label {
position: absolute;
right: -15px;
top: -6px;
width: 40px;
height: 24px;
background: #67c23a;
text-align: center;
transform: rotate(45deg);
box-shadow: 0 0 1pc 1px #0003;
.success-icon {
position: absolute;
left: 13px;
top: 1px;
transform: rotate(-45deg);
}
}
#progress {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background: rgba(255, 255, 255, 0.7);
:deep(.el-progress-circle) {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.thumbnail-actions {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: all 0.4s ease;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
border-radius: 6px;
.thumbnail-preview,
.thumbnail-delete {
cursor: pointer;
margin: 0 8px;
display: inline-block;
}
&:hover {
opacity: 1;
}
}
}
:deep(.vel-img) {
box-shadow: 0 5px 20px 2px rgba(0, 0, 0, 0.35);
}
</style>
2.3、文件上传
import axios from "axios"
import { ElMessage } from 'element-plus'
const service = axios.create({
baseURL: "https://ip:port/ossClass",
headers: {
"Content-Type": "multipart/form-data"
}
})
service.interceptors.response.use(response => {
const code = response.data.code || 200
if (code === 200) {
return response.data.data
}
let msg = response.data.code + " " + response.data.msg
ElMessage.error(msg)
return Promise.reject('上传图片失败:' + msg)
})
/**
* 上传图片
* @param {File} file 图片文件
* @param {RefImpl} progress 上传进度
* @returns promise
*/
function uploadImage(file, progress) {
let formData = new FormData();
formData.append("file", file)
return service({
url: "/upload",
method: "post",
data: formData,
onUploadProgress(event) {
let v = Math.round(event.loaded / event.total * 100)
progress.value = v == 100 ? 80 : v
},
})
}
export { uploadImage }
3、排序页面
<template>
<div class="component-with-type-box">
<transition-group name="drag" class="row-line">
<ul
v-for="(item, index) in componentTypeList"
class="component-type-box"
:draggable="true"
@dragstart="dragstart(1, index, null)"
@dragenter="dragenterType($event, index, null)"
@dragover="dragover($event)"
@dragend="dragend()"
@mouseover="topicTypeHover($event)"
@mouseout="removeTopicTypeHover($event)"
:key="index"
>
<div class="component-type">
<div class="component-type-title">
{{ item.name }}
</div>
<span class="drag-sort">拖拽换序</span>
</div>
<li
v-for="(component, i) in item.componentList"
class="component-type-info"
:draggable="true"
@dragstart="dragstart(2, index, i)"
@dragenter="dragenter($event, index, i)"
@dragover="dragover($event)"
@dragend="dragend()"
@mouseover="addNumberHover($event)"
@mouseout="removeNumberHover($event)"
:key="i"
>
<el-image style="width: 100px; height: 100px" :src="component.imgUrl" :fit="'fill'" @click="bigImage(component.imgUrl)" />
{{ component ? component.name : "" }}
</li>
<div style="clear: both; height: 0px"></div>
</ul>
</transition-group>
<el-image-viewer
style="z-index:1500"
v-if="showImageViewer"
@close="closeBigImage"
:url-list="imageData"/>
</div>
</template>
<script lang="ts" setup>
import {onMounted, ref, reactive} from 'vue'
import {cwtList} from '@/api/sort'
interface QueryParams {
name: string
}
const queryParams = reactive<QueryParams>({
name: '',
})
const componentTypeList = ref([])
onMounted(() => {
cwtList(queryParams).then(res => {
if (res.code != 200) return
componentTypeList.value = res.data
})
})
const dragType = ref(0)
const typeDefaultIndex = ref(0)
const typeIndex = ref(0)
const dragDefaultIndex = ref(0)
const dragIndex = ref(0)
const updateLi = ref(false)
const typeInfo = ref({})
const moveInfo = ref({})
const showImageViewer = ref(false)
const imageData = ref([])
const bigImage = (imgUrl: any) => {
imageData.value.push(imgUrl)
showImageViewer.value = true
}
const closeBigImage = () => {
showImageViewer.value = false
}
/**
* 拖拽开始
* @param type 类型:1-题型,2-题目
* @param index 题型下标
* @param i 题目下标
*/
const dragstart = (type, index, i) => {
if (dragType.value !== 0) {
if (dragType.value === 1) {
typeDefaultIndex.value = index;
typeIndex.value = index;
typeInfo.value = JSON.parse(JSON.stringify(componentTypeList.value[index]));
}
} else {
dragType.value = type
if (dragType.value === 1) {
typeDefaultIndex.value = index;
typeIndex.value = index;
typeInfo.value = JSON.parse(JSON.stringify(componentTypeList.value[index]));
} else {
updateLi.value = true
typeDefaultIndex.value = index;
typeIndex.value = index;
dragDefaultIndex.value = i;
dragIndex.value = i;
componentTypeList.value[index].componentList[i].sort = "";
moveInfo.value = componentTypeList.value[index].componentList[i];
removeNumberHover(event);
}
}
}
// 拖拽停留位置 --- 增加拖拽效果
const dragover = (event: any) => {
event.preventDefault();
}
// 拖拽鼠标释放
const dragenter = (event: any, index: number, i: number) => {
event.preventDefault();
if (dragType.value === 1) {
componentTypeList.value.splice(typeIndex.value, 1);
componentTypeList.value.splice(index, 0, typeInfo.value);
// 排序变化后目标对象的索引变成源对象的索引
typeIndex.value = index;
} else {
// 避免源对象触发自身的dragenter事件
// 是否跨区域,启示集合下标和移动至集合下标位置不同
if (typeIndex.value !== index) {
componentTypeList.value[typeIndex.value].componentList.splice(dragIndex.value, 1);
componentTypeList.value[index].componentList.splice(i, 0, moveInfo.value);
// 排序变化后目标对象的索引变成源对象的索引
} else if (dragIndex.value !== i) {
componentTypeList.value[typeIndex.value].componentList.splice(dragIndex.value, 1);
componentTypeList.value[typeIndex.value].componentList.splice(i, 0, moveInfo.value);
// 排序变化后目标对象的索引变成源对象的索引
}
typeIndex.value = index;
dragIndex.value = i;
}
}
// 跨类型拖拽
const dragenterType = (event: any, index: number) => {
event.preventDefault();
if (dragType.value === 1) {
// 避免源对象触发自身的dragenter事件
// 是否跨区域,启示集合下标和移动至集合下标位置不同
componentTypeList.value.splice(typeIndex.value, 1);
componentTypeList.value.splice(index, 0, typeInfo.value);
// 排序变化后目标对象的索引变成源对象的索引
typeIndex.value = index;
} else {
// 避免源对象触发自身的dragenter事件
// 是否跨区域,启示集合下标和移动至集合下标位置不同
if (typeIndex.value !== index) {
componentTypeList.value[typeIndex.value].componentList.splice(dragIndex.value, 1);
componentTypeList.value[index].componentList.splice(componentTypeList.value[index].componentList.length, 0, moveInfo.value);
// 排序变化后目标对象的索引变成源对象的索引
typeIndex.value = index;
dragIndex.value = componentTypeList.value[index].componentList.length - 1;
}
}
}
// 拖拽结束
const dragend = () => {
if (dragType.value === 1) {
componentTypeList.value.forEach((item, index) => {
index++;
item.sort = index
/*item.componentList.forEach((component, i) => {
i++;
component.sort = i;
});*/
});
} else {
componentTypeList.value.forEach((item, index) => {
index++;
item.sort = index
/*item.componentList.forEach((component, i) => {
i++;
component.sort = i;
});*/
});
}
dragType.value = 0
}
// 题号鼠标经过时添加样式
const addNumberHover = (event:any) => {
event.currentTarget.className = "component-type-info-hover";
}
// 题号鼠标经过后移除样式
const removeNumberHover = (event:any) => {
event.currentTarget.className = "component-type-info";
}
// 题型经过时添加样式
const topicTypeHover = (event:any) => {
event.currentTarget.className = "component-type-box-hover";
}
// 题型经过后移除样式
const removeTopicTypeHover = (event:any) => {
event.currentTarget.className = "component-type-box";
}
</script>
<style lang="scss" scoped>
$ratio: 0.85;
.component-with-type-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px solid #78edaf;
.component-type-box {
min-width: 300px;
list-style: none;
padding: 10px;
border: 1px solid #78edaf;
.component-type {
height: 40px;
line-height: 40px;
margin-bottom: 10px;
display: flex;
align-items: center;
.component-type-title {
display: flex;
.item_nameBox {
width: 80%;
box-sizing: border-box;
//padding: 15px * $ratio 0 15px * $ratio;
// display: flex;
// flex-direction: column;
.item_title {
}
}
}
.drag-sort {
display: none;
}
}
}
.component-type-box-hover {
min-width: 300px;
list-style: none;
padding: 9px;
border: 1px solid #78edaf;
background: #F8FAFF;
cursor: pointer;
.component-type {
height: 40px;
line-height: 40px;
margin-bottom: 10px;
display: flex;
align-items: center;
.component-type-title {
width: 85%;
display: flex;
.item_nameBox {
width: 80%;
box-sizing: border-box;
//padding: 15px * $ratio 0 15px * $ratio;
// display: flex;
// flex-direction: column;
.item_title {
}
}
.item_title {
display: flex;
}
i {
color: #78edaf
}
}
.drag-sort {
display: inline-block;
width: 60px;
height: 20px;
font-size: 14px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #989898;
line-height: 20px;
}
}
}
.drag-move {
transition: transform 0.3s;
}
.component-type-info {
cursor: pointer;
line-height: 27px;
background: #ffffff;
border-radius: 6px;
border: 1px solid #78edaf;
color: #78edaf;
float: left;
text-align: center;
margin-right: 20px;
margin-bottom: 20px;
display: flex;
flex-direction: column;
::v-deep .el-image {
border-radius: 6px;
}
}
.component-type-info-hover {
cursor: pointer;
line-height: 27px;
border-radius: 6px;
border: 1px solid #78edaf;
float: left;
text-align: center;
margin-right: 20px;
margin-bottom: 20px;
background: #78edaf;
color: #ffffff;
display: flex;
flex-direction: column;
::v-deep .el-image {
border-radius: 6px;
}
}
}
</style>
4、中间界面(意在实现将组件拖拽实现,目前是借鉴网上的代码,后续再实现)
<template>
<div class="box">
<!-- 左侧拖拽组件 -->
<!-- v-if="false" -->
<div class="drap">
<!-- <p>元素</p> -->
<!--
@dragstart < -- 是元素开始拖拽的时候触发
draggable="true" < -- 为了使元素可拖动,把 draggable 属性设置为 true :
@dragover.prevent < -- 阻止浏览器默认行为,不然会显示一个叉叉,不好看, 加上会显示一个添加的符号
-->
<div
v-for="(item, index) in drapLeftElList"
class="drap-item"
:key="index"
@dragstart="handleDrapEvList($event, item)"
@dragover.prevent
draggable="true"
>
<img
class="drap-item-img"
draggable="false"
:src="item.imgUrl"
:alt="item.name"
/>
<div class="drap-item-name">{{ item.name }}</div>
</div>
</div>
<!-- 主体部分 -->
<div
class="drap-container"
@dragover.prevent
@mousedown="laryerMouseDown"
@mousemove="laryerMouseMove"
@mouseup="laryerMouseUp"
@drop="handleDrap"
>
<h1>画布</h1>
<div
v-for="(item, index) in componentsList"
class="drap-container-item"
:class="{
'drap-container-item-active':
curControl && item.identifier == curControl.identifier,
}"
:key="index"
:style="{
top: `${item.position.y}px`,
left: `${item.position.x}px`,
width: `${item.position.w}px`,
height: `${item.position.h}px`,
'background-color': `${item.position.bg}`,
}"
@mousedown.stop="handleMouseDown($event, item, index)"
>
<img
class="drap-item-img"
:src="item.imgUrl"
draggable="false"
:alt="item.name"
/>
<div class="drap-item-name">{{ item.name }}</div>
</div>
</div>
<!-- 属性配置 -->
<div class="drap-right" style="width: 300px; height: 100%">
<h2>属性配置</h2>
{{ identifier }}
<br />
{{ curControl }}
<br />
{{ containerMoveObj }}
</div>
</div>
</template>
<script>
export default {
name: "drap",
data() {
return {
// 保存拖拽的元素的列表
componentsList: [
{
id: 11,
name: "团队1",
imgUrl:
"https://wegosmart.oss-cn-shenzhen.aliyuncs.com/1717066356123369472.png",
sort: 1,
identifier: 666,
position: {
x: 100,
y: 100,
w: 80,
h: 120,
bg: "#ffffff",
},
style: {},
temp: {
position: {
x: 100,
y: 100,
},
},
},
],
// 元件库
drapLeftElList: [
{
id: 11,
name: "团队1",
imgUrl:
"https://wegosmart.oss-cn-shenzhen.aliyuncs.com/1717066356123369472.png",
sort: 1,
position: {
x: 0,
y: 0,
w: 80,
h: 120,
bg: "#fff",
},
temp: {
position: {
x: 0,
y: 0,
},
},
},
{
id: 13,
name: "团队2",
imgUrl:
"https://wegosmart.oss-cn-shenzhen.aliyuncs.com/1717066356123369472.png",
sort: 2,
position: {
x: 0,
y: 0,
w: 80,
h: 120,
bg: "#fff",
},
temp: {
position: {
x: 0,
y: 0,
},
},
},
{
id: 14,
name: "团队3",
imgUrl:
"https://wegosmart.oss-cn-shenzhen.aliyuncs.com/1717066356123369472.png",
sort: 3,
position: {
x: 0,
y: 0,
w: 80,
h: 120,
bg: "#fff",
},
temp: {
position: {
x: 0,
y: 0,
},
},
},
{
id: 15,
name: "团队4",
imgUrl:
"https://wegosmart.oss-cn-shenzhen.aliyuncs.com/1717066356123369472.png",
sort: 3,
position: {
x: 0,
y: 0,
w: 80,
h: 120,
bg: "#fff",
},
temp: {
position: {
x: 0,
y: 0,
},
},
},
],
identifier: "", // 当前项的 唯一标识
curControl: null, //
flag: "",
containerMoveObj: {
type: "",
x: "",
y: "",
},
};
},
methods: {
// 点击画布的时候, 取消选择组件
laryerMouseDown() {
console.log("laryerMouseDown");
this.curControl = null;
},
// 给画布绑定的mousemove事件
laryerMouseMove(ev) {
// 判断是需要移动的类型
if (this.flag == "move") {
// 用当前移动的距离减去点击的位置
let dx = ev.pageX - this.containerMoveObj.x,
dy = ev.pageY - this.containerMoveObj.y;
// 上次旧的位置加上 处理完的距离就得到当前位置
let x = this.curControl.temp.position.x + dx,
y = this.curControl.temp.position.y + dy;
// 这里只是让元素跟着鼠标移动, 如果再这里直接赋值
this.curControl.position.x = x;
this.curControl.position.y = y;
}
},
// 给画布绑定的mouseup事件
laryerMouseUp() {
// 在鼠标抬起的时候判断是否
if (this.flag == "") {
return false;
}
const x = this.curControl.position.x;
const y = this.curControl.position.y;
// 这里才是实际给元素位置赋值的地方!!!!
// 查询是否有对应的模块然后, 对应的赋值
this.componentsList.forEach((item) => {
if (item.identifier == this.identifier) {
console.log(item, "找到了");
item.temp.position.x = x;
item.temp.position.y = y;
item.position.x = x;
item.position.y = y;
}
});
this.flag = "";
},
// 拖拽元素
handleDrapEvList(event, value) {
let { offsetX, offsetY } = event;
var infoJson = JSON.stringify({
...value,
position: {
...value.position,
x: offsetX,
y: offsetY,
},
});
// 将数据绑定到dataTransfer身上
event.dataTransfer.setData("drapData", infoJson);
},
// 监听拖拽元素结束
handleDrap(event) {
event.preventDefault();
const value = event.dataTransfer.getData("drapData");
// 获取绑定到拖拽元素身上的 drapData属性
if (value) {
let drapData = JSON.parse(value);
const { position } = drapData;
const identifier = Math.floor(Math.random() * 10000);
this.componentsList.push({
...drapData,
identifier,
position: {
...position,
x: event.offsetX - position.x,
y: event.offsetY - position.y,
},
temp: {
position: {
x: event.offsetX - position.x,
y: event.offsetY - position.y,
},
},
});
}
},
// 点击元素获取组件配置
handleClickTarget(row, index) {
console.log(row);
this.identifier = row.identifier;
this.curControl = row;
},
// 移动元素
handleMouseDown(e, row, index) {
this.flag = "move";
// 获取组件配置, 为接下来的属性配置做准备
this.handleClickTarget(row, index);
e = e || window.event;
// 记录下当前点击的位置
this.containerMoveObj.x = e.pageX;
this.containerMoveObj.y = e.pageY;
},
},
};
</script>
<style lang="scss">
.box {
display: flex;
flex-direction: row;
align-items: center;
position: relative;
height: 500px;
.drap {
width: 300px;
height: 500px;
background: #f2f2f2;
display: flex;
flex-direction: row;
flex-wrap: wrap;
cursor: pointer;
.drap-item {
height: 120px;
margin-right: 20px;
.drap-item-img {
display: block;
width: 80px;
height: 80px;
}
.drap-item-name {
text-align: center;
}
}
}
.drap-container {
flex: 1;
height: 500px;
background: #ccc;
position: relative;
.drap-container-item {
-webkit-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
position: absolute;
user-select: none;
cursor: pointer;
border: 1px solid transparent;
.drap-item-img {
display: block;
width: 100%;
// height: 80px;
user-select: none;
}
.drap-item-name {
text-align: center;
}
}
.drap-container-item-active {
border: 1px solid skyblue;
}
}
}
</style>
将组件加入首页展示
<template>
<nav>
<router-link to="/">Home</router-link>
| <router-link to="/about">About</router-link>
| <router-link to="/componentType">componentType</router-link>
| <router-link to="/component">component</router-link>
| <router-link to="/sort">sort</router-link>
| <router-link to="/center">center</router-link>
</nav>
<router-view/>
</template>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>
路由也需要添加
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
},
{
path: '/componentType',
name: 'componentType',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/componentType/ComponentTypeIndex.vue')
},
{
path: '/component',
name: 'component',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/component/ComponentIndex.vue')
},
{
path: '/sort',
name: 'sort',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/sort/sort.vue')
},
{
path: '/center',
name: 'center',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/center/center.vue')
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router