xiongzhu 7 anni fa
parent
commit
e492134212

BIN
vue/src/assets/icon_switch.png


BIN
vue/src/assets/icon_switch_pre.png


BIN
vue/src/assets/icon_upload.png


+ 1 - 1
vue/src/base.less

@@ -1,6 +1,6 @@
 @pageBg: #0E1822;
 @main: #F15436;
-@mainDark: #0B7475;
+@mainDark: #F15436;
 
 @text1: #ffffff;
 @text2: #666666;

+ 555 - 0
vue/src/components/DoubleElimination.vue

@@ -0,0 +1,555 @@
+<template>
+    <div class="svg-map" ref="svgMap">
+        <svg
+            xmlns="http://www.w3.org/2000/svg"
+            :width="(loserRound + 2) * (width + xGap) / 1.2"
+            :height="(loserRoundParticipantNum[0] / 2 * (height * 2 + yGap) * 3 + 2 * (titleHeight + yGap) + yGap) / 1.2"
+            :viewBox="`0 0 ${(loserRound + 2) * (width + xGap)} ${loserRoundParticipantNum[0] / 2 * (height * 2 + yGap) * 3 + 2 * (titleHeight + yGap) + yGap}`"
+            class="svg-vs-info-double-elimination"
+        >
+            <rect class="title-rect" x="20" y="0" rx="8" ry="8" :height="titleHeight" :width="(loserRound + 2) * (width + xGap) + xGap / 2 - 40"></rect>
+            <text
+                v-for="(n,i) in loserRound"
+                class="round-text"
+                :x="i * (width + xGap) + xGap / 2 + width / 2"
+                :y="titleHeight / 2 + 6"
+                text-anchor="middle"
+            >胜者组第{{n}}轮</text>
+            <text class="round-text" :x="loserRound * (width + xGap) + xGap / 2 + width / 2" :y="titleHeight / 2 + 6" text-anchor="middle">决赛</text>
+            <text class="round-text" :x="(loserRound + 1) * (width + xGap) + xGap / 2 + width / 2" :y="titleHeight / 2 + 6" text-anchor="middle">附加赛</text>
+            <template v-for="(i,r) in round">
+                <vs-pair
+                    v-for="(j,g) in roundParticipantNum[r] / 2"
+                    :key="`w-${r}-${g}`"
+                    type="win"
+                    :round="round"
+                    :width="width"
+                    :height="height"
+                    :xGap="xGap"
+                    :yGap="yGap"
+                    :titleHeight="titleHeight"
+                    :r="r"
+                    :g="g"
+                    :mode="mode"
+                    :allMatchData="matchData"
+                    @switchAgainst="switchAgainst"
+                    :switchData="switchData"
+                ></vs-pair>
+            </template>
+            <template v-for="(i,r) in round">
+                <image
+                    v-for="(j,g) in roundParticipantNum[r] / 2"
+                    v-if="showUploadIcon(r,g,'win')"
+                    class="upload-icon"
+                    :x="uploadIconPosition(r,g,'win').x"
+                    :y="uploadIconPosition(r,g,'win').y"
+                    :width="28"
+                    :height="28"
+                    xlink:href="../assets/icon_upload.png"
+                    @click="uploadMatchData(r,g,'win')"
+                ></image>
+            </template>
+            <rect
+                class="title-rect"
+                rx="8"
+                ry="8"
+                x="20"
+                :y="roundParticipantNum[0] / 2 * (2 * height + yGap) + titleHeight + yGap"
+                :height="titleHeight"
+                :width="loserRound * (width + xGap) - 40"
+            ></rect>
+            <text
+                v-for="(n,i) in loserRound"
+                class="round-text"
+                :x="i * (width + xGap) + xGap / 2 + width / 2"
+                :y="titleHeight / 2 + 6 + roundParticipantNum[0] / 2 * (2 * height + yGap) + titleHeight + yGap"
+                text-anchor="middle"
+            >败者组第{{n}}轮</text>
+            <template v-for="(i,r) in loserRound">
+                <vs-pair
+                    v-for="(j,g) in loserRoundParticipantNum[r] / 2"
+                    type="lose"
+                    :key="`l-${r}-${g}`"
+                    :round="loserRound"
+                    :width="width"
+                    :height="height"
+                    :xGap="xGap"
+                    :yGap="yGap"
+                    :titleHeight="titleHeight"
+                    :r="r"
+                    :g="g"
+                    :mode="mode"
+                    :participantNum="participantNum"
+                    :allMatchData="matchData"
+                    @switchAgainst="switchAgainst"
+                    :switchData="switchData"
+                ></vs-pair>
+            </template>
+            <template v-for="(i,r) in loserRound">
+                <image
+                    v-for="(j,g) in loserRoundParticipantNum[r] / 2"
+                    v-if="showUploadIcon(r,g,'lose')"
+                    class="upload-icon"
+                    :x="uploadIconPosition(r,g,'lose').x"
+                    :y="uploadIconPosition(r,g,'lose').y"
+                    :width="28"
+                    :height="28"
+                    xlink:href="../assets/icon_upload.png"
+                    @click="uploadMatchData(r,g,'lose')"
+                ></image>
+            </template>
+            <g>
+                <rect
+                    :x="finalX"
+                    :y="finalY - height"
+                    :width="width"
+                    :height="height"
+                    class="team-rect"
+                    :style="{fill:finalData&&finalData.user1===highlightUser?'#F15436':''}"
+                ></rect>
+                <text :x="finalX + 40" :y="finalY - height + height/2 + 4" class="team-name">{{finalData.applyInfo1 ? finalData.applyInfo1.nickname : '暂无'}}</text>
+                <text
+                    :x="finalX + width - 20"
+                    :y="finalY - height + height/2 + 4"
+                    text-anchor="end"
+                    class="team-score"
+                >{{finalData.score1 >= 0 ? finalData.score1 : '-'}}</text>
+                <rect
+                    :x="finalX"
+                    :y="finalY"
+                    :width="width"
+                    :height="height"
+                    class="team-rect"
+                    :style="{fill:finalData&&finalData.user2===highlightUser?'#F15436':''}"
+                ></rect>
+                <text :x="finalX + 40" :y="finalY + height/2 + 4" class="team-name">{{finalData.applyInfo2 ? finalData.applyInfo2.nickname : '暂无'}}</text>
+                <text
+                    :x="finalX + width - 20"
+                    :y="finalY + height/2 + 4"
+                    text-anchor="end"
+                    class="team-score"
+                >{{finalData.score2 >= 0 ? finalData.score2 : '-'}}</text>
+                <text :x="finalX - 10" :y="finalY + 4" text-anchor="end" class="vs-num">{{finalNum}}</text>
+                <polyline :points="getPath(0)" class="line"></polyline>
+                <polyline :points="getPath(1)" class="line"></polyline>
+                <image
+                    :x="finalX + 5"
+                    :y="finalY - height + (height - 20) / 2"
+                    :width="30"
+                    :height="20"
+                    xlink:href="http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/0.jpg"
+                ></image>
+                <image
+                    :x="finalX + 5"
+                    :y="finalY + (height - 20) / 2"
+                    :width="30"
+                    :height="20"
+                    xlink:href="http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/0.jpg"
+                ></image>
+                <rect
+                    :x="finalX"
+                    :y="finalY - height"
+                    :width="width"
+                    :height="height"
+                    style="fill: rgba(0,0,0,0)"
+                    @mouseover="mouseover(0,1)"
+                    @mouseout="mouseleave"
+                ></rect>
+                <rect :x="finalX" :y="finalY" :width="width" :height="height" style="fill: rgba(0,0,0,0)" @mouseover="mouseover(0,2)" @mouseout="mouseleave"></rect>
+            </g>
+            <g>
+                <rect
+                    :x="finalX + width + xGap"
+                    :y="finalY - height"
+                    :width="width"
+                    :height="height"
+                    :style="{fill:extraData&&extraData.user1===highlightUser?'#F15436':''}"
+                    class="team-rect"
+                ></rect>
+                <text
+                    :x="finalX + width + xGap + 40"
+                    :y="finalY - height + height/2 + 4"
+                    class="team-name"
+                >{{extraData.applyInfo1 ? extraData.applyInfo1.nickname : '暂无'}}</text>
+                <text
+                    :x="finalX + width + xGap + width - 20"
+                    :y="finalY - height + height/2 + 4"
+                    text-anchor="end"
+                    class="team-score"
+                >{{extraData.score1 >= 0 ? extraData.score1 : '-'}}</text>
+                <rect
+                    :x="finalX + width + xGap"
+                    :y="finalY"
+                    :width="width"
+                    :height="height"
+                    :style="{fill:extraData&&extraData.user2===highlightUser?'#F15436':''}"
+                    class="team-rect"
+                ></rect>
+                <text
+                    :x="finalX + width + xGap + 40"
+                    :y="finalY + height/2 + 4"
+                    class="team-name"
+                >{{extraData.applyInfo2 ? extraData.applyInfo2.nickname : '暂无'}}</text>
+                <text
+                    :x="finalX + width + xGap + width - 20"
+                    :y="finalY + height/2 + 4"
+                    text-anchor="end"
+                    class="team-score"
+                >{{extraData.score2 >= 0 ? extraData.score2 : '-'}}</text>
+                <text :x="finalX + width + xGap - 10" :y="finalY + 4" text-anchor="end" class="vs-num">{{finalNum + 1}}</text>
+                <polyline :points="`${finalX + width} ${finalY}, ${finalX + width + xGap - 40} ${finalY}`" class="line"></polyline>
+                <image
+                    :x="finalX + width + xGap + 5"
+                    :y="finalY - height + (height - 20) / 2"
+                    :width="30"
+                    :height="20"
+                    xlink:href="http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/0.jpg"
+                ></image>
+                <image
+                    :x="finalX + width + xGap + 5"
+                    :y="finalY + (height - 20) / 2"
+                    :width="30"
+                    :height="20"
+                    xlink:href="http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/0.jpg"
+                ></image>
+                <rect
+                    :x="finalX + width + xGap"
+                    :y="finalY - height"
+                    :width="width"
+                    :height="height"
+                    style="fill: rgba(0,0,0,0)"
+                    @mouseover="mouseover(1,1)"
+                    @mouseout="mouseleave"
+                ></rect>
+                <rect
+                    :x="finalX + width + xGap"
+                    :y="finalY"
+                    :width="width"
+                    :height="height"
+                    style="fill: rgba(0,0,0,0)"
+                    @mouseover="mouseover(1,2)"
+                    @mouseout="mouseleave"
+                ></rect>
+            </g>
+            <image
+                v-if="showUploadIcon(null,null,null,finalNum)"
+                class="upload-icon"
+                :x="finalX + width - 14"
+                :y="finalY - 14"
+                :width="28"
+                :height="28"
+                xlink:href="../assets/icon_upload.png"
+                @click="uploadMatchData(null,null,null,finalNum)"
+            ></image>
+            <image
+                v-if="showUploadIcon(null,null,null,finalNum+1)"
+                class="upload-icon"
+                :x="finalX + width + xGap + width - 14"
+                :y="finalY - 14"
+                :width="28"
+                :height="28"
+                xlink:href="../assets/icon_upload.png"
+                @click="uploadMatchData(null,null,null,finalNum+1)"
+            ></image>
+        </svg>
+    </div>
+</template>
+<script>
+import VsPair from './VsPair'
+import { mapState } from 'vuex'
+
+export default {
+    props: {
+        mode: String,
+        matchData: {
+            default() {
+                return []
+            }
+        },
+        participantNum: {
+            required: true
+        },
+        admin: {}
+    },
+    data() {
+        return {
+            width: 200,
+            height: 30,
+            xGap: 100,
+            yGap: 20,
+            titleHeight: 44,
+            switchData: []
+        }
+    },
+    computed: {
+        ...mapState(['userInfo', 'highlightUser']),
+        finalData() {
+            return this.matchData.find(i => {
+                return i.round === this.finalNum;
+            }) || {}
+        },
+        extraData() {
+            return this.matchData.find(i => {
+                return i.round === this.finalNum + 1;
+            }) || {}
+        },
+        finalX() {
+            return this.xGap / 2 + (this.xGap + this.width) * this.loserRound;
+        },
+        finalY() {
+            return ((this.height * 2 * Math.pow(2, Math.floor((this.loserRound - 1) / 2))
+                + this.yGap * (Math.pow(2, Math.floor((this.loserRound - 1) / 2)) - 1) - this.height * 2) / 2
+                + this.yGap + (this.titleHeight + this.yGap) * 2 - Math.ceil((this.loserRound - 1) / 2) * this.height
+                + this.height + Math.pow(2, this.loserRound / 2) * (2 * this.height + this.yGap)
+                + (this.height * 2 * Math.pow(2, (this.round - 1)) + this.yGap * (Math.pow(2, (this.round - 1)) - 1) - this.height * 2) / 2
+                + this.yGap + this.titleHeight + this.yGap) / 2;
+        },
+        finalNum() {
+            return this.participantNum * 2 - 2;
+        },
+        round() {
+            let round = 0;
+            let temp = this.participantNum;
+            while (temp > 1) {
+                temp /= 2;
+                round++;
+            }
+            return round;
+        },
+        loserRound() {
+            let round = 0;
+            let temp = this.participantNum / 2;
+            while (temp > 1) {
+                temp /= 2;
+                round++;
+            }
+            return round * 2;
+        },
+        roundParticipantNum() {
+            let roundParticipantNum = [];
+            for (let i = 0; i < this.round; i++) {
+                roundParticipantNum.push(Math.pow(2, this.round - i))
+            }
+            return roundParticipantNum;
+        },
+        loserRoundParticipantNum() {
+            let roundParticipantNum = [];
+            for (let i = 0; i < this.loserRound / 2; i++) {
+                roundParticipantNum.push(Math.pow(2, this.loserRound / 2 - i));
+                roundParticipantNum.push(Math.pow(2, this.loserRound / 2 - i));
+            }
+            return roundParticipantNum;
+        }
+    },
+    methods: {
+        getPath(i) {
+            if (i === 0) {
+                let y0 = (this.height * 2 * Math.pow(2, this.round - 1) + this.yGap * (Math.pow(2, this.round - 1) - 1) - this.height * 2) / 2
+                    + this.titleHeight + this.yGap + this.height;
+                let y1 = this.finalY - this.height + 5;
+                let x0 = this.finalX - this.xGap;
+                let x1 = this.finalX - 15;
+                return `${x0} ${y0}, ${x1} ${y0}, ${x1} ${y1}`
+            } else {
+                let y0 = (this.height * 2 * Math.pow(2, Math.floor((this.loserRound - 1) / 2)) + this.yGap * (Math.pow(2, Math.floor((this.loserRound - 1) / 2)) - 1) - this.height * 2) / 2
+                    + (this.titleHeight + this.yGap) * 2 - Math.ceil((this.loserRound - 1) / 2) * this.height + this.height
+                    + Math.pow(2, this.loserRound / 2) * (2 * this.height + this.yGap) + this.height;
+                let y1 = this.finalY + this.height - 5;
+                let x0 = this.finalX - this.xGap;
+                let x1 = this.finalX - 15;
+                return `${x0} ${y0}, ${x1} ${y0}, ${x1} ${y1}`
+            }
+        },
+        uploadIconPosition(r, g, type) {
+            let x, y;
+            if (type === 'lose') {
+                x = (this.xGap + this.width) * r + this.xGap / 2;
+            } else {
+                x = (this.xGap + this.width) * r + (r > 1 ? (this.width + this.xGap) * (r - 1) : 0) + this.xGap / 2;
+            }
+            x = x + this.width - 14;
+
+            let i = 0;
+            let groupGap;
+            let yOffset;
+            if (type === 'lose') {
+                groupGap = (this.height * 2 + this.yGap) * Math.pow(2, Math.floor(r / 2));
+                if (r === 0) {
+                    yOffset = 0;
+                } else {
+                    yOffset = (this.height * 2 * Math.pow(2, Math.floor(r / 2)) + this.yGap * (Math.pow(2, Math.floor(r / 2)) - 1) - this.height * 2) / 2
+                }
+                y = yOffset + g * groupGap + this.yGap * i + (this.titleHeight + this.yGap) * 2 - Math.ceil(r / 2) * this.height + this.height
+                    + Math.pow(2, this.loserRound / 2) * (2 * this.height + this.yGap) + this.height - 14;
+            } else {
+                groupGap = (this.height * 2 + this.yGap) * Math.pow(2, r);
+                if (r === 0) {
+                    yOffset = 0;
+                } else {
+                    yOffset = (this.height * 2 * Math.pow(2, r) + this.yGap * (Math.pow(2, r) - 1) - this.height * 2) / 2
+                }
+                y = yOffset + g * groupGap + this.yGap * i + this.titleHeight + this.yGap + this.height - 14;
+            }
+            return { x, y }
+        },
+        switchAgainst(userId, matchDataId) {
+            let find = this.switchData.findIndex(i => {
+                return i.userId === userId
+            });
+            if (find > -1) {
+                this.switchData.splice(find, 1);
+            } else {
+                this.switchData.push({ userId: userId, matchDataId: matchDataId })
+            }
+
+            if (this.switchData.length === 2) {
+                this.$http.get({
+                    url: '/against/switchAgainst',
+                    data: {
+                        type: 1,
+                        matchDataId1: this.switchData[0].matchDataId,
+                        userId1: this.switchData[0].userId,
+                        matchDataId2: this.switchData[1].matchDataId,
+                        userId2: this.switchData[1].userId,
+                    }
+                }).then(res => {
+                    if (res.success) {
+                        this.$emit('switchComplete');
+                        this.switchData = [];
+                    }
+                });
+            }
+        },
+        getNum(r, g, type) {
+            let num = 0;
+            if (type === 'win') {
+                if (r === 0) {
+                    num = g + 1;
+                } else {
+                    num = 0;
+                    for (let i = 0; i < r; i++) {
+                        num += Math.pow(2, this.round - i - 1);
+                    }
+                    num = num + g + 1;
+                }
+            } else {
+                if (r === 0) {
+                    num = this.participantNum + g;
+                } else {
+                    num = 0;
+                    for (let i = 0; i < Math.floor(r / 2); i++) {
+                        num += Math.pow(2, this.loserRound / 2 - i - 1) * 2;
+                    }
+                    num = this.participantNum + num + g + Math.pow(2, this.loserRound / 2 - Math.floor(r / 2) - 1) * (r % 2);
+                }
+            }
+            return num;
+        },
+        showUploadIcon(r, g, type, num) {
+            if (!num) {
+                num = this.getNum(r, g, type);
+            }
+            let matchData = this.matchData.find(i => {
+                return i.round === num;
+            });
+            if (this.mode === 'upload' && matchData && this.userInfo) {
+                if (matchData.user1 && matchData.user2) {
+                    if (matchData.state === 0) {
+                        if (this.admin) {
+                            return true
+                        } else if (matchData.user1 === this.userInfo.id || matchData.user2 === this.userInfo.id) {
+                            return true
+                        }
+                    }
+                }
+            }
+            // return this.mode === 'upload' && matchData && matchData.user1 && matchData.user2 && matchData.state !== 2;
+        },
+        uploadMatchData(r, g, type, num) {
+            if (!num) {
+                num = this.getNum(r, g, type);
+            }
+            this.$emit('uploadMatchData', this.matchData.find(i => {
+                return i.round === num;
+            }));
+        },
+        onMouseDown(e) {
+            this.move = true
+        },
+        onMouseMove(e) {
+            if (this.move) {
+                this.$refs.svgMap.scrollLeft -= e.movementX || 0
+            }
+        },
+        onTouchMove(e) {
+        },
+        onMouseUp(e) {
+            this.move = false
+        },
+        mouseover(i, j) {
+            if (i === 0) {
+                if (this.finalData && this.finalData[`user${j}`]) {
+                    this.$store.commit('updateHighlightUser', this.finalData[`user${j}`])
+                }
+            } else {
+                if (this.extraData && this.extraData[`user${j}`]) {
+                    this.$store.commit('updateHighlightUser', this.extraData[`user${j}`])
+                }
+            }
+        },
+        mouseleave() {
+            this.$store.commit('updateHighlightUser', null)
+        }
+    },
+    watch: {
+        participantNum() {
+
+        }
+    },
+    components: {
+        VsPair
+    }
+}
+</script>
+<style lang="less">
+.svg-map {
+    width: 1220px;
+    overflow-x: auto;
+}
+
+.svg-vs-info-double-elimination {
+    cursor: default;
+    user-select: none;
+    .title-rect {
+        fill: #192836;
+    }
+    .round-text {
+        font-size: 13px;
+        fill: #f2f3f4;
+    }
+    .team-rect {
+        fill: #192836;
+    }
+    .team-name {
+        font-size: 13px;
+        fill: #f2f3f4;
+    }
+    .team-score {
+        font-size: 13px;
+        fill: #f2f3f4;
+    }
+    .vs-num {
+        fill: #f2f3f4;
+    }
+    .line {
+        fill: none;
+        stroke: rgba(255, 255, 255, 0.5);
+        stroke-width: 1px;
+        fill-opacity: 0;
+    }
+    .upload-icon,
+    .switch-icon {
+        cursor: pointer;
+    }
+}
+</style>

