Full Code of webyang-male/vue3-mallManage for AI

main 4e81ca0a1a0e cached
28 files
31.2 KB
9.1k tokens
23 symbols
1 requests
Download .txt
Repository: webyang-male/vue3-mallManage
Branch: main
Commit: 4e81ca0a1a0e
Files: 28
Total size: 31.2 KB

Directory structure:
gitextract_4pghmfdd/

├── .gitignore
├── LICENSE
├── README.md
├── index.html
├── package.json
├── src/
│   ├── App.vue
│   ├── api/
│   │   └── manager.js
│   ├── axios.js
│   ├── components/
│   │   ├── FormDrawer.vue
│   │   └── HelloWorld.vue
│   ├── composables/
│   │   ├── auth.js
│   │   ├── useManager.js
│   │   ├── useTabList.js
│   │   └── util.js
│   ├── layouts/
│   │   ├── admin.vue
│   │   └── components/
│   │       ├── FHeader.vue
│   │       ├── FMenu.vue
│   │       └── FTagList.vue
│   ├── main.js
│   ├── pages/
│   │   ├── 404.vue
│   │   ├── category/
│   │   │   └── list.vue
│   │   ├── goods/
│   │   │   └── list.vue
│   │   ├── index.vue
│   │   └── login.vue
│   ├── permission.js
│   ├── router/
│   │   └── index.js
│   └── store/
│       └── index.js
└── vite.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
u.sh

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 是大赵同学鸭

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<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>



### 项目简介:

`Vue3` + `ElementPlus` + `Vite`实战开发商城后台管理系统。

### 功能简介:

用户多权限管理、商品多规格实现、订单发货、导出订单、图库模块、分销模块、分享海报等。

### 采用技术

- `Vue3.2 + Vue-router4 + Vuex4 + Vite2 + Vueuse`
- `ElementPlus`
- `Naive-ui`
- `Windicss`

### 使用简介

#### 安装依赖

```bash
yarn or yarn install 
```

#### 项目运行

```bash
yarn dev
```

#### 项目打包

```bash
yarn build
```

### 项目效果

![](./ProjectShow/1.png)

![](./ProjectShow/2.png)



### ![](./ProjectShow/3.png)

![](./ProjectShow/4.png)

### 其他

感谢<a href="https://study.163.com/user/1135343179.htm" target="_blank">楚绵</a>(靓仔)的精品课程,受益匪浅。

如果项目大家觉得比较好,请留一个🌟。


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue3 编程商城后台</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>


================================================
FILE: package.json
================================================
{
  "name": "shop-admin",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@element-plus/icons-vue": "^1.1.4",
    "@vueuse/core": "^9.3.0",
    "@vueuse/integrations": "^8.4.1",
    "axios": "^0.27.2",
    "element-plus": "^2.1.11",
    "jquery": "2.2.0",
    "nprogress": "^0.2.0",
    "universal-cookie": "^4.0.4",
    "vue": "^3.2.25",
    "vue-router": "^4.0.15",
    "vuex": "^4.0.2"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^2.3.1",
    "naive-ui": "^2.33.3",
    "vite": "^2.9.7",
    "vite-plugin-windicss": "^1.8.4",
    "windicss": "^3.5.1"
  }
}


================================================
FILE: src/App.vue
================================================
<script setup>

</script>

<template>
  <router-view></router-view>
</template>

<style>
#nprogress .bar{
  background-color: #f4f4f4!important;
  height: 3px!important;
}
</style>


================================================
FILE: src/api/manager.js
================================================
import axios from '~/axios'

export function login(username,password){
    return axios.post("/admin/login",{
        username,
        password
    })
}

export function getinfo(){
    return axios.post("/admin/getinfo")
}

export function logout(){
    return axios.post("/admin/logout")
}

export function updatepassword(data){
    return axios.post("/admin/updatepassword",data)
}

================================================
FILE: src/axios.js
================================================
import axios from "axios"
import { toast } from '~/composables/util'
import { getToken } from '~/composables/auth'
import store from "./store"

const service = axios.create({
    baseURL:"/api"
})

// 添加请求拦截器
service.interceptors.request.use(function (config) {

    // 往header头自动添加token
    const token = getToken()
    if(token){
        config.headers["token"] = token
    }

    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
service.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response.data.data;
  }, function (error) {
    const msg = error.response.data.msg || "请求失败"
    
    if(msg == "非法token,请先登录!"){
      store.dispatch("logout").finally(()=>location.reload())
    }

    toast(msg,"error")

    return Promise.reject(error);
 })

export default service

================================================
FILE: src/components/FormDrawer.vue
================================================
<template>
    <el-drawer v-model="showDrawer" 
    :title="title" 
    :size="size" 
    :close-on-click-modal="false"
    :destroy-on-close="destroyOnClose">
        <div class="formDrawer">
            <div class="body">
                <slot></slot>
            </div>
            <div class="actions">
                <el-button type="primary" @click="submit" :loading="loading">{{ confirmText }}</el-button>
                <el-button type="default" @click="close">取消</el-button>
            </div>
        </div>
    </el-drawer>
