Browse Source

create project

mins 1 year ago
parent
commit
69458fb2f5

+ 17 - 1
README.md

@@ -1,3 +1,19 @@
 # designer
 
-设计者标注平台
+## Project setup
+```
+npm install
+```
+
+### Compiles and hot-reloads for development
+```
+npm run serve
+```
+
+### Compiles and minifies for production
+```
+npm run build
+```
+
+### Customize configuration
+See [Configuration Reference](https://cli.vuejs.org/config/).

+ 19 - 0
jsconfig.json

@@ -0,0 +1,19 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "module": "esnext",
+    "baseUrl": "./",
+    "moduleResolution": "node",
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    },
+    "lib": [
+      "esnext",
+      "dom",
+      "dom.iterable",
+      "scripthost"
+    ]
+  }
+}

File diff suppressed because it is too large
+ 13614 - 0
package-lock.json


+ 21 - 0
package.json

@@ -0,0 +1,21 @@
+{
+  "name": "designer",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build"
+  },
+  "dependencies": {
+    "element-ui": "^2.15.9",
+    "vue": "^2.6.14",
+    "vue-router": "^3.5.1"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-router": "~5.0.0",
+    "@vue/cli-service": "~5.0.0",
+    "less": "^4.0.0",
+    "less-loader": "^8.0.0",
+    "vue-template-compiler": "^2.6.14"
+  }
+}

BIN
public/favicon.ico


+ 18 - 0
public/index.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="">
+	<head>
+		<meta charset="utf-8">
+		<meta http-equiv="X-UA-Compatible" content="IE=edge">
+		<meta name="viewport" content="width=device-width,initial-scale=1.0">
+		<link rel="icon" href="/favicon.ico">
+		<link rel="stylesheet" href="http://designer.xuxiangbo.com/icon/iconfont.css">
+		<title><%= htmlWebpackPlugin.options.title %></title>
+	</head>
+	<body>
+		<noscript>
+			<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+		</noscript>
+		<div id="app"></div>
+		<!-- built files will be auto injected -->
+	</body>
+</html>

+ 49 - 0
src/App.vue

@@ -0,0 +1,49 @@
+<template>
+	<div id="app">
+		<router-view />
+	</div>
+</template>
+
+<script>
+    export default {
+        mounted (){
+            this.$nextTick(() => {
+                document.body.oncontextmenu = (e) => {
+                    return false;
+                }
+            })
+        }
+    }
+</script>
+
+<style lang="less">
+    *{
+        margin: 0;
+        padding: 0;
+    }
+    html, body{
+        width: 100%;
+        height: 100%;
+        position: absolute;
+        top: 0;
+        left: 0;
+    }
+    #app {
+        width: 100%;
+        height: 100%;
+        overflow: hidden;
+        position: absolute;
+        top: 0;
+        left: 0;
+        font-size: 14px;
+        color: rgb(66, 66, 66);
+        font-family: Avenir, Helvetica, Arial, sans-serif;
+        background: #f5f6f9;
+    }
+
+    // 重置 element 样式
+    // .el-message-box{
+    //     position: relative;
+    //     top: -10%;
+    // }
+</style>

BIN
src/assets/logo.png


+ 377 - 0
src/components/myCanvas.vue

@@ -0,0 +1,377 @@
+<template>
+    <div class="myCanvas">
+        <div class="menu" v-show="showMenu" @mouseleave="showMenu = false" :style="{top: menuY + 'px', left: menuX + 'px'}">
+            <p v-show="!showDel" @click="addForm.show = true">添加标记点</p>
+            <!-- <p v-show="showDel" @click="editForm.show = true">修改此标记点</p> -->
+            <p v-show="showDel" @click="del">移除此标记点</p>
+            <p @click="showMenu = false">取消</p>
+        </div>
+        <div class="img">
+            <span v-for="(item, index) in marks" :key="index" :id="item.id" :style="{left: item.x + 'px', top: item.y + 'px'}">{{item.mark_num}}</span>
+        </div>
+
+        <!-- 添加标记弹窗 -->
+        <el-dialog title="添加标记点" class="form" :visible.sync="addForm.show" width="30%" :before-close="() => {}" :show-close="false">
+            <el-input v-model="addForm.title" placeholder="请输入标记内容"></el-input>
+            <el-input v-model="addForm.content" style="margin-top: 20px" placeholder="请输入详细描述"></el-input>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="cancelAdd" size="small">取 消</el-button>
+                <el-button type="primary" @click="add" size="small">确 定</el-button>
+            </span>
+        </el-dialog>
+
+        <!-- 修改标记弹窗 -->
+        <el-dialog title="修改标记点" class="form" :visible.sync="editForm.show" width="30%" :before-close="() => {}" :show-close="false">
+            <el-input v-model="editForm.title" placeholder="请输入标记内容"></el-input>
+            <el-input v-model="editForm.content" style="margin-top: 20px" placeholder="请输入详细描述"></el-input>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="cancelEdit" size="small">取 消</el-button>
+                <el-button type="primary" @click="editForm.show = false" size="small">确 定</el-button>
+            </span>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+    export default {
+        name: 'myCanvas',
+        props: ['url', 'datas', 'imgId'],
+        data (){
+            return {
+                marks: [],
+                box: {
+                    width: 0,
+                    height: 0
+                },
+                img: {
+                    width: 0,
+                    height: 0
+                },
+                scale: 1,
+                maxScale: 2,
+                minScale: 0.3,
+                scaleUnit: 0.1,
+                top: 0,
+                transTop: 0,
+                left: 0,
+                transLeft: 0,
+                transUnit: 2,
+                showMenu: false,
+                menuX: 0,
+                menuY: 0,
+                showDel: false,
+                addForm: {
+                    show: false,
+                    title: '',
+                    content: ''
+                },
+                editForm: {
+                    show: false,
+                    title: '',
+                    content: ''
+                },
+                currentMarkId: 0,
+                currentMArkPosX: 0,
+                currentMArkPosY: 0
+            }
+        },
+        mounted (){
+            // 获取图片地址
+            let timer = setInterval(() => {
+                if(this.url){
+                    clearInterval(timer);
+                    this.init();
+
+                    // 标注
+                    this.marks = this.datas;
+                }
+            }, 200);
+        },
+        methods: {
+            // 初始化图片
+            init (){
+                this.$nextTick(() => {
+                    let box = document.querySelector('.myCanvas');
+                    let img = document.querySelector('.img');
+                    // 容器尺寸
+                    this.box = {
+                        width: box.clientWidth,
+                        height: box.clientHeight
+                    }
+
+                    // 图片尺寸
+                    let image = new Image();
+                    image.onload = () => {
+                        this.img = {
+                            width: image.clientWidth,
+                            height: image.clientHeight
+                        }
+                        this.defaultSize();
+                    }
+                    image.src = this.url;
+                    img.appendChild(image);
+                    box.appendChild(img);
+                })
+            },
+            // 控制图片默认尺寸
+            defaultSize (){
+                // 如果图片宽度大于容器宽度,缩小一半
+                console.log(this.img, this.box)
+                if(this.img.width > this.box.width){
+                    this.img.width = this.img.width/2;
+                    this.img.height = this.img.height/2;
+                }
+
+                // 如果高度大于容器高度,缩小一半
+                if(this.img.height > this.box.height){
+                    this.img.width = this.img.width/2;
+                    this.img.height = this.img.height/2;
+                }
+
+                // 位置
+                this.defaultPosition();
+            },
+            // 控制图片默认位置
+            defaultPosition (){
+                this.top = this.box.height/2 - this.img.height/2;
+                this.left = this.box.width/2 - this.img.width/2;
+
+                // 确认图片尺寸
+                this.$nextTick(() => {
+                    document.querySelector('.img img').style.cssText = `width: ${this.img.width}px; height: ${this.img.height}px`;
+                })
+
+                // 设置图片
+                this.mouseSlide();
+            },
+            // 监听鼠标滚轮操作
+            mouseSlide (){
+                this.$nextTick(() => {
+                    let img = document.querySelector('.img');
+                    img.onmouseover = () => {
+                        img.onmousewheel = (e) => {
+                            this.scale = e.deltaY > 0 ? this.scale - this.scaleUnit : this.scale + this.scaleUnit;
+                            
+                            // 超大 && 超小
+                            if(this.scale > this.maxScale){
+                                this.scale = this.scale - this.scaleUnit;
+                                return;
+                            }
+                            if(this.scale < this.minScale){
+                                this.scale = this.scale + this.scaleUnit;
+                                return;
+                            }
+                            
+                            this.set();
+                        }
+                    }
+                })
+                this.draw();
+            },
+            // 监听拖拽操作
+            draw (){
+                this.$nextTick(() => {
+                    let img = document.querySelector('.img');
+
+                    // 右键菜单
+                    img.oncontextmenu = (e) => {
+                        this.showDel = e.path[0].nodeName == 'SPAN' ? true : false;
+                        if(this.showDel){
+                            this.currentMarkId = e.target.id;
+                        }
+                        this.menuX = e.pageX - 220 - 20;
+                        this.menuY = e.pageY - 50 - 20;
+                        this.currentMArkPosX = e.offsetX;
+                        this.currentMArkPosY = e.offsetY;
+                        this.showMenu = true;
+                        document.querySelector('.menu').focus();
+                        return false;
+                    }
+
+                    // 点击移动
+                    img.onmousedown = (e) => {
+                        if(this.showMenu){
+                            this.showMenu = false;
+                        }
+                        e.preventDefault();
+                        let cando = true;
+                        if(e.button == 2){
+                            return false;
+                        }
+
+                        let x = e.screenX;
+                        let y = e.screenY;
+
+                        img.onmousemove = (ee) => {
+                            if(!cando) return;
+                            cando = false;
+                            
+                            setTimeout(() => {
+                                if(ee.screenX > x){
+                                    this.left += ee.screenX - x;
+                                }else{
+                                    this.left -= x - ee.screenX;
+                                }
+
+                                if(ee.screenY > y){
+                                    this.top += ee.screenY - y;
+                                }else{
+                                    this.top -= y - ee.screenY;
+                                }
+
+                                this.set();
+                                cando = true;
+                                x = ee.screenX;
+                                y = ee.screenY;
+                            }, 10);
+                        }
+                    }
+
+                    // 取消移动
+                    img.onmouseup = () => {
+                        img.onmousemove = null;
+                    }
+                    img.onmouseleave = () => {
+                        img.onmousemove = null;
+                    }
+                })
+                this.set();
+            },
+            // 渲染图片
+            set (){
+                this.$nextTick(() => {
+                    let img = document.querySelector('.img');
+                    img.style.cssText = `
+                        transform: scale(${this.scale});
+                        width: ${this.img.width}px;
+                        height: ${this.img.height}px;
+                        top: ${this.top}px;
+                        left: ${this.left}px;
+                    `;
+                })
+            },
+            // 添加标记点
+            add (){
+                // 计算 num
+                let num = 0;
+                this.marks.map(item => {
+                    if(item.mark_num >= num){
+                        num = Number(item.mark_num);
+                    }
+                })
+                num++;
+
+                // 添加的数据
+                let data = {
+                    user: JSON.parse(localStorage.getItem('designer_user')).id,
+                    project: this.$route.query.id,
+                    img: this.imgId,
+                    title: this.addForm.title,
+                    text: this.addForm.content,
+                    num,
+                    x: this.currentMArkPosX,
+                    y: this.currentMArkPosY
+                }
+                
+                this.$ajax.post('/marks/add/', data).then(result => {
+                    this.marks.push(result);
+                    this.addForm = {
+                        show: false,
+                        title: '',
+                        content: ''
+                    }
+                })
+            },
+            // 取消添加标记点
+            cancelAdd (){
+                this.addForm = {
+                    show: false,
+                    title: '',
+                    content: ''
+                }
+            },
+            // 取消修改标记点
+            cancelEdit (){
+                this.addForm = {
+                    show: false,
+                    title: '',
+                    content: ''
+                }
+            },
+            // 删除标记点
+            del (){
+                this.$ajax.get(`/marks/del/?id=${this.currentMarkId}`).then(result => {
+                    let index = 0;
+                    this.marks.map((item, i) =>{
+                        if(item.id == this.currentMarkId){
+                            index = i;
+                        }
+                    })
+                    this.marks.splice(index, 1);
+                    this.$message.success('移除成功');
+                    this.showDel = false;
+                    this.currentMarkId = 0;
+                    this.showMenu = false;
+                })
+            }
+        }
+    }
+</script>
+
+<style lang="less" scoped>
+    @import url('../tools/common.less');
+    .myCanvas{
+        width: 100%;
+        height: 100%;
+        position: absolute;
+        top: 0;
+        left: 0;
+        .menu{
+            width: 140px;
+            background: #ffffff;
+            box-shadow: 0px 3px 7px 0px rgba(0,0,0,0.0500);
+            border-radius: 5px;
+            overflow: hidden;
+            position: absolute;
+            z-index: 999;
+            padding: 10px 0;
+            p{
+                padding: 10px 15px;
+                color: #797979;
+                cursor: pointer;
+                user-select: none;
+                font-size: 13px;
+            }
+            p:hover{
+                color: @color;
+                background: rgb(233, 239, 247);
+            }
+        }
+        .img{
+            cursor: pointer;
+            position: absolute;
+            z-index: 10;
+            background: red;
+            transition: transform 0.3s;
+            span{
+                display: inline-block;
+                width: 30px;
+                height: 30px;
+                font-size: 12px;
+                background: #FD4F4F;
+                border-radius: 50%;
+                line-height: 30px;
+                text-align: center;
+                color: #ffffff;
+                position: absolute;
+            }
+        }
+        .form{
+            height: 80%;
+            padding-bottom: 20%;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+        }
+    }
+</style>

