commit 355cc0532ac728fa02a3fe9fc70bca9b87e53571 Author: lyc Date: Mon Jul 8 16:24:56 2024 +0800 init diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3dce414 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..6da57b4 --- /dev/null +++ b/.env.development @@ -0,0 +1,12 @@ +# 页面标题 +VITE_APP_TITLE = AIx智慧教育 + +# 开发环境配置 +VITE_APP_ENV = 'development' + +# AIx融合数字管理系统/开发环境 +VITE_APP_BASE_API = '/dev-api' + +VITE_APP_RES_FILE_PATH = 'https://file.ysaix.com:7868/src/assets/textbook/booktxt/' + +VITE_APP_BUILD_BASE_PATH = 'https://file.ysaix.com:7868/' \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..a6f34fe --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +node_modules +dist +out +.gitignore diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..55db58d --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,15 @@ +/* eslint-env node */ +require('@rushstack/eslint-patch/modern-module-resolution') + +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:vue/vue3-recommended', + '@electron-toolkit', + '@vue/eslint-config-prettier' + ], + rules: { + 'vue/require-default-prop': 'off', + 'vue/multi-word-component-names': 'off' + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8d8189 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +node_modules +dist +out +.DS_Store +*.log* + +/node_modules/ +/.idea/.gitignore +/.idea/Alx.iml +/.idea/modules.xml +/.idea/vcs.xml +/dist/ +/.idea/workspace.xml + +yarn.lock + +pnpm-lock.yaml + +node_modules +.vscode/* +!.vscode/extensions.json + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +/.idea/deployment.xml +/.idea/encodings.xml +/.idea/misc.xml +/dist.zip +/package-lock.json +/pnpm-lock.json +/build +/out \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..34862ff --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +electron_mirror=https://npmmirror.com/mirrors/electron/ +electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9c6b791 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +out +dist +pnpm-lock.yaml +LICENSE.md +tsconfig.json +tsconfig.*.json diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..35893b3 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,4 @@ +singleQuote: true +semi: false +printWidth: 100 +trailingComma: none diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..940260d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["dbaeumer.vscode-eslint"] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae002ac --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# electron-app + +- [Vue3](https://cn.vuejs.org) + [ElementPlus](https://element-plus.org/zh-CN/) + [electron-vite](https://cn-evite.netlify.app/) + [Electron](https://www.electronjs.org/zh/) + + + +### 安装 + +```bash +$ npm install +``` + +### 启动 + +```bash +$ npm run dev +``` + +### 打包 + +```bash +# For windows +$ npm run build:win + +# For macOS +$ npm run build:mac + +# For Linux +$ npm run build:linux +``` + +### 项目目录结构 + +```bash +├── build // 编译过程输出文件目录 +├── dist // 打包后输出目录 +├── node_modules // 依赖模块 +├── out //编译过程输出文件目录 +├── resources // 公共资源文件,主进程使用 +│ └── icon.png //默认图标 +├──src +│ ├── main // 主进程开发目录 +│ │ └── index.js //主进程入口文件 +│ ├── preload // 预加载脚本开发目录 +│ │ └── index.js // 预加载默认脚本 +│ └── renderer // 渲染进程开发目录,类似纯web项目根目录 +│ ├── src +│ │ ├── assets //资源文件目录 +│ │ ├── components //组件目录 +│ │ ├── App.vue // 入口页面 +│ │ └── main.js // 入口文件 +│ └── index.js.html // 默认html文件 +├── .editorconfig +├── .eslintignore //eslint代码检查忽略配置文件 +├── .eslintrc.cjs //eslint代码检查配置文件 +├── .gitignore //git忽略配置文件 +├── .npmrc // npm源配置文件 +├── .prettierignore //prettier代码格式化忽略配置文件 +├── .prettierrc.yaml //prettier代码格式化配置文件 +├── dev-app-update.yml +├── electron-builder.yml //打包配置文件 +├──electron.vite.config.mjs //electron-vite配置文件 +├── package-lock.json +├── package.json +└──README.md //项目说明 + +``` + diff --git a/dev-app-update.yml b/dev-app-update.yml new file mode 100644 index 0000000..0a21494 --- /dev/null +++ b/dev-app-update.yml @@ -0,0 +1,3 @@ +provider: generic +url: https://example.com/auto-updates +updaterCacheDirName: electron-app-updater diff --git a/electron-builder.yml b/electron-builder.yml new file mode 100644 index 0000000..5c1d02b --- /dev/null +++ b/electron-builder.yml @@ -0,0 +1,46 @@ +appId: com.electron.app +productName: electron-app +directories: + buildResources: build +files: + - '!**/.vscode/*' + - '!src/*' + - '!electron.vite.config.{js,ts,mjs,cjs}' + - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' + - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' +asarUnpack: + - resources/** +win: + executableName: electron-app +nsis: + oneClick: false + allowToChangeInstallationDirectory: true + artifactName: ${name}-${version}-setup.${ext} + shortcutName: ${productName} + uninstallDisplayName: ${productName} + createDesktopShortcut: always +mac: + entitlementsInherit: build/entitlements.mac.plist + extendInfo: + - NSCameraUsageDescription: Application requests access to the device's camera. + - NSMicrophoneUsageDescription: Application requests access to the device's microphone. + - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. + - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. + notarize: false +dmg: + artifactName: ${name}-${version}.${ext} +linux: + target: + - AppImage + - snap + - deb + maintainer: electronjs.org + category: Utility +appImage: + artifactName: ${name}-${version}.${ext} +npmRebuild: false +publish: + provider: generic + url: https://example.com/auto-updates +electronDownload: + mirror: https://npmmirror.com/mirrors/electron/ diff --git a/electron.vite.config.mjs b/electron.vite.config.mjs new file mode 100644 index 0000000..4390ed1 --- /dev/null +++ b/electron.vite.config.mjs @@ -0,0 +1,33 @@ +import { resolve } from 'path' +// const path = require('path') +import { defineConfig, externalizeDepsPlugin } from 'electron-vite' +import vue from '@vitejs/plugin-vue' +import WindiCSS from "vite-plugin-windicss" + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()] + }, + preload: { + plugins: [externalizeDepsPlugin()] + }, + renderer: { + resolve: { + alias: { + '@': resolve('./src/renderer/src'), + // '@': path.join(__dirname, './src/renderer/src'), + } + }, + server: { + proxy: { + '/dev-api': { + target: 'http://27.128.240.72:7865', + changeOrigin: true, + rewrite: (p) => p.replace(/^\/dev-api/, '') + } + }, + }, + plugins: [vue(), WindiCSS()], + assetsInclude:('**/*.woff', '**/*.woff2', '**/*.ttf'), + } +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..8e45d8a --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "electron-app", + "version": "1.0.0", + "description": "An Electron application with Vue", + "main": "./out/main/index.js", + "author": "example.com", + "homepage": "https://electron-vite.org", + "scripts": { + "format": "prettier --write .", + "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", + "start": "electron-vite preview", + "dev": "electron-vite dev", + "build": "electron-vite build", + "postinstall": "electron-builder install-app-deps", + "build:unpack": "npm run build && electron-builder --dir", + "build:win": "npm run build && electron-builder --win", + "build:mac": "npm run build && electron-builder --mac", + "build:linux": "npm run build && electron-builder --linux" + }, + "dependencies": { + "@electron-toolkit/preload": "^3.0.1", + "@electron-toolkit/utils": "^3.0.0", + "electron-updater": "^6.1.7", + "element-plus": "^2.7.6", + "js-cookie": "^3.0.5", + "jsencrypt": "^3.3.2", + "pinia": "^2.1.7", + "pinia-plugin-persistedstate": "^3.2.1", + "vue-router": "^4.4.0" + }, + "devDependencies": { + "@electron-toolkit/eslint-config": "^1.0.2", + "@rushstack/eslint-patch": "^1.10.3", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/eslint-config-prettier": "^9.0.0", + "axios": "^1.7.2", + "electron": "^31.0.2", + "electron-builder": "^24.13.3", + "electron-vite": "^2.3.0", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.26.0", + "prettier": "^3.3.2", + "sass": "^1.77.6", + "vite": "^5.3.1", + "vite-plugin-windicss": "^1.9.3", + "vue": "^3.4.30", + "windicss": "^3.5.6" + } +} diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000..cf9e8b2 Binary files /dev/null and b/resources/icon.png differ diff --git a/src/main/index.js b/src/main/index.js new file mode 100644 index 0000000..7f853f5 --- /dev/null +++ b/src/main/index.js @@ -0,0 +1,109 @@ +import { app, shell, BrowserWindow, ipcMain } from 'electron' +import { join } from 'path' +import { electronApp, optimizer, is } from '@electron-toolkit/utils' +import icon from '../../resources/icon.png?asset' + +function createWindow() { + // Create the browser window. + const mainWindow = new BrowserWindow({ + width: 888, + height: 520, + show: false, + frame: false, + autoHideMenuBar: true, + ...(process.platform === 'linux' ? { icon } : {}), + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + sandbox: false, + nodeIntegration: true + } + }) + + mainWindow.on('ready-to-show', () => { + mainWindow.show() + }) + + mainWindow.webContents.openDevTools() + + mainWindow.webContents.setWindowOpenHandler((details) => { + shell.openExternal(details.url) + return { action: 'deny' } + }) + + // HMR for renderer base on electron-vite cli. + // Load the remote URL for development or the local html file for production. + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) + // mainWindow.loadURL('https://file.ysaix.com:7868/') + + } else { + // mainWindow.loadURL('https://file.ysaix.com:7868/') + mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + } +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(() => { + // Set app user model id for windows + electronApp.setAppUserModelId('com.electron') + + // Default open or close DevTools by F12 in development + // and ignore CommandOrControl + R in production. + // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils + app.on('browser-window-created', (_, window) => { + optimizer.watchWindowShortcuts(window) + }) + + + + createWindow() + + app.on('activate', function () { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +ipcMain.on('toggle-top', (event) => { + const win = BrowserWindow.getFocusedWindow(); + const isAlwaysOnTop = win.isAlwaysOnTop(); + win.setAlwaysOnTop(!isAlwaysOnTop); + event.sender.send('top-status-changed', !isAlwaysOnTop); +}) + + +ipcMain.on('minimize-window', () => { + const win = BrowserWindow.getFocusedWindow(); + win.minimize(); +}); + +ipcMain.on('maximize-window', () => { + const win = BrowserWindow.getFocusedWindow(); + if (win.isMaximized()) { + win.unmaximize(); + } else { + win.maximize(); + } +}); + +ipcMain.on('close-window', () => { + const win = BrowserWindow.getFocusedWindow(); + win.close(); +}); +console.log(100) +ipcMain.on('set-winsize', (e, {x, y})=>{ + const win = BrowserWindow.getFocusedWindow(); + win.setSize(x,y); +}) \ No newline at end of file diff --git a/src/preload/index.js b/src/preload/index.js new file mode 100644 index 0000000..8d62cb9 --- /dev/null +++ b/src/preload/index.js @@ -0,0 +1,20 @@ +import { contextBridge } from 'electron' +import { electronAPI } from '@electron-toolkit/preload' + +// Custom APIs for renderer +const api = {} + +// Use `contextBridge` APIs to expose Electron APIs to +// renderer only if context isolation is enabled, otherwise +// just add to the DOM global. +if (process.contextIsolated) { + try { + contextBridge.exposeInMainWorld('electron', electronAPI) + contextBridge.exposeInMainWorld('api', api) + } catch (error) { + console.error(error) + } +} else { + window.electron = electronAPI + window.api = api +} diff --git a/src/renderer/index.html b/src/renderer/index.html new file mode 100644 index 0000000..aa01b18 --- /dev/null +++ b/src/renderer/index.html @@ -0,0 +1,19 @@ + + + + + Electron + + + + + + + +
+ + + diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue new file mode 100644 index 0000000..4ece3da --- /dev/null +++ b/src/renderer/src/App.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/src/renderer/src/api/login.js b/src/renderer/src/api/login.js new file mode 100644 index 0000000..649f59c --- /dev/null +++ b/src/renderer/src/api/login.js @@ -0,0 +1,59 @@ +import request from '@/utils/request' + +// 登录方法 +export function login(username, password, code, uuid) { + const data = { + username, + password, + code, + uuid + } + return request({ + url: '/login', + headers: { + isToken: false + }, + method: 'post', + data: data + }) +} + +// 注册方法 +export function register(data) { + return request({ + url: '/register', + headers: { + isToken: false + }, + method: 'post', + data: data + }) +} + +// 获取用户详细信息 +export function getInfo() { + return request({ + url: '/getInfo', + method: 'get' + }) +} + +// 退出方法 +export function logout() { + return request({ + url: '/logout', + method: 'post' + }) +} + +// 获取验证码 +export function getCodeImg() { + return request({ + url: '/captchaImage', + headers: { + isToken: false + }, + method: 'get', + timeout: 20000 + }) +} \ No newline at end of file diff --git a/src/renderer/src/assets/electron.svg b/src/renderer/src/assets/electron.svg new file mode 100644 index 0000000..45ef09c --- /dev/null +++ b/src/renderer/src/assets/electron.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/renderer/src/assets/iconfont/iconfont.css b/src/renderer/src/assets/iconfont/iconfont.css new file mode 100644 index 0000000..cfd642f --- /dev/null +++ b/src/renderer/src/assets/iconfont/iconfont.css @@ -0,0 +1,31 @@ +@font-face { + font-family: "iconfont"; /* Project id 2794390 */ + src: url('./iconfont.woff2?t=1719991760511') format('woff2'), + url('./iconfont.woff?t=1719991760511') format('woff'), + url('./iconfont.ttf?t=1719991760511') format('truetype'); +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-zuidahua:before { + content: "\e600"; +} + +.icon-window-max_line:before { + content: "\e695"; +} + +.icon-zuixiaohua:before { + content: "\e650"; +} + +.icon-close:before { + content: "\e608"; +} + diff --git a/src/renderer/src/assets/iconfont/iconfont.js b/src/renderer/src/assets/iconfont/iconfont.js new file mode 100644 index 0000000..aeb6957 --- /dev/null +++ b/src/renderer/src/assets/iconfont/iconfont.js @@ -0,0 +1 @@ +window._iconfont_svg_string_2794390='',function(n){var t=(t=document.getElementsByTagName("script"))[t.length-1],e=t.getAttribute("data-injectcss"),t=t.getAttribute("data-disable-injectsvg");if(!t){var i,o,a,c,d,l=function(t,e){e.parentNode.insertBefore(t,e)};if(e&&!n.__iconfont__svg__cssinject__){n.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(t){console&&console.log(t)}}i=function(){var t,e=document.createElement("div");e.innerHTML=n._iconfont_svg_string_2794390,(e=e.getElementsByTagName("svg")[0])&&(e.setAttribute("aria-hidden","true"),e.style.position="absolute",e.style.width=0,e.style.height=0,e.style.overflow="hidden",e=e,(t=document.body).firstChild?l(e,t.firstChild):t.appendChild(e))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(i,0):(o=function(){document.removeEventListener("DOMContentLoaded",o,!1),i()},document.addEventListener("DOMContentLoaded",o,!1)):document.attachEvent&&(a=i,c=n.document,d=!1,r(),c.onreadystatechange=function(){"complete"==c.readyState&&(c.onreadystatechange=null,s())})}function s(){d||(d=!0,a())}function r(){try{c.documentElement.doScroll("left")}catch(t){return void setTimeout(r,50)}s()}}(window); \ No newline at end of file diff --git a/src/renderer/src/assets/iconfont/iconfont.json b/src/renderer/src/assets/iconfont/iconfont.json new file mode 100644 index 0000000..3350638 --- /dev/null +++ b/src/renderer/src/assets/iconfont/iconfont.json @@ -0,0 +1,37 @@ +{ + "id": "2794390", + "name": "test", + "font_family": "iconfont", + "css_prefix_text": "icon-", + "description": "", + "glyphs": [ + { + "icon_id": "4485682", + "name": "最大化", + "font_class": "zuidahua", + "unicode": "e600", + "unicode_decimal": 58880 + }, + { + "icon_id": "7538862", + "name": "窗口-最大化_line", + "font_class": "window-max_line", + "unicode": "e695", + "unicode_decimal": 59029 + }, + { + "icon_id": "11490918", + "name": "最小化", + "font_class": "zuixiaohua", + "unicode": "e650", + "unicode_decimal": 58960 + }, + { + "icon_id": "11727010", + "name": "关闭", + "font_class": "close", + "unicode": "e608", + "unicode_decimal": 58888 + } + ] +} diff --git a/src/renderer/src/assets/iconfont/iconfont.ttf b/src/renderer/src/assets/iconfont/iconfont.ttf new file mode 100644 index 0000000..6684e31 Binary files /dev/null and b/src/renderer/src/assets/iconfont/iconfont.ttf differ diff --git a/src/renderer/src/assets/iconfont/iconfont.woff b/src/renderer/src/assets/iconfont/iconfont.woff new file mode 100644 index 0000000..cff54f3 Binary files /dev/null and b/src/renderer/src/assets/iconfont/iconfont.woff differ diff --git a/src/renderer/src/assets/iconfont/iconfont.woff2 b/src/renderer/src/assets/iconfont/iconfont.woff2 new file mode 100644 index 0000000..598db3d Binary files /dev/null and b/src/renderer/src/assets/iconfont/iconfont.woff2 differ diff --git a/src/renderer/src/assets/images/login/ali-icon.png b/src/renderer/src/assets/images/login/ali-icon.png new file mode 100644 index 0000000..c81d647 Binary files /dev/null and b/src/renderer/src/assets/images/login/ali-icon.png differ diff --git a/src/renderer/src/assets/images/login/blue-login-bg.png b/src/renderer/src/assets/images/login/blue-login-bg.png new file mode 100644 index 0000000..706cb9c Binary files /dev/null and b/src/renderer/src/assets/images/login/blue-login-bg.png differ diff --git a/src/renderer/src/assets/images/login/douyin-icon.png b/src/renderer/src/assets/images/login/douyin-icon.png new file mode 100644 index 0000000..82525fe Binary files /dev/null and b/src/renderer/src/assets/images/login/douyin-icon.png differ diff --git a/src/renderer/src/assets/images/login/feishu-icon.png b/src/renderer/src/assets/images/login/feishu-icon.png new file mode 100644 index 0000000..dc8468c Binary files /dev/null and b/src/renderer/src/assets/images/login/feishu-icon.png differ diff --git a/src/renderer/src/assets/images/login/google-icon.png b/src/renderer/src/assets/images/login/google-icon.png new file mode 100644 index 0000000..465069c Binary files /dev/null and b/src/renderer/src/assets/images/login/google-icon.png differ diff --git a/src/renderer/src/assets/images/login/left-bg1.png b/src/renderer/src/assets/images/login/left-bg1.png new file mode 100644 index 0000000..83084aa Binary files /dev/null and b/src/renderer/src/assets/images/login/left-bg1.png differ diff --git a/src/renderer/src/assets/images/login/left-bg2.png b/src/renderer/src/assets/images/login/left-bg2.png new file mode 100644 index 0000000..ba4eaec Binary files /dev/null and b/src/renderer/src/assets/images/login/left-bg2.png differ diff --git a/src/renderer/src/assets/images/login/login-bg.png b/src/renderer/src/assets/images/login/login-bg.png new file mode 100644 index 0000000..16cf505 Binary files /dev/null and b/src/renderer/src/assets/images/login/login-bg.png differ diff --git a/src/renderer/src/assets/images/login/login-bg2.png b/src/renderer/src/assets/images/login/login-bg2.png new file mode 100644 index 0000000..fa33669 Binary files /dev/null and b/src/renderer/src/assets/images/login/login-bg2.png differ diff --git a/src/renderer/src/assets/images/login/login-bg3.png b/src/renderer/src/assets/images/login/login-bg3.png new file mode 100644 index 0000000..5533b14 Binary files /dev/null and b/src/renderer/src/assets/images/login/login-bg3.png differ diff --git a/src/renderer/src/assets/images/login/login-bg4 .png b/src/renderer/src/assets/images/login/login-bg4 .png new file mode 100644 index 0000000..cb51b61 Binary files /dev/null and b/src/renderer/src/assets/images/login/login-bg4 .png differ diff --git a/src/renderer/src/assets/images/login/login-form-open-eyes-close.png b/src/renderer/src/assets/images/login/login-form-open-eyes-close.png new file mode 100644 index 0000000..89f8530 Binary files /dev/null and b/src/renderer/src/assets/images/login/login-form-open-eyes-close.png differ diff --git a/src/renderer/src/assets/images/login/login-form-open-eyes.png b/src/renderer/src/assets/images/login/login-form-open-eyes.png new file mode 100644 index 0000000..f8eb3d7 Binary files /dev/null and b/src/renderer/src/assets/images/login/login-form-open-eyes.png differ diff --git a/src/renderer/src/assets/images/login/login-qr.png b/src/renderer/src/assets/images/login/login-qr.png new file mode 100644 index 0000000..9f00281 Binary files /dev/null and b/src/renderer/src/assets/images/login/login-qr.png differ diff --git a/src/renderer/src/assets/images/login/qq-icon.png b/src/renderer/src/assets/images/login/qq-icon.png new file mode 100644 index 0000000..2b1e45e Binary files /dev/null and b/src/renderer/src/assets/images/login/qq-icon.png differ diff --git a/src/renderer/src/assets/images/login/wechat-icon.png b/src/renderer/src/assets/images/login/wechat-icon.png new file mode 100644 index 0000000..644137b Binary files /dev/null and b/src/renderer/src/assets/images/login/wechat-icon.png differ diff --git a/src/renderer/src/assets/images/login/weibo-icon.png b/src/renderer/src/assets/images/login/weibo-icon.png new file mode 100644 index 0000000..e0ff440 Binary files /dev/null and b/src/renderer/src/assets/images/login/weibo-icon.png differ diff --git a/src/renderer/src/assets/images/loginbg-sunraise.jpg b/src/renderer/src/assets/images/loginbg-sunraise.jpg new file mode 100644 index 0000000..e57cfec Binary files /dev/null and b/src/renderer/src/assets/images/loginbg-sunraise.jpg differ diff --git a/src/renderer/src/assets/images/user.png b/src/renderer/src/assets/images/user.png new file mode 100644 index 0000000..d5904db Binary files /dev/null and b/src/renderer/src/assets/images/user.png differ diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css new file mode 100644 index 0000000..0f8ad66 --- /dev/null +++ b/src/renderer/src/assets/main.css @@ -0,0 +1,26 @@ + +html,body{ + height: 100%; +} + +#app { + height: 100%; +} +@media (max-width: 720px) { + .text { + font-size: 20px; + } +} + +@media (max-width: 620px) { + .versions { + display: none; + } +} + +@media (max-width: 350px) { + .tip, + .actions { + display: none; + } +} diff --git a/src/renderer/src/assets/wavy-lines.svg b/src/renderer/src/assets/wavy-lines.svg new file mode 100644 index 0000000..d08c611 --- /dev/null +++ b/src/renderer/src/assets/wavy-lines.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/src/components/Versions.vue b/src/renderer/src/components/Versions.vue new file mode 100644 index 0000000..35136c0 --- /dev/null +++ b/src/renderer/src/components/Versions.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/renderer/src/layout/components/AppMain.vue b/src/renderer/src/layout/components/AppMain.vue new file mode 100644 index 0000000..da5dc9e --- /dev/null +++ b/src/renderer/src/layout/components/AppMain.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/src/renderer/src/layout/components/Header.vue b/src/renderer/src/layout/components/Header.vue new file mode 100644 index 0000000..3cb4a20 --- /dev/null +++ b/src/renderer/src/layout/components/Header.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/src/renderer/src/layout/index.vue b/src/renderer/src/layout/index.vue new file mode 100644 index 0000000..b495d19 --- /dev/null +++ b/src/renderer/src/layout/index.vue @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/src/renderer/src/main.js b/src/renderer/src/main.js new file mode 100644 index 0000000..cae0813 --- /dev/null +++ b/src/renderer/src/main.js @@ -0,0 +1,21 @@ +import './assets/main.css' + +import { createApp } from 'vue' + +import ElementPlus from 'element-plus' +import './utils/messageConfig' +import 'element-plus/dist/index.css' +import './assets/iconfont/iconfont.css' + +import 'virtual:windi.css' + +import { store } from '@/store' +import App from './App.vue' + +import router from './router' + +const app = createApp(App) + +app.use(router) + .use(store) + .use(ElementPlus).mount('#app') diff --git a/src/renderer/src/plugins/cache.js b/src/renderer/src/plugins/cache.js new file mode 100644 index 0000000..6b5c00b --- /dev/null +++ b/src/renderer/src/plugins/cache.js @@ -0,0 +1,77 @@ +const sessionCache = { + set (key, value) { + if (!sessionStorage) { + return + } + if (key != null && value != null) { + sessionStorage.setItem(key, value) + } + }, + get (key) { + if (!sessionStorage) { + return null + } + if (key == null) { + return null + } + return sessionStorage.getItem(key) + }, + setJSON (key, jsonValue) { + if (jsonValue != null) { + this.set(key, JSON.stringify(jsonValue)) + } + }, + getJSON (key) { + const value = this.get(key) + if (value != null) { + return JSON.parse(value) + } + }, + remove (key) { + sessionStorage.removeItem(key); + } +} +const localCache = { + set (key, value) { + if (!localStorage) { + return + } + if (key != null && value != null) { + localStorage.setItem(key, value) + } + }, + get (key) { + if (!localStorage) { + return null + } + if (key == null) { + return null + } + return localStorage.getItem(key) + }, + setJSON (key, jsonValue) { + if (jsonValue != null) { + this.set(key, JSON.stringify(jsonValue)) + } + }, + getJSON (key) { + const value = this.get(key) + if (value != null) { + return JSON.parse(value) + } + }, + remove (key) { + localStorage.removeItem(key); + } +} + +export default { + /** + * 会话级缓存 + */ + session: sessionCache, + /** + * 本地缓存 + */ + local: localCache +} diff --git a/src/renderer/src/router/index.js b/src/renderer/src/router/index.js new file mode 100644 index 0000000..6278f80 --- /dev/null +++ b/src/renderer/src/router/index.js @@ -0,0 +1,33 @@ +import { createRouter, createWebHashHistory } from 'vue-router' + +import Layout from '../layout/index.vue' + +export const constantRoutes = [ + { + path: '/login', + component: () => import('@/views/login/index.vue'), + // component: ()=> import('../login/index.vue'), + hidden: true + }, + { + path: '/', + component: Layout, + redirect: '/login', + children: [ + { + path: '/index', + component: () => import('@/views/resource/index.vue'), + name: 'index', + + } + ] + }, + +] + +const router = createRouter({ + history: createWebHashHistory(), //hash 模式 + routes: constantRoutes +}) + +export default router diff --git a/src/renderer/src/store/index.js b/src/renderer/src/store/index.js new file mode 100644 index 0000000..156ead2 --- /dev/null +++ b/src/renderer/src/store/index.js @@ -0,0 +1,3 @@ +import { createPinia } from 'pinia'; + +export const store = createPinia(); \ No newline at end of file diff --git a/src/renderer/src/store/modules/user.js b/src/renderer/src/store/modules/user.js new file mode 100644 index 0000000..8066ded --- /dev/null +++ b/src/renderer/src/store/modules/user.js @@ -0,0 +1,99 @@ +import { defineStore } from "pinia" +import { login, logout, getInfo } from '@/api/login' +import { getToken, setToken, removeToken } from '@/utils/auth' +import defAva from '@/assets/images/user.png' + +const useUserStore = defineStore( + 'user', + { + state: () => ({ + token: getToken(), + id: '', + name: '', + avatar: '', + roles: [], + permissions: [], + user: {} + }), + actions: { + // 登录 + login(userInfo) { + const username = userInfo.username.trim() + const password = userInfo.password + const code = userInfo.code + const uuid = userInfo.uuid + return new Promise((resolve, reject) => { + login(username, password, code, uuid).then(res => { + setToken(res.token) + this.token = res.token + resolve(res) + }).catch(error => { + reject(error) + }) + }) + }, + // 获取用户信息 + getInfo() { + return new Promise((resolve, reject) => { + getInfo().then(res => { + const user = res.user + this.user = user + const avatar = (user.avatar == "" || user.avatar == null) ? defAva : user.avatar; + + if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组 + this.roles = res.roles + this.permissions = res.permissions + } else { + this.roles = ['ROLE_DEFAULT'] + } + + + this.id = user.userId + this.userName = user.userName + this.nickName = user.nickName; + this.avatar = avatar; + this.userType = user.userType; + this.deptId = user.deptId; + this.deptName = user.deptName; + this.deptLogo = user.deptLogo; + this.deptSlogan = user.deptSlogan; + this.activeDeptId = user.activeDeptId; + this.activeDeptName = user.activeDeptName; + this.parentDeptId = user.parentDeptId; + this.parentDeptName = user.parentDeptName; + this.edusubject = user.edusubject; + this.edudegree = user.edudegree; + this.edustage = user.edustage; + this.userType = user.userType; + this.studentId = user.studentId; + this.timUserId = user.timuserid; + this.plainpwd = user.plainpwd; + + this.roles = res.roles; + + resolve(res) + }).catch(error => { + reject(error) + }) + }) + }, + // 退出系统 + logOut() { + return new Promise((resolve, reject) => { + logout(this.token).then(() => { + this.token = '' + this.roles = [] + this.permissions = [] + removeToken() + resolve() + }).catch(error => { + removeToken() // zdg: 网络异常时,清除前端退出进入登录页 + reject(error) + }) + }) + } + }, + persist: true + }) + +export default useUserStore diff --git a/src/renderer/src/utils/auth.js b/src/renderer/src/utils/auth.js new file mode 100644 index 0000000..08a43d6 --- /dev/null +++ b/src/renderer/src/utils/auth.js @@ -0,0 +1,15 @@ +import Cookies from 'js-cookie' + +const TokenKey = 'Admin-Token' + +export function getToken() { + return Cookies.get(TokenKey) +} + +export function setToken(token) { + return Cookies.set(TokenKey, token) +} + +export function removeToken() { + return Cookies.remove(TokenKey) +} diff --git a/src/renderer/src/utils/errorCode.js b/src/renderer/src/utils/errorCode.js new file mode 100644 index 0000000..d2111ee --- /dev/null +++ b/src/renderer/src/utils/errorCode.js @@ -0,0 +1,6 @@ +export default { + '401': '认证失败,无法访问系统资源', + '403': '当前操作没有权限', + '404': '访问资源不存在', + 'default': '系统未知错误,请反馈给管理员' +} diff --git a/src/renderer/src/utils/jsencrypt.js b/src/renderer/src/utils/jsencrypt.js new file mode 100644 index 0000000..048e6d1 --- /dev/null +++ b/src/renderer/src/utils/jsencrypt.js @@ -0,0 +1,30 @@ +import JSEncrypt from "jsencrypt" + +// 密钥对生成 http://web.chacuo.net/netrsakeypair + +const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' + + 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' + +const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' + + '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' + + 'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' + + 'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' + + 'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' + + 'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' + + 'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' + + 'UP8iWi1Qw0Y=' + +// 加密 +export function encrypt(txt) { + const encryptor = new JSEncrypt() + encryptor.setPublicKey(publicKey) // 设置公钥 + return encryptor.encrypt(txt) // 对数据进行加密 +} + +// 解密 +export function decrypt(txt) { + const encryptor = new JSEncrypt() + encryptor.setPrivateKey(privateKey) // 设置私钥 + return encryptor.decrypt(txt) // 对数据进行解密 +} + diff --git a/src/renderer/src/utils/messageConfig.js b/src/renderer/src/utils/messageConfig.js new file mode 100644 index 0000000..b63e620 --- /dev/null +++ b/src/renderer/src/utils/messageConfig.js @@ -0,0 +1,6 @@ + +import { ElMessage } from 'element-plus'; + +ElMessage.defaultOptions = { + plain : true +} \ No newline at end of file diff --git a/src/renderer/src/utils/request.js b/src/renderer/src/utils/request.js new file mode 100644 index 0000000..f48a47f --- /dev/null +++ b/src/renderer/src/utils/request.js @@ -0,0 +1,128 @@ +import axios from 'axios' +import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus' +import { getToken } from '@/utils/auth' +import errorCode from '@/utils/errorCode' +import { tansParams, blobValidate } from '@/utils/ruoyi' +import cache from '@/plugins/cache' + +import useUserStore from '@/store/modules/user' + + +// 是否显示重新登录 +export let isRelogin = { show: false }; + +axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8' +// 创建axios实例 +const service = axios.create({ + // axios中请求配置有baseURL选项,表示请求URL公共部分 + baseURL: import.meta.env.VITE_APP_BASE_API, + // 超时 + timeout: 100000 +}) + +// request拦截器 +service.interceptors.request.use(config => { + // 是否需要设置 token + const isToken = (config.headers || {}).isToken === false + // 是否需要防止数据重复提交 + const isRepeatSubmit = (config.headers || {}).repeatSubmit === false + if (getToken() && !isToken) { + config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 + } + // get请求映射params参数 + if (config.method === 'get' && config.params) { + let url = config.url + '?' + tansParams(config.params); + url = url.slice(0, -1); + config.params = {}; + config.url = url; + } + if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) { + const requestObj = { + url: config.url, + data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, + time: new Date().getTime() + } + const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小 + const limitSize = 5 * 1024 * 1024; // 限制存放数据5M + if (requestSize >= limitSize) { + console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。') + return config; + } + const sessionObj = cache.session.getJSON('sessionObj') + if (sessionObj === undefined || sessionObj === null || sessionObj === '') { + cache.session.setJSON('sessionObj', requestObj) + } else { + const s_url = sessionObj.url; // 请求地址 + const s_data = sessionObj.data; // 请求数据 + const s_time = sessionObj.time; // 请求时间 + const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交 + if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { + const message = '数据正在处理,请勿重复提交'; + console.warn(`[${s_url}]: ` + message) + return Promise.reject(new Error(message)) + } else { + cache.session.setJSON('sessionObj', requestObj) + } + } + } + return config +}, error => { + console.log(error) + Promise.reject(error) +}) + +// 响应拦截器 +service.interceptors.response.use(res => { + // 未设置状态码则默认成功状态 + const code = res.data.code || 200; + // 获取错误信息 + const msg = errorCode[code] || res.data.msg || errorCode['default'] + // 二进制数据则直接返回 + if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') { + return res.data + } + if (code === 401) { + if (!isRelogin.show) { + isRelogin.show = true; + ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { + isRelogin.show = false; + useUserStore().logOut().then(() => { + location.href = '/index'; + }) + }).catch(() => { + isRelogin.show = false; + }); + } + return Promise.reject('') + } else if (code === 500) { + ElMessage({ message: msg, type: 'error' }) + return Promise.reject(new Error(msg)) + } else if (code === 601) { + ElMessage({ message: msg, type: 'warning' }) + return Promise.reject(new Error(msg)) + } else if (code !== 200) { + ElNotification.error({ title: msg }) + return Promise.reject('error') + } else { + return Promise.resolve(res.data) + } + }, + error => { + console.log('err' + error) + let { message } = error; + if (message == "Network Error") { + message = "后端接口连接异常"; + } else if (message.includes("timeout")) { + message = "系统接口请求超时"; + } else if (message.includes("Request failed with status code")) { + message = "系统接口" + message.substr(message.length - 3) + "异常"; + } + ElMessage({ message: message, type: 'error', duration: 5 * 1000 }) + return Promise.reject(error) + } +) + +// 通用下载方法 + + +export default service diff --git a/src/renderer/src/utils/ruoyi.js b/src/renderer/src/utils/ruoyi.js new file mode 100644 index 0000000..b20ee53 --- /dev/null +++ b/src/renderer/src/utils/ruoyi.js @@ -0,0 +1,247 @@ + + +/** + * 通用js方法封装处理 + * Copyright (c) 2019 ruoyi + */ + +// 日期格式化 +export function parseTime(time, pattern) { + if (arguments.length === 0 || !time) { + return null + } + const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { + time = parseInt(time) + } else if (typeof time === 'string') { + time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), ''); + } + if ((typeof time === 'number') && (time.toString().length === 10)) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + } + const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { + let value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] } + if (result.length > 0 && value < 10) { + value = '0' + value + } + return value || 0 + }) + return time_str +} + +// 表单重置 +export function resetForm(refName) { + if (this.$refs[refName]) { + this.$refs[refName].resetFields(); + } +} + +// 添加日期范围 +export function addDateRange(params, dateRange, propName) { + let search = params; + search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {}; + dateRange = Array.isArray(dateRange) ? dateRange : []; + if (typeof (propName) === 'undefined') { + search.params['beginTime'] = dateRange[0]; + search.params['endTime'] = dateRange[1]; + } else { + search.params['begin' + propName] = dateRange[0]; + search.params['end' + propName] = dateRange[1]; + } + return search; +} + +// 回显数据字典 +export function selectDictLabel(datas, value) { + if (value === undefined) { + return ""; + } + var actions = []; + Object.keys(datas).some((key) => { + if (datas[key].value == ('' + value)) { + actions.push(datas[key].label); + return true; + } + }) + if (actions.length === 0) { + actions.push(value); + } + return actions.join(''); +} + +// 回显数据字典(字符串数组) +export function selectDictLabels(datas, value, separator) { + if (value === undefined || value.length ===0) { + return ""; + } + if (Array.isArray(value)) { + value = value.join(","); + } + var actions = []; + var currentSeparator = undefined === separator ? "," : separator; + var temp = value.split(currentSeparator); + Object.keys(value.split(currentSeparator)).some((val) => { + var match = false; + Object.keys(datas).some((key) => { + if (datas[key].value == ('' + temp[val])) { + actions.push(datas[key].label + currentSeparator); + match = true; + } + }) + if (!match) { + actions.push(temp[val] + currentSeparator); + } + }) + return actions.join('').substring(0, actions.join('').length - 1); +} + +// 字符串格式化(%s ) +export function sprintf(str) { + var args = arguments, flag = true, i = 1; + str = str.replace(/%s/g, function () { + var arg = args[i++]; + if (typeof arg === 'undefined') { + flag = false; + return ''; + } + return arg; + }); + return flag ? str : ''; +} + +// 转换字符串,undefined,null等转化为"" +export function parseStrEmpty(str) { + if (!str || str == "undefined" || str == "null") { + return ""; + } + return str; +} + +// 数据合并 +export function mergeRecursive(source, target) { + for (var p in target) { + try { + if (target[p].constructor == Object) { + source[p] = mergeRecursive(source[p], target[p]); + } else { + source[p] = target[p]; + } + } catch (e) { + source[p] = target[p]; + } + } + return source; +}; + +/** + * 构造树型结构数据 + * @param {*} data 数据源 + * @param {*} id id字段 默认 'id' + * @param {*} parentId 父节点字段 默认 'parentId' + * @param {*} children 孩子节点字段 默认 'children' + */ +export function handleTree(data, id, parentId, children) { + let config = { + id: id || 'id', + parentId: parentId || 'parentId', + childrenList: children || 'children' + }; + + var childrenListMap = {}; + var nodeIds = {}; + var tree = []; + + for (let d of data) { + let parentId = d[config.parentId]; + if (childrenListMap[parentId] == null) { + childrenListMap[parentId] = []; + } + nodeIds[d[config.id]] = d; + childrenListMap[parentId].push(d); + } + + for (let d of data) { + let parentId = d[config.parentId]; + if (nodeIds[parentId] == null) { + tree.push(d); + } + } + + for (let t of tree) { + adaptToChildrenList(t); + } + + function adaptToChildrenList(o) { + if (childrenListMap[o[config.id]] !== null) { + o[config.childrenList] = childrenListMap[o[config.id]]; + } + if (o[config.childrenList]) { + for (let c of o[config.childrenList]) { + adaptToChildrenList(c); + } + } + } + return tree; +} + +/** +* 参数处理 +* @param {*} params 参数 +*/ +export function tansParams(params) { + let result = '' + for (const propName of Object.keys(params)) { + const value = params[propName]; + var part = encodeURIComponent(propName) + "="; + if (typeof value === 'object') { + for (const key of Object.keys(value)) { + if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') { + let params = propName + '[' + key + ']'; + var subPart = encodeURIComponent(params) + "="; + result += subPart + encodeURIComponent(value[key]) + "&"; + } + } + } else { + result += part + encodeURIComponent(value) + "&"; + } + /*if (value !== null && value !== "" && typeof (value) !== "undefined") { + + }*/ + } + return result +} + + +// 返回项目路径 +export function getNormalPath(p) { + if (p.length === 0 || !p || p == 'undefined') { + return p + }; + let res = p.replace('//', '/') + if (res[res.length - 1] === '/') { + return res.slice(0, res.length - 1) + } + return res; +} + +// 验证是否为blob格式 +export function blobValidate(data) { + return data.type !== 'application/json' +} diff --git a/src/renderer/src/views/login/index.vue b/src/renderer/src/views/login/index.vue new file mode 100644 index 0000000..780f5b8 --- /dev/null +++ b/src/renderer/src/views/login/index.vue @@ -0,0 +1,261 @@ + + + diff --git a/src/renderer/src/views/resource/index.vue b/src/renderer/src/views/resource/index.vue new file mode 100644 index 0000000..93efbca --- /dev/null +++ b/src/renderer/src/views/resource/index.vue @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file