</template>
<script setup>
    import { ref } from "vue"
    const showDrawer = ref(false)

    const props = defineProps({
        title:String,
        size:{
            type:String,
            default:"45%"
        },
        destroyOnClose:{
            type:Boolean,
            default:false
        },
        confirmText:{
            type:String,
            default:"提交"
        }
    })

    const loading = ref(false)
    const showLoading = ()=>loading.value = true
    const hideLoading = ()=>loading.value = false

    // 打开
    const open = ()=> showDrawer.value = true

    // 关闭
    const close = ()=>showDrawer.value = false

    // 提交
    const emit = defineEmits(["submit"])
    const submit = ()=> emit("submit")

    // 向父组件暴露以下方法
    defineExpose({
        open,
        close,
        showLoading,
        hideLoading
    })

</script>
<style>
    .formDrawer{
        width: 100%;
        height: 100%;
        position: relative;
        @apply flex flex-col;
    }

    .formDrawer .body{
        flex: 1;
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 50px;
        overflow-y: auto;
    }

    .formDrawer .actions{
        height: 50px;
        @apply mt-auto flex items-center;
    }
</style>

================================================
FILE: src/components/HelloWorld.vue
================================================
<script setup>
import { ref } from 'vue'

defineProps({
  msg: String
})

const count = ref(0)
</script>

<template>
  <h1>{{ msg }}</h1>

  <p>
    Recommended IDE setup:
    <a href="https://code.visualstudio.com/" target="_blank">VS Code</a>
    +
    <a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
  </p>

  <p>
    <a href="https://vitejs.dev/guide/features.html" target="_blank">
      Vite Documentation
    </a>
    |
    <a href="https://v3.vuejs.org/" target="_blank">Vue 3 Documentation</a>
  </p>

  <button type="button" @click="count++">count is: {{ count }}</button>
  <p>
    Edit
    <code>components/HelloWorld.vue</code> to test hot module replacement.
  </p>
</template>

<style scoped>
a {
  color: #42b983;
}
</style>


================================================
FILE: src/composables/auth.js
================================================
import { useCookies } from '@vueuse/integrations/useCookies'
const TokenKey = "admin-token"
const cookie = useCookies()

// 获取token
export function getToken(){
    return cookie.get(TokenKey)
}

// 设置token
export function setToken(token){
    return cookie.set(TokenKey,token)
}

// 清除token
export function removeToken(){
    return cookie.remove(TokenKey)
}

================================================
FILE: src/composables/useManager.js
================================================
import { ref, reactive } from 'vue'
import { logout, updatepassword } from "~/api/manager"
import { showModal, toast } from "~/composables/util"
import { useRouter } from "vue-router"
import { useStore } from "vuex"

export function useRepassword() {
    const router = useRouter()
    const store = useStore()
    // 修改密码
    const formDrawerRef = ref(null)
    const form = reactive({
        oldpassword: "",
        password: "",
        repassword: ""
    })

    const rules = {
        oldpassword: [
            {
                required: true,
                message: '旧密码不能为空',
                trigger: 'blur'
            },
        ],
        password: [
            {
                required: true,
                message: '新密码不能为空',
                trigger: 'blur'
            },
        ],
        repassword: [
            {
                required: true,
                message: '确认密码不能为空',
                trigger: 'blur'
            },
        ]
    }

    const formRef = ref(null)
    const onSubmit = () => {
        formRef.value.validate((valid) => {
            if (!valid) {
                return false
            }
            formDrawerRef.value.showLoading()
            updatepassword(form)
                .then(res => {
                    toast("修改密码成功,请重新登录")
                    store.dispatch("logout")
                    // 跳转回登录页
                    router.push("/login")
                })
                .finally(() => {
                    formDrawerRef.value.hideLoading()
                })

        })
    }

    const openRePasswordForm = () => formDrawerRef.value.open()

    return {
        formDrawerRef,
        form,
        rules,
        formRef,
        onSubmit,
        openRePasswordForm
    }
}

export function useLogout() {
    const router = useRouter()
    const store = useStore()
    function handleLogout() {
        showModal("是否要退出登录?").then(res => {
            logout().finally(() => {
                store.dispatch("logout")
                // 跳转回登录页
                router.push("/login")
                // 提示退出登录成功
                toast("退出登录成功")
            })
        })
    }

    return {
        handleLogout
    }
}

================================================
FILE: src/composables/useTabList.js
================================================
import { ref } from "vue";
import { useRoute, onBeforeRouteUpdate } from "vue-router";
import { useCookies } from "@vueuse/integrations/useCookies";
import { router } from "~/router";