+ 40 - 0
src/main.js

@@ -0,0 +1,40 @@
+import Vue from 'vue'
+import App from './App.vue'
+import router from './router'
+import ElementUI from 'element-ui';
+import 'element-ui/lib/theme-chalk/index.css';
+import lang from './tools/lang';
+import ajax from './tools/ajax';
+
+
+Vue.use(ElementUI);
+Vue.config.productionTip = false;
+
+// 多语言支持
+Vue.prototype.$lang = moduleName => {
+    let type = localStorage.getItem('lang') || 'zht';
+    return lang[type][moduleName];
+}
+
+// 复制分享链接
+Vue.prototype.$copy = (id, title) => {
+    let user = JSON.parse(localStorage.getItem('designer_user')).user_name;
+    let input = document.createElement('textarea');
+    input.value = `${user}为您分享了一个项目,快来看看吧!\n项目地址:http://designer.xuxiangbo.com/${id}\n项目名称: ${title}`;
+    document.body.append(input);
+    input.select();
+    document.execCommand('copy');
+    input.remove();
+    ElementUI.Message({
+        type: 'success',
+        message: '已复制项目链接!'
+    })
+}
+
+// ajax
+Vue.prototype.$ajax = ajax;
+
+new Vue({
+    router,
+    render: function (h) { return h(App) }
+}).$mount('#app')

+ 89 - 0
src/router/index.js

@@ -0,0 +1,89 @@
+import Vue from 'vue'
+import VueRouter from 'vue-router'
+import index from '../views/index.vue'
+
+Vue.use(VueRouter)
+
+const originalPush = VueRouter.prototype.push;
+VueRouter.prototype.push = function push (location) {
+	return originalPush.call(this, location).catch(err => err)
+}
+
+const routes = [
+    {
+        path: '/',
+        name: 'index',
+        component: index,
+		children: [
+			{
+				path: '/group',
+				name: 'group',
+				component: () => import('../views/group.vue')
+			},
+			{
+				path: '/list',
+				name: 'list',
+				component: () => import('../views/list.vue')
+			},
+			{
+				path: '/user',
+				name: 'user',
+				component: () => import('../views/user.vue')
+			},
+			{
+				path: '/team',
+				name: 'team',
+				component: () => import('../views/team.vue')
+			},
+			{
+				path: '/404',
+				name: '404',
+				component: () => import('../views/404.vue')
+			}
+		]
+    },
+	{
+		path: '/project',
+		name: 'project',
+		component: () => import('../views/project.vue')
+	},
+    {
+        path: '/login',
+        name: 'login',
+        component: () => import('../views/login.vue')
+    }
+]
+
+const router = new VueRouter({
+    mode: 'hash',
+    base: process.env.BASE_URL,
+    routes
+})
+
+// 路由拦截,自定义404
+let routesNameArray = [];
+routes.map(item => {
+	routesNameArray.push(item.name);
+	item.children && item.children.map(i => {
+		routesNameArray.push(i.name);
+	})
+})
+
+router.beforeEach((to, from, next) => {
+	// 判断是否在登陆状态
+	if(to.path != '/login' && !localStorage.getItem('designer_user')){
+		next({
+			path: '/login'
+		})
+	}
+
+	// 自定义404
+	if(routesNameArray.indexOf(to.name) == -1){
+		next({
+			path: '/404'
+		})
+	}
+
+	next();
+})
+export default router

+ 81 - 0
src/tools/ajax.js

