gdy96 4 роки тому
батько
коміт
0a2898783e

+ 54 - 0
src/api/index.ts

@@ -23,6 +23,9 @@ import {
   AdgroupList,
   IUserAdmin,
   AccountPlanConfig,
+  IGZHItem,
+  IDomainItem,
+  IPBookItem
 } from "@/types/api";
 
 /**
@@ -527,4 +530,55 @@ export const landingDelete = (data: {id: string | number}) => {
  */
  export const landingSubmit = (data: {id: string | number}) => {
   return axios("/landingPage/submit", {params: data});
+ }
+
+ /*
+ * 获取落地页公众号列表
+ * @returns
+ */
+export const getLandingOfficials = (): AxiosPromise<IList<IGZHItem>> => {
+  return axios("/landingPage/getGzh");
+};
+
+/**
+ * 获取落地页域名列表
+ * @returns
+ */
+export const getLandingDomains = (): AxiosPromise<IList<IDomainItem>> => {
+  return axios("/landingPage/domain");
+};
+
+/**
+ * 获取落地页推广书籍列表
+ * @param official_name
+ * @param book_name
+ * @returns
+ */
+export const getLandingBooks = (
+  official_name?: string,
+  book_name?: string
+): AxiosPromise<IList<IPBookItem>> => {
+  return axios("/landingPage/books", {
+    params: {
+      official_name,
+      book_name,
+      page_size: 999
+    }
+  });
+};
+
+/**
+ * 图片上传
+ * @param file
+ * @param type
+ * @returns
+ */
+export const onUpload = (
+  file: File,
+  type: string
+): AxiosPromise<{ url: string }> => {
+  const formData = new FormData();
+  formData.append("file", file);
+  formData.append("type", type);
+  return axios.post("/landingPage/upload", formData);
 };

+ 110 - 0
src/components/image-upload/index.vue

