作者 xiaoqiu

首次提交

正在显示 82 个修改的文件 包含 2823 行增加0 行删除
不能预览此文件类型
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
... ...
node_modules
dist
out
.gitignore
... ...
/* 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'
}
}
... ...
node_modules
dist
out
*.log*
package-lock.json
.vscode
.idea
... ...
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
... ...
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json
... ...
singleQuote: true
semi: false
printWidth: 100
trailingComma: none
... ...
MIT License
Copyright (c) 2024 typsusan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
... ...
# 页面标题
VITE_APP_TITLE = 消防维保助手
# 开发环境配置
VITE_APP_ENV = 'development'
# 若依管理系统/开发环境
VITE_APP_BASE_API = '/dev-api'
VITE_APP_TAB_URL_PREFIX = 'https://xfwbzshd.crgx.net'
# VITE_APP_TAB_URL_PREFIX = 'http://bxhd.crgx.net'
\ No newline at end of file
... ...
# 页面标题
VITE_APP_TITLE = 广西车险投保登记平台
# 生产环境配置
VITE_APP_ENV = 'production'
# 若依管理系统/生产环境
VITE_APP_BASE_API = 'https://xfwbzshd.crgx.net'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip
# 开打新的tab的url前缀
VITE_APP_TAB_URL_PREFIX = 'https://xfwbzshd.crgx.net'
\ No newline at end of file
... ...
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>
... ...
不能预览此文件类型
不能预览此文件类型
appId: com.electron.app
productName: signature-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: signature-app
nsis:
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
... ...
import { resolve } from 'path'
import { defineConfig, externalizeDepsPlugin, loadEnv } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
// 导出环境变量
const envDir = resolve('build')
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
export default defineConfig(({mode}) => {
return {
main: {
plugins: [externalizeDepsPlugin()]
},
preload: {
plugins: [externalizeDepsPlugin()]
},
renderer: {
envDir,
envPrefix: 'VITE_',
resolve: {
alias: {
'@': resolve('src/renderer/src')
}
},
plugins: [vue()],
server:{
host: true,
port:5441,
proxy:{
'/dev-api': {
target: 'https://xfwbzshd.crgx.net',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '')
}
}
}
}
}
})
... ...
{
"name": "signature-app",
"version": "1.0.0",
"description": "An Electron application with Vue",
"main": "./out/main/index.js",
"author": "example.com",
"homepage": "https://www.electronjs.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:win": "npm run build && electron-builder --win --config",
"build:mac": "npm run build && electron-builder --mac --config",
"build:linux": "npm run build && electron-builder --linux --config"
},
"dependencies": {
"@electron-toolkit/preload": "^2.0.0",
"@electron-toolkit/utils": "^2.0.0",
"axios": "^1.7.8",
"element-plus": "^2.11.5",
"face-effet": "^1.5.5",
"js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2",
"pinia": "^3.0.1",
"typeface-roboto": "^1.1.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.1",
"@rushstack/eslint-patch": "^1.3.3",
"@vitejs/plugin-vue": "^4.3.1",
"@vue/eslint-config-prettier": "^8.0.0",
"autoprefixer": "^10.4.21",
"electron": "^25.6.0",
"electron-builder": "^24.6.3",
"electron-vite": "^1.0.27",
"eslint": "^8.47.0",
"eslint-plugin-vue": "^9.17.0",
"postcss": "^8.5.6",
"prettier": "^3.0.2",
"sass": "^1.85.1",
"vite": "^4.4.9",
"vue": "^3.3.4"
}
}
... ...
module.exports = {
plugins: {
autoprefixer: {},
},
}
... ...
不能预览此文件类型
import { app, shell, BrowserWindow, screen, ipcMain} from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
function createWindow(width, height) {
// Create the browser window.
const mainWindow = new BrowserWindow({
icon:icon,
width,
height: height,
center: true,
show: false,
transparent:true,
autoHideMenuBar: true,
resizable: false,
titleBarStyle:'hidden',
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
contextIsolation:false,
webSecurity: false,
}
})
// 点击最小化
ipcMain.on('minimizing',(event,args)=>{
event.preventDefault(); // 阻止默认最小化行为
mainWindow.minimize(); // 最小化到任务栏
})
// 点击全屏显示
ipcMain.on('toggle-fullscreen', (event) => {
if (mainWindow.isFullScreen()) {
mainWindow.setFullScreen(false)
} else {
mainWindow.setFullScreen(true)
}
});
// 在主进程中
mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.key === 'Escape' && mainWindow.isFullScreen()) {
mainWindow.setFullScreen(false)
event.preventDefault() // 阻止默认行为
}
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
mainWindow.setFullScreen(true)
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
// 窗口调试 mainWindow.webContents.openDevTools()
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
app.whenReady().then(() => {
// 主进程获取工作区宽高
const primaryDisplay = screen.getPrimaryDisplay()
const { width, height } = primaryDisplay.bounds
electronApp.setAppUserModelId('com.electron')
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
createWindow(width, height)
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow(width, height)
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
... ...
import { contextBridge,ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
const api = {}
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error(error)
}
} else {
window.electron = electronAPI
window.api = api
}
... ...
不能预览此文件类型
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Electron</title>
<!-- Content-Security-Policy -->
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
... ...
不能预览此文件类型
<script setup>
import bar from './components/bar/index.vue'
</script>
<template>
<bar />
<router-view />
</template>
<style>
* {
margin: 0;
padding: 0;
}
div {
box-sizing: border-box;
}
body::-webkit-scrollbar {
display: none; /* Chrome/Safari/Edge */
}
body {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
}
</style>
... ...
import request from '@/utils/request'
// 登录方法
export function login(username, password) {
const data = {
username,
password
}
return request({
url: '/login',
headers: {
isToken: false,
constentType: 'application/json;charset=UTF-8'
},
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 uploadAvatar(data) {
return request({
url: '/system/user/profile/avatar',
method: 'post',
headers: {
'Content-Type': 'multipart/form-data'
},
data: data
})
}
// 退出方法
export function logout() {
return request({
url: '/logout',
method: 'post'
})
}
// 获取验证码
export function getCodeImg() {
return request({
url: '/captchaImage',
headers: {
isToken: false
},
method: 'get',
timeout: 20000
})
}
... ...
import request from '@/utils/request'
// 获取路由
export const getRouters = () => {
return request({
url: '/getRouters',
method: 'get'
})
}
... ...
import request from '@/utils/request'
// 获取签名列表
export const getSignatureList = (query) => {
return request({
url: '/system/sign/list',
method: 'get',
params: query
})
}
// 获取应用背景
export const getUseBg = () => {
return request({
url: '/system/bg/getUsedBg',
method: 'get',
})
}
\ No newline at end of file
... ...
不能预览此文件类型
<svg t="1732849122721" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18087" width="200" height="200"><path d="M240.512 180.181333l271.530667 271.488 271.530666-271.488a42.666667 42.666667 0 0 1 56.32-3.541333l4.010667 3.541333a42.666667 42.666667 0 0 1 0 60.330667l-271.530667 271.530667 271.530667 271.530666a42.666667 42.666667 0 0 1-56.32 63.872l-4.010667-3.541333-271.530666-271.530667-271.530667 271.530667-4.010667 3.541333a42.666667 42.666667 0 0 1-56.32-63.872l271.488-271.530666-271.488-271.530667a42.666667 42.666667 0 0 1 60.330667-60.330667z" fill="#151717" p-id="18088"></path></svg>
... ...
<svg t="1732848995457" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15178" width="200" height="200"><path d="M217.6 320l486.144 486.4H217.6V320z m601.344 358.4L332.8 192h486.144v486.4z" fill="#151717" p-id="15179"></path></svg>
... ...
<svg t="1732780895530" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5154" width="200" height="200"><path d="M919.9 417.7c-22.1-54.8-54.5-103.8-96.4-145.7L579.7 28.1C561.6 10 537.6 0 512 0s-49.6 10-67.7 28.1L200.5 272c-41.9 41.9-74.3 90.9-96.4 145.7C82.8 470.5 72 526.4 72 583.7s10.8 113.2 32.1 166c22.1 54.8 54.5 103.8 96.4 145.7 41.9 41.9 90.9 74.4 145.6 96.5 52.8 21.3 108.6 32.1 165.9 32.1 57.3 0 113.1-10.8 165.9-32.1 54.7-22.1 103.7-54.6 145.6-96.5 41.9-41.9 74.3-90.9 96.4-145.7 21.3-52.8 32.1-108.7 32.1-166s-10.8-113.2-32.1-166z m-55.7 309.5c-19.1 47.3-47.1 89.6-83.2 125.7-36.1 36.2-78.4 64.2-125.6 83.3C609.8 954.7 561.5 964 512 964s-97.8-9.3-143.4-27.8c-47.2-19.1-89.5-47.1-125.6-83.3-36.1-36.2-64.1-78.5-83.2-125.7-18.4-45.7-27.8-94-27.8-143.5s9.3-97.9 27.8-143.5c19.1-47.3 47.1-89.6 83.2-125.7l243.7-244C493.5 63.7 502.5 60 512 60s18.5 3.7 25.3 10.5L781 314.4c36.1 36.2 64.1 78.5 83.2 125.7 18.4 45.7 27.8 94 27.8 143.5s-9.3 98-27.8 143.6z" fill="#151717" p-id="5155"></path><path d="M741.1 514.5c-11.7-11.7-30.8-11.7-42.4 0L581.3 631.9 463.9 514.5c-11.7-11.7-30.8-11.7-42.4 0L282.9 653.1c-11.7 11.7-11.7 30.8 0 42.4 11.7 11.7 30.8 11.7 42.4 0l117.4-117.4 117.4 117.4c11.7 11.7 30.8 11.7 42.4 0l138.6-138.6c11.7-11.7 11.7-30.7 0-42.4z" fill="#151717" p-id="5156"></path></svg>
... ...
<svg t="1732849065701" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17032" width="200" height="200"><path d="M863.7 552.5H160.3c-10.6 0-19.2-8.6-19.2-19.2v-41.7c0-10.6 8.6-19.2 19.2-19.2h703.3c10.6 0 19.2 8.6 19.2 19.2v41.7c0 10.6-8.5 19.2-19.1 19.2z" p-id="17033" fill="#151717"></path></svg>
... ...
body {
font-family: 'Roboto', sans-serif!important;
}
* {
padding: 0;
margin: 0;
}
... ...
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
.title-bar {
position: relative; /* 为定位子元素提供参照 */
height: 45px;
display: flex;
align-items: center;
padding: 0;
-webkit-app-region: drag;
user-select: none;
overflow: hidden; /* 防止内容溢出 */
border-top-left-radius: 20px;
border-top-right-radius: 20px;
border-top: 1px solid #d9d9d9;
border-left: 1px solid #d9d9d9;
border-right: 1px solid #d9d9d9;
}
.title-bar-left,.title-bar-right{
width: 25%;
height: 45px;
float: left;
display: flex;
justify-content: center;
align-items: center;
}
.title-bar-left img{
margin-left: 10px;
}
.title-bar-right{
width: 75%;
float: right;
display: flex;
justify-content: right;
background-color: white;
}
.title-bar .icon {
width: 16px;
height: 16px;
margin-right: 10px;
-webkit-app-region: drag;
user-select: none;
}
.title-bar .title {
flex-grow: 1;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
-webkit-app-region: drag;
color: #151717;
font-size: 13px;
margin-top: -2px;
}
.title-bar .operating-button {
width: 15px;
height: 15px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
-webkit-app-region: no-drag;
transition: background-color 0.2s ease, opacity 0.2s ease;
border-radius: 50%;
background-color: var(--button-bg);
margin-right: 8px;
}
.title-bar .operating-button img {
width: 10px;
height: 10px;
opacity: 0;
transition: opacity 0.2s ease;
}
.title-bar .operating-button:hover img {
opacity: 1;
}
.title-bar .close-button {
--button-bg: #f56057;
}
.title-bar .minimum-button {
--button-bg: #fec428;
}
.title-bar .expand-button {
--button-bg: #1fd42a;
}
... ...
<template>
<div class="title-bar">
<div class="title-bar-left" :style="{ backgroundColor: '#fff' }">
<img src="@/assets/bar/logo.png" alt="图标" class="icon" />
<div class="title">签名墙</div>
</div>
<div class="title-bar-right">
<div class="operating-button close-button" @click="closeWindow">
<img src="~@/assets/bar/close.svg" />
</div>
<div class="operating-button minimum-button" @click="minimumWindow">
<img src="~@/assets/bar/minimum.svg" />
</div>
<div class="operating-button expand-button">
<img src="~@/assets/bar/expand.svg" @click="expandWindow" />
</div>
<div style="width: 10px"></div>
</div>
</div>
</template>
<script setup>
function closeWindow() {
window.close() // 关闭窗口
}
function minimumWindow() {
electron.ipcRenderer.send('minimizing')
}
function expandWindow() {
electron.ipcRenderer.send('toggle-fullscreen')
}
</script>
<style scoped>
@import './index.css';
</style>
... ...
import { createApp } from 'vue';
import App from './App.vue';
import store from './store'
import router from './router';
import ElementPlus, { ElMessage } from 'element-plus';
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import 'element-plus/dist/index.css';
const app = createApp(App);
// 将 ElementPlus 和路由器挂载到应用实例
app.use(ElementPlus, {
locale: zhCn
})
// 全局注册ElementPlus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.config.globalProperties.$message = ElMessage;
app.use(router);
app.use(store);
// 挂载应用
app.mount('#app');
... ...
import useUserStore from '@/store/modules/user'
function authPermission(permission) {
const all_permission = "*:*:*";
const permissions = useUserStore().permissions
if (permission && permission.length > 0) {
return permissions.some(v => {
return all_permission === v || v === permission
})
} else {
return false
}
}
function authRole(role) {
const super_admin = "admin";
const roles = useUserStore().roles
if (role && role.length > 0) {
return roles.some(v => {
return super_admin === v || v === role
})
} else {
return false
}
}
export default {
// 验证用户是否具备某权限
hasPermi(permission) {
return authPermission(permission);
},
// 验证用户是否含有指定权限,只需包含其中一个
hasPermiOr(permissions) {
return permissions.some(item => {
return authPermission(item)
})
},
// 验证用户是否含有指定权限,必须全部拥有
hasPermiAnd(permissions) {
return permissions.every(item => {
return authPermission(item)
})
},
// 验证用户是否具备某角色
hasRole(role) {
return authRole(role);
},
// 验证用户是否含有指定角色,只需包含其中一个
hasRoleOr(roles) {
return roles.some(item => {
return authRole(item)
})
},
// 验证用户是否含有指定角色,必须全部拥有
hasRoleAnd(roles) {
return roles.every(item => {
return authRole(item)
})
}
}
... ...
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
}
... ...
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { saveAs } from 'file-saver'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { blobValidate } from '@/utils/ruoyi'
const baseURL = import.meta.env.VITE_APP_BASE_API
export default {
name(name, isDelete = true) {
var url = baseURL + "/common/download?fileName=" + encodeURIComponent(name) + "&delete=" + isDelete
axios({
method: 'get',
url: url,
responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then(async (res) => {
const isLogin = await blobValidate(res.data);
if (isLogin) {
const blob = new Blob([res.data])
this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
} else {
this.printErrMsg(res.data);
}
})
},
resource(resource) {
var url = baseURL + "/common/download/resource?resource=" + encodeURIComponent(resource);
axios({
method: 'get',
url: url,
responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then(async (res) => {
const isLogin = await blobValidate(res.data);
if (isLogin) {
const blob = new Blob([res.data])
this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
} else {
this.printErrMsg(res.data);
}
})
},
zip(url, name) {
var url = baseURL + url
axios({
method: 'get',
url: url,
responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then(async (res) => {
const isLogin = await blobValidate(res.data);
if (isLogin) {
const blob = new Blob([res.data], { type: 'application/zip' })
this.saveAs(blob, name)
} else {
this.printErrMsg(res.data);
}
})
},
saveAs(text, name, opts) {
saveAs(text, name, opts);
},
async printErrMsg(data) {
const resText = await data.text();
const rspObj = JSON.parse(resText);
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
ElMessage.error(errMsg);
}
}
... ...
import tab from './tab'
import auth from './auth'
import cache from './cache'
import modal from './modal'
import download from './download'
export default function installPlugins(app){
// 页签操作
app.config.globalProperties.$tab = tab
// 认证对象
app.config.globalProperties.$auth = auth
// 缓存对象
app.config.globalProperties.$cache = cache
// 模态框对象
app.config.globalProperties.$modal = modal
// 下载文件
app.config.globalProperties.$download = download
}
... ...
import { ElMessage, ElMessageBox, ElNotification, ElLoading } from 'element-plus'
let loadingInstance;
export default {
// 消息提示
msg(content) {
ElMessage.info(content)
},
// 错误消息
msgError(content) {
ElMessage.error(content)
},
// 成功消息
msgSuccess(content) {
ElMessage.success(content)
},
// 警告消息
msgWarning(content) {
ElMessage.warning(content)
},
// 弹出提示
alert(content) {
ElMessageBox.alert(content, "系统提示")
},
// 错误提示
alertError(content) {
ElMessageBox.alert(content, "系统提示", { type: 'error' })
},
// 成功提示
alertSuccess(content) {
ElMessageBox.alert(content, "系统提示", { type: 'success' })
},
// 警告提示
alertWarning(content) {
ElMessageBox.alert(content, "系统提示", { type: 'warning' })
},
// 通知提示
notify(content) {
ElNotification.info(content)
},
// 错误通知
notifyError(content) {
ElNotification.error(content);
},
// 成功通知
notifySuccess(content) {
ElNotification.success(content)
},
// 警告通知
notifyWarning(content) {
ElNotification.warning(content)
},
// 确认窗体
confirm(content) {
return ElMessageBox.confirm(content, "系统提示", {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: "warning",
})
},
// 提交内容
prompt(content) {
return ElMessageBox.prompt(content, "系统提示", {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: "warning",
})
},
// 打开遮罩层
loading(content) {
loadingInstance = ElLoading.service({
lock: true,
text: content,
background: "rgba(0, 0, 0, 0.7)",
})
},
// 关闭遮罩层
closeLoading() {
loadingInstance.close();
}
}
... ...
import useTagsViewStore from '@/store/modules/tagsView'
import router from '@/router'
export default {
// 刷新当前tab页签
refreshPage(obj) {
const { path, query, matched } = router.currentRoute.value;
if (obj === undefined) {
matched.forEach((m) => {
if (m.components && m.components.default && m.components.default.name) {
if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
obj = { name: m.components.default.name, path: path, query: query };
}
}
});
}
return useTagsViewStore().delCachedView(obj).then(() => {
const { path, query } = obj
router.replace({
path: '/redirect' + path,
query: query
})
})
},
// 关闭当前tab页签,打开新页签
closeOpenPage(obj) {
useTagsViewStore().delView(router.currentRoute.value);
if (obj !== undefined) {
return router.push(obj);
}
},
// 关闭指定tab页签
closePage(obj) {
if (obj === undefined) {
return useTagsViewStore().delView(router.currentRoute.value).then(({ lastPath }) => {
return router.push(lastPath || '/index');
});
}
return useTagsViewStore().delView(obj);
},
// 关闭所有tab页签
closeAllPage() {
return useTagsViewStore().delAllViews();
},
// 关闭左侧tab页签
closeLeftPage(obj) {
return useTagsViewStore().delLeftTags(obj || router.currentRoute.value);
},
// 关闭右侧tab页签
closeRightPage(obj) {
return useTagsViewStore().delRightTags(obj || router.currentRoute.value);
},
// 关闭其他tab页签
closeOtherPage(obj) {
return useTagsViewStore().delOthersViews(obj || router.currentRoute.value);
},
// 打开tab页签
openPage(url) {
return router.push(url);
},
// 修改tab页签
updatePage(obj) {
return useTagsViewStore().updateVisitedView(obj);
}
}
... ...
import { createRouter, createWebHashHistory } from 'vue-router'
// 公共路由
export const constantRoutes = [
{
path: '/',
name: '首页',
component: () => import('@/views/index.vue'),
hidden: true
}
]
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: constantRoutes,
})
export default router
... ...
import { createPinia } from 'pinia'
const store = createPinia()
export default store
... ...
import { defineStore } from 'pinia'
import { login, logout, getInfo } from '@/api/login.js'
import { getToken, setToken, removeToken } from '@/utils/auth'
const useUserStore = defineStore('user', {
state: () => ({
token: getToken(),
name: '',
avatar: '',
roles: [],
permissions: []
}),
actions: {
// 登录
login(userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
return new Promise((resolve, reject) => {
login(username, password)
.then((res) => {
setToken(res.token)
this.token = res.token
resolve()
})
.catch((error) => {
reject(error)
})
})
},
// 获取用户信息
getInfo() {
return new Promise((resolve, reject) => {
getInfo()
.then((res) => {
const user = res.user
const avatar =
user.avatar == '' || user.avatar == null
? ''
: import.meta.env.VITE_APP_BASE_API + user.avatar
if (res.roles && res.roles.length > 0) {
// 验证返回的roles是否是一个非空数组
this.roles = res.roles
this.permissions = res.permissions
} else {
this.roles = ['ROLE_DEFAULT']
}
this.name = user.userName
this.avatar = avatar
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) => {
reject(error)
})
})
}
}
})
export default useUserStore
... ...
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)
}
... ...
// 在渲染进程中发送base64图片
import { uploadAvatar } from '@/api/login';
// 辅助函数:Base64转Blob
function base64ToBlob(base64Data, contentType = 'image/png') {
// 分割Base64字符串获取实际数据部分
const byteString = atob(base64Data.split(',')[1]);
// 将字符串转换为字节数组
const arrayBuffer = new ArrayBuffer(byteString.length);
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
uint8Array[i] = byteString.charCodeAt(i);
}
// 创建Blob对象
return new Blob([arrayBuffer], { type: contentType });
}
export async function uploadImage(base64Data) {
// 1. 将Base64字符串转换为Blob对象
const blob = base64ToBlob(base64Data);
console.log(blob)
// 2. 创建FormData对象
const formData = new FormData();
formData.append('avatarfile', blob); // 'image'是后端要求的字段名
console.log(formData)
// 3. 添加其他表单数据(可选)
// formData.append('userId', '12345');
// formData.append('type', 'avatar');
try {
// await fetch('https://xfwbzshd.crgx.net/system/user/profile/avatar', {
// method: 'POST',
// headers: {
// // 如果需要,可以添加其他HTTP头,如Authorization等
// Authorization: 'Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjcxOWM1ZjE3LTNjMjYtNGFhNi04NWUwLThlOGZjZDBkYTAxMSJ9._gpSVz_UbRGTH0O0vMDlJEQ4uepf619ELGXgsdA1Ul0puNiWicQuddfwv-4c--qdaqN5hxSnKlJADNv7gfR64g',
// },
// body: formData,
// // 注意:不要手动设置Content-Type!FormData会自动设置正确的multipart/form-data和boundary
// });
await uploadAvatar(formData);
console.log('上传成功:');
} catch (error) {
console.error('上传错误:', error);
throw error;
}
}
\ No newline at end of file
... ...
function objectToFormData(obj) {
const fd = new FormData();
Object.keys(obj).forEach(key => {
const value = obj[key];
fd.set(key, value);
});
return fd;
}
function objectToQueryStr(obj, filterNull = false) {
let queryStr = "";
Object.keys(obj).forEach(key => {
if (filterNull && !obj[key]) {
return;
}
queryStr += `&${key}=${obj[key] || ''}`
});
console.log(queryStr);
return queryStr.slice(1);
}
function openWindow(path) {
const prefix = import.meta.env.VITE_APP_TAB_URL_PREFIX;
window.open(`${prefix}${path}`);
}
// 运行实例
function listToTree(list) {
const nodeMap = {};
const firstLevelList = list.filter(item => item.parentExecutionId === "0");
firstLevelList.forEach(item => {
const {executionId} = item;
nodeMap[executionId] = item;
});
while (true) {
list.forEach(item => {
const {parentExecutionId, executionId} = item;
if (nodeMap[executionId]) return;
if (nodeMap[parentExecutionId]) {
const parent = nodeMap[parentExecutionId];
if (parent.children) {
parent.children.push(item);
} else {
parent.children = [item];
}
nodeMap[executionId] = item;
}
});
if (Object.keys(nodeMap).length === list.length) {
return firstLevelList;
}
}
}
function normalizeDateTimeString(rawDatatimeStr) {
function genNumStr(num) {
return Number(num) < 10 ? "0" + num : ("" + num)
}
if (rawDatatimeStr) {
const dateObj = new Date(rawDatatimeStr);
const yyyy = dateObj.getFullYear();
const MM = dateObj.getMonth() + 1;
const dd = dateObj.getDate();
const HH = dateObj.getHours();
const mm = dateObj.getMinutes();
const ss = dateObj.getSeconds();
return `${yyyy}-${genNumStr(MM)}-${genNumStr(dd)} ${genNumStr(HH)}:${genNumStr(mm)}:${genNumStr(ss)}`;
}
}
export default {
objectToFormData,
objectToQueryStr,
openWindow,
listToTree,
normalizeDateTimeString
};
\ No newline at end of file
... ...
import useDictStore from '@/store/modules/dict'
import { getDicts } from '@/api/system/dict/data'
/**
* 获取字典数据
*/
export function useDict(...args) {
const res = ref({});
return (() => {
args.forEach((dictType, index) => {
res.value[dictType] = [];
const dicts = useDictStore().getDict(dictType);
if (dicts) {
res.value[dictType] = dicts;
} else {
getDicts(dictType).then(resp => {
res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
useDictStore().setDict(dictType, res.value[dictType]);
})
}
})
return toRefs(res.value);
})()
}
\ No newline at end of file
... ...
import store from '@/store'
import defaultSettings from '@/settings'
import useSettingsStore from '@/store/modules/settings'
/**
* 动态修改标题
*/
export function useDynamicTitle() {
const settingsStore = useSettingsStore();
if (settingsStore.dynamicTitle) {
document.title = settingsStore.title + ' - ' + defaultSettings.title;
} else {
document.title = defaultSettings.title;
}
}
\ No newline at end of file
... ...
export default {
'401': '认证失败,无法访问系统资源',
'403': '当前操作没有权限',
'404': '访问资源不存在',
default: '系统未知错误,请反馈给管理员'
}
... ...
import { parseTime } from './ruoyi'
/**
* 表格时间格式化
*/
export function formatDate(cellValue) {
if (cellValue == null || cellValue == "") return "";
var date = new Date(cellValue)
var year = date.getFullYear()
var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1
var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()
var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours()
var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds
}
/**
* @param {number} time
* @param {string} option
* @returns {string}
*/
export function formatTime(time, option) {
if (('' + time).length === 10) {
time = parseInt(time) * 1000
} else {
time = +time
}
const d = new Date(time)
const now = Date.now()
const diff = (now - d) / 1000
if (diff < 30) {
return '刚刚'
} else if (diff < 3600) {
// less 1 hour
return Math.ceil(diff / 60) + '分钟前'
} else if (diff < 3600 * 24) {
return Math.ceil(diff / 3600) + '小时前'
} else if (diff < 3600 * 24 * 2) {
return '1天前'
}
if (option) {
return parseTime(time, option)
} else {
return (
d.getMonth() +
1 +
'月' +
d.getDate() +
'日' +
d.getHours() +
'时' +
d.getMinutes() +
'分'
)
}
}
/**
* @param {string} url
* @returns {Object}
*/
export function getQueryObject(url) {
url = url == null ? window.location.href : url
const search = url.substring(url.lastIndexOf('?') + 1)
const obj = {}
const reg = /([^?&=]+)=([^?&=]*)/g
search.replace(reg, (rs, $1, $2) => {
const name = decodeURIComponent($1)
let val = decodeURIComponent($2)
val = String(val)
obj[name] = val
return rs
})
return obj
}
/**
* @param {string} input value
* @returns {number} output value
*/
export function byteLength(str) {
// returns the byte length of an utf8 string
let s = str.length
for (var i = str.length - 1; i >= 0; i--) {
const code = str.charCodeAt(i)
if (code > 0x7f && code <= 0x7ff) s++
else if (code > 0x7ff && code <= 0xffff) s += 2
if (code >= 0xDC00 && code <= 0xDFFF) i--
}
return s
}
/**
* @param {Array} actual
* @returns {Array}
*/
export function cleanArray(actual) {
const newArray = []
for (let i = 0; i < actual.length; i++) {
if (actual[i]) {
newArray.push(actual[i])
}
}
return newArray
}
/**
* @param {Object} json
* @returns {Array}
*/
export function param(json) {
if (!json) return ''
return cleanArray(
Object.keys(json).map(key => {
if (json[key] === undefined) return ''
return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])
})
).join('&')
}
/**
* @param {string} url
* @returns {Object}
*/
export function param2Obj(url) {
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
if (!search) {
return {}
}
const obj = {}
const searchArr = search.split('&')
searchArr.forEach(v => {
const index = v.indexOf('=')
if (index !== -1) {
const name = v.substring(0, index)
const val = v.substring(index + 1, v.length)
obj[name] = val
}
})
return obj
}
/**
* @param {string} val
* @returns {string}
*/
export function html2Text(val) {
const div = document.createElement('div')
div.innerHTML = val
return div.textContent || div.innerText
}
/**
* Merges two objects, giving the last one precedence
* @param {Object} target
* @param {(Object|Array)} source
* @returns {Object}
*/
export function objectMerge(target, source) {
if (typeof target !== 'object') {
target = {}
}
if (Array.isArray(source)) {
return source.slice()
}
Object.keys(source).forEach(property => {
const sourceProperty = source[property]
if (typeof sourceProperty === 'object') {
target[property] = objectMerge(target[property], sourceProperty)
} else {
target[property] = sourceProperty
}
})
return target
}
/**
* @param {HTMLElement} element
* @param {string} className
*/
export function toggleClass(element, className) {
if (!element || !className) {
return
}
let classString = element.className
const nameIndex = classString.indexOf(className)
if (nameIndex === -1) {
classString += '' + className
} else {
classString =
classString.substr(0, nameIndex) +
classString.substr(nameIndex + className.length)
}
element.className = classString
}
/**
* @param {string} type
* @returns {Date}
*/
export function getTime(type) {
if (type === 'start') {
return new Date().getTime() - 3600 * 1000 * 24 * 90
} else {
return new Date(new Date().toDateString())
}
}
/**
* @param {Function} func
* @param {number} wait
* @param {boolean} immediate
* @return {*}
*/
export function debounce(func, wait, immediate) {
let timeout, args, context, timestamp, result
const later = function() {
// 据上一次触发时间间隔
const last = +new Date() - timestamp
// 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last)
} else {
timeout = null
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
}
}
return function(...args) {
context = this
timestamp = +new Date()
const callNow = immediate && !timeout
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait)
if (callNow) {
result = func.apply(context, args)
context = args = null
}
return result
}
}
/**
* This is just a simple version of deep copy
* Has a lot of edge cases bug
* If you want to use a perfect deep copy, use lodash's _.cloneDeep
* @param {Object} source
* @returns {Object}
*/
export function deepClone(source) {
if (!source && typeof source !== 'object') {
throw new Error('error arguments', 'deepClone')
}
const targetObj = source.constructor === Array ? [] : {}
Object.keys(source).forEach(keys => {
if (source[keys] && typeof source[keys] === 'object') {
targetObj[keys] = deepClone(source[keys])
} else {
targetObj[keys] = source[keys]
}
})
return targetObj
}
/**
* @param {Array} arr
* @returns {Array}
*/
export function uniqueArr(arr) {
return Array.from(new Set(arr))
}
/**
* @returns {string}
*/
export function createUniqueString() {
const timestamp = +new Date() + ''
const randomNum = parseInt((1 + Math.random()) * 65536) + ''
return (+(randomNum + timestamp)).toString(32)
}
/**
* Check if an element has a class
* @param {HTMLElement} elm
* @param {string} cls
* @returns {boolean}
*/
export function hasClass(ele, cls) {
return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
}
/**
* Add class to element
* @param {HTMLElement} elm
* @param {string} cls
*/
export function addClass(ele, cls) {
if (!hasClass(ele, cls)) ele.className += ' ' + cls
}
/**
* Remove class from element
* @param {HTMLElement} elm
* @param {string} cls
*/
export function removeClass(ele, cls) {
if (hasClass(ele, cls)) {
const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
ele.className = ele.className.replace(reg, ' ')
}
}
export function makeMap(str, expectsLowerCase) {
const map = Object.create(null)
const list = str.split(',')
for (let i = 0; i < list.length; i++) {
map[list[i]] = true
}
return expectsLowerCase
? val => map[val.toLowerCase()]
: val => map[val]
}
export const exportDefault = 'export default '
export const beautifierConf = {
html: {
indent_size: '2',
indent_char: ' ',
max_preserve_newlines: '-1',
preserve_newlines: false,
keep_array_indentation: false,
break_chained_methods: false,
indent_scripts: 'separate',
brace_style: 'end-expand',
space_before_conditional: true,
unescape_strings: false,
jslint_happy: false,
end_with_newline: true,
wrap_line_length: '110',
indent_inner_html: true,
comma_first: false,
e4x: true,
indent_empty_lines: true
},
js: {
indent_size: '2',
indent_char: ' ',
max_preserve_newlines: '-1',
preserve_newlines: false,
keep_array_indentation: false,
break_chained_methods: false,
indent_scripts: 'normal',
brace_style: 'end-expand',
space_before_conditional: true,
unescape_strings: false,
jslint_happy: true,
end_with_newline: true,
wrap_line_length: '110',
indent_inner_html: true,
comma_first: false,
e4x: true,
indent_empty_lines: true
}
}
// 首字母大小
export function titleCase(str) {
return str.replace(/( |^)[a-z]/g, L => L.toUpperCase())
}
// 下划转驼峰
export function camelCase(str) {
return str.replace(/_[a-z]/g, str1 => str1.substr(-1).toUpperCase())
}
export function isNumberStr(str) {
return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str)
}
... ...
//计算两个时间之间的时间差 多少天时分秒
export function intervalTime(timestamp1, timestamp2) {
// 将时间戳转换为毫秒
const diffInMilliseconds = Math.abs(timestamp2 - timestamp1);
// 1 小时等于 3600000 毫秒
return (diffInMilliseconds / 3600000).toFixed(2) + '时';
}
\ No newline at end of file
... ...
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
// 密钥对生成 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) // 对数据进行解密
}
... ...
import useUserStore from '@/store/modules/user'
/**
* 字符权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
export function checkPermi(value) {
if (value && value instanceof Array && value.length > 0) {
const permissions = useUserStore().permissions
const permissionDatas = value
const all_permission = "*:*:*";
const hasPermission = permissions.some(permission => {
return all_permission === permission || permissionDatas.includes(permission)
})
if (!hasPermission) {
return false
}
return true
} else {
console.error(`need roles! Like checkPermi="['system:user:add','system:user:edit']"`)
return false
}
}
/**
* 角色权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
export function checkRole(value) {
if (value && value instanceof Array && value.length > 0) {
const roles = useUserStore().roles
const permissionRoles = value
const super_admin = "admin";
const hasRole = roles.some(role => {
return super_admin === role || permissionRoles.includes(role)
})
if (!hasRole) {
return false
}
return true
} else {
console.error(`need roles! Like checkRole="['admin','editor']"`)
return false
}
}
\ No newline at end of file
... ...
import axios from 'axios'
import { ElNotification, ElMessageBox, ElMessage } from 'element-plus'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams } from '@/utils/ruoyi'
import cache from '@/plugins/cache'
import useUserStore from '@/store/modules/user'
// 是否显示重新登录
export let isRelogin = { show: false }
const server_base = localStorage.getItem('crgx_server')
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
baseURL: server_base,
// 超时
timeout: 10000
})
// 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 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(() => {
console.log('重新登录')
location.href = '/login'
})
})
.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 function download(url, params, filename, config) {
// downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
// return service.post(url, params, {
// transformRequest: [(params) => { return tansParams(params) }],
// headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
// responseType: 'blob',
// ...config
// }).then(async (data) => {
// const isLogin = await blobValidate(data);
// if (isLogin) {
// const blob = new Blob([data])
// saveAs(blob, filename)
// } else {
// const resText = await data.text();
// const rspObj = JSON.parse(resText);
// const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
// ElMessage.error(errMsg);
// }
// downloadLoadingInstance.close();
// }).catch((r) => {
// console.error(r)
// ElMessage.error('下载文件出现错误,请联系管理员!')
// downloadLoadingInstance.close();
// })
// }
export default service
... ...
/**
* 通用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 (value !== null && value !== '' && typeof value !== 'undefined') {
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) + '&'
}
}
}
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 async function blobValidate(data) {
try {
const text = await data.text()
JSON.parse(text)
return false
} catch (error) {
return true
}
}
... ...
Math.easeInOutQuad = function(t, b, c, d) {
t /= d / 2
if (t < 1) {
return c / 2 * t * t + b
}
t--
return -c / 2 * (t * (t - 2) - 1) + b
}
// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
var requestAnimFrame = (function() {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
})()
/**
* Because it's so fucking difficult to detect the scrolling element, just move them all
* @param {number} amount
*/
function move(amount) {
document.documentElement.scrollTop = amount
document.body.parentNode.scrollTop = amount
document.body.scrollTop = amount
}
function position() {
return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
}
/**
* @param {number} to
* @param {number} duration
* @param {Function} callback
*/
export function scrollTo(to, duration, callback) {
const start = position()
const change = to - start
const increment = 20
let currentTime = 0
duration = (typeof (duration) === 'undefined') ? 500 : duration
var animateScroll = function() {
// increment the time
currentTime += increment
// find the value with the quadratic in-out easing function
var val = Math.easeInOutQuad(currentTime, start, change, duration)
// move the document.body
move(val)
// do the animation unless its over
if (currentTime < duration) {
requestAnimFrame(animateScroll)
} else {
if (callback && typeof (callback) === 'function') {
// the animation is done so lets callback
callback()
}
}
}
animateScroll()
}
... ...
// 处理主题样式
export function handleThemeStyle(theme) {
document.documentElement.style.setProperty('--el-color-primary', theme)
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(theme, i / 10)}`)
}
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(theme, i / 10)}`)
}
}
// hex颜色转rgb颜色
export function hexToRgb(str) {
str = str.replace('#', '')
let hexs = str.match(/../g)
for (let i = 0; i < 3; i++) {
hexs[i] = parseInt(hexs[i], 16)
}
return hexs
}
// rgb颜色转Hex颜色
export function rgbToHex(r, g, b) {
let hexs = [r.toString(16), g.toString(16), b.toString(16)]
for (let i = 0; i < 3; i++) {
if (hexs[i].length == 1) {
hexs[i] = `0${hexs[i]}`
}
}
return `#${hexs.join('')}`
}
// 变浅颜色值
export function getLightColor(color, level) {
let rgb = hexToRgb(color)
for (let i = 0; i < 3; i++) {
rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i])
}
return rgbToHex(rgb[0], rgb[1], rgb[2])
}
// 变深颜色值
export function getDarkColor(color, level) {
let rgb = hexToRgb(color)
for (let i = 0; i < 3; i++) {
rgb[i] = Math.floor(rgb[i] * (1 - level))
}
return rgbToHex(rgb[0], rgb[1], rgb[2])
}
... ...
/**
* 判断url是否是http或https
* @param {string} path
* @returns {Boolean}
*/
export function isHttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
}
/**
* 判断path是否为外链
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ['admin', 'editor']
return valid_map.indexOf(str.trim()) >= 0
}
/**
* @param {string} url
* @returns {Boolean}
*/
export function validURL(url) {
const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
return reg.test(url)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validLowerCase(str) {
const reg = /^[a-z]+$/
return reg.test(str)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUpperCase(str) {
const reg = /^[A-Z]+$/
return reg.test(str)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validAlphabets(str) {
const reg = /^[A-Za-z]+$/
return reg.test(str)
}
/**
* @param {string} email
* @returns {Boolean}
*/
export function validEmail(email) {
const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
return reg.test(email)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function isString(str) {
if (typeof str === 'string' || str instanceof String) {
return true
}
return false
}
/**
* @param {Array} arg
* @returns {Boolean}
*/
export function isArray(arg) {
if (typeof Array.isArray === 'undefined') {
return Object.prototype.toString.call(arg) === '[object Array]'
}
return Array.isArray(arg)
}
... ...
不能预览此文件类型
<template>
<div
class="photo-container"
:style="{ backgroundImage: `url(${bgImgUrl})`, backgroundSize: 'cover' }"
@click="handleOther"
>
<!-- 相册展开大小 容器 -->
<div id="dragBox">
<div
v-for="(item, index) in spanBoxList"
:key="index"
class="spinBox"
:id="`spinBox${index + 1}`"
>
<img
v-for="(childen, number) in item"
:src="childen"
@click.stop="handleClick($event, childen)"
/>
</div>
<div v-show="closeImg" class="fixed-img" :style="{ opacity: fixedOpacity }">
<img :src="fixedImg" />
</div>
</div>
</div>
<!-- 翻页按钮 -->
<button
v-show="queryParams.pageNum > 1"
type="button"
class="slick-prev slick-arrow slick-prev-button"
@click="PreviousPage"
>
Previous
</button>
<button type="button" class="slick-next slick-arrow slick-next-button" @click="NextPage">
Next
</button>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getSignatureList, getUseBg } from '@/api/signature'
let imgList = import.meta.glob('../assets/img/*.*', { eager: true })
let imgArr = Object.values(imgList).map((item) => item.default)
let spanBoxList = ref([])
let aEls = ref([])
let radius = ref(760)
let bgImgUrl = ref('')
const closeImg = ref(false) // 是否关闭中间图片
let outDom = ref(null)
let startX = ref(0)
let startY = ref(0)
let endX = ref(0)
let endY = ref(0)
let tX = ref(0)
let tY = ref(30)
let desX = ref(0)
let desY = ref(0)
const queryParams = ref({
pageNum: 1,
pageSize: 60
})
const server_base = localStorage.getItem('crgx_server')
const fixedImg = ref('')
const fixedOpacity = ref(0)
function randomGroupWithRepeats(arr, groupCount, groupSize) {
// 参数校验
if (!Array.isArray(arr) || arr.length === 0) {
throw new Error('输入必须是非空数组')
}
if (typeof groupCount !== 'number' || groupCount <= 0) {
throw new Error('分组数必须是正整数')
}
if (typeof groupSize !== 'number' || groupSize <= 0) {
throw new Error('每组大小必须是正整数')
}
const result = []
for (let i = 0; i < groupCount; i++) {
const group = []
for (let j = 0; j < groupSize; j++) {
// 随机选择元素(允许重复)
const randomIndex = Math.floor(Math.random() * arr.length)
group.push(arr[randomIndex])
}
result.push(group)
}
return result
}
spanBoxList.value = randomGroupWithRepeats(imgArr, 3, 20)
// 设置样式
function setStyle(delayTime, dom, i, len) {
//给元素加动画 展开
dom.style.transform = 'rotateY(' + i * (360 / len) + 'deg) translateZ(' + radius.value + 'px)'
dom.style.transition = 'all 1s'
dom.style.opacity = 1
dom.style.transitionDelay = delayTime || (len - i) / 4 + 's'
}
// 初始化每一层图片动画
function init(delayTime) {
closeImg.value = false
for (let i = 0; i < aEls.value.length; i++) {
for (let j = 0; j < aEls.value[i].length; j++) {
setStyle(delayTime, aEls.value[i][j], j, aEls.value[i].length)
}
}
}
// 获取图片
function getSpinBoxDom() {
for (let i = 0; i < spanBoxList.value.length; i++) {
let spinDom = document.getElementById(`spinBox${i + 1}`)
let aImg = spinDom.getElementsByTagName('img')
aEls.value.push(aImg)
}
}
// 改变整体的旋转角度
function changeRotate(obj) {
// X轴旋转0-180度
if (tY.value > 180) tY.value = 180
if (tY.value < 0) tY.value = 0
// y轴旋转角度不限制
obj.style.transform = 'rotateX(' + -tY.value + 'deg) rotateY(' + tX.value + 'deg)'
}
// 点击图片展示
const handleClick = (event, childen) => {
init(1)
fixedImg.value = childen
event.target.style.transform = 'rotateY(0deg) translateZ(0px) scale(3)'
event.target.style.opacity = 0
fixedOpacity.value = 1
closeImg.value = true
}
// 点击空白处关闭中间图片并恢复初始
const handleOther = () => {
if (closeImg.value) {
radius.value = 760
init(1)
}
}
// 获取图片列表
const getImgList = async () => {
const signRes = await getSignatureList(queryParams.value)
const imgList = signRes.rows?.map((item) => {
return `${server_base}${item.path}`
})
spanBoxList.value = randomGroupWithRepeats(imgList, 3, 20)
}
// 上一页
function PreviousPage() {
queryParams.value.pageNum--
getImgList()
}
// 下一页
function NextPage() {
queryParams.value.pageNum++
getImgList()
}
// 鼠标滚动函数
const handleMouseWheel = (e) => {
e || e.window.event
let d = e.wheelDelta / 20 || -e.detail
radius.value += d // 旋转半径
init(1)
}
// 是否打开填写服务器地址
const openServer = () => {
const server = localStorage.getItem('crgx_server')
ElMessageBox.prompt('请填写服务器地址以便正常使用', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
inputValue: server,
inputPattern: /^http/i,
inputErrorMessage: '请正常填写地址'
})
.then(({ value }) => {
localStorage.setItem('crgx_server', value)
ElMessage({
type: 'success',
message: `填写完成,即将刷新页面`
})
window.location.reload()
})
.catch(() => {})
}
onMounted(async () => {
if (!localStorage.getItem('crgx_server')) {
return openServer(true)
}
const signRes = await getSignatureList()
const bgRes = await getUseBg()
bgImgUrl.value = server_base + bgRes.data.path
const imgList = signRes.rows?.map((item) => {
return `${server_base}${item.path}`
})
spanBoxList.value = randomGroupWithRepeats(imgList, 3, 20)
let spinDom1 = document.getElementById('spinBox1')
getSpinBoxDom()
//相册容器
outDom.value = document.getElementById('dragBox')
// 开始旋转动画
setTimeout(() => {
init()
}, 100)
//鼠标滚动事件
document.addEventListener('mousewheel', handleMouseWheel)
//暂停开始旋转
function playSpin(yes) {
spinDom1.style.animationPlayState = yes ? 'running' : 'paused'
}
//鼠标移动事件
document.onpointerdown = function (e) {
//清除惯性定时器
clearInterval(outDom.value.timer)
e = e || ewindow.event
//鼠标点击位置
;(startX.value = e.clientX), (startY.value = e.clientY)
this.onpointermove = function (e) {
playSpin(false)
//鼠标点击时 停止自动旋转//鼠标点击时 停止自动旋转
e = e || window.event
//记录结束时位置
;(endX.value = e.clientX), (endY.value = e.clientY)
//计算移动距离 并修改角度
desX.value = endX.value - startX.value
desY.value = endY.value - startY.value
tX.value += desX.value * 0.1
tY.value += desY.value * 0.1
changeRotate(outDom.value)
startX.value = endX.value
startY.value = endY.value
}
//鼠标离开时 开始自动旋转
this.onpointerup = function (e) {
//惯性旋转
outDom.value.timer = setInterval(function () {
desX.value *= 0.95
desY.value *= 0.95
tX.value += desX.value * 0.1
tY.value += desY.value * 0.1
changeRotate(outDom.value)
playSpin(false)
if (Math.abs(desX.value) < 0.5 && Math.abs(desY.value) < 0.5) {
clearInterval(outDom.value.timer)
playSpin(true)
}
})
this.onpointermove = this.onpointerup = null
}
return false
}
})
onUnmounted(() => {
document.removeEventListener('mousewheel', handleMouseWheel)
})
</script>
<style scoped lang="scss">
@use 'sass:math'; // 引入 math 模块
.photo-container {
min-height: 100vh;
touch-action: none;
overflow: hidden;
display: flex;
perspective: 1500px;
background: #111;
}
* {
margin: 0;
padding: 0;
}
/* perspective指定了观察者与 Z=9 平面的距离,使具有三维位置变换的元素产生透视效果。 */
@property --d {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
.moveAni {
transform: rotateY(0deg) translateZ(0px) !important;
}
#dragBox,
.spinBox {
position: relative;
display: flex;
margin: auto;
transform-style: preserve-3d;
transform: rotateX(-10deg);
}
.fixed-img {
position: fixed;
top: 0;
background-color: #fff;
width: 180px;
height: 240px;
opacity: 0;
transform: scale(3) rotateY(180deg);
transition: all 0.5s ease-in-out;
img {
transform: rotateY(180deg);
}
}
#dragBox {
transform: rotateX(-30deg);
}
.spinBox {
width: 180px;
height: 240px;
animation: spin 100s infinite linear;
}
@for $i from 2 through 7 {
$top-value: if($i % 2 == 0, 120% * math.div($i, 2), -120% * math.div($i - 1, 2));
#spinBox#{$i} {
position: absolute;
top: $top-value;
}
}
#dragBox img {
transform-style: preserve-3d;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #fff;
box-shadow: 0 0 8px #fff;
object-fit: contain;
/*倒影 */
--webkit-box-reflect: below 10px linear-gradient(transparent, transparent, #0005);
}
#dragBox img:hover {
box-shadow: 0 0 15px #fff;
}
/*自动旋转 */
@keyframes spin {
from {
transform: rotateY(0deg);
}
to {
transform: rotateY(360deg);
}
}
.slick-prev-button,
.slick-next-button {
position: absolute;
top: 50%;
bottom: auto;
z-index: 20;
margin-top: -2rem;
height: 4rem;
width: 4rem;
cursor: pointer;
border-radius: 50%;
border-style: none;
background-color: rgb(0 0 23 / 0.1);
padding: 0px;
font-size: 0;
color: transparent;
outline: 2px solid transparent;
outline-offset: 2px;
}
.slick-prev-button {
left: 2.5rem;
}
.slick-next-button {
right: 2.5rem;
}
.slick-prev:before {
content: '';
width: 100%;
height: 100%;
background: url('https://www.guet.edu.cn/_upload/tpl/01/39/313/template313/images/leftarrow.png')
center no-repeat;
}
.slick-next:before {
content: '';
width: 100%;
height: 100%;
background: url('https://www.guet.edu.cn/_upload/tpl/01/39/313/template313/images/rightarrow.png')
center no-repeat;
}
.slick-arrow:before {
display: block;
opacity: 0.3;
transition: 0.4s;
}
.slick-arrow:hover::before {
opacity: 1;
}
</style>
... ...