@@ -0,0 +1,81 @@
+/**
+ * ajax 封装
+ */
+
+import { Message } from "element-ui";
+import { MessageBox } from 'element-ui';
+import router from "@/router";
+
+const apiHost = 'http://designer.xuxiangbo.com/api';
+const ajax = {
+    get(url) {
+        return new Promise((reslove, reject) => {
+            let xhr = new XMLHttpRequest();
+            xhr.open('GET', `${apiHost}${url}`, true);
+            xhr.send();
+            xhr.onreadystatechange = () => {
+                if (xhr.status == 200 && xhr.readyState == 4){
+					let data = JSON.parse(xhr.response);
+					if(data.code == 200){
+						reslove(data.data);
+					}else{
+						Message.error({
+							message: data.msg
+						})
+						reject(data);
+					}
+                }
+            }
+            xhr.onerror = () => {
+				MessageBox({
+					title: 'System Error: #500',
+					message: '网络错误,请联系管理员!错误代码:500',
+					type: 'error',
+					closeOnPressEscape: false,
+					closeOnClickModal: false,
+					showClose: false,
+					confirmButtonClass: 'el-button--danger'
+				})
+			}
+        })
+    },
+    post(url, data = {}) {
+        return new Promise((reslove, reject) => {
+            let xhr = new XMLHttpRequest();
+            let form = new FormData();
+            Object.keys(data).map(item => {
+                form.append(item, data[item]);
+            })
+            xhr.open('POST', `${apiHost}${url}`, true);
+            xhr.send(form);
+            xhr.onreadystatechange = () => {
+                if (xhr.status == 200 && xhr.readyState == 4){
+					let data = JSON.parse(xhr.response);
+					if(data.code == 200){
+						reslove(data.data);
+					}else{
+						Message.error({
+							message: data.msg
+						})
+						reject(data);
+					}
+                }
+            }
+            xhr.onerror = () => {
+				MessageBox({
+					title: 'System Error: #500',
+					message: '网络错误,请联系管理员!错误代码:500',
+					type: 'error',
+					closeOnPressEscape: false,
+					closeOnClickModal: false,
+					showClose: false,
+					confirmButtonClass: 'el-button--danger',
+					callback (){
+						window.location.reload();
+					}
+				})
+			}
+        })
+    }
+}
+export default ajax;

+ 2 - 0
src/tools/common.less

@@ -0,0 +1,2 @@
+@imgurl: 'http://designer.xuxiangbo.com'; // 图片地址
+@color: #2878FF; // 主题色

+ 3 - 0
src/tools/global.js

@@ -0,0 +1,3 @@
+export default {
+    host: 'http://designer.xuxiangbo.com'
+}

+ 137 - 0
src/tools/lang.js

@@ -0,0 +1,137 @@
+export default {
+    zht: { // 繁体中文,默认
+        // 登录模块
+        login: {
+            title: '歡迎登陸',
+            inputs: ['登入帳號', '登入密碼'],
+            foot: ['記住密碼', '忘記密碼'],
+            submit: '登入',
+            tips: {
+                form: {
+                    account: '請輸入帳號',
+                    passwd: '請輸入密碼'
+                }
+            }
+        },
+        // 首页
+        index: {
+            logout: '登出',
+            menu: {
+                list: '項目清單',
+                user: '個人中心',
+                team: '成員管理',
+                default: '項目清單'
+            },
+            role: {
+                admin: '管理員',
+                designer: '設計師',
+                custom: '尊敬的客戶',
+                hi: '您好'
+            },
+            tips: {
+                logout: {
+                    title: '是否要登出?',
+                    t: '提示',
+                    cancel: '取消',
+                    confirm: '確定'
+                }
+            }
+        },
+        // 分组列表
+        group: {
+
+        },
+        // 项目列表
+        project: {
+
+        },
+        // 项目详情
+        canvas: {
+
+        },
+        // 个人中心
+        user: {
+            title: '個人中心',
+            title2: '管理你的帳戶',
+            form: {
+                account: '賬戶',
+                passwd: '密碼',
+                reset: '修改密碼'
+            },
+            alert: {
+                account: {
+                    title: '修改用戶名',
+                    title2: '請輸入新的用戶名',
+                    cancel: '取消',
+                    confirm: '確定',
+                    error: '請輸入用戶名',
+                    success: '修改成功!',
+                    success2: '身份資訊發生變化,請重新登入!'
+                },
+                passwd: {
+                    title: '修改密碼',
+                    title2: '請輸入新的密碼',
+                    cancel: '取消',
+                    confirm: '確定',
+                    error: '請輸入用戶名',
+                    success: '修改成功!',
+                    success2: '身份資訊發生變化,請重新登入!'
+                },
+            }
+        },
+        // 团队管理
+        team: {
+            title: '成員管理',
+            title2: '共',
+            title3: '個成員',
+            add: '添加成員',
+            table: {
+                nick: '昵稱',
+                regTime: '註冊時間',
+                lastTime: '最後登錄時間',
+                role: '用戶角色',
+                status: '狀態',
+                dost: '操作',
+            },
+            role: ['管理員', '設計師', '客戶'],
+            status1: '正常', 
+            status2: '凍結', 
+            tips1: '操作成功',
+            tips2: '確定要删除此用戶嗎?',
+            tips3: '提示',
+            tips4: '取消',
+            tips5: '確定',
+            tips6: '删除成功',
+            tips7: '請輸入帳戶',
+            tips8: '删除帳戶',
+            tips9: '解鎖帳戶',
+            tips10: '鎖定帳戶',
+            tips11: '添加成員',
+            tips12: '用戶帳戶',
+            tips13: '請輸入用戶帳戶',
+            tips14: '用戶身份',
+            tips15: '請選擇用戶身份',
+            tips16: '客戶',
+            tips17: '設計師',
+            tips18: '管理員',
+            tips19: '取 消',
+            tips20: '確 定',
+        }
+    },
+    zh: { // 简体中文
+        login: {
+            title: '欢迎登陆',
+            inputs: ['手机号或邮箱', '登录密码'],
+            foot: ['记住密码', '忘记密码'],
+            submit: '登录'
+        }
+    },
+    en: { // 英文
+        login: {
+            title: '',
+            inputs: ['Phone or mail', 'Password'],
+            foot: ['Remenber me', 'Forgot password'],
+            submit: 'login'
+        }
+    }
+}

+ 111 - 0
src/tools/timer.js

@@ -0,0 +1,111 @@
+/**
+ * 时间插件
+ */
+
+const timer = {
+	// 小于10的数字前面补0
+	to2: number => Number(number) < 10 ? `0${number}` : String(number),
+
+	// 获取指定时间
+	time (format = 'y-m-d', time = null){
+		let date = time ? new Date(time) : new Date();
+		format = format.replace('y', date.getFullYear());
+		format = format.replace('m', this.to2(date.getMonth() + 1));
+		format = format.replace('d', this.to2(date.getDate()));
+		format = format.replace('h', this.to2(date.getHours()));
+		format = format.replace('i', this.to2(date.getMinutes()));
+		format = format.replace('s', this.to2(date.getSeconds()));
+		format = format.replace('w', date.getDay());
+		return format;
+	},
+
+	// 获取当前月份有多少天
+	monthLength (time = null){
+		let y = this.time('y', time) * 1;
+		let m = this.to2(this.time('m', time) * 1 + 1);
+		let unix = new Date(`${y}-${m}-01`).getTime() - 24 * 60 * 60 * 1000;
+		return Number(this.time('d', unix));
+	},
+
+	// 获取上个月有多少天
+	preMonthLenggth (time = null){
+		let y = this.time('y', time) * 1;
+		let m = this.to2(this.time('m', time) * 1);
+		let unix = new Date(`${y}-${m}-01`).getTime() - 24 * 60 * 60 * 1000;
+		return Number(this.time('d', unix));
+	},
+
+	// 获取下个月有多少天
+	nextMonthLenggth (time = null){
+		let y = this.time('y', time) * 1;
+		let m = this.to2(this.time('m', time) * 1 + 2);
+		let unix = new Date(`${y}-${m}-01`).getTime() - 24 * 60 * 60 * 1000;
+		return Number(this.time('d', unix));
+	},
+
+	// 获取当月第一天
+	monthFirst (time = null){
+		let unix = this.time('y-m', time) + '-01';
+		return this.time('y-m-d', unix);
+	},
+
+	// 获取当月最后一天
+	monthLast (time = null){
+		let y = this.time('y', time) * 1;
+		let m = this.to2(this.time('m', time) * 1 + 1);
+		let unix = new Date(`${y}-${m}-01`).getTime() - 24 * 60 * 60 * 1000;
+		return this.time('y-m-d', unix);
+	},
+
+	// 获取上个月的今天
+	preMonthToday (time = null){
+		let y = this.time('y', time);
+		let m = this.to2(this.time('m', time) * 1 - 1);
+		let d = this.time('d', time);
+		return this.time('y-m-d', `${y}-${m}-${d}`);
+	},
+
+	// 获取下个月的今天
+	nextMonthToday (time = null){
+		let y = this.time('y', time);
+		let m = this.to2(this.time('m', time) * 1 + 1);
+		let d = this.time('d', time);
+		return this.time('y-m-d', `${y}-${m}-${d}`);
+    },
+    
+    // 已经过去了多少时间
+    timeout (time){
+
+    },
+
+    // 倒计时
+    timeRemaining (time, format){
+        time = time - parseInt(new Date().getTime()/1000);
+        return this.unixToString(format, time);
+    },
+
+    // 时间戳的差值转化为时间字符串
+    unixToString (format, time){
+        let d = parseInt(time / 60 / 60 / 24);
+        let h = parseInt((time - d * 86400) / 3600);
+        let i = parseInt((time - d * 86400 - h * 3600) / 60);
+        let s = time - d * 86400 - h * 3600 - i * 60;
+        format = format.replace('d', timer.to2(timer.low(d)));
+        format = format.replace('h', timer.to2(timer.low(h)));
+        format = format.replace('i', timer.to2(timer.low(i)));
+        format = format.replace('s', timer.to2(timer.low(s)));
+        return format;
+    },
+
+    // 小于0的直接输出0
+    low (num){
+        return num < 0 ? 0 : num;
+    },
+
+	// 把日期转成对象
+	parse (string){
+		return new Date(string);
+	}
+}
+
+export default timer;

