Browse Source

更换项目模版,换成正式的

zhusiqing 5 years ago
parent
commit
e3b4878aac
17 changed files with 1934 additions and 55 deletions
  1. 2 0
      .env.production
  2. 11 2
      .eslintrc.js
  3. 674 0
      data/data.json
  4. 3 0
      data/reviewData.json
  5. 9 1
      package.json
  6. 114 0
      server.js
  7. 44 10
      src/App.vue
  8. 135 0
      src/components/Card.vue
  9. 88 0
      src/components/FormInput.vue
  10. 57 0
      src/components/FormRadio.vue
  11. 103 0
      src/components/Game.vue
  12. 91 0
      src/components/Start.vue
  13. 44 0
      src/http.js
  14. 17 0
      src/pm2.config.js
  15. 17 0
      src/store/index.js
  16. 11 0
      vue.config.js
  17. 514 42
      yarn.lock

+ 2 - 0
.env.production

@@ -0,0 +1,2 @@
+NODE_ENV="production"
+VUE_APP_WS_URL="ws://39.104.169.128:5556"

+ 11 - 2
.eslintrc.js

@@ -8,10 +8,19 @@ module.exports = {
     '@vue/standard'
   ],
   rules: {
-    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
-    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
+    // 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+    'no-console': 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+    'space-before-function-paren': ['error', {
+      'anonymous': 'always',
+      'named': 'never',
+      'asyncArrow': 'always'
+  }],
   },
   parserOptions: {
     parser: 'babel-eslint'
+  },
+  globals: {
+    'process': true
   }
 }

+ 674 - 0
data/data.json