+ 217 - 0
vue/src/components/RoundGroup.vue

@@ -0,0 +1,217 @@
+<template>
+    <div class="card">
+        <div class="title">{{name}}</div>
+        <div class="row">
+            <div class="name"></div>
+            <div class="col head">胜</div>
+            <div class="col head">负</div>
+            <div class="col head">积分</div>
+        </div>
+        <div class="row" v-for="n in groupNum" :key="`row-${n}`">
+            <div class="name">
+                <div :style="{width:width+'px'}">{{n}}.{{statisticsData[n-1].name}}</div>
+            </div>
+            <div class="col">{{statisticsData[n-1].win}}</div>
+            <div class="col">{{statisticsData[n-1].lose}}</div>
+            <div class="col">{{statisticsData[n-1].points}}</div>
+        </div>
+        <template v-for="j in loop">
+            <div class="row" :key="`loop-${j}`">
+                <div class="name"></div>
+                <div class="col head" style="width:100%;text-align:right;">{{loopText(j)}}</div>
+            </div>
+            <div class="row" v-for="n in groupNum * (groupNum - 1) / 2" :key="`vs-${j}-${n}`">
+                <div class="num">{{n}}.</div>
+                <div class="name1" :style="{width:vsNameWidth+'px'}">{{matchData[j-1][n-1].applyInfo1.nickname}}</div>
+                <div class="score1">{{matchData[j-1][n-1].score1||'-'}}</div>
+                <div class="vs">VS</div>
+                <div class="score2">{{matchData[j-1][n-1].score2||'-'}}</div>
+                <div class="name2" :style="{width:vsNameWidth+'px'}">{{matchData[j-1][n-1].applyInfo2.nickname}}</div>
+            </div>
+        </template>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex'
+
+export default {
+    props: {
+        i: Number,
+        width: Number,
+        titleHeight: Number,
+        colWidth: Number,
+        rowHeight: Number,
+        gap: Number,
+        groupNum: Number,
+        loop: Number,
+        participantNum: Number,
+        allMatchData: Array,
+        allStatisticsData: Array,
+        mode: String,
+        switchData: Array,
+        admin: Boolean
+    },
+    data() {
+        return {}
+    },
+    computed: {
+        ...mapState(['userInfo']),
+        width() {
+            return window.innerWidth - 181
+        },
+        vsNameWidth() {
+            return (window.innerWidth - 150) / 2
+        },
+        colWidth() { },
+        height() {
+            return this.titleHeight + this.rowHeight * this.groupNum
+                + (this.mode === 'edit' ? 0 : this.loop * (this.titleHeight + this.groupNum * (this.groupNum - 1) / 2 * this.rowHeight))
+        },
+        x() {
+            return this.i % 2 * (this.width + this.gap)
+        },
+        y() {
+            return Math.floor(this.i / 2) * (this.gap + this.height)
+        },
+        name() {
+            let length = (this.participantNum / this.groupNum).toString(26).length;
+            let name = new Array(length).fill('A');
+            let str = (this.i).toString(26);
+            for (let i = 0; i < str.length; i++) {
+                let charCode = parseInt(str[i], 26) + 65;
+                name[length - str.length + i] = String.fromCharCode(charCode);
+            }
+            return name.join('') + '组'
+        },
+        filteredData() {
+            return this.allMatchData.filter(i => {
+                return i.groupName === this.name
+            });
+        },
+        matchData() {
+            let matchData = [];
+            for (let i = 0; i < this.loop; i++) {
+                matchData[i] = this.filteredData.filter(e => {
+                    return e.loopNum === i;
+                })
+            }
+            return matchData;
+        },
+        statisticsData() {
+            return this.allStatisticsData.filter(i => {
+                return i.groupName === this.name
+            });
+        }
+    },
+    methods: {
+        loopText(i) {
+            let chinese = ['一', '二', '三', '四', '五', '六', '七', '八', '九'];
+            return `详细对阵循环${chinese[i - 1]}`;
+        },
+        showSwitchIcon(userId) {
+            let hide = this.switchData.filter(i => {
+                return i.groupName === this.name && i.userId !== userId;
+            }).length > 0 || this.switchData.filter(i => {
+                return i.groupName !== this.name
+            }).length === 2;
+            return userId && this.mode === 'edit' && !hide;
+        },
+        highLight(userId) {
+            return this.switchData.findIndex(i => {
+                return i.userId === userId;
+            }) >= 0;
+        },
+        switchAgainst(userId) {
+            this.$emit('switchAgainst', {
+                userId: userId,
+                groupName: this.name
+            })
+        },
+        showUploadIcon(matchData) {
+            if (this.mode === 'upload' && matchData && this.userInfo) {
+                if (matchData.user1 && matchData.user2) {
+                    if (matchData.state === 0) {
+                        if (this.admin) {
+                            return true
+                        } else if (matchData.user1 === this.userInfo.id || matchData.user2 === this.userInfo.id) {
+                            return true
+                        }
+                    }
+                }
+            }
+            // return this.mode === 'upload' && !(matchData.state > 1);
+        },
+        uploadMatchData(matchData) {
+            this.$emit('uploadMatchData', matchData);
+        }
+    }
+}
+</script>
+<style lang="less" scoped>
+.card {
+    position: relative;
+    background: rgba(25, 40, 54, 1);
+    border-radius: 6px;
+    margin-left: 14px;
+    margin-right: 14px;
+    margin-bottom: 14px;
+    padding: 14px 12px 18px 12px;
+    font-size: 13px;
+    font-weight: 400;
+    color: rgba(255, 255, 255, 0.8);
+    line-height: 18px;
+    letter-spacing: 1px;
+    .title {
+        font-size: 13px;
+        font-weight: 700;
+        color: rgba(241, 84, 54, 1);
+        text-align: center;
+        margin-block-end: 20px;
+    }
+    .row {
+        height: 36px;
+        display: flex;
+        align-items: center;
+        .name {
+            flex-grow: 1;
+            > div {
+                text-overflow: ellipsis;
+                white-space: nowrap;
+                overflow: hidden;
+            }
+        }
+        .col {
+            width: 43px;
+            text-align: center;
+            font-size: 13px;
+            &.head {
+                font-weight: 700;
+                color: #ffffff;
+            }
+        }
+        .vs {
+            font-size: 13px;
+            font-weight: 700;
+            color: rgba(241, 84, 54, 1);
+        }
+        .num {
+            width: 30px;
+        }
+        .score1 {
+            text-align: center;
+            width: 30px;
+        }
+        .score2 {
+            text-align: center;
+            width: 30px;
+        }
+        .name1,
+        .name2 {
+            text-overflow: ellipsis;
+            overflow: hidden;
+            white-space: nowrap;
+            text-align: center;
+        }
+    }
+}
+</style>

