可持续发展项目(九):前端页面实现


前言

为自己搭建一个可以自我思考的平台,其核心为“心想事成”。


一、思考过程?

此时是我已实现过得一个前端案例,其中借鉴网上的拖拽事件的介绍实现的功能。

二、完善

搭建时遇到过的问题。

其中使用图片上传功能,我想着是上传到本地的前端项目的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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值