[
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\nu.sh"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 是大赵同学鸭\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\" dir=\"auto\"><a href=\"https://vuejs.org\" rel=\"nofollow\"><img  src=\"./public/Vue.png\" alt=\"Vue logo\" data-canonical-src=\"https://vuejs.org/images/logo.png\" style=\"max-width: 100%;\"></a></p>\n\n\n\n### 项目简介：\n\n`Vue3` + `ElementPlus` + `Vite`实战开发商城后台管理系统。\n\n### 功能简介：\n\n用户多权限管理、商品多规格实现、订单发货、导出订单、图库模块、分销模块、分享海报等。\n\n### 采用技术\n\n- `Vue3.2 + Vue-router4 + Vuex4 + Vite2 + Vueuse`\n- `ElementPlus`\n- `Naive-ui`\n- `Windicss`\n\n### 使用简介\n\n#### 安装依赖\n\n```bash\nyarn or yarn install \n```\n\n#### 项目运行\n\n```bash\nyarn dev\n```\n\n#### 项目打包\n\n```bash\nyarn build\n```\n\n### 项目效果\n\n![](./ProjectShow/1.png)\n\n![](./ProjectShow/2.png)\n\n\n\n### ![](./ProjectShow/3.png)\n\n![](./ProjectShow/4.png)\n\n### 其他\n\n感谢<a href=\"https://study.163.com/user/1135343179.htm\" target=\"_blank\">楚绵</a>（靓仔）的精品课程，受益匪浅。\n\n如果项目大家觉得比较好，请留一个🌟。\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Vue3 编程商城后台</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"shop-admin\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@element-plus/icons-vue\": \"^1.1.4\",\n    \"@vueuse/core\": \"^9.3.0\",\n    \"@vueuse/integrations\": \"^8.4.1\",\n    \"axios\": \"^0.27.2\",\n    \"element-plus\": \"^2.1.11\",\n    \"jquery\": \"2.2.0\",\n    \"nprogress\": \"^0.2.0\",\n    \"universal-cookie\": \"^4.0.4\",\n    \"vue\": \"^3.2.25\",\n    \"vue-router\": \"^4.0.15\",\n    \"vuex\": \"^4.0.2\"\n  },\n  \"devDependencies\": {\n    \"@vitejs/plugin-vue\": \"^2.3.1\",\n    \"naive-ui\": \"^2.33.3\",\n    \"vite\": \"^2.9.7\",\n    \"vite-plugin-windicss\": \"^1.8.4\",\n    \"windicss\": \"^3.5.1\"\n  }\n}\n"
  },
  {
    "path": "src/App.vue",
    "content": "<script setup>\n\n</script>\n\n<template>\n  <router-view></router-view>\n</template>\n\n<style>\n#nprogress .bar{\n  background-color: #f4f4f4!important;\n  height: 3px!important;\n}\n</style>\n"
  },
  {
    "path": "src/api/manager.js",
    "content": "import axios from '~/axios'\n\nexport function login(username,password){\n    return axios.post(\"/admin/login\",{\n        username,\n        password\n    })\n}\n\nexport function getinfo(){\n    return axios.post(\"/admin/getinfo\")\n}\n\nexport function logout(){\n    return axios.post(\"/admin/logout\")\n}\n\nexport function updatepassword(data){\n    return axios.post(\"/admin/updatepassword\",data)\n}"
  },
  {
    "path": "src/axios.js",
    "content": "import axios from \"axios\"\nimport { toast } from '~/composables/util'\nimport { getToken } from '~/composables/auth'\nimport store from \"./store\"\n\nconst service = axios.create({\n    baseURL:\"/api\"\n})\n\n// 添加请求拦截器\nservice.interceptors.request.use(function (config) {\n\n    // 往header头自动添加token\n    const token = getToken()\n    if(token){\n        config.headers[\"token\"] = token\n    }\n\n    return config;\n  }, function (error) {\n    // 对请求错误做些什么\n    return Promise.reject(error);\n  });\n\n// 添加响应拦截器\nservice.interceptors.response.use(function (response) {\n    // 对响应数据做点什么\n    return response.data.data;\n  }, function (error) {\n    const msg = error.response.data.msg || \"请求失败\"\n    \n    if(msg == \"非法token，请先登录！\"){\n      store.dispatch(\"logout\").finally(()=>location.reload())\n    }\n\n    toast(msg,\"error\")\n\n    return Promise.reject(error);\n })\n\nexport default service"
  },
  {
    "path": "src/components/FormDrawer.vue",
    "content": "<template>\n    <el-drawer v-model=\"showDrawer\" \n    :title=\"title\" \n    :size=\"size\" \n    :close-on-click-modal=\"false\"\n    :destroy-on-close=\"destroyOnClose\">\n        <div class=\"formDrawer\">\n            <div class=\"body\">\n                <slot></slot>\n            </div>\n            <div class=\"actions\">\n                <el-button type=\"primary\" @click=\"submit\" :loading=\"loading\">{{ confirmText }}</el-button>\n                <el-button type=\"default\" @click=\"close\">取消</el-button>\n            </div>\n        </div>\n    </el-drawer>\n</template>\n<script setup>\n    import { ref } from \"vue\"\n    const showDrawer = ref(false)\n\n    const props = defineProps({\n        title:String,\n        size:{\n            type:String,\n            default:\"45%\"\n        },\n        destroyOnClose:{\n            type:Boolean,\n            default:false\n        },\n        confirmText:{\n            type:String,\n            default:\"提交\"\n        }\n    })\n\n    const loading = ref(false)\n    const showLoading = ()=>loading.value = true\n    const hideLoading = ()=>loading.value = false\n\n    // 打开\n    const open = ()=> showDrawer.value = true\n\n    // 关闭\n    const close = ()=>showDrawer.value = false\n\n    // 提交\n    const emit = defineEmits([\"submit\"])\n    const submit = ()=> emit(\"submit\")\n\n    // 向父组件暴露以下方法\n    defineExpose({\n        open,\n        close,\n        showLoading,\n        hideLoading\n    })\n\n</script>\n<style>\n    .formDrawer{\n        width: 100%;\n        height: 100%;\n        position: relative;\n        @apply flex flex-col;\n    }\n\n    .formDrawer .body{\n        flex: 1;\n        position: absolute;\n        top: 0;\n        left: 0;\n        right: 0;\n        bottom: 50px;\n        overflow-y: auto;\n    }\n\n    .formDrawer .actions{\n        height: 50px;\n        @apply mt-auto flex items-center;\n    }\n</style>"
  },
  {
    "path": "src/components/HelloWorld.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\n\ndefineProps({\n  msg: String\n})\n\nconst count = ref(0)\n</script>\n\n<template>\n  <h1>{{ msg }}</h1>\n\n  <p>\n    Recommended IDE setup:\n    <a href=\"https://code.visualstudio.com/\" target=\"_blank\">VS Code</a>\n    +\n    <a href=\"https://github.com/johnsoncodehk/volar\" target=\"_blank\">Volar</a>\n  </p>\n\n  <p>\n    <a href=\"https://vitejs.dev/guide/features.html\" target=\"_blank\">\n      Vite Documentation\n    </a>\n    |\n    <a href=\"https://v3.vuejs.org/\" target=\"_blank\">Vue 3 Documentation</a>\n  </p>\n\n  <button type=\"button\" @click=\"count++\">count is: {{ count }}</button>\n  <p>\n    Edit\n    <code>components/HelloWorld.vue</code> to test hot module replacement.\n  </p>\n</template>\n\n<style scoped>\na {\n  color: #42b983;\n}\n</style>\n"
  },
  {
    "path": "src/composables/auth.js",
    "content": "import { useCookies } from '@vueuse/integrations/useCookies'\nconst TokenKey = \"admin-token\"\nconst cookie = useCookies()\n\n// 获取token\nexport function getToken(){\n    return cookie.get(TokenKey)\n}\n\n// 设置token\nexport function setToken(token){\n    return cookie.set(TokenKey,token)\n}\n\n// 清除token\nexport function removeToken(){\n    return cookie.remove(TokenKey)\n}"
  },
  {
    "path": "src/composables/useManager.js",
    "content": "import { ref, reactive } from 'vue'\nimport { logout, updatepassword } from \"~/api/manager\"\nimport { showModal, toast } from \"~/composables/util\"\nimport { useRouter } from \"vue-router\"\nimport { useStore } from \"vuex\"\n\nexport function useRepassword() {\n    const router = useRouter()\n    const store = useStore()\n    // 修改密码\n    const formDrawerRef = ref(null)\n    const form = reactive({\n        oldpassword: \"\",\n        password: \"\",\n        repassword: \"\"\n    })\n\n    const rules = {\n        oldpassword: [\n            {\n                required: true,\n                message: '旧密码不能为空',\n                trigger: 'blur'\n            },\n        ],\n        password: [\n            {\n                required: true,\n                message: '新密码不能为空',\n                trigger: 'blur'\n            },\n        ],\n        repassword: [\n            {\n                required: true,\n                message: '确认密码不能为空',\n                trigger: 'blur'\n            },\n        ]\n    }\n\n    const formRef = ref(null)\n    const onSubmit = () => {\n        formRef.value.validate((valid) => {\n            if (!valid) {\n                return false\n            }\n            formDrawerRef.value.showLoading()\n            updatepassword(form)\n                .then(res => {\n                    toast(\"修改密码成功，请重新登录\")\n                    store.dispatch(\"logout\")\n                    // 跳转回登录页\n                    router.push(\"/login\")\n                })\n                .finally(() => {\n                    formDrawerRef.value.hideLoading()\n                })\n\n        })\n    }\n\n    const openRePasswordForm = () => formDrawerRef.value.open()\n\n    return {\n        formDrawerRef,\n        form,\n        rules,\n        formRef,\n        onSubmit,\n        openRePasswordForm\n    }\n}\n\nexport function useLogout() {\n    const router = useRouter()\n    const store = useStore()\n    function handleLogout() {\n        showModal(\"是否要退出登录？\").then(res => {\n            logout().finally(() => {\n                store.dispatch(\"logout\")\n                // 跳转回登录页\n                router.push(\"/login\")\n                // 提示退出登录成功\n                toast(\"退出登录成功\")\n            })\n        })\n    }\n\n    return {\n        handleLogout\n    }\n}"
  },
  {
    "path": "src/composables/useTabList.js",
    "content": "import { ref } from \"vue\";\nimport { useRoute, onBeforeRouteUpdate } from \"vue-router\";\nimport { useCookies } from \"@vueuse/integrations/useCookies\";\nimport { router } from \"~/router\";\n\nexport function useTabList() {\n  const route = useRoute();\n  const cookie = useCookies();\n\n  const activeTab = ref(route.path);\n  const tabList = ref([\n    {\n      title: \"后台首页\",\n      path: \"/\",\n    },\n  ]);\n\n  // 添加标签导航\n  function addTab(tab) {\n    let noTab = tabList.value.findIndex((t) => t.path == tab.path) == -1;\n    if (noTab) {\n      tabList.value.push(tab);\n    }\n\n    cookie.set(\"tabList\", tabList.value);\n  }\n\n  // 初始化标签导航列表\n  function initTabList() {\n    let tbs = cookie.get(\"tabList\");\n    if (tbs) {\n      tabList.value = tbs;\n    }\n  }\n\n  initTabList();\n\n  onBeforeRouteUpdate((to, from) => {\n    activeTab.value = to.path;\n    addTab({\n      title: to.meta.title,\n      path: to.path,\n    });\n  });\n\n  const changeTab = (t) => {\n    activeTab.value = t;\n    router.push(t);\n  };\n\n  const removeTab = (t) => {\n    let tabs = tabList.value;\n    let a = activeTab.value;\n    if (a == t) {\n      tabs.forEach((tab, index) => {\n        if (tab.path == t) {\n          const nextTab = tabs[index + 1] || tabs[index - 1];\n          if (nextTab) {\n            a = nextTab.path;\n          }\n        }\n      });\n    }\n\n    activeTab.value = a;\n    tabList.value = tabList.value.filter((tab) => tab.path != t);\n\n    cookie.set(\"tabList\", tabList.value);\n  };\n\n  //定时清除cookie\n  const time = 2 * 60 * 60 * 1000;\n  setInterval(() => {\n    const key = cookie.get(\"tabList\");\n    if (key) {\n      cookie.remove(key);\n    }\n  }, time);\n\n  const handleClose = (c) => {\n    console.log(c);\n    if (c == \"clearAll\") {\n      activeTab.value = \"/\";\n      tabList.value = [\n        {\n          title: \"后台首页\",\n          path: \"/\",\n        },\n      ];\n    } else if (c == \"clearOther\") {\n      tabList.value = tabList.value.filter(\n        (tab) => tab.path == \"/\" || tab.path == activeTab.value\n      );\n    }\n    cookie.set(\"tabList\", tabList.value);\n  };\n\n  return {\n    activeTab,\n    tabList,\n    changeTab,\n    removeTab,\n    handleClose,\n  };\n}\n"
  },
  {
    "path": "src/composables/util.js",
    "content": "import { ElNotification,ElMessageBox } from 'element-plus'\nimport nprogress from 'nprogress'\n// 消息提示\nexport function toast(message,type = \"success\",dangerouslyUseHTMLString = false){\n    ElNotification({\n        message,\n        type,\n        dangerouslyUseHTMLString,\n        duration:3000\n    })\n}\n\n// 显示全屏loading\nexport function showFullLoading(){\n  nprogress.start()\n}\n\n// 隐藏全屏loading\nexport function hideFullLoading(){\n  nprogress.done()\n}\n\nexport function showModal(content = \"提示内容\",type = \"warning\",title = \"\"){\n    return ElMessageBox.confirm(\n        content,\n        title,\n        {\n          confirmButtonText: '确认',\n          cancelButtonText: '取消',\n          type,\n        }\n      )\n}\n"
  },
  {
    "path": "src/layouts/admin.vue",
    "content": "<template>\n    <el-container>\n        <el-header>\n            <f-header />\n        </el-header>\n        <el-container>\n            <el-aside :width=\"$store.state.asideWidth\">\n                <f-menu></f-menu>\n            </el-aside>\n            <el-main>\n                <f-tag-list />\n                <router-view v-slot=\"{ Component }\">\n                    <transition name=\"fade\">\n                        <keep-alive :max=\"10\">\n                            <component :is=\"Component\"></component>\n                        </keep-alive>\n                    </transition>\n                </router-view>\n            </el-main>\n        </el-container>\n    </el-container>\n</template>\n<script setup>\nimport FHeader from './components/FHeader.vue';\nimport FMenu from './components/FMenu.vue';\nimport FTagList from './components/FTagList.vue';\n</script>\n<style>\n.el-aside {\n    transition: all 0.2s;\n}\n\n.fade-enter-from {\n    opacity: 0;\n}\n\n.fade-enter-to {\n    opacity: 1;\n}\n\n.fade-leave-from {\n    opacity: 1;\n}\n\n.fade-leave-to {\n    opacity: 0;\n}\n\n.fade-enter-active,\n.fade-leave-active {\n    transition: all .3s;\n}\n\n.fade-enter-active {\n    transition-delay: .3s;\n}\n</style>"
  },
  {
    "path": "src/layouts/components/FHeader.vue",
    "content": "<template>\n    <div class=\"f-header\">\n        <span class=\"logo\">\n            <el-icon class=\"mr-1\">\n                <eleme-filled />\n            </el-icon>\n            追梦编程\n        </span>\n        <el-icon class=\"icon-btn\" @click=\"$store.commit('handleAsideWidth')\">\n            <fold v-if=\"$store.state.asideWidth == '250px'\"/>\n            <Expand v-else/>\n        </el-icon>\n        <el-tooltip effect=\"dark\" content=\"刷新\" placement=\"bottom\">\n            <el-icon class=\"icon-btn\" @click=\"handleRefresh\">\n                <refresh />\n            </el-icon>\n        </el-tooltip>\n\n        <div class=\"ml-auto flex items-center\">\n            <el-tooltip effect=\"dark\" content=\"全屏\" placement=\"bottom\">\n                <el-icon class=\"icon-btn\" @click=\"toggle\">\n                    <full-screen v-if=\"!isFullscreen\" />\n                    <aim v-else />\n                </el-icon>\n            </el-tooltip>\n\n            <el-dropdown class=\"dropdown\" @command=\"handleCommand\">\n                <span class=\"flex items-center text-light-50\">\n                    <el-avatar class=\"mr-2\" :size=\"25\" :src=\"$store.state.user.avatar\" />\n                    {{ $store.state.user.username }}\n                    <el-icon class=\"el-icon--right\">\n                        <arrow-down />\n                    </el-icon>\n                </span>\n                <template #dropdown>\n                    <el-dropdown-menu>\n                        <el-dropdown-item command=\"rePassword\">修改密码</el-dropdown-item>\n                        <el-dropdown-item command=\"logout\">退出登录</el-dropdown-item>\n                    </el-dropdown-menu>\n                </template>\n            </el-dropdown>\n        </div>\n    </div>\n\n    <form-drawer ref=\"formDrawerRef\" title=\"修改密码\" destroyOnClose @submit=\"onSubmit\">\n        <el-form ref=\"formRef\" :rules=\"rules\" :model=\"form\" label-width=\"80px\" size=\"small\">\n            <el-form-item prop=\"oldpassword\" label=\"旧密码\">\n                <el-input v-model=\"form.oldpassword\" placeholder=\"请输入旧密码\"></el-input>\n            </el-form-item>\n            <el-form-item prop=\"password\" label=\"新密码\">\n                <el-input type=\"password\" v-model=\"form.password\" placeholder=\"请输入密码\" show-password></el-input>\n            </el-form-item>\n            <el-form-item prop=\"repassword\" label=\"确认密码\">\n                <el-input type=\"password\" v-model=\"form.repassword\" placeholder=\"请输入确认密码\" show-password></el-input>\n            </el-form-item>\n        </el-form>\n    </form-drawer>\n\n</template>\n<script setup>\nimport FormDrawer from '~/components/FormDrawer.vue'\nimport { useFullscreen } from '@vueuse/core'\nimport { useRepassword,useLogout } from \"~/composables/useManager\"\nconst {\n    // 是否全屏状态\n    isFullscreen,\n    // 切换全屏\n    toggle\n} = useFullscreen()\n\nconst {\n    formDrawerRef,\n    form,\n    rules,\n    formRef,\n    onSubmit,\n    openRePasswordForm\n} = useRepassword()\n\nconst {\n    handleLogout\n} = useLogout()\n\nconst handleCommand = (c) => {\n    switch (c) {\n        case \"logout\":\n            handleLogout()\n            break;\n        case \"rePassword\":\n            openRePasswordForm()\n            break;\n    }\n}\n\n// 刷新\nconst handleRefresh = () => location.reload()\n\n</script>\n<style>\n.f-header {\n    @apply flex items-center  text-light-50 fixed top-0 left-0 right-0;\n    height: 64px;\n    background-color: #1b91ff;\n}\n\n.logo {\n    width: 250px;\n    @apply flex justify-center items-center text-xl font-thin;\n}\n\n.icon-btn {\n    @apply flex justify-center items-center;\n    width: 42px;\n    height: 64px;\n    cursor: pointer;\n}\n\n.icon-btn:hover {\n    @apply bg-indigo-600;\n}\n\n.f-header .dropdown {\n    height: 64px;\n    cursor: pointer;\n    @apply flex justify-center items-center mx-5;\n}\n</style>"
  },
  {
    "path": "src/layouts/components/FMenu.vue",
    "content": "<template>\n    <div class=\"f-menu\" :style=\"{ width: $store.state.asideWidth }\">\n        <el-menu :default-active=\"defaultActive\" unique-opened :collapse=\"isCollapse\" default-active=\"2\"\n            class=\"border-0\" @select=\"handleSelect\" :collapse-transition=\"false\">\n\n            <template v-for=\"(item, index) in asideMenus\" :key=\"index\">\n                <el-sub-menu v-if=\"item.child && item.child.length > 0\" :index=\"item.name\">\n                    <template #title>\n                        <el-icon>\n                            <component :is=\"item.icon\"></component>\n                        </el-icon>\n                        <span>{{ item.name }}</span>\n                    </template>\n                    <el-menu-item v-for=\"(item2, index2) in item.child\" :key=\"index2\" :index=\"item2.frontpath\">\n                        <el-icon>\n                            <component :is=\"item2.icon\"></component>\n                        </el-icon>\n                        <span>{{ item2.name }}</span>\n                    </el-menu-item>\n                </el-sub-menu>\n\n                <el-menu-item v-else :index=\"item.frontpath\">\n                    <el-icon>\n                        <component :is=\"item.icon\"></component>\n                    </el-icon>\n                    <span>{{ item.name }}</span>\n                </el-menu-item>\n            </template>\n        </el-menu>\n    </div>\n</template>\n<script setup>\nimport { computed, ref } from 'vue';\nimport { useRouter, useRoute } from 'vue-router';\nimport { useStore } from 'vuex';\nconst router = useRouter()\nconst store = useStore()\nconst route = useRoute()\n\n// 默认选中\nconst defaultActive = ref(route.path)\n\n// 是否折叠\nconst isCollapse = computed(() => !(store.state.asideWidth == '250px'))\n\nconst asideMenus = computed(() =>\n    store.state.menus\n)\n\nconst handleSelect = (e) => {\n    router.push(e)\n}\n</script>\n<style>\n.f-menu {\n    transition: all 0.3s;\n    top: 64px;\n    bottom: 0;\n    left: 0;\n    overflow-y: auto;\n    overflow-x: hidden;\n    @apply shadow-md fixed bg-light-50;\n}\n\n.f-menu::-webkit-scrollbar {\n    width: 0px;\n}\n</style>"
  },
  {
    "path": "src/layouts/components/FTagList.vue",
    "content": "<template>\n    <div class=\"f-tag-list\" :style=\"{ left: $store.state.asideWidth }\">\n\n        <el-tabs v-model=\"activeTab\" type=\"card\" class=\"flex-1\" @tab-remove=\"removeTab\" style=\"min-width:100px;\"\n            @tab-change=\"changeTab\">\n            <el-tab-pane :closable=\"item.path != '/'\" v-for=\"item in tabList\" :key=\"item.path\" :label=\"item.title\"\n                :name=\"item.path\"></el-tab-pane>\n        </el-tabs>\n\n        <span class=\"tag-btn\">\n            <el-dropdown @command=\"handleClose\">\n                <span class=\"el-dropdown-link\">\n                    <el-icon>\n                        <arrow-down />\n                    </el-icon>\n                </span>\n                <template #dropdown>\n                    <el-dropdown-menu>\n                        <el-dropdown-item command=\"clearOther\">关闭其他</el-dropdown-item>\n                        <el-dropdown-item command=\"clearAll\">全部关闭</el-dropdown-item>\n                    </el-dropdown-menu>\n                </template>\n            </el-dropdown>\n        </span>\n    </div>\n    <div style=\"height:44px;\"></div>\n</template>\n<script setup>\nimport { useTabList } from \"~/composables/useTabList.js\";\n\nconst { activeTab,\n    tabList,\n    changeTab,\n    removeTab,\n    handleClose } = useTabList()\n</script>\n<style scoped>\n.f-tag-list {\n    @apply fixed bg-gray-100 flex items-center px-2;\n    top: 64px;\n    right: 0;\n    height: 44px;\n    z-index: 100;\n}\n\n.tag-btn {\n    @apply bg-white rounded ml-auto flex items-center justify-center px-2;\n    height: 32px;\n}\n\n:deep(.el-tabs__header) {\n    border: 0 !important;\n    @apply mb-0;\n}\n\n:deep(.el-tabs__nav) {\n    border: 0 !important;\n}\n\n:deep(.el-tabs__item) {\n    border: 0 !important;\n    height: 32px;\n    line-height: 32px;\n    @apply bg-white mx-1 rounded;\n}\n\n:deep(.el-tabs__nav-next),\n:deep(.el-tabs__nav-prev) {\n    line-height: 32px;\n    height: 32px;\n}\n\n:deep(.is-disabled) {\n    cursor: not-allowed;\n    @apply text-gray-300;\n}\n</style>"
  },
  {
    "path": "src/main.js",
    "content": "import { createApp } from \"vue\";\nimport ElementPlus from \"element-plus\";\nimport \"element-plus/dist/index.css\";\nimport App from \"./App.vue\";\nimport { router } from \"./router\";\nimport store from \"./store\";\nimport * as ElementPlusIconsVue from \"@element-plus/icons-vue\";\nconst app = createApp(App);\napp.use(store);\napp.use(router);\n\napp.use(ElementPlus);\n\nfor (const [key, component] of Object.entries(ElementPlusIconsVue)) {\n  app.component(key, component);\n}\nimport \"virtual:windi.css\";\n\nimport \"./permission\";\n\nimport \"nprogress/nprogress.css\";\n\napp.mount(\"#app\");\n"
  },
  {
    "path": "src/pages/404.vue",
    "content": "<template>\n    <div class=\"main\">\n        <a class=\"btn\" href=\"#\" @click=\"$router.push('/')\">\n            返回网站首页\n        </a>\n        <!-- <el-result\n            icon=\"warning\"\n            title=\"404提示\"\n            sub-title=\"你找的页面走丢了~\"\n        >\n            <template #extra>\n                <el-button type=\"primary\" @click=\"$router.push('/')\">回到首页</el-button>\n            </template>\n        </el-result> -->\n    </div>\n</template>\n<style>\n.main {\n    width: 100vw;\n    height: 100vh;\n    background: url(\"/public/404.png\") center fixed;\n    position: relative;\n}\n\n.main .btn {\n    position: absolute;\n    max-width: 138px;\n    margin-top: 27%;\n    margin-left: 12%;\n\n    border: 2px solid #fff;\n    display: flex;\n    padding: 12px 19px;\n\n    font-size: 16px;\n    font-weight: 700;\n    text-decoration: none;\n    -webkit-border-radius: 50px;\n    -moz-border-radius: 50px;\n\n    color: #fff;\n    text-align: center;\n    white-space: nowrap;\n    vertical-align: middle;\n    touch-action: manipulation;\n    cursor: pointer;\n}\n</style>"
  },
  {
    "path": "src/pages/category/list.vue",
    "content": "<template>\n    <div>\n        分类列表\n    </div>\n</template>\n\n<script setup>\n\n</script>\n"
  },
  {
    "path": "src/pages/goods/list.vue",
    "content": "<template>\n    <div>\n        商品管理\n    </div>\n</template>"
  },
  {
    "path": "src/pages/index.vue",
    "content": "<template>\n    <div>\n        后台首页 \n        \n        {{ $store.state.user.username }}\n    </div>\n</template>\n<script setup>\n    \n</script>"
  },
  {
    "path": "src/pages/login.vue",
    "content": "<template>\n    <el-row class=\"login-container\">\n        <el-col :lg=\"16\" :md=\"12\" class=\"left\">\n            <div>\n                <div>欢迎光临 🎓</div>\n                <div>此站点是《vue3 + vite实战商城后台开发》的演示地址</div>\n            </div>\n        </el-col>\n        <el-col :lg=\"8\" :md=\"12\" class=\"right\">\n            <h2 class=\"title\">欢迎回来</h2>\n            <div>\n                <span class=\"line\"></span>\n                <span>账号密码登录</span>\n                <span class=\"line\"></span>\n            </div>\n            <el-form ref=\"formRef\" :rules=\"rules\" :model=\"form\" class=\"w-[250px]\">\n                <el-form-item prop=\"username\">\n                    <el-input v-model=\"form.username\" placeholder=\"请输入用户名\">\n                        <template #prefix>\n                            <el-icon>\n                                <user />\n                            </el-icon>\n                        </template>\n                    </el-input>\n                </el-form-item>\n                <el-form-item prop=\"password\">\n                    <el-input type=\"password\" v-model=\"form.password\" placeholder=\"请输入密码\" show-password>\n                        <template #prefix>\n                            <el-icon>\n                                <lock />\n                            </el-icon>\n                        </template>\n                    </el-input>\n                </el-form-item>\n                <el-form-item>\n                    <el-button round color=\"#626aef\" class=\"w-[250px]\" type=\"primary\" @click=\"onSubmit\"\n                        :loading=\"loading\">登 录</el-button>\n                </el-form-item>\n            </el-form>\n        </el-col>\n    </el-row>\n</template>\n\n<script setup>\nimport { ref, reactive, onMounted, onBeforeUnmount } from 'vue'\nimport { toast } from '~/composables/util'\nimport { useRouter } from 'vue-router'\nimport { useStore } from 'vuex'\n\nconst store = useStore()\nconst router = useRouter()\n\n// do not use same name with ref\nconst form = reactive({\n    username: \"\",\n    password: \"\"\n})\n\nconst rules = {\n    username: [\n        {\n            required: true,\n            message: '用户名不能为空',\n            trigger: 'blur'\n        },\n    ],\n    password: [\n        {\n            required: true,\n            message: '用户名不能为空',\n            trigger: 'blur'\n        },\n    ]\n}\n\nconst formRef = ref(null)\nconst loading = ref(false)\nconst onSubmit = () => {\n    formRef.value.validate((valid) => {\n        if (!valid) {\n            return false\n        }\n        loading.value = true\n\n        store.dispatch(\"login\", form).then(res => {\n            toast(\"登录成功\")\n            router.push(\"/\")\n        }).finally(() => {\n            loading.value = false\n        })\n    })\n}\n\n// 监听回车事件\nfunction onKeyUp(e) {\n    if (e.key == \"Enter\") onSubmit()\n}\n\n// 添加键盘监听\nonMounted(() => {\n    document.addEventListener(\"keyup\", onKeyUp)\n})\n// 移除键盘监听\nonBeforeUnmount(() => {\n    document.removeEventListener(\"keyup\", onKeyUp)\n})\n\n</script>\n\n<style scoped>\n.login-container {\n    @apply min-h-screen;\n    background-image: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);\n}\n\n.login-container .left,\n.login-container .right {\n    @apply flex items-center justify-center;\n}\n\n.login-container .right {\n    @apply bg-light-50 flex-col;\n}\n\n.left>div>div:first-child {\n    @apply font-bold text-5xl text-light-50 mb-4;\n}\n\n.left>div>div:last-child {\n    @apply text-gray-200 text-sm;\n}\n\n.right .title {\n    @apply font-bold text-3xl text-gray-800;\n}\n\n.right>div {\n    @apply flex items-center justify-center my-5 text-gray-300 space-x-2;\n}\n\n.right .line {\n    @apply h-[1px] w-16 bg-gray-200;\n}\n</style>"
  },
  {
    "path": "src/permission.js",
    "content": "import { router, addRoutes } from \"~/router\";\nimport { getToken } from \"~/composables/auth\";\nimport { toast, showFullLoading, hideFullLoading } from \"~/composables/util\";\nimport store from \"./store\";\n\n// 全局前置守卫\nlet hasGetInfo = false;\nrouter.beforeEach(async (to, from, next) => {\n  // 显示loading\n  showFullLoading();\n\n  const token = getToken();\n\n  // 没有登录，强制跳转回登录页\n  if (!token && to.path != \"/login\") {\n    toast(\"请先登录\", \"error\");\n    return next({ path: \"/login\" });\n  }\n\n  // 防止重复登录\n  if (token && to.path == \"/login\") {\n    toast(\"请勿重复登录\", \"error\");\n    return next({ path: from.path ? from.path : \"/\" });\n  }\n\n  // 如果用户登录了，自动获取用户信息，并存储在vuex当中\n  let hasNewRoutes = false;\n  if (token && !hasGetInfo) {\n    let { menus } = await store.dispatch(\"getinfo\");\n    hasGetInfo = true;\n    //动态添加路由\n    hasNewRoutes = addRoutes(menus);\n  }\n\n  // 设置页面标题\n  let title = (to.meta.title ? to.meta.title : \"Vue3\") + \"-追梦编程商城后台\";\n  document.title = title;\n\n  hasNewRoutes ? next(to.fullPath) : next();\n});\n\n// 全局后置守卫\nrouter.afterEach((to, from) => hideFullLoading());\n"
  },
  {
    "path": "src/router/index.js",
    "content": "import { createRouter, createWebHashHistory } from \"vue-router\";\n\nimport Admin from \"~/layouts/admin.vue\";\nimport Index from \"~/pages/index.vue\";\nimport Login from \"~/pages/login.vue\";\nimport NotFound from \"~/pages/404.vue\";\nimport GoodList from \"~/pages/goods/list.vue\";\nimport CategoryList from \"~/pages/category/list.vue\";\n\n/* const routes = [\n    {\n        path:\"/\",\n        component:Admin,\n        // 子路由\n        children:[]\n    },\n{\n    path:\"/login\",\n    component:Login,\n    meta:{\n        title:\"登录页\"\n    }\n},{\n    path: '/:pathMatch(.*)*',\n    name: 'NotFound',\n    component: NotFound\n}] */\n\n//默认路由,所有用户共享\nconst routes = [\n  {\n    path: \"/\",\n    name: \"admin\",\n    component: Admin,\n  },\n  {\n    path: \"/login\",\n    component: Login,\n    meta: {\n      title: \"登录页\",\n    },\n  },\n  {\n    path: \"/:pathMatch(.*)*\",\n    name: \"NotFound\",\n    component: NotFound,\n  },\n];\n\n//动态匹配添加路由\nconst asyncRoutes = [\n  {\n    path: \"/\",\n    name: \"/\",\n    component: Index,\n    meta: {\n      title: \"后台首页\",\n    },\n  },\n  {\n    path: \"/goods/list\",\n    name: \"/goods/list\",\n    component: GoodList,\n    meta: {\n      title: \"商品管理\",\n    },\n  },\n  {\n    path: \"/category/list\",\n    name: \"/category/list\",\n    component: CategoryList,\n    meta: {\n      title: \"分类列表\",\n    },\n  },\n];\n\nexport const router = createRouter({\n  history: createWebHashHistory(),\n  routes,\n});\n\n//动态添加路由方法\nexport function addRoutes(menus) {\n  //是否有新的路由\n  let hasNewRoutes = false;\n  const findAndAddRoutesByMenus = (arr) => {\n    arr.forEach((e) => {\n      let item = asyncRoutes.find((o) => o.path == e.frontpath);\n      if (item && !router.hasRoute(item.path)) {\n        router.addRoute(\"admin\", item);\n        hasNewRoutes = true;\n      }\n      //子路由\n      if (e.child && e.child.length > 0) {\n        findAndAddRoutesByMenus(e.child);\n      }\n    });\n  };\n  findAndAddRoutesByMenus(menus)\n\n  // console.log(\"获取路由\",router.getRoutes());\n  return hasNewRoutes;\n}\n"
  },
  {
    "path": "src/store/index.js",
    "content": "import { createStore } from \"vuex\";\nimport { login, getinfo } from \"~/api/manager\";\nimport { setToken, removeToken } from \"~/composables/auth\";\nconst store = createStore({\n  state() {\n    return {\n      // 用户信息\n      user: {},\n\n      // 侧边宽度\n      asideWidth: \"250px\",\n      menus: [],\n      ruleNames: [],\n    };\n  },\n  mutations: {\n    // 记录用户信息\n    SET_USERINFO(state, user) {\n      state.user = user;\n    },\n    // 展开/缩起侧边\n    handleAsideWidth(state) {\n      state.asideWidth = state.asideWidth == \"250px\" ? \"64px\" : \"250px\";\n    },\n    SET_MENUS(state, menu) {\n      state.menus = menu;\n    },\n    SET_RULENAMES(state, ruleNames) {\n      state.ruleNames = ruleNames;\n    },\n  },\n  actions: {\n    // 登录\n    login({ commit }, { username, password }) {\n      return new Promise((resolve, reject) => {\n        login(username, password)\n          .then((res) => {\n            setToken(res.token);\n\n            resolve(res);\n          })\n          .catch((err) => reject(err));\n      });\n    },\n    // 获取当前登录用户信息\n    getinfo({ commit }) {\n      return new Promise((resolve, reject) => {\n        getinfo()\n          .then((res) => {\n            console.log(res);\n            commit(\"SET_USERINFO\", res);\n            commit(\"SET_MENUS\", res.menus);\n            commit(\"SET_RULENAMES\", res.ruleNames);\n            resolve(res);\n          })\n          .catch((err) => reject(err));\n      });\n    },\n    // 退出登录\n    logout({ commit }) {\n      // 移除cookie里的token\n      removeToken();\n      // 清除当前用户状态 vuex\n      commit(\"SET_USERINFO\", {});\n    },\n  },\n});\n\nexport default store;\n"
  },
  {
    "path": "vite.config.js",
    "content": "import { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport WindiCSS from 'vite-plugin-windicss'\n\nimport path from \"path\"\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  resolve:{\n    // src目录别名\n    alias:{\n      \"~\":path.resolve(__dirname,\"src\"),\n    }\n  },\n\n  server:{\n    proxy:{\n      '/api': {\n        target: 'http://ceshi13.dishait.cn',\n        changeOrigin: true,\n        rewrite: (path) => path.replace(/^\\/api/, '')\n      },\n    }\n  },\n\n  plugins: [vue(),WindiCSS()]\n})\n"
  }
]