之前调整
This commit is contained in:
parent
086cf94ae5
commit
2198380c9f
4
.env
4
.env
@ -9,8 +9,8 @@ VITE_PORT = 3006
|
|||||||
VITE_BASE_URL = /
|
VITE_BASE_URL = /
|
||||||
|
|
||||||
# API 地址前缀
|
# API 地址前缀
|
||||||
VITE_API_URL = http://192.168.110.7:25928
|
# VITE_API_URL = http://219.157.255.213:25832
|
||||||
# VITE_API_URL = https://bsh.yw.server.hnzhwlkj.cn
|
VITE_API_URL = https://bsh.yw.server.hnzhwlkj.cn
|
||||||
|
|
||||||
# 权限模式( frontend | backend )
|
# 权限模式( frontend | backend )
|
||||||
VITE_ACCESS_MODE = frontend
|
VITE_ACCESS_MODE = frontend
|
||||||
|
|||||||
1
dist/assets/index-5Q9YfFGs.css
vendored
Normal file
1
dist/assets/index-5Q9YfFGs.css
vendored
Normal file
File diff suppressed because one or more lines are too long
63
dist/assets/index-CsXt3cEk.js
vendored
Normal file
63
dist/assets/index-CsXt3cEk.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/index-CsXt3cEk.js.map
vendored
Normal file
1
dist/assets/index-CsXt3cEk.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
63
dist/assets/index-DFqZsSAt.js
vendored
63
dist/assets/index-DFqZsSAt.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-DFqZsSAt.js.map
vendored
1
dist/assets/index-DFqZsSAt.js.map
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-DagmUA4t.css
vendored
1
dist/assets/index-DagmUA4t.css
vendored
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
<title>数据看板</title>
|
<title>数据看板</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DFqZsSAt.js"></script>
|
<script type="module" crossorigin src="/assets/index-CsXt3cEk.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DagmUA4t.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-5Q9YfFGs.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
22
src/App.vue
22
src/App.vue
@ -3,6 +3,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
// 检测是否是移动端
|
||||||
|
function isMobile() {
|
||||||
|
// 方法1: 检测用户代理
|
||||||
|
const userAgent = navigator.userAgent || navigator.vendor || window.opera
|
||||||
|
const mobileRegex = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i
|
||||||
|
const isMobileByUA = mobileRegex.test(userAgent.toLowerCase())
|
||||||
|
|
||||||
|
// 方法2: 检测屏幕宽度
|
||||||
|
const isMobileByWidth = window.innerWidth <= 768
|
||||||
|
|
||||||
|
return isMobileByUA || isMobileByWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局移动端检测和跳转
|
||||||
|
onMounted(() => {
|
||||||
|
if (isMobile()) {
|
||||||
|
// 如果是移动端,跳转到移动端地址
|
||||||
|
window.location.href = 'https://bsh.yw.m.hnzhwlkj.cn/'
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@ -40,3 +40,6 @@ export function resetPassword(payload){
|
|||||||
export function uploadImage(payload){
|
export function uploadImage(payload){
|
||||||
return request.post('/upload/image', payload)
|
return request.post('/upload/image', payload)
|
||||||
}
|
}
|
||||||
|
export function getconfig(params){
|
||||||
|
return request.get('/common/config/find',{params})
|
||||||
|
}
|
||||||
BIN
src/assets/logopc.png
Normal file
BIN
src/assets/logopc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/assets/macgroup.png
Normal file
BIN
src/assets/macgroup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 772 B |
Binary file not shown.
|
Before Width: | Height: | Size: 951 B After Width: | Height: | Size: 1.1 KiB |
@ -20,21 +20,58 @@
|
|||||||
<!-- 左侧导航 -->
|
<!-- 左侧导航 -->
|
||||||
<aside :class="['sidebar', { open: sidebarOpen }]">
|
<aside :class="['sidebar', { open: sidebarOpen }]">
|
||||||
<ul>
|
<ul>
|
||||||
|
<template v-for="(item, index) in menus" :key="item.id">
|
||||||
<li
|
<li
|
||||||
v-for="(item, index) in menus"
|
@click.stop="toggleSubMenu(item, index)"
|
||||||
:key="item.id"
|
|
||||||
@click="onNav(item.id, index)"
|
|
||||||
:class="{ active: activeMenu === index }"
|
:class="{ active: activeMenu === index }"
|
||||||
|
class="parent-menu"
|
||||||
>
|
>
|
||||||
<i class="iconfont-sys" v-html="item.icon" style="margin-right: 10px;"></i>{{ item.name }}
|
<span class="menu-content">
|
||||||
|
<i class="iconfont-sys" v-html="item.icon" style="margin-right: 10px;"></i>
|
||||||
|
{{ item.name }}
|
||||||
|
</span>
|
||||||
|
<i
|
||||||
|
v-if="item.children && item.children.length > 0"
|
||||||
|
class="arrow-icon"
|
||||||
|
:class="{ expanded: expandedMenus.has(item.id) }"
|
||||||
|
>▼</i>
|
||||||
</li>
|
</li>
|
||||||
|
<!-- 子菜单 -->
|
||||||
|
<transition name="submenu">
|
||||||
|
<ul v-if="item.children && item.children.length > 0 && expandedMenus.has(item.id)" class="submenu">
|
||||||
|
<li
|
||||||
|
v-for="children in item.children"
|
||||||
|
:key="children.id"
|
||||||
|
@click.stop="onNav(children.id, index, children.id)"
|
||||||
|
:class="['submenu-item', { active: activeChildId === children.id }]"
|
||||||
|
>
|
||||||
|
{{ children.name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
<div v-if="sidebarOpen" class="drawer-overlay" @click="sidebarOpen = false"></div>
|
<div v-if="sidebarOpen" class="drawer-overlay" @click="sidebarOpen = false"></div>
|
||||||
|
|
||||||
<!-- 右侧内容 -->
|
<!-- 右侧内容 -->
|
||||||
<main class="content">
|
<main class="content">
|
||||||
<section v-for="m in menus" :key="m.id" :id="m.id" class="section">
|
<!-- 看板 banner -->
|
||||||
|
<div v-if="bannerImages.length" class="banner">
|
||||||
|
<template v-if="bannerImages.length === 1">
|
||||||
|
<img :src="bannerImages[0]" alt="banner" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-carousel height="220px" indicator-position="outside" class="banner-carousel">
|
||||||
|
<el-carousel-item v-for="(img, idx) in bannerImages" :key="`${img}-${idx}`">
|
||||||
|
<img :src="img" alt="banner" />
|
||||||
|
</el-carousel-item>
|
||||||
|
</el-carousel>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<template v-for="m in menus" :key="m.id">
|
||||||
|
<!-- 父级 section -->
|
||||||
|
<section :id="m.id" class="section" v-if="m.cards && m.cards.length > 0">
|
||||||
<h3>{{ m.name }}</h3>
|
<h3>{{ m.name }}</h3>
|
||||||
<div class="title-block"></div>
|
<div class="title-block"></div>
|
||||||
<div class="card-grid">
|
<div class="card-grid">
|
||||||
@ -49,6 +86,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<!-- 子级 section -->
|
||||||
|
<template v-for="children in m.processedChildren" :key="children.id">
|
||||||
|
<section :id="children.id" class="section" v-if="children.cards && children.cards.length > 0">
|
||||||
|
<h3>{{ children.parentName }} / {{ children.name }}</h3>
|
||||||
|
<div class="title-block"></div>
|
||||||
|
<div class="card-grid">
|
||||||
|
<div class="card" v-for="(item, index) in children.cards" :key="index" @click="onCardClick(item)">
|
||||||
|
<div class="icon">
|
||||||
|
<img src="/src/assets/card-icon.png" alt="icon" />
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<div class="title">{{ item.title }}</div>
|
||||||
|
<div class="desc">{{ item.description }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- 修改资料弹窗 -->
|
<!-- 修改资料弹窗 -->
|
||||||
@ -151,12 +207,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片预览 -->
|
||||||
|
<div v-if="imagePreview.visible" class="image-preview-overlay" @click.self="closeImagePreview">
|
||||||
|
<div class="image-preview-box">
|
||||||
|
<div class="preview-header">
|
||||||
|
<span>{{ imagePreview.title }}</span>
|
||||||
|
<div class="preview-actions">
|
||||||
|
<button class="preview-btn" @click="downloadCurrentImage" :disabled="!currentPreviewImage">下载图片</button>
|
||||||
|
<button class="preview-btn" @click="prevImage" :disabled="imagePreview.images.length <= 1">上一张</button>
|
||||||
|
<button class="preview-btn" @click="nextImage" :disabled="imagePreview.images.length <= 1">下一张</button>
|
||||||
|
<button class="preview-btn" @click="closeImagePreview">关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="preview-body">
|
||||||
|
<img
|
||||||
|
v-if="currentPreviewImage"
|
||||||
|
:src="currentPreviewImage"
|
||||||
|
:alt="imagePreview.title"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="preview-footer" v-if="imagePreview.images.length > 1">
|
||||||
|
<div
|
||||||
|
v-for="(thumb, idx) in imagePreview.images"
|
||||||
|
:key="thumb"
|
||||||
|
class="preview-thumb"
|
||||||
|
:class="{ active: idx === imagePreview.currentIndex }"
|
||||||
|
@click="imagePreview.currentIndex = idx"
|
||||||
|
>
|
||||||
|
<img :src="thumb" :alt="`thumbnail-${idx}`" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted, computed } from "vue";
|
||||||
import { getMenus, getUser, updateUserInfo, resetPassword, uploadImage } from "../api";
|
import { getMenus, getUser, updateUserInfo, resetPassword, uploadImage,getconfig } from "../api";
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
const menus = ref([]);
|
const menus = ref([]);
|
||||||
const profile = ref({ username: "", password: "" });
|
const profile = ref({ username: "", password: "" });
|
||||||
@ -168,20 +257,64 @@ const pwdForm = ref({ password: "", password_new: "", confirm: "" });
|
|||||||
const showUserMenu = ref(false);
|
const showUserMenu = ref(false);
|
||||||
const showLogoutConfirm = ref(false);
|
const showLogoutConfirm = ref(false);
|
||||||
const userInfo = ref({});
|
const userInfo = ref({});
|
||||||
|
const activeChildId = ref(null);
|
||||||
|
const bannerImages = ref([]);
|
||||||
|
const imagePreview = ref({
|
||||||
|
visible: false,
|
||||||
|
images: [],
|
||||||
|
currentIndex: 0,
|
||||||
|
title: '',
|
||||||
|
downloadImages: []
|
||||||
|
});
|
||||||
|
const currentPreviewImage = computed(() => imagePreview.value.images[imagePreview.value.currentIndex] || '');
|
||||||
|
const currentDownloadImage = computed(() => imagePreview.value.downloadImages[imagePreview.value.currentIndex] || '');
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
|
const { data: bannerData } = await getconfig({ code: 'pc_banner' })
|
||||||
|
const bannerStr = (bannerData?.value_pull || '').trim();
|
||||||
|
bannerImages.value = bannerStr
|
||||||
|
? bannerStr
|
||||||
|
.split(',')
|
||||||
|
.map((url) => url.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
const m = await getMenus();
|
const m = await getMenus();
|
||||||
const res = m.data;
|
const res = m.data;
|
||||||
// 模拟数据结构:[{ id, name, cards: [{ title, desc }] }]
|
// 处理数据结构,为父级和子级都准备 cards
|
||||||
menus.value = res.map((section) => ({
|
menus.value = res.map((section) => {
|
||||||
...section,
|
// 处理父级的 nav_item
|
||||||
cards: section.nav_item.map((c) =>
|
const parentCards = (section.nav_item || []).map((c) =>
|
||||||
typeof c === "string"
|
typeof c === "string"
|
||||||
? { title: c, description: "" }
|
? { title: c, description: "" }
|
||||||
: c
|
: c
|
||||||
),
|
);
|
||||||
}));
|
|
||||||
|
// 处理子级,为每个子级准备 cards 数据
|
||||||
|
const processedChildren = (section.children || []).map((children) => {
|
||||||
|
const childCards = (children.nav_item || []).map((c) =>
|
||||||
|
typeof c === "string"
|
||||||
|
? { title: c, description: "" }
|
||||||
|
: c
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...children,
|
||||||
|
cards: childCards,
|
||||||
|
parentName: section.name, // 保存父级名称,用于显示标题
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...section,
|
||||||
|
cards: parentCards,
|
||||||
|
processedChildren: processedChildren, // 保存处理后的子级数据
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// 初始化展开当前激活的菜单项
|
||||||
|
const activeItem = menus.value[activeMenu.value];
|
||||||
|
if (activeItem && activeItem.children && activeItem.children.length > 0) {
|
||||||
|
expandedMenus.value.add(activeItem.id);
|
||||||
|
}
|
||||||
const {data} = await getUser();
|
const {data} = await getUser();
|
||||||
userInfo.value = data.user;
|
userInfo.value = data.user;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -191,53 +324,70 @@ onMounted(async () => {
|
|||||||
// 下载文件
|
// 下载文件
|
||||||
async function onCardClick(item){
|
async function onCardClick(item){
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
const url = item.url_pull;
|
const url_pull = item.url_pull;
|
||||||
const type = item.type;
|
const type = item.type;
|
||||||
if (!url) return;
|
if (type !== 'image' && !url_pull) return;
|
||||||
if (type === 'link') {
|
if (type === 'link') {
|
||||||
window.open(url, '_blank');
|
window.open(url_pull, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'image') {
|
||||||
|
const images = getImageList(item);
|
||||||
|
const downloadImages = getProxyList(item);
|
||||||
|
if (!images.length) {
|
||||||
|
ElMessage.warning('暂无可预览的图片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
imagePreview.value = {
|
||||||
|
visible: true,
|
||||||
|
images,
|
||||||
|
currentIndex: 0,
|
||||||
|
title: item.title || '图片预览',
|
||||||
|
downloadImages: downloadImages.length ? downloadImages : images
|
||||||
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'file') {
|
if (type === 'file') {
|
||||||
window.open(url, '_blank');
|
try {
|
||||||
|
// 对所有文件类型都使用 fetch 下载,确保真正的下载
|
||||||
|
const response = await fetch(url_pull, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: '*/*'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// try {
|
if (!response.ok) {
|
||||||
// // 对所有文件类型都使用 fetch 下载,确保真正的下载
|
throw new Error(`下载失败: ${response.status} ${response.statusText}`)
|
||||||
// const response = await fetch(url, {
|
}
|
||||||
// method: 'GET',
|
const blob = await response.blob()
|
||||||
// headers: {
|
const url = window.URL.createObjectURL(blob)
|
||||||
// Accept: '*/*'
|
const link = document.createElement('a')
|
||||||
// }
|
link.href = url
|
||||||
// })
|
link.download = item.title || 'download'
|
||||||
|
link.style.display = 'none'
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
|
||||||
// if (!response.ok) {
|
ElMessage.success('下载成功')
|
||||||
// throw new Error(`下载失败: ${response.status} ${response.statusText}`)
|
} catch (error) {
|
||||||
// }
|
console.error('下载失败:', error)
|
||||||
// const blob = await response.blob()
|
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||||||
// const url = window.URL.createObjectURL(blob)
|
ElMessage.error(`下载失败: ${errorMessage}`)
|
||||||
// const link = document.createElement('a')
|
}
|
||||||
// link.href = url
|
|
||||||
// link.download = file.name || file.file_name || 'download'
|
|
||||||
// link.style.display = 'none'
|
|
||||||
// document.body.appendChild(link)
|
|
||||||
// link.click()
|
|
||||||
// document.body.removeChild(link)
|
|
||||||
// window.URL.revokeObjectURL(url)
|
|
||||||
|
|
||||||
// ElMessage.success('下载成功')
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('下载失败:', error)
|
|
||||||
// const errorMessage = error instanceof Error ? error.message : '未知错误'
|
|
||||||
// ElMessage.error(`下载失败: ${errorMessage}`)
|
|
||||||
// }
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeMenu = ref(0);
|
const activeMenu = ref(0);
|
||||||
function onNav(id, index) {
|
const expandedMenus = ref(new Set()); // 用于跟踪展开的菜单项
|
||||||
|
function onNav(id, index, childId = null) {
|
||||||
activeMenu.value = index;
|
activeMenu.value = index;
|
||||||
|
activeChildId.value = childId;
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) {
|
if (el) {
|
||||||
const topbarHeight = 56;
|
const topbarHeight = 56;
|
||||||
@ -248,6 +398,111 @@ function onNav(id, index) {
|
|||||||
}
|
}
|
||||||
sidebarOpen.value = false;
|
sidebarOpen.value = false;
|
||||||
}
|
}
|
||||||
|
// 处理父级菜单点击事件(展开/收起子菜单)
|
||||||
|
function toggleSubMenu(item, index) {
|
||||||
|
// 设置选中状态并导航
|
||||||
|
activeMenu.value = index;
|
||||||
|
activeChildId.value = null;
|
||||||
|
const el = document.getElementById(item.id);
|
||||||
|
if (el) {
|
||||||
|
const topbarHeight = 56;
|
||||||
|
const mobileHeight = 44;
|
||||||
|
const offset = window.innerWidth <= 768 ? mobileHeight : topbarHeight;
|
||||||
|
const y = el.getBoundingClientRect().top + window.scrollY - offset;
|
||||||
|
window.scrollTo({ top: y, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
sidebarOpen.value = false;
|
||||||
|
|
||||||
|
// 如果有子菜单,切换展开/收起状态(手风琴效果:同时只展开一个)
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
if (expandedMenus.value.has(item.id)) {
|
||||||
|
// 如果当前菜单已展开,则关闭它
|
||||||
|
expandedMenus.value.delete(item.id);
|
||||||
|
} else {
|
||||||
|
// 如果当前菜单未展开,先关闭其他所有已展开的菜单,再展开当前菜单
|
||||||
|
expandedMenus.value.clear();
|
||||||
|
expandedMenus.value.add(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageList(item = {}) {
|
||||||
|
const candidates = [];
|
||||||
|
if (Array.isArray(item.url_pull)) candidates.push(...item.url_pull);
|
||||||
|
if (typeof item.url_pull === 'string') {
|
||||||
|
candidates.push(...item.url_pull.split(','));
|
||||||
|
}
|
||||||
|
const unique = Array.from(
|
||||||
|
new Set(
|
||||||
|
candidates
|
||||||
|
.map((url) => (typeof url === 'string' ? url.trim() : url?.url || ''))
|
||||||
|
.filter(Boolean)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return unique;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProxyList(item = {}) {
|
||||||
|
const list = item.url_pull_proxy;
|
||||||
|
if (!list) return [];
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
return list.map((url) => (typeof url === 'string' ? url.trim() : '')).filter(Boolean);
|
||||||
|
}
|
||||||
|
if (typeof list === 'string') {
|
||||||
|
return list
|
||||||
|
.split(',')
|
||||||
|
.map((url) => url.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeImagePreview() {
|
||||||
|
imagePreview.value.visible = false;
|
||||||
|
imagePreview.value.images = [];
|
||||||
|
imagePreview.value.currentIndex = 0;
|
||||||
|
imagePreview.value.downloadImages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevImage() {
|
||||||
|
if (!imagePreview.value.images.length) return;
|
||||||
|
imagePreview.value.currentIndex =
|
||||||
|
(imagePreview.value.currentIndex - 1 + imagePreview.value.images.length) %
|
||||||
|
imagePreview.value.images.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextImage() {
|
||||||
|
if (!imagePreview.value.images.length) return;
|
||||||
|
imagePreview.value.currentIndex =
|
||||||
|
(imagePreview.value.currentIndex + 1) % imagePreview.value.images.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadImage(url, filename = 'image') {
|
||||||
|
try {
|
||||||
|
if (!url) throw new Error('无效的下载地址');
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename || 'download';
|
||||||
|
link.target = '_blank';
|
||||||
|
link.style.display = 'none';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
ElMessage.success('图片已开始下载');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载图片失败:', error);
|
||||||
|
ElMessage.error('下载图片失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadCurrentImage() {
|
||||||
|
const img = currentDownloadImage.value || currentPreviewImage.value;
|
||||||
|
if (!img) {
|
||||||
|
ElMessage.warning('暂无可下载图片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
downloadImage(img, imagePreview.value.title || 'image');
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadAvatar(e){
|
async function uploadAvatar(e){
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
@ -403,7 +658,67 @@ window.addEventListener('click', () => { showUserMenu.value = false; });
|
|||||||
color: #009999;
|
color: #009999;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.parent-menu {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #999;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon.expanded {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background: #f8f9fa;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .submenu-item {
|
||||||
|
padding: 8px 18px 8px 48px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .submenu-item:hover,
|
||||||
|
.sidebar .submenu-item.active {
|
||||||
|
background: #eef6f6;
|
||||||
|
color: #009999;
|
||||||
|
border-left-color: #009999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 子菜单展开/收起动画 */
|
||||||
|
.submenu-enter-active,
|
||||||
|
.submenu-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-enter-from,
|
||||||
|
.submenu-leave-to {
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
/* 内容区 */
|
/* 内容区 */
|
||||||
.content {
|
.content {
|
||||||
margin-left: 220px;
|
margin-left: 220px;
|
||||||
@ -512,6 +827,106 @@ window.addEventListener('click', () => { showUserMenu.value = false; });
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-preview-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 250;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-box {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: min(900px, 96vw);
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 18px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-hint {
|
||||||
|
margin-right: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-btn {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background: #fff;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #000;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-body img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 70vh;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-thumb {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-thumb.active {
|
||||||
|
border-color: #14C9C9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-thumb img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@ -519,6 +934,25 @@ window.addEventListener('click', () => { showUserMenu.value = false; });
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner img {
|
||||||
|
width: 100%;
|
||||||
|
height: 220px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.banner-carousel .el-carousel__container) {
|
||||||
|
height: 220px !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* 移动端适配 */
|
/* 移动端适配 */
|
||||||
.drawer-overlay {
|
.drawer-overlay {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@ -1,41 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login-page">
|
<div class="login-page">
|
||||||
<div class="login-left"></div>
|
<div class="login-left">
|
||||||
|
<div>
|
||||||
|
<img src="/src/assets/logopc.png" alt="logo" style="height: 24px;"/>
|
||||||
|
<p class="titles">豫皖大区</p>
|
||||||
|
<p class="titles">西门子家电<span>数据看板</span></p>
|
||||||
|
<p class="titles1">YUWAN REGION DATA DASHBOARD FOR SIEMENS HOME APPLIANCES</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="login-right">
|
<div class="login-right">
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<div class="corner-qrcode" @click="tabFlip">
|
<div
|
||||||
|
class="corner-qrcode"
|
||||||
|
@click="tabFlip"
|
||||||
|
:style="{ backgroundImage: `url(/src/assets/${tab === 'account' ? 'qrcode.png' : 'macgroup.png'})` }"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="title">{{ tab==='account' ? '账号登录' : '扫码登录' }}</h2>
|
<h2 class="title">{{ tab==='account' ? '欢迎登录' : '扫码登录' }}</h2>
|
||||||
<div v-if="tab==='account'" class="login-form">
|
<div v-if="tab==='account'" class="login-form">
|
||||||
<el-form
|
<el-form
|
||||||
ref="loginFormRef"
|
ref="loginFormRef"
|
||||||
:model="loginForm"
|
:model="loginForm"
|
||||||
:rules="loginFormRules"
|
:rules="loginFormRules"
|
||||||
label-width="60px"
|
|
||||||
label-position="left"
|
label-position="left"
|
||||||
>
|
>
|
||||||
<el-form-item label="账号" prop="account">
|
<el-form-item prop="account">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="loginForm.account"
|
v-model="loginForm.account"
|
||||||
placeholder="请输入账号"
|
placeholder="请输入账号"
|
||||||
clearable
|
clearable
|
||||||
prefix-icon="User"
|
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<span class="input-label">账号</span>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="密码" prop="password">
|
<el-form-item prop="password">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="loginForm.password"
|
v-model="loginForm.password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="请输入密码"
|
placeholder="请输入密码"
|
||||||
show-password
|
show-password
|
||||||
clearable
|
clearable
|
||||||
prefix-icon="Lock"
|
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<span class="input-label">密码</span>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<div class="remember-account">
|
||||||
|
<el-checkbox v-model="rememberAccount">保持登录</el-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button
|
<el-button
|
||||||
@ -72,7 +91,7 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { apiLogin, apiqrcode } from '../api'
|
import { apiLogin, apiqrcode,getconfig } from '../api'
|
||||||
import { isLoggedIn, saveLoginData, handleAuthRedirect } from '../utils/auth'
|
import { isLoggedIn, saveLoginData, handleAuthRedirect } from '../utils/auth'
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const tab = ref('account')
|
const tab = ref('account')
|
||||||
@ -95,9 +114,22 @@ const loginFormRules = {
|
|||||||
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
//获取登录背景图
|
||||||
|
const getBackgroundImage = async () => {
|
||||||
|
const res = await getconfig({ code: 'pc_loginbg' })
|
||||||
|
if (res.data.value_pull) {
|
||||||
|
const loginElement = document.getElementsByClassName('login-page')[0]
|
||||||
|
const bannerStr = res.data.value_pull.trim();
|
||||||
|
if (bannerStr && loginElement) {
|
||||||
|
// 按逗号分隔,过滤空字符串
|
||||||
|
loginElement.style.backgroundImage = `url(${bannerStr.split(',').map(url => url.trim()).filter(url => url)[0]})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 页面加载时检查登录状态
|
// 页面加载时检查登录状态
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
getBackgroundImage()
|
||||||
if (isLoggedIn()) {
|
if (isLoggedIn()) {
|
||||||
ElMessage.info('您已登录,正在跳转到首页...')
|
ElMessage.info('您已登录,正在跳转到首页...')
|
||||||
router.replace('/dashboard')
|
router.replace('/dashboard')
|
||||||
@ -201,20 +233,37 @@ async function onScanLogin(){
|
|||||||
/* left large image-ish area (you can replace with your image) */
|
/* left large image-ish area (you can replace with your image) */
|
||||||
.login-left{
|
.login-left{
|
||||||
flex:1;
|
flex:1;
|
||||||
|
margin-left: 10%;
|
||||||
|
margin-top: -14%;
|
||||||
|
|
||||||
|
}
|
||||||
|
.titles {
|
||||||
|
font-size: 44px;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.titles span{
|
||||||
|
color:#009688 ;
|
||||||
|
}
|
||||||
|
.titles1{
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
/* right card */
|
/* right card */
|
||||||
.login-right{
|
.login-right{
|
||||||
width:520px;
|
width:600px;
|
||||||
display:flex;
|
display:flex;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
justify-content:center;
|
justify-content:center;
|
||||||
padding-right:6%;
|
padding-right:10%;
|
||||||
}
|
}
|
||||||
.login-card{
|
.login-card{
|
||||||
width:420px;
|
width:420px;
|
||||||
background:#fff;
|
background: rgb(0 10 34 / 50%);
|
||||||
border-radius:12px;
|
border-radius:12px;
|
||||||
box-shadow: 0 10px 30px rgba(14,42,51,0.08);
|
/* box-shadow: 0 10px 30px rgba(14,42,51,0.08); */
|
||||||
position:relative;
|
position:relative;
|
||||||
}
|
}
|
||||||
.login-form {
|
.login-form {
|
||||||
@ -227,6 +276,11 @@ async function onScanLogin(){
|
|||||||
|
|
||||||
.login-form .el-input {
|
.login-form .el-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 55px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.input-label{
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
.corner-qrcode{
|
.corner-qrcode{
|
||||||
position:absolute;
|
position:absolute;
|
||||||
@ -237,14 +291,16 @@ async function onScanLogin(){
|
|||||||
font-size:12px;
|
font-size:12px;
|
||||||
color:#009688;
|
color:#009688;
|
||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
background: url('/src/assets/qrcode.png') no-repeat center;
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
}
|
}
|
||||||
/* title */
|
/* title */
|
||||||
.title{
|
.title{
|
||||||
text-align:center;
|
text-align:center;
|
||||||
margin:60 0 12px;
|
margin:60 0 12px;
|
||||||
font-size:20px;
|
font-size:32px;
|
||||||
color:#333;
|
color:#fff;
|
||||||
font-weight:700;
|
font-weight:700;
|
||||||
}
|
}
|
||||||
/* tabs */
|
/* tabs */
|
||||||
@ -255,7 +311,13 @@ async function onScanLogin(){
|
|||||||
/* form */
|
/* form */
|
||||||
.form label{ display:block; margin-top:8px; color:#666; font-size:13px;}
|
.form label{ display:block; margin-top:8px; color:#666; font-size:13px;}
|
||||||
.form input{ width:100%; padding:10px; margin-top:6px; border-radius:6px; border:1px solid #eee; background:#fafafa;}
|
.form input{ width:100%; padding:10px; margin-top:6px; border-radius:6px; border:1px solid #eee; background:#fafafa;}
|
||||||
.login-btn{ width:100%; padding:12px; margin-top:60px; border-radius:16px; border:none; background:#009688; color:#fff; cursor:pointer; font-size:16px;}
|
.login-btn{ width:100%; padding:28px; margin-top:60px; border-radius:30px; border:none; background:#009688; color:#fff; cursor:pointer; font-size:20px;}
|
||||||
|
.login-btn:hover {
|
||||||
|
background-color:#009688;
|
||||||
|
border-color: #009688;
|
||||||
|
color: #fff;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
.form-row {
|
.form-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -313,4 +375,7 @@ async function onScanLogin(){
|
|||||||
height: 400px;
|
height: 400px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
:deep(.el-checkbox__label) {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user