作者 xiaoqiu

首次提交

正在显示 82 个修改的文件 包含 2823 行增加0 行删除
不能预览此文件类型
  1 +root = true
  2 +
  3 +[*]
  4 +charset = utf-8
  5 +indent_style = space
  6 +indent_size = 2
  7 +end_of_line = lf
  8 +insert_final_newline = true
  9 +trim_trailing_whitespace = true
  1 +node_modules
  2 +dist
  3 +out
  4 +.gitignore
  1 +/* eslint-env node */
  2 +require('@rushstack/eslint-patch/modern-module-resolution')
  3 +
  4 +module.exports = {
  5 + extends: [
  6 + 'eslint:recommended',
  7 + 'plugin:vue/vue3-recommended',
  8 + '@electron-toolkit',
  9 + '@vue/eslint-config-prettier'
  10 + ],
  11 + rules: {
  12 + 'vue/require-default-prop': 'off',
  13 + 'vue/multi-word-component-names': 'off'
  14 + }
  15 +}
  1 +node_modules
  2 +dist
  3 +out
  4 +*.log*
  5 +
  6 +package-lock.json
  7 +
  8 +.vscode
  9 +
  10 +.idea
  1 +ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
  1 +out
  2 +dist
  3 +pnpm-lock.yaml
  4 +LICENSE.md
  5 +tsconfig.json
  6 +tsconfig.*.json
  1 +singleQuote: true
  2 +semi: false
  3 +printWidth: 100
  4 +trailingComma: none
  1 +MIT License
  2 +
  3 +Copyright (c) 2024 typsusan
  4 +
  5 +Permission is hereby granted, free of charge, to any person obtaining a copy
  6 +of this software and associated documentation files (the "Software"), to deal
  7 +in the Software without restriction, including without limitation the rights
  8 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9 +copies of the Software, and to permit persons to whom the Software is
  10 +furnished to do so, subject to the following conditions:
  11 +
  12 +The above copyright notice and this permission notice shall be included in all
  13 +copies or substantial portions of the Software.
  14 +
  15 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21 +SOFTWARE.
  1 +# 页面标题
  2 +VITE_APP_TITLE = 消防维保助手
  3 +
  4 +# 开发环境配置
  5 +VITE_APP_ENV = 'development'
  6 +
  7 +# 若依管理系统/开发环境
  8 +VITE_APP_BASE_API = '/dev-api'
  9 +
  10 +VITE_APP_TAB_URL_PREFIX = 'https://xfwbzshd.crgx.net'
  11 +
  12 +# VITE_APP_TAB_URL_PREFIX = 'http://bxhd.crgx.net'
  1 +# 页面标题
  2 +VITE_APP_TITLE = 广西车险投保登记平台
  3 +
  4 +# 生产环境配置
  5 +VITE_APP_ENV = 'production'
  6 +
  7 +# 若依管理系统/生产环境
  8 +VITE_APP_BASE_API = 'https://xfwbzshd.crgx.net'
  9 +
  10 +# 是否在打包时开启压缩,支持 gzip 和 brotli
  11 +VITE_BUILD_COMPRESS = gzip
  12 +
  13 +# 开打新的tab的url前缀
  14 +VITE_APP_TAB_URL_PREFIX = 'https://xfwbzshd.crgx.net'
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
  3 +<plist version="1.0">
  4 + <dict>
  5 + <key>com.apple.security.cs.allow-jit</key>
  6 + <true/>
  7 + <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
  8 + <true/>
  9 + <key>com.apple.security.cs.allow-dyld-environment-variables</key>
  10 + <true/>
  11 + </dict>
  12 +</plist>
不能预览此文件类型
不能预览此文件类型
  1 +appId: com.electron.app
  2 +productName: signature-app
  3 +directories:
  4 + buildResources: build
  5 +files:
  6 + - '!**/.vscode/*'
  7 + - '!src/*'
  8 + - '!electron.vite.config.{js,ts,mjs,cjs}'
  9 + - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
  10 + - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
  11 +asarUnpack:
  12 + - resources/**
  13 +win:
  14 + executableName: signature-app
  15 +nsis:
  16 + artifactName: ${name}-${version}-setup.${ext}
  17 + shortcutName: ${productName}
  18 + uninstallDisplayName: ${productName}
  19 + createDesktopShortcut: always
  20 +mac:
  21 + entitlementsInherit: build/entitlements.mac.plist
  22 + extendInfo:
  23 + - NSCameraUsageDescription: Application requests access to the device's camera.
  24 + - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
  25 + - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
  26 + - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
  27 + notarize: false
  28 +dmg:
  29 + artifactName: ${name}-${version}.${ext}
  30 +linux:
  31 + target:
  32 + - AppImage
  33 + - snap
  34 + - deb
  35 + maintainer: electronjs.org
  36 + category: Utility
  37 +appImage:
  38 + artifactName: ${name}-${version}.${ext}
  39 +npmRebuild: false
  40 +publish:
  41 + provider: generic
  42 + url: https://example.com/auto-updates
  1 +import { resolve } from 'path'
  2 +import { defineConfig, externalizeDepsPlugin, loadEnv } from 'electron-vite'
  3 +import vue from '@vitejs/plugin-vue'
  4 +
  5 +// 导出环境变量
  6 +const envDir = resolve('build')
  7 +process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
  8 +export default defineConfig(({mode}) => {
  9 + return {
  10 + main: {
  11 + plugins: [externalizeDepsPlugin()]
  12 + },
  13 + preload: {
  14 + plugins: [externalizeDepsPlugin()]
  15 + },
  16 + renderer: {
  17 + envDir,
  18 + envPrefix: 'VITE_',
  19 + resolve: {
  20 + alias: {
  21 + '@': resolve('src/renderer/src')
  22 + }
  23 + },
  24 + plugins: [vue()],
  25 + server:{
  26 + host: true,
  27 + port:5441,
  28 + proxy:{
  29 + '/dev-api': {
  30 + target: 'https://xfwbzshd.crgx.net',
  31 + changeOrigin: true,
  32 + rewrite: (p) => p.replace(/^\/dev-api/, '')
  33 + }
  34 + }
  35 + }
  36 + }
  37 + }
  38 +})
  1 +{
  2 + "name": "signature-app",
  3 + "version": "1.0.0",
  4 + "description": "An Electron application with Vue",
  5 + "main": "./out/main/index.js",
  6 + "author": "example.com",
  7 + "homepage": "https://www.electronjs.org",
  8 + "scripts": {
  9 + "format": "prettier --write .",
  10 + "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
  11 + "start": "electron-vite preview",
  12 + "dev": "electron-vite dev",
  13 + "build": "electron-vite build",
  14 + "postinstall": "electron-builder install-app-deps",
  15 + "build:win": "npm run build && electron-builder --win --config",
  16 + "build:mac": "npm run build && electron-builder --mac --config",
  17 + "build:linux": "npm run build && electron-builder --linux --config"
  18 + },
  19 + "dependencies": {
  20 + "@electron-toolkit/preload": "^2.0.0",
  21 + "@electron-toolkit/utils": "^2.0.0",
  22 + "axios": "^1.7.8",
  23 + "element-plus": "^2.11.5",
  24 + "face-effet": "^1.5.5",
  25 + "js-cookie": "^3.0.5",
  26 + "jsencrypt": "^3.3.2",
  27 + "pinia": "^3.0.1",
  28 + "typeface-roboto": "^1.1.13",
  29 + "vue-router": "^4.5.0"
  30 + },
  31 + "devDependencies": {
  32 + "@electron-toolkit/eslint-config": "^1.0.1",
  33 + "@rushstack/eslint-patch": "^1.3.3",
  34 + "@vitejs/plugin-vue": "^4.3.1",
  35 + "@vue/eslint-config-prettier": "^8.0.0",
  36 + "autoprefixer": "^10.4.21",
  37 + "electron": "^25.6.0",
  38 + "electron-builder": "^24.6.3",
  39 + "electron-vite": "^1.0.27",
  40 + "eslint": "^8.47.0",
  41 + "eslint-plugin-vue": "^9.17.0",
  42 + "postcss": "^8.5.6",
  43 + "prettier": "^3.0.2",
  44 + "sass": "^1.85.1",
  45 + "vite": "^4.4.9",
  46 + "vue": "^3.3.4"
  47 + }
  48 +}
  1 +module.exports = {
  2 + plugins: {
  3 + autoprefixer: {},
  4 + },
  5 +}
不能预览此文件类型
  1 +import { app, shell, BrowserWindow, screen, ipcMain} from 'electron'
  2 +import { join } from 'path'
  3 +import { electronApp, optimizer, is } from '@electron-toolkit/utils'
  4 +import icon from '../../resources/icon.png?asset'
  5 +
  6 +function createWindow(width, height) {
  7 + // Create the browser window.
  8 + const mainWindow = new BrowserWindow({
  9 + icon:icon,
  10 + width,
  11 + height: height,
  12 + center: true,
  13 + show: false,
  14 + transparent:true,
  15 + autoHideMenuBar: true,
  16 + resizable: false,
  17 + titleBarStyle:'hidden',
  18 + webPreferences: {
  19 + preload: join(__dirname, '../preload/index.js'),
  20 + sandbox: false,
  21 + contextIsolation:false,
  22 + webSecurity: false,
  23 + }
  24 + })
  25 + // 点击最小化
  26 + ipcMain.on('minimizing',(event,args)=>{
  27 + event.preventDefault(); // 阻止默认最小化行为
  28 + mainWindow.minimize(); // 最小化到任务栏
  29 + })
  30 +
  31 + // 点击全屏显示
  32 + ipcMain.on('toggle-fullscreen', (event) => {
  33 + if (mainWindow.isFullScreen()) {
  34 + mainWindow.setFullScreen(false)
  35 + } else {
  36 + mainWindow.setFullScreen(true)
  37 + }
  38 + });
  39 +
  40 + // 在主进程中
  41 + mainWindow.webContents.on('before-input-event', (event, input) => {
  42 + if (input.key === 'Escape' && mainWindow.isFullScreen()) {
  43 + mainWindow.setFullScreen(false)
  44 + event.preventDefault() // 阻止默认行为
  45 + }
  46 + })
  47 +
  48 + mainWindow.on('ready-to-show', () => {
  49 + mainWindow.show()
  50 + mainWindow.setFullScreen(true)
  51 + })
  52 +
  53 + mainWindow.webContents.setWindowOpenHandler((details) => {
  54 + shell.openExternal(details.url)
  55 + return { action: 'deny' }
  56 + })
  57 +
  58 + if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
  59 + // 窗口调试 mainWindow.webContents.openDevTools()
  60 + mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
  61 + } else {
  62 + mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
  63 + }
  64 +}
  65 +
  66 +app.whenReady().then(() => {
  67 + // 主进程获取工作区宽高
  68 + const primaryDisplay = screen.getPrimaryDisplay()
  69 + const { width, height } = primaryDisplay.bounds
  70 + electronApp.setAppUserModelId('com.electron')
  71 +
  72 + app.on('browser-window-created', (_, window) => {
  73 + optimizer.watchWindowShortcuts(window)
  74 + })
  75 +
  76 + createWindow(width, height)
  77 +
  78 + app.on('activate', function () {
  79 + if (BrowserWindow.getAllWindows().length === 0) createWindow(width, height)
  80 + })
  81 +})
  82 +
  83 +
  84 +app.on('window-all-closed', () => {
  85 + if (process.platform !== 'darwin') {
  86 + app.quit()
  87 + }
  88 +})
  1 +import { contextBridge,ipcRenderer } from 'electron'
  2 +import { electronAPI } from '@electron-toolkit/preload'
  3 +const api = {}
  4 +if (process.contextIsolated) {
  5 + try {
  6 + contextBridge.exposeInMainWorld('electron', electronAPI)
  7 + contextBridge.exposeInMainWorld('api', api)
  8 + } catch (error) {
  9 + console.error(error)
  10 + }
  11 +} else {
  12 + window.electron = electronAPI
  13 + window.api = api
  14 +}
不能预览此文件类型
  1 +<!doctype html>
  2 +<html>
  3 +<head>
  4 + <meta charset="UTF-8" />
  5 + <title>Electron</title>
  6 + <!-- Content-Security-Policy -->
  7 +</head>
  8 +<body>
  9 +<div id="app"></div>
  10 +<script type="module" src="/src/main.js"></script>
  11 +</body>
  12 +</html>
不能预览此文件类型
  1 +<script setup>
  2 +import bar from './components/bar/index.vue'
  3 +</script>
  4 +
  5 +<template>
  6 + <bar />
  7 + <router-view />
  8 +</template>
  9 +
  10 +<style>
  11 +* {
  12 + margin: 0;
  13 + padding: 0;
  14 +}
  15 +div {
  16 + box-sizing: border-box;
  17 +}
  18 +body::-webkit-scrollbar {
  19 + display: none; /* Chrome/Safari/Edge */
  20 +}
  21 +
  22 +body {
  23 + scrollbar-width: none; /* Firefox */
  24 + -ms-overflow-style: none; /* IE 10+ */
  25 +}
  26 +</style>
  1 +import request from '@/utils/request'
  2 +
  3 +// 登录方法
  4 +export function login(username, password) {
  5 + const data = {
  6 + username,
  7 + password
  8 + }
  9 + return request({
  10 + url: '/login',
  11 + headers: {
  12 + isToken: false,
  13 + constentType: 'application/json;charset=UTF-8'
  14 + },
  15 + method: 'post',
  16 + data: data
  17 + })
  18 +}
  19 +
  20 +// 注册方法
  21 +export function register(data) {
  22 + return request({
  23 + url: '/register',
  24 + headers: {
  25 + isToken: false
  26 + },
  27 + method: 'post',
  28 + data: data
  29 + })
  30 +}
  31 +
  32 +// 获取用户详细信息
  33 +export function getInfo() {
  34 + return request({
  35 + url: '/getInfo',
  36 + method: 'get'
  37 + })
  38 +}
  39 +
  40 +// 上传头像方法
  41 +export function uploadAvatar(data) {
  42 + return request({
  43 + url: '/system/user/profile/avatar',
  44 + method: 'post',
  45 + headers: {
  46 + 'Content-Type': 'multipart/form-data'
  47 + },
  48 + data: data
  49 + })
  50 +}
  51 +
  52 +// 退出方法
  53 +export function logout() {
  54 + return request({
  55 + url: '/logout',
  56 + method: 'post'
  57 + })
  58 +}
  59 +
  60 +// 获取验证码
  61 +export function getCodeImg() {
  62 + return request({
  63 + url: '/captchaImage',
  64 + headers: {
  65 + isToken: false
  66 + },
  67 + method: 'get',
  68 + timeout: 20000
  69 + })
  70 +}
  1 +import request from '@/utils/request'
  2 +
  3 +// 获取路由
  4 +export const getRouters = () => {
  5 + return request({
  6 + url: '/getRouters',
  7 + method: 'get'
  8 + })
  9 +}
  1 +import request from '@/utils/request'
  2 +
  3 +// 获取签名列表
  4 +export const getSignatureList = (query) => {
  5 + return request({
  6 + url: '/system/sign/list',
  7 + method: 'get',
  8 + params: query
  9 + })
  10 +}
  11 +
  12 +// 获取应用背景
  13 +export const getUseBg = () => {
  14 + return request({
  15 + url: '/system/bg/getUsedBg',
  16 + method: 'get',
  17 + })
  18 +}
