前言
项目仓库
本项目是为开发一套容器化的开发、运行、测试环境,用以支持Web开发、程序设计等课程的实验教学。
代码内容均为我和肖同学共同完成。
任务目标
设计并且编写主界面
主界面内容:
- 学生界面
- 项目中心
- 课程任务
- 课程通知
- 我的项目
- 上传中心
- 编辑中心
- 组织中心
- 项目中心
- 老师界面
- 组织中心
- 发布中心
- 学生课程作业
我的任务
- 页面外观与布局设计
- 页面交互设计
- 页面编写
展示内容
- 学生端 Layout
- 学生端App.vue
- 学生端router
代码
App.vue
<!--按照路由进行画面跳转-->
<template>
<div>
<router-view/>
</div>
</template>
router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import codingViewVue from '../views/coding-view.vue';
import registerViewVue from '../views/register-view.vue';
import loginViewVue from '../views/login-view.vue';
import forgetpasswViewVue from '../views/forgetpassw-view.vue';
import updatepasswViewVue from '../views/updatepassw-view.vue';
import personalViewVue from '../views/personal-view.vue';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
component: loginViewVue,
meta:{
isLogin:false,
}
},
{
path: "/register",
name: "register-view",
component: registerViewVue,
meta:{
isLogin:false,
}
},
{
path: "/forget_password",
name: "forgetpassw-view",
component: forgetpasswViewVue,
meta:{
isLogin:false,
}
},
{
path: "/update_password",
name: "update_password",
component: updatepasswViewVue,
meta:{
isLogin:true,
}
},
{
path: "/coding",
name: "coding",
component:codingViewVue,
meta:{
isLogin:true,
}
},
{
path: "/personal",
name: "personal",
component:personalViewVue,
meta:{
isLogin:true,
}
},
],
});
export default router;
layout/topmenu.vue
<template>
<el-menu :default-active="$props.activeIndex" class="el-menu-demo" mode="horizontal" @select="handleSelect">
<el-menu-item index="1" disabled>WebLab</el-menu-item>
<el-menu-item index="2" >项目中心</el-menu-item>
<el-menu-item index="3">上传中心</el-menu-item>
<el-menu-item index="4">编辑中心</el-menu-item>
<!-- <el-sub-menu index="4">-->
<!-- <template #title>待补充</template>-->
<!-- <el-menu-item index="4-1">item one</el-menu-item>-->
<!-- <el-menu-item index="4-2">item two</el-menu-item>-->
<!-- <el-menu-item index="4-3">item three</el-menu-item>-->
<!-- <el-sub-menu index="4-4">-->
<!-- <template #title>item four</template>-->
<!-- <el-menu-item index="4-4-1">item one</el-menu-item>-->
<!-- <el-menu-item index="4-4-2">item two</el-menu-item>-->
<!-- <el-menu-item index="4-4-3">item three</el-menu-item>-->
<!-- </el-sub-menu>-->
<!-- </el-sub-menu>-->
<el-menu-item index="5">组织中心</el-menu-item>
<el-sub-menu index="6" style="position: absolute;right:15px;">
<template #title>
<div class="block">
<el-avatar :size="45" :icon="UserFilled" fit :src="avatarUrl" />
</div>
</template>
<el-menu-item index="5-1" @click="personal()">个人资料更新</el-menu-item>
<el-menu-item index="5-2" @click="updatePassword()">更新密码</el-menu-item>
<!-- <el-menu-item index="5-2">item two</el-menu-item> -->
<el-menu-item index="5-3" @click="logOut">登出</el-menu-item>
</el-sub-menu>
</el-menu>
<div class="h-6"></div>
</template>
<script lang="ts">
import { UserFilled } from '@element-plus/icons-vue'
import { ref, defineComponent } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from "vue-router"
import { useLoginStore } from '@/stores/store';
import { request } from '@/network/request';
export default defineComponent({
props: {
activeIndex: String
},
emits:['onIndexChange'],
setup(props,context) {
const store = useLoginStore();
const router = useRouter();
// const activeIndex = ref(props.activeIndex);
const activeIndex2 = ref('2');
const avatarUrl=ref('');
request('/weblab/user/getUserWithInfo', undefined)
.then(res => {
if (res.data.msg == 'success') {
const data = res.data.pkg;
if(data.headImg!=null)
avatarUrl.value = data.headImg;
}
})
const handleSelect = (key: string, keyPath: string[]) => {
// activeIndex.value = key;
context.emit('onIndexChange',key);
}
function logOut() {
ElMessageBox.confirm(
'是否确认退出?',
'退出提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
}
)
.then(() => {
// store.userLogout();
// router.push({ path: '/' });
let param = new FormData();
request('/weblab/user/logout', param, store.getToken)
.then(res => {
store.userLogout();
router.push({ path: '/' });
// if (res.status == 200 && res.data.msg == 'success') {
// }
})
.catch(error => {
console.log(error);
})
})
.catch(() => {
//点击取消
// activeIndex.value = '2';
context.emit('onIndexChange','2');
})
}
function updatePassword() {
router.push({ path: '/update_password' });
}
function personal() {
router.push({ path: '/personal' });
}
return {
// activeIndex,
activeIndex2,
handleSelect,
logOut,
updatePassword,
UserFilled,
personal,
avatarUrl
}
},
});
</script>
<style>
</style>
layout/sidecolumn.vue
<template>
<div class="custom-tree-container">
<el-menu default-active="2" class="el-menu-vertical-demo">
<el-input v-model="query" placeholder="输入关键词" style="width: 220px" />
<el-tree ref="treeRef" :data="dataSource" :props="props" :filter-node-method="filterMethod" :height="720"
node-key="id" default-expand-all @node-contextmenu="RightClick" @node-click="LeftClick" highlight-current>
<template #default="{ node, data }">
<el-icon>
<folder v-if="data.type == FileType.folder || data.type == FileType.root"></folder>
<document v-else></document>
</el-icon>
<span>{{ node.label }}</span>
</template>
</el-tree>
<el-card v-show="visible" id="menu" shadow="always"
:style="{ position: 'fixed', left: mouseX + 'px', top: mouseY + 'px', zIndex: '999', cursor: 'pointer' }">
<div>
<el-button type="text" @click="AddFoloder()"
v-show="targetData?.type === FileType.folder || targetData?.type === FileType.root">添加文件夹
</el-button>
</div>
<div>
<el-button type="text" @click="AddFile()"
v-show="targetData?.type === FileType.folder || targetData?.type === FileType.root">添加文件
</el-button>
</div>
<div>
<el-button type="text" @click="Delete()" v-show="targetData?.type != FileType.root">删除</el-button>
</div>
<div>
<el-button type="text" @click="Rename()">重命名</el-button>
</div>
</el-card>
</el-menu>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import type { ElTree } from 'element-plus'
import type Node from 'element-plus/es/components/tree/src/model/node'
import { ElMessage, ElMessageBox } from 'element-plus'
import { fileTypes, FileType, ziper } from '../fileziper';
import { Folder, Document } from '@element-plus/icons-vue';
interface Tree {
id: string
name: string
type: FileType
children: Tree[]
value?: string
}
const visible = ref(false)
const query = ref('')
const treeRef = ref<InstanceType<typeof ElTree>>()
const targetData = ref<Tree>()
const targetNode = ref<Node>()
// const selectedData = ref<Tree>()
const totalId = ref(0);
const mouseX = ref(0)
const mouseY = ref(0)
const props = {
value: 'id',
label: 'name',
children: 'children',
}
const dataSource = ref<Tree[]>
([
{
id: `${totalId.value++}`, name: 'Web', type: FileType.root, children:
[
{
id: `${totalId.value++}`, name: 'src', type: FileType.folder, children:
[
{
id: `${totalId.value++}`, name: 'main.js', type: FileType.js, children: [], value: ''
},
{
id: `${totalId.value++}`, name: 'main.html', type: FileType.html, children: [], value: ''
},
{
id: `${totalId.value++}`, name: 'main.css', type: FileType.css, children: [], value: ''
},
{
id: `${totalId.value++}`, name: 'README.md', type: FileType.md, children: [], value: ''
}
]
},
{
id: `${totalId.value++}`, name: 'public', type: FileType.folder, children:
[
{
id: `${totalId.value++}`, name: 'README.md', type: FileType.md, children: [], value: ''
}
]
}
]
}
])
watch(query, (val) => {
treeRef.value!.filter(val)
})
const filterMethod = (query: string, data: Tree) => {
if (!query) return true;
return data.name!.includes(query)
}
const RightClick = function (e: PointerEvent, data: Tree, node: Node) {
visible.value = true;
targetData.value = data;
targetNode.value = node;
mouseX.value = e.clientX;
mouseY.value = e.clientY;
document.addEventListener('click', cancelRightClick)
}
const LeftClick = function (data: Tree) {
visible.value = false;
if (data.type != FileType.folder && data.type != FileType.root) {
// selectedData.value = data;
emit("setFileContext", data.name, data.id, data.value, data.type);
} else {
// selectedData.value = undefined;
}
}
const cancelRightClick = function () {
visible.value = false;
document.removeEventListener('click', cancelRightClick);
}
const AddFile = () => {
ElMessageBox.prompt('输入文件名称(name.type)', '添加新文件', {
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
inputPattern:
/[\w!#$%&'*+/=?^_`{|}~-]+(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?/,
inputErrorMessage: '请正确输入文件名称(name.type)',
})
.then(({ value }) => {
const val = value.split(".");
let type = val[val.length - 1];
let fileType = fileTypes(type);
if (fileType != undefined) {
if (targetData.value != null) {
for (let i = 0; i < targetData.value.children.length; i++) {
if (fileType == targetData.value.children[i].type && value == targetData.value.children[i].name)
throw false;
}
}
targetData.value!.children.push({
id: `${totalId.value++}`,
name: value,
type: fileType,
children: [],
value: ''
})
dataSource.value = [...dataSource.value]
} else {
ElMessage({
type: 'error',
message: `暂不支持该类型文件`,
})
}
// ElMessage({
// type: 'success',
// message: `Your file is:${value}`,
// })
})
.catch(() => {
ElMessage({
type: 'error',
message: "文件已存在",
})
})
}
const AddFoloder = () => {
ElMessageBox.prompt('输入文件夹名称', '添加文件夹', {
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
inputPattern:
/[\w!#$%&'*+/=?^_`{|}~-]?/,
inputErrorMessage: '请输入文件夹名称',
})
.then(({ value }) => {
if (targetData.value != null) {
for (let i = 0; i < targetData.value.children.length; i++) {
if (targetData.value.children[i].type == FileType.folder && value == targetData.value.children[i].name)
throw false;
}
}
targetData.value!.children.push({
id: `${totalId.value++}`,
name: value,
type: FileType.folder,
children: []
})
dataSource.value = [...dataSource.value]
// ElMessage({
// type: 'success',
// message: `Your folder is:${value}`,
// })
})
.catch(() => {
ElMessage({
type: 'error',
message: "文件夹已经存在",
})
})
}
const Delete = function () {
ElMessageBox.confirm('将删除该文件,继续?', 'Warning',
{
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
})
.then(() => {
if (targetData.value?.type == FileType.folder) {
deleteFile(targetData.value.children);
} else {
emit('deteleFile', targetData.value?.id);
}
const parent = targetNode.value?.parent
const children: Tree[] = parent?.data.children || parent?.data
const index = children.findIndex((d) => d.id === targetData.value?.id)
children.splice(index, 1)
dataSource.value = [...dataSource.value]
// selectedData.value = undefined;
})
.catch(() => {
})
}
const deleteFile = (data: Tree[]) => {
for (var item in data) {
if (data[item].type == FileType.folder) {
deleteFile(data[item].children);
} else {
emit('deteleFile', data[item].id);
}
}
}
const Rename = function () {
const isFolder = (targetData.value?.type === FileType.folder) || (targetData.value?.type === FileType.root);
ElMessageBox.prompt('输入新名称', '重命名', {
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
inputPattern: isFolder
? /[\w!#$%&'*+/=?^_`{|}~-]?/ : /[\w!#$%&'*+/=?^_`{|}~-]+(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?/,
inputErrorMessage: '请正确输入新名称',
})
.then(({ value }) => {
let fileType;
if (targetNode.value?.data.type != FileType.root) {
const pa = targetNode.value?.parent.data.children;
if (!isFolder) {
const val = value.split(".");
let type = val[val.length - 1];
fileType = fileTypes(type);
for (let i = 0; i < pa.length; i++) {
if (value == pa[i].name && fileType == pa[i].type)
throw '文件';
}
} else {
for (let i = 0; i < pa.length; i++) {
if (pa[i].type == FileType.folder && value == pa[i].name)
throw '文件夹';
}
fileType = FileType.folder;
}
}else{
fileType=FileType.root;
}
if (fileType != undefined) {
emit('rename', targetData.value?.id, value);
targetData.value!.name = value;
if (targetData.value?.id == '0') {
ziper.setProjectName(value);
}
dataSource.value = [...dataSource.value]
} else {
ElMessage({
type: 'error',
message: `暂不支持该类型文件`,
})
}
// ElMessage({
// type: 'success',
// message: `Your folder is:${value}`,
// })
})
.catch((e) => {
ElMessage({
type: 'error',
message: `${e}已存在`,
})
})
}
const emit = defineEmits(['setFileContext', 'deteleFile', 'rename']);
const setCode = (id: string, value: string) => {
// if (selectedData.value != null) {
// selectedData.value.value = value;
// }
const node = treeRef.value?.getNode(id);
if (node) {
node.data.value = value;
}
}
const getCode = (id: string) => {
const node = treeRef.value?.getNode(id);
return { "code": node?.data.value, "type": node?.data.type };
}
const getData = () => {
return dataSource.value[0].children;
}
const getProjectName = () => {
return dataSource.value[0].name;
}
defineExpose({ setCode, getData, getProjectName, getCode })
</script>
<style>
.dialog-footer button:first-child {
margin-right: 10px;
}
</style>