登录验证码使用

This commit is contained in:
LittleBoy 2022-05-20 09:34:49 +08:00
parent aa94cb3307
commit fe6edf9e5d
65 changed files with 15884 additions and 35 deletions

View File

@ -0,0 +1,52 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues">
<value>
<list size="7">
<item index="0" class="java.lang.String" itemvalue="nobr" />
<item index="1" class="java.lang.String" itemvalue="noembed" />
<item index="2" class="java.lang.String" itemvalue="comment" />
<item index="3" class="java.lang.String" itemvalue="noscript" />
<item index="4" class="java.lang.String" itemvalue="embed" />
<item index="5" class="java.lang.String" itemvalue="script" />
<item index="6" class="java.lang.String" itemvalue="testb" />
</list>
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredUrls">
<list>
<option value="http://localhost" />
<option value="http://127.0.0.1" />
<option value="http://0.0.0.0" />
<option value="http://www.w3.org/" />
<option value="http://json-schema.org/draft" />
<option value="http://java.sun.com/" />
<option value="http://xmlns.jcp.org/" />
<option value="http://javafx.com/javafx/" />
<option value="http://javafx.com/fxml" />
<option value="http://maven.apache.org/xsd/" />
<option value="http://maven.apache.org/POM/" />
<option value="http://www.springframework.org/schema/" />
<option value="http://www.springframework.org/tags" />
<option value="http://www.springframework.org/security/tags" />
<option value="http://www.thymeleaf.org" />
<option value="http://www.jboss.org/j2ee/schema/" />
<option value="http://www.jboss.com/xml/ns/" />
<option value="http://www.ibm.com/webservices/xsd" />
<option value="http://activemq.apache.org/schema/" />
<option value="http://schema.cloudfoundry.org/spring/" />
<option value="http://schemas.xmlsoap.org/" />
<option value="http://cxf.apache.org/schemas/" />
<option value="http://primefaces.org/ui" />
<option value="http://tiles.apache.org/" />
<option value="http://${process.env[" />
</list>
</option>
</inspection_tool>
</profile>
</component>

10
client-pc/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.debug.env
tmp
**/.tmp
release

13
client-pc/CHANGELOG.md Normal file
View File

@ -0,0 +1,13 @@
## 22-01-27
- Refactor the scripts part.
- Remove `configs` directory.
## 21-11-11
- Refactor the project. Use vite.config.ts build `Main-process`, `Preload-script` and `Renderer-process` alternative rollup.
- Scenic `Vue>=3.2.13`, `@vue/compiler-sfc` is no longer necessary.
- If you prefer Rollup, Use rollup branch.
```bash
Error: @vitejs/plugin-vue requires vue (>=3.2.13) or @vue/compiler-sfc to be present in the dependency tree.
```

24
client-pc/Dockerfile Normal file
View File

@ -0,0 +1,24 @@
# use the version that corresponds to your electron version
FROM node:14.16
LABEL NAME="electron-wrapper"
LABEL RUN="docker run --rm -it electron-wrapper bash"
# install electron dependencies or more if your library has other dependencies
RUN apt-get update && apt-get install \
git libx11-xcb1 libxcb-dri3-0 libxtst6 libnss3 libatk-bridge2.0-0 libgtk-3-0 libxss1 libasound2 \
-yq --no-install-suggests --no-install-recommends \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# copy the source into /app
WORKDIR /app
COPY . .
RUN chown -R node /app
# install node modules and perform an electron rebuild
USER node
RUN npm install
RUN npm run build
USER node
CMD bash

74
client-pc/README.md Normal file
View File