+ 42 - 0
src/views/404.vue

@@ -0,0 +1,42 @@
+<template>
+	<div class="welcome">
+		<p class="face"><i class="osadmin osadminyepian"></i></p>
+		<p class="hi">当前页面未找到</p>
+		<p class="vsrsion">404 Not Found</p>
+		<el-button type="info" plain style="width: 200px; margin-top: 20px" @click="back">返 回</el-button>
+	</div>
+</template>
+
+<script>
+	export default {
+		methods: {
+			back (){
+				this.$router.go(-1);
+			}
+		}
+	}
+</script>
+
+<style lang="less" scoped>
+	@topHeight: 150px;
+	.welcome{
+		width: 100%;
+		height: calc(100% - @topHeight);
+		padding-top: @topHeight;
+		font-size: 32px;
+		text-align: center;
+		user-select: none;
+		i{
+			font-size: 300px;
+			color: rgb(233, 233, 233);
+		}
+		.hi{
+			color: rgb(189, 189, 189);
+		}
+		.vsrsion{
+			font-size: 18px;
+			margin-top: 20px;
+			color: rgb(189, 189, 189);
+		}
+	}
+</style>

+ 298 - 0
src/views/group.vue

@@ -0,0 +1,298 @@
+<template>
+    <div class="group" v-loading="loading">
+        <div class="group_head" v-if="user.role != 2">
+            <el-button type="primary" size="small" icon="el-icon-circle-plus-outline" @click="addProject">添加新项目</el-button>
+        </div>
+        <div class="group_item">
+            <div class="nodata" v-if="group.length == 0"><el-empty description="暂无项目"></el-empty></div>
+            <div class="box" v-for="item in group" :key="item.id">
+                <div class="main">
+                    <div class="menu">
+                        <i class="el-icon-share" @click="share(item.id)"></i>
+                        <i class="el-icon-edit" @click="rename(item.id)" v-if="item.creater_id == user.id || user.role == 0"></i>
+                        <i class="el-icon-delete" @click="del(item.id)" v-if="item.creater_id == user.id || user.role == 0"></i>
+                    </div>
+                    <div class="cover" @click="goProject(item.id)">
+                        <img class="project_cover" :src="global.host + item.cover" alt="">
+                    </div>
+                </div>
+                <div class="title">
+                    <p>{{item.group_name}}</p>
+                    <p>{{item.user_name}} · {{item.create_time}}</p>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+    import timer from '../tools/timer';
+    import global from '@/tools/global';
+    export default {
+        name: 'group',
+        data (){
+            return {
+                user: {},
+                group: [],
+                loading: true,
+                global
+            }
+        },
+        created (){
+            this.user = JSON.parse(localStorage.getItem('designer_user'));
+
+            // 没有参与项目
+            if(this.user.projects.substring(1).length == 0){
+                this.group = [];
+                this.loading = false;
+                return;
+            }
+
+            this.getgroup();
+            
+        },
+        methods: {
+            // 获取项目组列表
+            getgroup (){
+                // 根据id列表获取项目信息
+                this.user.projects = this.user.projects.substring(1);
+                this.$ajax.get('/group/list/?ids=' + this.user.projects).then(r => {
+                    this.group = r.map(item => {
+                        item.create_time = timer.time('y-m-d', item.create_time * 1000);
+                        return item;
+                    });
+                    
+                    // 处理封面展示方式,横图 = row,竖图 = col
+                    this.$nextTick(() => {
+                        let imgs = document.querySelectorAll('.project_cover');
+                        imgs.forEach(item => {
+                            let img = new Image();
+                            img.onload = () => {
+                                if(img.width > img.height){
+                                    item.classList.add('row');
+                                }else{
+                                    item.classList.add('col');
+                                }
+                            }
+                            img.src = item.src;
+                        })
+                        this.loading = false;
+                    })
+                })
+            },
+            // 分享项目名
+            share (id){
+                this.group.map(item => {
+                    if(item.id == id){
+                        this.$copy(id, item.title);
+                    }
+                })
+            },
+            // 重命名
+            rename (id){
+                let that = this;
+                that.$prompt('请输入新的项目名称', '重命名', {
+                    inputPattern: /\S/,
+                    inputErrorMessage: '请输入项目名',
+                    closeOnPressEscape: false,
+                    closeOnClickModal: false,
+                    beforeClose (action, instance, done){
+                        if(action == 'confirm'){
+                            that.$ajax.post('/group/rename', {
+                                id,
+                                name: instance.inputValue
+                            }).then(r => {
+                                that.$message.success('修改成功');
+                                that.group.map(item => {
+                                    if(item.id == id){
+                                        item.group_name = instance.inputValue;
+                                    }
+                                })
+                                done();
+                            })
+                        }else{
+                            done();
+                        }
+                    }
+                }).catch(() => {});
+            },
+            // 删除项目
+            del (id){
+                let that = this;
+                this.$confirm('此操作将永久删除该项目, 是否继续?', '提示', {
+                    type: 'warning',
+                    closeOnPressEscape: false,
+                    closeOnClickModal: false,
+                    beforeClose (action, instance, done){
+                        if(action == 'confirm'){
+                            let index = 0;
+                            that.group.map((item, i) => {
+                                if(item.id == id){
+                                    index = i;
+                                }
+                            })
+                            that.$ajax.get('/group/del/?id=' + id).then(r => {
+                                that.group.splice(index, 1);
+                                that.$message.success('删除成功');
+                                done();
+                            })
+                        }else{
+                            done();
+                        }
+                    }
+                }).catch(() => {});
+            },
+            // 查看项目
+            goProject (id){
+                this.$router.push({
+                    path: '/list',
+                    query: { id }
+                })
+            },
+            // 添加新项目
+            addProject (){
+                let that = this;
+                that.$prompt('请输入项目名称', '添加新项目', {
+                    inputPattern: /\S/,
+                    inputErrorMessage: '请输入项目名',
+                    closeOnPressEscape: false,
+                    closeOnClickModal: false,
+                    beforeClose (action, instance, done){
+                        if(action == 'confirm'){
+                            that.$ajax.post('/group/add', {
+                                name: instance.inputValue,
+                                createrId: JSON.parse(localStorage.getItem('designer_user')).id
+                            }).then(r => {
+                                done();
+                                that.setGroup(r);
+                                that.$message.success('添加成功');
+                            })
+                        }else{
+                            done();
+                        }
+                    }
+                }).catch(() => {});
+            },
+            // 更新项目id
+            setGroup (r){
+                this.user.projects = r;
+                localStorage.setItem('designer_user', JSON.stringify(this.user));
+                this.getgroup();
+            }
+        }
+    }
+</script>
+
+<style lang="less" scoped>
+    @import url('../tools/common.less');
+    .group{
+        width: calc(100% - 60px);
+        height: 100%;
+        padding: 0 30px;
+        display: flex;
+        justify-content: flex-start;
+        align-content: flex-start;
+        flex-wrap: wrap;
+        overflow-x: hidden;
+        overflow-y: auto;
+        .group_head{
+            width: calc(100% - 20px);
+            height: 40px;
+            padding: 20px 10px;
+            display: flex;
+            justify-content: flex-start;
+            align-items: center;
+        }
+        .group_item{
+            width: 100%;
+            height: calc(100% - 80px);
+            display: flex;
+            justify-content: flex-start;
+            align-content: flex-start;
+            flex-wrap: wrap;
+            overflow-x: hidden;
+            overflow-y: auto;
+            .nodata{
+                width: 100%;
+                height: 70%;
+                display: flex;
+                justify-content: center;
+                align-items: center;
+            }
+            .box{
+                width: 240px;
+                height: 300px;
+                background: #ffffff;
+                border-radius: 5px;
+                margin: 10px;
+                box-shadow: 0px 3px 7px 0px rgba(0,0,0,0.0500);
+                .main{
+                    width: 220px;
+                    height: 220px;
+                    background: #F5F5F7;
+                    border-radius: 5px;
+                    margin: 10px;
+                    transition: all 0.3s;
+                    .menu{
+                        width: 220px;
+                        height: 30px;
+                        line-height: 30px;
+                        display: flex;
+                        justify-content: flex-end;
+                        align-items: center;
+                        i{
+                            display: block;
+                            font-size: 14px;
+                            margin-right: 15px;
+                            color: #888888;
+                            cursor: pointer;
+                        }
+                        i:hover{
+                            color: @color;
+                        }
+                        i.designer-gengduo{
+                            font-size: 20px;
+                        }
+                    }
+                    .cover{
+                        width: 200px;
+                        height: 170px;
+                        padding: 10px;
+                        display: flex;
+                        justify-content: center;
+                        align-items: center;
+                        cursor: pointer;
+                        overflow: hidden;
+                        img.row{
+                            width: 100%;
+                            border-radius: 10px;
+                        }
+                        img.col{
+                            height: 100%;
+                            border-radius: 10px;
+                        }
+                    }
+                }
+                .title{
+                    width: 240px;
+                    height: 60px;
+                    text-align: center;
+                    p:last-child{
+                        margin-top: 5px;
+                        font-size: 12px;
+                        color: #888888;
+                    }
+                }
+                &:hover{
+                    .main{
+                        background: #F8F5F2;
+                    }
+                }
+            }
+        }
+            
+        .group_item::-webkit-scrollbar{
+            display: none;
+        }
+    }
+</style>