+ 250 - 0
vue/src/components/RoundGroup1.vue

@@ -0,0 +1,250 @@
+<template>
+    <g>
+        <rect class="title-rect" :x="x" :y="y" :width="width" :height="titleHeight"></rect>
+        <rect class="title-indicator" :x="x" :y="y" width="6" :height="titleHeight"></rect>
+        <text class="group-text" :x="x + (width - 6 * colWidth) / 2 + colWidth / 4" :y="y + titleHeight / 2 + 8"
+              text-anchor="middle">
+            {{name}}
+        </text>
+        <text class="title-text" :x="x + width - 2 * colWidth - colWidth / 2" :y="y + titleHeight / 2 + 5"
+              text-anchor="middle">胜
+        </text>
+        <!-- <text class="title-text" :x="x + width - 4 * colWidth - colWidth / 2" :y="y + titleHeight / 2 + 5"
+              text-anchor="middle">平
+        </text> -->
+        <text class="title-text" :x="x + width - 1 * colWidth - colWidth / 2" :y="y + titleHeight / 2 + 5"
+              text-anchor="middle">负
+        </text>
+        <!-- <text class="title-text" :x="x + width - 2 * colWidth - colWidth / 2" :y="y + titleHeight / 2 + 5"
+              text-anchor="middle">大场
+        </text>
+        <text class="title-text" :x="x + width - colWidth - colWidth / 2" :y="y + titleHeight / 2 + 5"
+              text-anchor="middle">小场
+        </text> -->
+        <text class="title-text" :x="x + width - colWidth / 2" :y="y + titleHeight / 2 + 5"
+              text-anchor="middle">积分
+        </text>
+        <template v-for="n in groupNum">
+            <rect class="row" :x="x"
+                  :y="y + titleHeight + rowHeight * (n - 1)"
+                  :width="width" :height="rowHeight">
+            </rect>
+            <text class="title-text"
+                  :x="x + colWidth / 2"
+                  :y="y + titleHeight + rowHeight * (n - 1) + rowHeight / 2 + 5"
+                  text-anchor="middle">
+                {{n}}.
+            </text>
+            <text class="title-text"
+                  :x="x + 40"
+                  :y="y + titleHeight + rowHeight * (n - 1) + rowHeight / 2 + 5">
+                {{statisticsData[n-1].name}}
+            </text>
+            <text class="title-text"
+                  :x="x + width - 2 * colWidth - colWidth / 2"
+                  :y="y + titleHeight + rowHeight * (n - 1) + rowHeight / 2 + 5"
+                  text-anchor="middle">{{statisticsData[n-1].win}}
+            </text>
+            <!-- <text class="title-text"
+                  :x="x + width - 4 * colWidth - colWidth / 2"
+                  :y="y + titleHeight + rowHeight * (n - 1) + rowHeight / 2 + 5"
+                  text-anchor="middle">{{statisticsData[n-1].draw}}
+            </text> -->
+            <text class="title-text"
+                  :x="x + width - 1 * colWidth - colWidth / 2"
+                  :y="y + titleHeight + rowHeight * (n - 1) + rowHeight / 2 + 5"
+                  text-anchor="middle">{{statisticsData[n-1].lose}}
+            </text>
+            <!-- <text class="title-text"
+                  :x="x + width - 2 * colWidth - colWidth / 2"
+                  :y="y + titleHeight + rowHeight * (n - 1) + rowHeight / 2 + 5"
+                  text-anchor="middle">{{statisticsData[n-1].win}}-{{statisticsData[n-1].lose}}
+            </text>
+            <text class="title-text" :x="x + width - colWidth - colWidth / 2"
+                  :y="y + titleHeight + rowHeight * (n - 1) + rowHeight / 2 + 5"
+                  text-anchor="middle">{{statisticsData[n-1].winScore}}-{{statisticsData[n-1].loseScore}}
+            </text> -->
+            <text class="title-text" :x="x + width - colWidth / 2"
+                  :y="y + titleHeight + rowHeight * (n - 1) + rowHeight / 2 + 5"
+                  text-anchor="middle">{{statisticsData[n-1].points}}
+            </text>
+            <image
+                v-if="showSwitchIcon(statisticsData[n-1].userId)"
+                width="22" height="22" :x="x + 260"
+                :y="y + titleHeight + rowHeight * (n - 1) + (rowHeight - 22) / 2"
+                :xlink:href="highLight(statisticsData[n-1].userId) ? require('../assets/icon_switch_pre.png') : require('../assets/icon_switch.png')"
+                @click="switchAgainst(statisticsData[n-1].userId)">
+            </image>
+        </template>
+        <template v-for="j in loop" v-if="mode !== 'edit'">
+            <template v-for="n in groupNum * (groupNum - 1) / 2">
+                <rect class="title-rect" :x="x"
+                      :y="y + titleHeight + groupNum * rowHeight + (j - 1) * (titleHeight + rowHeight * groupNum * (groupNum - 1) / 2)"
+                      :width="width" :height="titleHeight">
+                </rect>
+                <text class="title-text" :x="x + 20"
+                      :y="y + titleHeight + groupNum * rowHeight + (j - 1) * (titleHeight + rowHeight * groupNum * (groupNum - 1) / 2) + titleHeight / 2 + 5">
+                    {{loopText(j)}}
+                </text>
+                <rect class="row" :x="x"
+                      :y="y + 2 * titleHeight + groupNum * rowHeight + (n - 1) * rowHeight + (j - 1) * (titleHeight + rowHeight * groupNum * (groupNum - 1) / 2)"
+                      :width="width" :height="rowHeight">
+                </rect>
+                <text class="title-text"
+                      :x="x + colWidth / 2"
+                      :y="y + 2 * titleHeight + groupNum * rowHeight + (n - 1) * rowHeight + (j - 1) * (titleHeight + rowHeight * groupNum * (groupNum - 1) / 2) + rowHeight / 2 + 5"
+                      text-anchor="middle">
+                    {{n}}.
+                </text>
+                <text class="vs"
+                      :x="x + width - (width - 50) / 2"
+                      :y="y + 2 * titleHeight + groupNum * rowHeight + (n - 1) * rowHeight + (j - 1) * (titleHeight + rowHeight * groupNum * (groupNum - 1) / 2) + rowHeight / 2 + 5"
+                      text-anchor="middle">
+                    vs
+                </text>
+                <text class="round-text"
+                      :x="x + width - (width - 50) / 2 - 15"
+                      :y="y + 2 * titleHeight + groupNum * rowHeight + (n - 1) * rowHeight + (j - 1) * (titleHeight + rowHeight * groupNum * (groupNum - 1) / 2) + rowHeight / 2 + 5"
+                      text-anchor="end">
+                    {{matchData[j-1][n-1].score1||'-'}}
+                </text>
+                <text class="round-text"
+                      :x="x + width - (width - 50) / 2 + 15"
+                      :y="y + 2 * titleHeight + groupNum * rowHeight + (n - 1) * rowHeight + (j - 1) * (titleHeight + rowHeight * groupNum * (groupNum - 1) / 2) + rowHeight / 2 + 5"
+                      text-anchor="start">
+                    {{matchData[j-1][n-1].score2||'-'}}
+                </text>
+                <text class="round-text"
+                      :x="x + width - (width - 50) / 2 - (width - 100) / 4"
+                      :y="y + 2 * titleHeight + groupNum * rowHeight + (n - 1) * rowHeight + (j - 1) * (titleHeight + rowHeight * groupNum * (groupNum - 1) / 2) + rowHeight / 2 + 5"
+                      text-anchor="middle">
+                    {{matchData[j-1][n-1].applyInfo1.nickname}}
+                </text>
+                <text class="round-text"
+                      :x="x + width - (width - 50) / 2 + (width - 100) / 4"
+                      :y="y + 2 * titleHeight + groupNum * rowHeight + (n - 1) * rowHeight + (j - 1) * (titleHeight + rowHeight * groupNum * (groupNum - 1) / 2) + rowHeight / 2 + 5"
+                      text-anchor="middle">
+                    {{matchData[j-1][n-1].applyInfo2.nickname}}
+                </text>
+                <image
+                    v-if="showUploadIcon(matchData[j-1][n-1])"
+                    class="upload-icon"
+                    width="22" height="22"
+                    :x="x + 60"
+                    :y="y + 2 * titleHeight + groupNum * rowHeight + (n - 1) * rowHeight + (j - 1) * (titleHeight + rowHeight * groupNum * (groupNum - 1) / 2) + (rowHeight - 22) / 2"
+                    xlink:href="../assets/icon_upload.png"
+                    @click="uploadMatchData(matchData[j-1][n-1])">
+                </image>
+            </template>
+        </template>
+    </g>
+</template>
+<script>
+    import {mapState} from 'vuex'
+
+    export default {
+        props: {
+            i: Number,
+            width: Number,
+            titleHeight: Number,
+            colWidth: Number,
+            rowHeight: Number,
+            gap: Number,
+            groupNum: Number,
+            loop: Number,
+            participantNum: Number,
+            allMatchData: Array,
+            allStatisticsData: Array,
+            mode: String,
+            switchData: Array,
+            admin: Boolean
+        },
+        data() {
+            return {}
+        },
+        computed: {
+            ...mapState(['userInfo']),
+            height() {
+                return this.titleHeight + this.rowHeight * this.groupNum
+                    + (this.mode === 'edit' ? 0 : this.loop * (this.titleHeight + this.groupNum * (this.groupNum - 1) / 2 * this.rowHeight))
+            },
+            x() {
+                return this.i % 2 * (this.width + this.gap)
+            },
+            y() {
+                return Math.floor(this.i / 2) * (this.gap + this.height)
+            },
+            name() {
+                let length = (this.participantNum / this.groupNum).toString(26).length;
+                let name = new Array(length).fill('A');
+                let str = (this.i).toString(26);
+                for (let i = 0; i < str.length; i++) {
+                    let charCode = parseInt(str[i], 26) + 65;
+                    name[length - str.length + i] = String.fromCharCode(charCode);
+                }
+                return name.join('') + '组'
+            },
+            filteredData() {
+                return this.allMatchData.filter(i => {
+                    return i.groupName === this.name
+                });
+            },
+            matchData() {
+                let matchData = [];
+                for (let i = 0; i < this.loop; i++) {
+                    matchData[i] = this.filteredData.filter(e => {
+                        return e.loopNum === i;
+                    })
+                }
+                return matchData;
+            },
+            statisticsData() {
+                return this.allStatisticsData.filter(i => {
+                    return i.groupName === this.name
+                });
+            }
+        },
+        methods: {
+            loopText(i) {
+                let chinese = ['一', '二', '三', '四', '五', '六', '七', '八', '九'];
+                return `详细对阵循环${chinese[i - 1]}`;
+            },
+            showSwitchIcon(userId) {
+                let hide = this.switchData.filter(i => {
+                    return i.groupName === this.name && i.userId !== userId;
+                }).length > 0 || this.switchData.filter(i => {
+                    return i.groupName !== this.name
+                }).length === 2;
+                return userId && this.mode === 'edit' && !hide;
+            },
+            highLight(userId) {
+                return this.switchData.findIndex(i => {
+                    return i.userId === userId;
+                }) >= 0;
+            },
+            switchAgainst(userId) {
+                this.$emit('switchAgainst', {
+                    userId: userId,
+                    groupName: this.name
+                })
+            },
+            showUploadIcon(matchData) {
+                if (this.mode === 'upload' && matchData && this.userInfo) {
+                    if (matchData.user1 && matchData.user2) {
+                        if (matchData.state === 0) {
+                            if (this.admin) {
+                                return true
+                            } else if (matchData.user1 === this.userInfo.id || matchData.user2 === this.userInfo.id) {
+                                return true
+                            }
+                        }
+                    }
+                }
+                // return this.mode === 'upload' && !(matchData.state > 1);
+            },
+            uploadMatchData(matchData) {
+                this.$emit('uploadMatchData', matchData);
+            }
+        }
+    }
+</script>

+ 267 - 0
vue/src/components/RoundRobin.vue