@@ -0,0 +1,674 @@
+{
+  "data": [
+    "痤疮",
+    "英亩",
+    "附录",
+    "广告",
+    "飞机",
+    "升",
+    "鳄鱼",
+    "按字母顺序排列",
+    "美国",
+    "踝关节",
+    "冷漠",
+    "鼓掌",
+    "苹果酱",
+    "应用",
+    "考古学家",
+    "贵族",
+    "手臂",
+    "无敌舰队",
+    "睡着",
+    "宇航员",
+    "运动员",
+    "亚特兰蒂斯",
+    "阿姨",
+    "鳄梨",
+    "保姆",
+    "骨干",
+    "袋子",
+    "面包",
+    "秃头",
+    "气球",
+    "香蕉",
+    "栏杆",
+    "棒球",
+    "踢脚板",
+    "篮球",
+    "蝙蝠",
+    "电池",
+    "沙滩",
+    "豆茎",
+    "臭虫",
+    "啤酒",
+    "贝多芬",
+    "带子",
+    "围兜",
+    "单车",
+    "大",
+    "自行车",
+    "广告牌",
+    "鸟",
+    "生日",
+    "咬",
+    "铁匠",
+    "毯子",
+    "漂白剂",
+    "飞艇",
+    "开花",
+    "蓝图",
+    "钝",
+    "模糊",
+    "宝儿",
+    "舟",
+    "鲍勃",
+    "雪橇",
+    "身体",
+    "炸弹",
+    "阀盖",
+    "书",
+    "展位",
+    "领结",
+    "盒子",
+    "男孩",
+    "头脑风暴",
+    "牌子",
+    "勇敢",
+    "新娘",
+    "桥",
+    "西兰花",
+    "破坏",
+    "扫帚",
+    "挫伤",
+    "黑发",
+    "泡沫",
+    "巴迪",
+    "水牛",
+    "灯泡",
+    "兔子",
+    "巴士",
+    "买",
+    "小屋",
+    "餐厅",
+    "蛋糕",
+    "计算器",
+    "营地",
+    "可以 ",
+    "蜡烛",
+    "糖",
+    "海角",
+    "资本主义",
+    "车",
+    "纸板",
+    "制图",
+    "猫",
+    "唱片",
+    "天花板",
+    "细胞",
+    "世纪",
+    "椅子",
+    "记录",
+    "冠军",
+    "充电器",
+    "啦啦队长",
+    "厨师",
+    "象棋",
+    "咀嚼",
+    "鸡",
+    "鸣响",
+    "巧克力",
+    "教堂",
+    "圆",
+    "粘土",
+    "悬崖",
+    "斗篷",
+    "顺时针",
+    "小丑",
+    "线索",
+    "教练",
+    "煤",
+    "过山车",
+    "装配",
+    "冷",
+    "学院",
+    "舒适",
+    "计算机",
+    "圆锥",
+    "括约肌",
+    "闭联",
+    "回话",
+    "烹饪",
+    "公司",
+    "绳索",
+    "灯芯绒",
+    "简易床",
+    "咳嗽",
+    "奶牛",
+    "牛仔",
+    "蜡笔",
+    "奶油",
+    "脆",
+    "批评",
+    "乌鸦",
+    "巡航",
+    "面包屑",
+    "外壳",
+    "袖口",
+    "窗帘",
+    "角质层",
+    "沙皇",
+    "爸爸",
+    "投掷",
+    "黎明",
+    "白天",
+    "深",
+    "缺点",
+    "齿",
+    "牙医",
+    "书桌",
+    "字典",
+    "酒窝",
+    "脏",
+    "拆除",
+    "挖沟",
+    "潜水者",
+    "医生",
+    "狗",
+    "狗窝",
+    "洋娃娃",
+    "多米诺骨",
+    "门",
+    "圆点",
+    "排水",
+    "抓",
+    "梦",
+    "裙子",
+    "喝",
+    "滴",
+    "鼓",
+    "烘干机",
+    "鸭子",
+    "倾倒",
+    "灌篮",
+    "垃圾",
+    "耳",
+    "吃",
+    "木制",
+    "肘部",
+    "电气",
+    "大象",
+    "电梯",
+    "精灵",
+    "榆木",
+    "引擎",
+    "人类工程学",
+    "扶梯",
+    "有了",
+    "进化",
+    "延长",
+    "眉毛",
+    "粉丝",
+    "幻想",
+    "快",
+    "宴请",
+    "栅栏",
+    "封建",
+    "小提琴",
+    "虚构",
+    "手指",
+    "火",
+    "第一",
+    "钓鱼",
+    "修",
+    "兴奋",
+    "旗杆",
+    "法兰绒",
+    "火光",
+    "聚集",
+    "废料",
+    "花",
+    "流感",
+    "激动",
+    "飘动",
+    "雾",
+    "衬托",
+    "足球",
+    "前额",
+    "永远",
+    "两周",
+    "雀斑",
+    "运送",
+    "边缘",
+    "蛙",
+    "蛙",
+    "皱眉",
+    "疾驰",
+    "游戏",
+    "垃圾袋",
+    "花园",
+    "汽油",
+    "宝石",
+    "姜",
+    "姜饼",
+    "女孩",
+    "眼镜",
+    "地精",
+    "金",
+    "再见",
+    "奶奶",
+    "葡萄",
+    "草",
+    "感激",
+    "灰",
+    "绿",
+    "吉他",
+    "口香糖",
+    "橡皮",
+    "头发",
+    "半",
+    "掌控",
+    "手写",
+    "挂",
+    "高兴",
+    "帽子",
+    "孵化",
+    "头痛",
+    "心",
+    "基金",
+    "直升机",
+    "边",
+    "藏",
+    "山",
+    "冰球",
+    "作业",
+    "喇叭声",
+    "跳房子",
+    "马",
+    "软管",
+    "热",
+    "房",
+    "游艇",
+    "拥抱",
+    "加湿器",
+    "饿",
+    "障碍",
+    "伤害",
+    "笼子",
+    "冰",
+    "内爆",
+    "墨水",
+    "调查",
+    "内部",
+    "网络",
+    "引诱",
+    "讽刺",
+    "象牙",
+    "常青藤",
+    "翡翠",
+    "牛仔裤",
+    "果冻",
+    "喷气",
+    "抖动",
+    "慢跑",
+    "杂志",
+    "跳",
+    "钥匙",
+    "杀手",
+    "公斤",
+    "国王",
+    "厨房",
+    "风筝",
+    "膝盖",
+    "下跪",
+    "刀",
+    "骑士",
+    "考拉",
+    "花边",
+    "梯子",
+    "瓢虫",
+    "拖延",
+    "垃圾堆",
+    "一圈",
+    "笑",
+    "洗衣店",
+    "法",
+    "草地",
+    "剪草机",
+    "泄露",
+    "腿",
+    "字母",
+    "等级",
+    "生活方式",
+    "韧带",
+    "光",
+    "光剑",
+    "石灰",
+    "狮子",
+    "蜥蜴",
+    "伐木",
+    "虚度",
+    "棒棒糖",
+    "双人沙发",
+    "忠诚",
+    "午餐",
+    "午餐盒",
+    "歌词",
+    "机器",
+    "男子气概",
+    "信箱",
+    "长毛象",
+    "记号",
+    "吉祥物",
+    "桅杆",
+    "火柴棍",
+    "大副",
+    "床垫",
+    "混乱",
+    "仲夏",
+    "矿",
+    "错误",
+    "现代",
+    "塑造",
+    "妈",
+    "周一",
+    "猴子",
+    "监视器",
+    "怪物",
+    "偷",
+    "月",
+    "擦",
+    "蛾",
+    "摩托车",
+    "山脉",
+    "鼠",
+    "割草机",
+    "泥巴",
+    "音乐",
+    "静音",
+    "自然",
+    "协商",
+    "邻居",
+    "鸟巢",
+    "中子",
+    "侄女",
+    "夜",
+    "噩梦",
+    "鼻",
+    "桨",
+    "天文台",
+    "办公室",
+    "油",
+    "老",
+    "威严",
+    "不透明",
+    "瓶撬",
+    "轨道",
+    "器官",
+    "组织",
+    "外面",
+    "外部",
+    "欢迎",
+    "序幕",
+    "桶",
+    "画",
+    "睡衣",
+    "宫殿",
+    "裤子",
+    "纸",
+    "纸",
+    "公园",
+    "山寨",
+    "聚会",
+    "密码",
+    "糕饼",
+    "卒",
+    "梨子",
+    "笔",
+    "铅笔",
+    "钟摆",
+    "阳物",
+    "便士",
+    "辣椒",
+    "个人",
+    "哲人",
+    "手机",
+    "照片",
+    "钢琴",
+    "野餐",
+    "猪舍",
+    "枕头",
+    "飞行员",
+    "勒索",
+    "乒",
+    "纸风车",
+    "海盗",
+    "格子",
+    "计划",
+    "厚木板",
+    "平",
+    "鸭嘴兽",
+    "操场",
+    "犁田",
+    "水管工",
+    "口袋",
+    "诗",
+    "点",
+    "极点",
+    "浮华",
+    "乓",
+    "水池",
+    "冰棒",
+    "人口",
+    "公文包",
+    "积极",
+    "寄",
+    "公主",
+    "耽搁",
+    "抗议",
+    "心理学家",
+    "出版商",
+    "废物",
+    "木偶",
+    "小狗",
+    "推",
+    "谜题",
+    "检疫",
+    "皇后",
+    "流沙",
+    "安静",
+    "赛",
+    "收音机",
+    "筏",
+    "破布",
+    "彩虹",
+    "雨水",
+    "区域",
+    "射线",
+    "回收",
+    "红",
+    "遗憾",
+    "退还",
+    "报复",
+    "肋骨",
+    "谜语",
+    "边缘",
+    "溜冰场",
+    "滚筒",
+    "房间",
+    "玫瑰",
+    "轮",
+    "迂回",
+    "横木",
+    "小牛",
+    "发情期",
+    "忧伤",
+    "安全",
+    "鲑鱼",
+    "盐",
+    "沙盒",
+    "沙堡",
+    "三明治",
+    "腰带",
+    "卫星",
+    "恐惧",
+    "刀疤",
+    "学校",
+    "卑鄙",
+    "攀登",
+    "磨损",
+    "海贝",
+    "季节",
+    "句子",
+    "亮片",
+    "设置",
+    "轴",
+    "影子 ",
+    "香波",
+    "鲨鱼",
+    "绵羊",
+    "被单",
+    "警官",
+    "海难",
+    "衬衫",
+    "鞋带",
+    "短",
+    "喷头",
+    "收缩",
+    "病",
+    "午睡",
+    "侧影",
+    "歌手",
+    "抿",
+    "滑冰",
+    "溜冰",
+    "划水",
+    "扣篮",
+    "睡",
+    "吊索",
+    "慢",
+    "下降",
+    "史密斯",
+    "打喷嚏",
+    "雪",
+    "紧抱",
+    "歌",
+    "太空",
+    "节约",
+    "演讲者",
+    "蜘蛛",
+    "吐痰",
+    "抹掉",
+    "缠绕",
+    "勺子",
+    "春",
+    "洒水车",
+    "间谍",
+    "广场",
+    "斜视",
+    "楼梯",
+    "长期",
+    "星",
+    "州",
+    "棍子",
+    "隔板",
+    "红灯",
+    "结实",
+    "火炉",
+    "匿身处",
+    "稻草",
+    "蒸汽",
+    "流线型",
+    "条纹",
+    "学生",
+    "日",
+    "日出",
+    "寿司",
+    "沼泽",
+    "群集",
+    "毛衣",
+    "游泳",
+    "摇摆",
+    "转速",
+    "说",
+    "出租车",
+    "老师",
+    "茶壶",
+    "青少年",
+    "电话",
+    "十",
+    "网球",
+    "贼",
+    "思考",
+    "王座",
+    "穿越",
+    "雷",
+    "潮水",
+    "虎",
+    "时间",
+    "着色",
+    "脚尖",
+    "绝顶",
+    "疲倦",
+    "纸巾",
+    "面包",
+    "厕所",
+    "工具",
+    "牙刷",
+    "龙卷风",
+    "锦标赛",
+    "拖拉机",
+    "火车",
+    "销毁",
+    "财宝",
+    "树",
+    "三角",
+    "旅程",
+    "卡车",
+    "浴盆",
+    "大号",
+    "辅导",
+    "电视",
+    "鼻音",
+    "理解",
+    "推特",
+    "类型",
+    "失业",
+    "升级",
+    "背心",
+    "视野",
+    "饶舌",
+    "水",
+    "西瓜",
+    "蜡",
+    "婚礼",
+    "除草",
+    "焊接工",
+    "无论如何",
+    "轮椅",
+    "鞭打",
+    "搅拌",
+    "口哨",
+    "白",
+    "假发",
+    "意愿",
+    "风车",
+    "冬天",
+    "愿望",
+    "狼",
+    "羊毛",
+    "世界",
+    "蠕虫",
+    "手表",
+    "码尺",
+    "磨冰机",
+    "禅",
+    "零",
+    "拉链",
+    "区域",
+    "动物园",
+    "橡皮糖",
+    "PM2.5",
+    "书包",
+    "口红"
+  ]
+}

