diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4e1edd1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.development b/.env.development index 2d7a8a1..c2bf3b2 100644 --- a/.env.development +++ b/.env.development @@ -5,4 +5,4 @@ VITE_APP_TITLE=里派基础框架 VITE_APP_ENV='development' # 开发环境 -VITE_APP_BASE_API='/dev-api' \ No newline at end of file +VITE_API_BASE='/dev-api' diff --git a/.env.production b/.env.production index 3761888..9cdad2b 100644 --- a/.env.production +++ b/.env.production @@ -5,7 +5,7 @@ VITE_APP_TITLE=里派基础框架 VITE_APP_ENV='production' # 生产环境 -VITE_APP_BASE_API='/prod-api' +VITE_API_BASE='/prod-api' # 是否在打包时开启压缩,支持 gzip 和 brotli -VITE_BUILD_COMPRESS=gzip \ No newline at end of file +VITE_BUILD_COMPRESS=gzip diff --git a/.env.staging b/.env.staging index a2c5eba..a8eb331 100644 --- a/.env.staging +++ b/.env.staging @@ -5,7 +5,7 @@ VITE_APP_TITLE=里派基础框架 VITE_APP_ENV='staging' # 生产环境 -VITE_APP_BASE_API='/staging-api' +VITE_API_BASE='/staging-api' # 是否在打包时开启压缩,支持 gzip 和 brotli -VITE_BUILD_COMPRESS=gzip \ No newline at end of file +VITE_BUILD_COMPRESS=gzip diff --git a/package.json b/package.json index b81de03..503bcfb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "admin", "version": "1.0.0", - "description": "one basic framework", + "description": "a basic framework", "author": "cfn", "type": "module", "scripts": { @@ -28,6 +28,7 @@ "@vitejs/plugin-vue": "^5.2.4", "eslint": "^9.27.0", "sass": "^1.89.0", + "typescript": "^5.8.3", "vite": "^6.3.5" }, "license": "MIT" diff --git a/public/images/404.png b/public/images/404.png new file mode 100644 index 0000000..47197ec Binary files /dev/null and b/public/images/404.png differ diff --git a/src/api/index.ts b/src/api/index.ts index fc4edcb..e52b365 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,8 +1,9 @@ -const modules = import.meta.glob('./model/**/*.ts') // 异步加载 - -// 使用示例 -const loadModel = async (modelName: string) => { - const path = `./model/${modelName}.ts` - const module = await modules[path]() - return module.default -} \ No newline at end of file +const files = import.meta.glob('./model/*.ts', { + eager: true, + import: 'default' // 可选,指定要导入的 export +}) +const modules = {} +for (const path in files) { + modules[path.replace(/(\.\/model\/|\.ts)/g, '')] = files[path] +} +export default modules diff --git a/src/api/model/auth.ts b/src/api/model/auth.ts new file mode 100644 index 0000000..226fd48 --- /dev/null +++ b/src/api/model/auth.ts @@ -0,0 +1,10 @@ +import http from "@/utils/request" + +export default { + login: async function (data = {}) { + return await http.post("v1/login", data); + }, + menu: async function (data = {}) { + return await http.get("v1/menu", data) + } +} diff --git a/src/config/index.ts b/src/config/index.ts index c7fc925..cdde218 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,4 +1,16 @@ export default { + // + APP_NAME: import.meta.env.VITE_APP_TITLE, + //接口地址 + API_URL: import.meta.env.VITE_API_BASE, + //请求超时 + TIMEOUT: 10000, + //请求是否开启缓存 + REQUEST_CACHE: false, //语言 LANG: 'zh-cn', -} \ No newline at end of file + //TokenName + TOKEN_NAME: "Authorization", + //Token前缀,注意最后有个空格,如不需要需设置空字符串 + TOKEN_PREFIX: "Bearer ", +} diff --git a/src/layout/404.vue b/src/layout/404.vue new file mode 100644 index 0000000..432cf07 --- /dev/null +++ b/src/layout/404.vue @@ -0,0 +1,78 @@ + + + + + + + 无权限或找不到页面 + 当前页面无权限访问或者打开了一个不存在的链接,请检查当前账户权限和链接的可访问性。 + 返回首页 + 重新登录 + 返回上一页 + + + + + + + diff --git a/src/layout/empty.vue b/src/layout/empty.vue new file mode 100644 index 0000000..497d470 --- /dev/null +++ b/src/layout/empty.vue @@ -0,0 +1,3 @@ + + + diff --git a/src/layout/index.vue b/src/layout/index.vue new file mode 100644 index 0000000..39f41e0 --- /dev/null +++ b/src/layout/index.vue @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 8377398..3a46182 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,11 +3,11 @@ import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import 'element-plus/theme-chalk/display.css' import i18n from './locales' +import App from './App.vue' -// import leapy from './leapy' // import router from './router' // import store from './store' -import App from './App.vue' +import pi from './pi' const app = createApp(App); @@ -15,7 +15,7 @@ const app = createApp(App); // app.use(router); app.use(ElementPlus); app.use(i18n); -// app.use(leapy); +app.use(pi); //挂载app -app.mount('#app'); \ No newline at end of file +app.mount('#app'); diff --git a/src/pi.ts b/src/pi.ts new file mode 100644 index 0000000..082b13b --- /dev/null +++ b/src/pi.ts @@ -0,0 +1,22 @@ +import config from "./config" +import api from './api' +import tools from './utils/tools' +import http from "./utils/request" + +import * as elIcons from '@element-plus/icons-vue' +import {App} from "vue"; + +export default { + install(app: App) { + //挂载全局对象 + app.config.globalProperties.$CONFIG = config; + app.config.globalProperties.$TOOLS = tools; + app.config.globalProperties.$HTTP = http; + app.config.globalProperties.$API = api; + + //统一注册el-icon图标 + for (let icon in elIcons) { + app.component(`ElIcon${icon}`, elIcons[icon]) + } + } +} diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..3fcf778 --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,191 @@ +import {createRouter, createWebHashHistory} from 'vue-router'; +import { ElNotification } from 'element-plus'; +import config from "@/config" +import NProgress from 'nprogress' +import 'nprogress/nprogress.css' +import tools from '@/utils/tools'; +import api from "@/api"; +import sRouter from './system'; +import {beforeEach, afterEach} from '@/utils/route'; + +//系统路由 +const routes = sRouter + +//系统特殊路由 +const routes_404 = { + path: "/:pathMatch(.*)*", + hidden: true, + component: () => import(/* webpackChunkName: "404" */ '@/layout/404'), +} +let routes_404_r = ()=>{} + +const router = createRouter({ + history: createWebHashHistory(), + routes: routes +}) + +//设置标题 +document.title = config.APP_NAME + +//判断是否已加载过动态/静态路由 +var isGetRouter = false; + +router.beforeEach(async (to, from, next) => { + + NProgress.start() + //动态标题 + document.title = to.meta.title ? `${to.meta.title} - ${config.APP_NAME}` : `${config.APP_NAME}` + + let token = tools.data.get("TOKEN"); + + if(to.path === "/login"){ + //删除路由(替换当前layout路由) + router.addRoute(routes[0]) + //删除路由(404) + routes_404_r() + isGetRouter = false; + next(); + return false; + } + + if(routes.findIndex(r => r.path === to.path) >= 0){ + next(); + return false; + } + + if(!token){ + next({ + path: '/login' + }); + return false; + } + + //整页路由处理 + if(to.meta.fullpage){ + to.matched = [to.matched[to.matched.length-1]] + } + console.log(11) + //加载动态/静态路由 + if(!isGetRouter){ + // 动态加载菜单 + const [res, err] = await tools.go(api.auth.menu()) + console.log(res) + if (err) { + return false + } + // tools.data.set("MENU", tools.makeMenu(res.data.menus, 0)) + // tools.data.set("PERMISSIONS", res.data.buttons) + // tools.data.set("ROLE", res.data.roles) + + let apiMenu = tools.data.get("MENU") || [] + let userInfo = tools.data.get("USER_INFO") + let userMenu = treeFilter([], node => { + return node.meta.role ? node.meta.role.filter(item=>userInfo.role.indexOf(item)>-1).length > 0 : true + }) + let menu = [...userMenu, ...apiMenu] + var menuRouter = filterAsyncRouter(menu) + menuRouter = flatAsyncRoutes(menuRouter) + menuRouter.forEach(item => { + router.addRoute("layout", item) + }) + routes_404_r = router.addRoute(routes_404) + if (to.matched.length == 0) { + router.push(to.fullPath); + } + isGetRouter = true; + } + beforeEach(to, from) + next(); +}); + +router.afterEach((to, from) => { + afterEach(to) + NProgress.done() +}); + +router.onError((error) => { + NProgress.done(); + ElNotification.error({ + title: '路由错误', + message: error.message + }); +}); + +//入侵追加自定义方法、对象 +// router.sc_getMenu = () => { +// var apiMenu = tools.data.get("MENU") || [] +// let userInfo = tools.data.get("USER_INFO") +// let userMenu = treeFilter([], node => { +// return node.meta.role ? node.meta.role.filter(item=>userInfo.role.indexOf(item)>-1).length > 0 : true +// }) +// var menu = [...userMenu, ...apiMenu] +// return menu +// } + +//转换 +function filterAsyncRouter(routerMap) { + const accessedRouters = [] + routerMap.forEach(item => { + item.meta = item.meta?item.meta:{}; + //处理外部链接特殊路由 + if(item.meta.type=='iframe'){ + item.meta.url = item.path; + item.path = `/i/${item.name}`; + } + //MAP转路由对象 + var route = { + path: item.path, + name: item.name, + meta: item.meta, + redirect: item.redirect, + children: item.children ? filterAsyncRouter(item.children) : null, + component: loadComponent(item.component) + } + accessedRouters.push(route) + }) + return accessedRouters +} +function loadComponent(component){ + if(component){ + return () => import(`@/views/${component}`) + }else{ + return () => import(`@/layout/empty`) + } + +} + +//路由扁平化 +function flatAsyncRoutes(routes, breadcrumb=[]) { + let res = [] + routes.forEach(route => { + const tmp = {...route} + if (tmp.children) { + let childrenBreadcrumb = [...breadcrumb] + childrenBreadcrumb.push(route) + let tmpRoute = { ...route } + tmpRoute.meta.breadcrumb = childrenBreadcrumb + delete tmpRoute.children + res.push(tmpRoute) + let childrenRoutes = flatAsyncRoutes(tmp.children, childrenBreadcrumb) + childrenRoutes.map(item => { + res.push(item) + }) + } else { + let tmpBreadcrumb = [...breadcrumb] + tmpBreadcrumb.push(tmp) + tmp.meta.breadcrumb = tmpBreadcrumb + res.push(tmp) + } + }) + return res +} + +//过滤树 +function treeFilter(tree, func) { + return tree.map(node => ({ ...node })).filter(node => { + node.children = node.children && treeFilter(node.children, func) + return func(node) || (node.children && node.children.length) + }) +} + +export default router diff --git a/src/router/system.ts b/src/router/system.ts new file mode 100644 index 0000000..7040984 --- /dev/null +++ b/src/router/system.ts @@ -0,0 +1,20 @@ +//系统路由 +import {RouteRecordRaw} from "vue-router"; + +const routes: RouteRecordRaw[] = [ + { + name: "layout", + path: "/", + component: () => import('@/layout'), + redirect: '/dashboard', + children: [] + }, + { + path: "/login", + component: () => import('@/views/system/login'), + meta: { + title: "登录" + } + } +] +export default routes; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..f6142c6 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,15 @@ +import {createStore} from 'vuex'; + +const files = import.meta.glob('./model/*.ts', { + eager: true, + import: 'default' +}) + +const modules = {} +for (const path in files) { + modules[path.replace(/(\.\/model\/|\.ts)/g, '')] = files[path] +} + +export default createStore({ + modules +}); diff --git a/src/store/model/views.ts b/src/store/model/views.ts new file mode 100644 index 0000000..5946559 --- /dev/null +++ b/src/store/model/views.ts @@ -0,0 +1,46 @@ +import router from '@/router' + +export default { + state: { + viewTags: [] + }, + mutations: { + pushViewTags(state, route){ + let backPathIndex = state.viewTags.findIndex(item => item.fullPath == router.options.history.state.back) + let target = state.viewTags.find((item) => item.fullPath === route.fullPath) + let isName = route.name + if(!target && isName){ + if(backPathIndex == -1){ + state.viewTags.push(route) + }else{ + state.viewTags.splice(backPathIndex+1, 0, route) + } + } + }, + removeViewTags(state, route){ + state.viewTags.forEach((item, index) => { + if (item.fullPath === route.fullPath){ + state.viewTags.splice(index, 1) + } + }) + }, + updateViewTags(state, route){ + state.viewTags.forEach((item) => { + if (item.fullPath == route.fullPath){ + item = Object.assign(item, route) + } + }) + }, + updateViewTagsTitle(state, title=''){ + const nowFullPath = location.hash.substring(1) + state.viewTags.forEach((item) => { + if (item.fullPath == nowFullPath){ + item.meta.title = title + } + }) + }, + clearViewTags(state){ + state.viewTags = [] + } + } +} diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..46ae5ae --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,143 @@ +import axios from 'axios'; +import {ElNotification} from 'element-plus'; +import sConfig from "@/config"; +import tools from '@/utils/tools'; +import router from '@/router'; + +// 请求地址 +axios.defaults.baseURL = sConfig.API_URL +// 超时时间 +axios.defaults.timeout = sConfig.TIMEOUT +// 请求拦截 +axios.interceptors.request.use((config) => { + let token = tools.data.get("TOKEN") + if (token) { + config.headers[sConfig.TOKEN_NAME] = sConfig.TOKEN_PREFIX + token + } + // get请求添加时间戳 + if (!sConfig.REQUEST_CACHE && config.method === 'get') { + config.params = config.params || {}; + config.params['_'] = new Date().getTime(); + } + return config; +}) +//响应拦截 +axios.interceptors.response.use((response) => { + let res = response.data + if (res.code == 0) { + return Promise.resolve(res) + } else if (res.code == 1) { // 操作失败拦截 + ElNotification.error({title: '操作失败', message: res.msg}); + return Promise.reject(res) + } else if (res.code == 2) { // 权限不足拦截 + ElNotification.error({title: '权限不足', message: res.msg}); + return Promise.reject(res) + } else { // 登录失效拦截 + ElNotification.error({title: '登录失效', message: res.msg}); + router.replace({path: '/login'}).then(r => { + }); + } +}, (error) => { + if (error.response) { + if (error.response.status === 404) { + ElNotification.error({title: '请求错误', message: "Status:404,正在请求不存在的资源!"}); + } else if (error.response.status === 500) { + ElNotification.error({ + title: '请求错误', + message: error.response.data.message || "Status:500,服务器发生错误!" + }); + } else { + ElNotification.error({ + title: '请求错误', + message: error.message || `Status:${error.response.status},未知错误!` + }); + } + } else { + ElNotification.error({title: '请求错误', message: "请求服务器无响应!"}); + } + return Promise.reject(error.response); +}) + +const http = { + /** get 请求 + * @param url 请求地址 + * @param params 参数 + * @param config 配置 + */ + get: function (url: string, params = {}, config = {}) { + return new Promise((resolve, reject) => { + axios({ + method: 'get', + url: url, + params: params, + ...config + }).then((response) => { + resolve(response); + }).catch((error) => { + reject(error); + }) + }) + }, + + /** post 请求 + * @param url 请求地址 + * @param data 参数 + * @param config 配置 + */ + post: function (url: string, data = {}, config = {}) { + return new Promise((resolve, reject) => { + axios({ + method: 'post', + url: url, + data: data, + ...config + }).then((response) => { + resolve(response); + }).catch((error) => { + reject(error); + }) + }) + }, + + /** put 请求 + * @param url 请求地址 + * @param data 参数 + * @param config 配置 + */ + put: function (url: string, data = {}, config = {}) { + return new Promise((resolve, reject) => { + axios({ + method: 'put', + url: url, + data: data, + ...config + }).then((response) => { + resolve(response); + }).catch((error) => { + reject(error); + }) + }) + }, + + /** delete 请求 + * @param url 请求地址 + * @param params 参数 + * @param config 配置 + */ + delete: function (url: string, params = {}, config = {}) { + return new Promise((resolve, reject) => { + axios({ + method: 'delete', + url: url, + params: params, + ...config + }).then((response) => { + resolve(response); + }).catch((error) => { + reject(error); + }) + }) + } +} + +export default http; diff --git a/src/utils/route.ts b/src/utils/route.ts new file mode 100644 index 0000000..9200cd0 --- /dev/null +++ b/src/utils/route.ts @@ -0,0 +1,27 @@ +// import store from '@/store' +import {nextTick} from 'vue' +import {RouteLocationNormalized, RouteLocationNormalizedLoaded} from "vue-router"; + +export function beforeEach(to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded) { + const adminMain = document.querySelector('#pi-main'); + if (!adminMain) { + return false + } + // store.commit("updateViewTags", { + // fullPath: from.fullPath, + // scrollTop: adminMain.scrollTop + // }) +} + +export function afterEach(to: RouteLocationNormalized) { + const adminMain = document.querySelector('#pi-main'); + if (!adminMain) { + return false + } + // nextTick(() => { + // const beforeRoute = store.state.views.viewTags.filter(v => v.fullPath == to.fullPath)[0]; + // if (beforeRoute) { + // adminMain.scrollTop = beforeRoute.scrollTop || 0 + // } + // }).then(r => {}) +} diff --git a/src/utils/tools.ts b/src/utils/tools.ts index d11969a..a481da9 100644 --- a/src/utils/tools.ts +++ b/src/utils/tools.ts @@ -1,45 +1,53 @@ import CryptoJS from 'crypto-js'; const tools = { - data: { - set(cacheKey, data, expireIn = 0) { - let cacheValue = { - content: data, - expireIn: expireIn === 0 ? 0 : new Date().getTime() + expireIn * 1000 - } - return localStorage.setItem(cacheKey, tools.base64.encrypt(JSON.stringify(cacheValue))) - }, - get(cacheKey) { - try { - const cacheValue = JSON.parse(tools.base64.decrypt(localStorage.getItem(cacheKey))) - if (cacheValue) { - let nowTime = new Date().getTime() - if (nowTime > cacheValue.expireIn && cacheValue.expireIn !== 0) { - localStorage.removeItem(cacheKey) - return null; - } - return cacheValue.content - } - return null - } catch (err) { - return null - } - }, - remove(cacheKey) { - return localStorage.removeItem(cacheKey) - }, - clear() { - return localStorage.clear() - } - }, - base64: { - encrypt(data){ - return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data)) - }, - decrypt(cipher){ - return CryptoJS.enc.Base64.parse(cipher).toString(CryptoJS.enc.Utf8) - } - } + data: { + set(cacheKey, data, expireIn = 0) { + let cacheValue = { + content: data, + expireIn: expireIn === 0 ? 0 : new Date().getTime() + expireIn * 1000 + } + return localStorage.setItem(cacheKey, tools.base64.encrypt(JSON.stringify(cacheValue))) + }, + get(cacheKey) { + try { + const cacheValue = JSON.parse(tools.base64.decrypt(localStorage.getItem(cacheKey))) + if (cacheValue) { + let nowTime = new Date().getTime() + if (nowTime > cacheValue.expireIn && cacheValue.expireIn !== 0) { + localStorage.removeItem(cacheKey) + return null; + } + return cacheValue.content + } + return null + } catch (err) { + return null + } + }, + remove(cacheKey) { + return localStorage.removeItem(cacheKey) + }, + clear() { + return localStorage.clear() + } + }, + base64: { + encrypt(data) { + return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data)) + }, + decrypt(cipher) { + return CryptoJS.enc.Base64.parse(cipher).toString(CryptoJS.enc.Utf8) + } + }, + go: async function (fn: Function) { + try { + let res = await fn + return [res, null] + } catch (err) { + return [null, err] + } + } } -export default tools \ No newline at end of file +export default tools diff --git a/src/views/system/login/index.vue b/src/views/system/login/index.vue new file mode 100644 index 0000000..60eee98 --- /dev/null +++ b/src/views/system/login/index.vue @@ -0,0 +1,13 @@ + + + 12312 + + + + + + diff --git a/tsconfig.json b/tsconfig.json index 7ae935f..f251303 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,17 @@ { - "compilerOptions": { - "module": "ESNext", - "moduleResolution": "node", - "types": ["node"], - "baseUrl": "./", - "paths": { - "@/*": ["src/*"] - }, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true - } -} \ No newline at end of file + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "node", + "types": [ + "node" + ], + "baseUrl": "./", + "paths": { + "@/*": [ + "src/*" + ] + }, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + } +} diff --git a/vite-env.d.ts b/vite-env.d.ts index ebb3c5e..70300b5 100644 --- a/vite-env.d.ts +++ b/vite-env.d.ts @@ -1,5 +1,28 @@ -declare module "*.vue" { - import { DefineComponent } from "vue"; - const component: DefineComponent<{}, {}, any>; - export default component; -} \ No newline at end of file +// 如果使用 Vite 4+ 推荐官方类型 +interface ImportMeta { + readonly glob: ( + pattern: string, + options?: { eager?: boolean; import?: string, query?: string } + ) => Record +} + +interface ImportMetaEnv { + readonly VITE_API_BASE: string + readonly VITE_APP_TITLE: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} + +declare module '@/*' { + import { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/vite.config.ts b/vite.config.ts index cf21991..d1f2b87 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,6 @@ import {defineConfig, loadEnv} from 'vite' import vue from '@vitejs/plugin-vue' -import * as path from 'path' +import path from 'path' export default defineConfig(({mode, command}) => { const env = loadEnv(mode, process.cwd()) @@ -25,7 +25,7 @@ export default defineConfig(({mode, command}) => { // open: true, proxy: { '/api': { - target: 'http://dev.api.eswhyf.cn', + target: 'https://mock.apipost.net/mock/2a749651c864000/', changeOrigin: true, rewrite: (p) => p.replace(/^\/api/, '') } @@ -54,4 +54,4 @@ export default defineConfig(({mode, command}) => { } } } -}) \ No newline at end of file +})
当前页面无权限访问或者打开了一个不存在的链接,请检查当前账户权限和链接的可访问性。