export function useTabList() {
  const route = useRoute();
  const cookie = useCookies();

  const activeTab = ref(route.path);
  const tabList = ref([
    {
      title: "后台首页",
      path: "/",
    },
  ]);

  // 添加标签导航
  function addTab(tab) {
    let noTab = tabList.value.findIndex((t) => t.path == tab.path) == -1;
    if (noTab) {
      tabList.value.push(tab);
    }

    cookie.set("tabList", tabList.value);
  }

  // 初始化标签导航列表
  function initTabList() {
    let tbs = cookie.get("tabList");
    if (tbs) {
      tabList.value = tbs;
    }
  }

  initTabList();

  onBeforeRouteUpdate((to, from) => {
    activeTab.value = to.path;
    addTab({
      title: to.meta.title,
      path: to.path,
    });
  });

  const changeTab = (t) => {
    activeTab.value = t;
    router.push(t);
  };

  const removeTab = (t) => {
    let tabs = tabList.value;
    let a = activeTab.value;
    if (a == t) {
      tabs.forEach((tab, index) => {
        if (tab.path == t) {
          const nextTab = tabs[index + 1] || tabs[index - 1];
          if (nextTab) {
            a = nextTab.path;
          }
        }
      });
    }

    activeTab.value = a;
    tabList.value = tabList.value.filter((tab) => tab.path != t);

    cookie.set("tabList", tabList.value);
  };

  //定时清除cookie
  const time = 2 * 60 * 60 * 1000;
  setInterval(() => {
    const key = cookie.get("tabList");
    if (key) {
      cookie.remove(key);
    }
  }, time);

  const handleClose = (c) => {
    console.log(c);
    if (c == "clearAll") {
      activeTab.value = "/";
      tabList.value = [
        {
          title: "后台首页",
          path: "/",
        },
      ];
    } else if (c == "clearOther") {
      tabList.value = tabList.value.filter(
        (tab) => tab.path == "/" || tab.path == activeTab.value
      );
    }
    cookie.set("tabList", tabList.value);
  };

  return {
    activeTab,
    tabList,
    changeTab,
    removeTab,
    handleClose,
  };
}


================================================
FILE: src/composables/util.js
================================================
import { ElNotification,ElMessageBox } from 'element-plus'
import nprogress from 'nprogress'
// 消息提示
export function toast(message,type = "success",dangerouslyUseHTMLString = false){
    ElNotification({
        message,
        type,
        dangerouslyUseHTMLString,
        duration:3000
    })
}

// 显示全屏loading
export function showFullLoading(){
  nprogress.start()
}

// 隐藏全屏loading
export function hideFullLoading(){
  nprogress.done()
}

export function showModal(content = "提示内容",type = "warning",title = ""){
    return ElMessageBox.confirm(
        content,
        title,
        {
          confirmButtonText: '确认',
          cancelButtonText: '取消',
          type,
        }
      )
}


================================================
FILE: src/layouts/admin.vue
================================================
<template>
    <el-container>
        <el-header>
            <f-header />
        </el-header>
        <el-container>
            <el-aside :width="$store.state.asideWidth">
                <f-menu></f-menu>
            </el-aside>
            <el-main>
                <f-tag-list />
                <router-view v-slot="{ Component }">
                    <transition name="fade">
                        <keep-alive :max="10">
                            <component :is="Component"></component>
                        </keep-alive>
                    </transition>
                </router-view>
            </el-main>
        </el-container>
    </el-container>
</template>
<script setup>
import FHeader from './components/FHeader.vue';
import FMenu from './components/FMenu.vue';
import FTagList from './components/FTagList.vue';
</script>
<style>
.el-aside {
    transition: all 0.2s;
}

.fade-enter-from {
    opacity: 0;
}

.fade-enter-to {
    opacity: 1;
}

.fade-leave-from {
    opacity: 1;
}

.fade-leave-to {
    opacity: 0;
}

.fade-enter-active,
.fade-leave-active {
    transition: all .3s;
}

.fade-enter-active {
    transition-delay: .3s;
}
</style>

================================================
FILE: src/layouts/components/FHeader.vue
================================================
<template>
    <div class="f-header">
        <span class="logo">
            <el-icon class="mr-1">
                <eleme-filled />
            </el-icon>
            追梦编程
        </span>
        <el-icon class="icon-btn" @click="$store.commit('handleAsideWidth')">
            <fold v-if="$store.state.asideWidth == '250px'"/>
            <Expand v-else/>
        </el-icon>
        <el-tooltip effect="dark" content="刷新" placement="bottom">
            <el-icon class="icon-btn" @click="handleRefresh">
                <refresh />
            </el-icon>
        </el-tooltip>

        <div class="ml-auto flex items-center">
            <el-tooltip effect="dark" content="全屏" placement="bottom">
                <el-icon class="icon-btn" @click="toggle">
                    <full-screen v-if="!isFullscreen" />
                    <aim v-else />
                </el-icon>
            </el-tooltip>

            <el-dropdown class="dropdown" @command="handleCommand">
                <span class="flex items-center text-light-50">
                    <el-avatar class="mr-2" :size="25" :src="$store.state.user.avatar" />
                    {{ $store.state.user.username }}
                    <el-icon class="el-icon--right">
                        <arrow-down />
                    </el-icon>
                </span>
                <template #dropdown>
                    <el-dropdown-menu>
                        <el-dropdown-item command="rePassword">修改密码</el-dropdown-item>
                        <el-dropdown-item command="logout">退出登录</el-dropdown-item>
                    </el-dropdown-menu>
                </template>
            </el-dropdown>
        </div>
    </div>

    <form-drawer ref="formDrawerRef" title="修改密码" destroyOnClose @submit="onSubmit">
        <el-form ref="formRef" :rules="rules" :model="form" label-width="80px" size="small">
            <el-form-item prop="oldpassword" label="旧密码">
                <el-input v-model="form.oldpassword" placeholder="请输入旧密码"></el-input>
            </el-form-item>
            <el-form-item prop="password" label="新密码">
                <el-input type="password" v-model="form.password" placeholder="请输入密码" show-password></el-input>
            </el-form-item>
            <el-form-item prop="repassword" label="确认密码">
                <el-input type="password" v-model="form.repassword" placeholder="请输入确认密码" show-password></el-input>
            </el-form-item>
        </el-form>
    </form-drawer>