+ 3 - 0
data/reviewData.json

@@ -0,0 +1,3 @@
+{
+  "data": []
+}

+ 9 - 1
package.json

@@ -23,6 +23,14 @@
     "eslint-plugin-vue": "^5.0.0",
     "node-sass": "^4.12.0",
     "sass-loader": "^8.0.0",
-    "vue-template-compiler": "^2.6.10"
+    "vue-template-compiler": "^2.6.10",
+    "koa": "^2.11.0",
+    "koa-bodyparser": "^4.2.1",
+    "koa-router": "^7.4.0",
+    "koa-static": "^5.0.0",
+    "koa-websocket": "^6.0.0",
+    "lru-cache": "^5.1.1",
+    "pm2": "^4.2.1",
+    "ws": "^7.2.1"
   }
 }

+ 114 - 0
server.js

@@ -0,0 +1,114 @@
+const fs = require('fs')
+const path = require('path')
+const Koa = require('koa')
+const Router = require('koa-router')
+const bodyParser = require('koa-bodyparser')
+const LRU = require('lru-cache')
+const WebSocket = require('ws')
+const koaStatic = require('koa-static')
+
+const routerOptions = {
+  prefix: '/api'
+}
+const lruOptions = {
+  max: 50,
+  maxAge: 1000 * 60 * 60 * 24
+}
+const dataPath = './data/data.json'
+const staticPath = './dist'
+
+const router = new Router(routerOptions)
+const cache = new LRU(lruOptions)
+
+let STATIC_DATA = ''
+fs.readFile(dataPath, (err, buf) => {
+  if (err) {
+    console.error(err)
+    return
+  }
+  STATIC_DATA = buf.toString()
+})
+
+const app = new Koa()
+const wss = new WebSocket.Server({ port: 5556 })
+
+app.use(koaStatic(path.join(__dirname, staticPath)))
+
+wss.on('connection', ws => {
+  ws.on('message', msg => {
+    const res = JSON.parse(msg)
+    console.log('msg: ', res)
+    if (res.code && res.status === 'update') {
+      const list = cache.get(res.code)
+      const data = JSON.stringify({ data: list })
+      wss.clients.forEach(client => {
+        if (client.readyState === WebSocket.OPEN) {
+          client.send(data)
+        }
+      })
+    }
+  })
+})
+
+app.use(bodyParser())
+
+// 判断是不是api
+// app.use((ctx, next) => {
+//   const url = ctx.url
+//   const isApi = /^\/api/.test(url)
+//   ctx.$isApi = isApi
+//   next()
+// })
+
+router.get('/list', (ctx, next) => {
+  const { code } = ctx.query
+  let data
+  if (cache.has(code)) {
+    data = cache.get(code)
+  } else {
+    const jsonData = JSON.parse(STATIC_DATA).data || []
+    const len = 25
+    const sum = jsonData.length - len
+    const arr = new Array(25).fill(1).map(el => Math.floor(Math.random() * sum))
+    const groupList = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 0, 0, 0, 0, 0, 0, 0]
+    data = new Array(25).fill(1).map((el, i) => {
+      const del = groupList.length ? Math.floor(Math.random() * groupList.length) : 0
+      const group = groupList.splice(del, 1)[0]
+      return {
+        text: jsonData.splice(arr[i], 1)[0],
+        status: 0,
+        group
+      }
+    })
+    cache.set(code, data)
+  }
+
+  ctx.body = {
+    code,
+    data
+  }
+})
+
+router.put('/status/:code', ctx => {
+  const { params, request } = ctx
+  const { code } = params
+  const { index } = request.body
+  const list = cache.get(code) || []
+  if (!index) {
+    throw new Error('index is not find')
+  }
+  if (!list || !list.length) {
+    throw new Error('cache list is not find')
+  }
+  list[index].status = list[index].status ? 0 : 1
+  cache.set(code, list)
+  ctx.body = { success: true, msg: '更新成功', data: list[index] }
+})
+
+app.use(router.routes()).use(router.allowedMethods())
+
+app.on('error', err => console.error(err))
+
+app.listen(5555)
+
+console.log('http://127.0.0.1:5555')