@@ -0,0 +1,110 @@
+<template>
+  <a-upload :show-upload-list="false" :custom-request="onFileChange">
+    <div class="cover-upload">
+      <img v-if="cover" :src="cover" alt="" />
+      <div class="cover-edit-wrap">
+        <template v-if="uploading">
+          <LoadingOutlined />
+        </template>
+        <template v-else>
+          <div class="add">
+            <PlusOutlined />
+          </div>
+        </template>
+      </div>
+    </div>
+  </a-upload>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive, toRefs } from "vue";
+import { message } from "ant-design-vue";
+import { LoadingOutlined, PlusOutlined } from "@ant-design/icons-vue";
+
+import { onUpload } from "@/api";
+
+const ImageUpload = defineComponent({
+  name: "ImageUpload",
+  components: {
+    LoadingOutlined,
+    PlusOutlined
+  },
+  props: {
+    type: {
+      type: String,
+      default: () => ""
+    },
+    name: {
+      type: String,
+      required: true,
+      default: () => ""
+    },
+    value: {
+      type: String,
+      default: () => ""
+    }
+  },
+  emits: ["change"],
+  setup(props, { emit }) {
+    const state = reactive({
+      uploading: false,
+      cover: props.value ?? ""
+    });
+
+    const onFileChange = async (file: { file: File }) => {
+      try {
+        state.uploading = true;
+        let tempFile = file.file;
+        const { data } = await onUpload(tempFile, props.type ?? "");
+        state.cover = data.url;
+        emit("change", { url: state.cover, type: props.name });
+      } catch (error) {
+        message.error(error.msg ?? "系统错误");
+      } finally {
+        state.uploading = false;
+      }
+    };
+
+    return { ...toRefs(state), onFileChange };
+  }
+});
+
+export default ImageUpload;
+</script>
+
+<style lang="scss" scoped>
+.cover-upload {
+  font-size: 0;
+  position: relative;
+  border-radius: 6px;
+  width: 128px;
+  height: 128px;
+  overflow: hidden;
+
+  img {
+    width: 128px;
+    height: 128px;
+  }
+
+  .cover-edit-wrap {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    left: 0;
+    top: 0;
+    background: rgba($color: #000, $alpha: 0.3);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 18px;
+    color: #fff;
+    cursor: pointer;
+    transition: opacity 0.3s;
+    opacity: 1;
+
+    // &:hover {
+    //   opacity: 1;
+    // }
+  }
+}
+</style>

+ 5 - 1
src/plugins/install.ts

@@ -29,6 +29,8 @@ import {
   Affix,
   Descriptions,
   Card,
+  Steps,
+  Upload
 } from "ant-design-vue";
 
 import VueClipboard3 from "./vue-clipboard";
@@ -70,7 +72,9 @@ const install = (app: App<Element>) => {
     .use(Modal)
     .use(Descriptions)
     .use(Drawer)
-    .use(Card);
+    .use(Card)
+    .use(Steps)
+    .use(Upload);
 };
 
 export default install;

+ 8 - 10
src/router/async.ts

@@ -56,7 +56,14 @@ const LandingAddPage: RouteConfig = {
   hidden: true,
   component: () => import("@/views/put/landing/add.vue")
 };
-
+export const LandingEditPage: RouteConfig = {
+  name: "LandingEditPage",
+  path: "/put/landing/edit",
+  meta: {
+    title: "落地页编辑",
+  },
+  component: () => import("@/views/put/landing/edit.vue"),
+};
 const PutLog: RouteConfig = {
   name: "PutLog",
   path: "log",
@@ -87,14 +94,6 @@ const PutAdGroup: RouteConfig = {
   component: () => import("@/views/put/ad-group.vue")
 };
 
-export const Operation: RouteConfig = {
-  name: "Operation",
-  path: "/Operation",
-  meta: {
-    title: "落地页编辑",
-  },
-  component: () => import("@/views/operation/editcontent.vue"),
-};
 const PutAdAcountL: RouteConfig = {
   name: "PutAdAcountL",
   path: "/put/datas/count",
@@ -139,7 +138,6 @@ export const PutManager: RouteConfig = {
     PutDataTab,
     PutData,
     PutDataMore,
-    Operation,
     LandingPage,
     LandingAddPage
   ],

+ 17 - 0
src/types/api.d.ts

@@ -408,3 +408,20 @@ interface AccountPlanConfig {
   channel_id: number | string;
   report_module: string;
 }
+
+interface IGZHItem {
+  gzh_name: string;
+  gzh_code: string;
+  gzh_img: string;
+  sub_img: string;
+}
+
+interface IDomainItem {
+  domain: string;
+  company_name: string;
+}
+
+interface IPBookItem {
+  bid: string | number;
+  book_name: string;
+}

+ 0 - 15
src/views/operation/index.vue

@@ -1,15 +0,0 @@
-<template>
-  <router-view></router-view>
-</template>
-
-<script lang="ts">
-import { defineComponent } from "vue";
-
-import useStore from "@/hooks/useStore";
-
-const OperationIndex = defineComponent({
-  setup() {},
-});
-
-export default OperationIndex;
-</script>

+ 90 - 2
src/views/put/landing/add.vue

@@ -1,3 +1,91 @@
 <template>
-  <div>落地页添加</div>
-</template>
+  <div class="landing-add">
+    <div class="landing-container">
+      <div class="step-container">
+        <a-steps :current="stepCurrent">
+          <a-step title="填写基本信息" />
+          <a-step title="编辑内容" />
+        </a-steps>
+      </div>
+      <div class="step-content-container">
+        <component
+          :is="stepComponent[stepCurrent]"
+          :content="forms"
+          @next="onTapNext"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive, toRefs, createVNode, ref } from "vue";
+import { onBeforeRouteLeave } from "vue-router";
+import { Modal } from "ant-design-vue";
+import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
+
+import StepOne from "./stepComp/step-one.vue";
+import StepTwo from "./stepComp/step-two.vue";
+
+const LandingAddPage = defineComponent({
+  name: "LandingAddPage",
+  components: {
+    StepOne,
+    StepTwo
+  },
+  setup() {
+    const state = reactive({
+      stepCurrent: 0,
+      stepComponent: ["step-one", "step-two"],
+      forms: ref<Record<string, any>>({})
+    });
+
+    onBeforeRouteLeave((_, __, next) => {
+      Modal.confirm({
+        title: "确认离开当前页面吗?",
+        content: "未保存的内容将会丢失",
+        icon: createVNode(ExclamationCircleOutlined),
+        onOk: () => {
+          next();
+        }
+      });
+    });
+
+    const onTapNext = (content: Record<string, any>) => {
+      state.stepCurrent++;
+      state.forms = content;
+      console.log(state.forms);
+    };
+
+    return {
+      ...toRefs(state),
+      onTapNext
+    };
+  }
+});
+
+export default LandingAddPage;
+</script>
+
+<style lang="scss">
+.landing-add {
+  padding: 22px;
+  min-height: 100%;
+
+  .landing-container {
+    height: inherit;
+    background: #fff;
+    box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.08);
+    padding: 16px;
+  }
+
+  .step-container {
+    padding: 20px 60px 36px;
+    border-bottom: 1px solid #e5e5e5;
+  }
+
+  .step-content-container {
+    padding: 20px 42px;
+  }
+}
+</style>

+ 3 - 7
src/views/operation/editcontent.vue

@@ -1,7 +1,5 @@
 <template>
-  <div class="edit-wrap">
-    <div class=""></div>
-  </div>
+  
 </template>
 
 <script lang="ts">
@@ -19,14 +17,12 @@ const EditContent = defineComponent({
     LockOutlined,
   },
   setup() {
-   
-    return { };
+
+    return {};
   },
 });
 
 export default EditContent;
 </script>
 