</template>
<script setup>
import FormDrawer from '~/components/FormDrawer.vue'
import { useFullscreen } from '@vueuse/core'
import { useRepassword,useLogout } from "~/composables/useManager"
const {
    // 是否全屏状态
    isFullscreen,
    // 切换全屏
    toggle
} = useFullscreen()

const {
    formDrawerRef,
    form,
    rules,
    formRef,
    onSubmit,
    openRePasswordForm
} = useRepassword()

const {
    handleLogout
} = useLogout()

const handleCommand = (c) => {
    switch (c) {
        case "logout":
            handleLogout()
            break;
        case "rePassword":
            openRePasswordForm()
            break;
    }
}

// 刷新
const handleRefresh = () => location.reload()

</script>
<style>
.f-header {
    @apply flex items-center  text-light-50 fixed top-0 left-0 right-0;
    height: 64px;
    background-color: #1b91ff;
}

.logo {
    width: 250px;
    @apply flex justify-center items-center text-xl font-thin;
}

.icon-btn {
    @apply flex justify-center items-center;
    width: 42px;
    height: 64px;
    cursor: pointer;
}

.icon-btn:hover {
    @apply bg-indigo-600;
}

.f-header .dropdown {
    height: 64px;
    cursor: pointer;
    @apply flex justify-center items-center mx-5;
}
</style>

================================================
FILE: src/layouts/components/FMenu.vue
================================================
<template>
    <div class="f-menu" :style="{ width: $store.state.asideWidth }">
        <el-menu :default-active="defaultActive" unique-opened :collapse="isCollapse" default-active="2"
            class="border-0" @select="handleSelect" :collapse-transition="false">

            <template v-for="(item, index) in asideMenus" :key="index">
                <el-sub-menu v-if="item.child && item.child.length > 0" :index="item.name">
                    <template #title>
                        <el-icon>
                            <component :is="item.icon"></component>
                        </el-icon>
                        <span>{{ item.name }}</span>
                    </template>
                    <el-menu-item v-for="(item2, index2) in item.child" :key="index2" :index="item2.frontpath">
                        <el-icon>
                            <component :is="item2.icon"></component>
                        </el-icon>
                        <span>{{ item2.name }}</span>
                    </el-menu-item>
                </el-sub-menu>

                <el-menu-item v-else :index="item.frontpath">
                    <el-icon>
                        <component :is="item.icon"></component>
                    </el-icon>
                    <span>{{ item.name }}</span>
                </el-menu-item>
            </template>
        </el-menu>
    </div>
</template>
<script setup>
import { computed, ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useStore } from 'vuex';
const router = useRouter()
const store = useStore()
const route = useRoute()

// 默认选中
const defaultActive = ref(route.path)

// 是否折叠
const isCollapse = computed(() => !(store.state.asideWidth == '250px'))

const asideMenus = computed(() =>
    store.state.menus
)

const handleSelect = (e) => {
    router.push(e)
}
</script>
<style>
.f-menu {
    transition: all 0.3s;
    top: 64px;
    bottom: 0;
    left: 0;
    overflow-y: auto;
    overflow-x: hidden;
    @apply shadow-md fixed bg-light-50;
}

.f-menu::-webkit-scrollbar {
    width: 0px;
}
</style>

================================================
FILE: src/layouts/components/FTagList.vue
================================================
<template>
    <div class="f-tag-list" :style="{ left: $store.state.asideWidth }">

        <el-tabs v-model="activeTab" type="card" class="flex-1" @tab-remove="removeTab" style="min-width:100px;"
            @tab-change="changeTab">
            <el-tab-pane :closable="item.path != '/'" v-for="item in tabList" :key="item.path" :label="item.title"
                :name="item.path"></el-tab-pane>
        </el-tabs>

        <span class="tag-btn">
            <el-dropdown @command="handleClose">
                <span class="el-dropdown-link">
                    <el-icon>
                        <arrow-down />
                    </el-icon>
                </span>
                <template #dropdown>
                    <el-dropdown-menu>
                        <el-dropdown-item command="clearOther">关闭其他</el-dropdown-item>
                        <el-dropdown-item command="clearAll">全部关闭</el-dropdown-item>
                    </el-dropdown-menu>
                </template>
            </el-dropdown>
        </span>
    </div>
    <div style="height:44px;"></div>
</template>
<script setup>
import { useTabList } from "~/composables/useTabList.js";

const { activeTab,
    tabList,
    changeTab,
    removeTab,
    handleClose } = useTabList()