+ 44 - 10
src/App.vue

@@ -1,28 +1,62 @@
 <template>
   <div id="app">
-    <img alt="Vue logo" src="./assets/logo.png">
-    <HelloWorld msg="Welcome to Your Vue.js App"/>
+    <Game v-if="selectMode === 'game'"></Game>
+    <Start v-else @game="onStartGame"></Start>
   </div>
 </template>
 
 <script>
-import HelloWorld from './components/HelloWorld.vue'
+import Game from '@/components/Game'
+import Start from '@/components/Start'
 
+const orientationchangeHandle = () => {
+  const orientation = window.orientation
+  if (orientation === 90 || orientation === -90) {
+    // alert('|')
+  } else if (orientation === 0 || orientation === 180) {
+    // alert('-')
+  }
+}
 export default {
   name: 'app',
   components: {
-    HelloWorld
+    Game,
+    Start
+  },
+  data() {
+    return {
+      selectMode: 'code'
+    }
+  },
+  mounted() {
+    this.rotateDevice()
+  },
+  methods: {
+    rotateDevice() {
+      window.addEventListener('orientationchange', orientationchangeHandle, false)
+    },
+    onStartGame() {
+      this.selectMode = 'game'
+    }
   }
 }
 </script>
 
 <style lang="scss">