@@ -0,0 +1,267 @@
+<template>
+    <div class="svg-map" ref="svgMap">
+        <!-- <svg
+            xmlns="http://www.w3.org/2000/svg"
+            class="svg-vs-info-round-robin"
+            :width="viewWidth"
+            :height="viewHeight"
+            :viewBox="`0 0 ${viewWidth} ${viewHeight}`"
+        > -->
+            <round-group
+                v-for="n in Math.ceil(participantNum / groupNum)"
+                :key="n"
+                :participantNum="participantNum"
+                :i="n - 1"
+                :width="width"
+                :titleHeight="titleHeight"
+                :colWidth="colWidth"
+                :rowHeight="rowHeight"
+                :gap="gap"
+                :groupNum="groupNum"
+                :loop="loop"
+                :mode="mode"
+                :allMatchData="matchData"
+                :allStatisticsData="statisticsData"
+                @switchAgainst="switchAgainst"
+                :switchData="switchData"
+                @uploadMatchData="$emit('uploadMatchData',$event)"
+                :admin="admin"
+            ></round-group>
+        <!-- </svg> -->
+    </div>
+</template>
+<script>
+import RoundGroup from './RoundGroup'
+
+export default {
+    props: {
+        participantNum: Number,
+        matchData: {
+            default() {
+                return []
+            }
+        },
+        mode: {
+            default: 'normal'
+        },
+        admin: {},
+        config: {
+            default() {
+                return {
+                    groupNum: 4,
+                    loopNum: 1,
+                    pointsWinLarge: 3,
+                    pointsDrawLarge: 1,
+                    pointsLoseLarge: 0,
+                    pointsWinSmall: 1,
+                    pointsLoseSmall: 0
+                }
+            }
+        }
+    },
+    data() {
+        return {
+            width: window.innerWidth - 28,
+            titleHeight: 36,
+            colWidth: 50,
+            rowHeight: 36,
+            gap: 30,
+            switchData: []
+        }
+    },
+    computed: {
+        groupNum() {
+            return this.config ?
+                (this.config.groupNum || 4)
+                : 4;
+        },
+        loop() {
+            return this.config ?
+                (this.config.loopNum || 1)
+                : 1;
+        },
+        viewWidth() {
+            return this.width * 2 + this.gap
+        },
+        viewHeight() {
+            return (Math.ceil(Math.ceil(this.participantNum / this.groupNum) / 2))
+                * ((1 + this.loop) * this.titleHeight
+                    + (this.groupNum + this.loop * this.groupNum * (this.groupNum - 1) / 2) * this.rowHeight + this.gap)
+        },
+        statisticsData() {
+            let data = [];
+            this.matchData.forEach(matchData => {
+                let index1 = data.findIndex(i => {
+                    return i.userId === matchData.user1;
+                });
+                let index2 = data.findIndex(i => {
+                    return i.userId === matchData.user2;
+                });
+                if (index1 < 0) {
+                    data.push({
+                        userId: matchData.user1,
+                        name: matchData.applyInfo1.nickname,
+                        lose: 0,
+                        win: 0,
+                        draw: 0,
+                        winScore: 0,
+                        loseScore: 0,
+                        points: 0,
+                        groupName: matchData.groupName
+                    });
+                    index1 = data.length - 1;
+                }
+                if (index2 < 0) {
+                    data.push({
+                        userId: matchData.user2,
+                        name: matchData.applyInfo2.nickname,
+                        lose: 0,
+                        win: 0,
+                        draw: 0,
+                        winScore: 0,
+                        loseScore: 0,
+                        points: 0,
+                        groupName: matchData.groupName
+                    });
+                    index2 = data.length - 1;
+                }
+                let data1 = data[index1];
+                let data2 = data[index2];
+                if (matchData.state === 2) {
+                    if (matchData.score1 > matchData.score2) {
+                        data1.win++;
+                        data2.lose++;
+                        data1.winScore += matchData.score1;
+                        data2.loseScore += matchData.score2;
+                    } else if (matchData.score1 < matchData.score2) {
+                        data1.lose++;
+                        data2.win++;
+                        data1.loseScore += matchData.score1;
+                        data2.winScore += matchData.score2;
+                    } else {
+                        data1.draw++;
+                        data2.draw++;
+                        data1.winScore += matchData.score1;
+                        data2.winScore += matchData.score2;
+                    }
+                }
+            });
+            data.forEach(i => {
+                i.points = i.win * this.config.pointsWinLarge
+                    + i.draw * this.config.pointsDrawLarge
+                    + i.lose * this.config.pointsLoseLarge
+                    + i.winScore * this.config.pointsWinSmall
+                    + i.loseScore * this.config.pointsLoseSmall
+            });
+            return data;
+        }
+    },
+    methods: {
+        onMouseDown(e) {
+            this.move = true
+        },
+        onMouseMove(e) {
+            if (this.move) {
+                this.$refs.svgMap.scrollLeft -= e.movementX || 0
+            }
+        },
+        onTouchMove(e) {
+        },
+        onMouseUp(e) {
+            this.move = false
+        },
+        switchAgainst(e) {
+            let find = this.switchData.findIndex(i => {
+                return i.userId === e.userId
+            });
+            if (find > -1) {
+                this.switchData.splice(find, 1);
+            } else {
+                this.switchData.push(e);
+            }
+
+            if (this.switchData.length === 2) {
+
+                this.$http.get({
+                    url: '/against/switchAgainst',
+                    data: {
+                        matchId: this.matchData[0].matchId,
+                        groupName1: this.switchData[0].groupName,
+                        userId1: this.switchData[0].userId,
+                        groupName2: this.switchData[1].groupName,
+                        userId2: this.switchData[1].userId,
+                        type: 3
+                    }
+                }).then(res => {
+                    if (res.success) {
+                        this.$emit('switchComplete');
+                        this.switchData = [];
+                    }
+                });
+            }
+        },
+    },
+    components: {
+        RoundGroup
+    }
+}
+</script>
+<style lang="less">
+.svg-map {
+    width: 100%;
+    overflow-x: auto;
+}
+
+.svg-vs-info-round-robin {
+    .title-rect {
+        fill: #293844;
+    }
+    .title-indicator {
+        fill: #f15436;
+    }
+    .group-text {
+        fill: #f15436;
+        font-size: 18px;
+        font-weight: 700;
+    }
+    .title-text {
+        font-size: 13px;
+        fill: #f2f3f4;
+    }
+    .row {
+        fill: #192836;
+    }
+    .vs {
+        fill: #f15436;
+        font-size: 14px;
+        font-weight: 700;
+    }
+    .round-text {
+        font-size: 13px;
+        fill: #f2f3f4;
+    }
+    .team-rect {
+        fill: #192836;
+    }
+    .team-name {
+        font-size: 13px;
+        fill: #f2f3f4;
+    }
+    .team-score {
+        font-size: 13px;
+        fill: #f2f3f4;
+    }
+    .vs-num {
+        fill: #f2f3f4;
+    }
+    .line {
+        fill: none;
+        stroke: #f2f3f4;
+        stroke-width: 2px;
+        fill-opacity: 0;
+    }
+    .upload-icon {
+        cursor: pointer;
+    }
+}
+</style>

+ 250 - 0
vue/src/components/SingleElimination.vue

@@ -0,0 +1,250 @@
+<template>
+    <div
+        class="svg-map"
+        ref="svgMap"
+    >
+        <svg
+            xmlns="http://www.w3.org/2000/svg"
+            :width="(round * (width + xGap) + xGap) / 1.2"
+            :height="(roundParticipantNum[0] / 2 * (height * 2 + yGap) + titleHeight + yGap) / 1.2"
+            :viewBox="`0 0 ${round * (width + xGap) + xGap} ${roundParticipantNum[0] / 2 * (height * 2 + yGap)  + titleHeight + yGap}`"
+            class="svg-vs-info-single-elimination"
+        >
+            <rect class="title-rect" x="20" y="0" rx="8" ry="8" :height="titleHeight" :width="round * (width + xGap) + xGap - 40"></rect>
+            <text v-for="(n,i) in round" class="round-text" :x="i * (width + xGap) + xGap + width / 2" :y="titleHeight / 2 + 6" text-anchor="middle">第{{n}}轮</text>
+            <template v-for="(i,r) in round">
+                <vs-pair
+                    v-for="(j,g) in roundParticipantNum[r] / 2"
+                    :key="`${r}-${g}`"
+                    type="normal"
+                    :round="round"
+                    :width="width"
+                    :height="height"
+                    :xGap="xGap"
+                    :yGap="yGap"
+                    :titleHeight="titleHeight"
+                    :r="r"
+                    :g="g"
+                    :showLine="showLine"
+                    :allMatchData="matchData"
+                    :mode="mode"
+                    @switchAgainst="switchAgainst"
+                    :switchData="switchData"
+                ></vs-pair>
+            </template>
+            <template v-for="(i,r) in round">
+                <image
+                    v-for="(j,g) in roundParticipantNum[r] / 2"
+                    v-if="showUploadIcon(r,g)"
+                    class="upload-icon"
+                    :x="uploadIconPosition(r,g).x"
+                    :y="uploadIconPosition(r,g).y"
+                    :width="28"
+                    :height="28"
+                    xlink:href="../assets/icon_upload.png"
+                    @click="uploadMatchData(r,g)"
+                ></image>
+            </template>
+        </svg>
+    </div>
+</template>
+<script>
+import VsPair from './VsPair'
+import { mapState } from 'vuex'
+
+export default {
+    props: {
+        mode: String,
+        matchData: {
+            default() {
+                return []
+            }
+        },
+        participantNum: {
+            required: true
+        },
+        admin: {}
+    },
+    data() {
+        return {
+            width: 200,
+            height: 30,
+            xGap: 60,
+            yGap: 20,
+            titleHeight: 44,
+            showLine: true,
+            switchData: []
+        }
+    },
+    computed: {
+        ...mapState(['userInfo']),
+        round() {
+            // let round = 0;
+            // let temp = this.participantNum;
+            // while (temp > 1) {
+            //     temp /= 2;
+            //     round++;
+            // }
+            // return round;
+            return parseInt(Math.log(this.participantNum) / Math.log(2)) || 0;
+        },
+        roundParticipantNum() {
+            let roundParticipantNum = [];
+            for (let i = 0; i < this.round; i++) {
+                roundParticipantNum.push(Math.pow(2, this.round - i))
+            }
+            return roundParticipantNum;
+        }
+    },
+    methods: {
+        uploadIconPosition(r, g) {
+            let i = 0;
+            let groupGap;
+            let yOffset;
+            let x = this.xGap + (this.xGap + this.width) * r + this.width - 14;
+            groupGap = (this.height * 2 + this.yGap) * Math.pow(2, r);
+            if (r === 0) {
+                yOffset = 0;
+            } else {
+                yOffset = (this.height * 2 * Math.pow(2, r) + this.yGap * (Math.pow(2, r) - 1) - this.height * 2) / 2
+            }
+            let y = yOffset + g * groupGap + this.yGap * i + this.titleHeight + this.yGap + this.height - 14;
+            return { x, y }
+        },
+        showUploadIcon(r, g) {
+            let num;
+            if (r === 0) {
+                num = g + 1;
+            } else {
+                num = 0;
+                for (let i = 0; i < r; i++) {
+                    num += Math.pow(2, this.round - i - 1);
+                }
+                num = num + g + 1;
+            }
+            let matchData = this.matchData.find(i => {
+                return i.round === num;
+            });
+            if (this.mode === 'upload' && matchData && this.userInfo) {
+                if (matchData.user1 && matchData.user2) {
+                    if (matchData.state === 0) {
+                        if (this.admin) {
+                            return true
+                        } else if (matchData.user1 === this.userInfo.id || matchData.user2 === this.userInfo.id) {
+                            return true
+                        }
+                    }
+                }
+            }
+            return false;
+            // return this.mode === 'upload' && matchData && matchData.user1 && matchData.user2 && matchData.state === 0 && (this.admin || (matchData.user1 === this.userInfo.id || matchData.user2 === this.userInfo.id));
+        },
+        switchAgainst(userId, matchDataId) {
+            let find = this.switchData.findIndex(i => {
+                return i.userId === userId
+            });
+            if (find > -1) {
+                this.switchData.splice(find, 1);
+            } else {
+                this.switchData.push({ userId: userId, matchDataId: matchDataId })
+            }
+
+            if (this.switchData.length === 2) {
+                this.$http.get({
+                    url: '/against/switchAgainst',
+                    data: {
+                        type: 1,
+                        matchDataId1: this.switchData[0].matchDataId,
+                        userId1: this.switchData[0].userId,
+                        matchDataId2: this.switchData[1].matchDataId,
+                        userId2: this.switchData[1].userId,
+                    }
+                }).then(res => {
+                    if (res.success) {
+                        this.$emit('switchComplete');
+                        this.switchData = [];
+                    }
+                });
+            }
+        },
+        uploadMatchData(r, g) {
+            let num;
+            if (r === 0) {
+                num = g + 1;
+            } else {
+                num = 0;
+                for (let i = 0; i < r; i++) {
+                    num += Math.pow(2, this.round - i - 1);
+                }
+                num = num + g + 1;
+            }
+            this.$emit('uploadMatchData', this.matchData.find(i => {
+                return i.round === num;
+            }));
+        },
+        onMouseDown(e) {
+            this.move = true
+        },
+        onMouseMove(e) {
+            if (this.move) {
+                this.$refs.svgMap.scrollLeft -= e.movementX || 0
+            }
+        },
+        onTouchMove(e) {
+        },
+        onMouseUp(e) {
+            this.move = false
+        }
+    },
+    watch: {
+        participantNum() {
+
+        }
+    },
+    components: {
+        VsPair
+    }
+}
+</script>
+<style lang="less">
+.svg-map {
+    width: 100%;
+    overflow-x: auto;
+}
+
+.svg-vs-info-single-elimination {
+    cursor: default;
+    user-select: none;
+    .title-rect {
+        fill: #192836;
+    }
+    .round-text {
+        font-size: 13px;
+        fill: #f2f3f4;
+    }
+    .team-rect {
+        fill: #192836;
+    }
+    .team-name {
+        font-size: 13px;
+        fill: #f2f3f4;
+    }
+    .team-score {
+        font-size: 13px;
+        fill: #f2f3f4;
+    }
+    .vs-num {
+        fill: #f2f3f4;
+    }
+    .line {
+        fill: none;
+        stroke: #f2f3f4;
+        stroke-width: 2px;
+        fill-opacity: 0;
+    }
+    .upload-icon,
+    .switch-icon {
+        cursor: pointer;
+    }
+}
+</style>

+ 274 - 0
vue/src/components/VsPair.vue