</script>
<style scoped>
.f-tag-list {
    @apply fixed bg-gray-100 flex items-center px-2;
    top: 64px;
    right: 0;
    height: 44px;
    z-index: 100;
}

.tag-btn {
    @apply bg-white rounded ml-auto flex items-center justify-center px-2;
    height: 32px;
}

:deep(.el-tabs__header) {
    border: 0 !important;
    @apply mb-0;
}

:deep(.el-tabs__nav) {
    border: 0 !important;
}

:deep(.el-tabs__item) {
    border: 0 !important;
    height: 32px;
    line-height: 32px;
    @apply bg-white mx-1 rounded;
}

:deep(.el-tabs__nav-next),
:deep(.el-tabs__nav-prev) {
    line-height: 32px;
    height: 32px;
}

:deep(.is-disabled) {
    cursor: not-allowed;
    @apply text-gray-300;
}
</style>

================================================
FILE: src/main.js
================================================
import { createApp } from "vue";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import App from "./App.vue";
import { router } from "./router";
import store from "./store";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
const app = createApp(App);
app.use(store);
app.use(router);

app.use(ElementPlus);

for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component);
}
import "virtual:windi.css";

import "./permission";

import "nprogress/nprogress.css";

app.mount("#app");


================================================
FILE: src/pages/404.vue
================================================
<template>
    <div class="main">
        <a class="btn" href="#" @click="$router.push('/')">
            返回网站首页
        </a>
        <!-- <el-result
            icon="warning"
            title="404提示"
            sub-title="你找的页面走丢了~"
        >
            <template #extra>
                <el-button type="primary" @click="$router.push('/')">回到首页</el-button>
            </template>
        </el-result> -->
    </div>
</template>
<style>
.main {
    width: 100vw;
    height: 100vh;
    background: url("/public/404.png") center fixed;
    position: relative;
}

.main .btn {
    position: absolute;
    max-width: 138px;
    margin-top: 27%;
    margin-left: 12%;

    border: 2px solid #fff;
    display: flex;
    padding: 12px 19px;

    font-size: 16px;
    font-weight: 700;
    text-decoration: none;
    -webkit-border-radius: 50px;
    -moz-border-radius: 50px;

    color: #fff;
    text-align: center;
    white-space: nowrap;
    vertical-align: middle;
    touch-action: manipulation;
    cursor: pointer;
}
</style>

================================================
FILE: src/pages/category/list.vue
================================================
<template>
    <div>
        分类列表
    </div>
</template>

<script setup>

</script>


================================================
FILE: src/pages/goods/list.vue
================================================
<template>
    <div>
        商品管理
    </div>
</template>

================================================
FILE: src/pages/index.vue
================================================
<template>
    <div>
        后台首页 
        
        {{ $store.state.user.username }}
    </div>
</template>
<script setup>
    
</script>

================================================
FILE: src/pages/login.vue
================================================
<template>
    <el-row class="login-container">
        <el-col :lg="16" :md="12" class="left">
            <div>
                <div>欢迎光临 🎓</div>
                <div>此站点是《vue3 + vite实战商城后台开发》的演示地址</div>
            </div>
        </el-col>
        <el-col :lg="8" :md="12" class="right">
            <h2 class="title">欢迎回来</h2>
            <div>
                <span class="line"></span>
                <span>账号密码登录</span>
                <span class="line"></span>
            </div>
            <el-form ref="formRef" :rules="rules" :model="form" class="w-[250px]">
                <el-form-item prop="username">
                    <el-input v-model="form.username" placeholder="请输入用户名">
                        <template #prefix>
                            <el-icon>
                                <user />
                            </el-icon>
                        </template>
                    </el-input>
                </el-form-item>
                <el-form-item prop="password">
                    <el-input type="password" v-model="form.password" placeholder="请输入密码" show-password>
                        <template #prefix>
                            <el-icon>
                                <lock />
                            </el-icon>
                        </template>
                    </el-input>
                </el-form-item>
                <el-form-item>
                    <el-button round color="#626aef" class="w-[250px]" type="primary" @click="onSubmit"
                        :loading="loading">登 录</el-button>
                </el-form-item>
            </el-form>
        </el-col>
    </el-row>
</template>

<script setup>
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
import { toast } from '~/composables/util'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'

const store = useStore()
const router = useRouter()

// do not use same name with ref
const form = reactive({
    username: "",
    password: ""
})

const rules = {
    username: [
        {
            required: true,
            message: '用户名不能为空',
            trigger: 'blur'
        },
    ],
    password: [
        {
            required: true,
            message: '用户名不能为空',
            trigger: 'blur'
        },
    ]
}

const formRef = ref(null)
const loading = ref(false)
const onSubmit = () => {
    formRef.value.validate((valid) => {
        if (!valid) {
            return false
        }
        loading.value = true

        store.dispatch("login", form).then(res => {
            toast("登录成功")
            router.push("/")
        }).finally(() => {
            loading.value = false
        })
    })
}

// 监听回车事件
function onKeyUp(e) {
    if (e.key == "Enter") onSubmit()
}

