瀏覽代碼

Merge branch 'master' of git.gitinn.com:proginn/proginn-frontend

lushuncheng 7 年之前
父節點
當前提交
f4ecda5b7b

二進制
.DS_Store


+ 1 - 1
.gitignore

@@ -96,4 +96,4 @@ build/Release
 # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
 node_modules
 
-yarn.lock
+.DS_Store

+ 108 - 13
README.md

@@ -1,26 +1,121 @@
-# frontend
+# 程序员客栈--前端项目
 
-> proginn ssr project
+> 基于Nuxtjs,Element UI,该项目包括客栈主站和技术圈。自动化部署,dev分支自动更新到dev环境,master分支自动更新到线上环境
 
-## Build Setup
+## Build Setup 建议用yarn保证版本稳定运行
 
 ``` bash
 # install dependencies
 $ yarn install
 
-# serve with hot reload at localhost:3000
+# serve with hot reload at localhost:3000, local.proginn.com:3000
 $ yarn run dev
 
-# build for production and launch server
-$ yarn run build
-$ yarn start
+```
+
+## 目录结构
 
-# generate static project
-$ yarn run generate
+``` bash
+.
+├── LICENSE
+├── README.md
+├── app.html                # app html template
+├── assets                  # static img css
+│   ├── README.md
+│   ├── css
+│   └── img
+├── components              # components
+│   ├── README.md
+│   ├── agreement.vue
+│   ├── footer.vue
+│   ├── group
+│   ├── header.vue
+│   ├── inner_header.vue
+│   ├── input
+│   ├── type
+│   ├── ver_code.vue
+│   ├── ws
+│   └── wx_header.vue
+├── dist                    # useless
+│   ├── 200.html
+│   ├── README.md
+│   ├── _nuxt
+│   ├── cert
+│   ├── favicon.ico
+│   ├── index.html
+│   └── type
+├── layouts                 # layout
+│   ├── README.md
+│   ├── default.vue
+│   └── opacity_header.vue
+├── middleware              # middelware useless now
+│   ├── README.md
+│   └── authenticated.js
+├── mixins                  # mixins
+│   ├── getDeviceType.js    # deviceType
+│   ├── group.js
+│   └── wx.js
+├── nuxt.config.js          # nuxt config
+├── package.json            # package.json
+├── pages                   # pages(static router)
+│   ├── README.md
+│   ├── cert
+│   ├── community
+│   ├── group
+│   ├── index.vue
+│   ├── setting
+│   ├── type
+│   ├── user
+│   └── wo
+├── plugins
+│   ├── README.md
+│   ├── axios.js            # useless
+│   ├── common.js           # common methods
+│   ├── element.js          # element ui inject
+│   ├── http.js             # useless
+│   └── nuxtAxios.js        # nuxt axios config
+├── static
+│   ├── README.md
+│   └── favicon.ico         # favicon
+├── store
+│   ├── README.md
+│   └── index.js            # create store
+└── yarn.lock               # 勿删
 ```
 