-<style lang="scss" scoped>
 
-</style>

+ 256 - 0
src/views/put/landing/stepComp/step-one.vue

@@ -0,0 +1,256 @@
+<template>
+  <div class="step-one-container">
+    <a-form
+      :label-col="{ span: 2 }"
+      :wrapper-col="{ offset: 1, span: 12 }"
+      :colon="false"
+    >
+      <a-form-item label="落地页名称" v-bind="validateInfos.title">
+        <a-input placeholder="请输入落地页名称" v-model:value="forms.title" />
+      </a-form-item>
+      <a-form-item label="公众号" v-bind="validateInfos.gzh_name">
+        <a-select
+          placeholder="请选择公众号"
+          v-model:value="forms.gzh_name"
+          @change="onGzhChange"
+        >
+          <template v-for="official in officials" :key="official.gzh_name">
+            <a-select-option :value="official.gzh_name">
+              {{ official.gzh_name }}
+            </a-select-option>
+          </template>
+        </a-select>
+      </a-form-item>
+      <a-form-item label="微信号">
+        <a-input placeholder="请输入微信号" disabled :value="forms.gzh_code" />
+      </a-form-item>
+      <a-form-item label="域名" v-bind="validateInfos.domain">
+        <a-select
+          placeholder="请选择域名"
+          v-model:value="forms.domain"
+          @change="onDomainChange"
+        >
+          <template v-for="domain in domains" :key="domain.domain">
+            <a-select-option :value="domain.domain">
+              {{ domain.domain }}
+            </a-select-option>
+          </template>
+        </a-select>
+      </a-form-item>
+      <a-form-item label="推广书籍" v-bind="validateInfos.bid">
+        <a-select placeholder="请选择需要推广的书籍" v-model:value="forms.bid">
+          <template v-for="book in books" :key="book.bid">
+            <a-select-option :value="book.bid">
+              {{ book.book_name }}
+            </a-select-option>
+          </template>
+        </a-select>
+      </a-form-item>
+      <a-form-item label="公司主体">
+        <a-input
+          placeholder="请输入公司主体"
+          disabled
+          :value="forms.company_name"
+        />
+        <!-- <a-select placeholder="请选择公司主体">
+          <a-select-option value="1">1</a-select-option>
+          <a-select-option value="2">2</a-select-option>
+        </a-select> -->
+      </a-form-item>
+      <a-form-item label="渠道" v-bind="validateInfos.link_source">
+        <a-select placeholder="请选择渠道" v-model:value="forms.link_source">
+          <a-select-option value="uc">UC</a-select-option>
+          <a-select-option value="iqiyi">爱奇艺</a-select-option>
+          <a-select-option value="tiktok">抖音</a-select-option>
+          <a-select-option value="vivo">Vivo</a-select-option>
+          <a-select-option value="baidu">百度</a-select-option>
+        </a-select>
+      </a-form-item>
+      <a-form-item label="公众号后缀">
+        <a-radio-group v-model:value="forms.gzh_suffix_mode">
+          <a-radio :value="0">无后缀</a-radio>
+          <a-radio :value="3">官方后缀(推荐)</a-radio>
+        </a-radio-group>
+      </a-form-item>
+      <a-form-item label="跳转类型">
+        <a-radio-group v-model:value="forms.jump_type">
+          <a-radio value="copy_name">文本粘贴</a-radio>
+          <a-radio value="xcx">小程序</a-radio>
+        </a-radio-group>
+      </a-form-item>
+      <a-form-item label="关注图片" v-bind="validateInfos.sub_img">
+        <image-upload
+          name="sub_img"
+          type="关注图片"
+          :value="forms.sub_img"
+          @change="onUploadChange"
+        />
+      </a-form-item>
+      <a-form-item label="公众号头像">
+        <image-upload
+          name="gzh_img"
+          type="公众号头像"
+          :value="forms.gzh_img"
+          @change="onUploadChange"
+        />
+      </a-form-item>
+      <a-form-item label="简介">
+        <a-textarea v-model="forms.name"></a-textarea>
+      </a-form-item>
+      <a-form-item :wrapper-col="{ offset: 3 }">
+        <a-button @click="onBack" style="margin-right: 10px">返回</a-button>
+        <a-button type="primary" @click="onNextStep">下一步</a-button>
+      </a-form-item>
+    </a-form>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive, ref, toRefs } from "vue";
+import { useForm } from "@ant-design-vue/use";
+import { useRouter } from "vue-router";
+
+import ImageUpload from "@/components/image-upload/index.vue";
+
+import { getLandingBooks, getLandingDomains, getLandingOfficials } from "@/api";
+import { IDomainItem, IGZHItem, IPBookItem } from "@/types/api";
+
+const StepOne = defineComponent({
+  name: "StepOne",
+  components: {
+    ImageUpload
+  },
+  props: {},
+  emits: ["next"],
+  setup(props, { emit }) {
+    const router = useRouter();
+
+    const state = reactive({
+      officials: ref<IGZHItem[]>([]),
+      domains: ref<IDomainItem[]>([]),
+      books: ref<IPBookItem[]>([]),
+      forms: {
+        title: "",
+        name: "",
+        gzh_code: "",
+        gzh_name: "",
+        bid: "",
+        domain: "",
+        company_name: "",
+        link_source: "uc",
+        gzh_suffix_mode: 0,
+        gzh_suffix: "",
+        jump_type: "copy_name",
+        sub_img: "",
+        gzh_img: "",
+        content: ""
+      }
+    });
+
+    const formsRules = reactive({
+      title: [{ required: true, trigger: "blur", message: "请输入落地页名称" }],
+      gzh_name: [
+        { required: true, trigger: "change", message: "请选择公众号" }
+      ],
+      domain: [
+        {
+          required: true,
+          trigger: "change",
+          message: "请选择域名"
+        }
+      ],
+      bid: [
+        {
+          required: true,
+          type: "number",
+          trigger: "change",
+          message: "请选择推广书籍"
+        }
+      ],
+      link_source: [
+        {
+          required: true,
+          trigger: "change",
+          message: "请选择渠道"
+        }
+      ],
+      sub_img: [
+        {
+          required: true,
+          message: "请上传关注图片"
+        }
+      ]
+    });
+
+    const { validate, validateInfos } = useForm(state.forms, formsRules);
+
+    const initConfigData = async () => {
+      try {
+        const [
+          { data: official },
+          { data: domains },
+          { data: books }
+        ] = await Promise.all([
+          getLandingOfficials(),
+          getLandingDomains(),
+          getLandingBooks()
+        ]);
+        state.officials = official.list;
+        state.domains = domains.list;
+        state.books = books.list;
+      } catch (error) {
+        console.log("error happened in initLandingConfig:", error.message);
+        router.back();
+      }
+    };
+
+    initConfigData();
+
+    const onGzhChange = (name: string) => {
+      const target = state.officials.find((gzh) => gzh.gzh_name === name);
+      // state.forms.channel_id = target!.channel_id.toString() ?? "";
+      state.forms.gzh_img = target!.gzh_img ?? "";
+      state.forms.sub_img = target!.sub_img ?? "";
+      state.forms.gzh_code = target!.gzh_code ?? "";
+    };
+
+    const onDomainChange = (domain: string) => {
+      const target = state.domains.find((item) => item.domain === domain);
+      state.forms.company_name = target!.company_name ?? "";
+    };
+
+    const onUploadChange = (result: {
+      url: string;
+      type: "sub_img" | "gzh_img";
+    }) => {
+      state.forms[result.type] = result.url;
+      console.log(state.forms);
+    };
+
+    const onBack = () => {
+      router.replace("/put/landing");
+    };
+
+    const onNextStep = async () => {
+      try {
+        await validate();
+        emit("next", state.forms);
+      } catch (error) {
+        console.log(error);
+      }
+    };
+
+    return {
+      ...toRefs(state),
+      validateInfos,
+      onGzhChange,
+      onDomainChange,
+      onUploadChange,
+      onBack,
+      onNextStep
+    };
+  }
+});
+
+export default StepOne;
+</script>