// 添加键盘监听
onMounted(() => {
    document.addEventListener("keyup", onKeyUp)
})
// 移除键盘监听
onBeforeUnmount(() => {
    document.removeEventListener("keyup", onKeyUp)
})

</script>

<style scoped>
.login-container {
    @apply min-h-screen;
    background-image: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);
}

.login-container .left,
.login-container .right {
    @apply flex items-center justify-center;
}

.login-container .right {
    @apply bg-light-50 flex-col;
}

.left>div>div:first-child {
    @apply font-bold text-5xl text-light-50 mb-4;
}

.left>div>div:last-child {
    @apply text-gray-200 text-sm;
}

.right .title {
    @apply font-bold text-3xl text-gray-800;
}

.right>div {
    @apply flex items-center justify-center my-5 text-gray-300 space-x-2;
}

.right .line {
    @apply h-[1px] w-16 bg-gray-200;
}
</style>

================================================
FILE: src/permission.js
================================================
import { router, addRoutes } from "~/router";
import { getToken } from "~/composables/auth";
import { toast, showFullLoading, hideFullLoading } from "~/composables/util";
import store from "./store";

// 全局前置守卫
let hasGetInfo = false;
router.beforeEach(async (to, from, next) => {
  // 显示loading
  showFullLoading();

  const token = getToken();

  // 没有登录,强制跳转回登录页
  if (!token && to.path != "/login") {
    toast("请先登录", "error");
    return next({ path: "/login" });
  }

  // 防止重复登录
  if (token && to.path == "/login") {
    toast("请勿重复登录", "error");
    return next({ path: from.path ? from.path : "/" });
  }

  // 如果用户登录了,自动获取用户信息,并存储在vuex当中
  let hasNewRoutes = false;
  if (token && !hasGetInfo) {
    let { menus } = await store.dispatch("getinfo");
    hasGetInfo = true;
    //动态添加路由
    hasNewRoutes = addRoutes(menus);
  }

  // 设置页面标题
  let title = (to.meta.title ? to.meta.title : "Vue3") + "-追梦编程商城后台";
  document.title = title;

  hasNewRoutes ? next(to.fullPath) : next();
});

// 全局后置守卫
router.afterEach((to, from) => hideFullLoading());


================================================
FILE: src/router/index.js
================================================
import { createRouter, createWebHashHistory } from "vue-router";

import Admin from "~/layouts/admin.vue";
import Index from "~/pages/index.vue";
import Login from "~/pages/login.vue";
import NotFound from "~/pages/404.vue";
import GoodList from "~/pages/goods/list.vue";
import CategoryList from "~/pages/category/list.vue";

/* const routes = [
    {
        path:"/",
        component:Admin,
        // 子路由
        children:[]
    },
{
    path:"/login",
    component:Login,
    meta:{
        title:"登录页"
    }
},{
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound
}] */

//默认路由,所有用户共享
const routes = [
  {
    path: "/",
    name: "admin",
    component: Admin,
  },
  {
    path: "/login",
    component: Login,
    meta: {
      title: "登录页",
    },
  },
  {
    path: "/:pathMatch(.*)*",
    name: "NotFound",
    component: NotFound,
  },
];

//动态匹配添加路由
const asyncRoutes = [
  {
    path: "/",
    name: "/",
    component: Index,
    meta: {
      title: "后台首页",
    },
  },
  {
    path: "/goods/list",
    name: "/goods/list",
    component: GoodList,
    meta: {
      title: "商品管理",
    },
  },
  {
    path: "/category/list",
    name: "/category/list",
    component: CategoryList,
    meta: {
      title: "分类列表",
    },
  },
];

export const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

//动态添加路由方法
export function addRoutes(menus) {
  //是否有新的路由
  let hasNewRoutes = false;
  const findAndAddRoutesByMenus = (arr) => {
    arr.forEach((e) => {
      let item = asyncRoutes.find((o) => o.path == e.frontpath);
      if (item && !router.hasRoute(item.path)) {
        router.addRoute("admin", item);
        hasNewRoutes = true;
      }
      //子路由
      if (e.child && e.child.length > 0) {
        findAndAddRoutesByMenus(e.child);
      }
    });
  };
  findAndAddRoutesByMenus(menus)

  // console.log("获取路由",router.getRoutes());
  return hasNewRoutes;
}


================================================
FILE: src/store/index.js
================================================
import { createStore } from "vuex";
import { login, getinfo } from "~/api/manager";
import { setToken, removeToken } from "~/composables/auth";
const store = createStore({
  state() {
    return {
      // 用户信息
      user: {},

      // 侧边宽度
      asideWidth: "250px",
      menus: [],
      ruleNames: [],
    };
  },
  mutations: {
    // 记录用户信息
    SET_USERINFO(state, user) {
      state.user = user;
    },
    // 展开/缩起侧边
    handleAsideWidth(state) {
      state.asideWidth = state.asideWidth == "250px" ? "64px" : "250px";
    },
    SET_MENUS(state, menu) {
      state.menus = menu;
    },
    SET_RULENAMES(state, ruleNames) {
      state.ruleNames = ruleNames;
    },
  },
  actions: {
    // 登录
    login({ commit }, { username, password }) {
      return new Promise((resolve, reject) => {
        login(username, password)
          .then((res) => {
            setToken(res.token);

            resolve(res);
          })
          .catch((err) => reject(err));
      });
    },
    // 获取当前登录用户信息
    getinfo({ commit }) {
      return new Promise((resolve, reject) => {
        getinfo()
          .then((res) => {
            console.log(res);
            commit("SET_USERINFO", res);
            commit("SET_MENUS", res.menus);
            commit("SET_RULENAMES", res.ruleNames);
            resolve(res);
          })
          .catch((err) => reject(err));
      });
    },
    // 退出登录
    logout({ commit }) {
      // 移除cookie里的token
      removeToken();
      // 清除当前用户状态 vuex
      commit("SET_USERINFO", {});
    },
  },
});