@@ -0,0 +1,274 @@
+<template>
+    <g>
+        <rect :x="x" :y="y0" :width="width" :height="height"
+              :style="{fill:matchData&&matchData.user1===highlightUser?'#F15436':''}" class="team-rect"></rect>
+        <text :x="x + 40" :y="y0 + height/2 + 4" class="team-name">
+            {{name(1)}}
+        </text>
+        <text :x="x + width - 20" :y="y0 + height/2 + 4" text-anchor="end" class="team-score">
+            {{matchData.score1 >= 0 ? matchData.score1 : '-'}}
+        </text>
+        <rect :x="x" :y="y1" :width="width" :height="height"
+              :style="{fill:matchData&&matchData.user2===highlightUser?'#F15436':''}" class="team-rect"></rect>
+        <text :x="x + 40" :y="y1 + height/2 + 4" class="team-name">
+            {{name(2)}}
+        </text>
+        <text :x="x + width - 20" :y="y1 + height/2 + 4" text-anchor="end" class="team-score">
+            {{matchData.score2 >= 0 ? matchData.score2 : '-'}}
+        </text>
+        <text :x="x - 10" :y="y1 + 4" text-anchor="end" class="vs-num">
+            {{num}}
+        </text>
+        <template v-if="type!=='lose'">
+            <polyline :points="getPath(0)" class="line"></polyline>
+            <polyline :points="getPath(1)" class="line"></polyline>
+        </template>
+        <polyline v-else :points="getPath(n-1)" v-for="n in ((r+1) % 2 + 1)" class="line">{{n}}</polyline>
+        <image v-if="showSwitchIcon1" class="switch-icon"
+               :x="x + width + 5" :y="y0 + height/2 - 11" width="22" height="22"
+               :xlink:href="selected1 ? require('../assets/icon_switch_pre.png') : require('../assets/icon_switch.png')"
+               @click="switchAgainst(matchData.user1)">
+        </image>
+        <image v-if="showSwitchIcon2" class="switch-icon"
+               :x="x + width + 5" :y="y1 + height/2 - 9" width="22" height="22"
+               :xlink:href="selected2 ? require('../assets/icon_switch_pre.png') : require('../assets/icon_switch.png')"
+               @click="switchAgainst(matchData.user2)">
+        </image>
+        <image :x="x + 5" :y="y0 + (height - 20) / 2" :width="30" :height="20"
+               xlink:href="http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/0.jpg"></image>
+        <image :x="x + 5" :y="y1 + (height - 20) / 2" :width="30" :height="20"
+               xlink:href="http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/0.jpg"></image>
+        <rect :x="x" :y="y0" :width="width" :height="height" style="opacity: 0;" @mouseover="mouseover(1)"
+              @mouseout="mouseleave"></rect>
+        <rect :x="x" :y="y1" :width="width" :height="height" style="opacity: 0;" @mouseover="mouseover(2)"
+              @mouseout="mouseleave"></rect>
+    </g>
+</template>
+<script>
+    import country from './country'
+    import {mapState} from 'vuex'
+
+    export default {
+        props: {
+            type: {
+                type: String,
+                required: true
+            },
+            round: Number,
+            width: Number,
+            height: Number,
+            xGap: Number,
+            yGap: Number,
+            titleHeight: Number,
+            r: Number,
+            g: Number,
+            mode: String,
+            allMatchData: {
+                default() {
+                    return []
+                }
+            },
+            switchData: Array,
+            participantNum: Number
+        },
+        data() {
+            return {selected: null}
+        },
+        computed: {
+            ...mapState(['highlightUser']),
+            x() {
+                switch (this.type) {
+                    case 'normal':
+                        return this.xGap + (this.xGap + this.width) * this.r;
+                    case 'lose':
+                        return (this.xGap + this.width) * this.r + this.xGap / 2;
+                    case 'win':
+                        return (this.xGap + this.width) * this.r
+                            + (this.r > 1 ? (this.width + this.xGap) * (this.r - 1) : 0) + this.xGap / 2;
+                }
+            },
+            y0() {
+                let i = 0;
+                let groupGap;
+                let yOffset;
+                switch (this.type) {
+                    case 'normal':
+                        groupGap = (this.height * 2 + this.yGap) * Math.pow(2, this.r);
+                        if (this.r === 0) {
+                            yOffset = 0;
+                        } else {
+                            yOffset = (this.height * 2 * Math.pow(2, this.r) + this.yGap * (Math.pow(2, this.r) - 1) - this.height * 2) / 2
+                        }
+                        return yOffset + this.g * groupGap + this.yGap * i + this.titleHeight + this.yGap;
+                    case 'lose':
+                        groupGap = (this.height * 2 + this.yGap) * Math.pow(2, Math.floor(this.r / 2));
+                        if (this.r === 0) {
+                            yOffset = 0;
+                        } else {
+                            yOffset = (this.height * 2 * Math.pow(2, Math.floor(this.r / 2)) + this.yGap * (Math.pow(2, Math.floor(this.r / 2)) - 1) - this.height * 2) / 2
+                        }
+                        return yOffset + this.g * groupGap + this.yGap * i + (this.titleHeight + this.yGap) * 2 - Math.ceil(this.r / 2) * this.height + this.height
+                            + Math.pow(2, this.round / 2) * (2 * this.height + this.yGap);
+                    case 'win':
+                        groupGap = (this.height * 2 + this.yGap) * Math.pow(2, this.r);
+                        if (this.r === 0) {
+                            yOffset = 0;
+                        } else {
+                            yOffset = (this.height * 2 * Math.pow(2, this.r) + this.yGap * (Math.pow(2, this.r) - 1) - this.height * 2) / 2
+                        }
+                        return yOffset + this.g * groupGap + this.yGap * i + this.titleHeight + this.yGap;
+                }
+            },
+            y1() {
+                return this.y0 + this.height;
+            },
+            num() {
+                switch (this.type) {
+                    case 'normal':
+                        if (this.r === 0) {
+                            return this.g + 1;
+                        } else {
+                            let num = 0;
+                            for (let i = 0; i < this.r; i++) {
+                                num += Math.pow(2, this.round - i - 1);
+                            }
+                            return num + this.g + 1;
+                        }
+                    case 'lose':
+                        if (this.r === 0) {
+                            return this.participantNum + this.g;
+                        } else {
+                            let num = 0;
+                            for (let i = 0; i < Math.floor(this.r / 2); i++) {
+                                num += Math.pow(2, this.round / 2 - i - 1) * 2;
+                            }
+                            return this.participantNum + num + this.g + Math.pow(2, this.round / 2 - Math.floor(this.r / 2) - 1) * (this.r % 2);
+                        }
+                    case 'win':
+                        if (this.r === 0) {
+                            return this.g + 1;
+                        } else {
+                            let num = 0;
+                            for (let i = 0; i < this.r; i++) {
+                                num += Math.pow(2, this.round - i - 1);
+                            }
+                            return num + this.g + 1;
+                        }
+                }
+            },
+            matchData() {
+                let matchData = {};
+                this.allMatchData.forEach(i => {
+                    if (i.round === this.num) {
+                        matchData = i;
+                    }
+                });
+                return matchData
+            },
+            selected1() {
+                let selected = false;
+                this.switchData.forEach(i => {
+                    if (this.matchData.user1 && i.userId === this.matchData.user1) {
+                        selected = true
+                    }
+                });
+                return selected
+            },
+            selected2() {
+                let selected = false;
+                this.switchData.forEach(i => {
+                    if (this.matchData.user2 && i.userId === this.matchData.user2) {
+                        selected = true
+                    }
+                });
+                return selected
+            },
+            showSwitchIcon1() {
+                return this.mode === 'edit' && this.matchData.user1 && !this.selected2 && (this.switchData.length < 2 || this.selected1)
+            },
+            showSwitchIcon2() {
+                return this.mode === 'edit' && this.matchData.user2 && !this.selected1 && (this.switchData.length < 2 || this.selected2)
+            }
+        },
+        methods: {
+            getPath(i) {
+                switch (this.type) {
+                    case 'normal':
+                        if (this.r === 0) {
+                            return ''
+                        }
+                        if (i === 0) {
+                            let x = this.x;
+                            let y = this.y0;
+                            let dy = Math.pow(2, this.r - 1) * (this.yGap + 2 * this.height) / 2 - this.height;
+                            return `${x - this.xGap} ${y - dy}, ${x - 15} ${y - dy}, ${x - 15} ${y + this.height - 12}`
+                        } else {
+                            let x = this.x;
+                            let y = this.y1 + this.height;
+                            let dy = Math.pow(2, this.r - 1) * (this.yGap + 2 * this.height) / 2 - this.height;
+                            return `${x - this.xGap} ${y + dy}, ${x - 15} ${y + dy}, ${x - 15} ${y - this.height + 12}`
+                        }
+                    case 'lose':
+                        if (this.r === 0) {
+                            return ''
+                        }
+                        if (this.r % 2 === 1) {
+                            return `${this.x - this.xGap} ${this.y0 + 2 * this.height}, ${this.x - 45} ${this.y0 + 2 * this.height}, ${this.x - 45} ${this.y0 + this.height}, ${this.x - 35} ${this.y0 + this.height}`
+                        }
+                        if (i === 0) {
+                            let x = this.x;
+                            let y = this.y0;
+                            let dy = Math.pow(2, Math.floor((this.r - 1) / 2)) * (this.yGap + 2 * this.height) / 2 - this.height;
+                            return `${x - this.xGap} ${y - dy}, ${x - 15} ${y - dy}, ${x - 15} ${y + this.height - 14}`
+                        } else {
+                            let x = this.x;
+                            let y = this.y1 + this.height;
+                            let dy = Math.pow(2, Math.floor((this.r - 1) / 2)) * (this.yGap + 2 * this.height) / 2 - this.height;
+                            return `${x - this.xGap} ${y + dy}, ${x - 15} ${y + dy}, ${x - 15} ${y - this.height + 14}`
+                        }
+                    case 'win':
+                        if (this.r === 0) {
+                            return ''
+                        }
+                        let dx = this.r > 1 ? (this.width + this.xGap) : 0;
+                        if (i === 0) {
+                            let x = this.x;
+                            let y = this.y0;
+                            let dy = Math.pow(2, this.r - 1) * (this.yGap + 2 * this.height) / 2 - this.height;
+                            return `${x - this.xGap - dx} ${y - dy}, ${x - 15} ${y - dy}, ${x - 15} ${y + this.height - 14}`
+                        } else {
+                            let x = this.x;
+                            let y = this.y1 + this.height;
+                            let dy = Math.pow(2, this.r - 1) * (this.yGap + 2 * this.height) / 2 - this.height;
+                            return `${x - this.xGap - dx} ${y + dy}, ${x - 15} ${y + dy}, ${x - 15} ${y - this.height + 14}`
+                        }
+                }
+            },
+            name(i) {
+                return this.matchData[`applyInfo${i}`] ? (this.matchData[`applyInfo${i}`].nickname || '暂无') : '暂无';
+            },
+            switchAgainst(userId) {
+                if (this.selected === userId) {
+                    this.selected = null
+                } else {
+                    this.selected = userId;
+                }
+                this.$emit('switchAgainst', userId, this.matchData.id)
+            },
+            mouseover(i) {
+                if (i === 1) {
+                    if (this.matchData && this.matchData.user1) {
+                        this.$store.commit('updateHighlightUser', this.matchData.user1)
+                    }
+                } else {
+                    if (this.matchData && this.matchData.user2) {
+                        this.$store.commit('updateHighlightUser', this.matchData.user2)
+                    }
+                }
+            },
+            mouseleave(i) {
+                this.$store.commit('updateHighlightUser', null)
+            }
+        }
+    }
+</script>

+ 373 - 0
vue/src/components/country.js