@ -0,0 +1,74 @@
# electron-vite-vue
[![awesome-vite](https://awesome.re/mentioned-badge.svg)](https://github.com/vitejs/awesome-vite)
[![Netlify Status](https://api.netlify.com/api/v1/badges/ae3863e3-1aec-4eb1-8f9f-1890af56929d/deploy-status)](https://app.netlify.com/sites/electron-vite/deploys)
![GitHub license](https://img.shields.io/github/license/caoxiemeihao/electron-vite-vue?style=flat)
![GitHub stars](https://img.shields.io/github/stars/caoxiemeihao/electron-vite-vue?color=fa6470&style=flat)
![GitHub forks](https://img.shields.io/github/forks/caoxiemeihao/electron-vite-vue?style=flat)
**English | [简体中文](README.zh-CN.md)**
🥳 Real simple `Electron` + `Vue` + `Vite` boilerplate.
## Overview
📦 Out of the box
💪 Support C/C++ addons
🔩 Support Use Electron、Node.js API in Renderer-process
🌱 Simple directory structurereal flexible
🖥 It's easy to implement multiple windows
## Quick Start
```sh
npm create electron-vite
```
<!-- [![quick-start](https://asciinema.org/a/483731.svg)](https://asciinema.org/a/483731) -->
![electron-vite-vue.gif](https://github.com/electron-vite/electron-vite-vue/blob/main/packages/renderer/public/electron-vite-vue.gif?raw=true)
## Debug
![electron-vite-react-debug.gif](https://github.com/electron-vite/electron-vite-react/blob/main/packages/renderer/public/electron-vite-react-debug.gif?raw=true)
## Directory
A `dist` folder will be generated everytime when `dev` or `build` command is executed. File structure of `dist` is identical to the `packages` directory to avoid any potential path calculation errors.
```tree
├── dist Will be generated following the structure of "packages" directory
| ├── main
| ├── preload
| └── renderer
|
├── scripts
| ├── build.mjs Build script -> npm run build
| └── watch.mjs Develop script -> npm run dev
|
├── packages
| ├── main Main-process source code
| | └── vite.config.ts
| ├── preload Preload-script source code
| | └── vite.config.ts
| └── renderer Renderer-process source code
| └── vite.config.ts
```
## List the modules you may use as far as possible
Used in `Main-process` 👉 [electron-vite-boilerplate](https://github.com/caoxiemeihao/electron-vite-boilerplate)
Used in `Renderer-process` 👉 [electron-vite-boilerplate/tree/nodeIntegration](https://github.com/caoxiemeihao/electron-vite-boilerplate/tree/nodeIntegration)
**ES Modules**
- [execa](https://www.npmjs.com/package/execa)
- [node-fetch](https://www.npmjs.com/package/node-fetch)
- [file-type](https://www.npmjs.com/package/file-type)
**Native Addons(C/C++)**
- [sqlite3](https://www.npmjs.com/package/sqlite3)
- [serialport](https://www.npmjs.com/package/serialport)

71
client-pc/README.zh-CN.md Normal file
View File

@ -0,0 +1,71 @@
# electron-vite-vue
[![awesome-vite](https://awesome.re/mentioned-badge.svg)](https://github.com/vitejs/awesome-vite)
![GitHub license](https://img.shields.io/github/license/caoxiemeihao/electron-vite-vue?style=flat)
![GitHub stars](https://img.shields.io/github/stars/caoxiemeihao/electron-vite-vue?color=fa6470&style=flat)
![GitHub forks](https://img.shields.io/github/forks/caoxiemeihao/electron-vite-vue?style=flat)
**[English](README.md) | 简体中文**
🥳 Electron + Vite + Vue 整合模板 -- **结构简单,容易上手!**
## 概述
📦 开箱即用
💪 支持 C/C++ 模块
🔩 支持在渲染进程中使用 Electron、Node.js API
🌱 结构清晰,可塑性强
🖥 很容易实现多窗口
## 快速开始
```sh
npm create electron-vite
```
<!-- [![quick-start](https://asciinema.org/a/483731.svg)](https://asciinema.org/a/483731) -->
![electron-vite-vue.gif](https://github.com/electron-vite/electron-vite-vue/blob/main/packages/renderer/public/electron-vite-vue.gif?raw=true)
## 调试
![electron-vite-react-debug.gif](https://github.com/electron-vite/electron-vite-react/blob/main/packages/renderer/public/electron-vite-react-debug.gif?raw=true)
## 目录结构
```tree
├── dist 构建后,根据 packages 目录生成
| ├── main
| ├── preload
| └── renderer
|
├── scripts
| ├── build.mjs 项目开发脚本 npm run build
| └── watch.mjs 项目开发脚本 npm run dev
|
├── packages
| ├── main 主进程源码
| | └── vite.config.ts
| ├── preload 预加载脚本源码
| | └── vite.config.ts
| └── renderer 渲染进程源码
| └── vite.config.ts
```
## 一些常见的案例
在 Main-process 中使用 👉 [electron-vite-boilerplate](https://github.com/caoxiemeihao/electron-vite-boilerplate)
在 Renderer-process 中使用 👉 [electron-vite-boilerplate/tree/nodeIntegration](https://github.com/caoxiemeihao/electron-vite-boilerplate/tree/nodeIntegration)
**ES Modules**
- [execa](https://www.npmjs.com/package/execa)
- [node-fetch](https://www.npmjs.com/package/node-fetch)
- [file-type](https://www.npmjs.com/package/file-type)
**Native Addons(C/C++)**
- [sqlite3](https://www.npmjs.com/package/sqlite3)
- [serialport](https://www.npmjs.com/package/serialport)

View File

@ -0,0 +1,36 @@
/**
* @see https://www.electron.build/configuration/configuration
*/
{
"appId": "YourAppID",
"asar": true,
"directories": {
"output": "release/${version}"
},
"files": [
"dist"
],
"mac": {
"artifactName": "${productName}_${version}.${ext}",
"target": [
"dmg"
]
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"artifactName": "${productName}_${version}.${ext}"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"deleteAppDataOnUninstall": false
}
}

View File

@ -0,0 +1,7 @@
export default {
// eslint
'*.{js,ts,tsx,vue}': 'eslint --cache --fix',
// typecheck
'packages/renderer/**/{*.ts,*.tsx,*.vue,tsconfig.json}': ({ filenames }) =>
'npm run typecheck',
}

11216
client-pc/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
client-pc/package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "electron-vue-vite",
"version": "1.0.0",
"main": "dist/main/index.cjs",
"author": "草鞋没号 <308487730@qq.com>",
"license": "MIT",
"scripts": {
"client_dev": "node scripts/watch.mjs",
"prebuild": "vue-tsc --noEmit --p packages/renderer/tsconfig.json && node scripts/build.mjs",
"build": "electron-builder",
"init": "git config core.hooksPath .git/hooks/ && rm -rf .git/hooks && npx simple-git-hooks",
"test:e2e": "npx playwright test",
"test:e2e:headless": "npx playwright test --headed"
},
"engines": {
"node": ">=14.17.0"
},
"devDependencies": {
"@playwright/test": "^1.21.1",
"@vitejs/plugin-vue": "^2.3.2",
"electron": "18.2.0",
"electron-builder": "^23.0.3",
"nano-staged": "^0.8.0",
"simple-git-hooks": "^2.7.0",
"typescript": "^4.6.4",
"vite": "^2.9.8",
"vite-plugin-electron": "^0.4.4",
"vite-plugin-resolve": "^2.1.1",
"vue": "^3.2.33",
"vue-tsc": "^0.34.11"
},
"env": {
"VITE_DEV_SERVER_HOST": "127.0.0.1",
"VITE_DEV_SERVER_PORT": 3344,
"PORT": 3344
},
"keywords": [
"electron",
"rollup",
"vite",
"vue3",
"vue"
],
"dependencies": {
"ant-design-vue": "^3.2.3",
"dayjs": "^1.11.2",
"less": "^4.1.2",
"vue-router": "^4.0.15",
"vuex": "^4.0.2"
}
}

View File

@ -0,0 +1,75 @@
import { app, BrowserWindow, shell } from 'electron'
import { release } from 'os'
import { join } from 'path'
// Disable GPU Acceleration for Windows 7
if (release().startsWith('6.1')) app.disableHardwareAcceleration()
// Set application name for Windows 10+ notifications
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
}
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
let win: BrowserWindow | null = null
async function createWindow() {
win = new BrowserWindow({
title: '天牛网盘',
minWidth:1000,
minHeight:600,
webPreferences: {
preload: join(__dirname, '../preload/index.cjs'),
nodeIntegration: true,
contextIsolation: false,
},
})
if (app.isPackaged) {
await win.loadFile(join(__dirname, '../renderer/index.html'))
} else {
// 🚧 Use ['ENV_NAME'] avoid vite:define plugin
const url = `http://${process.env['VITE_DEV_SERVER_HOST']}:${process.env['VITE_DEV_SERVER_PORT']}`
await win.loadURL(url)
// win.webContents.openDevTools()
}
// Test active push message to Renderer-process
win.webContents.on('did-finish-load', () => {
win?.webContents.send('main-process-message', 'load content finish from main process!')
})
// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https:')) shell.openExternal(url)
return { action: 'deny' }
})
}
app.whenReady().then(createWindow)
app.on('window-all-closed', () => {
win = null
if (process.platform !== 'darwin') app.quit()
})
app.on('second-instance', () => {
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore()
win.focus()
}
})
app.on('activate', () => {
const allWindows = BrowserWindow.getAllWindows()
if (allWindows.length) {
allWindows[0].focus()
} else {
createWindow()
}
})

View File

@ -0,0 +1,26 @@
import { builtinModules } from 'module'
import { defineConfig } from 'vite'
import pkg from '../../package.json'
export default defineConfig({
root: __dirname,
build: {
outDir: '../../dist/main',
emptyOutDir: true,
minify: process.env./* from mode option */NODE_ENV === 'production',
sourcemap: true,
lib: {
entry: 'index.ts',
formats: ['cjs'],
fileName: () => '[name].cjs',
},
rollupOptions: {
external: [
'electron',
...builtinModules,
// @ts-ignore
...Object.keys(pkg.dependencies || {}),
],
},
},
})

View File

@ -0,0 +1,7 @@
import { domReady } from './utils'
import { useLoading } from './loading'
const { appendLoading, removeLoading } = useLoading()
window.removeLoading = removeLoading
domReady().then(appendLoading)

View File

@ -0,0 +1,67 @@
/**
* https://tobiasahlin.com/spinkit
* https://connoratherton.com/loaders
* https://projects.lukehaas.me/css-loaders
* https://matejkustec.github.io/SpinThatShit
*/
export function useLoading() {
const className = `loaders-css__square-spin`
const styleContent = `
@keyframes square-spin {
25% { transform: perspective(100px) rotateX(180deg) rotateY(0); }
50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); }
75% { transform: perspective(100px) rotateX(0) rotateY(180deg); }
100% { transform: perspective(100px) rotateX(0) rotateY(0); }
}
.${className} > div {
animation-fill-mode: both;
width: 50px;
height: 50px;
background: #282c34;
animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite;
}
.app-loading-wrap {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
z-index: 9;
}
`
const oStyle = document.createElement('style')
const oDiv = document.createElement('div')
oStyle.id = 'app-loading-style'
oStyle.innerHTML = styleContent
oDiv.className = 'app-loading-wrap'
oDiv.innerHTML = `<div class="${className}"><div></div></div>`
return {
appendLoading() {
safe.append(document.head, oStyle)
safe.append(document.body, oDiv)
},
removeLoading() {
safe.remove(document.head, oStyle)
safe.remove(document.body, oDiv)
},
}
}
const safe = {
append(parent: HTMLElement, child: HTMLElement) {
if (!Array.from(parent.children).find(e => e === child)) {
return parent.appendChild(child)
}
},
remove(parent: HTMLElement, child: HTMLElement) {
if (Array.from(parent.children).find(e => e === child)) {
return parent.removeChild(child)
}
},
}

View File

@ -0,0 +1,15 @@
/** docoment ready */
export function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) {
return new Promise(resolve => {
if (condition.includes(document.readyState)) {
resolve(true)
} else {
document.addEventListener('readystatechange', () => {
if (condition.includes(document.readyState)) {
resolve(true)
}
})
}
})
}

View File

@ -0,0 +1,32 @@
import { join } from 'path'
import { builtinModules } from 'module'
import { defineConfig } from 'vite'
import pkg from '../../package.json'
export default defineConfig({
root: __dirname,
build: {
outDir: '../../dist/preload',
emptyOutDir: true,
minify: process.env./* from mode option */NODE_ENV === 'production',
// https://github.com/caoxiemeihao/electron-vue-vite/issues/61
sourcemap: 'inline',
rollupOptions: {
input: {
// multiple entry
index: join(__dirname, 'index.ts'),
},
output: {
format: 'cjs',
entryFileNames: '[name].cjs',
manualChunks: {},
},
external: [
'electron',
...builtinModules,
// @ts-ignore
...Object.keys(pkg.dependencies || {}),
],
},
},
})

View File

@ -0,0 +1,14 @@
<!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" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
<title>天牛网盘</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import {useStore} from "vuex";
import api from "./service/api";
const store = useStore();
if (store.getters.userToken) {
api.user.info().then(info => {
store.commit('setLoginUserInfo', info)
})
}
</script>
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<style lang="less">
#app {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#page-login {
.ant-input-affix-wrapper, .ant-btn {
border-radius: 5px;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1651895924070" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2966" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><defs><style type="text/css"></style></defs><path d="M549.766 97.925c19.974 4.609 50.703 35.339 62.995 55.312 35.339-13.828 36.875 41.484 46.094 55.312 23.047-16.901 46.094-35.339 46.094-55.312 0-21.51-30.729-35.339-30.729-41.484 0-12.292 38.411-12.292 41.484-12.292 50.703 0 106.016 39.948 106.016 96.797 0 27.656-9.219 41.484-33.802 70.677h32.266c21.51 0 27.656-3.073 55.312 7.682C923.125 294.591 960 348.367 960 389.852c0 3.073 0 9.219-1.536 13.828-4.609 3.073-12.292 3.073-16.901 4.609-15.365 1.536-32.266 6.146-47.63 7.682-47.63 0-50.703 0-53.776-1.536-1.536 1.536-1.536 0-1.536 4.609 0 3.073 7.682 44.557 10.755 67.604 9.219 56.849 12.292 115.234 19.974 173.62 1.536 7.682 7.682 15.365 9.219 23.047 3.073 13.828 6.146 27.656 6.146 39.948 0 153.646-245.833 208.958-321.119 208.958h-86.042c-115.234-9.219-248.906-52.24-301.145-138.281-4.609-7.682-18.437-39.948-18.437-50.703v-36.875c3.073-16.901 7.682-33.802 21.51-50.703v-84.505l12.292-132.135c-12.292 1.536-33.802 1.536-38.411 1.536-21.51 0-38.411-3.073-61.458-6.146-7.682-1.536-18.437-3.073-24.583-6.146-4.609-1.536-3.073-12.292-3.073-13.828 0-44.557 44.557-107.552 98.333-119.844 4.609-1.536 13.828-1.536 19.974-3.073l41.484-1.536c-13.828-10.755-36.875-46.094-36.875-59.922v-29.193c13.828-58.385 62.995-82.969 106.016-82.969 1.536 0 41.484 0 41.484 12.292 0 6.146-30.729 19.974-30.729 41.484 0 1.536 6.146 32.266 16.901 32.266 3.073 0-1.536-3.073 3.073-3.073 3.073 0 32.266 10.755 36.875 10.755h12.292l3.073-3.073c-19.974-16.901-30.729-36.875-36.875-53.776 12.292 7.682 18.438 9.219 29.193 9.219 27.656 0 46.094-15.365 78.359-29.193 24.583-10.755 52.24-13.828 78.359-18.437-12.292-9.219-24.583-16.901-36.875-23.047l38.411-1.536c7.68 1.537 15.362 4.61 23.044 6.146z" p-id="2967"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,112 @@
html,body{
user-select: none;
}
/*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/
::-webkit-scrollbar
{
width: 4px;
height: 10px;
background-color: transparent;
}
/*定义滚动条轨道 内阴影+圆角*/
::-webkit-scrollbar-track
{
border-radius: 2px;
background-color: transparent;
}
/*定义滑块 内阴影+圆角*/
::-webkit-scrollbar-thumb
{
border-radius: 2px;
background-color: #ccc;
}
.d-flex{
display: flex;
}
.flex-1{
flex: 1;
}
.page-main{
.avatar{
width: 32px;
height: 32px;
border-radius: 50%;
}
.main-header{
background-color:#fff;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
position: relative;
z-index: 2;
padding:0 20px;
.header-item{
margin-left: 20px;
}
}
.main-layout{
position: relative;
z-index: 1;
}
.main-side-bar{
background-color:#fff;
}
.file-content-wrapper{
padding:10px 0;
background-color: #fff;
overflow: auto;
}
.files-wrapper{
.header{
padding:10px;
}
.more-action{
display: none;
font-size:12px;
}
.action-item{
margin-left: 10px;
display: inline-block;
cursor: pointer;
&:hover,&:active{
color:#2579f8;
}
.action{
margin-right: 5px;
}
}
.list-item{
line-height: 36px;
&.list-item{
border-top:solid 1px #efefef;
}
&:hover{
background-color: #fafafa;
.more-action{
display: block;
}
}
}
.file-name{
flex:1;
.name{
flex:1
}
}
.file-type{
width: 100px;
}
.file-size{
width: 100px;
}
.file-date{
width: 100px;
}
.content-wrapper{
.file-name{
padding: 0 10px;
}
}
}
}

View File

@ -0,0 +1,15 @@
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,144 @@
<template>
<div class="drag-wrapper" ref="dragDiv">
<div class="drag_bg" ref="dragBg"></div>
<div class="drag_text f14" :style="{'color':confirmSuccess?'#fff':'inherit'}">{{ confirmWords }}</div>
<!-- 移动的模块 -->
<div ref="dragItem" :class="{'handler_ok_bg': confirmSuccess}" class="handler handler_bg"
@mousedown="mousedown($event)"></div>
</div>
</template>
<script>
export default {
name: "DragValidator",
props: {
placeholder: {
type: String,
default: '请按住滑块拖动到最右边'
},
successText: {
type: String,
default: '验证通过'
}
},
emits: ['onValidation'],
data() {
return {
beginClientX: 0, /* 距离屏幕左端距离 */
allowDrag: false, /*触发拖动状态 判断*/
maxWidth: 0, /*拖动最大宽度,依据滑块宽度算出来的*/
confirmWords: '', /*滑块文字*/
confirmSuccess: false, /*验证成功判断*/
/**
* @type {HTMLElement}
*/
rootHtml: null
}
},
mounted() {
this.confirmWords = this.placeholder;
this.maxWidth = this.$refs.dragDiv.clientWidth - this.$refs.dragItem.clientWidth
this.rootHtml = document.getElementsByTagName('html')[0]
//
this.rootHtml.addEventListener('mousemove', this.mousemove)
//
this.rootHtml.addEventListener('mouseup', this.moseup)
},
methods: {
mousedown: function (e) {
if (!this.confirmSuccess) {
e.preventDefault && e.preventDefault() //
this.allowDrag = true //
this.beginClientX = e.clientX
this.$refs.dragBg.style.transition = ''
this.$refs.dragItem.style.transition = ''
}
},
success() {
this.confirmSuccess = true
this.confirmWords = this.successText
this.$emit('onValidation', true)
if (window.addEventListener) {
this.rootHtml.removeEventListener('mousemove', this.mousemove)
this.rootHtml.removeEventListener('mouseup', this.moseup)
}
this.$refs.dragItem.style.left = (this.maxWidth - 2) + 'px'
this.$refs.dragBg.style.width = (this.maxWidth + 20) + 'px'
},
//
mousemove(e) {
if (this.allowDrag) {
let width = e.clientX - this.beginClientX //
if (width > 0 && width < this.maxWidth) {
this.$refs.dragItem.style.left = (width - 2) + 'px'
this.$refs.dragBg.style.width = (width + 20) + 'px'
} else if (width >= this.maxWidth) {
this.success()
}
}
},
//mousemove
moseup(e) {
this.allowDrag = false
const width = e.clientX - this.beginClientX
if (width < this.maxWidth) {
this.$refs.dragBg.style.transition = 'width .5s'
this.$refs.dragItem.style.transition = 'left .5s'
this.$refs.dragItem.style.left = 0 + 'px'
this.$refs.dragBg.style.width = 0 + 'px'
}
}
}
}
</script>
<style scoped>
.drag-wrapper {
position: relative;
background-color: #e8e8e8;
width: 100%;
line-height: 40px;
text-align: center;
border-radius: 5px;
overflow: hidden;
}
.handler {
width: 40px;
height: 40px;
border: 1px solid #ccc;
cursor: move;
position: absolute;
top: 0px;
left: 0px;
}
.handler_bg {
background: #fff url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo0ZDhlNWY5My05NmI0LTRlNWQtOGFjYi03ZTY4OGYyMTU2ZTYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NTEyNTVEMURGMkVFMTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NTEyNTVEMUNGMkVFMTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo2MTc5NzNmZS02OTQxLTQyOTYtYTIwNi02NDI2YTNkOWU5YmUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NGQ4ZTVmOTMtOTZiNC00ZTVkLThhY2ItN2U2ODhmMjE1NmU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+YiRG4AAAALFJREFUeNpi/P//PwMlgImBQkA9A+bOnfsIiBOxKcInh+yCaCDuByoswaIOpxwjciACFegBqZ1AvBSIS5OTk/8TkmNEjwWgQiUgtQuIjwAxUF3yX3xyGIEIFLwHpKyAWB+I1xGSwxULIGf9A7mQkBwTlhBXAFLHgPgqEAcTkmNCU6AL9d8WII4HOvk3ITkWJAXWUMlOoGQHmsE45ViQ2KuBuASoYC4Wf+OUYxz6mQkgwAAN9mIrUReCXgAAAABJRU5ErkJggg==") no-repeat center;
border-radius: 5px;
}
.handler_ok_bg {
background: #fff url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo0ZDhlNWY5My05NmI0LTRlNWQtOGFjYi03ZTY4OGYyMTU2ZTYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDlBRDI3NjVGMkQ2MTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDlBRDI3NjRGMkQ2MTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDphNWEzMWNhMC1hYmViLTQxNWEtYTEwZS04Y2U5NzRlN2Q4YTEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NGQ4ZTVmOTMtOTZiNC00ZTVkLThhY2ItN2U2ODhmMjE1NmU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+k+sHwwAAASZJREFUeNpi/P//PwMyKD8uZw+kUoDYEYgloMIvgHg/EM/ptHx0EFk9I8wAoEZ+IDUPiIMY8IN1QJwENOgj3ACo5gNAbMBAHLgAxA4gQ5igAnNJ0MwAVTsX7IKyY7L2UNuJAf+AmAmJ78AEDTBiwGYg5gbifCSxFCZoaBMCy4A4GOjnH0D6DpK4IxNSVIHAfSDOAeLraJrjgJp/AwPbHMhejiQnwYRmUzNQ4VQgDQqXK0ia/0I17wJiPmQNTNBEAgMlQIWiQA2vgWw7QppBekGxsAjIiEUSBNnsBDWEAY9mEFgMMgBk00E0iZtA7AHEctDQ58MRuA6wlLgGFMoMpIG1QFeGwAIxGZo8GUhIysmwQGSAZgwHaEZhICIzOaBkJkqyM0CAAQDGx279Jf50AAAAAABJRU5ErkJggg==") no-repeat center;
}
.drag_bg {
background-color: #7ac23c;
height: 40px;
width: 0px;
}
.drag_text {
position: absolute;
top: 0px;
width: 100%;
text-align: center;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
-o-user-select: none;
-ms-user-select: none;
}
</style>

View File

@ -0,0 +1,39 @@
<template>
<div class="app-logo">
<svg class="logo-img" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="1000" height="1000">
<path
d="M549.766 97.925c19.974 4.609 50.703 35.339 62.995 55.312 35.339-13.828 36.875 41.484 46.094 55.312 23.047-16.901 46.094-35.339 46.094-55.312 0-21.51-30.729-35.339-30.729-41.484 0-12.292 38.411-12.292 41.484-12.292 50.703 0 106.016 39.948 106.016 96.797 0 27.656-9.219 41.484-33.802 70.677h32.266c21.51 0 27.656-3.073 55.312 7.682C923.125 294.591 960 348.367 960 389.852c0 3.073 0 9.219-1.536 13.828-4.609 3.073-12.292 3.073-16.901 4.609-15.365 1.536-32.266 6.146-47.63 7.682-47.63 0-50.703 0-53.776-1.536-1.536 1.536-1.536 0-1.536 4.609 0 3.073 7.682 44.557 10.755 67.604 9.219 56.849 12.292 115.234 19.974 173.62 1.536 7.682 7.682 15.365 9.219 23.047 3.073 13.828 6.146 27.656 6.146 39.948 0 153.646-245.833 208.958-321.119 208.958h-86.042c-115.234-9.219-248.906-52.24-301.145-138.281-4.609-7.682-18.437-39.948-18.437-50.703v-36.875c3.073-16.901 7.682-33.802 21.51-50.703v-84.505l12.292-132.135c-12.292 1.536-33.802 1.536-38.411 1.536-21.51 0-38.411-3.073-61.458-6.146-7.682-1.536-18.437-3.073-24.583-6.146-4.609-1.536-3.073-12.292-3.073-13.828 0-44.557 44.557-107.552 98.333-119.844 4.609-1.536 13.828-1.536 19.974-3.073l41.484-1.536c-13.828-10.755-36.875-46.094-36.875-59.922v-29.193c13.828-58.385 62.995-82.969 106.016-82.969 1.536 0 41.484 0 41.484 12.292 0 6.146-30.729 19.974-30.729 41.484 0 1.536 6.146 32.266 16.901 32.266 3.073 0-1.536-3.073 3.073-3.073 3.073 0 32.266 10.755 36.875 10.755h12.292l3.073-3.073c-19.974-16.901-30.729-36.875-36.875-53.776 12.292 7.682 18.438 9.219 29.193 9.219 27.656 0 46.094-15.365 78.359-29.193 24.583-10.755 52.24-13.828 78.359-18.437-12.292-9.219-24.583-16.901-36.875-23.047l38.411-1.536c7.68 1.537 15.362 4.61 23.044 6.146z"
p-id="2967"></path>
</svg>
<span class="logo-text">天牛网盘</span>
</div>
</template>
<script>
export default {
name: "MainLogo"
}
</script>
<style scoped>
.app-logo{
height: 100%;
position: relative;
}
.logo-img {
width: 60px;
height: 100%;
margin-right: 10px;
fill: #666;
}
.logo-text {
text-shadow: 0 0 1px rgba(0, 0, 0, 0.3);
position: absolute;
top:50%;
transform: translateY(-50%);
}
</style>

View File

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@ -0,0 +1,8 @@
export { }
declare global {
interface Window {
removeLoading: () => void
}
}

View File

@ -0,0 +1,16 @@
import { createApp } from 'vue'
import Antd from 'ant-design-vue';
import router from "./service/router";
import App from './App.vue';
import 'ant-design-vue/dist/antd.css';
import './assets/style.less'
import './service/node-api'
import store from "./service/store";
createApp(App)
.use(store)
.use(Antd)
.use(router)
.mount('#app')
.$nextTick(window.removeLoading)
.then( r =>console.log('app render finish'))

View File

@ -0,0 +1,158 @@
import store from "./store";
import router from "./router";
const API_PATH = "http://localhost:8080"
/**
*
* @param api
* @param {'GET'|'POST'|'FILE'|string} method
* @param postData
*/
function request<T>(api:string, method = 'GET', postData:{[key:string]:string}|any = {}) {
//return fetch(API_PATH + api).then(res => res.json());
let options:RequestInit = {
method
};
if (method.toUpperCase() == 'GET') {
// post => 'key=value&key1=value1'
const params = [];
for (let key in postData) {
params.push(`${key}=${postData[key]}`);
}
api += (api.includes('?') ? '&' : '?') + params.join('&');
} else {
options = {
method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(postData) // 参数
}
}
let Authorization = store.getters.userToken
if (Authorization) {
options = {
headers: {
Authorization
},
...options
}
}
return new Promise<T>((resolve, reject) => {
const processResult = (result:ApiResult) => {
if (result.code === 0) { // 判断响应码是否正常
resolve(result.data)
} else {
const route = router.currentRoute.value;
// 未登录或token无效
if (result.code == 401) {
store.commit('clearToken') // 清除token
if (route.path != '/login') router.replace('/login').then(() => console.log('auth show login')).catch();
reject(null)
return;
}
// 其他异常 直接抛出错误
reject(new Error(result.message))
}
}
fetch(API_PATH + api, options)
.then(res => res.json())
.then(processResult)
.catch(e => {
console.log(e)
reject(e);
})
});
}
export default {
user: {
login(params:LoginForm) {
return request<LoginUser>('/api/user/login', 'POST', {
device:'client',
...params
});
},
signup(params:any) {
return request('/api/user/reg', 'POST', params);
},
info(){
return request<LoginUser>('/api/user/info');
}
},
folder: {
/**
*
* @param path
* @returns {Promise<Array>}
*/
list(folderPath = '/') {
return request<FileItem[]>(`/api/folder/list`, 'GET', {folderPath})
},
create(parent:string, name:string) {
return request(`/api/folder/create`, 'POST', {parent, name});
}
},
file: {
/**
*
* @param params
* @returns {Promise<unknown>}
*/
fastUpload(params:any) {
return request('/api/fast-upload', 'POST', params);
},
delete(file:string) {
return request('/api/file/delete', 'POST', file);
},
rename(file:string) {
return request('/api/file/rename', 'POST', file);
},
upload(parent:string, file:File, onProcess = null) {
// 上传文件到某个目录
const postData = new FormData(); // 将数据封装成form表单
postData.append("parent", parent); // 父目录
postData.append("file", file);// 文件
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open('POST', API_PATH + '/api/upload');
request.upload.addEventListener('progress', function (e) {
// upload progress as percentage
let progress = (e.loaded / e.total) * 100;
// @ts-ignore
onProcess({
uploaded: e.loaded,
total: e.total,
progress
})
});
request.addEventListener('load', function (e) {
// HTTP status message (200, 404 etc)
let ret = {message: null}
try {
ret = JSON.parse(request.response)
if (request.status == 200) {
resolve(JSON.parse(request.response));
} else {
reject(Error(ret.message || '上传文件出错!'));
}
} catch (e) {
console.log(e)
reject(Error('上传文件异常'))
}
});
request.send(postData);
})
// return request('/api/upload', 'FILE', postData, onProcess)
}
},
share: {
create(info:ShareInfo) {
return request('/api/share/create', 'POST', info)
}
}
}

View File

@ -0,0 +1,14 @@
// import {lstat, readdir} from 'fs/promises'
// import {cwd} from 'process'
import {ipcRenderer} from 'electron'
// Usage of ipcRenderer.on
ipcRenderer.on('main-process-message', (_event, ...args) => {
console.log('[Receive Main-process message]:', ...args)
})
// readdir(cwd()).then(stats => {
// console.log('[readdir]', stats)
// }).catch(err => {
// console.error(err)
// })

View File

@ -0,0 +1,31 @@
import {createMemoryHistory, createRouter} from "vue-router";
import store from "./store";
const router = createRouter({
history: createMemoryHistory(),
routes: [
{
name: 'Main',
path: '/',
component: () => import('../views/Main.vue')
},
{
name: "Login",
path: "/login",
component: () => import('../views/Login.vue')
}
]
})
const loginPath = '/login' // 登录路径
router.beforeEach((to, from, next) => {
const toPath = to.path; // 获取要展示得路径
if (toPath == loginPath) next() // 判断是否可匿名访问
else {
// 验证token
if (!store.getters.userToken) next(loginPath) // 没有登录直接打开登录页面
else next();
}
})
export default router

View File

@ -0,0 +1,57 @@
import {createStore, Store} from 'vuex'
import api from "./api";
const TOKEN_KEY = 'user_auth_token';
declare type StoreState = {
/**
*
*/
loginUser: UserInfo | null,
token: string | null
}
const store = createStore<StoreState>({
state() {
return {
loginUser: null,
token: null
}
},
mutations: {
// 此选项中的方法必须同步
setLoginUserInfo(state:StoreState, info:UserInfo) {
state.loginUser = info;
},
setToken(state:StoreState, token:string) {
state.token = token;
// sessionStorage.setItem(TOKEN_KEY,token);
localStorage.setItem(TOKEN_KEY, token);
},
clearToken(state:StoreState) {
state.token = null;
localStorage.removeItem(TOKEN_KEY);
sessionStorage.removeItem(TOKEN_KEY)
}
},
actions: {
login({commit}, data: LoginForm) {
return new Promise((resolve, reject) => {
api.user.login(data).then(
(ret: LoginUser) => {
commit('setToken', ret.token);
commit('setLoginUserInfo', ret.userInfo)
resolve(ret)
}).catch((e:Error) => reject(e))
});
}
},
getters: {
userToken(state:StoreState) {
const token = state.token || sessionStorage.getItem(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY);
// 如果内存中没有token 存储一份
if (!state.token) state.token = token;
return token
}
}
})
export default store

View File

@ -0,0 +1,65 @@
declare type ApiResult = { code: number, message: string, data: any }
declare type FileItem = {
createTime: string,
id: number,
name: string,
path: string,
/**
*
*/
thumb: string,
size: number,
type: 'folder' | string,
updateTime: string
}
declare type FileIconInstance = {
file: FileItem,
ext: String,
/**
*
*/
showPreview: Function
}
/**
*
*/
declare type ShareInfo = {
id?: string,
title: string,
uid: number,
fileId: number,
type: 1 | 2,
password: string,
live: number,
createTime?: string,
updateTime?: string,
status: number
}
declare type UserInfo = {
account: string
avatar: string
createTime: string
email: string
id: number
lastIp: string
lastLogin: string
nickname: string
password: string
salt: string
sex: number
status: number
updateTime: string
}
declare type LoginUser = {
account: string
permissions: string[]
roles: string[]
token: string
userInfo: UserInfo
}
interface LoginForm {
username: string;
password: string;
remember: boolean;
}

View File

@ -0,0 +1,152 @@
<script lang="ts">
import {UserOutlined, LockOutlined} from '@ant-design/icons-vue';
import {computed, defineComponent, reactive, ref} from "vue";
import DragValidator from "../components/DragValidator.vue";
import {message} from "ant-design-vue";
import {useStore} from "vuex";
import {useRouter} from "vue-router";
import MainLogo from "../components/MainLogo.vue";
interface FormState {
username: string;
password: string;
}
export default defineComponent({
components: {
MainLogo,
DragValidator,
UserOutlined,
LockOutlined,
},
setup() {
const formState = reactive<FormState>({
username: '',
password: ''
});
const dragChecked = ref<boolean>(false);
const store = useStore(),router = useRouter();
const onFinish = async (values: any) => {
// if(!dragChecked){
// message.info('')
// return;
// }
try {
await store.dispatch('login',values)
router.replace('/').then(()=>console.log('login success')).catch((e)=>console.log(e));
}catch (e:Error|any) {
message.error(e.message || '登录失败')
}
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
const disabled = computed(() => {
return !(formState.username && formState.password && dragChecked.value);
});
return {
formState,
onFinish,
onFinishFailed,
disabled,
onValidate(state:boolean){
dragChecked.value = state
}
};
}
})
</script>
<template>
<div id="page-login">
<div class="logo">
<main-logo class="login-logo" />
</div>
<div id="login-form">
<h3 class="title">登录</h3>
<div class="wrapper">
<a-form
:model="formState"
name="normal_login"
class="login-form"
@finish="onFinish"
@finishFailed="onFinishFailed"
>
<a-form-item
name="username"
:rules="[{ required: true, message: 'Please input your username!' }]"
>
<a-input size="large" v-model:value="formState.username">
<template #prefix>
<UserOutlined class="site-form-item-icon"/>
</template>
</a-input>
</a-form-item>
<a-form-item>
<drag-validator @onValidation="onValidate" />
</a-form-item>
<a-form-item
name="password"
:rules="[{ required: true, message: 'Please input your password!' }]"
>
<a-input-password size="large" v-model:value="formState.password">
<template #prefix>
<LockOutlined class="site-form-item-icon"/>
</template>
</a-input-password>
</a-form-item>
<a-form-item style="margin-top:40px;">
<a-button size="large" :disabled="disabled" type="primary" block
html-type="submit" class="login-form-button"> </a-button>
</a-form-item>
</a-form>
</div>
</div>
</div>
</template>
<style scoped lang="less">
#page-login {
height: 100vh;
background-image: url("https://g.alicdn.com/uc-cloud-drive-web-system/cloud-drive-web/3.5.96/assets/a7cb99cdcd7cf16599f3ca6a554a0e44.jpg");
background-repeat: no-repeat;
background-position: center;
}
.logo {
font-size: 34px;
position: absolute;
left: 30px;
top: 30px;
display: flex;
line-height: 60px;
color: #333;
}
.login-logo{
height: 60px;
}
.title {
background-color: #eaeaea;
padding: 10px 30px;
font-size: 24px;
}
#login-form {
width: 400px;
background-color: #fff;
border-radius: 5px;
overflow: hidden;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 50px;
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2);
}
.wrapper {
padding: 30px;
}
</style>

View File

@ -0,0 +1,113 @@
<template>
<a-layout class="page-main">
<a-layout-header class="main-header d-flex">
<div class="flex-1">
<main-logo class="main-logo" />
</div>
<div class="d-flex">
<div class="header-item">
<a-dropdown :trigger="['click']">
<div>
<img class="avatar" :src="userinfo.avatar"/>
<span>{{ userinfo.nickname }}</span>
</div>
<template #overlay>
<a-menu>
<a-menu-item key="0">
<a href="http://www.alipay.com/">1st menu item</a>
</a-menu-item>
<a-menu-item key="1">
<a href="http://www.taobao.com/">2nd menu item</a>
</a-menu-item>
<a-menu-divider/>
<a-menu-item key="3">3rd menu item</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div class="header-item">
<setting-filled style="font-size: 24px" />
</div>
</div>
</a-layout-header>
<a-layout class="main-layout">
<a-layout-sider class="main-side-bar">
<a-button @click="reload">Reload</a-button>
</a-layout-sider>
<a-layout-content class="file-content-wrapper">
<div class="tool-bar"></div>
<div class="files-wrapper">
<div class="header d-flex">
<div class="file-name">文件名</div>
<div class="file-type">类型</div>
<div class="file-size">大小</div>
<div class="file-date">修改日期</div>
</div>
<div class="content-wrapper">
<div class="list-item d-flex" v-for="f in fileList" :key="f.id">
<div class="file-name d-flex">
<div class="name">{{ f.name }}</div>
<div class="more-action">
<span class="action-item"><download-outlined/>下载</span>
<span class="action-item"><redo-outlined/>重命名</span>
<span class="action-item"><share-alt-outlined/>分享</span>
<span class="action-item"><delete-outlined/>删除</span>
</div>
</div>
<div class="file-type">{{ f.type }}</div>
<div class="file-size">{{ f.size }}</div>
<div class="file-date">{{ formatDate(f.createTime) }}</div>
</div>
</div>
</div>
</a-layout-content>
</a-layout>
<!-- <a-layout-footer>Footer</a-layout-footer>-->
</a-layout>
</template>
<script lang="ts">
import api from "../service/api";
import {computed, onMounted, ref} from "vue";
import dayjs from "dayjs";
import {DownloadOutlined, RedoOutlined, DeleteOutlined, ShareAltOutlined,SettingFilled} from '@ant-design/icons-vue';
import {useStore} from "vuex";
import MainLogo from "../components/MainLogo.vue";
export default {
name: "Main",
components: {MainLogo, DownloadOutlined, RedoOutlined, DeleteOutlined, ShareAltOutlined,SettingFilled},
setup() {
const fileList = ref<FileItem[]>([])
const store = useStore()
const userinfo = computed(() => store.state.loginUser)
const loadFolderFiles = () => {
api.folder.list().then(list => {
fileList.value = list
})
};
onMounted(loadFolderFiles)
return {
userinfo,
fileList,
reload() {
location.reload()
},
formatDate(time) {
return dayjs(time).format('MM-DD HH:mm')
}
}
}
}
</script>
<style scoped>
.page-main {
height: 100vh;
overflow: auto;
}
.main-logo{
font-size: 24px;
}
</style>

View File

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@ -0,0 +1,59 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import resolve, { lib2esm } from 'vite-plugin-resolve'
import electron from 'vite-plugin-electron/renderer'
import pkg from '../../package.json'
// https://vitejs.dev/config/
export default defineConfig({
mode: process.env.NODE_ENV,
root: __dirname,
plugins: [
vue(),
electron(),
resolve(
/**
* Here you can specify other modules
* 🚧 You have to make sure that your module is in `dependencies` and not in the` devDependencies`,
* which will ensure that the electron-builder can package it correctly
*/
{
// If you use the following modules, the following configuration will work
// What they have in common is that they will return - ESM format code snippets
// ESM format string
'electron-store': 'export default require("electron-store");',
// Use lib2esm() to easy to convert ESM
// Equivalent to
/**
* sqlite3: () => `
* const _M_ = require('sqlite3');
* const _D_ = _M_.default || _M_;
* export { _D_ as default }
* `
*/
sqlite3: lib2esm('sqlite3', { format: 'cjs' }),
serialport: lib2esm(
// CJS lib name
'serialport',
// export memebers
[
'SerialPort',
'SerialPortMock',
],
{ format: 'cjs' },
),
}
),
],
base: './',
build: {
outDir: '../../dist/renderer',
emptyOutDir: true,
sourcemap: true,
},
server: {
host: pkg.env.VITE_DEV_SERVER_HOST,
port: pkg.env.VITE_DEV_SERVER_PORT,
},
})

View File

@ -0,0 +1,16 @@
import { PlaywrightTestConfig, devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
use: {
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
};
export default config;

View File

@ -0,0 +1,5 @@
import { build } from 'vite'
await build({ configFile: 'packages/main/vite.config.ts' })
await build({ configFile: 'packages/preload/vite.config.ts' })
await build({ configFile: 'packages/renderer/vite.config.ts' })

View File

@ -0,0 +1,99 @@
import { spawn } from 'child_process'
import { createServer, build } from 'vite'
import electron from 'electron'
import readline from 'readline'
const query = new URLSearchParams(import.meta.url.split('?')[1])
const debug = query.has('debug')
/** The log will display on the next screen */
function clearConsole() {
const blank = '\n'.repeat(process.stdout.rows)
console.log(blank)
readline.cursorTo(process.stdout, 0, 0)
readline.clearScreenDown(process.stdout)
}
/**
* @type {(server: import('vite').ViteDevServer) => Promise<import('rollup').RollupWatcher>}
*/
function watchMain(server) {
/**
* @type {import('child_process').ChildProcessWithoutNullStreams | null}
*/
let electronProcess = null
const address = server.httpServer.address()
const env = Object.assign(process.env, {
VITE_DEV_SERVER_HOST: address.address,
VITE_DEV_SERVER_PORT: address.port,
})
/**
* @type {import('vite').Plugin}
*/
const startElectron = {
name: 'electron-main-watcher',
writeBundle() {
clearConsole()
if (electronProcess) {
electronProcess.removeAllListeners()
electronProcess.kill()
electronProcess = null
}
electronProcess = spawn(electron, ['.'], { env })
electronProcess.on('exit', process.exit)
// https://github.com/electron-vite/electron-vite-vue/pull/129
electronProcess.stdout.on('data', (data) => {
const str = data.toString().trim()
str && console.log(str)
})
electronProcess.stderr.on('data', (data) => {
const str = data.toString().trim()
str && console.error(str)
})
},
}
return build({
configFile: 'packages/main/vite.config.ts',
mode: 'development',
plugins: [!debug && startElectron].filter(Boolean),
build: {
watch: {},
},
})
}
/**
* @type {(server: import('vite').ViteDevServer) => Promise<import('rollup').RollupWatcher>}
*/
function watchPreload(server) {
return build({
configFile: 'packages/preload/vite.config.ts',
mode: 'development',
plugins: [{
name: 'electron-preload-watcher',
writeBundle() {
clearConsole()
server.ws.send({ type: 'full-reload' })
},
}],
build: {
watch: {},
},
})
}
// Block the CTRL + C shortcut on a Windows terminal and exit the application without displaying a query
if (process.platform === 'win32') {
readline.createInterface({ input: process.stdin, output: process.stdout }).on('SIGINT', process.exit)
}
// bootstrap
const server = await createServer({ configFile: 'packages/renderer/vite.config.ts' })
await server.listen()
await watchPreload(server)
await watchMain(server)

View File

@ -0,0 +1,16 @@
// example.spec.ts
import { test, expect } from '@playwright/test'
import { env } from '../package.json'
const VITE_SERVER_ADDRESS = `http://127.0.0.1:${env.PORT || 3344}`
test('example test case', async ({ page }) => {
await page.goto(VITE_SERVER_ADDRESS)
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Vite App/)
// Expect an attribute "Hello Vue 3 + TypeScript + Vite" to be visible on the page.
await expect(
page.locator('text=Hello Vue 3 + TypeScript + Vite').first(),
).toBeVisible()
})

17
client-pc/tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"importHelpers": true,
"jsx": "preserve",
"esModuleInterop": true,
"resolveJsonModule": true,
"sourceMap": true,
"baseUrl": "./",
"strict": true,
"paths": {},
"allowSyntheticDefaultImports": true,
"skipLibCheck": true
}
}

8
client-pc/types.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production'
readonly VITE_DEV_SERVER_HOST: string
readonly VITE_DEV_SERVER_PORT: string
}
}

2600
client-pc/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,77 @@
package xyz.longicorn.driver.controller;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.captcha.generator.RandomGenerator;
import cn.hutool.core.lang.UUID;
import lombok.SneakyThrows;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.longicorn.driver.dto.ApiResult;
import xyz.longicorn.driver.util.RedisUtil;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@RestController
public class CodeController {
@Resource
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/code-id")
public String createCodeId() {
return UUID.fastUUID().toString();
}
@RequestMapping("/code")
@SneakyThrows
public void createImage(String id, HttpServletResponse response) {
// 自定义纯数字的验证码随机4位数字可重复
RandomGenerator randomGenerator = new RandomGenerator("0123456789", 4);
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);
lineCaptcha.setGenerator(randomGenerator);
final ServletOutputStream os = response.getOutputStream();
String code = lineCaptcha.getCode();
// 使用id保存对应的验证码
stringRedisTemplate.opsForValue().set("code:" + id, code, 180, TimeUnit.SECONDS);
System.out.println("当前验证码:" + code);
lineCaptcha.write(os);
}
@RequestMapping("/verify")
public String verify(String id, String code) {
String key = "code:" + id;
String code1 = stringRedisTemplate.opsForValue().get(key);
if (code1 != null && code1.equals(code)) {
// 使原始验证码失效
stringRedisTemplate.delete(key);
return "true";
}
return "false";
}
@RequestMapping("/create")
public ApiResult createBase64Code() {
RandomGenerator randomGenerator = new RandomGenerator("0123456789", 4);
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);
lineCaptcha.setGenerator(randomGenerator);
String code = lineCaptcha.getCode();
String id = UUID.fastUUID().toString();
// 使用id保存对应的验证码
stringRedisTemplate.opsForValue().set("code:" + id, code, 180, TimeUnit.SECONDS);
// 获取验证码的base64
String image = lineCaptcha.getImageBase64Data();
Map<String, String> data = new HashMap<>();
data.put("id", id);
data.put("image", image);
return ApiResult.success(data);
}
}

View File

@ -1,7 +1,9 @@
package xyz.longicorn.driver.controller;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.IdUtil;
import lombok.SneakyThrows;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@ -19,6 +21,8 @@ import java.util.Map;
@RestController
@RequestMapping("/api/user")
public class UserController {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private UserService userService;
@ -27,9 +31,23 @@ public class UserController {
@SneakyThrows
@PostMapping("/login")
public ApiResult login(@Validated @RequestBody LoginModel model) {
Thread.sleep(3);
String key = "code:" + model.getCodeId();
String code1 = stringRedisTemplate.opsForValue().get(key);
if (code1 == null || !code1.equals(model.getCode())) {
return ApiResult.error(1,"验证码无效");
}
// 使原始验证码失效
stringRedisTemplate.delete(key);
Thread.sleep(2);
final LoginUser user = userService.login(model.getUsername(), model.getPassword());
return ApiResult.success(user);
}
@RequestMapping("/info")
public ApiResult loginUserInfo() {
return ApiResult.success(StpUtil.getSession().get("user"));
}
}

View File

@ -11,5 +11,8 @@ public class LoginModel {
@NotBlank(message = "密码必须填写")
private String password;
// 验证码
@NotBlank(message = "验证码必须填写")
private String code;
@NotBlank(message = "验证码必须填写")
private String codeId;
}

View File

@ -1,11 +1,22 @@
<template>
<!-- 所有子页面显示在这里 -->
<router-view />
<router-view/>
</template>
<script>
import api from "./service/api";
export default {
name: "App.vue"
name: "App.vue",
mounted() {
const store = this.$store
const route = this.$route;
if(store.getters.userToken){
api.user.info().then(info=>{
store.commit('setLoginUserInfo',info)
})
}
}
}
</script>

View File

@ -63,15 +63,7 @@ export default {
}
},
methods: {
formatDate(time, format = 'MM-DD') {
return dayjs(time).format(format);
},
formatSize(a, b = 2) {
if (0 == a) return "0 B";
let c = 1024, d = b || 2, e = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
f = Math.floor(Math.log(a) / Math.log(c));
return parseFloat((a / Math.pow(c, f)).toFixed(d)) + " " + e[f];
},
...strings,
//
formatName(name) {
if (!name || name.length < 15) return name;

View File

@ -0,0 +1,11 @@
<template>
<h1>TestA ==> {{ data.state.username }}</h1>
</template>
<script setup>
import data from '../service/data'
</script>
<style scoped>
</style>

View File

@ -0,0 +1,11 @@
<template>
<h1>TestB ==> {{ data.state.username }}</h1>
</template>
<script setup>
import data from '../service/data'
</script>
<style scoped>
</style>

View File

@ -11,10 +11,12 @@ import router from './router'
// 导入入口组件
import App from "./App.vue";
import data from "./service/data";
const app = createApp(App)
app.component('ContextMenu', ContextMenu);
app.use(ElementPlus)
.use(router) // 使用路由实例
.use(store)
.use(router) // 使用路由实例
// .use(data) // 使用vuex创建的store
.mount("#app");

View File

@ -58,10 +58,14 @@
</template>
</el-table-column>
<el-table-column prop="type" label="类型" width="200"/>
<el-table-column prop="size" label="大小" width="200"/>
<el-table-column prop="size" label="大小" width="200">
<template #default="file">
<span class="list-file-size">{{ formatSize(file.row.size) }}</span>
</template>
</el-table-column>
<el-table-column label="创建时间" width="200">
<template #default="file">
<span class="list-file-time">{{ formatDate(file.row.createTime) }}</span>
<span class="list-file-time">{{ formatDate(file.row.createTime,'YYYY-MM-DD HH:mm') }}</span>
</template>
</el-table-column>
</el-table>
@ -132,6 +136,7 @@ import {dayjs, ElMessage, ElMessageBox} from 'element-plus'
import api from "../service/api";
import qs from "qs";
import FileBlockItem from "../components/FileBlockItem.vue";
import strings from "../service/strings";
export default {
name: "All",
@ -234,6 +239,7 @@ export default {
// window.removeEventListener('hashchange', this.handleHashChange); //
},
methods: {
...strings,
//
async loadFileByPath(path = '/') {
this.currentPath = path //
@ -364,5 +370,17 @@ export default {
</script>
<style scoped>
.list-file-info{
display: flex;
line-height: 40px;
height: 40px;
}
.list-file-icon{
width: 40px;
display: flex;
align-items: center;
}
.list-file-name{
margin-left: 5px;
}
</style>

18
web/src/pages/Forget.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<div>
<h2 style="color: red">{{ store.state.username }}</h2>
<button @click="changeName">change username</button>
<TestA/>
sss
<TestB v-if="store.state.username == '456456'" />
</div>
</template>
<script setup>
import TestA from "../components/TestA.vue";
import TestB from "../components/TestB.vue";
import {useStore} from 'vuex'
const store = useStore();
function changeName() {
store.commit('changeUsername','456789')
}
</script>

View File

@ -49,6 +49,17 @@
text-decoration: none;
color: #999999;
}
.input-code-item {
position: relative;
}
.verify-code {
height: 35px;
position: absolute;
right: 2px;
top: 2px;
}
</style>
<template>
<div id="page-login">
@ -77,6 +88,10 @@
<el-form-item label="" prop="password" class="input-item">
<el-input v-model="loginForm.password" placeholder="登录密码" type="password" autocomplete="off"/>
</el-form-item>
<el-form-item label="" prop="code" class="input-item input-code-item">
<el-input v-model="loginForm.code" placeholder="验证码" autocomplete="off"/>
<img @click="refreshCode" :src="currentCodeSrc" class="verify-code" alt="">
</el-form-item>
<el-form-item>
<el-button :loading="loginLoading" style="width: 100%" type="primary" @click="submitForm">登录
</el-button>
@ -91,10 +106,11 @@
</template>
<script>
import {defineComponent, reactive, ref} from 'vue'
import {defineComponent, onMounted, reactive, ref} from 'vue'
import {ElMessage, ElMessageBox} from "element-plus";
import {useRouter} from 'vue-router'
import {useStore} from "vuex";
import api from "../service/api";
export default defineComponent({
name: 'Login',
@ -109,10 +125,31 @@ export default defineComponent({
* @type {Ref<FormInstance>}
*/
const form = ref(null);
//
const codeId = crypto.randomUUID();
// 使reactive
const loginForm = reactive({
password: '',
username: ''
username: '',
code: '',
codeId
})
//
const currentCodeSrc = ref('')
//
const refreshCode = ()=>{
//
//currentCodeSrc.value = '//localhost:8080/code?id=' + loginForm.codeId + '&r=' + Math.random();
api.code().then(ret=>{
// console.log(ret)
loginForm.codeId = ret.id
currentCodeSrc.value = ret.image
});
}
//
onMounted(()=>{
refreshCode()
})
const checkPassword = (rule, value, callback) => {
@ -131,19 +168,22 @@ export default defineComponent({
const store = useStore();
const loginLoading = ref(false)
const submitForm = () => {
if(loginLoading.value) return;
if (loginLoading.value) return;
//
form.value.validate(async (isValid) => {
if (isValid) {
// loadingtrue
loginLoading.value = true
try {
// action
// actionPromise
const _data = await store.dispatch('login', loginForm)
// token
// localStorage.setItem('token',_data);
ElMessage.success('登录成功')
router.replace('/').then().catch();
} catch (e) {
refreshCode()
ElMessage.error(e.message || '登录错误')
} finally {
loginLoading.value = false
@ -152,7 +192,7 @@ export default defineComponent({
})
};
return {
loginForm, submitForm, rules, form, loginLoading
loginForm, submitForm, rules, form, loginLoading,refreshCode,currentCodeSrc
}
}
})

View File

@ -21,6 +21,11 @@ export default {
mounted() {
this.currentActiveIndex = this.$route.path
},
computed:{
userinfo(){
return this.$store.state.loginUser
}
},
methods: {
//
handleHashChange() {
@ -28,6 +33,9 @@ export default {
title: '成功 加载页面'
})
},
logout(){
this.$router.replace('/login');
}
}
}
</script>
@ -93,8 +101,8 @@ export default {
<el-dropdown>
<div class="el-dropdown-link">
<el-avatar class="avatar" :size="30"
src="https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png"/>
<span class="username">张三</span>
:src="userinfo.avatar"/>
<span class="username">{{ userinfo.nickname }}</span>
<el-icon class="el-icon--right">
<arrow-down/>
</el-icon>
@ -103,7 +111,7 @@ export default {
<el-dropdown-menu>
<el-dropdown-item>修改资料</el-dropdown-item>
<el-dropdown-item>登录日志查看</el-dropdown-item>
<el-dropdown-item>退出</el-dropdown-item>
<el-dropdown-item @click="logout">退出</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>

View File

@ -43,6 +43,11 @@ const routes = [
path: '/reg',
component: () => import('../pages/Reg.vue')
},
{
name: 'Forget',
path: '/forget',
component: () => import('../pages/Forget.vue')
},
{
name: 'Login',
path: '/login',

View File

@ -5,7 +5,13 @@ import {
import router from "../router";
const API_PATH = "http://localhost:8080"
class BizError extends Error{
code = -1;
constructor(message,code = -1) {
super(message);
this.code = code;
}
}
/**
*
* @param api
@ -71,7 +77,7 @@ function request(api, method = 'GET', postData = {}, progressChange = null) {
return;
}
// 其他异常 直接抛出错误
reject(new Error(result.message))
reject(new BizError(result.message,result.code))
}
}
fetch(API_PATH + api, options)
@ -91,6 +97,9 @@ export default {
},
signup(params) {
return request('/api/user/reg', 'POST', params);
},
info(){
return request('/api/user/info');
}
},
folder: {
@ -166,5 +175,8 @@ export default {
create(info) {
return request('/api/share/create', 'POST', info)
}
},
code(){
return request('/create');
}
}

15
web/src/service/data.js Normal file
View File

@ -0,0 +1,15 @@
import {createStore} from 'vuex'
const store = createStore({
state() {
return {
username: '123123'
}
},
mutations: {
changeUsername(state, newName) {
state.username = newName // 更新变量的值
}
}
})
export default store

View File

@ -3,12 +3,16 @@ import api from "./api";
const TOKEN_KEY = 'user_auth_token';
const store = createStore({
state: {
state() {
return {
/**
* 用户信息
* @type {UserInfo}
*/
loginUser: null,
// 所有的state在内存中
token: null
}
},
mutations: {
// 此选项中的方法必须同步
@ -20,7 +24,7 @@ const store = createStore({
// sessionStorage.setItem(TOKEN_KEY,token);
localStorage.setItem(TOKEN_KEY, token);
},
clearToken(state){
clearToken(state) {
state.token = null;
localStorage.removeItem(TOKEN_KEY);
sessionStorage.removeItem(TOKEN_KEY)
@ -45,7 +49,8 @@ const store = createStore({
getters: {
userToken(state) {
const token = state.token || sessionStorage.getItem(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY);
if(!state.token) state.token = token;
// 如果内存中没有token 存储一份
if (!state.token) state.token = token;
return token
}
}

View File

@ -1,3 +1,5 @@
import {dayjs} from "element-plus";
/**
*
* @param {string} type
@ -7,6 +9,17 @@ export function isImage(type) {
return type ? ['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(type.toLowerCase()) : false;
}
export default {
isImage
export function formatDate(time, format = 'MM-DD') {
return dayjs(time).format(format);
}
export function formatSize(a, b = 2) {
if (0 == a) return "0 B";
let c = 1024, d = b || 2, e = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
f = Math.floor(Math.log(a) / Math.log(c));
return parseFloat((a / Math.pow(c, f)).toFixed(d)) + " " + e[f];
}
export default {
isImage, formatSize, formatDate
}