+ 192 - 0
src/views/index.vue

@@ -0,0 +1,192 @@
+<template>
+    <div class="index">
+        <!-- 头部 -->
+        <div class="head">
+			<div class="menu">
+				<span v-for="item in menu" :key="item.name" :class="item.name == currentMenu ? 'current' : ''" @click="menuClick(item)">
+					<i :class="item.icon"></i> {{item.name}}
+				</span>
+			</div>
+			<div class="user">
+				<img :src="user.avatar" alt="">
+				<p>{{hi}}</p>
+				<span @click="logout"><i class="designer- designer-tuichu"></i> {{lang.logout}}</span>
+			</div>
+		</div>
+
+        <!-- 内容区 -->
+        <div class="content">
+			<router-view />
+		</div>
+    </div>
+</template>
+
+<script>
+	import global from '@/tools/global';
+	export default {
+		name: 'index',
+		data (){
+			return {
+				lang: {},
+				user: {},
+				hi: '',
+				menu: [],
+				currentMenu: ''
+			}
+		},
+		created (){
+			// 多语言支持
+			this.lang = this.$lang('index');
+			this.menu = [ 
+				{
+					name: this.lang.menu.list,
+					route: 'group',
+					icon: 'el-icon-pie-chart'
+				},
+				{
+					name: this.lang.menu.user,
+					route: 'user',
+					icon: 'el-icon-user'
+				}
+			];
+			this.currentMenu = this.lang.menu.default;
+		},
+		mounted (){
+			// 获取用户信息
+			this.user = JSON.parse(localStorage.getItem('designer_user'));
+			this.user.avatar = global.host + this.user.avatar;
+			if(this.user.role == 0){
+				this.menu.push({
+					name: this.lang.menu.team,
+					route: 'team',
+					icon: 'designer- designer-qunchengyuan-02'
+				})
+			}
+
+			// 欢迎语
+			if(this.user.role == 0){
+				this.hi = `${this.lang.role.admin} ${this.user.user_name} ${this.lang.role.hi}`;
+			}
+			if(this.user.role == 1){
+				this.hi = `${this.lang.role.designer} ${this.user.user_name} ${this.lang.role.hi}`;
+			}
+			if(this.user.role == 2){
+				this.hi = `${this.lang.role.custom} ${this.user.user_name} ${this.lang.role.hi}`;
+			}
+
+			// 默认进入 group 路由
+			if(this.$route.name == 'index'){
+				this.$router.push('/group');
+			}
+
+			// 刷新时菜单高亮
+			switch (this.$route.name){
+				case 'list':
+					this.currentMenu = this.lang.menu.list;
+					break;
+				case 'user':
+					this.currentMenu = this.lang.menu.user;
+					break;
+				case 'team':
+					this.currentMenu = this.lang.menu.team;
+					break;
+			}
+		},
+		methods: {
+			// 切换菜单
+			menuClick (item){
+				this.currentMenu = item.name;
+				this.$router.push(item.route);
+			},
+			// 退出登录
+			logout (){
+				let that = this;
+				this.$confirm(this.lang.tips.logout.title, this.lang.tips.logout.t, {
+                    type: 'warning',
+					cancelButtonText: this.lang.tips.logout.cancel,
+					confirmButtonText: this.lang.tips.logout.confirm,
+                    beforeClose (action, instance, done){
+                        if(action == 'confirm'){
+                            that.$router.push('/login');
+							localStorage.removeItem('designer_user');
+                            done();
+                        }else{
+                            done();
+                        }
+                    }
+                }).catch(() => {});
+			}
+		}
+	}
+</script>
+
+<style lang="less" scoped>
+	@import url('../tools/common.less');
+    .index{
+		width: 100%;
+		height: 100%;
+
+		/* 头部 */
+		.head{
+			width: calc(100% - 60px);
+			height: 60px;
+			border-bottom: 1px solid rgb(236, 236, 236);
+			padding: 0 30px;
+			background: #ffffff;
+			position: absolute;
+			top: 0;
+			left: 0;
+			z-index: 3;
+			display: flex;
+			justify-content: space-between;
+			align-items: center;
+			.user{
+				display: flex;
+				justify-content: flex-start;
+				align-items: center;
+				img{
+					width: 45px;
+					height: 45px;
+					border-radius: 50%;
+				}
+				p{
+					margin-left: 20px;
+				}
+				span{
+					margin-left: 30px;
+					cursor: pointer;
+				}
+			}
+			.menu{
+				display: flex;
+				justify-content: flex-end;
+				align-items: center;
+				span{
+					display: block;
+					padding: 0 15px;
+					height: 58px;
+					line-height: 60px;
+					margin: 0 10px;
+					cursor: pointer;
+					user-select: none;
+					border-bottom: 2px solid transparent;
+				}
+				span.current{
+					color: @color;
+					border-bottom: 2px solid rgb(126, 172, 250);
+				}
+			}
+		}
+
+
+		/* 内容区 */
+		.content{
+			width: 100%;
+			height: calc(100% - 61px);
+			position: absolute;
+			top: 60px;
+			left: 0;
+			z-index: 1;
+		}
+	}
+</style>

+ 320 - 0
src/views/list.vue

