Browse Source

Merge branch 'master' of qk:zhuishuyun/precise_delivery_distribution_front

gdy96 4 years ago
parent
commit
274c088108

+ 1 - 1
.env.production

@@ -1,4 +1,4 @@
 NODE_ENV = production
 VUE_APP_BASE_URL = '/'
-VUE_APP_PUB_URL = './'
+VUE_APP_PUB_URL = 'https://firemanage.oss-cn-hangzhou.aliyuncs.com/FE-resource/'
 PROXY_API_URL = 'https://promoter.58duke.com/'

+ 2 - 0
package.json

@@ -11,12 +11,14 @@
     "axios": "^0.21.0",
     "clipboard": "^2.0.6",
     "core-js": "^3.6.5",
+    "qrcode": "^1.4.4",
     "vue": "^3.0.0",
     "vue-router": "^4.0.0-0",
     "vuex": "^4.0.0-0"
   },
   "devDependencies": {
     "@types/clipboard": "^2.0.1",
+    "@types/qrcode": "^1.3.5",
     "@vue/cli-plugin-babel": "~4.5.0",
     "@vue/cli-plugin-router": "~4.5.0",
     "@vue/cli-plugin-typescript": "~4.5.0",

+ 37 - 36
prod.config.js

@@ -1,3 +1,5 @@
+const webpack = require("webpack");
+const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
 const isProd = process.env.NODE_ENV === "production";
 
 // * 避免打包项
@@ -51,20 +53,7 @@ const optimization = {
   },
 };
 
-// * 资源配置
-const cdns = {
-  dev: {},
-  build: {
-    css: [],
-    js: [
-      "https://cdn-novel.iycdm.com/static/vue.min.js",
-      "https://cdn-novel.iycdm.com/static/vuex.min.js",
-      "https://cdn-novel.iycdm.com/static/vue-router.min.js",
-      "https://cdn-novel.iycdm.com/static/vue-lazyload.js",
-      "https://cdn-novel.iycdm.com/static/axios.min.js",
-    ],
-  },
-};
+const ossCDN = "https://firemanage.oss-cn-hangzhou.aliyuncs.com/FE-resource";
 
 // * oss config
 const ossConfig = {
@@ -72,11 +61,22 @@ const ossConfig = {
   region: "oss-cn-hangzhou",
   ak: "LTAIowrHAk6HHxb8",
   sk: "vhrLQEn1WW8WQphOPBfcDE8zwx7nel",
-  bucket: "zhuishuyun",
+  bucket: "firemanage",
 };
 
-const ossCDN = "https://cdn-novel.iycdm.com/";
-
+// * 资源配置
+const cdns = {
+  dev: {},
+  build: {
+    css: [],
+    js: [
+      `${ossCDN}/library/vue.next.min.js`,
+      `${ossCDN}/library/vuex.next.min.js`,
+      `${ossCDN}/library/vue-router.next.min.js`,
+      `${ossCDN}/library/axios.min.js`,
+    ],
+  },
+};
 // * 打包后资源上传oss
 const uploadAssetsToOSS = (config) => {
   config
@@ -112,32 +112,33 @@ const assetsGzip = (config) => {
 };
 
 // * 代码压缩
-const codeUglify = (config) => {
-  config
-    .plugin("uglifyjs-webpack-plugin")
-    .use(require("uglifyjs-webpack-plugin"), [
-      {
-        uglifyOptions: {
-          //生产环境自动删除console
-          compress: {
-            drop_debugger: true,
-            drop_console: false,
-            pure_funcs: ["console.log"],
-          },
-        },
-        sourceMap: false,
-        parallel: true,
-      },
-    ]);
+const codeUglifyConfig = {
+  uglifyOptions: {
+    //生产环境自动删除console
+    compress: {
+      drop_debugger: true,
+      drop_console: false,
+      pure_funcs: ["console.log"],
+    },
+  },
+  sourceMap: false,
+  parallel: true,
 };
 
+const plugins = isProd
+  ? [
+      new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
+      new webpack.HashedModuleIdsPlugin(),
+      new UglifyJsPlugin(codeUglifyConfig),
+    ]
+  : [];
+
 module.exports = {
   uploadAssetsToOSS,
   assetsGzip,
-  codeUglify,
+  plugins,
   externals,
   optimization,
   cdns,
-  ossCDN,
   assetsDir,
 };

+ 20 - 2
public/index.html

@@ -3,8 +3,26 @@
   <head>
     <meta charset="utf-8" />
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
-    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
-    <title><%= htmlWebpackPlugin.options.title %></title>
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
+    />
+    <link
+      rel="icon"
+      type="image/png"
+      href="data:image/png;base64,iVBORw0KGgo="
+    />
+    <!-- 使用CDN加速的CSS文件,配置在vue.config.js下 -->
+    <% for (var i in
+    htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.css) { %>
+    <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet" />
+    <% } %>
+    <!-- 使用CDN加速的JS文件,配置在vue.config.js下 -->
+    <% for (var i in
+    htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
+    <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
+    <% } %>
+    <title>精准投放后台</title>
   </head>
   <body>
     <div id="app"></div>

+ 19 - 0
src/api/index.ts

@@ -126,3 +126,22 @@ export const getDeliveryStatList = (
 export const getPlatforms = (): AxiosPromise<IPlatform[]> => {
   return axios("/simplePlatforms");
 };
+
+/**
+ * 添加投放书籍
+ * @param data
+ */
+export const addDeliveryBook = (data: {
+  delivery_bid: string | number;
+  official_id: string | number;
+  platform: string;
+}) => {
+  return axios.post("/addDeliveryBook", data);
+};
+
+/**
+ * 退出登录
+ */
+export const logout = () => {
+  return axios("/logout");
+};

+ 8 - 7
src/components/tool-bar/index.vue

@@ -14,7 +14,7 @@
     <slot />
     <div class="tool-bar-item">
       <a-button type="primary"
-                :loading="btn_loading"
+                :loading="loading"
                 @click="onConfirm">
         <template #icon>
           <search-outlined />
@@ -27,7 +27,7 @@
 </template>
 
 <script lang="ts">
-import { defineComponent, PropType, ref, watchEffect } from "vue";
+import { defineComponent, PropType, ref, watch, watchEffect } from "vue";
 import { SearchOutlined } from "@ant-design/icons-vue";
 
 // TODO 可以考虑之后通过json生成
@@ -49,24 +49,25 @@ const ToolBar = defineComponent({
       default: false,
     },
   },
+  emits: ["update:loading", "confirm"],
   setup(props, { emit, slots }) {
     const fields = ref<{ [key: string]: string }>({});
-    let btn_loading = ref(props.loading);
+    let loading = ref(props.loading);
     let showPickerSlots = ref(!!slots.picker);
 
     props.text.forEach((field) => {
       fields.value[field] = "";
     });
 
-    watchEffect(() => (btn_loading.value = props.loading));
+    watchEffect(() => (loading.value = props.loading));
 
     const onConfirm = () => {
-      btn_loading.value = true;
-      emit("update:loading", btn_loading.value);
+      loading.value = true;
+      emit("update:loading", loading.value);
       emit("confirm", fields.value);
     };
 
-    return { fields, showPickerSlots, btn_loading, onConfirm };
+    return { fields, showPickerSlots, loading, onConfirm };
   },
 });
 

+ 2 - 0
src/global.d.ts

@@ -1,3 +1,4 @@
+import { MessageApi } from "ant-design-vue/lib/message";
 import { ModalFunc, ModalFuncProps } from "ant-design-vue/lib/modal/Modal";
 import { ComponentCustomProperties } from "vue";
 
@@ -8,5 +9,6 @@ declare module "@vue/runtime-core" {
       container?: HTMLElement
     ): Promise<ClipboardJS.Event>;
     $confirm(options: ModalFuncProps): ModalFunc;
+    $message: MessageApi;
   }
 }