-#app {
-  font-family: 'Avenir', Helvetica, Arial, sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
+* {
+  box-sizing: border-box;
+}
+body, html {
+  margin: 0;
+  padding: 0;
+  /* background-color: #333; */
+  background-color: #fff;
+  height: 100%;
+}
+body {
   text-align: center;
-  color: #2c3e50;
-  margin-top: 60px;
+}
+#app {
+  height: 100%;
 }
 </style>

+ 135 - 0
src/components/Card.vue

@@ -0,0 +1,135 @@
+<template>
+  <div class="card" :class="{ 'reverse': isBack, 'captain': isSelect }" @click="onReverse">
+    <div class="front">{{card.text}}</div>
+    <div class="back" :style="backColor">{{card.text}}</div>
+  </div>
+</template>
+
+<script>
+import http from '@/http'
+
+export default {
+  name: 'card',
+  props: {
+    card: {
+      type: Object,
+      default: () => ({
+        text: '',
+        status: 0,
+        group: 0
+      })
+    },
+    index: {
+      type: Number,
+      default: 0
+    },
+    isCaptain: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      // isBack: false
+    }
+  },
+  computed: {
+    backColor() {
+      const group = this.card.group
+      let color = ''
+      switch (group) {
+        case 0:
+          color = '#888'
+          break
+        case 1:
+          color = '#409EFF'
+          break
+        case 2:
+          color = 'red'
+          break
+        default:
+          color = '#000'
+      }
+      return {
+        backgroundColor: color
+      }
+    },
+    isBack() {
+      if (this.isCaptain) {
+        return 1
+      }
+      const status = this.card.status
+      return status !== 0
+    },
+    isSelect() {
+      if (!this.isCaptain) return false
+      if (this.card.status !== 0) return true
+      return false
+    }
+  },
+  methods: {
+    async onReverse() {
+      const { index, isCaptain } = this
+      const code = this.$store.getters.code
+      if (!code || isCaptain) return
+
+      const { success, data = {} } = await http.put(`/api/status/${code}`, { index })
+      if (success) {
+        this.$emit('statusChange', index, data.status)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.card {
+  position: relative;
+  color: #fff;
+  width: 100%;
+  height: 100%;
+  &.reverse {
+    .front {
+      transform: rotateY(180deg);
+    }
+    .back {
+      transform: rotateY(360deg);
+    }
+  }
+  &.captain {
+    .back {
+      filter: brightness(0.5) blur(1px);
+    }
+  }
+  .front, .back {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    backface-visibility: hidden;
+    transition: 1s;
+    background-position: center;
+    background-repeat: no-repeat;
+    background-size: cover;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 20px;
+  }
+  .front {
+    z-index: 2;
+    /* background-image: url('./scape.jpg'); */
+    color: #333;
+    background-color: #fff;
+    border: 1px solid #ddd;
+    /* box-shadow: 0 0 6px 3px #ddd; */
+  }
+  .back {
+    transform: rotateY(180deg);
+    z-index: 1;
+    border: 1px solid #ddd;
+    /* background-image: url('./scape1.jpg') */
+  }
+}
+</style>

+ 88 - 0
src/components/FormInput.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="form-input" :class="{ focus: isFocus }">
+    <input type="text" :value="value" @input="onChange" @focus="onFocus" @blur="onBlur">
+    <div class="placeholder">{{placeholder}}</div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'formInput',
+  props: {
+    value: {
+      type: [String, Number],
+      default: ''
+    },
+    placeholder: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      isFocus: false
+    }
+  },
+  methods: {
+    onFocus() {
+      if (this.value) return
+      this.isFocus = true
+    },
+    onBlur() {
+      if (this.value) return
+      this.isFocus = false
+    },
+    onChange(e) {
+      this.isFocus = true
+      const value = e.target.value
+      this.$emit('input', value)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+$height: 50px;
+.form-input {
+  position: relative;
+  width: 100%;
+  height: $height * 2;
+  min-width: 150px;
+  font-size: 16px;
+  &.focus {
+    .placeholder {
+      transform: translateY(-100%);
+      z-index: 2;
+      color: #333;
+      font-weight: 600;
+      padding-left: 0;
+    }
+  }
+  > input {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: $height;
+    line-height: $height;
+    z-index: 3;
+    outline: none;
+    border: 2px solid #ddd;
+    background-color: transparent;
+    padding-left: 8px;
+    font-size: 16px;
+  }
+  .placeholder {
+    transition: all 0.5s;
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    height: $height;
+    line-height: $height;
+    transform: translateY(0);
+    color: #999;
+    padding-left: 10px;
+    z-index: 4
+  }
+}
+</style>

+ 57 - 0
src/components/FormRadio.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="form-radio">
+    <label v-for="(item, i) in list" :key="i">
+      <input type="radio" :name="name" :value="item.value" :checked="value === item.value" @input="onChange">
+      <span>{{item.label}}</span>
+    </label>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'formRadio',
+  props: {
+    list: {
+      type: Array,
+      default: () => [1, 2, 3]
+    },
+    value: {
+      type: [String, Number],
+      default: ''
+    }
+  },
+  data() {
+    return {
+      name: `z${Math.floor(Math.random() * 100)}${Date.now()}`
+    }
+  },
+  methods: {
+    onChange(e) {
+      const value = e.target.value
+      this.$emit('input', value)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+$height: 50px;
+.form-radio {
+  height: $height;
+  line-height: $height;
+  width: 100%;
+  font-size: 16px;
+  > label {
+    display: inline-flex;
+    align-items: center;
+    line-height: $height;
+    margin-right: 20px;
+    > input {
+      margin: 0;
+    }
+    > span {
+      margin-left: 5px;
+    }
+  }
+}
+</style>

+ 103 - 0
src/components/Game.vue

@@ -0,0 +1,103 @@
+<template>
+  <div class="game">
+    <div class="prevserve">
+      <div class="item" v-for="(el ,i) in list" :key="i" >
+        <Card
+          :card="el"
+          :index="i"
+          :isCaptain="isCaptain"
+          @statusChange="onStatusChange"></Card>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import http from '@/http'
+import Card from '@/components/Card'
+
+export default {
+  name: 'game',
+  components: {
+    Card
+  },
+  data() {
+    return {
+      list: [],
+      isCaptain: false,
+      code: '',
+      ws: null
+    }
+  },
+  created() {
+    this.init()
+  },
+  beforeDestroy() {
+    this.ws.close()
+  },
+  methods: {
+    async init() {
+      const list = this.$store.getters.list
+      const code = this.$store.getters.code
+      this.code = code
+      const identity = this.$store.getters.identity
+      this.isCaptain = identity === 'captain'
+      if (!list || list.length) {
+        await this.getDataByCode(code)
+      } else {
+        this.list = list
+      }
+      this.createWebsocket()
+    },
+    async getDataByCode(code) {
+      const { data = [] } = await http.get('/api/list', { code })
+      this.list = data
+    },
+    onStatusChange(cardIndex, status) {
+      this.list[cardIndex].status = status
+      this.ws.send(JSON.stringify({ code: this.code, status: 'update' }))
+    },
+    createWebsocket() {
+      const ws = new WebSocket(process.env.VUE_APP_WS_URL)
+      this.ws = ws
+      ws.onopen = () => {
+        console.log('连接成功')
+        ws.send(JSON.stringify({ status: 'connect' }))
+      }
+      ws.onmessage = e => {
+        try {
+          const res = JSON.parse(e.data)
+          const list = res.data || []
+          this.$store.commit('SET_LIST', list)
+          this.list = list
+        } catch (error) {
+          console.error(error)
+        }
+      }
+      ws.onerror = e => console.error(e)
+      ws.onclose = () => {
+        console.log('connection closed')
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.game {
+  background-color: #ddd;
+  .prevserve {
+    transform-style: preserve-3d;
+    perspective: 1000px;
+    perspective-origin: 30% 30%;
+    display: grid;
+    grid-template-columns: repeat(5, 20%);
+    grid-template-rows: repeat(5, 20%);
+  }
+  .item {
+    width: 20vw;
+    height: 20vh;
+  }
+}
+
+</style>

+ 91 - 0
src/components/Start.vue

@@ -0,0 +1,91 @@
+<template>
+  <div class="start">
+    <form>
+      <FormInput class="start-input" v-model="code" placeholder="房间密码"></FormInput>
+      <FormRadio class="start-radio" v-model="identity" :list="list"></FormRadio>
+      <button class="start-button" @click.prevent="onSubmit">进入房间</button>
+    </form>
+  </div>
+</template>
+
+<script>
+import FormInput from '@/components/FormInput'
+import FormRadio from '@/components/FormRadio'
+import http from '@/http'
+export default {
+  name: 'start',
+  components: {
+    FormInput,
+    FormRadio
+  },
+  data() {
+    return {
+      code: '',
+      identity: 'spy',
+      list: [
+        {
+          label: '间谍',
+          value: 'spy'
+        },
+        {
+          label: '间谍头目',
+          value: 'captain'
+        }
+      ]
+    }
+  },
+  methods: {
+    async onSubmit() {
+      const { code: inputCode, identity } = this
+      if (!inputCode) {
+        alert('请输入房间密码')
+        return
+      } else if (!identity) {
+        alert('请选择身份')
+        return
+      }
+      const { code, data = [] } = await http.get('/api/list', { code: inputCode })
+      this.$store.commit('SET_LIST', data)
+      this.$store.commit('SET_CODE', code)
+      this.$store.commit('SET_IDENTITY', identity)
+      this.$emit('game')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.start {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100%;
+  form {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    flex-direction: column;
+    width: 90%;
+    min-width: 300px;
+    max-width: 800px;
+    margin-top: -50px;
+  }
+  .start-input {
+    width: 80%;
+    max-width: 400px;
+  }
+  .start-radio {
+    width: 80%;
+    max-width: 400px;
+  }
+  .start-button {
+    outline: none;
+    border: none;
+    background-color: #409EFF;
+    color: #fff;
+    border-radius: 5px;
+    padding: 5px 15px;
+    font-size: 16px;
+  }
+}
+</style>

+ 44 - 0
src/http.js

@@ -0,0 +1,44 @@
+const setHeaders = new Headers()
+setHeaders.append('Content-Type', 'application/json')
+setHeaders.append('Origin', 'http://codenames.zsqlm.cn')
+const originOptions = {
+  headers: setHeaders,
+  // 为了让浏览器发送包含凭据的请求(即使是跨域源)
+  credentials: 'include',
+  cache: 'no-cache',
+  referrer: 'client'
+}
+const http = (url = '', params = {}, options = {}) => {
+  const sendOptions = Object.assign({}, originOptions, options)
+  if (!sendOptions.method || sendOptions.method === 'GET') {
+    Object.entries(params).forEach(([key, value], i) => {
+      if (i === 0) {
+        url += `?${key}=${value}`
+      } else {
+        url += `&${key}=${value}`
+      }
+    })
+  } else {
+    sendOptions.body = JSON.stringify(params)
+  }
+  return window.fetch(url, sendOptions)
+}
+const get = (url, params, options) => http(url, params, options).then(res => res.json())
+const post = (url, params, options) => {
+  const getOptions = Object.assign({}, options, { method: 'POST' })
+  return http(url, params, getOptions).then(res => res.json())
+}
+const put = (url, params, options) => {
+  const getOptions = Object.assign({}, options, { method: 'PUT' })
+  return http(url, params, getOptions).then(res => res.json())
+}
+const del = (url, params, options) => {
+  const getOptions = Object.assign({}, options, { method: 'DELETE' })
+  return http(url, params, getOptions).then(res => res.json())
+}
+export default {
+  get,
+  post,
+  put,
+  del
+}

+ 17 - 0
src/pm2.config.js

@@ -0,0 +1,17 @@
+const pkg = require('./package.json')
+module.exports = {
+  apps: [{
+    name: pkg.name,
+    script: 'server.js',
+
+    // Options reference: https://pm2.io/doc/en/runtime/reference/ecosystem-file/
+    instances: 1,
+    max_memory_restart: '150M',
+    env: {
+      NODE_ENV: 'production'
+    },
+    watch: ['dist'],
+    ignore_watch: ['node_modules'],
+    watch_delay: 5e3
+  }]
+}

+ 17 - 0
src/store/index.js

@@ -5,8 +5,25 @@ Vue.use(Vuex)
 
 export default new Vuex.Store({
   state: {
+    list: [],
+    code: '',
+    identity: ''
+  },
+  getters: {
+    list: state => state.list,
+    code: state => state.code,
+    identity: state => state.identity
   },
   mutations: {
+    SET_LIST(state, list = []) {
+      state.list = list
+    },
+    SET_CODE(state, code = '') {
+      state.code = code
+    },
+    SET_IDENTITY(state, identity) {
+      state.identity = identity
+    }
   },
   actions: {
   },

+ 11 - 0
vue.config.js

@@ -0,0 +1,11 @@
+module.exports = {
+  devServer: {
+    proxy: {
+      '/api': {
+        'target': 'http://127.0.0.1:5555',
+        'ws': true,
+        'changeOrigin': true
+      }
+    }
+  }
+}

File diff suppressed because it is too large
+ 514 - 42
yarn.lock


Some files were not shown because too many files changed in this diff