@@ -0,0 +1,320 @@
+<template>
+    <div class="list" v-loading="loading">
+        <div class="list_head" v-if="user.role != 2">
+            <el-button type="primary" size="small" icon="el-icon-arrow-left" plain @click="$router.push('/group')">返回项目列表</el-button>
+            <div class="upload">
+                <el-button type="primary" size="small" icon="el-icon-upload">上传文件</el-button>
+                <input type="file" id="file" @change="uploadFile">
+            </div>
+        </div>
+        <div class="list_item">
+            <div class="nodata" v-if="list.length == 0"><el-empty description="暂无项目"></el-empty></div>
+            <div class="box" v-for="item in list" :key="item.id">
+                <div class="main">
+                    <div class="menu">
+                        <i class="el-icon-edit" @click="rename(item.id)" v-if="item.create_id == user.id"></i>
+                        <i class="el-icon-delete" @click="del(item.id)" v-if="item.create_id == user.id"></i>
+                    </div>
+                    <div class="cover" @click="goProject(item.id)">
+                        <img class="project_cover" :src="'http://designer.xuxiangbo.com/' + item.img_url" alt="">
+                    </div>
+                </div>
+                <div class="title">
+                    <p>{{item.title}}</p>
+                    <p>{{item.user_name}} · {{item.create_time}}</p>
+                </div>
+            </div>
+        </div>
+
+        <!-- 上传状态 -->
+        <div class="uploading" v-if="uploading">
+            <div class="box">
+                <el-progress type="circle" :percentage="uploadProgress"></el-progress>
+                <p v-for="item in uploadTips" :key="item">{{item}}...</p>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+    import timer from '../tools/timer';
+    export default {
+        name: 'list',
+        data (){
+            return {
+                user: {},
+                list: [],
+                loading: false,
+                uploading: false,
+                uploadProgress: 0,
+                uploadTips: [
+                    '正在上传文件',
+                    '正在解析文件',
+                    '正在保存数据'
+                ]
+            }
+        },
+        created (){
+            this.getList();
+        },
+        methods: {
+            // 获取列表
+            getList (){
+                // 根据id列表获取项目信息
+                this.$ajax.get('/project/list/?id=' + this.$route.query.id).then(r => {
+                    this.list = r.map(item => {
+                        item.create_time = timer.time('y-m-d', item.create_time * 1000);
+                        return item;
+                    });
+                    
+                    // 处理封面展示方式,横图 = row,竖图 = col
+                    this.$nextTick(() => {
+                        let imgs = document.querySelectorAll('.project_cover');
+                        imgs.forEach(item => {
+                            let img = new Image();
+                            img.onload = () => {
+                                if(img.width > img.height){
+                                    item.classList.add('row');
+                                }else{
+                                    item.classList.add('col');
+                                }
+                            }
+                            img.src = item.src;
+                        })
+                        this.loading = false;
+                    })
+                })
+            },
+            // 重命名
+            rename (id){
+                let that = this;
+                that.$prompt('请输入新的项目名称', '重命名', {
+                    inputPattern: /\S/,
+                    inputErrorMessage: '请输入项目名',
+                    closeOnPressEscape: false,
+                    closeOnClickModal: false,
+                    beforeClose (action, instance, done){
+                        if(action == 'confirm'){
+                            that.$ajax.post('/project/rename', {
+                                id,
+                                title: instance.inputValue
+                            }).then(r => {
+                                that.$message.success('修改成功');
+                                that.list.map(item => {
+                                    if(item.id == id){
+                                        item.title = instance.inputValue;
+                                    }
+                                })
+                                done();
+                            })
+                        }else{
+                            done();
+                        }
+                    }
+                }).catch(() => {});
+            },
+            // 删除项目
+            del (id){
+                let that = this;
+                this.$confirm('此操作将永久删除该项目, 是否继续?', '提示', {
+                    type: 'warning',
+                    closeOnPressEscape: false,
+                    closeOnClickModal: false,
+                    beforeClose (action, instance, done){
+                        if(action == 'confirm'){
+                            let index = 0;
+                            that.list.map((item, i) => {
+                                if(item.id == id){
+                                    index = i;
+                                }
+                            })
+                            that.$ajax.get('/project/del/?id=' + id).then(r => {
+                                that.list.splice(index, 1);
+                                that.$message.success('删除成功');
+                                done();
+                            })
+                        }else{
+                            done();
+                        }
+                    }
+                }).catch(() => {});
+            },
+            // 查看项目
+            goProject (id){
+                this.$router.push({
+                    path: '/project',
+                    query: { id }
+                })
+            },
+            // 上传文件
+            uploadFile (){
+                this.$nextTick(() => {
+                    let file = document.querySelector('#file').files[0];
+                    this.$message.error(file.name);
+                    file.value = null;
+                    let data = {
+                        file
+                    }
+                    // this.$ajax.post('/projects/upload/', data).then(result => {
+                        
+                    // })
+                })
+            }
+        }
+    }
+</script>
+
+<style lang="less" scoped>
+    @import url('../tools/common.less');
+    .list{
+        width: calc(100% - 60px);
+        height: 100%;
+        padding: 0 30px;
+        display: flex;
+        justify-content: flex-start;
+        align-content: flex-start;
+        flex-wrap: wrap;
+        overflow-x: hidden;
+        overflow-y: auto;
+        .uploading{
+            width: 100%;
+            height: 100%;
+            position: fixed;
+            top: 0;
+            left: 0%;
+            background: rgba(0, 0, 0, 0.6);
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            .box{
+                width: 300px;
+                padding: 50px 0;
+                background: #ffffff;
+                display: flex;
+                flex-wrap: wrap;
+                justify-content: center;
+                align-items: center;
+                border-radius: 5px;
+                p{
+                    width: 100%;
+                    text-align: center;
+                    margin-top: 10px;
+                    color: #888888;
+                }
+            }
+        }
+        .list_head{
+            width: calc(100% - 20px);
+            height: 40px;
+            padding: 20px 10px;
+            display: flex;
+            justify-content: flex-start;
+            align-items: center;
+            .upload{
+                margin-left: 20px;
+                position: relative;
+                input{
+                    width: 100px;
+                    height: 35px;
+                    opacity: 0;
+                    cursor: pointer;
+                    position: absolute;
+                    top: 0;
+                    left: 0;
+                    z-index: 99;
+                }
+            }
+        }
+        .list_item{
+            width: 100%;
+            height: calc(100% - 80px);
+            display: flex;
+            justify-content: flex-start;
+            align-content: flex-start;
+            flex-wrap: wrap;
+            overflow-x: hidden;
+            overflow-y: auto;
+            .nodata{
+                width: 100%;
+                height: 70%;
+                display: flex;
+                justify-content: center;
+                align-items: center;
+            }
+            .box{
+                width: 240px;
+                height: 300px;
+                background: #ffffff;
+                border-radius: 5px;
+                margin: 10px;
+                box-shadow: 0px 3px 7px 0px rgba(0,0,0,0.0500);
+                .main{
+                    width: 220px;
+                    height: 220px;
+                    background: #F5F5F7;
+                    border-radius: 5px;
+                    margin: 10px;
+                    transition: all 0.3s;
+                    .menu{
+                        width: 220px;
+                        height: 30px;
+                        line-height: 30px;
+                        display: flex;
+                        justify-content: flex-end;
+                        align-items: center;
+                        i{
+                            display: block;
+                            font-size: 14px;
+                            margin-right: 15px;
+                            color: #888888;
+                            cursor: pointer;
+                        }
+                        i:hover{
+                            color: @color;
+                        }
+                        i.designer-gengduo{
+                            font-size: 20px;
+                        }
+                    }
+                    .cover{
+                        width: 200px;
+                        height: 170px;
+                        padding: 10px;
+                        display: flex;
+                        justify-content: center;
+                        align-items: center;
+                        cursor: pointer;
+                        overflow: hidden;
+                        img.row{
+                            width: 100%;
+                            border-radius: 10px;
+                        }
+                        img.col{
+                            height: 100%;
+                            border-radius: 10px;
+                        }
+                    }
+                }
+                .title{
+                    width: 240px;
+                    height: 60px;
+                    text-align: center;
+                    p:last-child{
+                        margin-top: 5px;
+                        font-size: 12px;
+                        color: #888888;
+                    }
+                }
+                &:hover{
+                    .main{
+                        background: #F8F5F2;
+                    }
+                }
+            }
+        }
+            
+        .list_item::-webkit-scrollbar{
+            display: none;
+        }
+    }
+</style>

+ 113 - 0
src/views/login.vue

