complete rightclick menu

master
liuchen 2 weeks ago
parent cdfd97ab47
commit 7e0b3b9682

@ -1,13 +1,11 @@
<template> <template>
<div class="home"> <div class="home">
<div class="container"> <div class="card col-md-10 offset-md-1">
<div class="card">
<div class="card-body"> <div class="card-body">
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>

@ -1,72 +0,0 @@
<template>
<ContentBase>
<div class="file-item" v-for="item in items" :key="item.name">
<div class="file-item">
<div @dblclick="handledouleclick(item.name,item.type)" class="file-item-icon">
<img v-if="item.type==='folder'" src="@/assets/folder_icon.png" alt="">
<img v-else src="@/assets/file_icon.png" alt="">
</div>
<div class="file-item-title">
{{ item.name }}
</div>
</div>
</div>
</ContentBase>
</template>
<script>
import ContentBase from './ContentBase';
export default {
name: 'ContentShow',
components:{
ContentBase,
},
props:{
items:{
type:Object,
required:true,
}
},
setup(props,context){
const handledouleclick = (name,type) => {
context.emit("handledoubleclick",name,type);
}
return {
handledouleclick,
}
},
}
</script>
<style scoped>
.file-item{
width: 8vw;
display: inline-block;
}
.file-item {
width: 8vw;
height: 8vw;
overflow: hidden;
display: inline-block;
margin: 0 0.1vw 1vw 0.1vw;
border: 0.1vh solid white;
}
.file-item-icon > img {
width: 6vw;
height: 6vw;
}
.file-item-title{
width: 6vw;
height: 4vw;
text-align: center;
overflow: hidden;
font-size: 1.2vw;
word-break: break-all;
}
</style>

@ -0,0 +1,92 @@
<template>
<div @click="leftclick()" @dblclick="doubleclick()" @contextmenu.stop.prevent="rightclick($event)"
:class="{ 'file-item-selected' : is_selected }"
>
<div class="file-item-icon" >
<img src="@/assets/file_icon.png" alt="">
</div>
<FileTitle>
{{ item.name }}
</FileTitle>
</div>
</template>
<script>
import FileTitle from './FileTitle.vue';
import { ref } from 'vue';
import { useStore } from 'vuex';
export default {
name: 'FileFiled',
components:{
FileTitle,
},
props:{
item:{
type:Object,
required:true,
}
},
setup(props,context){
let store = useStore();
let is_selected = ref(false);
const leftclick = () => {
is_selected.value = true;
}
const rightclick = (event) => {
context.emit('filerightclick',{
item:props.item,
menuposition:{
top: event.clientY + 'px',
left: event.clientX + 'px',
},
});
}
const doubleclick = () => {
store.commit('forwardPath',props.item.name + '/');
}
return {
leftclick,
rightclick,
doubleclick,
is_selected,
}
}
}
</script>
<style scoped>
.file-item {
width: 8vw;
height: 8vw;
overflow: hidden;
display: inline-block;
margin: 0 0.1vw 1vw 0.1vw;
border: 0.1vh solid white;
}
.file-item-icon > img {
width: 6vw;
height: 6vw;
}
.file-item-title{
width: 6vw;
height: 4vw;
text-align: center;
overflow: hidden;
font-size: 1.2vw;
word-break: break-all;
}
.file-item-selected {
border: 0.1vh solid #99d1ff;
background: #cce8ff;
}
</style>

@ -0,0 +1,43 @@
<template>
<div class="file-item-title">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'FileTitle',
}
</script>
<style scoped>
.file-item {
width: 8vw;
height: 8vw;
overflow: hidden;
display: inline-block;
margin: 0 0.1vw 1vw 0.1vw;
border: 0.1vh solid white;
}
.file-item-icon > img {
width: 6vw;
height: 6vw;
}
.file-item-title{
width: 6vw;
height: 4vw;
text-align: center;
overflow: hidden;
font-size: 1.2vw;
word-break: break-all;
}
.file-item-selected {
border: 0.1vh solid #99d1ff;
background: #cce8ff;
}
</style>