export default store;


================================================
FILE: vite.config.js
================================================
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import WindiCSS from 'vite-plugin-windicss'

import path from "path"

// https://vitejs.dev/config/
export default defineConfig({
  resolve:{
    // src目录别名
    alias:{
      "~":path.resolve(__dirname,"src"),
    }
  },

  server:{
    proxy:{
      '/api': {
        target: 'http://ceshi13.dishait.cn',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      },
    }
  },

  plugins: [vue(),WindiCSS()]
})
Download .txt
gitextract_4pghmfdd/

├── .gitignore
├── LICENSE
├── README.md
├── index.html
├── package.json
├── src/
│   ├── App.vue
│   ├── api/
│   │   └── manager.js
│   ├── axios.js
│   ├── components/
│   │   ├── FormDrawer.vue
│   │   └── HelloWorld.vue
│   ├── composables/
│   │   ├── auth.js
│   │   ├── useManager.js
│   │   ├── useTabList.js
│   │   └── util.js
│   ├── layouts/
│   │   ├── admin.vue
│   │   └── components/
│   │       ├── FHeader.vue
│   │       ├── FMenu.vue
│   │       └── FTagList.vue
│   ├── main.js
│   ├── pages/
│   │   ├── 404.vue
│   │   ├── category/
│   │   │   └── list.vue
│   │   ├── goods/
│   │   │   └── list.vue
│   │   ├── index.vue
│   │   └── login.vue
│   ├── permission.js
│   ├── router/
│   │   └── index.js
│   └── store/
│       └── index.js
└── vite.config.js
Download .txt
SYMBOL INDEX (23 symbols across 7 files)

