bianjiang 1 year ago
parent
commit
b15d31aebd

+ 18 - 8
src/modules/admin/components/CategoryModal.tsx

@@ -7,7 +7,8 @@ import UploadImage from "./UploadImage";
 import { uploader } from "../objects";
 import loading from "@/components/Provider/Loading";
 const layout = {
-  wrapperCol: { span: 24 },
+  labelCol: { span: 6 },
+  wrapperCol: { span: 18 },
 };
 
 export default defineComponent({
@@ -21,6 +22,7 @@ export default defineComponent({
         ...{
           name: "",
           cover: "",
+          subName: "",
           sort: 0,
           type: "list",
           isHome: false,
@@ -34,8 +36,9 @@ export default defineComponent({
     const rules = reactive({
       name: [{ required: true, message: "名称不能为空", trigger: "change" }],
       cover: [{ required: false }],
+      subName: [{ required: false }],
       isHome: [{ required: false }],
-      type: [{ required: true, message: "类", trigger: "change" }],
+      type: [{ required: true, message: "类型不能为空", trigger: "change" }],
     });
 
     const { validate, validateInfos } = Form.useForm(formState.formData, rules);
@@ -61,21 +64,28 @@ export default defineComponent({
           <div class={"edit_content"}>
             <div class={"form_content"}>
               <Form {...layout} class={EditFormStyle} onSubmit={submit}>
-                <Form.Item {...validateInfos.cover}>
+                <Form.Item {...validateInfos.cover} wrapperCol={{ span: 24 }}>
                   <UploadImage
                     data={formState.formData.cover}
-                    text={"上传分类banner图"}
+                    text={"上传菜单banner图"}
                     onChange={changeBanner}
                   ></UploadImage>
                 </Form.Item>
-                <Form.Item {...validateInfos.name} label={"分类名称"}>
+                <Form.Item {...validateInfos.name} label={"菜单名称"}>
                   <Input
-                    placeholder={"请输入分类名称"}
+                    placeholder={"请输入菜单名称"}
                     v-model={[formState.formData.name, "value"]}
                     maxlength={30}
                   />
                 </Form.Item>
-                <Form.Item {...validateInfos.type} label={"分类类型"}>
+                <Form.Item {...validateInfos.subName} label={"副标题"}>
+                  <Input
+                    placeholder={"请输入副标题"}
+                    v-model={[formState.formData.subName, "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>
@@ -104,7 +114,7 @@ export default defineComponent({
   },
 });
 const EditStyle = css`
-  width: 400px;
+  width: 500px;
   .edit_content {
     display: flex;
     flex-direction: column;

+ 1 - 0
src/modules/admin/index.ts

@@ -1,2 +1,3 @@
 export * from "./module/category";
 export * from "./module/auth";
+export * from "./module/article";

+ 39 - 0
src/modules/admin/module/article/index.ts

@@ -0,0 +1,39 @@
+import { ListController } from "@/controllers/ListController";
+import { defineStore } from "pinia";
+import { request } from "../../objects";
+import loading from "@/components/Provider/Loading";
+import { message } from "ant-design-vue";
+export const useArticle = defineStore("article", {
+  state: () => ({
+    listController: new ListController(request),
+  }),
+  getters: {},
+  actions: {
+    initArticle() {
+      this.listController.setCrudPrefix("/article");
+    },
+    async addOrUpdateArticle(item: any) {
+      loading.show("保存中");
+      let res = {} as any;
+      if (item._id) {
+        res = await this.listController.saveItem(item);
+      } else {
+        res = await this.listController.addItem(item);
+      }
+      loading.hidden();
+      if (res.errorNo != 200) {
+        message.success("保存失败");
+        return;
+      }
+      message.success("保存成功");
+    },
+    async getArticleDetail(item: any) {
+      const res = await this.listController.itemDetail(item._id);
+      if (res.errorNo != 200) {
+        message.warn("未查询到数据!");
+        return;
+      }
+      return res.result;
+    },
+  },
+});

+ 41 - 18
src/modules/admin/module/category/index.ts

@@ -6,38 +6,56 @@ 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,
-  itemMaps: Map<string, CategoryItem>
-) {
-  itemMaps.set(id, item);
-  if (item.children instanceof Array) {
-    return item.children.forEach((d: any) => {
-      d.pid = item._id;
-      setMapItem(d._id, d, itemMaps);
+
+function parseTreeData(categoryList: any) {
+  const list = [...categoryList];
+
+  let roots = list.filter((item) => item.pid == "top");
+  function findChild(root: CategoryItem, list: CategoryItem[]) {
+    root.children = [];
+    list.forEach((it) => {
+      if (it.pid == root._id) {
+        root.children?.push(it);
+      }
     });
+    root.children.forEach((child) => findChild(child, list));
+  }
+  roots.forEach((item) => findChild(item, list));
+
+  function parseTreeItem(item: CategoryItem) {
+    let ret: any = {
+      _id: item._id,
+      name: item.name,
+      type: item.type,
+    };
+    if (item.children?.length) {
+      ret.children = [];
+      item.children.forEach((it) => {
+        ret.children.push(parseTreeItem(it));
+      });
+    }
+    return ret;
   }
+  return roots.map((item) => parseTreeItem(item));
 }
 export const useCategory = defineStore("category", {
   state: () => ({
     // categories: [],
-    listController: new ListController(request),
+    listController: new ListController<CategoryItem, any>(request),
   }),
   getters: {
     categories(state) {
       return state.listController.state.list;
     },
-    categoryMap() {
-      const itemMaps = new Map<string, CategoryItem>();
-      setMapItem("_", { children: this.categories }, itemMaps);
-      return itemMaps;
+    categoryTree() {
+      const tree = parseTreeData(this.categories);
+      return tree;
     },
   },
   actions: {
     async initCategories() {
       this.listController.setCrudPrefix("/category");
-      await this.listController.loadPage(1, 100);
+      await this.listController.loadPage(1, 200);
     },
     async addCategoryItem(pid = "top") {
       const base = await categoryActions.EditCategoryItem();
@@ -48,7 +66,12 @@ export const useCategory = defineStore("category", {
       message.success("添加成功");
     },
     async updateCategoryItem(item: any) {
-      const base = await categoryActions.EditCategoryItem(item);
+      const res = await this.listController.itemDetail(item._id);
+      if (res.errorNo != 200) {
+        message.warn("未查询到数据!");
+        return;
+      }
+      const base = await categoryActions.EditCategoryItem(res.result);
       loading.show("保存中");
       await this.listController.saveItem(base);
       loading.hidden();
@@ -57,7 +80,7 @@ export const useCategory = defineStore("category", {
     async deleteCategoryItem(item: any) {
       const ok = await Modal.confirm({
         title: "删除确认",
-        content: `删除后数据无法恢复,确认删除分类:${item.name}?`,
+        content: `删除后数据无法恢复,确认删除菜单:${item.name}?`,
         type: "danger",
       });
       if (ok) {

+ 1 - 1
src/modules/admin/objects/index.ts

@@ -2,7 +2,7 @@ import { UploadController } from "@/controllers/UploadController";
 import { createRequest } from "@/utils/request";
 const token = localStorage.getItem("token");
 export const request = createRequest({
-  baseURL: "https://3dqueen.cloud/adhuaxi/v1",
+  baseURL: "https://www.infish.cn/adhuaxi/v1",
   interceptors: {
     request(req: any) {
       if (!req.headers) req.headers = {};

+ 1 - 1
src/styles/main.css

@@ -3,7 +3,7 @@
 #app {
   width: 100%;
   height: 100%;
-  background-color: #f5f5f5;
+  background-color: #f0f2f5;
 }
 
 .global-w {

+ 1 - 1
src/typings/asset.d.ts

@@ -8,7 +8,7 @@ declare interface LayoutMenu {
   children?: { title: string; url: string }[];
 }
 declare type CategoryItem = {
-  id: string;
+  _id: string;
   pid: string; // 分类的上层id // 默认为top
   name: string; //分类封面图
   cover: string; //分类名称

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

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

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

@@ -25,10 +25,10 @@ export default defineComponent({
               <span class="item_tit">{item.name || "undefined"}</span>
             </div>
             <div class="item_btn">
-              {key != 1 && (
+              {key != 2 && (
                 <Button
                   class="tree_btn"
-                  title="添加子分类"
+                  title="添加子菜单"
                   icon={<PlusSquareOutlined />}
                   onClick={(e) => {
                     e.stopPropagation();
@@ -39,7 +39,7 @@ export default defineComponent({
 
               <Button
                 class="tree_btn"
-                title="编辑分类"
+                title="编辑"
                 icon={<FormOutlined />}
                 onClick={(e) => {
                   e.stopPropagation();
@@ -48,7 +48,7 @@ export default defineComponent({
               ></Button>
               <Button
                 class="tree_btn"
-                title="删除分类"
+                title="删除"
                 icon={<DeleteOutlined />}
                 onClick={(e) => {
                   e.stopPropagation();
@@ -78,6 +78,9 @@ export default defineComponent({
   },
 });
 const TreeRoot = css`
+  .ant-tree-switcher {
+    line-height: 32px;
+  }
   .ant-tree-node-selected {
     .tree_item {
       .item_btn {

+ 2 - 2
src/views/admin/category/index.tsx

@@ -24,7 +24,7 @@ export default defineComponent(() => {
       </PageHeader>
       <div class={"category_box"}>
         <CategoryTree
-          data={categoryStore.categoryMap.get("_")?.children}
+          data={categoryStore.categoryTree}
           onAdd={categoryStore.addCategoryItem}
           onUpdate={categoryStore.updateCategoryItem}
           onDelete={categoryStore.deleteCategoryItem}
@@ -34,7 +34,7 @@ export default defineComponent(() => {
   );
 });
 const Page = css`
-  height: 100%;
+  
   .category_box {
     padding: 20px;
     border: 1px solid #e5e5e5;

+ 54 - 14
src/views/admin/components/Editor.tsx

@@ -1,20 +1,41 @@
 import { css } from "@linaria/core";
-import { defineComponent, onMounted, ref } from "vue";
+import { defineComponent, onMounted, ref, watch } from "vue";
 
 import Quill from "quill";
-
+import { any } from "vue-types";
+import { uploader } from "@/modules/admin/objects";
+import { message } from "ant-design-vue";
 export default defineComponent({
-  setup(props, ctx) {
-    let editor = ref();
+  props: {
+    content: any(),
+  },
+  emits: ["change"],
+  setup(props, { emit }) {
+    let editor = null as any;
+    const once = ref(true);
     onMounted(() => {
       initEditor();
       initTitle();
     });
+    watch(
+      () => props.content,
+      () => {
+        if (props.content && once.value && editor) {
+          let delta = JSON.parse(props.content);
+          editor.updateContents(delta);
+          once.value = false;
+        }
+      }
+    );
     const initEditor = () => {
-      editor.value = new Quill("#editor", {
+      editor = null;
+      editor = new Quill("#editor", {
         theme: "snow",
         placeholder: "请在这里输入",
         modules: {
+          history: {
+            userOnly: true,
+          },
           toolbar: {
             container: [
               ["bold", "italic", "underline", "strike"], //加粗,斜体,下划线,删除线
@@ -29,18 +50,37 @@ export default defineComponent({
               ["clean"], //清除字体样式
               ["link", "image"], //上传图片
             ],
-            // handlers: {
-            //   link: (value: any) => {
-            //     if (value) {
-            //       console.log(value);
-            //     } else {
-            //       // editor.value.format("link", false);
-            //     }
-            //   },
-            // },
+            handlers: {
+              image: async (value: any) => {
+                if (value) {
+                  const [file] = await uploader.selectFile({
+                    accept: "jpg,png,jpeg",
+                  });
+                  if (file.size > 5 * 1024 * 1024) {
+                    message.warn("图片不能超过5M!");
+                    return;
+                  }
+                  let url = uploader.createObjectURL(file);
+
+                  console.log(url);
+                  var range = editor.getSelection();
+                  if (range) {
+                    editor.insertEmbed(range.index, "image", url);
+                    // editor.setSelection(range.index + 1);
+                    // editor.update();
+                  }
+                } else {
+                  // editor.value.format("link", false);
+                }
+              },
+            },
           },
         },
       });
+      editor.on("text-change", (data: any) => {
+        once.value = false;
+        emit("change", JSON.stringify(data));
+      });
     };
     const initTitle = () => {
       const titleConfig: { [key: string]: any } = {

+ 39 - 25
src/views/admin/components/PageMenu.tsx

@@ -1,40 +1,54 @@
 import { css } from "@linaria/core";
 import { Menu } from "ant-design-vue";
 
+import { useCategory } from "@/modules/admin";
+import { CategoryItem } from "@/typings/asset";
 import { defineComponent } from "vue";
-import { MenusConfig } from "../config/menus";
 import { useRouter } from "vue-router";
 export default defineComponent({
   setup() {
     const router = useRouter();
+    const categoryStore = useCategory();
+    const MenuRender = (item: CategoryItem) => {
+      if (item.children) {
+        return (
+          <Menu.SubMenu key={item._id} title={item.name}>
+            {item.children.map((subItem: any) => {
+              return MenuRender(subItem);
+            })}
+          </Menu.SubMenu>
+        );
+      } else {
+        return (
+          <Menu.Item key={`/detail/${item._id}`}>
+            <router-link to={`/detail/${item._id}`}>{item.name}</router-link>
+          </Menu.Item>
+        );
+      }
+    };
     return () => (
-      <div>
-        <Menu mode="inline" selectedKeys={[router.currentRoute.value.path]}>
-          {MenusConfig.map((item: any) => {
-            if (item.children) {
-              return (
-                <Menu.SubMenu key={item.name} title={item.name}>
-                  {item.children.map((subItem: any) => {
-                    return (
-                      <Menu.Item key={subItem.path}>
-                        <router-link to={subItem.path}>
-                          {subItem.name}
-                        </router-link>
-                      </Menu.Item>
-                    );
-                  })}
-                </Menu.SubMenu>
-              );
-            } else {
-              return (
-                <Menu.Item key={item.path}>
-                  <router-link to={item.path}>{item.name}</router-link>
-                </Menu.Item>
-              );
-            }
+      <div class={MenuRoot}>
+        <Menu
+          mode="inline"
+          inlineIndent={12}
+          selectedKeys={[router.currentRoute.value.path]}
+        >
+          <Menu.Item key={"/banner"}>
+            <router-link to={"/banner"}>首页轮播</router-link>
+          </Menu.Item>
+          <Menu.Item key={"/category"}>
+            <router-link to={"/category"}>菜单管理</router-link>
+          </Menu.Item>
+          {categoryStore.categoryTree.map((item: any) => {
+            return MenuRender(item);
           })}
         </Menu>
       </div>
     );
   },
 });
+const MenuRoot = css`
+  a {
+    display: block;
+  }
+`;

+ 68 - 0
src/views/admin/detail/components/DetailEditor.tsx

@@ -0,0 +1,68 @@
+import { Button, PageHeader } from "ant-design-vue";
+import { defineComponent, onMounted, reactive, watch } from "vue";
+
+import { useArticle } from "@/modules/admin";
+import { CategoryItem } from "@/typings/asset";
+import loading from "@/components/Provider/Loading";
+import { object } from "vue-types";
+import Editor from "../../components/Editor";
+
+export default defineComponent({
+  props: {
+    data: object<CategoryItem>(),
+  },
+  setup(props) {
+    const data: any = props.data || {};
+    const artStore = useArticle();
+    const state = reactive({
+      data: {} as any,
+    });
+    onMounted(() => {
+      initDetail();
+    });
+
+    const initDetail = async () => {
+      loading.show("");
+      artStore.listController.state.query = JSON.stringify({ cid: data._id });
+      await artStore.listController.loadPage(1);
+      const item = artStore.listController.state.list[0] || {};
+      if (item._id) {
+        const detRes = await artStore.getArticleDetail(item);
+        if (detRes) {
+          state.data = detRes;
+        }
+      }
+      loading.hidden();
+    };
+    const editorChange = (v: any) => {
+      state.data.content = v;
+    };
+    const submit = () => {
+      const subData = { ...state.data };
+      if (!subData._id) {
+        subData.title = data?.name;
+        subData.cid = data._id;
+      }
+      artStore.addOrUpdateArticle(subData);
+    };
+
+    return () => {
+      return (
+        <div>
+          <PageHeader title={data?.name}>
+            {{
+              extra: () => {
+                return (
+                  <Button type="primary" onClick={submit}>
+                    保存
+                  </Button>
+                );
+              },
+            }}
+          </PageHeader>
+          <Editor key={data._id} content={state.data.content} onChange={editorChange} />
+        </div>
+      );
+    };
+  },
+});

+ 0 - 0
src/views/admin/detail/components/DownloadEditor.tsx


+ 0 - 0
src/views/admin/detail/components/ListEditor.tsx


+ 35 - 0
src/views/admin/detail/index.tsx

@@ -0,0 +1,35 @@
+import { css } from "@linaria/core";
+import { Card } from "ant-design-vue";
+import { defineComponent } from "vue";
+import Editor from "../components/Editor";
+import { useArticle, useCategory } from "@/modules/admin";
+import { useRoute } from "vue-router";
+import { CategoryItem } from "@/typings/asset";
+import DetailEditor from "./components/DetailEditor";
+
+export default defineComponent({
+  setup() {
+    const route = useRoute();
+
+    const categoryStore = useCategory();
+
+    return () => {
+      const id = route.params.id;
+      const currCategory = categoryStore.listController.state.list.find(
+        (e: CategoryItem) => {
+          return e._id == id;
+        }
+      );
+      return (
+        <Card class={Page}>
+          {currCategory?.type == "detail" && (
+            <DetailEditor key={currCategory._id} data={currCategory} />
+          )}
+        </Card>
+      );
+    };
+  },
+});
+const Page = css`
+  height: 100%;
+`;

+ 18 - 4
src/views/admin/index/index.tsx

@@ -7,11 +7,11 @@ const { Header, Content, Sider } = Layout;
 
 export default defineComponent(() => {
   return () => (
-    <Layout class={Page}>
+    <Layout class={PageRoot}>
       <Header>
         <HeaderComponent />
       </Header>
-      <Layout class={Page}>
+      <Layout>
         <Sider class={SiderMenu}>
           <PageMenu />
         </Sider>
@@ -22,16 +22,30 @@ export default defineComponent(() => {
     </Layout>
   );
 });
-const Page = css`
-  height: 100%;
+const PageRoot = css`
   .ant-layout-header {
+    position: fixed;
+    left: 0;
+    top: 0;
+    width: 100%;
+    z-index: 3;
     padding: 0 20px;
   }
   .page_content {
+    margin-left: 200px;
+    margin-top: 64px;
     padding: 24px;
+    .ant-page-header {
+      padding: 16px 0;
+    }
   }
 `;
 const SiderMenu = css`
+  position: fixed;
+  left: 0;
+  top: 0;
+  padding-top: 64px;
+  z-index: 2;
   overflow-y: auto;
   overflow-x: hidden;
   height: 100%;

+ 0 - 16
src/views/admin/intro/index.tsx

@@ -1,16 +0,0 @@
-import { css } from "@linaria/core";
-import { Card } from "ant-design-vue";
-import { defineComponent } from "vue";
-import Editor from "../components/Editor";
-
-export default defineComponent(() => {
-  return () => (
-    <Card class={Page}>
-      学院简介
-      <Editor />
-    </Card>
-  );
-});
-const Page = css`
-  height: 100%;
-`;

+ 3 - 11
src/views/admin/router/index.ts

@@ -7,7 +7,7 @@ const router = createRouter({
     {
       path: "/",
       name: "home",
-      redirect: "/intro",
+      redirect: "/category",
       component: () => import("../index"),
       children: [
         {
@@ -19,16 +19,8 @@ const router = createRouter({
           component: () => import("../category"),
         },
         {
-          path: "/typeList",
-          component: () => import("../intro"),
-        },
-        {
-          path: "/typeDetail",
-          component: () => import("../intro"),
-        },
-        {
-          path: "/typeDownload",
-          component: () => import("../intro"),
+          path: "/detail/:id",
+          component: () => import("../detail"),
         },
       ],
     },