@ -0,0 +1,91 @@
<template>
<div @click="leftclick()" @dblclick="doubleclick()" @contextmenu.stop.prevent="rightclick($event)"
:class="{ 'file-item-selected' : is_selected }"
>
<div class="file-item-icon" >
<img src="@/assets/folder_icon.png" alt="">
</div>
<FileTitle>
{{ item.name }}
</FileTitle>
</div>
</template>
<script>
import FileTitle from './FileTitle.vue';
import { useStore } from 'vuex';
import { ref } from 'vue';
export default {
name: 'FolderFiled',
components:{
FileTitle,
},
props:{
item:{
type:Object,
required:true,
}
},
setup(props,context){
let store = useStore();
let is_selected = ref(false);
const leftclick = () => {
is_selected.value = true;
}
const rightclick = (event) => {
context.emit('filerightclick',{
item:props.item,
menuposition:{
top: event.clientY + 'px',
left: event.clientX + 'px',
},
})
}
const doubleclick = () => {
store.commit('forwardPath',props.item.name + '/');
}
return {
leftclick,
rightclick,
doubleclick,
is_selected,
}
}
}
</script>
<style scoped>
.file-item {
width: 8vw;
height: 8vw;
overflow: hidden;
display: inline-block;
margin: 0 0.1vw 1vw 0.1vw;
border: 0.1vh solid white;
}
.file-item-icon > img {
width: 6vw;
height: 6vw;
}
.file-item-title{
width: 6vw;
height: 4vw;
text-align: center;
overflow: hidden;
font-size: 1.2vw;
word-break: break-all;
}
.file-item-selected {
border: 0.1vh solid #99d1ff;
background: #cce8ff;
}
</style>

@ -1,5 +1,5 @@
<template> <template>
<nav class="nav"> <!-- <nav class="nav">
<a class="nav-link" href="#"> <a class="nav-link" href="#">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"/> <path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"/>
@ -10,12 +10,47 @@
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z"/> <path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z"/>
</svg> </svg>
</a> </a>
</nav> -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<router-link class="navbar-brand" :to="{name: 'home'}">Coeditor</router-link>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav ms-auto" >
<li class="nav-item">
<router-link v-if="!$store.state.user.is_login" class="nav-link" :to="{name: 'login'}"></router-link>
<router-link v-else class="nav-link" :to="{name: 'userprofile',params:{userId: $store.state.user.id}}">{{ $store.state.user.username }}</router-link>
</li>
<li class="nav-item">
<router-link v-if="!$store.state.user.is_login" class="nav-link" :to="{name: 'register'}"></router-link>
<a v-else class="nav-link" style="cursor: pointer" @click="logout">退</a>
</li>
</ul>
</div>
</div>
</nav> </nav>
</template> </template>
<script> <script>
import { useStore } from 'vuex';
import router from '../router/index.js';
export default ({ export default ({
name:"NavBar", name:"NavBar",
setup(){
const store = useStore();
// store.commit('dd');
const logout = () => {
store.commit('logout');
router.push('/');
location.reload();
}
return {
store,
logout,
}
}
}) })
</script> </script>

