bianjiang 1 سال پیش
والد
کامیت
324c5fb9ab

+ 27 - 0
src/components/Provider/Loading.less

@@ -0,0 +1,27 @@
+.que-loading {
+  position: fixed;
+  z-index: 9999;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+
+  .que-loading-box {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    background: rgba(0, 0, 0, 0.4);
+    padding: 20px;
+    border-radius: 6px;
+    color: #fff;
+
+    .ant-spin-dot-item {
+      background-color: #fff;
+    }
+  }
+}

+ 1 - 1
src/components/Provider/Loading.tsx

@@ -1,6 +1,6 @@
 import { Spin } from "ant-design-vue";
 import { defineComponent, reactive } from "vue";
-
+import "./Loading.less";
 const state = reactive({
   visible: false,
   tip: "",

+ 29 - 5
src/controllers/ListController.ts

@@ -1,4 +1,28 @@
+import { computed, reactive } from "vue";
 
+export class StateRoot {
+  protected computed<F extends (state: Omit<this, "reactive">) => any>(fn: F) {
+    return fn as ReturnType<F>;
+  }
+  reactive = () => {
+    return createReactive(this) as Omit<this, "reactive">;
+  };
+}
+function createReactive(obj: any) {
+  const data: any = {};
+  for (const name in obj) {
+    if (name === "reactive" || name === "computed" || name === "proxy")
+      continue;
+    const objItem: any = obj[name];
+    if (objItem instanceof Function) {
+      data[name] = computed(() => objItem(state));
+    } else {
+      data[name] = objItem;
+    }
+  }
+  const state: any = reactive(data);
+  return state;
+}
 
 class ListItemBase {
   _id?: string = "";
@@ -22,9 +46,9 @@ class StatePageList<T extends ListItemBase, Q> extends StateRoot {
 /**
  * 分页列表控制器
  */
-export class PageListController<T extends ListItemBase, Q> {
+export class ListController<T extends ListItemBase, Q> {
   state = new StatePageList<T, Q>().reactive();
-  http: any;
+  request: any;
   dataList: any[] = [];
   httpGet?: (
     page: number,
@@ -40,7 +64,7 @@ export class PageListController<T extends ListItemBase, Q> {
   listFilter: ((item: T) => T) | null = null;
 
   constructor(
-    http: any,
+    request: any,
     get?: () => (
       page: number,
       size: number,
@@ -48,7 +72,7 @@ export class PageListController<T extends ListItemBase, Q> {
       fields?: string
     ) => Promise<any>
   ) {
-    this.http = http;
+    this.request = request;
 
     if (get) {
       this.httpGet = get();
@@ -72,7 +96,7 @@ export class PageListController<T extends ListItemBase, Q> {
     let uri = `${prefix}/${type}`;
     if (type == "delete" || type == "detail") uri += `/${data}`;
 
-    return this.http.request(uri, config);
+    return this.request(uri, config);
   }
   setCrudPrefix(prefix: string) {
     this.httpGet = (

+ 131 - 0
src/controllers/UploadController.ts

@@ -0,0 +1,131 @@
+import { set } from "lodash";
+
+export class UploadController {
+  request: any;
+  blobURLMaps = new Map<string, any>();
+  constructor(request: any) {
+    this.request = request;
+  }
+  getBlobURLName(url: string) {
+    return this.blobURLMaps.get(url)?.name;
+  }
+  createObjectURL(obj: Blob): string {
+    const url = URL.createObjectURL(obj);
+    this.blobURLMaps.set(url, obj);
+    return url;
+  }
+  revokeObjectURL(url: string) {
+    URL.revokeObjectURL(url);
+    this.blobURLMaps.delete(url);
+  }
+  selectFile(opts?: { accept?: string; multiple?: boolean }): Promise<File[]> {
+    return new Promise((resolve, reject) => {
+      const fileInput = document.createElement("input");
+      fileInput.type = "file";
+      fileInput.accept = opts?.accept || "images/*";
+      fileInput.multiple = opts?.multiple || false;
+
+      fileInput.onchange = function (this: any) {
+        resolve([...this.files]);
+      };
+
+      window.addEventListener(
+        "focus",
+        () => {
+          setTimeout(() => {
+            reject(console.warn("select file cancel"));
+          }, 300);
+        },
+        { once: true }
+      );
+
+      fileInput.click();
+    });
+  }
+  async uploadBlobImages(
+    data: object,
+    options?: {
+      onProgress?: () => void;
+    }
+  ) {
+    const { onProgress } = options || {};
+    const todoMap = new Map<string, string>();
+    addBlobToList(data);
+    const todoList = todoMap.entries();
+
+    let err = "";
+
+    for (const item of todoList) {
+      const [blobUrl, path] = item;
+      const file = this.blobURLMaps.get(blobUrl);
+      if (file) {
+        const paths = path.split(",");
+        onProgress?.();
+        const ret = await this.uploadImage(file);
+
+        if (ret.url) {
+          paths.forEach((p) => {
+            set(data, p, ret.url);
+          });
+          this.revokeObjectURL(blobUrl);
+        } else {
+          err = ret.error || `上传文件失败[${path}]`;
+          break;
+        }
+      } else {
+        err = `上传文件为空[${path}]`;
+      }
+    }
+    if (err) throw err;
+    function addBlobToList(data: any, path = "") {
+      if (data instanceof Object) {
+        if (data instanceof Array) {
+          data.forEach((item, i) => {
+            addBlobToList(item, path ? path + "." + i : i.toString());
+          });
+        } else {
+          Object.entries(data).forEach(([key, value]) => {
+            addBlobToList(value, path ? path + "." + key : key);
+          });
+        }
+      }
+      if (typeof data === "string" && /^blob:/.test(data)) {
+        if (todoMap.has(data)) {
+          path = todoMap.get(data) + "," + path;
+        }
+        todoMap.set(data, path);
+      }
+    }
+  }
+  async uploadImage(file: File): Promise<{ url?: string; error?: string }> {
+    const fromData = new FormData();
+    fromData.append("image", file);
+    const ret = await this.request("/upload/image", {
+      method: "POST",
+      data: fromData,
+      headers: {
+        "Content-Type": "multipart/form-data",
+      },
+    });
+    if (ret.errorNo != 200) {
+      return { error: "上传失败!" };
+    }
+    const url = ret.result.url;
+    return { url };
+  }
+  getBlobURLExt(url: string) {
+    const blob = this.blobURLMaps.get(url);
+    let ext = "unkown";
+    if (blob) {
+      const exp = /^.+\.(.+)$/;
+      if (blob.name && exp.test(blob.name)) {
+        ext = (exp.exec(blob.name) as any)[1];
+      } else if (blob.type) {
+        ext = blob.type.split("/").pop();
+      }
+      return ext.toLowerCase();
+    } else {
+      return ext;
+    }
+  }
+}

+ 59 - 74
src/modules/admin/components/CategoryModal.tsx

@@ -1,9 +1,11 @@
-import { Button, Col, Form, Input, Row } from "ant-design-vue";
-import { defineComponent, onMounted, reactive, ref } from "vue";
-import { any } from "vue-types";
 import Modal from "@/components/Provider/Modal";
 import { css } from "@linaria/core";
-
+import { Button, Checkbox, Form, Input, Select } from "ant-design-vue";
+import { defineComponent, onMounted, reactive, ref } from "vue";
+import { any } from "vue-types";
+import UploadImage from "./UploadImage";
+import { uploader } from "../objects";
+import loading from "@/components/Provider/Loading";
 const layout = {
   wrapperCol: { span: 24 },
 };
@@ -18,10 +20,10 @@ export default defineComponent({
       formData: {
         ...{
           name: "",
-          designer: "",
-          thumbnail: "",
-          compCounts: 0,
-          components: [],
+          cover: "",
+          sort: 0,
+          type: "list",
+          isHome: false,
         },
         ...props.data,
       },
@@ -30,36 +32,28 @@ export default defineComponent({
     onMounted(() => {});
 
     const rules = reactive({
-      name: [
-        { required: true, message: "名称不能为空", trigger: "change" },
-        {
-          min: 2,
-          max: 20,
-          message: "长度为2~20位字符",
-          trigger: "change",
-        },
-      ],
+      name: [{ required: true, message: "名称不能为空", trigger: "change" }],
+      cover: [{ required: false }],
+      isHome: [{ required: false }],
+      type: [{ required: true, message: "分类", trigger: "change" }],
     });
 
-    const loading = ref(false);
     const { validate, validateInfos } = Form.useForm(formState.formData, rules);
 
     function submit() {
       validate().then(async () => {
-        // loading.value = true;
-        // let res;
-        // formState.formData.compCounts = formState.formData.components.length;
-        // if (isCreate) {
-        //   res = await controls.packList.addItem(formState.formData);
-        // } else {
-        //   res = await controls.packList.saveItem(formState.formData);
-        // }
-        // loading.value = false;
-        // if (res.errorNo != 200) return;
-
-        modal.submit();
+        await uploader.uploadBlobImages(formState.formData, {
+          onProgress() {
+            loading.show("上传中");
+          },
+        });
+        loading.hidden();
+        modal.submit(formState.formData);
       });
     }
+    const changeBanner = (v: string) => {
+      formState.formData.cover = v;
+    };
 
     return () => {
       return (
@@ -67,51 +61,42 @@ export default defineComponent({
           <div class={"edit_content"}>
             <div class={"form_content"}>
               <Form {...layout} class={EditFormStyle} onSubmit={submit}>
-                <Row>
-                  <Col span={22}>
-                    <Row>
-                      <Col>
-                        <Form.Item style={{ marginBottom: 0 }}>
-                          <div class={"thumb"}></div>
-                        </Form.Item>
-                      </Col>
-                      <Col class={"pl-10px"}>
-                        <Form.Item {...validateInfos.name}>
-                          <Input
-                            placeholder={"请输入包装名称"}
-                            v-model={[formState.formData.name, "value"]}
-                            maxlength={30}
-                          />
-                        </Form.Item>
-
-                        <Form.Item {...validateInfos.designer}>
-                          <Input
-                            placeholder={"请输入设计师名字"}
-                            v-model={[formState.formData.designer, "value"]}
-                            maxlength={10}
-                          />
-                        </Form.Item>
-                      </Col>
-                    </Row>
-                  </Col>
-                  <Col span={2}>
-                    <Form.Item
-                      style={{ marginBottom: 0, textAlign: "right" }}
-                      wrapperCol={{ span: 24 }}
-                    >
-                      <Button
-                        type="primary"
-                        htmlType="submit"
-                        loading={loading.value}
-                      >
-                        保存
-                      </Button>
-                    </Form.Item>
-                  </Col>
-                </Row>
+                <Form.Item {...validateInfos.cover}>
+                  <UploadImage
+                    data={formState.formData.cover}
+                    text={"上传分类banner图"}
+                    onChange={changeBanner}
+                  ></UploadImage>
+                </Form.Item>
+                <Form.Item {...validateInfos.name} label={"分类名称"}>
+                  <Input
+                    placeholder={"请输入分类名称"}
+                    v-model={[formState.formData.name, "value"]}
+                    maxlength={30}
+                  />
+                </Form.Item>
+                <Form.Item {...validateInfos.type} label={"分类类型"}>
+                  <Select v-model={[formState.formData.type, "value"]}>
+                    <Select.Option value="list">列表</Select.Option>
+                    <Select.Option value="detail">详情</Select.Option>
+                    <Select.Option value="download">下载</Select.Option>
+                  </Select>
+                </Form.Item>
+                <Form.Item {...validateInfos.isHome} label={"是否首页展示"}>
+                  <Checkbox
+                    v-model={[formState.formData.isHome, "checked"]}
+                  ></Checkbox>
+                </Form.Item>
+                <Form.Item
+                  style={{ marginBottom: 0 }}
+                  wrapperCol={{ span: 24 }}
+                >
+                  <Button type="primary" htmlType="submit" block>
+                    保存
+                  </Button>
+                </Form.Item>
               </Form>
             </div>
-            <div class={"comp_content mt-20px"}></div>
           </div>
         </div>
       );
@@ -119,10 +104,10 @@ export default defineComponent({
   },
 });
 const EditStyle = css`
+  width: 400px;
   .edit_content {
     display: flex;
     flex-direction: column;
-    padding: 24px;
     height: 100%;
   }
   .comp_content {

+ 72 - 0
src/modules/admin/components/UploadImage.tsx

@@ -0,0 +1,72 @@
+import { defineComponent } from "vue";
+import { any, string } from "vue-types";
+import { PlusOutlined } from "@ant-design/icons-vue";
+import { css } from "@linaria/core";
+import Image from "@/components/Image";
+import { uploader } from "../objects";
+
+import { message } from "ant-design-vue";
+export default defineComponent({
+  props: {
+    text: string(),
+    data: string(),
+  },
+  emits: ["change"],
+  setup(props, { emit }) {
+    const uploadImage = async () => {
+      const [file] = await uploader.selectFile({ accept: "jpg,png,jpeg" });
+      if (file.size > 5 * 1024 * 1024) {
+        return message.warn("图片不能超过5M!");
+      }
+      emit("change", uploader.createObjectURL(file));
+    };
+
+    return () => (
+      <div class={ImageStyle} onClick={uploadImage}>
+        <div class={"wapper"}>
+          {props.data ? (
+            <Image src={props.data} />
+          ) : (
+            <div class={"no_value"}>
+              <PlusOutlined />
+              <div class={"up_txt"}>{props.text}</div>
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  },
+});
+const ImageStyle = css`
+  width: 100%;
+  height: 100%;
+  color: #343434;
+  background-color: #f2f2f2;
+  font-size: 24px;
+  border-radius: 2px;
+  cursor: pointer;
+  .wapper {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  .no_value {
+    padding: 24px 0;
+    display: inline-flex;
+    flex-direction: column;
+    align-items: center;
+    .up_txt {
+      margin-top: 10px;
+      font-size: 14px;
+      color: #343434;
+    }
+  }
+  img {
+    width: 100%;
+    height: 100%;
+    border-radius: 2px;
+    object-fit: cover;
+  }
+`;

+ 14 - 0
src/modules/admin/https.ts

@@ -0,0 +1,14 @@
+import { request } from "./objects";
+
+export const uploadImage = (data: any) => {
+  return request("/upload/image", {
+    method: "POST",
+    data,
+  });
+};
+export const uploadFile = (data: any) => {
+  return request("/upload/file", {
+    method: "POST",
+    data,
+  });
+};

+ 4 - 25
src/modules/admin/module/category/actions.tsx

@@ -2,31 +2,10 @@ import Modal from "@/components/Provider/Modal";
 import CategoryModal from "../../components/CategoryModal";
 
 export const categoryActions = {
-  // categoryItem的【添加】
-  async addCategoryItem(pid = "top") {
-    const base: any = await Modal.show(<CategoryModal data={{}} />, {
-      title: "添加分类",
+  async EditCategoryItem(item?: any) {
+    const base: any = await Modal.show(<CategoryModal data={item} />, {
+      title: item ? "编辑分类" : "添加分类",
     });
-    const item = { pid, ...base };
-    // return this.store.addCategory(item);
-  },
-
-  // categoryItem的【重命名】
-  async renameCategoryItem(item: any) {
-    // const name = await queenApi.showInput({
-    //   title: "分类名称",
-    //   defaultValue: item.name,
-    //   placeholder: "请输入分类名称",
-    // });
-    // return this.store.updateCategory({ id: item.id, name });
-  },
-
-  // categoryItem的【删除】
-  async deleteCategoryItem(item: any) {
-    // await queenApi.showConfirm({
-    //   type: "danger",
-    //   content: "确定删除该分类?",
-    // });
-    // return this.store.deleteCategory(item);
+    return base;
   },
 };

+ 1 - 1
src/modules/admin/module/category/https.ts

@@ -1,4 +1,4 @@
-import { request } from "../http";
+import { request } from "../../objects";
 export const https = {
   createCategory(data: any) {
     return request("/category/create", {

+ 43 - 13
src/modules/admin/module/category/index.ts

@@ -1,7 +1,11 @@
 import { CategoryItem } from "@/typings/asset";
-
 import { defineStore } from "pinia";
 import { categoryActions } from "./actions";
+import { ListController } from "@/controllers/ListController";
+import { request } from "../../objects";
+import loading from "@/components/Provider/Loading";
+import { message } from "ant-design-vue";
+import Modal from "@/components/Provider/Modal";
 function setMapItem(
   id: string,
   item: any,
@@ -10,32 +14,58 @@ function setMapItem(
   itemMaps.set(id, item);
   if (item.children instanceof Array) {
     return item.children.forEach((d: any) => {
-      d.pid = item.id;
-      setMapItem(d.id, d, itemMaps);
+      d.pid = item._id;
+      setMapItem(d._id, d, itemMaps);
     });
   }
 }
 export const useCategory = defineStore("category", {
   state: () => ({
-    categories: [],
+    // categories: [],
+    listController: new ListController(request),
   }),
   getters: {
     categories(state) {
-      return state.categories;
+      return state.listController.state.list;
     },
-    categoryMap(state) {
+    categoryMap() {
       const itemMaps = new Map<string, CategoryItem>();
-      setMapItem("_", { children: state.categories }, itemMaps);
+      setMapItem("_", { children: this.categories }, itemMaps);
       return itemMaps;
     },
   },
   actions: {
-    initCategories() {},
-    addCategoryItem() {
-      categoryActions.addCategoryItem();
+    async initCategories() {
+      this.listController.setCrudPrefix("/category");
+      await this.listController.loadPage(1, 100);
+    },
+    async addCategoryItem(pid = "top") {
+      const base = await categoryActions.EditCategoryItem();
+      const item = { pid, ...base };
+      loading.show("保存中");
+      await this.listController.addItem(item);
+      loading.hidden();
+      message.success("添加成功");
+    },
+    async updateCategoryItem(item: any) {
+      const base = await categoryActions.EditCategoryItem(item);
+      loading.show("保存中");
+      await this.listController.saveItem(base);
+      loading.hidden();
+      message.success("更新成功");
+    },
+    async deleteCategoryItem(item: any) {
+      const ok = await Modal.confirm({
+        title: "删除确认",
+        content: `删除后数据无法恢复,确认删除分类:${item.name}?`,
+        type: "danger",
+      });
+      if (ok) {
+        loading.show("删除中");
+        await this.listController.deleteItem(item._id);
+        loading.hidden();
+        message.success("删除成功");
+      }
     },
-    renameCategoryItem(item: any) {},
-
-    deleteCategoryItem(item: any) {},
   },
 });

+ 3 - 3
src/modules/admin/http.ts → src/modules/admin/objects/index.ts

@@ -1,3 +1,4 @@
+import { UploadController } from "@/controllers/UploadController";
 import { createRequest } from "@/utils/request";
 const token = localStorage.getItem("token");
 export const request = createRequest({
@@ -8,8 +9,7 @@ export const request = createRequest({
       req.headers["authorization"] = `Bearer ${token}`;
       return req;
     },
-    response: (res: any) => {
-     
-    },
+    response: (res: any) => {},
   },
 });
+export const uploader = new UploadController(request);

+ 0 - 1
src/utils/request.ts

@@ -11,7 +11,6 @@ export type RequestConfig = AxiosRequestConfig & {
 export function createRequest(defReqConfig: RequestConfig) {
   const { interceptors, ...httpConfig } = defReqConfig;
   const http = axios.create({ ...config, ...httpConfig });
-
   Object.values(interceptors || {})?.forEach((item: any) => {
     item.request && http.interceptors.request.use(item.request);
     item.response && http.interceptors.response.use(item.response);

+ 3 - 1
src/views/admin/App.tsx

@@ -1,10 +1,12 @@
 import { Provider } from "@/components/Provider";
-import { useAuth } from "@/modules/admin";
+import { useAuth, useCategory } from "@/modules/admin";
 import { defineComponent } from "vue";
 
 export default defineComponent(() => {
   const storeAuth = useAuth();
+  const storeCategory = useCategory();
   storeAuth.initAuth();
+  storeCategory.initCategories();
   return () => (
     <Provider>
       <router-view></router-view>

+ 4 - 3
src/views/admin/category/CategoryTree.tsx

@@ -25,14 +25,14 @@ export default defineComponent({
               <span class="item_tit">{item.name || "undefined"}</span>
             </div>
             <div class="item_btn">
-              {key != 3 && (
+              {key != 1 && (
                 <Button
                   class="tree_btn"
                   title="添加子分类"
                   icon={<PlusSquareOutlined />}
                   onClick={(e) => {
                     e.stopPropagation();
-                    emit("add", key + 1, item.id);
+                    emit("add", item._id);
                   }}
                 ></Button>
               )}
@@ -98,7 +98,9 @@ const TreeRoot = css`
       display: flex;
       align-items: center;
       .item_tit {
+        height: 32px;
         font-size: 14px;
+        line-height: 32px;
       }
     }
     &:hover {
@@ -113,7 +115,6 @@ const TreeRoot = css`
       .tree_btn {
         margin: 0 6px;
         min-width: auto;
-
         border: none;
         background-color: transparent;
         box-shadow: none;

+ 14 - 8
src/views/admin/category/index.tsx

@@ -8,7 +8,7 @@ export default defineComponent(() => {
   const categoryStore = useCategory();
   return () => (
     <Card class={Page}>
-      <PageHeader title={"分类管理"}>
+      <PageHeader title={"菜单管理"}>
         {{
           extra: () => {
             return (
@@ -16,21 +16,27 @@ export default defineComponent(() => {
                 type="primary"
                 onClick={() => categoryStore.addCategoryItem()}
               >
-                + 添加分类
+                + 添加菜单
               </Button>
             );
           },
         }}
       </PageHeader>
-      <CategoryTree
-        data={categoryStore.categoryMap.get("_")?.children}
-        onAdd={categoryStore.addCategoryItem}
-        onUpdate={categoryStore.renameCategoryItem}
-        onDelete={categoryStore.deleteCategoryItem}
-      />
+      <div class={"category_box"}>
+        <CategoryTree
+          data={categoryStore.categoryMap.get("_")?.children}
+          onAdd={categoryStore.addCategoryItem}
+          onUpdate={categoryStore.updateCategoryItem}
+          onDelete={categoryStore.deleteCategoryItem}
+        />
+      </div>
     </Card>
   );
 });
 const Page = css`
   height: 100%;
+  .category_box {
+    padding: 20px;
+    border: 1px solid #e5e5e5;
+  }
 `;

+ 1 - 1
src/views/admin/config/menus.ts

@@ -4,7 +4,7 @@ export const MenusConfig = [
     path: "/banner",
   },
   {
-    name: "分类管理",
+    name: "菜单管理",
     path: "/category",
   },
   {

+ 9 - 1
src/views/admin/router/index.ts

@@ -19,7 +19,15 @@ const router = createRouter({
           component: () => import("../category"),
         },
         {
-          path: "/intro",
+          path: "/typeList",
+          component: () => import("../intro"),
+        },
+        {
+          path: "/typeDetail",
+          component: () => import("../intro"),
+        },
+        {
+          path: "/typeDownload",
           component: () => import("../intro"),
         },
       ],