@@ -0,0 +1,373 @@
+export default [{
+    "id": 1,
+    "hot_key": "A",
+    "en_value": "Afghanistan",
+    "name": "阿富汗",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/1.jpg"
+}, {
+    "id": 2,
+    "hot_key": "A",
+    "en_value": "Argentina",
+    "name": "阿根廷",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/2.jpg"
+}, {
+    "id": 3,
+    "hot_key": "A",
+    "en_value": "Australia",
+    "name": "澳大利亚",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/3.jpg"
+}, {
+    "id": 4,
+    "hot_key": "A",
+    "en_value": "Austria",
+    "name": "奥地利",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/4.jpg"
+}, {
+    "id": 5,
+    "hot_key": "B",
+    "en_value": "Belgium",
+    "name": "比利时",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/5.jpg"
+}, {
+    "id": 6,
+    "hot_key": "B",
+    "en_value": "Brazil",
+    "name": "巴西",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/6.jpg"
+}, {
+    "id": 7,
+    "hot_key": "B",
+    "en_value": "Bulgaria",
+    "name": "保加利亚",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/7.jpg"
+}, {
+    "id": 8,
+    "hot_key": "B",
+    "en_value": "Burma",
+    "name": "缅甸",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/8.jpg"
+}, {
+    "id": 9,
+    "hot_key": "C",
+    "en_value": "Canada",
+    "name": "加拿大",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/9.jpg"
+}, {
+    "id": 10,
+    "hot_key": "C",
+    "en_value": "Chile",
+    "name": "智利",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/10.jpg"
+}, {
+    "id": 11,
+    "hot_key": "C",
+    "en_value": "China",
+    "name": "中国",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/11.jpg"
+}, {
+    "id": 12,
+    "hot_key": "C",
+    "en_value": "Colombia",
+    "name": "哥伦比亚",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/12.jpg"
+}, {
+    "id": 13,
+    "hot_key": "C",
+    "en_value": "Czech Republic",
+    "name": "捷克",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/13.jpg"
+}, {
+    "id": 14,
+    "hot_key": "D",
+    "en_value": "Denmark",
+    "name": "丹麦",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/14.jpg"
+}, {
+    "id": 15,
+    "hot_key": "E",
+    "en_value": "Egypt",
+    "name": "埃及",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/15.jpg"
+}, {
+    "id": 16,
+    "hot_key": "F",
+    "en_value": "Finland",
+    "name": "芬兰",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/16.jpg"
+}, {
+    "id": 17,
+    "hot_key": "F",
+    "en_value": "France",
+    "name": "法国",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/17.jpg"
+}, {
+    "id": 18,
+    "hot_key": "G",
+    "en_value": "Germany",
+    "name": "德国",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/18.jpg"
+}, {
+    "id": 19,
+    "hot_key": "H",
+    "en_value": "Hongkong",
+    "name": "中国香港",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/19.jpg"
+}, {
+    "id": 20,
+    "hot_key": "I",
+    "en_value": "India",
+    "name": "印度",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/20.jpg"
+}, {
+    "id": 21,
+    "hot_key": "I",
+    "en_value": "Iran",
+    "name": "伊朗",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/21.jpg"
+}, {
+    "id": 22,
+    "hot_key": "I",
+    "en_value": "Iraq",
+    "name": "伊拉克",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/22.jpg"
+}, {
+    "id": 23,
+    "hot_key": "I",
+    "en_value": "Ireland",
+    "name": "爱尔兰",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/23.jpg"
+}, {
+    "id": 24,
+    "hot_key": "I",
+    "en_value": "Israel",
+    "name": "以色列",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/24.jpg"
+}, {
+    "id": 25,
+    "hot_key": "I",
+    "en_value": "Italy",
+    "name": "意大利",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/25.jpg"
+}, {
+    "id": 26,
+    "hot_key": "J",
+    "en_value": "Japan",
+    "name": "日本",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/26.jpg"
+}, {
+    "id": 27,
+    "hot_key": "K",
+    "en_value": "Kampuchea (Cambodia)",
+    "name": "柬埔寨",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/27.jpg"
+}, {
+    "id": 28,
+    "hot_key": "K",
+    "en_value": "Korea",
+    "name": "韩国",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/28.jpg"
+}, {
+    "id": 29,
+    "hot_key": "K",
+    "en_value": "Kuwait",
+    "name": "科威特",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/29.jpg"
+}, {
+    "id": 30,
+    "hot_key": "K",
+    "en_value": "Kyrgyzstan",
+    "name": "吉尔吉斯斯坦",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/30.jpg"
+}, {
+    "id": 31,
+    "hot_key": "L",
+    "en_value": "Laos",
+    "name": "老挝",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/31.jpg"
+}, {
+    "id": 32,
+    "hot_key": "M",
+    "en_value": "Macao",
+    "name": "中国澳门",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/32.jpg"
+}, {
+    "id": 33,
+    "hot_key": "M",
+    "en_value": "Malaysia",
+    "name": "马来西亚",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/33.jpg"
+}, {
+    "id": 34,
+    "hot_key": "M",
+    "en_value": "Mexico",
+    "name": "墨西哥",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/34.jpg"
+}, {
+    "id": 35,
+    "hot_key": "M",
+    "en_value": "Mongolia",
+    "name": "蒙古",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/35.jpg"
+}, {
+    "id": 36,
+    "hot_key": "N",
+    "en_value": "Netherlands",
+    "name": "荷兰",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/36.jpg"
+}, {
+    "id": 37,
+    "hot_key": "N",
+    "en_value": "New Zealand",
+    "name": "新西兰",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/37.jpg"
+}, {
+    "id": 38,
+    "hot_key": "N",
+    "en_value": "Norway",
+    "name": "挪威",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/38.jpg"
+}, {
+    "id": 39,
+    "hot_key": "P",
+    "en_value": "Pakistan",
+    "name": "巴基斯坦",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/39.jpg"
+}, {
+    "id": 40,
+    "hot_key": "P",
+    "en_value": "Peru",
+    "name": "秘鲁",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/40.jpg"
+}, {
+    "id": 41,
+    "hot_key": "P",
+    "en_value": "Philippines",
+    "name": "菲律宾",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/41.jpg"
+}, {
+    "id": 42,
+    "hot_key": "P",
+    "en_value": "Poland",
+    "name": "波兰",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/42.jpg"
+}, {
+    "id": 43,
+    "hot_key": "P",
+    "en_value": "Portugal",
+    "name": "葡萄牙",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/43.jpg"
+}, {
+    "id": 44,
+    "hot_key": "R",
+    "en_value": "Russia",
+    "name": "俄罗斯",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/44.jpg"
+}, {
+    "id": 45,
+    "hot_key": "S",
+    "en_value": "Saudi Arabia",
+    "name": "沙特阿拉伯",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/45.jpg"
+}, {
+    "id": 46,
+    "hot_key": "S",
+    "en_value": "Singapore",
+    "name": "新加坡",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/46.jpg"
+}, {
+    "id": 47,
+    "hot_key": "S",
+    "en_value": "South Africa",
+    "name": "南非",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/47.jpg"
+}, {
+    "id": 48,
+    "hot_key": "S",
+    "en_value": "Spain",
+    "name": "西班牙",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/48.jpg"
+}, {
+    "id": 49,
+    "hot_key": "S",
+    "en_value": "Sweden",
+    "name": "瑞典",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/49.jpg"
+}, {
+    "id": 50,
+    "hot_key": "S",
+    "en_value": "Switzerland",
+    "name": "瑞士",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/50.jpg"
+}, {
+    "id": 51,
+    "hot_key": "S",
+    "en_value": "Syria",
+    "name": "叙利亚",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/51.jpg"
+}, {
+    "id": 52,
+    "hot_key": "T",
+    "en_value": "Taiwan",
+    "name": "中国台湾",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/52.jpg"
+}, {
+    "id": 53,
+    "hot_key": "T",
+    "en_value": "Thailand",
+    "name": "泰国",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/53.jpg"
+}, {
+    "id": 54,
+    "hot_key": "T",
+    "en_value": "Turkey",
+    "name": "土耳其",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/54.jpg"
+}, {
+    "id": 55,
+    "hot_key": "U",
+    "en_value": "Ukraine",
+    "name": "乌克兰",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/55.jpg"
+}, {
+    "id": 56,
+    "hot_key": "U",
+    "en_value": "United Arab Emirates",
+    "name": "阿联酋",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/56.jpg"
+}, {
+    "id": 57,
+    "hot_key": "t",
+    "en_value": "the United Kingdom",
+    "name": "英国",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/57.jpg"
+}, {
+    "id": 58,
+    "hot_key": "U",
+    "en_value": "United States of America",
+    "name": "美国",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/58.jpg"
+}, {
+    "id": 59,
+    "hot_key": "V",
+    "en_value": "Vietnam",
+    "name": "越南",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/59.jpg"
+}, {
+    "id": 60,
+    "hot_key": "E",
+    "en_value": "EU",
+    "name": "欧盟",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/60.jpg"
+}, {
+    "id": 61,
+    "hot_key": "w",
+    "en_value": "wildcard",
+    "name": "外卡",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/61.jpg"
+}, {
+    "id": 62,
+    "hot_key": "K",
+    "en_value": "Kazakhstan",
+    "name": "哈萨克斯坦",
+    "img": "http://djq-img.oss-cn-hangzhou.aliyuncs.com/country/62.jpg"
+}]

+ 138 - 128
vue/src/main.js

@@ -1,119 +1,127 @@
 // The Vue build version to load with the `import` command
 // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
-import Vue from 'vue'
-import App from './App'
-import router from './router'
-import store from './store'
-import axios from 'axios'
-import Modal from './components/Modal'
-import Alert from './components/Alert'
-import ImagePreview from './components/ImagePreview'
-import ButtonX from './components/ButtonX'
-import InputX from './components/InputX'
-import VueAMap from 'vue-amap'
-import iNoBounce from 'inobounce/inobounce.min'
-import 'normalize.css/normalize.css'
-import './main.less'
+import Vue from "vue";
+import App from "./App";
+import router from "./router";
+import store from "./store";
+import axios from "axios";
+import Modal from "./components/Modal";
+import Alert from "./components/Alert";
+import ImagePreview from "./components/ImagePreview";
+import ButtonX from "./components/ButtonX";
+import InputX from "./components/InputX";
+import VueAMap from "vue-amap";
+import iNoBounce from "inobounce/inobounce.min";
+import "normalize.css/normalize.css";
+import "./main.less";
 
-const FastClick = require('fastclick')
-FastClick.attach(document.body)
-Vue.config.productionTip = false
-Vue.prototype.$modal = Modal
-Vue.prototype.$alert = Alert
-Vue.prototype.$preview = ImagePreview
-Vue.component('ButtonX', ButtonX)
-Vue.component('InputX', InputX)
+const FastClick = require("fastclick");
+FastClick.attach(document.body);
+Vue.config.productionTip = false;
+Vue.prototype.$modal = Modal;
+Vue.prototype.$alert = Alert;
+Vue.prototype.$preview = ImagePreview;
+Vue.component("ButtonX", ButtonX);
+Vue.component("InputX", InputX);
 Vue.prototype.$toast = msg => {
     if (window.cordova) {
-        window.plugins.toast.showWithOptions(
-            {
-                message: msg,
-                duration: "short",
-                position: "bottom",
-                addPixelsY: -120
-            }
-        )
+        window.plugins.toast.showWithOptions({
+            message: msg,
+            duration: "short",
+            position: "bottom",
+            addPixelsY: -120
+        });
     } else {
-        Modal.info(msg)
+        Modal.info(msg);
     }
-}
-
+};
 
 Vue.use(VueAMap);
 VueAMap.initAMapApiLoader({
-    key: 'bf91055058a47a7dc387e40ab6256a5f',
-    plugin: ['Autocomplete', 'PlaceSearch', 'Scale', 'MapType', 'PolyEditor', 'AMap.CircleEditor', 'AMap.Geolocation']
-})
+    key: "bf91055058a47a7dc387e40ab6256a5f",
+    plugin: [
+        "Autocomplete",
+        "PlaceSearch",
+        "Scale",
+        "MapType",
+        "PolyEditor",
+        "AMap.CircleEditor",
+        "AMap.Geolocation"
+    ]
+});
 