@ -0,0 +1,130 @@
<template>
<div>
<ul v-if="menutype === 'filemenu'" class="file-menu" :style="{top:menuposition.top,left:menuposition.left}">
<li>
<div @click="open_file">
打开
</div>
</li>
<li>
<div>
复制
</div>
</li>
<li>
<div>
粘贴
</div>
</li>
<li>
<div>
重命名
</div>
</li>
<li>
<div>
删除
</div>
</li>
</ul>
<ul v-else class="context-menu" :style="{top:menuposition.top,left:menuposition.left}">
<li>
<div>
新建
</div>
</li>
<li>
<div>
刷新
</div>
</li>
<li>
<div>
粘贴
</div>
</li>
<li @click="turn_back">
后退
</li>
</ul>
</div>
</template>
<script>
// import { computed } from 'vue';
export default {
name:"RightMenu",
props:{
menuposition:{
type:Object,
required:true,
},
menutype:{
type:String,
required:true,
}
},
setup(props,context){
const open_file = () => {
context.emit('open_file');
}
const turn_back = () => {
context.emit('turn_back');
}
return {
open_file,
turn_back,
}
}
}
</script>
<style scoped>
.file-menu {
/* 菜单样式 */
background-color: pink;
border: 1px solid #ccc;
position: fixed;
padding: 0; /* 移除默认的内边距 */
margin: 0;
list-style: none;
/* 其他样式 */
}
.file-menu > li {
padding: 10px; /* 添加内边距以确保内容不紧贴边框 */
text-align: left; /* 内容左对齐 */
width: 100%; /* 列表项占据父容器的全部宽度 */
box-sizing: border-box; /* 包含内边距和边框在内计算元素的总宽度 */
background-color: #f9f9f9; /* 可选:为列表项添加背景色 */
border-bottom: 1px solid #ddd; /* 可选:为列表项添加底部边框 */
cursor:pointer;
}
.context-menu {
/* 菜单样式 */
background-color: pink;
border: 1px solid #ccc;
position: fixed;
padding: 0; /* 移除默认的内边距 */
margin: 0;
list-style: none;
/* 其他样式 */
}
.context-menu > li {
padding: 10px; /* 添加内边距以确保内容不紧贴边框 */
text-align: left; /* 内容左对齐 */
width: 100%; /* 列表项占据父容器的全部宽度 */
box-sizing: border-box; /* 包含内边距和边框在内计算元素的总宽度 */
background-color: #f9f9f9; /* 可选:为列表项添加背景色 */
border-bottom: 1px solid #ddd; /* 可选:为列表项添加底部边框 */
cursor:pointer;
}
</style>

@ -1,11 +1,39 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView'
import RegisterView from '../views/RegisterView'
import NotFoundView from '../views/NotFoundView'
import UserProfileView from '../views/UserProfileView'
const routes = [ const routes = [
{ {
path: '/', path: '/',
name: 'home', name: 'home',
component: HomeView component: HomeView,
},
{
path: '/login/',
name: 'login',
component: LoginView,
},
{
path: '/register/',
name: 'register',
component: RegisterView,
},
{
path: '/userprofile/',
name:'userprofile',
component:UserProfileView,
},
{
path: '/404/',
name: '404',
component: NotFoundView
},
{
path: '/:catchAll(.*)',
redirect: '/404/',
} }
] ]