-For detailed explanation on how things work, checkout [Nuxt.js docs](https://nuxtjs.org).
-# proginn-frontend
+## 开发注意
+
+* 路由没有明确的区分技术圈和主站,所以修改之前最好确认请求路径以确认修改页面
+* 由于测试环境和线上环境cookie键值相同,所以尽量在隐身模式下测试开发环境避免不必要的麻烦
+* 开发之前需要本地设置host(127.0.0.1   local.proginn.com)以使用cookie
+* API交互格式统一application/x-www-form-urlencoded,否者无法访问成功
+* 调用API尽量只能使用Nuxtjs本身集成的Axios,使用方式
+``` js
+async asyncData({ $axios, params, req }) {
+    let id = params.id;
+    let headers = req && req.headers;
+    let res = await $axios.$get(
+        `/api/user/getUserInfo?id=${id}&page=1&size=10`,
+        { headers }
+    );
+    return {
+        title: `${res.data.info.nickname}的技术圈主页-程序员客栈`
+    };
+},
+methods: {
+    async getDetail() {
+      let res = await this.$axios.$get(
+        `/api/user/getUserInfo?id=${this.$route.params.id}&page=1&size=10`
+      );
+      // console.log(res.data)
+      document.title = `${res.data.info.nickname}的技术圈主页-程序员客栈`;
+      if (res) {
+        this.idInfo = res.data;
+      }
+    },
+}
+```
+* 项目全局使用Vuex作为状态管理保存登录状态,可以调用全局store获取userInfo
+* 使用mixins实现设备判断,设备状态包括pc,ios,android,通过this.deviceType可以获取
+* 路由鉴权未实现,有接口不同页面需要不同处理,即使API返回都是未登录可能也不要跳转到登录页
 
-客栈前端
-trigger   

二進制
assets/.DS_Store


二進制
assets/img/.DS_Store


二進制
assets/img/sign/success.png


二進制
assets/img/sign/success@2x.png


+ 29 - 0
components/breadcrumb.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="breadcrumb">
+    <el-breadcrumb separator-class="el-icon-arrow-right">
+      <el-breadcrumb-item v-for="location of locations" :key="location.url">
+        <a :href="location.url">{{location.title}}</a>
+      </el-breadcrumb-item>
+    </el-breadcrumb>
+  </div>
+</template>
+
+<script>
+export default {
+  props: ["locations"],
+  computed: {},
+  mounted() {},
+  methods: {
+    go(url) {
+      console.log(url);
+      location.href = url;
+    }
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.breadcrumb {
+  padding-bottom: 10px;
+}
+</style>

+ 136 - 0
components/editor.vue

@@ -0,0 +1,136 @@
+<template>
+  <div class="my-editor">
+    <div
+      class="quill-editor"
+      :content="content"
+      @change="handleChange"
+      v-quill:myQuillEditor="editorOption"
+    ></div>
+    <input type="file" id="imgInput" @change="handleContentFileChange" style="display: none;" />
+  </div>
+</template>
+
+<script>
+import Vue from "vue";
+import "quill/dist/quill.core.css";
+import "quill/dist/quill.snow.css";
+// import 'quill/dist/quill.bubble.css'
+if (process.browser) {
+  const VueQuillEditor = require("vue-quill-editor/dist/ssr");
+  Vue.use(VueQuillEditor);
+}
+import hljs from "hljs";
+export default {
+  props: ["content"],
+  components: {
+    // quillEditor
+    // Editor
+  },
+  data() {
+    return {
+      editorOption: {
+        theme: "snow",
+        placeholder: "请输入正文...",
+        modules: {
+          toolbar: [
+            ["bold", "italic", "underline", "strike"],
+            ["blockquote", "code-block"],
+            [{ header: 1 }, { header: 2 }],
+            [{ list: "ordered" }, { list: "bullet" }],
+            [{ script: "sub" }, { script: "super" }],
+            [{ indent: "-1" }, { indent: "+1" }],
+            [{ direction: "rtl" }],
+            [{ size: ["small", false, "large", "huge"] }],
+            [{ header: [1, 2, 3, 4, 5, 6, false] }],
+            [{ font: [] }],
+            [{ color: [] }, { background: [] }],
+            [{ align: [] }],
+            ["clean"],
+            ["link", "image"]
+          ],
+          syntax: {
+            highlight: text => hljs.highlightAuto(text).value
+          }
+        }
+      }
+    };
+  },
+  computed: {},
+  mounted() {
+    // 为图片ICON绑定事件  getModule 为编辑器的内部属性
+    this.myQuillEditor
+      .getModule("toolbar")
+      .addHandler("image", this.imgHandler);
+    // this.myQuillEditor
+    //   .getModule("toolbar")
+    //   .addHandler("video", this.videoHandler); // 为视频ICON绑定事件
+  },
+  methods: {
+    handleChange(e) {
+      this.$emit("change", e.html);
+    },
+    // 点击图片ICON触发事件
+    imgHandler(state) {
+      this.addRange = this.myQuillEditor.getSelection();
+      if (state) {
+        let fileInput = document.getElementById("imgInput");
+        fileInput.click(); // 加一个触发事件
+      }
+      this.uploadType = "image";
+    },
+
+    // 点击视频ICON触发事件
+    videoHandler(state) {
+      this.addRange = this.myQuillEditor.getSelection();
+      if (state) {
+        let fileInput = document.getElementById("imgInput");
+        fileInput.click(); // 加一个触发事件
+      }
+      // this.uploadType = "video";
+    },
+    handleContentFileChange(e) {
+      const file = e.target.files[0];
+      if (file.size / 1024 > 500) {
+        this.$message.error("图片大小不得超过500k,请重新选择");
+        return false;
+      }
+      const formData = new FormData();
+      formData.append("file", file);
+      formData.append("original_filename", file.name);
+      this.$axios
+        .$post(`/upload_image`, formData, {
+          headers: { "Content-Type": "multipart/form-data" }
+        })
+        .then(res => {
+          this.$emit(
+            "change",
+            this.content + `<img src="${res.filename}" alt="${file.name}"/>`
+          );
+        });
+    }
+  }
+};
+</script>
+
+<style lang="scss">
+.my-editor {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  min-height: 244px;
+  background: #fff;
+  .ql-toolbar {
+    border-width: 0 !important;
+  }
+  .ql-editor,
+  .quill-editor {
+    min-height: 244px;
+    border: 0 !important;
+    font-size: 14px;
+    line-height: 25px;
+  }
+  .ql-snow.ql-toolbar::after {
+    display: inline-block;
+  }
+}
+</style>

+ 11 - 0
components/sign/education.vue

@@ -0,0 +1,11 @@
+<template>
+  <div>目前SSR未有页面</div>
+</template>
+
+<script>
+export default {
+}
+</script>
+
+<style>
+</style>

+ 178 - 0
components/sign/experience.vue

@@ -0,0 +1,178 @@
+<template>
+  <div class="info">
+    <header>
+      <h5>工作经历</h5>
+      <div v-if="editing" class="opts">
+        <el-button type="info" @click="editing = false">取消</el-button>
+        <el-button type="primary" @click="onSubmit">确认</el-button>
+      </div>
+      <div v-else class="opts">
+        <el-button type="primary" @click="editing = true">编辑</el-button>
+      </div>
+    </header>
+    <div v-if="editing" class="edit">
+      <el-form ref="form" :rules="rules" :model="form" label-width="147px">
+        <el-form-item label="昵称">
+          <el-input v-model="form.name"></el-input>
+        </el-form-item>
+        <el-form-item label="工作状态">
+          <el-select v-model="form.region" placeholder="请选择">
+            <el-option label="区域一" value="shanghai"></el-option>
+            <el-option label="区域二" value="beijing"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="职业方向">
+          <el-select v-model="form.region" placeholder="请选择">
+            <el-option label="区域一" value="shanghai"></el-option>
+            <el-option label="区域二" value="beijing"></el-option>
+          </el-select>
+          <el-select v-model="form.region" placeholder="请选择">
+            <el-option label="区域一" value="shanghai"></el-option>
+            <el-option label="区域二" value="beijing"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="所在地区">
+          <el-select v-model="form.region" placeholder="请选择">
+            <el-option label="区域一" value="shanghai"></el-option>
+            <el-option label="区域二" value="beijing"></el-option>
+          </el-select>
+          <!-- <el-date-picker
+              type="date"
+              placeholder="选择日期"
+              v-model="form.date1"
+              style="width: 100%;"
+          ></el-date-picker>-->
+          <el-select v-model="form.region" placeholder="请选择">
+            <el-option label="区域一" value="shanghai"></el-option>
+            <el-option label="区域二" value="beijing"></el-option>
+          </el-select>
+          <!-- <el-time-picker placeholder="选择时间" v-model="form.date2" style="width: 100%;"></el-time-picker> -->
+        </el-form-item>
+        <el-form-item label="日薪">
+          <el-input-number
+            :min="200"
+            :max="2000"
+            :controls="false"
+            v-model="form.name"
+            :style="{width: '150px', marginRight: '10px'}"
+          ></el-input-number>元/天(8小时)
+        </el-form-item>
+        <el-form-item label="可工作时间">
+          <div class="times">
+            <el-checkbox v-model="form.workingday" label="工作日"></el-checkbox>
+            <el-time-select
+              v-model="form.workingStart"
+              :picker-options="{
+                  start: '00:00',
+                  step: '00:30',
+                  end: '24:00'
+                }"
+              placeholder="开始时间"
+            ></el-time-select>
+            <span class="to">至</span>
+            <el-time-select
+              v-model="form.workingEnd"
+              :picker-options="{
+                  start: '00:00',
+                  step: '00:30',
+                  end: '24:00'
+                }"
+              placeholder="结束时间"
+            ></el-time-select>
+          </div>
+          <div class="times">
+            <el-checkbox v-model="form.weekend" label="周末"></el-checkbox>
+            <el-time-select
+              v-model="form.weekendStart"
+              :picker-options="{
+                  start: '00:00',
+                  step: '00:30',
+                  end: '24:00'
+                }"
+              placeholder="开始时间"
+            ></el-time-select>
+            <span class="to">至</span>
+            <el-time-select
+              v-model="form.weekendEnd"
+              :picker-options="{
+                  start: '00:00',
+                  step: '00:30',
+                  end: '24:00'
+                }"
+              placeholder="结束时间"
+            ></el-time-select>
+          </div>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div v-else class="show">
+      <el-form ref="form" :rules="rules" :model="form" label-width="147px">
+        <el-form-item label="昵称">{{form.name}}</el-form-item>
+        <el-form-item label="工作状态">{{form.status}}</el-form-item>
+        <el-form-item label="职业方向">{{form.position}}</el-form-item>
+        <el-form-item label="所在地区">{{form.address}}</el-form-item>
+        <el-form-item label="日薪">{{form.dailyRate}}</el-form-item>
+        <el-form-item label="可工作时间">{{form.workingTime}}</el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      rules: {
+        name: ""
+      },
+      form: {
+        name: "1123",
+        status: "123",
+        position: "123",
+        address: "123",
+        region: "123",
+        workingDay: "33",
+        weekend: "333",
+        workingStart: "333",
+        workingEnd: "33",
+        weekendStart: "33",
+        weekendEnd: "333",
+        dailyRate: "312",
+        workingTime: "123"
+      },
+      editing: false
+    };
+  },
+  methods: {
+    onSubmit() {
+      console.log("submit!");
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.info {
+  .edit {
+    > form {
+      margin-top: 44px;
+      .el-select,
+      .el-input {
+        width: 217px;
+        margin-right: 10px;
+      }
+      .times {
+        .el-checkbox {
+          width: 88px;
+        }
+        .to {
+          margin-right: 10px;
+        }
+        .el-input {
+          width: 136px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 178 - 0
components/sign/info.vue

@@ -0,0 +1,178 @@
+<template>
+  <div class="info">
+    <header>
+      <h5>基本信息</h5>
+      <div v-if="editing" class="opts">
+        <el-button type="info" @click="editing = false">取消</el-button>
+        <el-button type="primary" @click="onSubmit">确认</el-button>
+      </div>
+      <div v-else class="opts">
+        <el-button type="primary" @click="editing = true">编辑</el-button>
+      </div>
+    </header>
+    <div v-if="editing" class="edit">
+      <el-form ref="form" :rules="rules" :model="form" label-width="147px">
+        <el-form-item label="昵称">
+          <el-input v-model="form.name"></el-input>
+        </el-form-item>
+        <el-form-item label="工作状态">
+          <el-select v-model="form.region" placeholder="请选择">
+            <el-option label="区域一" value="shanghai"></el-option>
+            <el-option label="区域二" value="beijing"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="职业方向">
+          <el-select v-model="form.region" placeholder="请选择">
+            <el-option label="区域一" value="shanghai"></el-option>
+            <el-option label="区域二" value="beijing"></el-option>
+          </el-select>
+          <el-select v-model="form.region" placeholder="请选择">
+            <el-option label="区域一" value="shanghai"></el-option>
+            <el-option label="区域二" value="beijing"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="所在地区">
+          <el-select v-model="form.region" placeholder="请选择">
+            <el-option label="区域一" value="shanghai"></el-option>
+            <el-option label="区域二" value="beijing"></el-option>
+          </el-select>
+          <!-- <el-date-picker
+              type="date"
+              placeholder="选择日期"
+              v-model="form.date1"
+              style="width: 100%;"
+          ></el-date-picker>-->
+          <el-select v-model="form.region" placeholder="请选择">
+            <el-option label="区域一" value="shanghai"></el-option>
+            <el-option label="区域二" value="beijing"></el-option>
+          </el-select>
+          <!-- <el-time-picker placeholder="选择时间" v-model="form.date2" style="width: 100%;"></el-time-picker> -->
+        </el-form-item>
+        <el-form-item label="日薪">
+          <el-input-number
+            :min="200"
+            :max="2000"
+            :controls="false"
+            v-model="form.name"
+            :style="{width: '150px', marginRight: '10px'}"
+          ></el-input-number>元/天(8小时)
+        </el-form-item>
+        <el-form-item label="可工作时间">
+          <div class="times">
+            <el-checkbox v-model="form.workingday" label="工作日"></el-checkbox>
+            <el-time-select
+              v-model="form.workingStart"
+              :picker-options="{
+                  start: '00:00',
+                  step: '00:30',
+                  end: '24:00'
+                }"
+              placeholder="开始时间"
+            ></el-time-select>
+            <span class="to">至</span>
+            <el-time-select
+              v-model="form.workingEnd"
+              :picker-options="{
+                  start: '00:00',
+                  step: '00:30',
+                  end: '24:00'
+                }"
+              placeholder="结束时间"
+            ></el-time-select>
+          </div>
+          <div class="times">
+            <el-checkbox v-model="form.weekend" label="周末"></el-checkbox>
+            <el-time-select
+              v-model="form.weekendStart"
+              :picker-options="{
+                  start: '00:00',
+                  step: '00:30',
+                  end: '24:00'
+                }"
+              placeholder="开始时间"
+            ></el-time-select>
+            <span class="to">至</span>
+            <el-time-select
+              v-model="form.weekendEnd"
+              :picker-options="{
+                  start: '00:00',
+                  step: '00:30',
+                  end: '24:00'
+                }"
+              placeholder="结束时间"
+            ></el-time-select>
+          </div>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div v-else class="show">
+      <el-form ref="form" :rules="rules" :model="form" label-width="147px">
+        <el-form-item label="昵称">{{form.name}}</el-form-item>
+        <el-form-item label="工作状态">{{form.status}}</el-form-item>
+        <el-form-item label="职业方向">{{form.position}}</el-form-item>
+        <el-form-item label="所在地区">{{form.address}}</el-form-item>
+        <el-form-item label="日薪">{{form.dailyRate}}</el-form-item>
+        <el-form-item label="可工作时间">{{form.workingTime}}</el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      rules: {
+        name: ""
+      },
+      form: {
+        name: "1123",
+        status: "123",
+        position: "123",
+        address: "123",
+        region: "123",
+        workingDay: "33",
+        weekend: "333",
+        workingStart: "333",
+        workingEnd: "33",
+        weekendStart: "33",
+        weekendEnd: "333",
+        dailyRate: "312",
+        workingTime: "123"
+      },
+      editing: false
+    };
+  },
+  methods: {
+    onSubmit() {
+      console.log("submit!");
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.info {
+  .edit {
+    > form {
+      margin-top: 44px;
+      .el-select,
+      .el-input {
+        width: 217px;
+        margin-right: 10px;
+      }
+      .times {
+        .el-checkbox {
+          width: 88px;
+        }
+        .to {
+          margin-right: 10px;
+        }
+        .el-input {
+          width: 136px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 53 - 0
components/sign/intro.vue

@@ -0,0 +1,53 @@
+<template>
+  <div class="intro">
+    <h3>
+      完善个人简历后,才能申请签约开发者进行接单
+      <span class="status success">已通过签约</span>
+    </h3>
+    <p>
+      成为程序员客栈签约开发者后,才能在平台接单,包括平台派单类项目、雇佣项目等,并
+      享受交易担保等服务。
+      <br />申请签约需要满足以下条件:
+      <br />1.三年及以上正规互联网工作经验
+      <br />2.按签约规则如实填写个人简历
+      <br />3.具有契约精神、服务意识,能按约定高效完成开发
+      <br />4.不在程序员客栈黑名单(无开发黑历史)
+    </p>
+  </div>
+</template>
+
+<script>
+export default {};
+</script>
+
+<style lang="scss" scoped>
+.intro {
+  padding: 13px 20px;
+  font-size: 22px;
+  font-family: PingFangSC-Medium;
+  font-weight: 500;
+  color: rgba(34, 34, 34, 1);
+  line-height: 30px;
+  h3 {
+    padding: 12px 0 25px;
+    font-size: 22px;
+    font-family: PingFangSC-Medium;
+    font-weight: 500;
+    color: rgba(34, 34, 34, 1);
+    line-height: 30px;
+    text-align: center;
+    border-bottom: 2px solid rgba(1, 1, 1, 0.06);
+    .success {
+      line-height: 30px;
+    }
+  }
+  p {
+    margin: 24px 0 20px;
+    font-size: 14px;
+    font-family: PingFangSC-Regular;
+    line-height: 30px;
+    font-weight: 400;
+    color: rgba(51, 51, 51, 1);
+  }
+}
+</style>

+ 108 - 0
components/sign/profile.vue

@@ -0,0 +1,108 @@
+<template>
+  <div class="profile">
+    <header>
+      <h5>个人介绍</h5>
+      <div v-if="editing" class="opts">
+        <el-button type="info" @click="editing = false">取消</el-button>
+        <el-button type="primary" @click="onSubmit">确认</el-button>
+      </div>
+      <div v-else class="opts">
+        <el-button type="primary" @click="editing = true">编辑</el-button>
+      </div>
+    </header>
+    <div v-if="editing" class="edit">
+      <editor :content="content" @change="handleChange"></editor>
+    </div>
+    <div v-else class="show">{{content}}</div>
+  </div>
+</template>
+
+<script>
+import editor from "@/components/editor";
+export default {
+  data() {
+    return {
+      editing: false,
+      content: "asdasd",
+      editorOption: {
+        theme: "snow",
+        placeholder: "请输入正文...",
+        modules: {
+          toolbar: [
+            ["bold", "italic", "underline", "strike"],
+            ["blockquote", "code-block"],
+            [{ header: 1 }, { header: 2 }],
+            [{ list: "ordered" }, { list: "bullet" }],
+            [{ script: "sub" }, { script: "super" }],
+            [{ indent: "-1" }, { indent: "+1" }],
+            [{ direction: "rtl" }],
+            [{ size: ["small", false, "large", "huge"] }],
+            [{ header: [1, 2, 3, 4, 5, 6, false] }],
+            [{ font: [] }],
+            [{ color: [] }, { background: [] }],
+            [{ align: [] }],
+            ["clean"],
+            ["link", "image"]
+          ],
+          syntax: {
+            highlight: text => hljs.highlightAuto(text).value
+          }
+        }
+      }
+    };
+  },
+  components: { editor },
+  watch: {
+    // content: function(val) {
+    //   console.log(val);
+    // }
+  },
+  mounted() {},
+  methods: {
+    handleChange(val) {
+      this.content = val;
+    },
+    onSubmit() {
+      console.log(this.content);
+      console.log("submit!");
+    },
+    handleContentFileChange(e) {
+      const file = e.target.files[0];
+      if (file.size / 1024 > 500) {
+        this.$message.error("图片大小不得超过500k,请重新选择");
+        return false;
+      }
+      const formData = new FormData();
+      formData.append("file", file);
+      formData.append("original_filename", file.name);
+      this.$axios
+        .$post(`/upload_image`, formData, {
+          headers: { "Content-Type": "multipart/form-data" }
+        })
+        .then(res => {
+          this.content += `<img src="${res.filename}" alt="${file.name}"/>`;
+        });
+    }
+  }
+};
+</script>
+
+<style lang="scss">
+.profile {
+  .edit {
+    padding: 0 20px;
+    width: 1000px;
+    min-height: 244px;
+    > div {
+      border: 0 !important;
+    }
+    & > .ql-toolbar.ql-snow {
+      border: 0 !important;
+    }
+  }
+
+  .show {
+    padding: 20px;
+  }
+}
+</style>

+ 11 - 0
components/sign/skills.vue

@@ -0,0 +1,11 @@
+<template>
+  <div>目前SSR未有页面</div>
+</template>
+
+<script>
+export default {
+}
+</script>
+
+<style>
+</style>

+ 11 - 0
components/sign/social.vue

@@ -0,0 +1,11 @@
+<template>
+  <div>目前SSR未有页面</div>
+</template>
+
+<script>
+export default {
+}
+</script>
+
+<style>
+</style>

+ 18 - 0
components/sign/verify.vue

@@ -0,0 +1,18 @@
+<template>
+  <div class="verify">
+    <h5>
+      实名认证
+      <span class="status success">已签约</span>
+    </h5>
+  </div>
+</template>
+
+<script>
+export default {};
+</script>
+
+<style lang="scss" scoped>
+.verify {
+  padding: 13px 20px;
+}
+</style>

+ 11 - 0
components/sign/works.vue

@@ -0,0 +1,11 @@
+<template>
+  <div>目前SSR未有页面</div>
+</template>
+
+<script>
+export default {
+}
+</script>
+
+<style>
+</style>

+ 51 - 0
components/topics/edit.vue

@@ -0,0 +1,51 @@
+<template>
+  <div>
+    <breadcrumb :locations="locations"></breadcrumb>
+    <editor-aside></editor-aside>
+    <editor></editor>
+  </div>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+import breadcrumb from '@/components/breadcrumb'
+import editorAside from '@/components/topics/editor-aside'
+import editor from '@/components/topics/editor'
+// import getDeviceType from '@/mixins/getDeviceType'
+import qs from 'qs';
+
+export default {
+  props: ['locations'],
+  head: {
+    title: '发布文章-技术圈',
+  },
+  components: {
+    breadcrumb,
+    editor,
+    editorAside,
+  },
+  // mixins: [getDeviceType],
+  data() {
+    return {
+    }
+  },
+  mounted() {
+    // this.getList()
+  },
+  methods: {
+    async getList() {
+      let extraHeaders = {};
+      if (this.deviceType === 'ios') {
+        extraHeaders = this.getSign();
+      }
+      let res = await this.$axios.$post(`/api/vip/getList`, extraHeaders)
+      if (res) {
+        this.vipList = res.data
+      }
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 91 - 0
components/topics/editor-aside.vue

@@ -0,0 +1,91 @@
+<template>
+  <div class="aside">
+    <h6>技术圈文章内容标准</h6>
+    <el-timeline class="list">
+      <el-timeline-item v-for="(activity, index) in activities" :key="index">{{activity.content}}</el-timeline-item>
+    </el-timeline>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      activities: [
+        {
+          content: '1. 技术结合开发应用的实例文章'
+        },
+        {
+          content: '2. 开发过程实战化梳理文章'
+        },
+        {
+          content: '3. 协同化开发经验文章'
+        },
+        {
+          content: '4. 模块化开发经验分享'
+        },
+        {
+          content: '5. 个人职业生涯规划'
+        },
+        {
+          content: '6. 知名互联网公司开发经验文章'
+        },
+        {
+          content: '7. 日常开发小经验分享类文章'
+        },
+        {
+          content: '8. Q&A形式解答类文章'
+        },
+        {
+          content: '9. 行业论坛聚焦化分享类文章'
+        },
+        {
+          content: '10. 互联网+类程序开发经验文章'
+        }
+      ]
+    };
+  },
+  computed: {
+  },
+  mounted() {
+  },
+  methods: {
+  }
+}
+</script>
+
+<style lang="scss">
+.aside {
+  position: relative;
+  float: right;
+  width: 250px;
+  background: #fff;
+  h6 {
+    position: relative;
+    padding: 14px 0;
+    text-align: center;
+    font-size: 14px;
+    font-family: PingFangSC-Semibold;
+    font-weight: 600;
+    color: rgba(51, 51, 51, 1);
+    line-height: 21px;
+    &::after {
+      content: "";
+      position: absolute;
+      left: 50%;
+      bottom: 0;
+      transform: translateX(-50%);
+      width: 230px;
+      height: 0px;
+      border-bottom: 2px dashed rgba(240, 240, 240, 1);
+    }
+  }
+  .list {
+    padding: 25px 0 25px 16px;
+    .el-timeline-item__wrapper {
+      top: -1px;
+      padding-left: 15px;
+    }
+  }
+}
+</style>

+ 402 - 0
components/topics/editor.vue

@@ -0,0 +1,402 @@
+<template>
+  <div class="editor">
+    <el-input v-model="title" class="title" placeholder="请输入文章标题" :maxlength="50"></el-input>
+    <el-input
+      type="textarea"
+      :autosize="{ minRows: 1, maxRows: 8}"
+      v-model="subTitle"
+      class="sub-title"
+      placeholder="请输入导语(选填)"
+      :maxlength="300"
+    ></el-input>
+    <editor :content="content" @change="handleChange"></editor>
+    <!-- <div class="quill-editor" v-model="content" v-quill:myQuillEditor="editorOption"></div>
+    <input type="file" id="imgInput" @change="handleContentFileChange" style="display: none;" />-->
+    <h5 class="label">封面图</h5>
+    <el-upload
+      class="avatar-uploader"
+      action="#"
+      :show-file-list="false"
+      :multiple="false"
+      accept="image/png, image/jpeg"
+      :before-upload="handleFileChange"
+    >
+      <i
+        v-if="cover_url"
+        class="el-icon-delete avatar-uploader-icon"
+        @click.stop="handleDeleteFile"
+      ></i>
+      <img v-if="cover_url" :src="cover_url" class="avatar" />
+      <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+      <div slot="tip" class="el-upload__tip">建议尺寸:480*330,大小在500k以内</div>
+    </el-upload>
+    <el-dialog :visible.sync="dialogVisible">
+      <img width="100%" :src="cover_url" alt />
+    </el-dialog>
+    <h5 class="label">选择分类</h5>
+    <el-select v-model="categoryId" placeholder="请选择文章分类" class="class">
+      <el-option
+        v-for="item in categoryList"
+        :key="item.value"
+        :label="item.label"
+        :value="item.value"
+      ></el-option>
+    </el-select>
+    <h5 class="label">标签</h5>
+    <el-select
+      class="tags"
+      v-model="tags"
+      multiple
+      filterable
+      allow-create
+      default-first-option
+      placeholder="每个标签以enter键隔开、最多5个,如:技术,经验"
+    >
+      <el-option
+        v-for="item in tagOptions"
+        :key="item.value"
+        :label="item.label"
+        :value="item.value"
+      ></el-option>
+    </el-select>
+    <footer>
+      <el-button type="primary" @click="publish">发布</el-button>
+      <el-button @click="cancel">取消</el-button>
+    </footer>
+  </div>
+</template>
+
+<script>
+import editor from "@/components/editor";
+export default {
+  head() {
+    return {
+      script: []
+    };
+  },
+  components: {
+    editor
+  },
+  data() {
+    return {
+      topicId: "", // 编辑
+      title: "",
+      subTitle: "",
+      content: "",
+      cover_url: "",
+      tags: [],
+      categoryId: "",
+      dialogVisible: false,
+      disabled: false,
+      categoryList: [
+        {
+          value: "default",
+          label: "推荐"
+        }
+      ],
+      fileList: [],
+      tagOptions: [
+        {
+          value: "技术",
+          label: "技术"
+        },
+        {
+          value: "人工智能",
+          label: "人工智能"
+        },
+        {
+          value: "区块链",
+          label: "区块链"
+        }
+      ],
+      uploading: false,
+      editorOption: {
+        theme: "snow",
+        placeholder: "请输入正文...",
+        modules: {
+          toolbar: [
+            ["bold", "italic", "underline", "strike"],
+            ["blockquote", "code-block"],
+            [{ header: 1 }, { header: 2 }],
+            [{ list: "ordered" }, { list: "bullet" }],
+            [{ script: "sub" }, { script: "super" }],
+            [{ indent: "-1" }, { indent: "+1" }],
+            [{ direction: "rtl" }],
+            [{ size: ["small", false, "large", "huge"] }],
+            [{ header: [1, 2, 3, 4, 5, 6, false] }],
+            [{ font: [] }],
+            [{ color: [] }, { background: [] }],
+            [{ align: [] }],
+            ["clean"],
+            ["link", "image"]
+          ],
+          syntax: {
+            highlight: text => hljs.highlightAuto(text).value
+          }
+        }
+      }
+    };
+  },
+  computed: {},
+  mounted() {
+    this.needLogin();
+    this.needVerify();
+    if (this.$route.params.id) {
+      this.loadData();
+    }
+    this.$axios.$post(`/api/community/topic/get_category`).then(res => {
+      if (res.status === 1) {
+        this.categoryList = res.data.map(it => ({
+          value: it.id,
+          label: it.name
+        }));
+      }
+    });
+  },
+  methods: {
+    loadData() {
+      const data = {
+        topicId: this.$route.params.id
+      };
+      this.$axios
+        .$post(`/api/community/topic/get_edit_topic`, data)
+        .then(res => {
+          console.log(res);
+          const topic = res.data;
+          if (topic) {
+            this.cover_url = topic.cover_url;
+            this.topicId = topic.id;
+            this.title = topic.title;
+            this.subTitle = topic.intro;
+            this.content = topic.body;
+            this.tags = topic.label ? topic.label.split(",") : "";
+            this.categoryId = topic.category_id;
+            if (this.cover_url) {
+              this.fileList.push({
+                name: "cover_image",
+                url: this.cover_url
+              });
+            }
+          } else {
+            this.goHome();
+          }
+          // TODO go details
+        });
+    },
+    publish() {
+      this.needVerify();
+      if (!this.content) {
+        this.$message.error("请输入文章正文");
+        return;
+      }
+      if (this.content.length > 100000) {
+        this.$message.error("文章正文不可超过10万字符,请删减");
+        return;
+      }
+      if (!this.title) {
+        this.$message.error("请输入文章标题");
+        return;
+      }
+      if (!this.categoryId) {
+        this.$message.error("请选择文章分类");
+        return;
+      }
+      if (this.tags.length > 5) {
+        this.$message.error("标签最多可填写5个标签,请删减");
+        return;
+      }
+      const data = {
+        title: this.title,
+        intro: this.subTitle,
+        body: this.content,
+        categoryId: this.categoryId,
+        label: this.tags && this.tags.length > 0 ? this.tags.join(",") : "",
+        cover_url: this.cover_url
+      };
+      if (this.topicId) {
+        data.topicId = this.topicId;
+        this.$axios
+          .$post(`/api/community/topic/update_topic`, data)
+          .then(res => {
+            console.log(res);
+            if (res.status === -99) {
+              this.goHome();
+            }
+            this.$message.success("编辑成功!");
+            // TODO go details
+            // window.location.href = `/p/${this.topicId}.html`;
+          });
+      } else {
+        this.$axios
+          .$post(`/api/community/topic/create_topic`, data)
+          .then(res => {
+            console.log(res);
+            if (!res.data) {
+              this.goHome();
+            }
+            this.$message.success("发布成功!");
+            // TODO go details
+            // window.location.href = `/p/${res.data.id}.html`;
+          });
+      }
+    },
+    cancel() {
+      this.$router.back();
+    },
+    handleChange(val) {
+      this.content = val;
+    },
+    handleDeleteFile() {
+      this.cover_url = "";
+    },
+    handleFileChange(file) {
+      console.log(file);
+      if (file.size / 1024 > 500) {
+        this.$message.error("图片大小不得超过500k,请重新选择");
+        return false;
+      }
+      const formData = new FormData();
+      formData.append("target", '{ "type": 3 }');
+      formData.append("id", "WU_FILE_0");
+      formData.append("name", file.name);
+      formData.append("type", file.type);
+      formData.append("lastModifiedDate", file.lastModifiedDate);
+      formData.append("size", file.size);
+      formData.append("file", file);
+      this.$axios
+        .$post(`/file/proxyUpload`, formData, {
+          headers: { "Content-Type": "multipart/form-data" }
+        })
+        .then(res => {
+          this.cover_url = (res.data && res.data.url) || "";
+          this.fileList = [
+            {
+              name: "file.name",
+              url: this.cover_url
+            }
+          ];
+        });
+    }
+  }
+};
+</script>
+
+<style lang="scss">
+.editor {
+  position: relative;
+  padding: 20px;
+  width: 740px;
+  background: #fff;
+  .title,
+  .sub-title {
+    .el-input__inner {
+      padding: 0;
+      border: 0;
+      border-radius: 0;
+    }
+  }
+  .title {
+    margin: 10px auto 20px;
+    font-size: 28px;
+    font-family: PingFangSC-Medium;
+    font-weight: 500;
+    color: rgba(29, 42, 58, 1);
+    line-height: 40px;
+  }
+  .sub-title {
+    margin-bottom: 28px;
+    min-height: 18px;
+    font-size: 14px;
+    font-family: PingFangSC-Regular;
+    font-weight: 400;
+    color: rgba(145, 154, 167, 1);
+    line-height: 18px;
+    border: 0;
+    .el-textarea__inner {
+      height: 18px;
+      line-height: 18px;
+      border: 0;
+      padding-left: 0;
+    }
+  }
+  .el-icon-delete {
+    display: none;
+  }
+  .avatar-uploader .el-upload {
+    width: 160px;
+    height: 110px;
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+    img {
+      width: 100%;
+      height: auto;
+      object-fit: contain;
+      object-position: top left;
+    }
+  }
+  .avatar-uploader .el-upload:hover {
+    border-color: #409eff;
+    .el-icon-delete {
+      display: block;
+    }
+  }
+  .avatar-uploader-icon {
+    position: absolute;
+    top: 0;
+    left: 0;
+    font-size: 28px;
+    color: #fff;
+    width: 160px;
+    height: 110px;
+    line-height: 110px;
+    text-align: center;
+    background: rgba(1, 1, 1, 0.3);
+    :hover {
+      color: #409eff;
+    }
+  }
+  .avatar {
+    width: 178px;
+    height: 178px;
+    display: block;
+  }
+  .label {
+    margin: 20px auto 10px;
+    font-size: 13px;
+    font-family: PingFangSC-Medium;
+    font-weight: 500;
+    color: rgba(25, 34, 46, 1);
+    line-height: 18px;
+  }
+  .ql-toolbar {
+    border-width: 1px 0 0 0 !important;
+  }
+  .quill-editor {
+    height: 450px;
+    border-left: 0 !important;
+    border-right: 0 !important;
+    font-size: 14px;
+    line-height: 25px;
+  }
+  .ql-snow.ql-toolbar::after {
+    display: inline-block;
+  }
+  .class {
+    width: 320px;
+  }
+  .tags {
+    width: 560px;
+  }
+  footer {
+    margin: 40px 0;
+    button {
+      width: 100px;
+      height: 40px;
+      border-radius: 0;
+    }
+  }
+}
+</style>

+ 110 - 0
components/uploader.vue

@@ -0,0 +1,110 @@
+<template>
+  <div class="uploader">
+    <el-upload
+      class="avatar-uploader"
+      action="#"
+      :show-file-list="false"
+      :multiple="false"
+      accept="image/png, image/jpeg"
+      :before-upload="handleFileChange"
+    >
+      <i v-if="imageUrl" class="el-icon-delete avatar-uploader-icon" @click.stop="handleDeleteFile"></i>
+      <img v-if="imageUrl" :src="imageUrl" class="avatar" />
+      <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+      <span v-if="title" class="title">{{title}}</span>
+    </el-upload>
+  </div>
+</template>
+
+<script>
+export default {
+  props: ["imageUrl", "title"],
+  components: {},
+  data() {
+    return {
+      uploading: false
+    };
+  },
+  computed: {},
+  mounted() {},
+  methods: {
+    handleDeleteFile() {
+      this.$emit("change", "");
+    },
+    handleFileChange(file) {
+      const file = e.target.files[0];
+      // if (file.size / 1024 > 500) {
+      //   this.$message.error("图片大小不得超过500k,请重新选择");
+      //   return false;
+      // }
+      const formData = new FormData();
+      formData.append("file", file);
+      formData.append("original_filename", file.name);
+      this.uploading = true;
+      this.$axios
+        .$post(`/upload_image`, formData, {
+          headers: { "Content-Type": "multipart/form-data" }
+        })
+        .then(res => {
+          this.$emit("change", res.filename);
+        })
+        .finally(() => {
+          this.uploading = false;
+        });
+    }
+  }
+};
+</script>
+
+<style lang="scss">
+.uploader {
+  position: relative;
+  padding: 20px;
+  width: 740px;
+  background: #fff;
+  .el-icon-delete {
+    display: none;
+  }
+  .avatar-uploader .el-upload {
+    width: 160px;
+    height: 110px;
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+    img {
+      width: 100%;
+      height: auto;
+      object-fit: contain;
+      object-position: top left;
+    }
+  }
+  .avatar-uploader .el-upload:hover {
+    border-color: #409eff;
+    .el-icon-delete {
+      display: block;
+    }
+  }
+  .avatar-uploader-icon {
+    position: absolute;
+    top: 0;
+    left: 0;
+    font-size: 28px;
+    color: #fff;
+    width: 160px;
+    height: 110px;
+    line-height: 110px;
+    text-align: center;
+    background: rgba(1, 1, 1, 0.3);
+    :hover {
+      color: #409eff;
+    }
+  }
+  .avatar {
+    width: 178px;
+    height: 178px;
+    display: block;
+  }
+}
+</style>

+ 4 - 2
nuxt.config.js

@@ -67,7 +67,7 @@ module.exports = {
   */
   axios: {
     // See https://github.com/nuxt-community/axios-module#options
-    // proxy: true,
+    proxy: true,
     // https: true,
     progress: true,
     // baseURL: process.env.BASE_URL || '',
@@ -75,13 +75,15 @@ module.exports = {
     timeout: 15000,
     credentials: true,
     proxyHeaders: true,
-    debug: true
+    // debug: true
   },
   /**
    * Proxy
    */
   proxy: [
     ['/api', { target: 'https://dev.test.proginn.com', changeOrigin: true }],
+    ['/file/proxyUpload', { target: 'https://dev.test.proginn.com', changeOrigin: true }],
+    ['/upload_image', { target: 'https://dev.test-jishuin.proginn.com', changeOrigin: true }],
     ['/image', { target: 'https://stacdn.proginn.com', changeOrigin: true }],
   ],
 

+ 5 - 1
package.json

@@ -16,8 +16,12 @@
     "@nuxtjs/proxy": "^1.3.1",
     "cross-env": "^5.2.0",
     "element-ui": "^2.4.11",
+    "hljs": "^6.2.3",
+    "node-sass": "^4.12.0",
     "nuxt": "^2.6.3",
-    "qs": "^6.7.0"
+    "qs": "^6.7.0",
+    "sass-loader": "^7.1.0",
+    "vue-quill-editor": "^3.0.6"
   },
   "devDependencies": {
     "less": "^3.9.0",

+ 18 - 6
pages/cert/type/_id.vue

@@ -3,7 +3,7 @@
   <section class="apply">
     <section class="apply-top">
       <h1>{{detail.name}}认证</h1>
-      <section class="info" v-html="detail.description"/>
+      <section class="info" v-html="detail.description" />
       <section v-if="conditions.length" class="cert-quals">
         <section class="cert-qual" v-for="(item, index) of conditions" :key="index">
           <section class="qual-status" :class="{'qual-completed': item.is_completed}">
@@ -103,8 +103,7 @@ export default {
     },
   },
   mounted() {
-    console.log('this.detail')
-    // console.log(this.detail)
+    console.log(this.detail)
   },
   methods: {
     clickVipPrice() {
@@ -117,14 +116,14 @@ export default {
           location.href = `https://www.proginn.com/index/app?from=index_h5`
           return
         } else if (this.$store.state.isPC) {
-          this.$confirm(`<p>1认证申请为付费服务,若因申请者不达标而认证不通过,费用不予退回,3个月内可以免费申请3次;</p><br><p>2认证通过后,如果违反平台相关规则,平台有权取消认证资质。</p>`, '提示', {
+          const handleConfirm = () => this.$confirm(`<p>1认证申请为付费服务,若因申请者不达标而认证不通过,费用不予退回,3个月内可以免费申请3次;</p><br><p>2认证通过后,如果违反平台相关规则,平台有权取消认证资质。</p>`, '提示', {
             dangerouslyUseHTMLString: true,
             confirmButtonText: '同意,我已理解上述内容',
             cancelButtonText: '拒绝,我再咨询了解一下',
           }).then(async () => {
             if (!this.detail.is_free_apply) location.href = `/pay?product_type=12&product_id=${this.detail.id}&next=/cert`
             else {
-              await this.$post('/api/cert/apply', { user_cert_id: this.detail.user_cert_id })
+              await this.$axios.$post('/api/cert/apply', { user_cert_id: this.detail.user_cert_id })
               this.$message({
                 message: '申请成功',
                 type: 'success'
@@ -133,7 +132,20 @@ export default {
             }
           }).catch(err => {
             console.log('cancel')
-          })
+          });
+          if (this.detail.questionnaire) {
+            this.$confirm(`<p>您当前申请的认证,需要填写并提交在线测试问卷,请问您是否完成该步骤?</p>`, '提示', {
+              dangerouslyUseHTMLString: true,
+              confirmButtonText: '已填写:下一步',
+              cancelButtonText: '忘记,前往填写',
+            }).then(() => {
+              handleConfirm();
+            }).catch(() => {
+              window.open(this.detail.questionnaire);
+            })
+          } else {
+            handleConfirm();
+          }
         } else {
           location.href = `proginn://pay?product_type=12&product_id=${this.detail.id}&next=/cert`
         }

+ 132 - 0
pages/sign/index.vue

@@ -0,0 +1,132 @@
+<template>
+  <div class="sign">
+    <intro></intro>
+    <verify></verify>
+    <info></info>
+    <profile></profile>
+    <experience></experience>
+    <education></education>
+    <skills></skills>
+    <works></works>
+    <social></social>
+  </div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import intro from "@/components/sign/intro";
+import verify from "@/components/sign/verify";
+import info from "@/components/sign/info";
+import profile from "@/components/sign/profile";
+import experience from "@/components/sign/experience";
+import education from "@/components/sign/education";
+import skills from "@/components/sign/skills";
+import works from "@/components/sign/works";
+import social from "@/components/sign/social";
+import qs from "qs";
+
+export default {
+  // async asyncData({ $axios, params }) {
+  //   let res = await $axios.$get(`/api/vip/getList`)
+  //   console.log('init', res)
+  // },
+  head: {
+    title: "申请签约 - 程序员客栈"
+  },
+  components: {
+    intro,
+    verify,
+    info,
+    profile,
+    experience,
+    education,
+    skills,
+    works,
+    social
+  },
+  // mixins: [getDeviceType],
+  data() {
+    return {
+      sign: null
+    };
+  },
+  computed: {
+    userInfo() {
+      return this.$store.state.userinfo;
+    }
+  },
+  mounted() {
+    // this.loadData();
+  },
+  methods: {
+    async loadData() {
+      let res = await this.$axios.$post(`/api/vip/getList`, extraHeaders);
+      if (res) {
+        this.vipList = res.data;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss">
+.sign {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  background: #f7f7f7;
+  > div {
+    margin-bottom: 10px;
+    width: 100%;
+    background: #fff;
+    &:last-of-type {
+      margin-bottom: 0;
+    }
+    > header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 13px 20px;
+      border-bottom: 1px solid #ebeef5;
+    }
+    h5 {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      font-size: 16px;
+      font-family: PingFangSC-Medium;
+      font-weight: 500;
+      color: rgba(29, 42, 58, 1);
+      line-height: 22px;
+    }
+    .status {
+      position: relative;
+      display: inline-block;
+      float: right;
+      font-size: 14px;
+      font-family: PingFangSC-Medium;
+      font-weight: 500;
+      line-height: 20px;
+    }
+    .success {
+      color: rgba(16, 185, 106, 1);
+      &::before {
+        content: "";
+        position: absolute;
+        display: inline-block;
+        left: -10px;
+        top: 50%;
+        transform: translate3d(-100%, -50%, 0);
+        width: 20px;
+        height: 20px;
+        background: url("~@/assets/img/sign/success.png");
+      }
+    }
+    button {
+      height: 34px;
+      line-height: 10px;
+      border-radius: 2px;
+    }
+  }
+}
+</style>

+ 48 - 0
pages/topics/_id/edit.vue

@@ -0,0 +1,48 @@
+<template>
+  <div>
+    <edit :locations="locations"></edit>
+  </div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import edit from "@/components/topics/edit";
+import qs from "qs";
+
+export default {
+  // async asyncData({ $axios, params }) {
+  //   let res = await $axios.$get(`/api/vip/getList`)
+  //   console.log('init', res)
+  // },
+  head: {
+    title: "编辑文章-技术圈"
+  },
+  components: {
+    edit
+  },
+  data() {
+    return {
+      locations: [
+        {
+          url: "/",
+          title: "技术圈"
+        },
+        {
+          url: `/p/${this.$route.params.id}.html`,
+          title: "文章详情"
+        },
+        {
+          url: "",
+          title: "编辑文章"
+        }
+      ]
+    };
+  },
+  computed: {},
+  mounted() {},
+  methods: {}
+};
+</script>
+
+<style>
+</style>

+ 44 - 0
pages/topics/create.vue

@@ -0,0 +1,44 @@
+<template>
+  <div>
+    <edit :locations="locations"></edit>
+  </div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import edit from "@/components/topics/edit";
+import qs from "qs";
+
+export default {
+  // async asyncData({ $axios, params }) {
+  //   let res = await $axios.$get(`/api/vip/getList`)
+  //   console.log('init', res)
+  // },
+  head: {
+    title: "发布文章-技术圈"
+  },
+  components: {
+    edit
+  },
+  data() {
+    return {
+      locations: [
+        {
+          url: "/",
+          title: "技术圈"
+        },
+        {
+          url: "/topics/create",
+          title: "发布文章"
+        }
+      ]
+    };
+  },
+  computed: {},
+  mounted() {},
+  methods: {}
+};
+</script>
+
+<style>
+</style>

+ 63 - 12
pages/user/_id.vue

@@ -2,7 +2,7 @@
   <div class="community-u" ref="container" @scroll="containerScroll">
     <div alt="back" class="background"></div>
     <div class="userinfo">
-      <img :src="info.icon_url" alt class="header-avatar">
+      <img :src="info.icon_url" alt class="header-avatar" />
       <div class="header-nickname">{{info.nickname}}</div>
       <div class="header-title">{{info.title}}</div>
       <div class="count-infos">
@@ -29,23 +29,33 @@
           <span style="color: var(--mainColor);">{{idInfo.topics_count}}</span> 篇文章
         </div>
         <div class="art" v-for="(art, index) of arts" :key="index" @click="clickArt(art, index)">
-          <img v-if="art.cover_url" :src="art.cover_url" alt class="art-img">
+          <img v-if="art.cover_url" :src="art.cover_url" alt class="art-img" />
           <section class="art-info">
             <h3 class="art-title">{{art.title}}</h3>
             <p class="art-summary">{{art.intro}}</p>
             <div class="art-subinfo">
               <div class="author">
-                <img :src="info.icon_url" alt="author-img" class="author-avatar">
+                <img :src="info.icon_url" alt="author-img" class="author-avatar" />
                 <span style="font-size: 10px;">{{info.nickname}}</span>
                 <span class="create-time">{{art.updated_at}}</span>
               </div>
               <div class="art-counts-info">
+                <a
+                  v-if="idInfo.has_edit_delete_access"
+                  class="delete"
+                  @click.stop="handleDelete(art)"
+                >删除</a>
+                <a
+                  v-if="idInfo.has_edit_delete_access"
+                  class="edit"
+                  :href="`/topics/${art.id}/edit`"
+                >编辑</a>
                 <div class="good">
-                  <img src="~@/assets/img/community/good_icon.png">
+                  <img src="~@/assets/img/community/good_icon.png" />
                   <span class="good-count">{{art.like_count}}</span>
                 </div>
                 <div class="comment">
-                  <img src="~@/assets/img/community/comment_icon.png">
+                  <img src="~@/assets/img/community/comment_icon.png" />
                   <span class="comment-count">{{art.reply_count}}</span>
                 </div>
               </div>
@@ -64,7 +74,7 @@
       </div>
       <div class="content-right">
         关注微信
-        <img src="~@/assets/img/wechat.jpg" alt="wechat" class="qr-code">
+        <img src="~@/assets/img/wechat.jpg" alt="wechat" class="qr-code" />
       </div>
     </div>
     <div class="loading" v-if="isLoading">
@@ -89,6 +99,11 @@ export default {
       `/api/user/getUserInfo?id=${id}&page=1&size=10`,
       { headers }
     );
+    if (!res.data) {
+      return {
+        title: '的技术圈主页-程序员客栈'
+      }
+    }
     return {
       title: `${res.data.info.nickname}的技术圈主页-程序员客栈`
     };
@@ -130,11 +145,35 @@ export default {
         `/api/user/getUserInfo?id=${this.$route.params.id}&page=1&size=10`
       );
       // console.log(res.data)
-      document.title = `${res.data.info.nickname}的技术圈主页-程序员客栈`;
-      if (res) {
+      if (res.data) {
+        document.title = `${res.data.info.nickname}的技术圈主页-程序员客栈`;
         this.idInfo = res.data;
       }
     },
+    async handleDelete(topic) {
+      console.log(topic)
+      await this.needLogin();
+      if (!this.idInfo.has_edit_delete_access) {
+        this.$message.error('你没有该文章的操作权限');
+      }
+      this.$confirm(`<p>确认删除该文章吗?</p>`, '提示', {
+        dangerouslyUseHTMLString: true,
+      }).then(async () => {
+        this.$axios.$post('/api/community/topic/delete', { topic_id: topic.id })
+          .then(() => {
+            this.$message({
+              message: '删除成功',
+              type: 'success'
+            })
+            this.getDetail();
+          }).catch(() => {
+            this.$message.error('该文章不存在')
+            setTimeout(() => location.reload(), 1000)
+          })
+      }).catch(err => {
+        console.log('cancel')
+      });
+    },
     clickLancer({ id }) {
       this.$router.push(`https://www.proginn.com/cert/type/${id}`);
     },
@@ -169,7 +208,7 @@ export default {
     configWx() {
       try {
         let conf = this.$store.state.wxConfig;
-        wx.ready(function() {
+        wx.ready(function () {
           //需在用户可能点击分享按钮前就先调用
           wx.config({
             debug: true,
@@ -184,10 +223,10 @@ export default {
               "onMenuShareQQ", // 分享到QQ接口
               "onMenuShareWeibo" // 分享到微博接口
             ],
-            success: function() {
+            success: function () {
               alert("wx.config ok");
             },
-            error: function(d) {
+            error: function (d) {
               alert("wx.config err:" + JSON.stringify(d));
             }
           });
@@ -196,7 +235,7 @@ export default {
             desc: "通过平台审核、认证,将获得更多接单机会", // 分享描述
             link: location.href, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
             imgUrl: "https://stacdn.proginn.com/favicon.ico", // 分享图标
-            success: function() {
+            success: function () {
               // 设置成功
               alert("微信图标设置成功");
             }
@@ -507,4 +546,16 @@ export default {
   align-items: center;
   margin: 20px 0;
 }
+.edit,
+.delete {
+  display: inline-block;
+  margin: 0 12px;
+  font-size: 12px;
+  font-weight: 500;
+  line-height: 15px;
+  color: rgba(48, 142, 255, 1);
+}
+.delete {
+  color: rgba(153, 153, 153, 1);
+}
 </style>

+ 33 - 2
plugins/common.js

@@ -1,12 +1,11 @@
 import Vue from 'vue'
 // import http from '@/plugins/http'
-
 // mixin
 Vue.mixin({
   async fetch({ $axios, store, req }) {
     let headers = req && req.headers
     let res = await $axios.$get('/api/user/getInfo', {headers});
-    if(res) {
+    if(res && res.data) {
       store.commit('updateUserinfo', { userinfo: res.data || {} })
     }
   },
@@ -26,6 +25,38 @@ Vue.mixin({
     },
   },
   methods: {
+    async needLogin() {
+      const userInfo = await this.getUserInfo();
+      console.log(userInfo)
+      if (!userInfo) {
+        this.goLogin();
+      }
+    },
+    async needVerify() {
+      const userInfo = await this.getUserInfo();
+      console.log(userInfo)
+      // 1是待审核,2审核通过,3是拒绝
+      if (userInfo.realname_verify_status !== '2') {
+        this.$message.error('根据互联网相关法规要求,请先完成实名认证')
+        this.goVerify();
+      }
+    },
+    async getUserInfo() {
+      let res = this.$store.userinfo;
+      if (!res) {
+        const result = await this.$axios.$get(
+          `/api/user/getInfo`
+        );
+        res = result.data;
+      }
+      return res;
+    },
+    goVerify() {
+      location.href = 'https://www.proginn.com/setting/user';
+    },
+    goHome() {
+      location.href = 'https://www.proginn.com/';
+    },
     goLogin(e, noAlert) {
       if(noAlert) {
         location.href = `https://www.proginn.com/?loginbox=show`

+ 12 - 9
plugins/nuxtAxios.js

@@ -3,7 +3,10 @@ import qs from 'qs';
 
 export default function ({ $axios, redirect, req, ...args }) {
     $axios.onRequest(config => {
-        config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
+        const isUpload = config.headers['Content-Type'] === 'multipart/form-data';
+        if (!isUpload) {
+            config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
+        }
         console.log('Before, making request to ', config.url, config.baseURL)
         const referer = config.headers.common && config.headers.common.referer;
         const url = config.url;
@@ -14,10 +17,10 @@ export default function ({ $axios, redirect, req, ...args }) {
         } else {
             // client http
             config.url = /https?/.test(url) ? `/${url.split('/').slice(3).join('/')}` : url;
-            config.baseURL = '';
+            // config.baseURL = '';
         }
         // stringify post data
-        if (config.method === 'post') {
+        if (config.method === 'post' && !isUpload) {
             // config.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
             const data = config.data;
             // const cookie = config.headers.cookie || config.headers.common.cookie || '';
@@ -35,7 +38,7 @@ export default function ({ $axios, redirect, req, ...args }) {
         console.log('After, making request to ', config.url, config.baseURL)
         return config;
     })
-  
+
     $axios.onResponse(res => {
         const data = res.data;
         // 不跳转到login的页面reg list
@@ -52,18 +55,18 @@ export default function ({ $axios, redirect, req, ...args }) {
             return result;
         }
         // req && req.url && needLogin(req.url)
-        if(data.status === 1) {
-            return res;
+        if(data.status === 1 || data.filename) {
         } else if(data.status === -99) {
             console.log(req && req.url)
             // if (req && req.url && needLogin(req.url)) {
             //     redirect('/?loginbox=show');
             // }
-            return res;
+            // return {code: '401', message: '请登录!'};
         } else {
             Vue.prototype.$message.error(data.info);
-            return data;
+            // return data;
         }
+        return res;
     })
     $axios.onError(error => {
         console.log('err', error);
@@ -72,4 +75,4 @@ export default function ({ $axios, redirect, req, ...args }) {
     //     redirect('/400')
     //   }
     })
-}
+}

文件差異過大導致無法顯示
+ 8193 - 0
yarn.lock