-const baseUrl = process.env.NODE_ENV === 'production' ? '../' : `http://${location.hostname}:8080`;
-Vue.prototype.$baseUrl = baseUrl
-axios.defaults.baseURL = baseUrl
-axios.defaults.withCredentials = true
+const baseUrl = `http://192.168.50.132:8080`;
+Vue.prototype.$baseUrl = baseUrl;
+axios.defaults.baseURL = baseUrl;
+axios.defaults.withCredentials = true;
 Vue.prototype.$http = {
     get(params) {
         return new Promise((resolve, reject) => {
             if (params instanceof String) {
                 params = { url: params };
             } else if (!params instanceof Object) {
-                reject('params error');
+                reject("params error");
                 return;
             }
             if (!params.url) {
-                reject('url error');
+                reject("url error");
                 return;
             } else if (!params.url instanceof String) {
-                reject('url error');
+                reject("url error");
                 return;
             } else if (params.url.length === 0) {
-                reject('url error');
+                reject("url error");
                 return;
             }
             // if (!/^(http:\/\/)|(https:\/\/)/.test(params.url)) {
             //     params.url = baseUrl + params.url;
             // }
-            store.commit('updateFetchingData', true);
-            axios.get(params.url, {
-                params: params.data
-            }).then(res => {
-                store.commit('updateFetchingData', false);
-                if (res.status === 200) {
-                    resolve(res.data);
-                } else {
-                    reject(res);
-                }
-                // try {
-                //     if (res.data.code === 10001) {
-                //         axios({
-                //             method: 'post',
-                //             url: '/userInfo/logout'
-                //         });
-                //         store.commit('updateUserInfo', null);
-                //         router.replace('/login');
-                //     }
-                // } catch (e) {
+            store.commit("updateFetchingData", true);
+            axios
+                .get(params.url, {
+                    params: params.data
+                })
+                .then(res => {
+                    store.commit("updateFetchingData", false);
+                    if (res.status === 200) {
+                        resolve(res.data);
+                    } else {
+                        reject(res);
+                    }
+                    // try {
+                    //     if (res.data.code === 10001) {
+                    //         axios({
+                    //             method: 'post',
+                    //             url: '/userInfo/logout'
+                    //         });
+                    //         store.commit('updateUserInfo', null);
+                    //         router.replace('/login');
+                    //     }
+                    // } catch (e) {
 
-                // }
-            }).catch(e => {
-                store.commit('updateFetchingData', false);
-                reject(e);
-            });
+                    // }
+                })
+                .catch(e => {
+                    store.commit("updateFetchingData", false);
+                    reject(e);
+                });
         });
     },
     post(params) {
         if (params instanceof String) {
             params = { url: params };
         } else if (!params instanceof Object) {
-            reject('params error');
+            reject("params error");
             return;
         }
         if (!params.url) {
-            reject('url error');
+            reject("url error");
             return;
         } else if (!params.url instanceof String) {
-            reject('url error');
+            reject("url error");
             return;
         } else if (params.url.length === 0) {
-            reject('url error');
+            reject("url error");
             return;
         }
         // if (!/^(http:\/\/)|(https:\/\/)/.test(params.url)) {
@@ -132,88 +140,90 @@ Vue.prototype.$http = {
             }
         }
         return new Promise((resolve, reject) => {
-            store.commit('updateFetchingData', true);
-            axios.post(params.url, data).then(res => {
-                store.commit('updateFetchingData', false);
-                if (res.status === 200) {
-                    resolve(res.data);
-                } else {
-                    reject(res);
-                }
-            }).catch(e => {
-                store.commit('updateFetchingData', false);
-                reject(e);
-            });
+            store.commit("updateFetchingData", true);
+            axios
+                .post(params.url, data)
+                .then(res => {
+                    store.commit("updateFetchingData", false);
+                    if (res.status === 200) {
+                        resolve(res.data);
+                    } else {
+                        reject(res);
+                    }
+                })
+                .catch(e => {
+                    store.commit("updateFetchingData", false);
+                    reject(e);
+                });
         });
     }
 };
 Vue.mixin({
     created() {
-        this.setupNavbar()
+        this.setupNavbar();
     },
     activated() {
-        this.setupNavbar()
+        this.setupNavbar();
     },
     data() {
-        return {
-        }
+        return {};
     },
     methods: {
-        setupNavbar() { }
+        setupNavbar() {}
     }
-})
+});
 
 const createApp = () => {
     new Vue({
-        el: '#app',
+        el: "#app",
         router,
         store,
         components: { App },
-        template: '<App/>',
+        template: "<App/>",
         mounted() {
-            // StatusBar.overlaysWebView(false)
-            // let count = 0;
-            // let interval = setInterval(() => {
-            //     try {
-            //         StatusBar.overlaysWebView(true)
-            //         if (/Android/.test(navigator.userAgent)) {
-            //             clearInterval(interval)
-            //         }
-            //     } catch (e) { }
-            //     count++
-            // }, 200);
+            axios.get("userInfo/getUserInfo").then(res => {
+                if (res.data.success) {
+                    this.$store.commit("updateUserInfo", res.data.data);
+                }
+            });
         }
-    })
-}
+    });
+};
 
 if (window.cordova) {
-    document.addEventListener('deviceready', () => {
-        createApp()
-    }, false)
-    var _ts = 0
-    document.addEventListener("backbutton", (e) => {
-        if (/^#\/home/.test(window.location.hash)) {
-            e.preventDefault()
-            var ts = (new Date()).getTime()
-            if (ts - _ts < 1500) {
-                navigator.app.exitApp();
-            } else {
-                window.plugins.toast.showWithOptions(
-                    {
-                        message: '再按一次返回键退出',
+    document.addEventListener(
+        "deviceready",
+        () => {
+            createApp();
+        },
+        false
+    );
+    var _ts = 0;
+    document.addEventListener(
+        "backbutton",
+        e => {
+            if (/^#\/home/.test(window.location.hash)) {
+                e.preventDefault();
+                var ts = new Date().getTime();
+                if (ts - _ts < 1500) {
+                    navigator.app.exitApp();
+                } else {
+                    window.plugins.toast.showWithOptions({
+                        message: "再按一次返回键退出",
                         duration: "short",
                         position: "bottom",
                         addPixelsY: -120
-                    }
-                )
+                    });
+                }
+                _ts = ts;
+            } else {
+                router.go(-1);
             }
-            _ts = ts
-        } else {
-            router.go(-1)
-        }
-    }, false)
+        },
+        false
+    );
 } else {
-    createApp()
+    createApp();
 }
 /* eslint-disable no-new */
 

+ 57 - 0
vue/src/pages/Apply.vue

@@ -0,0 +1,57 @@
+<template>
+    <div>
+        <div class="match-name">{{matchInfo.name}}</div>
+        <div class="game-name">{{matchInfo.gameInfo?matchInfo.gameInfo.name:''}}</div>
+    </div>
+</template>
+<script>
+export default {
+    created() {
+        if (this.$route.params.matchInfo && this.$route.params.matchInfo.id) {
+            this.matchInfo = this.$route.params.matchInfo
+        } else {
+            this.$http.get({
+                url: 'matchInfo/getMatchInfo',
+                data: {
+                    id: this.$route.query.id
+                }
+            }).then(res => {
+                if (res.success) {
+                    this.matchInfo = res.data;
+                }
+            })
+        }
+    },
+    data() {
+        return {
+            matchInfo: {}
+        }
+    },
+    methods: {
+        setupNavbar() {
+            this.$navbar.style = 'dark'
+            this.$navbar.title = '报名'
+            this.$navbar.background = '#0E1822'
+            this.$navbar.showBack = true
+            this.$navbar.showRightItem = false
+            this.$navbar.hidden = false
+        },
+    }
+}
+</script>
+<style lang="less">
+@import "../base.less";
+.match-name {
+    color: @text1;
+    text-align: center;
+    margin-top: 20px;
+    font-weight: 700;
+    font-size: 16px;
+}
+.game-name {
+    color: @text2;
+    text-align: center;
+    margin-top: 5px;
+    font-size: 12px;
+}
+</style>

+ 3 - 3
vue/src/pages/ChangePassword.vue

@@ -17,7 +17,8 @@ export default {
     },
     methods: {
         setupNavbar() {
-            this.$navbar.style = 'light'
+            this.$navbar.style = 'dark'
+            this.$navbar.background = '#0E1822'
             this.$navbar.title = '修改密码'
             this.$navbar.showBack = true
             this.$navbar.showRightItem = false
@@ -66,10 +67,9 @@ export default {
 }
 </script>
 <style lang="less" scoped>
-@import '../base.less';
+@import "../base.less";
 .container {
     border-top: 10px solid @pageBg;
-    background: white;
 }
 .input-password {
     margin-top: 24px;

+ 3 - 3
vue/src/pages/Edit.vue

@@ -23,7 +23,8 @@ export default {
     },
     methods: {
         setupNavbar() {
-            this.$navbar.style = 'light'
+            this.$navbar.style = 'dark'
+            this.$navbar.background = '#0E1822'
             this.$navbar.title = '修改昵称'
             this.$navbar.showBack = true
             this.$navbar.showRightItem = false
@@ -60,12 +61,11 @@ export default {
 }
 </script>
 <style lang="less" scoped>
-@import '../base.less';
+@import "../base.less";
 .container {
     flex-grow: 1;
     display: flex;
     flex-direction: column;
-    background: white;
     border-top: 10px solid @pageBg;
 }
 .input-name {

+ 1 - 0
vue/src/pages/Home.vue

@@ -24,6 +24,7 @@
 <script>
 import { mapState } from 'vuex'
 export default {
+    name: 'home',
     created() {
         if (this.$route.name === 'MatchList') {
             this.tab = 0

+ 2 - 2
vue/src/pages/Login.vue

@@ -1,7 +1,7 @@
 <template>
     <div class="page-bg">
         <div class="logo-wrapper">
-            <img src="../assets/logo.png" class="logo">
+            <div class="logo">LOGO</div>
         </div>
         <div class="title">手机快捷注册/登录</div>
         <input-x class="input-phone" :icon="require('../assets/icon_phone.png')" type="tel" placeholder="输入手机号" v-model="phone" clearable></input-x>
@@ -21,7 +21,7 @@ export default {
             this.$navbar.style = 'dark'
             this.$navbar.background = '#0E1822'
             this.$navbar.title = '登录'
-            this.$navbar.showBack = false
+            this.$navbar.showBack = true
             this.$navbar.showRightItem = false
             this.$navbar.hidden = false
         },

+ 108 - 1
vue/src/pages/MatchDetail.vue

@@ -108,6 +108,46 @@
                 </div>
             </div>
         </div>
+        <div class="card" style="margin-top:20px" v-if="matchInfo.state===5">
+            <div class="title">获奖名单</div>
+            <div v-for="(item,i) in participantList" :key="i">
+                <div v-if="matchInfo.type===3">{{item[0].groupName}}</div>
+                <div class="participant-item" v-for="n in 3" :key="n">
+                    <div class="rank" :class="{active:n<4}">{{n}}</div>
+                    <img class="avatar" :src="item[n-1].icon">
+                    <div class="rank-name">{{item[n-1].nickname}}</div>
+                </div>
+            </div>
+        </div>
+        <div class="card">
+            <div class="title">赛事简介</div>
+            {{matchInfo.introduction}}
+        </div>
+        <div class="card">
+            <div class="title">赛事奖励</div>
+            <table class="reward" align="center">
+                <tbody>
+                    <tr>
+                        <td>
+                            <img class="icon" src="../assets/icon_no2.png">
+                            <div class="money">{{matchInfo.reward2}}</div>
+                        </td>
+                        <td>
+                            <img class="icon" src="../assets/icon_no1.png">
+                            <div class="money">{{matchInfo.reward1}}</div>
+                        </td>
+                        <td>
+                            <img class="icon" src="../assets/icon_no3.png">
+                            <div class="money">{{matchInfo.reward3}}</div>
+                        </td>
+                    </tr>
+                </tbody>
+            </table>
+        </div>
+        <div class="card" v-for="(item,i) in extraRule" v-if="item.title&&item.content" :key="i">
+            <div class="title">{{item.title}}</div>
+            {{item.content}}
+        </div>
     </div>
 </template>
 <script>
@@ -116,7 +156,12 @@ import zh from 'date-fns/locale/zh_cn'
 import { mapState } from 'vuex'
 export default {
     props: {
-        matchInfo: {}
+        matchInfo: {},
+        participantList: {
+            dafault() {
+                return []
+            }
+        }
     },
     computed: {
         ...mapState(['userInfo']),
@@ -137,6 +182,12 @@ export default {
                 return this.matchInfo.reward1 + ' 等'
             }
         },
+        extraRule() {
+            if (this.matchInfo && this.matchInfo.extraRule) {
+                return JSON.parse(this.matchInfo.extraRule)
+            }
+            return []
+        },
         createTime() {
             if (this.matchInfo.createTime) {
                 return format(new Date(this.matchInfo.createTime), 'HH:mm', { locale: zh })
@@ -187,6 +238,44 @@ export default {
     margin-right: 14px;
     margin-bottom: 14px;
     padding: 14px 12px 18px 12px;
+    font-size: 13px;
+    font-weight: 400;
+    color: rgba(255, 255, 255, 0.8);
+    line-height: 18px;
+    letter-spacing: 1px;
+    .participant-item {
+        display: flex;
+        align-items: center;
+        height: 36px;
+        border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+        &:last-child {
+            border: none;
+        }
+        .rank {
+            width: 18px;
+            height: 18px;
+            border-radius: 4px;
+            text-align: center;
+            line-height: 18px;
+            font-size: 12px;
+            color: rgba(255, 255, 255, 1);
+            &.active {
+                background: rgba(241, 84, 54, 1);
+            }
+        }
+        .avatar {
+            width: 24px;
+            height: 24px;
+            border-radius: 50%;
+            margin-left: 10px;
+        }
+        .rank-name {
+            margin-left: 10px;
+            font-size: 13px;
+            font-weight: 400;
+            color: rgba(255, 255, 255, 1);
+        }
+    }
     .game-icon {
         width: 46px;
         height: 46px;
@@ -269,5 +358,23 @@ export default {
             }
         }
     }
+    .reward {
+        margin: auto;
+        text-align: center;
+        width: 100%;
+        table-layout: fixed;
+        td {
+        }
+        .icon {
+            width: 24px;
+            height: 30px;
+        }
+        .money {
+            font-size: 18px;
+            font-weight: 700;
+            color: rgba(255, 255, 255, 0.8);
+            line-height: 25px;
+        }
+    }
 }
 </style>

+ 198 - 12
vue/src/pages/MatchInfo.vue

@@ -1,17 +1,64 @@
 <template>
-    <div>
-        <div class="tabs-wrapper">
-            <div class="tabs">
-                <div class="tab" :class="{active:tab===1}" @click="tab=1">赛事详情</div>
-                <div class="tab" :class="{active:tab===2}" @click="tab=2">参赛选手</div>
-                <div class="tab" :class="{active:tab===3}" @click="tab=3">对阵图</div>
+    <div class="container">
+        <div class="content">
+            <div class="tabs-wrapper">
+                <div class="tabs">
+                    <div class="tab" :class="{active:tab===1}" @click="tab=1">赛事详情</div>
+                    <div class="tab" :class="{active:tab===2}" @click="tab=2">参赛选手</div>
+                    <div class="tab" :class="{active:tab===3}" @click="tab=3" v-if="matchInfo.state > 3">对阵图</div>
+                </div>
+            </div>
+            <match-detail v-if="tab===1" :matchInfo="matchInfo" :participantList="participantList"></match-detail>
+            <div v-if="tab===2">
+                <div class="card" style="margin-top:20px">
+                    <div class="title">选手名单</div>
+                    <div v-for="(item,i) in participantList" :key="i">
+                        <div v-if="matchInfo.type===3">{{item[0].groupName}}</div>
+                        <div class="participant-item" v-for="(user,n) in item" :key="n">
+                            <div class="rank" :class="{active:n<3}" v-if="matchInfo.state===5">{{n+1}}</div>
+                            <img class="avatar" :src="user.icon">
+                            <div class="name">{{user.nickname}}</div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div v-if="matchInfo.id&&tab===3" style="margin-top:20px">
+                <single-elimination
+                    v-if="matchInfo.type === 1"
+                    mode="normal"
+                    :admin="userInfo && userInfo.id === matchInfo.userId"
+                    :matchData="matchData"
+                    :participantNum="matchInfo.totalNum"
+                ></single-elimination>
+                <double-elimination
+                    v-if="matchInfo.type === 2"
+                    mode="normal"
+                    :admin="userInfo && userInfo.id === matchInfo.userId"
+                    :matchData="matchData"
+                    :participantNum="matchInfo.totalNum"
+                ></double-elimination>
+                <round-robin
+                    v-if="matchInfo.type === 3"
+                    mode="normal"
+                    :admin="userInfo && userInfo.id === matchInfo.userId"
+                    :matchData="matchData"
+                    :participantNum="matchInfo.totalNum"
+                    :loop="matchInfo.stageInfo[0].loopNum"
+                    :config="matchInfo.stageInfo[0]"
+                ></round-robin>
             </div>
         </div>
-        <match-detail v-if="tab===1" :matchInfo="matchInfo"></match-detail>
+        <div class="btn-wrapper" v-if="matchInfo.state===2&&!applied">
+            <button class="btn-apply" @click="apply">立即报名</button>
+        </div>
     </div>
 </template>
 <script>
+import { mapState } from 'vuex'
 import MatchDetail from './MatchDetail'
+import SingleElimination from '../components/SingleElimination'
+import DoubleElimination from '../components/DoubleElimination'
+import RoundRobin from '../components/RoundRobin'
 export default {
     created() {
         this.$http.get({
@@ -21,16 +68,61 @@ export default {
             }
         }).then(res => {
             if (res.success) {
-                this.matchInfo = res.data
+                this.matchInfo = res.data;
+                this.$http.get({
+                    url: 'matchInfo/participantList',
+                    data: {
+                        matchId: this.$route.query.id
+                    }
+                }).then(res => {
+                    if (res.success) {
+                        if (this.matchInfo.type !== 3) {
+                            this.participantList = [res.data];
+                        } else {
+                            let arr = [];
+                            res.data.forEach(i => {
+                                if (!arr[i.round]) {
+                                    arr[i.round] = [];
+                                }
+                                arr[i.round].push(i);
+                            });
+                            this.participantList = arr;
+                            console.log(arr)
+                        }
+                    }
+                });
+            }
+        });
+        this.$http.get({
+            url: '/against/getAgainstData',
+            data: {
+                matchId: this.$route.query.id
             }
-        })
+        }).then(res => {
+            if (res.success) {
+                // this.matchInfo = res.data.matchInfo;
+                this.matchData = res.data.matchData;
+            }
+        });
     },
+    // mounted() {
+    //     this.$el.parentElement.append(this.$refs.btn)
+    // },
+    // beforeDestroy() {
+    //     this.$el.parentElement.removeChild(this.$refs.btn)
+    // },
     data() {
         return {
             tab: 1,
-            matchInfo: {}
+            matchInfo: {},
+            participantList: [],
+            matchData: [],
+            applied: false
         }
     },
+    computed: {
+        ...mapState(['userInfo']),
+    },
     methods: {
         setupNavbar() {
             this.$navbar.style = 'dark'
@@ -40,22 +132,61 @@ export default {
             this.$navbar.showRightItem = false
             this.$navbar.hidden = false
         },
+        apply() {
+            this.$router.push({
+                name: 'Apply',
+                query: {
+                    id: this.$route.query.id,
+                    name: this.matchInfo.name
+                },
+                params: {
+                    matchInfo: this.matchInfo
+                }
+            })
+        }
     },
     components: {
-        MatchDetail
+        MatchDetail,
+        SingleElimination,
+        DoubleElimination,
+        RoundRobin
     }
 }
 </script>
 <style lang="less" scoped>
+.container {
+    display: flex;
+    flex-direction: column;
+    .content {
+        flex-basis: 0;
+        flex-grow: 1;
+        overflow: auto;
+        -webkit-overflow-scrolling: touch;
+    }
+    .btn-wrapper {
+        padding: 10px 14px;
+        background: #0e1822;
+        .btn-apply {
+            height: 44px;
+            background: rgba(241, 84, 54, 1);
+            border-radius: 6px;
+            font-size: 16px;
+            font-weight: 700;
+            color: rgba(255, 255, 255, 1);
+            line-height: 22px;
+            width: 100%;
+        }
+    }
+}
 .tabs-wrapper {
     text-align: center;
     .tabs {
-        width: 288px;
         height: 36px;
         background: rgba(0, 0, 0, 0.5);
         border-radius: 6px;
         display: flex;
         margin: auto;
+        display: inline-block;
         .tab {
             width: 96px;
             height: 36px;
@@ -64,6 +195,7 @@ export default {
             font-size: 14px;
             font-weight: 700;
             color: rgba(255, 255, 255, 0.68);
+            display: inline-block;
             &.active {
                 background: rgba(241, 84, 54, 1);
                 border-radius: 6px;
@@ -72,5 +204,59 @@ export default {
         }
     }
 }
+.card {
+    position: relative;
+    background: rgba(25, 40, 54, 1);
+    border-radius: 6px;
+    margin-left: 14px;
+    margin-right: 14px;
+    margin-bottom: 14px;
+    padding: 14px 12px 18px 12px;
+    font-size: 13px;
+    font-weight: 400;
+    color: rgba(255, 255, 255, 0.8);
+    line-height: 18px;
+    letter-spacing: 1px;
+    .title {
+        font-size: 13px;
+        font-weight: 700;
+        color: rgba(241, 84, 54, 1);
+        text-align: center;
+        margin-block-end: 20px;
+    }
+    .participant-item {
+        display: flex;
+        align-items: center;
+        height: 36px;
+        border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+        &:last-child {
+            border: none;
+        }
+        .rank {
+            width: 18px;
+            height: 18px;
+            border-radius: 4px;
+            text-align: center;
+            line-height: 18px;
+            font-size: 12px;
+            color: rgba(255, 255, 255, 1);
+            &.active {
+                background: rgba(241, 84, 54, 1);
+            }
+        }
+        .avatar {
+            width: 24px;
+            height: 24px;
+            border-radius: 50%;
+            margin-left: 10px;
+        }
+        .name {
+            margin-left: 10px;
+            font-size: 13px;
+            font-weight: 400;
+            color: rgba(255, 255, 255, 1);
+        }
+    }
+}
 </style>
 

+ 57 - 4
vue/src/pages/Mine.vue

@@ -1,8 +1,9 @@
 <template>
     <div>
         <div class="user-card">
-            <img class="avatar" :src="userInfo?userInfo.icon:require('../assets/avatar.png')">
-            <button class="btn-login">点击登录</button>
+            <img class="avatar" :src="userInfo?userInfo.icon:require('../assets/avatar.png')" @click="profile">
+            <div class="nickname" v-if="userInfo">{{userInfo.nickname}}</div>
+            <button class="btn-login" v-else @click="$router.push('/login')">点击登录</button>
             <div class="info">
                 <div class="item">
                     <div class="value">0场</div>
@@ -20,19 +21,28 @@
         </div>
         <div class="title">当前赛事</div>
         <div class="match-list">
-            <div class="empty">
+            <div class="empty" v-if="matchList.length===0">
                 <img src="../assets/icon_empty.png">
                 <span>当前没有进行中的赛事</span>
             </div>
+            <match-item v-for="item in matchList" :item="item" :key="item.id"></match-item>
         </div>
     </div>
 </template>
 
 <script>
 import { mapState } from 'vuex'
+import MatchItem from '../components/MatchItem'
 export default {
+    created() {
+        if (this.userInfo) {
+            this.getMatchList()
+        }
+    },
     data() {
-        return {}
+        return {
+            matchList: []
+        }
     },
     computed: {
         ...mapState(['userInfo'])
@@ -43,11 +53,44 @@ export default {
             this.$navbar.background = '#0E1822'
             this.$navbar.hidden = true
         },
+        getMatchList() {
+            this.$http.get({
+                url: '/matchInfo/manageMatch',
+                data: {
+                    userId: this.userInfo.id
+                }
+            }).then(res => {
+                if (res.success) {
+                    // this.manage = res.data.manage;
+                    this.matchList = res.data.join;
+                }
+            })
+        },
+        profile() {
+            if (this.userInfo) {
+                this.$router.push('/profile')
+            } else {
+                this.$router.push('/login')
+            }
+        }
+    },
+    watch: {
+        userInfo(val) {
+            if (val) {
+                this.getMatchList()
+            } else {
+                this.matchList = []
+            }
+        }
+    },
+    components: {
+        MatchItem
     }
 }
 </script>
 
 <style lang="less" scoped>
+@import "../base.less";
 .user-card {
     height: 154px;
     background: rgba(255, 255, 255, 1);
@@ -80,6 +123,16 @@ export default {
         color: rgba(255, 255, 255, 1);
         margin-top: 42px;
     }
+    .nickname {
+        width: 100%;
+        height: 32px;
+        line-height: 32px;
+        text-align: center;
+        font-size: 18px;
+        font-weight: 700;
+        color: #000;
+        margin-top: 42px;
+    }
     .info {
         display: flex;
         align-items: center;

+ 19 - 10
vue/src/pages/Profile.vue

@@ -1,6 +1,6 @@
 <template>
     <div class="container">
-        <div class="group">
+        <div class="group" style="margin-top:40px;">
             <div class="cell" @click="chooseAvatar">
                 <span class="label">头像</span>
                 <img v-if="userInfo.icon" :src="userInfo.icon" class="avatar">
@@ -18,15 +18,22 @@
                 <span class="empty" v-else>未设置</span>
                 <img src="../assets/icon_into.png" class="icon-into">
             </div>
-            <div class="cell">
+            <!-- <div class="cell">
                 <span class="label">生日</span>
                 <span class="value" v-if="userInfo.birthday">{{birthday}}</span>
                 <span class="empty" v-else>未设置</span>
                 <img src="../assets/icon_into.png" class="icon-into">
-                <input class="input-birthday" type="date" @blur="birthdayBlur" @change="birthdayChange" :style="{width:inputWidth,minWidth:inputWidth}" :value="birthday">
-            </div>
+                <input
+                    class="input-birthday"
+                    type="date"
+                    @blur="birthdayBlur"
+                    @change="birthdayChange"
+                    :style="{width:inputWidth,minWidth:inputWidth}"
+                    :value="birthday"
+                >
+            </div> -->
         </div>
-        <div class="group">
+        <!-- <div class="group" style="margin-top:20px">
             <div class="cell" @click="$router.push({name:'MyPhone'})">
                 <span class="label">手机</span>
                 <span class="value">{{userInfo ? userInfo.phone : ''}}</span>
@@ -36,7 +43,7 @@
                 <span class="label">修改密码</span>
                 <img src="../assets/icon_into.png" class="icon-into">
             </div>
-        </div>
+        </div> -->
         <button-x class="btn-logout" @click="logout">退出登录</button-x>
         <transition name="fade">
             <div class="mask" v-if="showBottomSheet" @click="showBottomSheet = false"></div>
@@ -89,7 +96,8 @@ export default {
             })
         },
         setupNavbar() {
-            this.$navbar.style = 'light'
+            this.$navbar.style = 'dark'
+            this.$navbar.background = '#0E1822'
             this.$navbar.title = '修改资料'
             this.$navbar.showBack = true
             this.$navbar.showRightItem = false
@@ -219,16 +227,17 @@ export default {
 }
 </script>
 <style lang="less" scoped>
-@import '../styles/base/setOnepx.less';
-@import '../base.less';
+@import "../styles/base/setOnepx.less";
+@import "../base.less";
 .container {
     display: flex;
     flex-direction: column;
-    background: #f2f4f5;
 }
 .group {
     background: #fff;
     margin: 15px 0 0 0;
+    border-radius: 4px;
+    margin: 0 14px;
     .cell {
         height: 60px;
         display: flex;

+ 10 - 17
vue/src/pages/VerifyCode.vue

@@ -2,7 +2,7 @@
     <div class="page-bg">
         <div class="title">验证码已发送至</div>
         <div class="phone-wrapper">
-            <span class="phone">{{`${phone.slice(0, 3)} ${phone.slice(3, 7)} ${phone.slice(7, 11)}`}} </span>
+            <span class="phone">{{`${phone.slice(0, 3)} ${phone.slice(3, 7)} ${phone.slice(7, 11)}`}}</span>
             <span class="btn-resend" :class="{pending: time > 0}" @click="snedCode">{{btnText}}</span>
         </div>
         <input-x class="input-code" :icon="require('../assets/icon_code.png')" type="tel" placeholder="输入短信验证码" v-model="code" clearable></input-x>
@@ -87,27 +87,20 @@ export default {
             }
             this.$modal.loading({ msg: '正在登录', duration: 0 })
             let registered = false
-            this.$http.get({
-                url: '/userInfo/phoneRegistered',
+            this.$http.post({
+                url: '/auth/loginSms',
                 data: {
-                    phone: this.$route.query.phone
+                    phone: this.phone,
+                    code: this.code,
+                    sessionId: this.sid,
+                    remember: true
                 }
-            }).then(res => {
-                registered = res.success
-                return this.$http.post({
-                    url: '/userInfo/loginSms',
-                    data: {
-                        phone: this.phone,
-                        code: this.code,
-                        sessionId: this.sid
-                    }
-                })
             }).then(res => {
                 this.$modal.close()
                 if (res.success) {
                     this.$store.commit('updateUserInfo', res.data)
                     this.$router.push({
-                        name: registered ? 'Map' : 'SetPassword'
+                        name: res.data.hasPassword ? 'MatchList' : 'SetPassword'
                     })
                 } else {
                     this.$toast(res.error)
@@ -120,7 +113,7 @@ export default {
 }
 </script>
 <style lang="less" scoped>
-@import '../base.less';
+@import "../base.less";
 .title {
     font-size: 28px;
     font-weight: 700;
@@ -169,7 +162,7 @@ export default {
         border-radius: 8px;
         margin-right: 10px;
         &:after {
-            content: ' ';
+            content: " ";
             width: 10px;
             height: 10px;
             margin: auto;

+ 11 - 1
vue/src/router/index.js

@@ -17,6 +17,7 @@ import UserLogin from "../pages/UserLogin";
 import ForgetPassword from "../pages/ForgetPassword";
 import MyPhone from "../pages/MyPhone";
 import ChangePassword from "../pages/ChangePassword";
+import Apply from "../pages/Apply";
 Vue.use(Router);
 
 const router = new Router({
@@ -104,6 +105,11 @@ const router = new Router({
             path: "/changePassword",
             name: "ChangePassword",
             component: ChangePassword
+        },
+        {
+            path: "/apply",
+            name: "Apply",
+            component: Apply
         }
     ]
 });
@@ -126,7 +132,11 @@ router.beforeEach((to, from, next) => {
         const toIndex = toTab > -1 ? "0" : history.getItem(to.path);
         const fromIndex = fromTab > -1 ? "0" : history.getItem(from.path);
         if (toIndex) {
-            if (toIndex > fromIndex || !fromIndex || (toIndex === "0" && fromIndex === "0")) {
+            if (
+                toIndex > fromIndex ||
+                !fromIndex ||
+                (toIndex === "0" && fromIndex === "0")
+            ) {
                 store.commit("updateDirection", "forward");
             } else {
                 store.commit("updateDirection", "reverse");

+ 14 - 10
vue/src/store/index.js

@@ -1,32 +1,36 @@
-import Vue from 'vue'
-import Vuex from 'vuex'
+import Vue from "vue";
+import Vuex from "vuex";
 
 Vue.use(Vuex);
 export default new Vuex.Store({
     state: {
-        direction: 'forward',
+        direction: "forward",
         fetchingData: false,
         showNavigationbar: true,
-        userInfo: null
+        userInfo: null,
+        highlightUser: null
     },
     mutations: {
         updateDirection(state, direction) {
-            state.direction = direction
+            state.direction = direction;
         },
         updateFetchingData(state, fetchingData) {
-            state.fetchingData = fetchingData
+            state.fetchingData = fetchingData;
         },
         updateUserInfo(state, userInfo) {
-            state.userInfo = userInfo
+            state.userInfo = userInfo;
         },
         updateUserProperty(state, info) {
             for (let key in info) {
-                state.userInfo[key] = info[key]
+                state.userInfo[key] = info[key];
             }
         },
         updateShowNavigationbar(state, showNavigationbar) {
-            state.showNavigationbar = showNavigationbar
+            state.showNavigationbar = showNavigationbar;
+        },
+        updateHighlightUser(state, highlightUser) {
+            state.highlightUser = highlightUser;
         }
     },
     actions: {}
-})
+});