@ -1,20 +1,71 @@
// import $ from 'jquery'; import $ from 'jquery';
// import { jwtDecode } from 'jwt-decode';
const ModulerUser = { const ModulerUser = {
state: { state: {
id:'',
username:'', username:'',
photo:'',
access: "", access: "",
refresh: "", refresh: "",
is_login: false, is_login: false,
path:['~/users/'],
}, },
getters: { getters: {
}, },
mutations: { mutations: {
updateUser(state,user){
state.username = user.username,
state.access = user.access,
state.refresh = user.refresh,
state.is_login = user.is_login;
},
logout(state){
state.username = '',
state.access = "",
state.refresh = "",
state.is_login = false,
state.path = ['~/users/']
},
forwardPath(state,item){
state.path.push(item);
},
backPath(state){
state.path.pop();
}
}, },
actions: { actions: {
login(context,data){
$.ajax({
url:'http://47.106.113.194:8000/token/',
type:'POST',
data:{
username:data.username,
password:data.password,
},
success:resp => {
const {access,refresh} = resp;
$.ajax({
url:' http://47.106.113.194:8000/getinfo/',
type:'GET',
headers:{
'Authorization': "Bearer " + access,
},
success(resp){
context.commit('updateUser',{
...resp,
access:access,
refresh:refresh,
is_login: true,
});
context.commit('forwardPath',resp.username + '/'
);
data.success();
}
});
},
error(){
data.error();
}
});
}
}, },
modules: { modules: {
} }

@ -1,49 +1,149 @@
<template> <template>
<ContentShow @handledoubleclick="handledoubleclick" :items="items"> <ContentBase @contextmenu.prevent="rightclick($event)">
<div v-for="item in items" :key="item.name" class="file-item">
</ContentShow> <FolderFiled @filerightclick="filerightclick" @ls="ls" v-if="item.type==='folder'" :item="item" />
<FileFiled @filerightclick="filerightclick" v-if="item.type==='file'" :item="item" />
</div>
<RightMenu v-if="menuvisible" @open_file="open_file" @turn_back="turn_back" :item="item" :menutype="menutype" :menuposition="menuposition" />
</ContentBase>
</template> </template>
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import ContentShow from '@/components/ContentShow'; import ContentBase from '../components/ContentBase';
import FileFiled from '../components/FileFiled';
import FolderFiled from '../components/FolderFiled';
import RightMenu from '../components/RightMenu.vue';
import { ref } from 'vue'; import { ref } from 'vue';
import { useStore } from 'vuex';
import router from '@/router/index.js';
export default { export default {
name: 'HomeView', name: 'HomeView',
components:{ components:{
ContentShow, ContentBase,
FileFiled,
FolderFiled,
RightMenu,
}, },
setup(){ setup(){
let store = useStore();
let items = ref([]); let items = ref([]);
let path = ['~/']; let menuvisible = ref(false);
let menutype = ref('filemenu');
let menuposition = ref({
top:"",
left:"",
});
let item = null;
const ls = () => { const ls = () => {
menuvisible.value = false;
$.ajax({ $.ajax({
url:'https://app7179.acapp.acwing.com.cn/filesystem/ls/', url:' http://47.106.113.194:8000/operation/command/ls/',
type:'get', type:'get',
data:{ data:{
'path':path.join(""), 'path':store.state.user.path.join(""),
}, },
success:resp => { success:resp => {
items.value = resp.items; let tmp = [];
for (let i = 0;i < resp.items.length;i++) {
let item = resp.items[i];
item.is_selected = false;
tmp.push(item);
}
items.value = tmp;
} }
}); });
} }
ls();
const handledoubleclick = (name,type) => { const select_item = (item) => {
if (type === "folder"){ let tmp = items.value;
path.push(name + "/"); for (let i = 0;i < tmp.length;i++){
if (tmp[i] === item){
tmp[i].is_selected = true;
}
else {
tmp[i].is_selected = false;
}
}
items.value = tmp;
}
const open_file = () => {
menuvisible.value = false;
menutype.value = "filemenu";
if (!item){
return;
}
if (item.type === 'folder'){
store.commit('forwardPath',item.name + '/'
);
ls(); ls();
} }
else { else {
console.log(name); $.ajax({
url:' http://47.106.113.194:8000/operation/open/',
type:'get',
data:{
'path':store.state.user.path.join("") + item.name,
},
success:resp => {
console.log(resp);
} }
});
} }
}
// const add_file = () => {
// }
const turn_back = () => {
console.log('turn_back',store.state.user.path.join(""));
store.commit('backPath');
ls();
}
const rightclick = (event) => {
menuvisible.value = true;
menutype.value = "contextmenu";
menuposition.value = {
top: event.clientY + 'px',
left: event.clientX + 'px',
};
}
const filerightclick = (data) => {
menutype.value = "filemenu";
menuposition.value = data.menuposition;
menuvisible.value = true;
item = data.item;
select_item(item);
}
if (store.state.user.is_login){
ls();
}
else {
router.push({name:'login'});
}
return { return {
items, items,
item,
ls, ls,
handledoubleclick, filerightclick,
rightclick,
open_file,
turn_back,
menuvisible,
menuposition,
menutype,
} }
}, },
} }
@ -52,3 +152,36 @@ export default {
<style scoped> <style scoped>
</style> </style>
<style scoped>
.file-item {
width: 8vw;
height: 8vw;
overflow: hidden;
display: inline-block;
margin: 0 0.1vw 1vw 0.1vw;
border: 0.1vh solid white;
}
.file-item-icon > img {
width: 6vw;
height: 6vw;
}
.file-item-title{
width: 6vw;
height: 4vw;
text-align: center;
overflow: hidden;
font-size: 1.2vw;
word-break: break-all;
}
.file-item-selected {
border: 0.1vh solid #99d1ff;
background: #cce8ff;
}
</style>

@ -0,0 +1,73 @@
<template>
<ContentBase>
<div class="row justify-content-md-center">
<div class="col-3">
<form @submit.prevent="login">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input v-model="username" type="text" class="form-control" id="username">
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input v-model="password" type="password" class="form-control" id="password">
</div>
<div class="error-message">{{ error_message }}</div>
<button type="submit" class="btn btn-primary">登录</button>
</form>
</div>
</div>
</ContentBase>
</template>
<script>
import ContentBase from '../components/ContentBase';
import { ref } from 'vue';
import { useStore } from 'vuex';
import router from '@/router/index.js';
export default {
name: 'LoginView',
components: {
ContentBase,
},
setup(){
let store = useStore();
let username = ref('123');
let password = ref('123');
let error_message = ref('');
const login = () => {
// http://47.106.113.194:8000/token/
store.dispatch('login',{
username:username.value,
password:password.value,
success(){
error_message.value = '';
router.push({name:'home'});
},
error(){
error_message.value = "用户名或密码错误";
}
});
};
return {
username,
password,
error_message,
login,
}
}
}
</script>
<style scoped>
button {
width: 100%;
}
.error-message {
color: red;
}
</style>

@ -0,0 +1,19 @@
<template>
<ContentBase>
404
</ContentBase>
</template>
<script>
import ContentBase from '../components/ContentBase';
export default {
name: 'NotFoundView',
components: {
ContentBase,
}
}
</script>
<style scoped>
</style>

@ -0,0 +1,96 @@
<template>
<ContentBase>
<div class="row justify-content-md-center">
<div class="col-3">
<form @submit.prevent="register">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input v-model="username" type="text" class="form-control" id="username">
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input v-model="password" type="password" class="form-control" id="password">
</div>
<div class="mb-3">
<label for="password" class="form-label">确认密码</label>
<input v-model="password_confirm" type="password" class="form-control" id="password_confirm">
</div>
<div class="error-message">{{ error_message }}</div>
<button type="submit" class="btn btn-primary">注册</button>
</form>
</div>
</div>
</ContentBase>
</template>
<script>
import ContentBase from '../components/ContentBase';
import { ref } from 'vue';
import { useStore } from 'vuex';
import $ from 'jquery';
import router from '@/router/index.js';
export default {
name: 'RegisterView',
components: {
ContentBase,
},
setup(){
let store = useStore();
let username = ref('');
let password = ref('');
let password_confirm = ref('');
let error_message = '';
const register = () => {
$.ajax({
url:'http://47.106.113.194:8000/register/',
type:'post',
data:{
username:username.value,
password:password.value,
password_confirm:password_confirm.value,
},
success(resp){
console.log(resp);
if (resp.result === 'success'){
store.dispatch('login',{
username:username.value,
password:password.value,
success(){
error_message = '';
router.push({name:'home'});
},
error(){
error_message = "系统异常";
}
});
}
else {
error_message = resp.result;
}
}
});
};
return {
username,
password,
password_confirm,
error_message,
register,
}
}
}
</script>
<style scoped>
button {
width: 100%;
}
.error-message {
color: red;
}
</style>

@ -0,0 +1,29 @@
<template>
<ContentBase>
<div>
{{ $store.state.user.username }}
</div>
</ContentBase>
</template>
<script>
import ContentBase from '../components/ContentBase';
import { useStore } from 'vuex';
export default {
name: 'UserProfileView',
components: {
ContentBase,
},
setup(){
let store = useStore();
return {
store,
}
}
}
</script>
<style scoped>
</style>
Loading…
Cancel
Save