FILE: src/api/manager.js
  function login (line 3) | function login(username,password){
  function getinfo (line 10) | function getinfo(){
  function logout (line 14) | function logout(){
  function updatepassword (line 18) | function updatepassword(data){

FILE: src/composables/auth.js
  function getToken (line 6) | function getToken(){
  function setToken (line 11) | function setToken(token){
  function removeToken (line 16) | function removeToken(){

FILE: src/composables/useManager.js
  function useRepassword (line 7) | function useRepassword() {
  function useLogout (line 75) | function useLogout() {

FILE: src/composables/useTabList.js
  function useTabList (line 6) | function useTabList() {

FILE: src/composables/util.js
  function toast (line 4) | function toast(message,type = "success",dangerouslyUseHTMLString = false){
  function showFullLoading (line 14) | function showFullLoading(){
  function hideFullLoading (line 19) | function hideFullLoading(){
  function showModal (line 23) | function showModal(content = "提示内容",type = "warning",title = ""){

FILE: src/router/index.js
  function addRoutes (line 84) | function addRoutes(menus) {

FILE: src/store/index.js
  method state (line 5) | state() {
  method SET_USERINFO (line 18) | SET_USERINFO(state, user) {
  method handleAsideWidth (line 22) | handleAsideWidth(state) {
  method SET_MENUS (line 25) | SET_MENUS(state, menu) {
  method SET_RULENAMES (line 28) | SET_RULENAMES(state, ruleNames) {
  method login (line 34) | login({ commit }, { username, password }) {
  method getinfo (line 46) | getinfo({ commit }) {
  method logout (line 60) | logout({ commit }) {
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (37K chars).
[
  {
    "path": ".gitignore",
    "chars": 257,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": "LICENSE",
    "chars": 1063,
    "preview": "MIT License\n\nCopyright (c) 2022 是大赵同学鸭\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
  },
  {
    "path": "README.md",
    "chars": 787,
    "preview": "<p align=\"center\" dir=\"auto\"><a href=\"https://vuejs.org\" rel=\"nofollow\"><img  src=\"./public/Vue.png\" alt=\"Vue logo\" data"
  },
  {
    "path": "index.html",
    "chars": 340,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <"
  },
  {
    "path": "package.json",
    "chars": 685,
    "preview": "{\n  \"name\": \"shop-admin\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite "
  },
  {
    "path": "src/App.vue",
    "chars": 181,
    "preview": "<script setup>\n\n</script>\n\n<template>\n  <router-view></router-view>\n</template>\n\n<style>\n#nprogress .bar{\n  background-c"
  },
  {
    "path": "src/api/manager.js",
    "chars": 384,
    "preview": "import axios from '~/axios'\n\nexport function login(username,password){\n    return axios.post(\"/admin/login\",{\n        us"
  },
  {
    "path": "src/axios.js",
    "chars": 860,
    "preview": "import axios from \"axios\"\nimport { toast } from '~/composables/util'\nimport { getToken } from '~/composables/auth'\nimpor"
  },
  {
    "path": "src/components/FormDrawer.vue",
    "chars": 1811,
    "preview": "<template>\n    <el-drawer v-model=\"showDrawer\" \n    :title=\"title\" \n    :size=\"size\" \n    :close-on-click-modal=\"false\"\n"
  },
  {
    "path": "src/components/HelloWorld.vue",
    "chars": 768,
    "preview": "<script setup>\nimport { ref } from 'vue'\n\ndefineProps({\n  msg: String\n})\n\nconst count = ref(0)\n</script>\n\n<template>\n  <"
  },
  {
    "path": "src/composables/auth.js",
    "chars": 358,
    "preview": "import { useCookies } from '@vueuse/integrations/useCookies'\nconst TokenKey = \"admin-token\"\nconst cookie = useCookies()\n"
  },
  {
    "path": "src/composables/useManager.js",
    "chars": 2203,
    "preview": "import { ref, reactive } from 'vue'\nimport { logout, updatepassword } from \"~/api/manager\"\nimport { showModal, toast } f"
  },
  {
    "path": "src/composables/useTabList.js",
    "chars": 2130,
    "preview": "import { ref } from \"vue\";\nimport { useRoute, onBeforeRouteUpdate } from \"vue-router\";\nimport { useCookies } from \"@vueu"
  },
  {
    "path": "src/composables/util.js",
    "chars": 699,
    "preview": "import { ElNotification,ElMessageBox } from 'element-plus'\nimport nprogress from 'nprogress'\n// 消息提示\nexport function toa"
  },
  {
    "path": "src/layouts/admin.vue",
    "chars": 1172,
    "preview": "<template>\n    <el-container>\n        <el-header>\n            <f-header />\n        </el-header>\n        <el-container>\n "
  },
  {
    "path": "src/layouts/components/FHeader.vue",
    "chars": 3705,
    "preview": "<template>\n    <div class=\"f-header\">\n        <span class=\"logo\">\n            <el-icon class=\"mr-1\">\n                <el"
  },
  {
    "path": "src/layouts/components/FMenu.vue",
    "chars": 2090,
    "preview": "<template>\n    <div class=\"f-menu\" :style=\"{ width: $store.state.asideWidth }\">\n        <el-menu :default-active=\"defaul"
  },
  {
    "path": "src/layouts/components/FTagList.vue",
    "chars": 1959,
    "preview": "<template>\n    <div class=\"f-tag-list\" :style=\"{ left: $store.state.asideWidth }\">\n\n        <el-tabs v-model=\"activeTab\""
  },
  {
    "path": "src/main.js",
    "chars": 565,
    "preview": "import { createApp } from \"vue\";\nimport ElementPlus from \"element-plus\";\nimport \"element-plus/dist/index.css\";\nimport Ap"
  },
  {
    "path": "src/pages/404.vue",
    "chars": 1034,
    "preview": "<template>\n    <div class=\"main\">\n        <a class=\"btn\" href=\"#\" @click=\"$router.push('/')\">\n            返回网站首页\n       "
  },
  {
    "path": "src/pages/category/list.vue",
    "chars": 84,
    "preview": "<template>\n    <div>\n        分类列表\n    </div>\n</template>\n\n<script setup>\n\n</script>\n"
  },
  {
    "path": "src/pages/goods/list.vue",
    "chars": 56,
    "preview": "<template>\n    <div>\n        商品管理\n    </div>\n</template>"
  },
  {
    "path": "src/pages/index.vue",
    "chars": 137,
    "preview": "<template>\n    <div>\n        后台首页 \n        \n        {{ $store.state.user.username }}\n    </div>\n</template>\n<script setu"
  },
  {
    "path": "src/pages/login.vue",
    "chars": 3590,
    "preview": "<template>\n    <el-row class=\"login-container\">\n        <el-col :lg=\"16\" :md=\"12\" class=\"left\">\n            <div>\n      "
  },
  {
    "path": "src/permission.js",
    "chars": 1058,
    "preview": "import { router, addRoutes } from \"~/router\";\nimport { getToken } from \"~/composables/auth\";\nimport { toast, showFullLoa"
  },
  {
    "path": "src/router/index.js",
    "chars": 1932,
    "preview": "import { createRouter, createWebHashHistory } from \"vue-router\";\n\nimport Admin from \"~/layouts/admin.vue\";\nimport Index "
  },
  {
    "path": "src/store/index.js",
    "chars": 1573,
    "preview": "import { createStore } from \"vuex\";\nimport { login, getinfo } from \"~/api/manager\";\nimport { setToken, removeToken } fro"
  },
  {
    "path": "vite.config.js",
    "chars": 515,
    "preview": "import { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport WindiCSS from 'vite-plugin-windicss'\n\nim"
  }
]

About this extraction

This page contains the full source code of the webyang-male/vue3-mallManage GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (31.2 KB), approximately 9.1k tokens, and a symbol index with 23 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!