@@ -0,0 +1,113 @@
+<template>
+	<div class="login">
+		<div class="box">
+			<div class="title">{{lang.title}}</div>
+			<div class="title">Welcome To Login</div>
+			<div class="form">
+				<el-input style="margin-top: 20px" v-model="account" :placeholder="lang.inputs[0]"></el-input>
+				<el-input style="margin-top: 20px" type="password" v-model="password" :placeholder="lang.inputs[1]"></el-input>
+				<div class="form_foot">
+					<el-checkbox v-model="remenberPassword">{{lang.foot[0]}}</el-checkbox>
+					<span>{{lang.foot[1]}}</span>
+				</div>
+			</div>
+			<el-button type="primary" class="submit" :loading="submitting" @click="submit">{{lang.submit}}</el-button>
+		</div>
+	</div>
+</template>
+
+<script>
+	export default {
+		name: 'login',
+		data (){
+			return {
+				lang: {},
+				account: '',
+				password: '',
+				remenberPassword: true,
+				submitting: false
+			}
+		},
+		created (){
+			// 多语言支持
+			this.lang = this.$lang('login');
+		},
+		methods: {
+			// 提交登录
+			submit (){
+				// 表单验证
+				if(this.account == ''){
+					this.$message.warning(this.lang.tips.form.account);
+					return;
+				}
+				if(this.password == ''){
+					this.$message.warning(this.lang.tips.form.passwd);
+					return;
+				}
+
+				// 登录请求
+				this.submitting = true;
+				this.$ajax.post('/user/login', {
+					account: this.account,
+					password: this.password
+				}).then(r => {
+					localStorage.setItem('designer_user', JSON.stringify(r));
+					this.$router.push('/');
+				}).catch(() => {
+					this.submitting = false;
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="less" scoped>
+	@import url('../tools/common.less');
+	.login{
+		width: 100%;
+		height: 100%;
+		position: absolute;
+		background: url('@{imgurl}/img/login-bg.jpg') #ffffff;
+		background-size: cover;
+		background-position: center center;
+		.box{
+			width: 600px;
+			height: calc(100% - 120px);
+			padding-top: 120px;
+			position: absolute;
+			top: 0;
+			right: 0;
+			background: #ffffff;
+			box-shadow: 0 0 30px 0 rgba(65, 95, 217, 0.3);
+			.title{
+				width: 400px;
+				margin: 10px auto;
+				font-size: 30px;
+				color: #555555;
+			}
+			.form{
+				width: 400px;
+				margin: 100px auto;
+				input{
+					width: 100%;
+					margin: 10px 0;
+				}
+				.form_foot{
+					width: 100%;
+					display: flex;
+					margin-top: 10px;
+					justify-content: space-between;
+					color: #666666;
+					span{
+						cursor: pointer;
+					}
+				}
+			}
+			.submit{
+				width: 400px;
+				display: block;
+				margin: 0 auto;
+			}
+		}
+	}
+</style>

+ 302 - 0
src/views/project.vue

@@ -0,0 +1,302 @@
+<template>
+    <div class="project">
+        <div class="project_head">
+            <div class="back" @click="$router.go(-1)">
+                <i class="el-icon-arrow-left"></i> 返回项目列表
+            </div>
+            <div class="title">{{info.title}}</div>
+            <div class="share" @click="share">
+                <i class="el-icon-share"></i>
+            </div>
+        </div>
+        <div class="project_main" v-loading="loading">
+            <!-- <div class="imgs" v-if="imgs[currentImg].url">
+                <p @click="imgClick(index)" v-for="(item, index) in imgs" :key="item.id" :class="index == currentImg ? 'current' : ''">{{item.title}}</p>
+            </div>
+            <div class="imgs nodata" v-if="!imgs[currentImg].url">
+                <span>暂无图片</span>
+            </div> -->
+            <div class="canvas" v-if="imgs[currentImg].url">
+                <myCanvas v-if="showImg" :url="imgs[currentImg].url" :imgId="imgs[currentImg].id" :datas="marks" @add="addMarks"></myCanvas>
+            </div>
+            <div class="canvas nodata" v-if="!imgs[currentImg].url">
+                <div>
+                    <el-button type="primary" size="small" :icon="uploading ? 'el-icon-loading' : 'el-icon-upload'" plain>上传文件</el-button>
+                    <input @change="upload" type="file">
+                </div>
+            </div>
+            <div class="marks" v-if="marks.length > 0">
+                <div class="mark_item" v-for="item in marks" :key="item.id">
+                    <div class="mark_item_num">
+                        <span>{{item.mark_num}}</span>
+                    </div>
+                    <div class="mark_item_main">
+                        <p class="mark_item_title">{{item.mark_title}}</p>
+                        <p class="mark_item_text">{{item.mark_text}}</p>
+                        <p class="mark_item_time">
+                            <span>{{item.mark_time}}</span>
+                            <span>{{item.status}}</span>
+                        </p>
+                    </div>
+                </div>
+            </div>
+            <div class="marks nodata" v-if="marks.length == 0">
+                暂无标注
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+    import timer from '../tools/timer';
+    import myCanvas from '../components/myCanvas.vue';
+    export default {
+        name: 'project',
+        data (){
+            return {
+                loading: true,
+                info: {},
+                imgs: [
+                    { url: '' }
+                ],
+                marks: [],
+                marksData: [],
+                currentImg: 0,
+                showImg: true,
+                uploading: false
+            }
+        },
+        components: {
+            myCanvas
+        },
+        beforeMount (){
+            this.$ajax.get(`/project/get/?id=${this.$route.query.id}`).then(r => {
+                this.info = r.info;
+
+                // 无数据
+                if(r.imgs.length == 0){
+                    this.loading = false;
+                    return;
+                }
+
+                this.imgs = r.imgs.map(item => {
+                    item.url = `http://designer.xuxiangbo.com/${item.url}`;
+                    return item;
+                })
+
+                let status = {
+                    '0': '已标记',
+                    '1': '修改中',
+                    '2': '已修改',
+                    '3': '已结束'
+                }
+                this.marksData = r.marks.map(item => {
+                    item.mark_time = timer.time('y-m-d h:i:s', item.mark_time * 1000);
+                    item.status = status[item.status];
+                    return item;
+                });
+                this.getMarks();
+                this.loading = false;
+            })
+        },
+        methods: {
+            // 根据当前图片的索引,生成标记列表
+            getMarks (){
+                let result = [];
+                this.marksData.map(item => {
+                    if(item.img_id == this.imgs[this.currentImg].id){
+                        result.push(item);
+                    }
+                })
+                this.marks = result;
+            },
+            // 分享
+            share (){
+                this.$copy(this.info.id, this.info.title);
+            },
+            // 添加标记点监听
+            addMarks (r){
+                let data = this.marksData.map(item => item);
+                data.push(r);
+                this.marksData = data;
+            },
+            // 点击切换图片
+            imgClick (index){
+                this.currentImg = index;
+                this.getMarks();
+            },
+            // 上传文件
+            upload (){
+                this.$nextTick(() => {
+                    let input = document.querySelector('input');
+                    let data = {
+                        file: input.files[0],
+                        project: this.$route.query.id,
+                    }
+                    this.$ajax.post('/project/upload/', data).then(result => {
+                        this.$router.go(0);
+                    })
+                })
+            }
+        }
+    }
+</script>
+
+<style lang="less" scoped>
+    .project{
+        width: 100%;
+        height: 100%;
+        overflow: hidden;
+        position: relative;
+        .project_head{
+            width: calc(100% - 40px);
+            height: 50px;
+            background: #3B3755;
+            position: absolute;
+            top: 0;
+            left: 0;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            color: #ffffff;
+            padding: 0 20px;
+            .back{
+                cursor: pointer; 
+            }
+            .share{
+                i{
+                    font-size: 16px;
+                    cursor: pointer;
+                }
+            }
+        }
+        .project_main{
+            width: 100%;
+            height: calc(100% - 50px);
+            position: absolute;
+            top: 50px;
+            left: 0;
+            .imgs{
+                width: 220px;
+                height: 100%;
+                overflow: hidden;
+                overflow-y: auto;
+                background: #ffffff;
+                position: absolute;
+                top: 0;
+                left: 0;
+                p{
+                    width: 100%;
+                    text-indent: 30px;
+                    padding: 10px 0;
+                    cursor: pointer;
+                    overflow: hidden;
+                    text-overflow: ellipsis;
+                    white-space: nowrap;
+                }
+                p.current{
+                    background: #EBEDEE;
+                }
+            }
+            .imgs::-webkit-scrollbar{
+                display: none;
+            }
+            .canvas{
+                width: calc(100% - 260px);
+                height: 100%;
+                overflow: hidden;
+                position: absolute;
+                top: 0;
+                left: 260px;
+            }
+            .marks{
+                width: 260px;
+                height: 100%;
+                overflow: hidden;
+                overflow-y: auto;
+                background: #ffffff;
+                position: absolute;
+                top: 0;
+                left: 0;
+                .mark_item{
+                    display: flex;
+                    justify-content: flex-start;
+                    align-items: flex-start;
+                    padding: 15px 0;
+                    border-bottom: 1px solid rgb(238, 238, 238);
+                    .mark_item_num{
+                        width: 40px;
+                        text-align: center;
+                        span{
+                            display: inline-block;
+                            width: 22px;
+                            height: 22px;
+                            border-radius: 50%;
+                            background: #FD4F4F;
+                            color: #ffffff;
+                            line-height: 22px;
+                            font-size: 12px;
+                            position: relative;
+                            top: -2px;
+                        }
+                    }
+                    .mark_item_main{
+                        width: 200px;
+                        .mark_item_title{
+                            color: #555555;
+                        }
+                        .mark_item_text{
+                            font-size: 12px;
+                            margin-top: 5px;
+                            line-height: 24px;
+                            color: #999999;
+                        }
+                        .mark_item_time{
+                            font-size: 12px;
+                            color: #999999;
+                            margin-top: 10px;
+                            display: flex;
+                            justify-content: space-between;
+                            align-items: center;
+                        }
+                    }
+                }
+            }
+            .marks::-webkit-scrollbar {
+                width : 6px;
+                height: 1px;
+            }
+            .marks::-webkit-scrollbar-thumb {
+                border-radius: 10px;
+                box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.05);
+                background: rgb(216, 216, 216);
+            }
+            .marks::-webkit-scrollbar-track {
+                box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.05);
+                border-radius: 10px;
+                background: rgb(241, 241, 241);
+            }
+            .nodata{
+                text-align: center;
+                padding-top: 50px;
+                color: #a1a1a1;
+            }
+            .canvas.nodata{
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                padding-top: 0;
+                input{
+                    width: 100px;
+                    height: 34px;
+                    opacity: 0;
+                    cursor: pointer;
+                    position: absolute;
+                    z-index: 2;
+                    top: calc(50% - 17px);
+                    left: calc(50% - 50px);
+                }
+            }
+        }
+    }
+</style>

+ 207 - 0
src/views/team.vue

@@ -0,0 +1,207 @@
+<template>
+    <div class="team">
+        <div class="head">
+            <div class="left">
+                <p>{{lang.title}}</p>
+                <p>{{lang.title2}}<span>{{count}}</span>{{lang.title3}}</p>
+            </div>
+            <div class="right">
+                <el-button type="primary" size="small" style="margin-left: 10px" @click="showAdd = true">{{lang.add}}</el-button>
+            </div>
+        </div>
+        <el-table class="table" border :data="list" size="small" v-loading="loading">
+            <el-table-column prop="id" label="ID" width="100"></el-table-column>
+            <el-table-column :label="lang.table.nick">
+                <template slot-scope="scope">
+                    <img :src="scope.row.avatar">
+                    <span>{{scope.row.user_name}}</span>
+                </template>
+            </el-table-column>
+            <el-table-column prop="create_time" :label="lang.table.regTime"></el-table-column>
+            <el-table-column prop="last_time" :label="lang.table.lastTime"></el-table-column>
+            <el-table-column prop="role" :label="lang.table.role" width="160"></el-table-column>
+            <el-table-column prop="statusText" :label="lang.table.status" width="160"></el-table-column>
+            <el-table-column :label="lang.table.dost" width="160">
+                <template slot-scope="scope">
+                    <el-tooltip v-if="scope.row.status == 1" class="item" effect="dark" :content="lang.tips9" placement="top">
+                        <el-button type="text" icon="el-icon-unlock" @click="off(scope.row.id, 0)"></el-button>
+                    </el-tooltip>
+                    <el-tooltip v-if="scope.row.status == 0" class="item" effect="dark" :content="lang.tips10" placement="top">
+                        <el-button type="text" icon="el-icon-lock" @click="off(scope.row.id, 1)"></el-button>
+                    </el-tooltip>
+                    <el-tooltip class="item" effect="dark" :content="lang.tips8" placement="top">
+                        <el-button type="text" icon="el-icon-delete" style="margin-left: 20px" @click="del(scope.row.id)"></el-button>
+                    </el-tooltip>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagelist">
+            <el-pagination background :page-size="50" layout="prev, pager, next" :total="count"></el-pagination>
+        </div>
+        <el-dialog :title="lang.tips11" append-to-body :visible.sync="showAdd" width="500px" :show-close="false" :before-close="() => {}">
+            <el-form :model="addForm" label-width="80px" size="small">
+                <el-form-item :label="lang.tips12">
+                    <el-input v-model="addForm.name" :placeholder="lang.tips13" style="width: 360px"></el-input>
+                </el-form-item>
+                <el-form-item :label="lang.tips14">
+                    <el-select v-model="addForm.role" :placeholder="lang.tips15" style="width: 360px">
+                        <el-option :label="lang.tips16" value="2"></el-option>
+                        <el-option :label="lang.tips17" value="1"></el-option>
+                        <el-option :label="lang.tips18" value="0"></el-option>
+                    </el-select>
+                </el-form-item>
+            </el-form>
+            <span slot="footer" class="dialog-footer">
+                <el-button size="small" @click="cancelAdd">{{lang.tips19}}</el-button>
+                <el-button size="small" type="primary" @click="add">{{lang.tips20}}</el-button>
+            </span>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+    import timer from '../tools/timer';
+    import global from '../tools/global';
+    export default {
+        name: 'team',
+        data (){
+            return {
+                key: '',
+                list: [],
+                count: 0,
+                page: 1,
+                loading: false,
+                showAdd: false,
+                addForm: {
+                    name: '',
+                    role: '2'
+                }
+            }
+        },
+        created (){
+            this.lang = this.$lang('team');
+        },
+        mounted (){
+            this.getlist();
+        },
+        methods: {
+            // 获取数据列表
+            getlist (page = 0){
+                if(page > 0){
+                    this.page = page;
+                }
+                this.loading = true;
+                this.$ajax.get(`/user/list/?page=${this.page}`).then(result => {
+                    console.log(result)
+                    const role = this.lang.role;
+                    this.count = Number(result.count);
+                    this.list = result.list.map(item => {
+                        item.id = Number(item.id) + 1000000;
+                        item.last_time = timer.time('y-m-d h:i:s', item.last_time * 1000);
+                        item.create_time = timer.time('y-m-d h:i:s', item.create_time * 1000);
+                        item.statusText = item.status == 0 ? this.lang.status1 : this.lang.status2;
+                        item.role = role[item.role];
+                        item.avatar = `${global.host}${item.avatar}`;
+                        return item;
+                    })
+                    this.loading = false;
+                })
+            },
+            // 锁定/解锁账户
+            off (id, status){
+                this.$ajax.get(`/user/off/?id=${id - 1000000}&status=${status}`).then(result => {
+                    this.$message.success(this.lang.tips1);
+                    this.getlist();
+                })
+            },
+            // 取消添加账户
+            cancelAdd (){
+                this.showAdd = false;
+                this.addForm = {
+                    name: '',
+                    role: '2'
+                }
+            },
+            // 删除
+            del (id){
+                this.$confirm(this.lang.tips2, this.lang.tips3, {
+                    confirmButtonText: this.lang.tips5,
+                    cancelButtonText: this.lang.tips4,
+                    type: 'warning'
+                }).then(() => {
+                    this.$ajax.get(`/user/del/?id=${id - 1000000}`).then(result => {
+                        this.$message.success(this.lang.tips6);
+                        this.getlist(1);
+                    })
+                }).catch(() => {});
+            },
+            // 添加账户
+            add (){
+                if(this.addForm.name == ''){
+                    this.$message.warning(this.lang.tips7);
+                    return;
+                }
+                this.$ajax.post('/user/add', {
+                    username: this.addForm.name,
+                    role: Number(this.addForm.role)
+                }).then(result => {
+                    this.showAdd = false;
+                    this.addForm = {
+                        name: '',
+                        role: '2'
+                    }
+                })
+                this.getlist(1);
+            }
+        }
+    }
+</script>
+
+<style lang="less" scoped>
+    @import url('../tools/common.less');
+    .team{
+        width: calc(100% - 60px);
+        height: calc(100% - 60px);
+        padding: 30px;
+        background: #ffffff;
+        .head{
+            display: flex;
+            justify-content: space-between;
+            align-items: flex-end;
+            .left{
+                p:first-child{
+                    font-size: 18px;
+                }
+                p:last-child{
+                    font-size: 14px;
+                    margin-top: 8px;
+                    color: #999999;
+                    span{
+                        color: @color;
+                    }
+                }
+            }
+            .right{
+                display: flex;
+                justify-content: flex-end;
+                align-items: center;
+            }
+        }
+        .table{
+            width: 100%;
+            max-height: calc(100% - 160px);
+            margin-top: 50px;
+            img{
+                width: 38px;
+                height: 38px;
+                vertical-align: middle;
+                border-radius: 50%;
+                margin-right: 10px;
+            }
+        }
+        .pagelist{
+            text-align: center;
+            margin-top: 20px;
+        }
+    }
+</style>

+ 167 - 0
src/views/user.vue

@@ -0,0 +1,167 @@
+<template>
+    <div class="user">
+        <div class="head">
+            <p>{{lang.title}}</p>
+            <p>{{lang.title2}}</p>
+        </div>
+        <div class="content">
+            <div class="item itemImg">
+                <img style="margin-left: 30px" :src="user.avatar" alt="">
+            </div>
+            <div class="item" style="margin-top: 20px">
+                <p>{{lang.form.account}}:{{user.user_name}}</p>
+                <p><i class="el-icon-edit-outline" @click="rename"></i></p>
+            </div>
+            <div class="item">
+                <p>{{lang.form.passwd}}:******</p>
+                <el-button type="text" style="margin-left: 20px" @click="repass">{{lang.form.reset}}</el-button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+    import global from '@/tools/global';
+    export default {
+        name: 'user',
+        data (){
+            return {
+                lang: {},
+                user: {}
+            }
+        },
+        created (){
+            this.lang = this.$lang('user');
+        },
+        mounted (){
+            this.user = JSON.parse(localStorage.getItem('designer_user'));
+			this.user.avatar = global.host + this.user.avatar;
+        },
+        methods: {
+            // 重命名
+            rename (id){
+                let that = this;
+                that.$prompt(that.lang.alert.account.title2, that.lang.alert.account.title, {
+                    inputPattern: /\S/,
+                    inputErrorMessage: that.lang.alert.account.error,
+                    closeOnPressEscape: false,
+                    closeOnClickModal: false,
+					cancelButtonText: this.lang.alert.account.cancel,
+					confirmButtonText: this.lang.alert.account.confirm,
+                    beforeClose (action, instance, done){
+                        if(action == 'confirm'){
+                            that.$ajax.post('/user/rename', {
+                                id: that.user.id,
+                                name: instance.inputValue
+                            }).then(r => {
+                                done();
+                                that.$message.success(that.lang.alert.account.success);
+                                setTimeout(() => {
+                                    that.$message.warning(that.lang.alert.account.success2);
+                                }, 200);
+                                setTimeout(() => {
+                                    that.$router.push('/login');
+                                }, 1000);
+                            })
+                        }else{
+                            done();
+                        }
+                    }
+                }).catch(() => {});
+            },
+            
+            // 重置密码
+            repass (id){
+                let that = this;
+                that.$prompt(that.lang.alert.passwd.title2, that.lang.alert.passwd.title, {
+                    inputPattern: /\S/,
+                    inputErrorMessage: that.lang.alert.passwd.error,
+                    closeOnPressEscape: false,
+                    closeOnClickModal: false,
+					cancelButtonText: this.lang.alert.passwd.cancel,
+					confirmButtonText: this.lang.alert.passwd.confirm,
+                    beforeClose (action, instance, done){
+                        if(action == 'confirm'){
+                            that.$ajax.post('/user/repass', {
+                                id: that.user.id,
+                                password: instance.inputValue
+                            }).then(r => {
+                                done();
+                                that.$message.success(that.lang.alert.passwd.success);
+                                setTimeout(() => {
+                                    that.$message.warning(that.lang.alert.passwd.success2);
+                                }, 200);
+                                setTimeout(() => {
+                                    that.$router.push('/login');
+                                }, 1000);
+                            })
+                        }else{
+                            done();
+                        }
+                    }
+                }).catch(() => {});
+            }
+        }
+    }
+</script>
+
+<style lang="less" scoped>
+    @import url('../tools/common.less');
+    .user{
+        width: 100%;
+        height: 100%;
+        .head{
+            width: 100%;
+            height: 200px;
+            background: url('@{imgurl}/img/userbg.png');
+            text-align: center;
+            color: #ffffff;
+            display: flex;
+            justify-content: center;
+            align-content: center;
+            flex-wrap: wrap;
+            p{
+                width: 100%;
+            }
+            p:first-child{
+                font-size: 30px;
+            }
+            p:last-child{
+                font-size: 14px;
+                margin-top: 10px;
+            }
+        }
+        .content{
+            width: calc(100% - 60px);
+            height: calc(100% - 240px);
+            margin: 20px auto;
+            box-shadow: 0px 3px 7px 0px rgba(0,0,0,0.0500);
+            border-radius: 10px;
+            background: #ffffff;
+            .item{
+                width: 200px;
+                margin: 0 auto;
+                padding: 5px 50px;
+                display: flex;
+                justify-content: space-between;
+                align-items: center;
+                img{
+                    width: 100px;
+                    cursor: pointer;
+                }
+                i{
+                    margin-left: 20px;
+                    font-size: 18px;
+                    cursor: pointer;
+                }
+                i:hover{
+                    color: @color;
+                }
+            }
+            .itemImg{
+                justify-content: center;
+                padding-top: 50px;
+            }
+        }
+    }
+</style>