不能预览此文件类型
  1 +<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>
  1 +<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>
  1 +<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>
  1 +<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>
  1 +body {
  2 + font-family: 'Roboto', sans-serif!important;
  3 +}
  4 +* {
  5 + padding: 0;
  6 + margin: 0;
  7 +}
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
  1 +.title-bar {
  2 + position: relative; /* 为定位子元素提供参照 */
  3 + height: 45px;
  4 + display: flex;
  5 + align-items: center;
  6 + padding: 0;
  7 + -webkit-app-region: drag;
  8 + user-select: none;
  9 + overflow: hidden; /* 防止内容溢出 */
  10 +
  11 + border-top-left-radius: 20px;
  12 + border-top-right-radius: 20px;
  13 + border-top: 1px solid #d9d9d9;
  14 + border-left: 1px solid #d9d9d9;
  15 + border-right: 1px solid #d9d9d9;
  16 +}
  17 +
  18 +.title-bar-left,.title-bar-right{
  19 + width: 25%;
  20 + height: 45px;
  21 + float: left;
  22 + display: flex;
  23 + justify-content: center;
  24 + align-items: center;
  25 +
  26 +}
  27 +.title-bar-left img{
  28 + margin-left: 10px;
  29 +}
  30 +
  31 +.title-bar-right{
  32 + width: 75%;
  33 + float: right;
  34 + display: flex;
  35 + justify-content: right;
  36 + background-color: white;
  37 +}
  38 +
  39 +
  40 +.title-bar .icon {
  41 + width: 16px;
  42 + height: 16px;
  43 + margin-right: 10px;
  44 + -webkit-app-region: drag;
  45 + user-select: none;
  46 +}
  47 +
  48 +.title-bar .title {
  49 + flex-grow: 1;
  50 + font-weight: bold;
  51 + white-space: nowrap;
  52 + overflow: hidden;
  53 + text-overflow: ellipsis;
  54 + -webkit-app-region: drag;
  55 + color: #151717;
  56 + font-size: 13px;
  57 + margin-top: -2px;
  58 +}
  59 +
  60 +.title-bar .operating-button {
  61 + width: 15px;
  62 + height: 15px;
  63 + display: flex;
  64 + justify-content: center;
  65 + align-items: center;
  66 + cursor: pointer;
  67 + -webkit-app-region: no-drag;
  68 + transition: background-color 0.2s ease, opacity 0.2s ease;
  69 + border-radius: 50%;
  70 + background-color: var(--button-bg);
  71 + margin-right: 8px;
  72 +}
  73 +
  74 +.title-bar .operating-button img {
  75 + width: 10px;
  76 + height: 10px;
  77 + opacity: 0;
  78 + transition: opacity 0.2s ease;
  79 +}
  80 +
  81 +.title-bar .operating-button:hover img {
  82 + opacity: 1;
  83 +}
  84 +
  85 +.title-bar .close-button {
  86 + --button-bg: #f56057;
  87 +}
  88 +
  89 +.title-bar .minimum-button {
  90 + --button-bg: #fec428;
  91 +}
  92 +
  93 +.title-bar .expand-button {
  94 + --button-bg: #1fd42a;
  95 +}
  1 +<template>
  2 + <div class="title-bar">
  3 + <div class="title-bar-left" :style="{ backgroundColor: '#fff' }">
  4 + <img src="@/assets/bar/logo.png" alt="图标" class="icon" />
  5 + <div class="title">签名墙</div>
  6 + </div>
  7 +
  8 + <div class="title-bar-right">
  9 + <div class="operating-button close-button" @click="closeWindow">
  10 + <img src="~@/assets/bar/close.svg" />
  11 + </div>
  12 + <div class="operating-button minimum-button" @click="minimumWindow">
  13 + <img src="~@/assets/bar/minimum.svg" />
  14 + </div>
  15 + <div class="operating-button expand-button">
  16 + <img src="~@/assets/bar/expand.svg" @click="expandWindow" />
  17 + </div>
  18 + <div style="width: 10px"></div>
  19 + </div>
  20 + </div>
  21 +</template>
  22 +
  23 +<script setup>
  24 +function closeWindow() {
  25 + window.close() // 关闭窗口
  26 +}
  27 +function minimumWindow() {
  28 + electron.ipcRenderer.send('minimizing')
  29 +}
  30 +
  31 +function expandWindow() {
  32 + electron.ipcRenderer.send('toggle-fullscreen')
  33 +}
  34 +</script>
  35 +
  36 +<style scoped>
  37 +@import './index.css';
  38 +</style>
  1 +import { createApp } from 'vue';
  2 +import App from './App.vue';
  3 +import store from './store'
  4 +import router from './router';
  5 +import ElementPlus, { ElMessage } from 'element-plus';
  6 +import zhCn from 'element-plus/es/locale/lang/zh-cn'
  7 +import * as ElementPlusIconsVue from '@element-plus/icons-vue'
  8 +import 'element-plus/dist/index.css';
  9 +
  10 +const app = createApp(App);
  11 +
  12 +// 将 ElementPlus 和路由器挂载到应用实例
  13 +app.use(ElementPlus, {
  14 + locale: zhCn
  15 +})
  16 +// 全局注册ElementPlus图标
  17 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  18 + app.component(key, component)
  19 +}
  20 +
  21 +app.config.globalProperties.$message = ElMessage;
  22 +app.use(router);
  23 +app.use(store);
  24 +
  25 +
  26 +// 挂载应用
  27 +app.mount('#app');
  1 +import useUserStore from '@/store/modules/user'
  2 +
  3 +function authPermission(permission) {
  4 + const all_permission = "*:*:*";
  5 + const permissions = useUserStore().permissions
  6 + if (permission && permission.length > 0) {
  7 + return permissions.some(v => {
  8 + return all_permission === v || v === permission
  9 + })
  10 + } else {
  11 + return false
  12 + }
  13 +}
  14 +
  15 +function authRole(role) {
  16 + const super_admin = "admin";
  17 + const roles = useUserStore().roles
  18 + if (role && role.length > 0) {
  19 + return roles.some(v => {
  20 + return super_admin === v || v === role
  21 + })
  22 + } else {
  23 + return false
  24 + }
  25 +}
  26 +
  27 +export default {
  28 + // 验证用户是否具备某权限
  29 + hasPermi(permission) {
  30 + return authPermission(permission);
  31 + },
  32 + // 验证用户是否含有指定权限,只需包含其中一个
  33 + hasPermiOr(permissions) {
  34 + return permissions.some(item => {
  35 + return authPermission(item)
  36 + })
  37 + },
  38 + // 验证用户是否含有指定权限,必须全部拥有
  39 + hasPermiAnd(permissions) {
  40 + return permissions.every(item => {
  41 + return authPermission(item)
  42 + })
  43 + },
  44 + // 验证用户是否具备某角色
  45 + hasRole(role) {
  46 + return authRole(role);
  47 + },
  48 + // 验证用户是否含有指定角色,只需包含其中一个
  49 + hasRoleOr(roles) {
  50 + return roles.some(item => {
  51 + return authRole(item)
  52 + })
  53 + },
  54 + // 验证用户是否含有指定角色,必须全部拥有
  55 + hasRoleAnd(roles) {
  56 + return roles.every(item => {
  57 + return authRole(item)
  58 + })
  59 + }
  60 +}
  1 +const sessionCache = {
  2 + set (key, value) {
  3 + if (!sessionStorage) {
  4 + return
  5 + }
  6 + if (key != null && value != null) {
  7 + sessionStorage.setItem(key, value)
  8 + }
  9 + },
  10 + get (key) {
  11 + if (!sessionStorage) {
  12 + return null
  13 + }
  14 + if (key == null) {
  15 + return null
  16 + }
  17 + return sessionStorage.getItem(key)
  18 + },
  19 + setJSON (key, jsonValue) {
  20 + if (jsonValue != null) {
  21 + this.set(key, JSON.stringify(jsonValue))
  22 + }
  23 + },
  24 + getJSON (key) {
  25 + const value = this.get(key)
  26 + if (value != null) {
  27 + return JSON.parse(value)
  28 + }
  29 + },
  30 + remove (key) {
  31 + sessionStorage.removeItem(key);
  32 + }
  33 +}
  34 +const localCache = {
  35 + set (key, value) {
  36 + if (!localStorage) {
  37 + return
  38 + }
  39 + if (key != null && value != null) {
  40 + localStorage.setItem(key, value)
  41 + }
  42 + },
  43 + get (key) {
  44 + if (!localStorage) {
  45 + return null
  46 + }
  47 + if (key == null) {
  48 + return null
  49 + }
  50 + return localStorage.getItem(key)
  51 + },
  52 + setJSON (key, jsonValue) {
  53 + if (jsonValue != null) {
  54 + this.set(key, JSON.stringify(jsonValue))
  55 + }
  56 + },
  57 + getJSON (key) {
  58 + const value = this.get(key)
  59 + if (value != null) {
  60 + return JSON.parse(value)
  61 + }
  62 + },
  63 + remove (key) {
  64 + localStorage.removeItem(key);
  65 + }
  66 +}
  67 +
  68 +export default {
  69 + /**
  70 + * 会话级缓存
  71 + */
  72 + session: sessionCache,
  73 + /**
  74 + * 本地缓存
  75 + */
  76 + local: localCache
  77 +}
  1 +import axios from 'axios'
  2 +import { ElMessage } from 'element-plus'
  3 +import { saveAs } from 'file-saver'
  4 +import { getToken } from '@/utils/auth'
  5 +import errorCode from '@/utils/errorCode'
  6 +import { blobValidate } from '@/utils/ruoyi'
  7 +
  8 +const baseURL = import.meta.env.VITE_APP_BASE_API
  9 +
  10 +export default {
  11 + name(name, isDelete = true) {
  12 + var url = baseURL + "/common/download?fileName=" + encodeURIComponent(name) + "&delete=" + isDelete
  13 + axios({
  14 + method: 'get',
  15 + url: url,
  16 + responseType: 'blob',
  17 + headers: { 'Authorization': 'Bearer ' + getToken() }
  18 + }).then(async (res) => {
  19 + const isLogin = await blobValidate(res.data);
  20 + if (isLogin) {
  21 + const blob = new Blob([res.data])
  22 + this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
  23 + } else {
  24 + this.printErrMsg(res.data);
  25 + }
  26 + })
  27 + },
  28 + resource(resource) {
  29 + var url = baseURL + "/common/download/resource?resource=" + encodeURIComponent(resource);
  30 + axios({
  31 + method: 'get',
  32 + url: url,
  33 + responseType: 'blob',
  34 + headers: { 'Authorization': 'Bearer ' + getToken() }
  35 + }).then(async (res) => {
  36 + const isLogin = await blobValidate(res.data);
  37 + if (isLogin) {
  38 + const blob = new Blob([res.data])
  39 + this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
  40 + } else {
  41 + this.printErrMsg(res.data);
  42 + }
  43 + })
  44 + },
  45 + zip(url, name) {
  46 + var url = baseURL + url
  47 + axios({
  48 + method: 'get',
  49 + url: url,
  50 + responseType: 'blob',
  51 + headers: { 'Authorization': 'Bearer ' + getToken() }
  52 + }).then(async (res) => {
  53 + const isLogin = await blobValidate(res.data);
  54 + if (isLogin) {
  55 + const blob = new Blob([res.data], { type: 'application/zip' })
  56 + this.saveAs(blob, name)
  57 + } else {
  58 + this.printErrMsg(res.data);
  59 + }
  60 + })
  61 + },
  62 + saveAs(text, name, opts) {
  63 + saveAs(text, name, opts);
  64 + },
  65 + async printErrMsg(data) {
  66 + const resText = await data.text();
  67 + const rspObj = JSON.parse(resText);
  68 + const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
  69 + ElMessage.error(errMsg);
  70 + }
  71 +}
  72 +
  1 +import tab from './tab'
  2 +import auth from './auth'
  3 +import cache from './cache'
  4 +import modal from './modal'
  5 +import download from './download'
  6 +
  7 +export default function installPlugins(app){
  8 + // 页签操作
  9 + app.config.globalProperties.$tab = tab
  10 + // 认证对象
  11 + app.config.globalProperties.$auth = auth
  12 + // 缓存对象
  13 + app.config.globalProperties.$cache = cache
  14 + // 模态框对象
  15 + app.config.globalProperties.$modal = modal
  16 + // 下载文件
  17 + app.config.globalProperties.$download = download
  18 +}
  1 +import { ElMessage, ElMessageBox, ElNotification, ElLoading } from 'element-plus'
  2 +
  3 +let loadingInstance;
  4 +
  5 +export default {
  6 + // 消息提示
  7 + msg(content) {
  8 + ElMessage.info(content)
  9 + },
  10 + // 错误消息
  11 + msgError(content) {
  12 + ElMessage.error(content)
  13 + },
  14 + // 成功消息
  15 + msgSuccess(content) {
  16 + ElMessage.success(content)
  17 + },
  18 + // 警告消息
  19 + msgWarning(content) {
  20 + ElMessage.warning(content)
  21 + },
  22 + // 弹出提示
  23 + alert(content) {
  24 + ElMessageBox.alert(content, "系统提示")
  25 + },
  26 + // 错误提示
  27 + alertError(content) {
  28 + ElMessageBox.alert(content, "系统提示", { type: 'error' })
  29 + },
  30 + // 成功提示
  31 + alertSuccess(content) {
  32 + ElMessageBox.alert(content, "系统提示", { type: 'success' })
  33 + },
  34 + // 警告提示
  35 + alertWarning(content) {
  36 + ElMessageBox.alert(content, "系统提示", { type: 'warning' })
  37 + },
  38 + // 通知提示
  39 + notify(content) {
  40 + ElNotification.info(content)
  41 + },
  42 + // 错误通知
  43 + notifyError(content) {
  44 + ElNotification.error(content);
  45 + },
  46 + // 成功通知
  47 + notifySuccess(content) {
  48 + ElNotification.success(content)
  49 + },
  50 + // 警告通知
  51 + notifyWarning(content) {
  52 + ElNotification.warning(content)
  53 + },
  54 + // 确认窗体
  55 + confirm(content) {
  56 + return ElMessageBox.confirm(content, "系统提示", {
  57 + confirmButtonText: '确定',
  58 + cancelButtonText: '取消',
  59 + type: "warning",
  60 + })
  61 + },
  62 + // 提交内容
  63 + prompt(content) {
  64 + return ElMessageBox.prompt(content, "系统提示", {
  65 + confirmButtonText: '确定',
  66 + cancelButtonText: '取消',
  67 + type: "warning",
  68 + })
  69 + },
  70 + // 打开遮罩层
  71 + loading(content) {
  72 + loadingInstance = ElLoading.service({
  73 + lock: true,
  74 + text: content,
  75 + background: "rgba(0, 0, 0, 0.7)",
  76 + })
  77 + },
  78 + // 关闭遮罩层
  79 + closeLoading() {
  80 + loadingInstance.close();
  81 + }
  82 +}
  1 +import useTagsViewStore from '@/store/modules/tagsView'
  2 +import router from '@/router'
  3 +
  4 +export default {
  5 + // 刷新当前tab页签
  6 + refreshPage(obj) {
  7 + const { path, query, matched } = router.currentRoute.value;
  8 + if (obj === undefined) {
  9 + matched.forEach((m) => {
  10 + if (m.components && m.components.default && m.components.default.name) {
  11 + if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
  12 + obj = { name: m.components.default.name, path: path, query: query };
  13 + }
  14 + }
  15 + });
  16 + }
  17 + return useTagsViewStore().delCachedView(obj).then(() => {
  18 + const { path, query } = obj
  19 + router.replace({
  20 + path: '/redirect' + path,
  21 + query: query
  22 + })
  23 + })
  24 + },
  25 + // 关闭当前tab页签,打开新页签
  26 + closeOpenPage(obj) {
  27 + useTagsViewStore().delView(router.currentRoute.value);
  28 + if (obj !== undefined) {
  29 + return router.push(obj);
  30 + }
  31 + },
  32 + // 关闭指定tab页签
  33 + closePage(obj) {
  34 + if (obj === undefined) {
  35 + return useTagsViewStore().delView(router.currentRoute.value).then(({ lastPath }) => {
  36 + return router.push(lastPath || '/index');
  37 + });
  38 + }
  39 + return useTagsViewStore().delView(obj);
  40 + },
  41 + // 关闭所有tab页签
  42 + closeAllPage() {
  43 + return useTagsViewStore().delAllViews();
  44 + },
  45 + // 关闭左侧tab页签
  46 + closeLeftPage(obj) {
  47 + return useTagsViewStore().delLeftTags(obj || router.currentRoute.value);
  48 + },
  49 + // 关闭右侧tab页签
  50 + closeRightPage(obj) {
  51 + return useTagsViewStore().delRightTags(obj || router.currentRoute.value);
  52 + },
  53 + // 关闭其他tab页签
  54 + closeOtherPage(obj) {
  55 + return useTagsViewStore().delOthersViews(obj || router.currentRoute.value);
  56 + },
  57 + // 打开tab页签
  58 + openPage(url) {
  59 + return router.push(url);
  60 + },
  61 + // 修改tab页签
  62 + updatePage(obj) {
  63 + return useTagsViewStore().updateVisitedView(obj);
  64 + }
  65 +}
  1 +import { createRouter, createWebHashHistory } from 'vue-router'
  2 +
  3 +// 公共路由
  4 +export const constantRoutes = [
  5 + {
  6 + path: '/',
  7 + name: '首页',
  8 + component: () => import('@/views/index.vue'),
  9 + hidden: true
  10 + }
  11 +]
  12 +
  13 +const router = createRouter({
  14 + history: createWebHashHistory(import.meta.env.BASE_URL),
  15 + routes: constantRoutes,
  16 +})
  17 +
  18 +export default router
  1 +import { createPinia } from 'pinia'
  2 +const store = createPinia()
  3 +
  4 +export default store
  1 +import { defineStore } from 'pinia'
  2 +import { login, logout, getInfo } from '@/api/login.js'
  3 +import { getToken, setToken, removeToken } from '@/utils/auth'
  4 +
  5 +const useUserStore = defineStore('user', {
  6 + state: () => ({
  7 + token: getToken(),
  8 + name: '',
  9 + avatar: '',
  10 + roles: [],
  11 + permissions: []
  12 + }),
  13 + actions: {
  14 + // 登录
  15 + login(userInfo) {
  16 + const username = userInfo.username.trim()
  17 + const password = userInfo.password
  18 + return new Promise((resolve, reject) => {
  19 + login(username, password)
  20 + .then((res) => {
  21 + setToken(res.token)
  22 + this.token = res.token
  23 + resolve()
  24 + })
  25 + .catch((error) => {
  26 + reject(error)
  27 + })
  28 + })
  29 + },
  30 + // 获取用户信息
  31 + getInfo() {
  32 + return new Promise((resolve, reject) => {
  33 + getInfo()
  34 + .then((res) => {
  35 + const user = res.user
  36 + const avatar =
  37 + user.avatar == '' || user.avatar == null
  38 + ? ''
  39 + : import.meta.env.VITE_APP_BASE_API + user.avatar
  40 +
  41 + if (res.roles && res.roles.length > 0) {
  42 + // 验证返回的roles是否是一个非空数组
  43 + this.roles = res.roles
  44 + this.permissions = res.permissions
  45 + } else {
  46 + this.roles = ['ROLE_DEFAULT']
  47 + }
  48 + this.name = user.userName
  49 + this.avatar = avatar
  50 + resolve(res)
  51 + })
  52 + .catch((error) => {
  53 + reject(error)
  54 + })
  55 + })
  56 + },
  57 + // 退出系统
  58 + logOut() {
  59 + return new Promise((resolve, reject) => {
  60 + logout(this.token)
  61 + .then(() => {
  62 + this.token = ''
  63 + this.roles = []
  64 + this.permissions = []
  65 + removeToken()
  66 + resolve()
  67 + })
  68 + .catch((error) => {
  69 + reject(error)
  70 + })
  71 + })
  72 + }
  73 + }
  74 +})
  75 +
  76 +export default useUserStore
  1 +import Cookies from 'js-cookie'
  2 +
  3 +const TokenKey = 'Admin-Token'
  4 +
  5 +export function getToken() {
  6 + return Cookies.get(TokenKey)
  7 +}
  8 +
  9 +export function setToken(token) {
  10 + return Cookies.set(TokenKey, token)
  11 +}
  12 +
  13 +export function removeToken() {
  14 + return Cookies.remove(TokenKey)
  15 +}
  1 +// 在渲染进程中发送base64图片
  2 +import { uploadAvatar } from '@/api/login';
  3 +
  4 +// 辅助函数:Base64转Blob
  5 +function base64ToBlob(base64Data, contentType = 'image/png') {
  6 + // 分割Base64字符串获取实际数据部分
  7 + const byteString = atob(base64Data.split(',')[1]);
  8 +
  9 + // 将字符串转换为字节数组
  10 + const arrayBuffer = new ArrayBuffer(byteString.length);
  11 + const uint8Array = new Uint8Array(arrayBuffer);
  12 +
  13 + for (let i = 0; i < byteString.length; i++) {
  14 + uint8Array[i] = byteString.charCodeAt(i);
  15 + }
  16 +
  17 + // 创建Blob对象
  18 + return new Blob([arrayBuffer], { type: contentType });
  19 +}
  20 +
  21 +export async function uploadImage(base64Data) {
  22 + // 1. 将Base64字符串转换为Blob对象
  23 + const blob = base64ToBlob(base64Data);
  24 + console.log(blob)
  25 + // 2. 创建FormData对象
  26 + const formData = new FormData();
  27 + formData.append('avatarfile', blob); // 'image'是后端要求的字段名
  28 + console.log(formData)
  29 + // 3. 添加其他表单数据(可选)
  30 + // formData.append('userId', '12345');
  31 + // formData.append('type', 'avatar');
  32 +
  33 + try {
  34 + // await fetch('https://xfwbzshd.crgx.net/system/user/profile/avatar', {
  35 + // method: 'POST',
  36 + // headers: {
  37 + // // 如果需要,可以添加其他HTTP头,如Authorization等
  38 + // Authorization: 'Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjcxOWM1ZjE3LTNjMjYtNGFhNi04NWUwLThlOGZjZDBkYTAxMSJ9._gpSVz_UbRGTH0O0vMDlJEQ4uepf619ELGXgsdA1Ul0puNiWicQuddfwv-4c--qdaqN5hxSnKlJADNv7gfR64g',
  39 + // },
  40 + // body: formData,
  41 + // // 注意:不要手动设置Content-Type!FormData会自动设置正确的multipart/form-data和boundary
  42 + // });
  43 + await uploadAvatar(formData);
  44 + console.log('上传成功:');
  45 + } catch (error) {
  46 + console.error('上传错误:', error);
  47 + throw error;
  48 + }
  49 +}
  1 +function objectToFormData(obj) {
  2 + const fd = new FormData();
  3 + Object.keys(obj).forEach(key => {
  4 + const value = obj[key];
  5 + fd.set(key, value);
  6 + });
  7 + return fd;
  8 +}
  9 +function objectToQueryStr(obj, filterNull = false) {
  10 + let queryStr = "";
  11 + Object.keys(obj).forEach(key => {
  12 + if (filterNull && !obj[key]) {
  13 + return;
  14 + }
  15 + queryStr += `&${key}=${obj[key] || ''}`
  16 + });
  17 + console.log(queryStr);
  18 + return queryStr.slice(1);
  19 +}
  20 +function openWindow(path) {
  21 + const prefix = import.meta.env.VITE_APP_TAB_URL_PREFIX;
  22 + window.open(`${prefix}${path}`);
  23 +}
  24 +
  25 +// 运行实例
  26 +function listToTree(list) {
  27 + const nodeMap = {};
  28 + const firstLevelList = list.filter(item => item.parentExecutionId === "0");
  29 + firstLevelList.forEach(item => {
  30 + const {executionId} = item;
  31 + nodeMap[executionId] = item;
  32 + });
  33 + while (true) {
  34 + list.forEach(item => {
  35 + const {parentExecutionId, executionId} = item;
  36 + if (nodeMap[executionId]) return;
  37 + if (nodeMap[parentExecutionId]) {
  38 + const parent = nodeMap[parentExecutionId];
  39 + if (parent.children) {
  40 + parent.children.push(item);
  41 + } else {
  42 + parent.children = [item];
  43 + }
  44 + nodeMap[executionId] = item;
  45 + }
  46 + });
  47 + if (Object.keys(nodeMap).length === list.length) {
  48 + return firstLevelList;
  49 + }
  50 + }
  51 +
  52 +}
  53 +
  54 +function normalizeDateTimeString(rawDatatimeStr) {
  55 + function genNumStr(num) {
  56 + return Number(num) < 10 ? "0" + num : ("" + num)
  57 + }
  58 + if (rawDatatimeStr) {
  59 + const dateObj = new Date(rawDatatimeStr);
  60 +
  61 + const yyyy = dateObj.getFullYear();
  62 + const MM = dateObj.getMonth() + 1;
  63 + const dd = dateObj.getDate();
  64 +
  65 + const HH = dateObj.getHours();
  66 + const mm = dateObj.getMinutes();
  67 + const ss = dateObj.getSeconds();
  68 +
  69 + return `${yyyy}-${genNumStr(MM)}-${genNumStr(dd)} ${genNumStr(HH)}:${genNumStr(mm)}:${genNumStr(ss)}`;
  70 + }
  71 +}
  72 +
  73 +export default {
  74 + objectToFormData,
  75 + objectToQueryStr,
  76 + openWindow,
  77 + listToTree,
  78 + normalizeDateTimeString
  79 +};
  1 +import useDictStore from '@/store/modules/dict'
  2 +import { getDicts } from '@/api/system/dict/data'
  3 +
  4 +/**
  5 + * 获取字典数据
  6 + */
  7 +export function useDict(...args) {
  8 + const res = ref({});
  9 + return (() => {
  10 + args.forEach((dictType, index) => {
  11 + res.value[dictType] = [];
  12 + const dicts = useDictStore().getDict(dictType);
  13 + if (dicts) {
  14 + res.value[dictType] = dicts;
  15 + } else {
  16 + getDicts(dictType).then(resp => {
  17 + res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
  18 + useDictStore().setDict(dictType, res.value[dictType]);
  19 + })
  20 + }
  21 + })
  22 + return toRefs(res.value);
  23 + })()
  24 +}
  1 +import store from '@/store'
  2 +import defaultSettings from '@/settings'
  3 +import useSettingsStore from '@/store/modules/settings'
  4 +
  5 +/**
  6 + * 动态修改标题
  7 + */
  8 +export function useDynamicTitle() {
  9 + const settingsStore = useSettingsStore();
  10 + if (settingsStore.dynamicTitle) {
  11 + document.title = settingsStore.title + ' - ' + defaultSettings.title;
  12 + } else {
  13 + document.title = defaultSettings.title;
  14 + }
  15 +}
  1 +export default {
  2 + '401': '认证失败,无法访问系统资源',
  3 + '403': '当前操作没有权限',
  4 + '404': '访问资源不存在',
  5 + default: '系统未知错误,请反馈给管理员'
  6 +}
  1 +import { parseTime } from './ruoyi'
  2 +
  3 +/**
  4 + * 表格时间格式化
  5 + */
  6 +export function formatDate(cellValue) {
  7 + if (cellValue == null || cellValue == "") return "";
  8 + var date = new Date(cellValue)
  9 + var year = date.getFullYear()
  10 + var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1
  11 + var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()
  12 + var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours()
  13 + var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
  14 + var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
  15 + return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds
  16 +}
  17 +
  18 +/**
  19 + * @param {number} time
  20 + * @param {string} option
  21 + * @returns {string}
  22 + */
  23 +export function formatTime(time, option) {
  24 + if (('' + time).length === 10) {
  25 + time = parseInt(time) * 1000
  26 + } else {
  27 + time = +time
  28 + }
  29 + const d = new Date(time)
  30 + const now = Date.now()
  31 +
  32 + const diff = (now - d) / 1000
  33 +
  34 + if (diff < 30) {
  35 + return '刚刚'
  36 + } else if (diff < 3600) {
  37 + // less 1 hour
  38 + return Math.ceil(diff / 60) + '分钟前'
  39 + } else if (diff < 3600 * 24) {
  40 + return Math.ceil(diff / 3600) + '小时前'
  41 + } else if (diff < 3600 * 24 * 2) {
  42 + return '1天前'
  43 + }
  44 + if (option) {
  45 + return parseTime(time, option)
  46 + } else {
  47 + return (
  48 + d.getMonth() +
  49 + 1 +
  50 + '月' +
  51 + d.getDate() +
  52 + '日' +
  53 + d.getHours() +
  54 + '时' +
  55 + d.getMinutes() +
  56 + '分'
  57 + )
  58 + }
  59 +}
  60 +
  61 +/**
  62 + * @param {string} url
  63 + * @returns {Object}
  64 + */
  65 +export function getQueryObject(url) {
  66 + url = url == null ? window.location.href : url
  67 + const search = url.substring(url.lastIndexOf('?') + 1)
  68 + const obj = {}
  69 + const reg = /([^?&=]+)=([^?&=]*)/g
  70 + search.replace(reg, (rs, $1, $2) => {
  71 + const name = decodeURIComponent($1)
  72 + let val = decodeURIComponent($2)
  73 + val = String(val)
  74 + obj[name] = val
  75 + return rs
  76 + })
  77 + return obj
  78 +}
  79 +
  80 +/**
  81 + * @param {string} input value
  82 + * @returns {number} output value
  83 + */
  84 +export function byteLength(str) {
  85 + // returns the byte length of an utf8 string
  86 + let s = str.length
  87 + for (var i = str.length - 1; i >= 0; i--) {
  88 + const code = str.charCodeAt(i)
  89 + if (code > 0x7f && code <= 0x7ff) s++
  90 + else if (code > 0x7ff && code <= 0xffff) s += 2
  91 + if (code >= 0xDC00 && code <= 0xDFFF) i--
  92 + }
  93 + return s
  94 +}
  95 +
  96 +/**
  97 + * @param {Array} actual
  98 + * @returns {Array}
  99 + */
  100 +export function cleanArray(actual) {
  101 + const newArray = []
  102 + for (let i = 0; i < actual.length; i++) {
  103 + if (actual[i]) {
  104 + newArray.push(actual[i])
  105 + }
  106 + }
  107 + return newArray
  108 +}
  109 +
  110 +/**
  111 + * @param {Object} json
  112 + * @returns {Array}
  113 + */
  114 +export function param(json) {
  115 + if (!json) return ''
  116 + return cleanArray(
  117 + Object.keys(json).map(key => {
  118 + if (json[key] === undefined) return ''
  119 + return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])
  120 + })
  121 + ).join('&')
  122 +}
  123 +
  124 +/**
  125 + * @param {string} url
  126 + * @returns {Object}
  127 + */
  128 +export function param2Obj(url) {
  129 + const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
  130 + if (!search) {
  131 + return {}
  132 + }
  133 + const obj = {}
  134 + const searchArr = search.split('&')
  135 + searchArr.forEach(v => {
  136 + const index = v.indexOf('=')
  137 + if (index !== -1) {
  138 + const name = v.substring(0, index)
  139 + const val = v.substring(index + 1, v.length)
  140 + obj[name] = val
  141 + }
  142 + })
  143 + return obj
  144 +}
  145 +
  146 +/**
  147 + * @param {string} val
  148 + * @returns {string}
  149 + */
  150 +export function html2Text(val) {
  151 + const div = document.createElement('div')
  152 + div.innerHTML = val
  153 + return div.textContent || div.innerText
  154 +}
  155 +
  156 +/**
  157 + * Merges two objects, giving the last one precedence
  158 + * @param {Object} target
  159 + * @param {(Object|Array)} source
  160 + * @returns {Object}
  161 + */
  162 +export function objectMerge(target, source) {
  163 + if (typeof target !== 'object') {
  164 + target = {}
  165 + }
  166 + if (Array.isArray(source)) {
  167 + return source.slice()
  168 + }
  169 + Object.keys(source).forEach(property => {
  170 + const sourceProperty = source[property]
  171 + if (typeof sourceProperty === 'object') {
  172 + target[property] = objectMerge(target[property], sourceProperty)
  173 + } else {
  174 + target[property] = sourceProperty
  175 + }
  176 + })
  177 + return target
  178 +}
  179 +
  180 +/**
  181 + * @param {HTMLElement} element
  182 + * @param {string} className
  183 + */
  184 +export function toggleClass(element, className) {
  185 + if (!element || !className) {
  186 + return
  187 + }
  188 + let classString = element.className
  189 + const nameIndex = classString.indexOf(className)
  190 + if (nameIndex === -1) {
  191 + classString += '' + className
  192 + } else {
  193 + classString =
  194 + classString.substr(0, nameIndex) +
  195 + classString.substr(nameIndex + className.length)
  196 + }
  197 + element.className = classString
  198 +}
  199 +
  200 +/**
  201 + * @param {string} type
  202 + * @returns {Date}
  203 + */
  204 +export function getTime(type) {
  205 + if (type === 'start') {
  206 + return new Date().getTime() - 3600 * 1000 * 24 * 90
  207 + } else {
  208 + return new Date(new Date().toDateString())
  209 + }
  210 +}
  211 +
  212 +/**
  213 + * @param {Function} func
  214 + * @param {number} wait
  215 + * @param {boolean} immediate
  216 + * @return {*}
  217 + */
  218 +export function debounce(func, wait, immediate) {
  219 + let timeout, args, context, timestamp, result
  220 +
  221 + const later = function() {
  222 + // 据上一次触发时间间隔
  223 + const last = +new Date() - timestamp
  224 +
  225 + // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
  226 + if (last < wait && last > 0) {
  227 + timeout = setTimeout(later, wait - last)
  228 + } else {
  229 + timeout = null
  230 + // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
  231 + if (!immediate) {
  232 + result = func.apply(context, args)
  233 + if (!timeout) context = args = null
  234 + }
  235 + }
  236 + }
  237 +
  238 + return function(...args) {
  239 + context = this
  240 + timestamp = +new Date()
  241 + const callNow = immediate && !timeout
  242 + // 如果延时不存在,重新设定延时
  243 + if (!timeout) timeout = setTimeout(later, wait)
  244 + if (callNow) {
  245 + result = func.apply(context, args)
  246 + context = args = null
  247 + }
  248 +
  249 + return result
  250 + }
  251 +}
  252 +
  253 +/**
  254 + * This is just a simple version of deep copy
  255 + * Has a lot of edge cases bug
  256 + * If you want to use a perfect deep copy, use lodash's _.cloneDeep
  257 + * @param {Object} source
  258 + * @returns {Object}
  259 + */
  260 +export function deepClone(source) {
  261 + if (!source && typeof source !== 'object') {
  262 + throw new Error('error arguments', 'deepClone')
  263 + }
  264 + const targetObj = source.constructor === Array ? [] : {}
  265 + Object.keys(source).forEach(keys => {
  266 + if (source[keys] && typeof source[keys] === 'object') {
  267 + targetObj[keys] = deepClone(source[keys])
  268 + } else {
  269 + targetObj[keys] = source[keys]
  270 + }
  271 + })
  272 + return targetObj
  273 +}
  274 +
  275 +/**
  276 + * @param {Array} arr
  277 + * @returns {Array}
  278 + */
  279 +export function uniqueArr(arr) {
  280 + return Array.from(new Set(arr))
  281 +}
  282 +
  283 +/**
  284 + * @returns {string}
  285 + */
  286 +export function createUniqueString() {
  287 + const timestamp = +new Date() + ''
  288 + const randomNum = parseInt((1 + Math.random()) * 65536) + ''
  289 + return (+(randomNum + timestamp)).toString(32)
  290 +}
  291 +
  292 +/**
  293 + * Check if an element has a class
  294 + * @param {HTMLElement} elm
  295 + * @param {string} cls
  296 + * @returns {boolean}
  297 + */
  298 +export function hasClass(ele, cls) {
  299 + return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
  300 +}
  301 +
  302 +/**
  303 + * Add class to element
  304 + * @param {HTMLElement} elm
  305 + * @param {string} cls
  306 + */
  307 +export function addClass(ele, cls) {
  308 + if (!hasClass(ele, cls)) ele.className += ' ' + cls
  309 +}
  310 +
  311 +/**
  312 + * Remove class from element
  313 + * @param {HTMLElement} elm
  314 + * @param {string} cls
  315 + */
  316 +export function removeClass(ele, cls) {
  317 + if (hasClass(ele, cls)) {
  318 + const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
  319 + ele.className = ele.className.replace(reg, ' ')
  320 + }
  321 +}
  322 +
  323 +export function makeMap(str, expectsLowerCase) {
  324 + const map = Object.create(null)
  325 + const list = str.split(',')
  326 + for (let i = 0; i < list.length; i++) {
  327 + map[list[i]] = true
  328 + }
  329 + return expectsLowerCase
  330 + ? val => map[val.toLowerCase()]
  331 + : val => map[val]
  332 +}
  333 +
  334 +export const exportDefault = 'export default '
  335 +
  336 +export const beautifierConf = {
  337 + html: {
  338 + indent_size: '2',
  339 + indent_char: ' ',
  340 + max_preserve_newlines: '-1',
  341 + preserve_newlines: false,
  342 + keep_array_indentation: false,
  343 + break_chained_methods: false,
  344 + indent_scripts: 'separate',
  345 + brace_style: 'end-expand',
  346 + space_before_conditional: true,
  347 + unescape_strings: false,
  348 + jslint_happy: false,
  349 + end_with_newline: true,
  350 + wrap_line_length: '110',
  351 + indent_inner_html: true,
  352 + comma_first: false,
  353 + e4x: true,
  354 + indent_empty_lines: true
  355 + },
  356 + js: {
  357 + indent_size: '2',
  358 + indent_char: ' ',
  359 + max_preserve_newlines: '-1',
  360 + preserve_newlines: false,
  361 + keep_array_indentation: false,
  362 + break_chained_methods: false,
  363 + indent_scripts: 'normal',
  364 + brace_style: 'end-expand',
  365 + space_before_conditional: true,
  366 + unescape_strings: false,
  367 + jslint_happy: true,
  368 + end_with_newline: true,
  369 + wrap_line_length: '110',
  370 + indent_inner_html: true,
  371 + comma_first: false,
  372 + e4x: true,
  373 + indent_empty_lines: true
  374 + }
  375 +}
  376 +
  377 +// 首字母大小
  378 +export function titleCase(str) {
  379 + return str.replace(/( |^)[a-z]/g, L => L.toUpperCase())
  380 +}
  381 +
  382 +// 下划转驼峰
  383 +export function camelCase(str) {
  384 + return str.replace(/_[a-z]/g, str1 => str1.substr(-1).toUpperCase())
  385 +}
  386 +
  387 +export function isNumberStr(str) {
  388 + return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str)
  389 +}
  390 +
  1 +//计算两个时间之间的时间差 多少天时分秒
  2 +export function intervalTime(timestamp1, timestamp2) {
  3 + // 将时间戳转换为毫秒
  4 + const diffInMilliseconds = Math.abs(timestamp2 - timestamp1);
  5 + // 1 小时等于 3600000 毫秒
  6 + return (diffInMilliseconds / 3600000).toFixed(2) + '时';
  7 +}
  1 +import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
  2 +
  3 +// 密钥对生成 http://web.chacuo.net/netrsakeypair
  4 +
  5 +const publicKey =
  6 + 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
  7 + 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
  8 +
  9 +const privateKey =
  10 + 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +
  11 + '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +
  12 + 'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' +
  13 + 'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' +
  14 + 'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' +
  15 + 'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +
  16 + 'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +
  17 + 'UP8iWi1Qw0Y='
  18 +
  19 +// 加密
  20 +export function encrypt(txt) {
  21 + const encryptor = new JSEncrypt()
  22 + encryptor.setPublicKey(publicKey) // 设置公钥
  23 + return encryptor.encrypt(txt) // 对数据进行加密
  24 +}
  25 +
  26 +// 解密
  27 +export function decrypt(txt) {
  28 + const encryptor = new JSEncrypt()
  29 + encryptor.setPrivateKey(privateKey) // 设置私钥
  30 + return encryptor.decrypt(txt) // 对数据进行解密
  31 +}
  1 +import useUserStore from '@/store/modules/user'
  2 +
  3 +/**
  4 + * 字符权限校验
  5 + * @param {Array} value 校验值
  6 + * @returns {Boolean}
  7 + */
  8 +export function checkPermi(value) {
  9 + if (value && value instanceof Array && value.length > 0) {
  10 + const permissions = useUserStore().permissions
  11 + const permissionDatas = value
  12 + const all_permission = "*:*:*";
  13 +
  14 + const hasPermission = permissions.some(permission => {
  15 + return all_permission === permission || permissionDatas.includes(permission)
  16 + })
  17 +
  18 + if (!hasPermission) {
  19 + return false
  20 + }
  21 + return true
  22 + } else {
  23 + console.error(`need roles! Like checkPermi="['system:user:add','system:user:edit']"`)
  24 + return false
  25 + }
  26 +}
  27 +
  28 +/**
  29 + * 角色权限校验
  30 + * @param {Array} value 校验值
  31 + * @returns {Boolean}
  32 + */
  33 +export function checkRole(value) {
  34 + if (value && value instanceof Array && value.length > 0) {
  35 + const roles = useUserStore().roles
  36 + const permissionRoles = value
  37 + const super_admin = "admin";
  38 +
  39 + const hasRole = roles.some(role => {
  40 + return super_admin === role || permissionRoles.includes(role)
  41 + })
  42 +
  43 + if (!hasRole) {
  44 + return false
  45 + }
  46 + return true
  47 + } else {
  48 + console.error(`need roles! Like checkRole="['admin','editor']"`)
  49 + return false
  50 + }
  51 +}
  1 +import axios from 'axios'
  2 +import { ElNotification, ElMessageBox, ElMessage } from 'element-plus'
  3 +import { getToken } from '@/utils/auth'
  4 +import errorCode from '@/utils/errorCode'
  5 +import { tansParams } from '@/utils/ruoyi'
  6 +import cache from '@/plugins/cache'
  7 +import useUserStore from '@/store/modules/user'
  8 +
  9 +// 是否显示重新登录
  10 +export let isRelogin = { show: false }
  11 +const server_base = localStorage.getItem('crgx_server')
  12 +
  13 +axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
  14 +// 创建axios实例
  15 +const service = axios.create({
  16 + // axios中请求配置有baseURL选项,表示请求URL公共部分
  17 + baseURL: server_base,
  18 + // 超时
  19 + timeout: 10000
  20 +})
  21 +
  22 +// request拦截器
  23 +service.interceptors.request.use(
  24 + (config) => {
  25 + // 是否需要设置 token
  26 + const isToken = (config.headers || {}).isToken === false
  27 + // 是否需要防止数据重复提交
  28 + const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
  29 + if (getToken() && !isToken) {
  30 + config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  31 + }
  32 + // get请求映射params参数
  33 + if (config.method === 'get' && config.params) {
  34 + let url = config.url + '?' + tansParams(config.params)
  35 + url = url.slice(0, -1)
  36 + config.params = {}
  37 + config.url = url
  38 + }
  39 + if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
  40 + const requestObj = {
  41 + url: config.url,
  42 + data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
  43 + time: new Date().getTime()
  44 + }
  45 + const sessionObj = cache.session.getJSON('sessionObj')
  46 + if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
  47 + cache.session.setJSON('sessionObj', requestObj)
  48 + } else {
  49 + const s_url = sessionObj.url // 请求地址
  50 + const s_data = sessionObj.data // 请求数据
  51 + const s_time = sessionObj.time // 请求时间
  52 + const interval = 1000 // 间隔时间(ms),小于此时间视为重复提交
  53 + if (
  54 + s_data === requestObj.data &&
  55 + requestObj.time - s_time < interval &&
  56 + s_url === requestObj.url
  57 + ) {
  58 + const message = '数据正在处理,请勿重复提交'
  59 + console.warn(`[${s_url}]: ` + message)
  60 + return Promise.reject(new Error(message))
  61 + } else {
  62 + cache.session.setJSON('sessionObj', requestObj)
  63 + }
  64 + }
  65 + }
  66 + return config
  67 + },
  68 + (error) => {
  69 + console.log(error)
  70 + Promise.reject(error)
  71 + }
  72 +)
  73 +
  74 +// 响应拦截器
  75 +service.interceptors.response.use(
  76 + (res) => {
  77 + // 未设置状态码则默认成功状态
  78 + const code = res.data.code || 200
  79 + // 获取错误信息
  80 + const msg = errorCode[code] || res.data.msg || errorCode['default']
  81 + // 二进制数据则直接返回
  82 + if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
  83 + return res.data
  84 + }
  85 + if (code === 401) {
  86 + if (!isRelogin.show) {
  87 + isRelogin.show = true
  88 + ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
  89 + confirmButtonText: '重新登录',
  90 + cancelButtonText: '取消',
  91 + type: 'warning'
  92 + })
  93 + .then(() => {
  94 + isRelogin.show = false
  95 + useUserStore()
  96 + .logOut()
  97 + .then(() => {
  98 + console.log('重新登录')
  99 + location.href = '/login'
  100 + })
  101 + })
  102 + .catch(() => {
  103 + isRelogin.show = false
  104 + })
  105 + }
  106 + return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
  107 + } else if (code === 500) {
  108 + ElMessage({ message: msg, type: 'error' })
  109 + return Promise.reject(new Error(msg))
  110 + } else if (code === 601) {
  111 + ElMessage({ message: msg, type: 'warning' })
  112 + return Promise.reject(new Error(msg))
  113 + } else if (code !== 200) {
  114 + ElNotification.error({ title: msg })
  115 + return Promise.reject('error')
  116 + } else {
  117 + return Promise.resolve(res.data)
  118 + }
  119 + },
  120 + (error) => {
  121 + console.log('err' + error)
  122 + let { message } = error
  123 + if (message == 'Network Error') {
  124 + message = '后端接口连接异常'
  125 + } else if (message.includes('timeout')) {
  126 + message = '系统接口请求超时'
  127 + } else if (message.includes('Request failed with status code')) {
  128 + message = '系统接口' + message.substr(message.length - 3) + '异常'
  129 + }
  130 + ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
  131 + return Promise.reject(error)
  132 + }
  133 +)
  134 +
  135 +// 通用下载方法
  136 +// export function download(url, params, filename, config) {
  137 +// downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
  138 +// return service.post(url, params, {
  139 +// transformRequest: [(params) => { return tansParams(params) }],
  140 +// headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  141 +// responseType: 'blob',
  142 +// ...config
  143 +// }).then(async (data) => {
  144 +// const isLogin = await blobValidate(data);
  145 +// if (isLogin) {
  146 +// const blob = new Blob([data])
  147 +// saveAs(blob, filename)
  148 +// } else {
  149 +// const resText = await data.text();
  150 +// const rspObj = JSON.parse(resText);
  151 +// const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
  152 +// ElMessage.error(errMsg);
  153 +// }
  154 +// downloadLoadingInstance.close();
  155 +// }).catch((r) => {
  156 +// console.error(r)
  157 +// ElMessage.error('下载文件出现错误,请联系管理员!')
  158 +// downloadLoadingInstance.close();
  159 +// })
  160 +// }
  161 +
  162 +export default service
  1 +/**
  2 + * 通用js方法封装处理
  3 + * Copyright (c) 2019 ruoyi
  4 + */
  5 +
  6 +// 日期格式化
  7 +export function parseTime(time, pattern) {
  8 + if (arguments.length === 0 || !time) {
  9 + return null
  10 + }
  11 + const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
  12 + let date
  13 + if (typeof time === 'object') {
  14 + date = time
  15 + } else {
  16 + if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
  17 + time = parseInt(time)
  18 + } else if (typeof time === 'string') {
  19 + time = time
  20 + .replace(new RegExp(/-/gm), '/')
  21 + .replace('T', ' ')
  22 + .replace(new RegExp(/\.[\d]{3}/gm), '')
  23 + }
  24 + if (typeof time === 'number' && time.toString().length === 10) {
  25 + time = time * 1000
  26 + }
  27 + date = new Date(time)
  28 + }
  29 + const formatObj = {
  30 + y: date.getFullYear(),
  31 + m: date.getMonth() + 1,
  32 + d: date.getDate(),
  33 + h: date.getHours(),
  34 + i: date.getMinutes(),
  35 + s: date.getSeconds(),
  36 + a: date.getDay()
  37 + }
  38 + const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
  39 + let value = formatObj[key]
  40 + // Note: getDay() returns 0 on Sunday
  41 + if (key === 'a') {
  42 + return ['日', '一', '二', '三', '四', '五', '六'][value]
  43 + }
  44 + if (result.length > 0 && value < 10) {
  45 + value = '0' + value
  46 + }
  47 + return value || 0
  48 + })
  49 + return time_str
  50 +}
  51 +
  52 +// 表单重置
  53 +export function resetForm(refName) {
  54 + if (this.$refs[refName]) {
  55 + this.$refs[refName].resetFields()
  56 + }
  57 +}
  58 +
  59 +// 添加日期范围
  60 +export function addDateRange(params, dateRange, propName) {
  61 + let search = params
  62 + search.params =
  63 + typeof search.params === 'object' && search.params !== null && !Array.isArray(search.params)
  64 + ? search.params
  65 + : {}
  66 + dateRange = Array.isArray(dateRange) ? dateRange : []
  67 + if (typeof propName === 'undefined') {
  68 + search.params['beginTime'] = dateRange[0]
  69 + search.params['endTime'] = dateRange[1]
  70 + } else {
  71 + search.params['begin' + propName] = dateRange[0]
  72 + search.params['end' + propName] = dateRange[1]
  73 + }
  74 + return search
  75 +}
  76 +
  77 +// 回显数据字典
  78 +export function selectDictLabel(datas, value) {
  79 + if (value === undefined) {
  80 + return ''
  81 + }
  82 + var actions = []
  83 + Object.keys(datas).some((key) => {
  84 + if (datas[key].value == '' + value) {
  85 + actions.push(datas[key].label)
  86 + return true
  87 + }
  88 + })
  89 + if (actions.length === 0) {
  90 + actions.push(value)
  91 + }
  92 + return actions.join('')
  93 +}
  94 +
  95 +// 回显数据字典(字符串数组)
  96 +export function selectDictLabels(datas, value, separator) {
  97 + if (value === undefined || value.length === 0) {
  98 + return ''
  99 + }
  100 + if (Array.isArray(value)) {
  101 + value = value.join(',')
  102 + }
  103 + var actions = []
  104 + var currentSeparator = undefined === separator ? ',' : separator
  105 + var temp = value.split(currentSeparator)
  106 + Object.keys(value.split(currentSeparator)).some((val) => {
  107 + var match = false
  108 + Object.keys(datas).some((key) => {
  109 + if (datas[key].value == '' + temp[val]) {
  110 + actions.push(datas[key].label + currentSeparator)
  111 + match = true
  112 + }
  113 + })
  114 + if (!match) {
  115 + actions.push(temp[val] + currentSeparator)
  116 + }
  117 + })
  118 + return actions.join('').substring(0, actions.join('').length - 1)
  119 +}
  120 +
  121 +// 字符串格式化(%s )
  122 +export function sprintf(str) {
  123 + var args = arguments,
  124 + flag = true,
  125 + i = 1
  126 + str = str.replace(/%s/g, function () {
  127 + var arg = args[i++]
  128 + if (typeof arg === 'undefined') {
  129 + flag = false
  130 + return ''
  131 + }
  132 + return arg
  133 + })
  134 + return flag ? str : ''
  135 +}
  136 +
  137 +// 转换字符串,undefined,null等转化为""
  138 +export function parseStrEmpty(str) {
  139 + if (!str || str == 'undefined' || str == 'null') {
  140 + return ''
  141 + }
  142 + return str
  143 +}
  144 +
  145 +// 数据合并
  146 +export function mergeRecursive(source, target) {
  147 + for (var p in target) {
  148 + try {
  149 + if (target[p].constructor == Object) {
  150 + source[p] = mergeRecursive(source[p], target[p])
  151 + } else {
  152 + source[p] = target[p]
  153 + }
  154 + } catch (e) {
  155 + source[p] = target[p]
  156 + }
  157 + }
  158 + return source
  159 +}
  160 +
  161 +/**
  162 + * 构造树型结构数据
  163 + * @param {*} data 数据源
  164 + * @param {*} id id字段 默认 'id'
  165 + * @param {*} parentId 父节点字段 默认 'parentId'
  166 + * @param {*} children 孩子节点字段 默认 'children'
  167 + */
  168 +export function handleTree(data, id, parentId, children) {
  169 + let config = {
  170 + id: id || 'id',
  171 + parentId: parentId || 'parentId',
  172 + childrenList: children || 'children'
  173 + }
  174 +
  175 + var childrenListMap = {}
  176 + var nodeIds = {}
  177 + var tree = []
  178 +
  179 + for (let d of data) {
  180 + let parentId = d[config.parentId]
  181 + if (childrenListMap[parentId] == null) {
  182 + childrenListMap[parentId] = []
  183 + }
  184 + nodeIds[d[config.id]] = d
  185 + childrenListMap[parentId].push(d)
  186 + }
  187 +
  188 + for (let d of data) {
  189 + let parentId = d[config.parentId]
  190 + if (nodeIds[parentId] == null) {
  191 + tree.push(d)
  192 + }
  193 + }
  194 +
  195 + for (let t of tree) {
  196 + adaptToChildrenList(t)
  197 + }
  198 +
  199 + function adaptToChildrenList(o) {
  200 + if (childrenListMap[o[config.id]] !== null) {
  201 + o[config.childrenList] = childrenListMap[o[config.id]]
  202 + }
  203 + if (o[config.childrenList]) {
  204 + for (let c of o[config.childrenList]) {
  205 + adaptToChildrenList(c)
  206 + }
  207 + }
  208 + }
  209 + return tree
  210 +}
  211 +
  212 +/**
  213 + * 参数处理
  214 + * @param {*} params 参数
  215 + */
  216 +export function tansParams(params) {
  217 + let result = ''
  218 + for (const propName of Object.keys(params)) {
  219 + const value = params[propName]
  220 + var part = encodeURIComponent(propName) + '='
  221 + if (value !== null && value !== '' && typeof value !== 'undefined') {
  222 + if (typeof value === 'object') {
  223 + for (const key of Object.keys(value)) {
  224 + if (value[key] !== null && value[key] !== '' && typeof value[key] !== 'undefined') {
  225 + let params = propName + '[' + key + ']'
  226 + var subPart = encodeURIComponent(params) + '='
  227 + result += subPart + encodeURIComponent(value[key]) + '&'
  228 + }
  229 + }
  230 + } else {
  231 + result += part + encodeURIComponent(value) + '&'
  232 + }
  233 + }
  234 + }
  235 + return result
  236 +}
  237 +
  238 +// 返回项目路径
  239 +export function getNormalPath(p) {
  240 + if (p.length === 0 || !p || p == 'undefined') {
  241 + return p
  242 + }
  243 + let res = p.replace('//', '/')
  244 + if (res[res.length - 1] === '/') {
  245 + return res.slice(0, res.length - 1)
  246 + }
  247 + return res
  248 +}
  249 +
  250 +// 验证是否为blob格式
  251 +export async function blobValidate(data) {
  252 + try {
  253 + const text = await data.text()
  254 + JSON.parse(text)
  255 + return false
  256 + } catch (error) {
  257 + return true
  258 + }
  259 +}
  1 +Math.easeInOutQuad = function(t, b, c, d) {
  2 + t /= d / 2
  3 + if (t < 1) {
  4 + return c / 2 * t * t + b
  5 + }
  6 + t--
  7 + return -c / 2 * (t * (t - 2) - 1) + b
  8 +}
  9 +
  10 +// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
  11 +var requestAnimFrame = (function() {
  12 + return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
  13 +})()
  14 +
  15 +/**
  16 + * Because it's so fucking difficult to detect the scrolling element, just move them all
  17 + * @param {number} amount
  18 + */
  19 +function move(amount) {
  20 + document.documentElement.scrollTop = amount
  21 + document.body.parentNode.scrollTop = amount
  22 + document.body.scrollTop = amount
  23 +}
  24 +
  25 +function position() {
  26 + return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
  27 +}
  28 +
  29 +/**
  30 + * @param {number} to
  31 + * @param {number} duration
  32 + * @param {Function} callback
  33 + */
  34 +export function scrollTo(to, duration, callback) {
  35 + const start = position()
  36 + const change = to - start
  37 + const increment = 20
  38 + let currentTime = 0
  39 + duration = (typeof (duration) === 'undefined') ? 500 : duration
  40 + var animateScroll = function() {
  41 + // increment the time
  42 + currentTime += increment
  43 + // find the value with the quadratic in-out easing function
  44 + var val = Math.easeInOutQuad(currentTime, start, change, duration)
  45 + // move the document.body
  46 + move(val)
  47 + // do the animation unless its over
  48 + if (currentTime < duration) {
  49 + requestAnimFrame(animateScroll)
  50 + } else {
  51 + if (callback && typeof (callback) === 'function') {
  52 + // the animation is done so lets callback
  53 + callback()
  54 + }
  55 + }
  56 + }
  57 + animateScroll()
  58 +}
  1 +// 处理主题样式
  2 +export function handleThemeStyle(theme) {
  3 + document.documentElement.style.setProperty('--el-color-primary', theme)
  4 + for (let i = 1; i <= 9; i++) {
  5 + document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(theme, i / 10)}`)
  6 + }
  7 + for (let i = 1; i <= 9; i++) {
  8 + document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(theme, i / 10)}`)
  9 + }
  10 +}
  11 +
  12 +// hex颜色转rgb颜色
  13 +export function hexToRgb(str) {
  14 + str = str.replace('#', '')
  15 + let hexs = str.match(/../g)
  16 + for (let i = 0; i < 3; i++) {
  17 + hexs[i] = parseInt(hexs[i], 16)
  18 + }
  19 + return hexs
  20 +}
  21 +
  22 +// rgb颜色转Hex颜色
  23 +export function rgbToHex(r, g, b) {
  24 + let hexs = [r.toString(16), g.toString(16), b.toString(16)]
  25 + for (let i = 0; i < 3; i++) {
  26 + if (hexs[i].length == 1) {
  27 + hexs[i] = `0${hexs[i]}`
  28 + }
  29 + }
  30 + return `#${hexs.join('')}`
  31 +}
  32 +
  33 +// 变浅颜色值
  34 +export function getLightColor(color, level) {
  35 + let rgb = hexToRgb(color)
  36 + for (let i = 0; i < 3; i++) {
  37 + rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i])
  38 + }
  39 + return rgbToHex(rgb[0], rgb[1], rgb[2])
  40 +}
  41 +
  42 +// 变深颜色值
  43 +export function getDarkColor(color, level) {
  44 + let rgb = hexToRgb(color)
  45 + for (let i = 0; i < 3; i++) {
  46 + rgb[i] = Math.floor(rgb[i] * (1 - level))
  47 + }
  48 + return rgbToHex(rgb[0], rgb[1], rgb[2])
  49 +}
  1 +/**
  2 + * 判断url是否是http或https
  3 + * @param {string} path
  4 + * @returns {Boolean}
  5 + */
  6 + export function isHttp(url) {
  7 + return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
  8 +}
  9 +
  10 +/**
  11 + * 判断path是否为外链
  12 + * @param {string} path
  13 + * @returns {Boolean}
  14 + */
  15 + export function isExternal(path) {
  16 + return /^(https?:|mailto:|tel:)/.test(path)
  17 +}
  18 +
  19 +/**
  20 + * @param {string} str
  21 + * @returns {Boolean}
  22 + */
  23 +export function validUsername(str) {
  24 + const valid_map = ['admin', 'editor']
  25 + return valid_map.indexOf(str.trim()) >= 0
  26 +}
  27 +
  28 +/**
  29 + * @param {string} url
  30 + * @returns {Boolean}
  31 + */
  32 +export function validURL(url) {
  33 + 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.,?'\\+&%$#=~_-]+))*$/
  34 + return reg.test(url)
  35 +}
  36 +
  37 +/**
  38 + * @param {string} str
  39 + * @returns {Boolean}
  40 + */
  41 +export function validLowerCase(str) {
  42 + const reg = /^[a-z]+$/
  43 + return reg.test(str)
  44 +}
  45 +
  46 +/**
  47 + * @param {string} str
  48 + * @returns {Boolean}
  49 + */
  50 +export function validUpperCase(str) {
  51 + const reg = /^[A-Z]+$/
  52 + return reg.test(str)
  53 +}
  54 +
  55 +/**
  56 + * @param {string} str
  57 + * @returns {Boolean}
  58 + */
  59 +export function validAlphabets(str) {
  60 + const reg = /^[A-Za-z]+$/
  61 + return reg.test(str)
  62 +}
  63 +
  64 +/**
  65 + * @param {string} email
  66 + * @returns {Boolean}
  67 + */
  68 +export function validEmail(email) {
  69 + 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,}))$/
  70 + return reg.test(email)
  71 +}
  72 +
  73 +/**
  74 + * @param {string} str
  75 + * @returns {Boolean}
  76 + */
  77 +export function isString(str) {
  78 + if (typeof str === 'string' || str instanceof String) {
  79 + return true
  80 + }
  81 + return false
  82 +}
  83 +
  84 +/**
  85 + * @param {Array} arg
  86 + * @returns {Boolean}
  87 + */
  88 +export function isArray(arg) {
  89 + if (typeof Array.isArray === 'undefined') {
  90 + return Object.prototype.toString.call(arg) === '[object Array]'
  91 + }
  92 + return Array.isArray(arg)
  93 +}