+ 135 - 0
src/views/put/landing/stepComp/step-two.vue

@@ -0,0 +1,135 @@
+<template>
+  <div class="edit-content">
+    <div class="edit-form">
+      <div class="edit-form-item">
+        <p class="edit-label">正文</p>
+        <div class="charpt-list form-content">
+          <template v-for="(item, idx) in charptList" :key="idx">
+            <div class="charpt-list-item">
+              <p class="item-label">文章标题</p>
+              <div class="item-input">
+                <a-input
+                  v-model:value="item.title"
+                  placeholder="请输入标题"
+                  style="width:638px"
+                />
+              </div>
+              <a href="javascript:;" class="delete-text" v-show="idx > 0" @click="deleteLine(idx)"
+                >删除</a
+              >
+            </div>
+            <div class="charpt-list-item bor-line">
+              <p class="item-label">正文内容</p>
+              <div class="item-input">
+                <a-textarea
+                  v-model:value="item.content"
+                  placeholder="请输入正文"
+                  :auto-size="{ minRows: 5, maxRows: 10 }"
+                />
+              </div>
+            </div>
+            <hr />
+          </template>
+          <div class="add-item" @click="addNewCharpt">
+            添加新章节+
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive, ref, toRefs } from "vue";
+
+const StepTwo = defineComponent({
+  name: "StepTwo",
+  setup(props, { emit }) {
+    const state = reactive({
+      charptList: ref<Array<{ content: string; title: string }>>([
+        {
+          title: "大苏打萨达速度阿萨德阿萨德阿士大夫撒方式打发的撒范德萨飞",
+          content: "sadassssssssssssssssasdsaddasdas",
+        },
+      ]),
+    });
+    const addNewCharpt = () => {
+      let data = {
+        title: "",
+        content: "",
+      };
+      state.charptList.push(data);
+    };
+    const deleteLine = (idx:number) =>{
+      state.charptList.splice(idx,1)
+    }
+    return {
+      ...toRefs(state),
+      addNewCharpt,
+      deleteLine
+    };
+  },
+});
+
+export default StepTwo;
+</script>
+<style lang="scss" scoped>
+.edit-form {
+  .add-item {
+    width: 270px;
+    height: 40px;
+    line-height: 35px;
+    color: #006eff;
+    font-size: 16px;
+    text-align: center;
+    background: #ffffff;
+    border-radius: 3px;
+    border: 2px solid #006eff;
+    margin: 15px auto 0;
+  }
+  .edit-form-item {
+    font-size: 16px;
+    display: flex;
+    align-items: center;
+    hr {
+      margin-bottom: 25px;
+      border: none;
+      height: 1px;
+      background-color: #e5e5e5;
+    }
+    .edit-label {
+      margin-right: 15px;
+    }
+    .form-content {
+      padding: 30px 50px;
+      background: #f7f7f7;
+    }
+
+    .charpt-list-item {
+      display: flex;
+      align-items: center;
+      margin-bottom: 30px;
+      overflow: hidden;
+      &.bor-line {
+        margin-bottom: 25px;
+      }
+      .item-label {
+        margin-right: 18px;
+      }
+      .delete-text {
+        display: block;
+        margin-left: 15px;
+      }
+    }
+  }
+}
+</style>
+<style lang="scss">
+.bor-line {
+  .item-input {
+    textarea {
+      width: 638px;
+    }
+  }
+}
+</style>