+ 13 - 0
src/helper/utils.ts

@@ -0,0 +1,13 @@
+const utilString = Object.prototype.toString;
+
+export const isDate = (date: any): date is Date => {
+  return utilString.call(date) === "[object Date]";
+};
+
+export const isObject = (value: any): value is object => {
+  return utilString.call(value) === "[object Object]";
+};
+
+export const isArray = (array: any[]): array is Array<any> => {
+  return utilString.call(array) === "[object Array]";
+};

+ 12 - 28
src/hooks/usePagination.ts

@@ -6,47 +6,31 @@ import { IMeta } from "@/types/api";
  * TODO 还需要根据具体情况做调整
  */
 const usePagination = () => {
-  let meta = reactive<IMeta>({
+  let meta = ref<Partial<IMeta>>({
     current_page: 0,
-    is_end: true,
-    last_page: 0,
-    next_page: 0,
-    next_page_url: "",
     per_page: 10,
-    prev_page_url: "",
     total: 0,
   });
 
   let loading = ref(false);
-  let current = ref(1);
   let tablePageOptions = reactive({
-    current: current.value,
-    pageSize: meta.per_page,
-    total: meta.total,
+    current: meta.value.current_page,
+    pageSize: meta.value.per_page,
+    total: meta.value.total,
   });
 
   watch(
-    () => meta.current_page,
-    (current_page) => {
-      console.log("change");
-      loading.value = current_page < meta.last_page;
-      current.value = current_page + 1;
+    () => meta.value,
+    () => {
+      console.log("change page");
+      tablePageOptions.current = meta.value.current_page;
+      tablePageOptions.pageSize = meta.value.per_page;
+      tablePageOptions.total = meta.value.total;
+      loading.value = false;
     }
   );
 
-  watch(
-    () => meta.total,
-    (total) => {
-      if (!total) {
-        loading.value = false;
-        meta.last_page = 0;
-        meta.current_page = 0;
-        current.value = 1;
-      }
-    }
-  );
-
-  return { loading, meta, current, tablePageOptions };
+  return { loading, meta, tablePageOptions };
 };
 
 export default usePagination;

+ 24 - 0
src/hooks/useQRCode.ts

@@ -0,0 +1,24 @@
+import { ComputedRef, Ref, ref, watch } from "vue";
+import QRCode from "qrcode";
+
+type MaybeRef<T> = T | Ref<T> | ComputedRef<T>;
+
+const useQRCode = (
+  text: MaybeRef<string>,
+  options?: QRCode.QRCodeToDataURLOptions
+) => {
+  const src = ref(text);
+  const result = ref("");
+
+  watch(
+    src,
+    async (value) => {
+      result.value = await QRCode.toDataURL(value, options);
+    },
+    { immediate: true }
+  );
+
+  return result;
+};
+
+export default useQRCode;

+ 18 - 0
src/hooks/useValidate.ts

@@ -0,0 +1,18 @@
+import { isObject } from "@/helper/utils";
+
+// TODO 简单判断字段空值
+const useValidate = <T extends Record<string, any>>(fields: T): T => {
+  const flagArray: boolean[] = [];
+  Object.keys(fields).forEach((field) => {
+    if (isObject(fields[field])) useValidate(fields[field]);
+    else flagArray.push(!!fields[field]);
+  });
+
+  const hasEmpty = flagArray.some((flag) => !flag);
+
+  if (hasEmpty) throw new Error("字段不能为空");
+
+  return fields as T;
+};
+
+export default useValidate;

+ 20 - 1
src/layout/components/AppHeaderUser.vue

@@ -2,7 +2,8 @@
   <a-popover trigger="click">
     <template v-slot:content>
       <div class="setting-group">
-        <p class="cursor">
+        <p class="cursor"
+           @click="onLogout">
           <PoweroffOutlined />安全退出
         </p>
       </div>
@@ -22,6 +23,9 @@ import {
   CaretDownFilled,
   PoweroffOutlined,
 } from "@ant-design/icons-vue";
+import useApp from "@/hooks/useApp";
+import { logout } from "@/api";
+import { MutationType } from "@/store/modules/app/_type";
 
 const AppHeaderUser = defineComponent({
   components: {
@@ -32,6 +36,21 @@ const AppHeaderUser = defineComponent({
   props: {
     name: String,
   },
+  setup() {
+    const { store, router } = useApp();
+
+    const onLogout = async () => {
+      try {
+        await logout();
+        store.commit(MutationType.setUser, {});
+        router.replace("/login");
+      } catch (error) {
+        console.log("logout error");
+      }
+    };
+
+    return { onLogout };
+  },
 });
 
 export default AppHeaderUser;

+ 4 - 0
src/plugins/install.ts

@@ -7,6 +7,7 @@ import {
   Input,
   Layout,
   Menu,
+  message,
   Modal,
   Popover,
   Select,
@@ -15,16 +16,19 @@ import {
 
 import VueClipboard3 from "./vue-clipboard";
 import VueConfirmDirective from "./vue-confirm";
+import VueQrCode from "./vue-qrcode";
 
 import { ModalConfirmKey } from "./injectionKey";
 
 const install = (app: App<Element>) => {
   app.provide(ModalConfirmKey, Modal.confirm);
   app.config.globalProperties.$confirm = Modal.confirm;
+  app.config.globalProperties.$message = message;
 
   return app
     .use(VueClipboard3)
     .use(VueConfirmDirective)
+    .use(VueQrCode)
     .use(ConfigProvider)
     .use(Layout)
     .use(Menu)

+ 48 - 0
src/plugins/vue-qrcode.ts

@@ -0,0 +1,48 @@
+import { DirectiveBinding, VNode, App } from "vue";
+import QRCode from "qrcode";
+
+const insertQrCode2Element = (el: HTMLElement | Element, qrCode: string) => {
+  if (el.nodeName === "IMG") el.setAttribute("src", qrCode);
+  else {
+    // 判断原来的node是否存在 存在就删除
+    const prevChild = el.lastElementChild;
+    if (prevChild) el.removeChild(prevChild);
+    const imgElement = document.createElement("img");
+    imgElement.src = qrCode;
+    el.appendChild(imgElement);
+  }
+};
+
+const bind = async (
+  el: HTMLElement | Element,
+  binding: DirectiveBinding,
+  vnode: VNode
+) => {
+  console.log("mounted:", binding);
+  const qrText = binding.value;
+  if (typeof qrText === "string") {
+    const code = await QRCode.toDataURL(qrText);
+    insertQrCode2Element(el, code);
+  } else console.log("v-qrode 需要一个string类型的值");
+};
+
+const update = async (el: HTMLElement | Element, binding: DirectiveBinding) => {
+  if (binding.oldValue === binding.value) return;
+  console.log("update:", binding);
+  const qrText = binding.value;
+  if (typeof qrText === "string") {
+    const code = await QRCode.toDataURL(qrText);
+    insertQrCode2Element(el, code);
+  } else console.log("v-qrode 需要一个string类型的值");
+};
+
+const VueQrCode = {
+  install: (app: App<Element>) => {
+    app.directive("qrcode", {
+      mounted: bind,
+      updated: update,
+    });
+  },
+};
+
+export default VueQrCode;

+ 1 - 1
src/router/async.ts

@@ -15,7 +15,7 @@ const PutBook: RouteConfig = {
   meta: {
     title: "投放书籍",
   },
-  component: () => import("@/views/put/put-book"),
+  component: () => import("@/views/put/put-book.vue"),
 };
 
 const PutAdAccount: RouteConfig = {

+ 2 - 2
src/views/Login.vue

@@ -66,8 +66,8 @@ const Login = defineComponent({
 
     // TODO 测试账号
     if (process.env.NODE_ENV === "development") {
-      data.forms.account = process.env.VUE_APP_LOGIN_ACCOUNT;
-      data.forms.passwd = process.env.VUE_APP_LOGIN_PASSWORD;
+      data.forms.account = process.env.VUE_APP_LOGIN_ACCOUNT as string;
+      data.forms.passwd = process.env.VUE_APP_LOGIN_PASSWORD as string;
     }
 
     const onLogin = async () => {

+ 3 - 3
src/views/account/account.vue

@@ -9,8 +9,8 @@
         <a-select class="full-width"
                   v-model:value="query.platform">
           <a-select-option v-for="platform in platforms"
-                           :key="platform.name"
-                           :value="platform.name">{{platform.desc}}</a-select-option>
+                           :key="platform.value"
+                           :value="platform.value">{{platform.label}}</a-select-option>
         </a-select>
       </div>
     </tool-bar>
@@ -76,7 +76,7 @@ const Account = defineComponent({
       loading.value = true;
       getOfficialAccounts(params).then(({ data }) => {
         state.list = data.list;
-        meta = data.meta;
+        meta.value = data.meta;
         loading.value = false;
         state.searching = false;
       });

+ 15 - 18
src/views/put/put-ad-account.vue

@@ -1,25 +1,22 @@
 <template>
   <div class="page-wrap page-wrap-put-books">
-    <tool-bar
-      :text="['email', 'account_id', 'account_name']"
-      :label="['邮箱', '账户ID', '用户名']"
-      v-model:loading="inSearching"
-      @confirm="onSearch"
-    >
+    <tool-bar :text="['email', 'account_id', 'account_name']"
+              :label="['邮箱', '账户ID', '用户名']"
+              v-model:loading="inSearching"
+              @confirm="onSearch">
       <template #exbutton>
-        <a-button type="primary" class="ml-10">
+        <a-button type="primary"
+                  class="ml-10">
           授权
         </a-button>
       </template>
     </tool-bar>
-    <a-table
-      :columns="columns"
-      :data-source="list"
-      :pagination="tablePageOptions"
-      :loading="loading.value"
-      @change="handleTableChange"
-      rowKey="id"
-    >
+    <a-table :columns="columns"
+             :data-source="list"
+             :pagination="tablePageOptions"
+             :loading="loading.value"
+             @change="handleTableChange"
+             rowKey="id">
     </a-table>
   </div>
 </template>
@@ -48,7 +45,7 @@ const PutAdAccount = defineComponent({
     });
     getAdPushList({ page: 1 }).then((res) => {
       state.list = res.data.list;
-      meta = res.data.meta;
+      meta.value = res.data.meta;
     });
     const onSearch = async (fields: Record<string, string>) => {
       try {
@@ -61,7 +58,7 @@ const PutAdAccount = defineComponent({
           page: 1,
         });
         state.list = data.list;
-        meta = data.meta;
+        meta.value = data.meta;
       } catch (e) {
         console.log(e);
       } finally {
@@ -74,7 +71,7 @@ const PutAdAccount = defineComponent({
       const { current, pageSize, total } = pagination;
       getAdPushList({ page: current }).then((res) => {
         state.list = res.data.list;
-        meta = res.data.meta;
+        meta.value = res.data.meta;
       });
     };
 

+ 27 - 16
src/views/put/put-ad-plan.vue

@@ -1,9 +1,7 @@
 <template>
   <div class="page-wrap page-wrap-account">
-    <tool-bar
-      :text="['account_name', 'email', 'ad_name', 'campaign_name']"
-      :label="['账户名', '邮箱', '计划名称', '广告组名称']"
-    >
+    <tool-bar :text="['account_name', 'email', 'ad_name', 'campaign_name']"
+              :label="['账户名', '邮箱', '计划名称', '广告组名称']">
       <template #picker>
         <p class="label">日期</p>
         <a-range-picker />
@@ -15,15 +13,21 @@
         </a-select>
       </div> -->
     </tool-bar>
-    <a-table
-      :columns="columns"
-      :data-source="list"
-      :pagination="tablePageOptions"
-      :loading="loading.value"
-      @change="handleTableChange"
-      rowKey="id"
-      :scroll="{ x: 2500 }"
-    ></a-table>
+    <a-table :columns="columns"
+             :data-source="list"
+             :pagination="tablePageOptions"
+             :loading="loading.value"
+             @change="handleTableChange"
+             rowKey="id"
+             :scroll="{ x: 2500 }"></a-table>
+
+    <a-modal title="Title"
+             :visible="visible"
+             :confirm-loading="confirmLoading"
+             @ok="handleOk"
+             @cancel="handleCancel">
+      <p>{{ ModalText }}</p>
+    </a-modal>
   </div>
 </template>
 
@@ -52,6 +56,8 @@ const PutAdPlan = defineComponent({
       list: ref<ADPlanItem[]>([]),
       inSearching: false,
       loading,
+      visible: false,
+      confirmLoading: false,
       tablePageOptions,
       columns: TableColumnOfPutAdPlan,
     });
@@ -78,7 +84,7 @@ const PutAdPlan = defineComponent({
           page: 1,
         });
         state.list = data.list;
-        meta = data.meta;
+        meta.value = data.meta;
       } catch (e) {
         console.log(e);
       } finally {
@@ -89,14 +95,19 @@ const PutAdPlan = defineComponent({
     getADPlanlist().then((res) => {
       state.list = res.data.list;
     });
+
+    const handleOk = () => {};
+
+    const handleCancel = () => {};
+
     const handleTableChange = (pagination: PageOptions) => {
       const { current, pageSize, total } = pagination;
       getADPlanlist({ page: current }).then((res) => {
         state.list = res.data.list;
-        meta = res.data.meta;
+        meta.value = res.data.meta;
       });
     };
-    return { ...toRefs(state), handleTableChange };
+    return { ...toRefs(state), handleTableChange, handleOk, handleCancel };
   },
 });
 

+ 0 - 149
src/views/put/put-book.tsx

@@ -1,149 +0,0 @@
-import { defineComponent, reactive, ref } from "vue";
-
-import ToolBar from "@/components/tool-bar/index.vue";
-
-import usePagination from "@/hooks/usePagination";
-import useFormLayout from "@/hooks/useFormLayout";
-import useDebounceFn from "@/hooks/useDebounceFn";
-
-import { getDeliveryBookList, getBooksByName } from "@/api";
-import { TableColumnOfPutBooks } from "../_pageOptions/table-put";
-import { IBookSearchResult, IDeliveryBook } from "@/types/api";
-
-// TODO 结构待优化
-
-const PutBooks = defineComponent({
-  components: {
-    ToolBar,
-  },
-  setup() {
-    let { loading, meta, tablePageOptions } = usePagination();
-
-    const { labelCol, wrapperCol } = useFormLayout();
-
-    const state = reactive({
-      inSearching: false,
-      open: false,
-      list: ref<IDeliveryBook[]>([]),
-      columns: TableColumnOfPutBooks,
-    });
-
-    const addFormState = reactive({
-      official_id: 0,
-      book: ref<Partial<IBookSearchResult>>({}),
-      platforms: 0,
-      books: ref<IBookSearchResult[]>([]),
-    });
-
-    const onSearch = async (fields: Record<string, string>) => {
-      try {
-        const { official_name, book_name } = fields;
-        const { data } = await getDeliveryBookList({
-          official_name,
-          book_name,
-          page: 1,
-        });
-        state.list = data.list;
-        meta = data.meta;
-      } catch (e) {
-        console.log(e);
-      } finally {
-        state.inSearching = false;
-      }
-    };
-
-    // 添加书籍弹窗
-    const renderAddForm = () => {
-      const onBookSearch = useDebounceFn(async (keywords: string) => {
-        if (!keywords) return;
-        const { data } = await getBooksByName(keywords);
-        addFormState.books.push(...data.list);
-      }, 500);
-
-      const onBookCheck = (value: number, options: any) => {
-        addFormState.book = options.key;
-      };
-
-      const ok = () => {
-        console.log("ok");
-      };
-
-      const renderSearchBooksList = () => {
-        return addFormState.books.map((book) => (
-          <a-select-option value={book.bid} key={book}>
-            {book.name}
-          </a-select-option>
-        ));
-      };
-
-      return (
-        <a-modal
-          title="投放书籍添加"
-          width="400px"
-          v-model={[state.open, "visible"]}
-          onOk={ok}
-        >
-          <a-form
-            model={addFormState}
-            labelCol={labelCol}
-            wrapperCol={wrapperCol}
-          >
-            <a-form-item label="公众号名称">
-              <a-select v-model={[addFormState.official_id, "value"]}>
-                <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="书籍">
-              <a-select
-                show-search
-                placeholder="请输入要搜索的书名"
-                not-found-content="暂无数据"
-                default-active-first-option={false}
-                filter-option={false}
-                show-arrow={false}
-                value={addFormState.book.bid}
-                onSearch={onBookSearch}
-                onChange={onBookCheck}
-              >
-                {renderSearchBooksList()}
-              </a-select>
-            </a-form-item>
-            <a-form-item label="流量平台">
-              <a-select v-model={[addFormState.platforms, "value"]}>
-                <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>
-        </a-modal>
-      );
-    };
-
-    return () => (
-      <div class="page-wrap page-wrap-put-books">
-        <tool-bar
-          text={["official_name", "book_name"]}
-          label={["公众号名称", "书名"]}
-          v-model={[state.inSearching, "loading"]}
-          onConfirm={onSearch}
-        />
-        <div class="operator-bar">
-          <a-button type="primary" onClick={() => (state.open = true)}>
-            添加
-          </a-button>
-        </div>
-        <a-table
-          row-key="id"
-          pagination={tablePageOptions}
-          loading={loading.value}
-          columns={state.columns}
-          data-source={state.list}
-        ></a-table>
-        {renderAddForm()}
-      </div>
-    );
-  },
-});
-
-export default PutBooks;

+ 199 - 0
src/views/put/put-book.vue

@@ -0,0 +1,199 @@
+<template>
+  <div class="page-wrap page-wrap-put-books">
+    <tool-bar :text="['official_name', 'book_name']"
+              :label="['公众号名称', '书名']"
+              v-model:loading="searching"
+              @confirm="onSearch" />
+    <div class="operator-bar">
+      <a-button type="primary"
+                @click="open = true">添加</a-button>
+    </div>
+    <a-table row-key="id"
+             :pagination="tablePageOptions"
+             :loading="loading"
+             :columns="columns"
+             :data-source="list"></a-table>
+    <!-- 添加 -->
+    <a-modal title="投放书籍添加"
+             width="400px"
+             v-model:visible="open"
+             :confirm-loading="addFormState.inConfirm"
+             :after-close="onReset"
+             @ok="onAdd">
+      <a-form :model="addFormState"
+              :labelCol="labelCol"
+              :wrapperCol="wrapperCol">
+        <a-form-item label="公众号名称">
+          <a-select v-model:value="addFormState.official_id">
+            <a-select-option v-for="official in addFormState.officials"
+                             :key="official.value"
+                             :value="official.value">
+              {{official.label}}
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+        <a-form-item label="书籍">
+          <a-select show-search
+                    placeholder="请输入要搜索的书名"
+                    not-found-content="暂无数据"
+                    :default-active-first-option="false"
+                    :filter-option="false"
+                    :show-arrow="false"
+                    :value="addFormState.book.bid"
+                    @search="onBookSearch"
+                    @change="onBookCheck">
+            <a-select-option v-for="book in addFormState.books"
+                             :value="book.bid"
+                             :key="book">
+              {{book.name}}
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+        <a-form-item label="流量平台">
+          <a-select v-model:value="addFormState.platform">
+            <a-select-option v-for="platform in addFormState.platforms"
+                             :key="platform.value"
+                             :value="platform.value">
+              {{platform.label}}
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+      </a-form>
+    </a-modal>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  reactive,
+  ref,
+  computed,
+  toRefs,
+  onMounted,
+} from "vue";
+
+import ToolBar from "@/components/tool-bar/index.vue";
+
+import useApp from "@/hooks/useApp";
+import usePagination from "@/hooks/usePagination";
+import useFormLayout from "@/hooks/useFormLayout";
+import useDebounceFn from "@/hooks/useDebounceFn";
+import useValidate from "@/hooks/useValidate";
+
+import { getDeliveryBookList, getBooksByName, addDeliveryBook } from "@/api";
+import { TableColumnOfPutBooks } from "../_pageOptions/table-put";
+import { IBookSearchResult, IDeliveryBook } from "@/types/api";
+
+const PutBooks = defineComponent({
+  components: {
+    ToolBar,
+  },
+  setup() {
+    let { loading, meta, tablePageOptions } = usePagination();
+
+    const { store } = useApp();
+    const { labelCol, wrapperCol } = useFormLayout();
+
+    const state = reactive({
+      searching: false,
+      open: false,
+      list: ref<IDeliveryBook[]>([]),
+      columns: TableColumnOfPutBooks,
+      official_name: "",
+      book_name: "",
+    });
+
+    const addFormState = reactive({
+      officials: computed(() => store.getters.officials),
+      platforms: computed(() => store.getters.platforms),
+      official_id: "",
+      book: ref<Partial<IBookSearchResult>>({}),
+      platform: "",
+      books: ref<IBookSearchResult[]>([]),
+      inConfirm: false,
+    });
+
+    const onSearch = (fields: Record<string, string>) => {
+      const { official_name, book_name } = fields;
+      state.official_name = official_name;
+      state.book_name = book_name;
+      onBookLoaded();
+    };
+
+    const onBookLoaded = async (query?: { page: 1 }) => {
+      try {
+        loading.value = true;
+        const { official_name, book_name } = state;
+        const { data } = await getDeliveryBookList({
+          official_name,
+          book_name,
+          page: query?.page ?? 1,
+        });
+        state.list = data.list;
+        meta.value = data.meta;
+      } catch (error) {
+        console.log("on get books list error");
+      } finally {
+        state.searching = false;
+      }
+    };
+
+    // 添加书籍弹窗
+    const onBookSearch = useDebounceFn(async (keywords: string) => {
+      if (!keywords) return;
+      const { data } = await getBooksByName(keywords);
+      addFormState.books.push(...data.list);
+    }, 500);
+
+    const onBookCheck = (value: number, options: any) => {
+      addFormState.book = options.key;
+    };
+
+    onMounted(onBookLoaded);
+
+    return {
+      ...toRefs(state),
+      addFormState,
+      loading,
+      tablePageOptions,
+      labelCol,
+      wrapperCol,
+      onBookSearch,
+      onSearch,
+      onBookCheck,
+    };
+  },
+  methods: {
+    async onAdd() {
+      try {
+        // TODO 没做字段校验 字段校验封装
+        this.addFormState.inConfirm = true;
+        const { official_id, platform, book } = this.addFormState;
+        // useValidate({ official_id, platform });
+        await addDeliveryBook(
+          useValidate({
+            delivery_bid: book.id!,
+            official_id,
+            platform,
+          })
+        );
+        this.open = false;
+        this.$message.success("添加成功");
+      } catch (error) {
+        console.log("error while add delivery book");
+        error.message && this.$message.error(error.message);
+      } finally {
+        this.addFormState.inConfirm = false;
+      }
+    },
+    onReset() {
+      this.addFormState.official_id = "";
+      this.addFormState.platform = "";
+      this.addFormState.book = {};
+    },
+  },
+});
+
+export default PutBooks;
+</script>

+ 4 - 3
vue.config.js

@@ -1,4 +1,3 @@
-const webpack = require("webpack");
 const prodConfig = require("./prod.config");
 
 module.exports = {
@@ -36,7 +35,7 @@ module.exports = {
     devtool: "source-map",
     externals: prodConfig.externals,
     optimization: prodConfig.optimization,
-    plugins: [new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)],
+    plugins: prodConfig.plugins,
     resolve: {
       extensions: [".js", ".vue", ".json", ".ts"],
     },
@@ -50,10 +49,12 @@ module.exports = {
     config.plugins.delete("preload");
     if (process.env.NODE_ENV === "production") {
       // config.entry("index").add("babel-polyfill");
+      prodConfig.uploadAssetsToOSS(config);
+      // prodConfig.assetsGzip(config);
       config.plugin("html").tap((args) => {
         // 加上属性引号
         args[0].minify.removeAttributeQuotes = false;
-        // args[0].cdn = cdns.build;
+        args[0].cdn = prodConfig.cdns.build;
         return args;
       });
     }

+ 69 - 3
yarn.lock

@@ -1088,6 +1088,13 @@
   resolved "https://registry.npm.taobao.org/@types/q/download/@types/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
   integrity sha1-FZJUFOCtLNdlv+9YhC9+JqesyyQ=
 
+"@types/qrcode@^1.3.5":
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.3.5.tgz#9c97cc2875f03e2b16a0d89856fc48414e380c38"
+  integrity sha512-92QMnMb9m0ErBU20za5Eqtf4lzUcSkk5w/Cz30q5qod0lWHm2loztmFs2EnCY06yT51GY1+m/oFq2D8qVK2Bjg==
+  dependencies:
+    "@types/node" "*"
+
 "@types/qs@*":
   version "6.9.5"
   resolved "https://registry.npm.taobao.org/@types/qs/download/@types/qs-6.9.5.tgz?cache=0&sync_timestamp=1600295937317&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fqs%2Fdownload%2F%40types%2Fqs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b"
@@ -2117,6 +2124,11 @@ base64-js@^1.0.2:
   resolved "https://registry.npm.taobao.org/base64-js/download/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
   integrity sha1-WOzoy3XdB+ce0IxzarxfrE2/jfE=
 
+base64-js@^1.3.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
 base@^0.11.1:
   version "0.11.2"
   resolved "https://registry.npm.taobao.org/base/download/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
@@ -2339,7 +2351,25 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4
     escalade "^3.1.1"
     node-releases "^1.1.65"
 
-buffer-from@^1.0.0:
+buffer-alloc-unsafe@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
+  integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
+
+buffer-alloc@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
+  integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
+  dependencies:
+    buffer-alloc-unsafe "^1.1.0"
+    buffer-fill "^1.0.0"
+
+buffer-fill@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
+  integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
+
+buffer-from@^1.0.0, buffer-from@^1.1.1:
   version "1.1.1"
   resolved "https://registry.npm.taobao.org/buffer-from/download/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
   integrity sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=
@@ -2368,6 +2398,14 @@ buffer@^4.3.0:
     ieee754 "^1.1.4"
     isarray "^1.0.0"
 
+buffer@^5.4.3:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+  dependencies:
+    base64-js "^1.3.1"
+    ieee754 "^1.1.13"
+
 builtin-modules@^1.1.1:
   version "1.1.1"
   resolved "https://registry.npm.taobao.org/builtin-modules/download/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
@@ -3558,6 +3596,11 @@ digest-header@^0.0.1:
   dependencies:
     utility "0.1.11"
 
+dijkstrajs@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.1.tgz#d3cd81221e3ea40742cfcde556d4e99e98ddc71b"
+  integrity sha1-082BIh4+pAdCz83lVtTpnpjdxxs=
+
 dir-glob@^2.0.0, dir-glob@^2.2.2:
   version "2.2.2"
   resolved "https://registry.npm.taobao.org/dir-glob/download/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
@@ -4911,7 +4954,7 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
   dependencies:
     postcss "^7.0.14"
 
-ieee754@^1.1.4:
+ieee754@^1.1.13, ieee754@^1.1.4:
   version "1.2.1"
   resolved "https://registry.npm.taobao.org/ieee754/download/ieee754-1.2.1.tgz?cache=0&sync_timestamp=1603838418666&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fieee754%2Fdownload%2Fieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
   integrity sha1-jrehCmP/8l0VpXsAFYbRd9Gw01I=
@@ -5367,6 +5410,11 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
   resolved "https://registry.npm.taobao.org/isarray/download/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
   integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
 
+isarray@^2.0.1:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
+  integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+
 isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.npm.taobao.org/isexe/download/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -6793,6 +6841,11 @@ platform@^1.3.1:
   resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7"
   integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==
 
+pngjs@^3.3.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
+  integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
+
 pnp-webpack-plugin@^1.6.4:
   version "1.6.4"
   resolved "https://registry.npm.taobao.org/pnp-webpack-plugin/download/pnp-webpack-plugin-1.6.4.tgz?cache=0&sync_timestamp=1592843223538&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpnp-webpack-plugin%2Fdownload%2Fpnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149"
@@ -7302,6 +7355,19 @@ q@^1.1.2:
   resolved "https://registry.npm.taobao.org/q/download/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
   integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
 
+qrcode@^1.4.4:
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83"
+  integrity sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==
+  dependencies:
+    buffer "^5.4.3"
+    buffer-alloc "^1.2.0"
+    buffer-from "^1.1.1"
+    dijkstrajs "^1.0.1"
+    isarray "^2.0.1"
+    pngjs "^3.3.0"
+    yargs "^13.2.4"
+
 qs@6.7.0:
   version "6.7.0"
   resolved "https://registry.npm.taobao.org/qs/download/qs-6.7.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fqs%2Fdownload%2Fqs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@@ -9430,7 +9496,7 @@ yargs-parser@^18.1.2:
     camelcase "^5.0.0"
     decamelize "^1.2.0"
 
-yargs@^13.3.2:
+yargs@^13.2.4, yargs@^13.3.2:
   version "13.3.2"
   resolved "https://registry.npm.taobao.org/yargs/download/yargs-13.3.2.tgz?cache=0&sync_timestamp=1602805619467&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fyargs%2Fdownload%2Fyargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
   integrity sha1-rX/+/sGqWVZayRX4Lcyzipwxot0=