不能预览此文件类型
  1 +<template>
  2 + <div
  3 + class="photo-container"
  4 + :style="{ backgroundImage: `url(${bgImgUrl})`, backgroundSize: 'cover' }"
  5 + @click="handleOther"
  6 + >
  7 + <!-- 相册展开大小 容器 -->
  8 + <div id="dragBox">
  9 + <div
  10 + v-for="(item, index) in spanBoxList"
  11 + :key="index"
  12 + class="spinBox"
  13 + :id="`spinBox${index + 1}`"
  14 + >
  15 + <img
  16 + v-for="(childen, number) in item"
  17 + :src="childen"
  18 + @click.stop="handleClick($event, childen)"
  19 + />
  20 + </div>
  21 + <div v-show="closeImg" class="fixed-img" :style="{ opacity: fixedOpacity }">
  22 + <img :src="fixedImg" />
  23 + </div>
  24 + </div>
  25 + </div>
  26 +
  27 + <!-- 翻页按钮 -->
  28 + <button
  29 + v-show="queryParams.pageNum > 1"
  30 + type="button"
  31 + class="slick-prev slick-arrow slick-prev-button"
  32 + @click="PreviousPage"
  33 + >
  34 + Previous
  35 + </button>
  36 + <button type="button" class="slick-next slick-arrow slick-next-button" @click="NextPage">
  37 + Next
  38 + </button>
  39 +</template>
  40 +
  41 +<script setup>
  42 +import { ref, onMounted, onUnmounted } from 'vue'
  43 +import { ElMessage, ElMessageBox } from 'element-plus'
  44 +import { getSignatureList, getUseBg } from '@/api/signature'
  45 +let imgList = import.meta.glob('../assets/img/*.*', { eager: true })
  46 +let imgArr = Object.values(imgList).map((item) => item.default)
  47 +let spanBoxList = ref([])
  48 +let aEls = ref([])
  49 +let radius = ref(760)
  50 +let bgImgUrl = ref('')
  51 +const closeImg = ref(false) // 是否关闭中间图片
  52 +let outDom = ref(null)
  53 +let startX = ref(0)
  54 +let startY = ref(0)
  55 +let endX = ref(0)
  56 +let endY = ref(0)
  57 +let tX = ref(0)
  58 +let tY = ref(30)
  59 +let desX = ref(0)
  60 +let desY = ref(0)
  61 +const queryParams = ref({
  62 + pageNum: 1,
  63 + pageSize: 60
  64 +})
  65 +const server_base = localStorage.getItem('crgx_server')
  66 +const fixedImg = ref('')
  67 +const fixedOpacity = ref(0)
  68 +function randomGroupWithRepeats(arr, groupCount, groupSize) {
  69 + // 参数校验
  70 + if (!Array.isArray(arr) || arr.length === 0) {
  71 + throw new Error('输入必须是非空数组')
  72 + }
  73 + if (typeof groupCount !== 'number' || groupCount <= 0) {
  74 + throw new Error('分组数必须是正整数')
  75 + }
  76 + if (typeof groupSize !== 'number' || groupSize <= 0) {
  77 + throw new Error('每组大小必须是正整数')
  78 + }
  79 +
  80 + const result = []
  81 +
  82 + for (let i = 0; i < groupCount; i++) {
  83 + const group = []
  84 + for (let j = 0; j < groupSize; j++) {
  85 + // 随机选择元素(允许重复)
  86 + const randomIndex = Math.floor(Math.random() * arr.length)
  87 + group.push(arr[randomIndex])
  88 + }
  89 + result.push(group)
  90 + }
  91 +
  92 + return result
  93 +}
  94 +spanBoxList.value = randomGroupWithRepeats(imgArr, 3, 20)
  95 +
  96 +// 设置样式
  97 +function setStyle(delayTime, dom, i, len) {
  98 + //给元素加动画 展开
  99 + dom.style.transform = 'rotateY(' + i * (360 / len) + 'deg) translateZ(' + radius.value + 'px)'
  100 + dom.style.transition = 'all 1s'
  101 + dom.style.opacity = 1
  102 + dom.style.transitionDelay = delayTime || (len - i) / 4 + 's'
  103 +}
  104 +// 初始化每一层图片动画
  105 +function init(delayTime) {
  106 + closeImg.value = false
  107 + for (let i = 0; i < aEls.value.length; i++) {
  108 + for (let j = 0; j < aEls.value[i].length; j++) {
  109 + setStyle(delayTime, aEls.value[i][j], j, aEls.value[i].length)
  110 + }
  111 + }
  112 +}
  113 +// 获取图片
  114 +function getSpinBoxDom() {
  115 + for (let i = 0; i < spanBoxList.value.length; i++) {
  116 + let spinDom = document.getElementById(`spinBox${i + 1}`)
  117 + let aImg = spinDom.getElementsByTagName('img')
  118 + aEls.value.push(aImg)
  119 + }
  120 +}
  121 +// 改变整体的旋转角度
  122 +function changeRotate(obj) {
  123 + // X轴旋转0-180度
  124 + if (tY.value > 180) tY.value = 180
  125 + if (tY.value < 0) tY.value = 0
  126 + // y轴旋转角度不限制
  127 + obj.style.transform = 'rotateX(' + -tY.value + 'deg) rotateY(' + tX.value + 'deg)'
  128 +}
  129 +// 点击图片展示
  130 +const handleClick = (event, childen) => {
  131 + init(1)
  132 + fixedImg.value = childen
  133 + event.target.style.transform = 'rotateY(0deg) translateZ(0px) scale(3)'
  134 + event.target.style.opacity = 0
  135 + fixedOpacity.value = 1
  136 + closeImg.value = true
  137 +}
  138 +// 点击空白处关闭中间图片并恢复初始
  139 +const handleOther = () => {
  140 + if (closeImg.value) {
  141 + radius.value = 760
  142 + init(1)
  143 + }
  144 +}
  145 +// 获取图片列表
  146 +const getImgList = async () => {
  147 + const signRes = await getSignatureList(queryParams.value)
  148 + const imgList = signRes.rows?.map((item) => {
  149 + return `${server_base}${item.path}`
  150 + })
  151 + spanBoxList.value = randomGroupWithRepeats(imgList, 3, 20)
  152 +}
  153 +// 上一页
  154 +function PreviousPage() {
  155 + queryParams.value.pageNum--
  156 + getImgList()
  157 +}
  158 +// 下一页
  159 +function NextPage() {
  160 + queryParams.value.pageNum++
  161 + getImgList()
  162 +}
  163 +// 鼠标滚动函数
  164 +const handleMouseWheel = (e) => {
  165 + e || e.window.event
  166 + let d = e.wheelDelta / 20 || -e.detail
  167 + radius.value += d // 旋转半径
  168 + init(1)
  169 +}
  170 +
  171 +// 是否打开填写服务器地址
  172 +const openServer = () => {
  173 + const server = localStorage.getItem('crgx_server')
  174 + ElMessageBox.prompt('请填写服务器地址以便正常使用', '提示', {
  175 + confirmButtonText: '确认',
  176 + cancelButtonText: '取消',
  177 + inputValue: server,
  178 + inputPattern: /^http/i,
  179 + inputErrorMessage: '请正常填写地址'
  180 + })
  181 + .then(({ value }) => {
  182 + localStorage.setItem('crgx_server', value)
  183 + ElMessage({
  184 + type: 'success',
  185 + message: `填写完成,即将刷新页面`
  186 + })
  187 + window.location.reload()
  188 + })
  189 + .catch(() => {})
  190 +}
  191 +
  192 +onMounted(async () => {
  193 + if (!localStorage.getItem('crgx_server')) {
  194 + return openServer(true)
  195 + }
  196 +
  197 + const signRes = await getSignatureList()
  198 + const bgRes = await getUseBg()
  199 + bgImgUrl.value = server_base + bgRes.data.path
  200 + const imgList = signRes.rows?.map((item) => {
  201 + return `${server_base}${item.path}`
  202 + })
  203 + spanBoxList.value = randomGroupWithRepeats(imgList, 3, 20)
  204 + let spinDom1 = document.getElementById('spinBox1')
  205 + getSpinBoxDom()
  206 + //相册容器
  207 + outDom.value = document.getElementById('dragBox')
  208 + // 开始旋转动画
  209 + setTimeout(() => {
  210 + init()
  211 + }, 100)
  212 + //鼠标滚动事件
  213 + document.addEventListener('mousewheel', handleMouseWheel)
  214 + //暂停开始旋转
  215 + function playSpin(yes) {
  216 + spinDom1.style.animationPlayState = yes ? 'running' : 'paused'
  217 + }
  218 +
  219 + //鼠标移动事件
  220 + document.onpointerdown = function (e) {
  221 + //清除惯性定时器
  222 + clearInterval(outDom.value.timer)
  223 + e = e || ewindow.event
  224 + //鼠标点击位置
  225 + ;(startX.value = e.clientX), (startY.value = e.clientY)
  226 + this.onpointermove = function (e) {
  227 + playSpin(false)
  228 + //鼠标点击时 停止自动旋转//鼠标点击时 停止自动旋转
  229 + e = e || window.event
  230 + //记录结束时位置
  231 + ;(endX.value = e.clientX), (endY.value = e.clientY)
  232 + //计算移动距离 并修改角度
  233 + desX.value = endX.value - startX.value
  234 + desY.value = endY.value - startY.value
  235 + tX.value += desX.value * 0.1
  236 + tY.value += desY.value * 0.1
  237 + changeRotate(outDom.value)
  238 + startX.value = endX.value
  239 + startY.value = endY.value
  240 + }
  241 + //鼠标离开时 开始自动旋转
  242 + this.onpointerup = function (e) {
  243 + //惯性旋转
  244 + outDom.value.timer = setInterval(function () {
  245 + desX.value *= 0.95
  246 + desY.value *= 0.95
  247 + tX.value += desX.value * 0.1
  248 + tY.value += desY.value * 0.1
  249 + changeRotate(outDom.value)
  250 + playSpin(false)
  251 + if (Math.abs(desX.value) < 0.5 && Math.abs(desY.value) < 0.5) {
  252 + clearInterval(outDom.value.timer)
  253 + playSpin(true)
  254 + }
  255 + })
  256 + this.onpointermove = this.onpointerup = null
  257 + }
  258 + return false
  259 + }
  260 +})
  261 +
  262 +onUnmounted(() => {
  263 + document.removeEventListener('mousewheel', handleMouseWheel)
  264 +})
  265 +</script>
  266 +
  267 +<style scoped lang="scss">
  268 +@use 'sass:math'; // 引入 math 模块
  269 +.photo-container {
  270 + min-height: 100vh;
  271 + touch-action: none;
  272 + overflow: hidden;
  273 + display: flex;
  274 + perspective: 1500px;
  275 + background: #111;
  276 +}
  277 +* {
  278 + margin: 0;
  279 + padding: 0;
  280 +}
  281 +
  282 +/* perspective指定了观察者与 Z=9 平面的距离,使具有三维位置变换的元素产生透视效果。 */
  283 +@property --d {
  284 + syntax: '<angle>';
  285 + inherits: true;
  286 + initial-value: 0deg;
  287 +}
  288 +.moveAni {
  289 + transform: rotateY(0deg) translateZ(0px) !important;
  290 +}
  291 +
  292 +#dragBox,
  293 +.spinBox {
  294 + position: relative;
  295 + display: flex;
  296 + margin: auto;
  297 + transform-style: preserve-3d;
  298 + transform: rotateX(-10deg);
  299 +}
  300 +
  301 +.fixed-img {
  302 + position: fixed;
  303 + top: 0;
  304 + background-color: #fff;
  305 + width: 180px;
  306 + height: 240px;
  307 + opacity: 0;
  308 + transform: scale(3) rotateY(180deg);
  309 + transition: all 0.5s ease-in-out;
  310 + img {
  311 + transform: rotateY(180deg);
  312 + }
  313 +}
  314 +
  315 +#dragBox {
  316 + transform: rotateX(-30deg);
  317 +}
  318 +
  319 +.spinBox {
  320 + width: 180px;
  321 + height: 240px;
  322 + animation: spin 100s infinite linear;
  323 +}
  324 +
  325 +@for $i from 2 through 7 {
  326 + $top-value: if($i % 2 == 0, 120% * math.div($i, 2), -120% * math.div($i - 1, 2));
  327 +
  328 + #spinBox#{$i} {
  329 + position: absolute;
  330 + top: $top-value;
  331 + }
  332 +}
  333 +
  334 +#dragBox img {
  335 + transform-style: preserve-3d;
  336 + position: absolute;
  337 + left: 0;
  338 + top: 0;
  339 + width: 100%;
  340 + height: 100%;
  341 + background: #fff;
  342 + box-shadow: 0 0 8px #fff;
  343 + object-fit: contain;
  344 + /*倒影 */
  345 + --webkit-box-reflect: below 10px linear-gradient(transparent, transparent, #0005);
  346 +}
  347 +
  348 +#dragBox img:hover {
  349 + box-shadow: 0 0 15px #fff;
  350 +}
  351 +
  352 +/*自动旋转 */
  353 +@keyframes spin {
  354 + from {
  355 + transform: rotateY(0deg);
  356 + }
  357 +
  358 + to {
  359 + transform: rotateY(360deg);
  360 + }
  361 +}
  362 +
  363 +.slick-prev-button,
  364 +.slick-next-button {
  365 + position: absolute;
  366 + top: 50%;
  367 + bottom: auto;
  368 + z-index: 20;
  369 + margin-top: -2rem;
  370 + height: 4rem;
  371 + width: 4rem;
  372 + cursor: pointer;
  373 + border-radius: 50%;
  374 + border-style: none;
  375 + background-color: rgb(0 0 23 / 0.1);
  376 + padding: 0px;
  377 + font-size: 0;
  378 + color: transparent;
  379 + outline: 2px solid transparent;
  380 + outline-offset: 2px;
  381 +}
  382 +.slick-prev-button {
  383 + left: 2.5rem;
  384 +}
  385 +.slick-next-button {
  386 + right: 2.5rem;
  387 +}
  388 +
  389 +.slick-prev:before {
  390 + content: '';
  391 + width: 100%;
  392 + height: 100%;
  393 + background: url('https://www.guet.edu.cn/_upload/tpl/01/39/313/template313/images/leftarrow.png')
  394 + center no-repeat;
  395 +}
  396 +
  397 +.slick-next:before {
  398 + content: '';
  399 + width: 100%;
  400 + height: 100%;
  401 + background: url('https://www.guet.edu.cn/_upload/tpl/01/39/313/template313/images/rightarrow.png')
  402 + center no-repeat;
  403 +}
  404 +
  405 +.slick-arrow:before {
  406 + display: block;
  407 + opacity: 0.3;
  408 + transition: 0.4s;
  409 +}
  410 +
  411 +.slick-arrow:hover::before {
  412 + opacity: 1;
  413 +}
  414 +</style>