feat: 添加库位操作子菜单功能,支持多种操作并优化右键菜单交互体验
This commit is contained in:
parent
1e86108745
commit
95fc2f4a16
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="visible" class="context-menu" :style="menuStyle" @click.stop @contextmenu.prevent>
|
||||
<div v-if="visible" ref="mainMenuRef" class="context-menu" :style="menuStyle" @click.stop @contextmenu.prevent>
|
||||
<div class="context-menu-header">
|
||||
<div class="menu-title">{{ headerTitle }}</div>
|
||||
</div>
|
||||
@ -12,6 +12,8 @@
|
||||
class="context-menu-item storage-item"
|
||||
:class="getStorageItemClass(location)"
|
||||
@click="handleSelectStorage(location)"
|
||||
@mouseenter="showSubMenu = location.id"
|
||||
@mouseleave="handleStorageMouseLeave"
|
||||
:title="getStorageTooltip(location)"
|
||||
>
|
||||
<div class="storage-info">
|
||||
@ -27,6 +29,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arrow-icon">▶</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -36,19 +39,63 @@
|
||||
<span>刷新</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 库位操作子菜单 -->
|
||||
<div
|
||||
v-if="showSubMenu && selectedLocation"
|
||||
class="sub-menu-container"
|
||||
:style="subMenuStyle"
|
||||
@click.stop
|
||||
@mouseenter="handleSubMenuMouseEnter"
|
||||
@mouseleave="handleSubMenuMouseLeave"
|
||||
>
|
||||
<!-- 连接区域,确保鼠标移动时不会断开 -->
|
||||
<div class="sub-menu-connector"></div>
|
||||
<div class="sub-menu">
|
||||
<div class="sub-menu-header">
|
||||
<div class="sub-menu-title">{{ selectedLocation.name }} - 操作</div>
|
||||
</div>
|
||||
<div class="sub-menu-item" @click="handleStorageAction('occupy', '占用')">
|
||||
<span class="action-icon">📦</span>
|
||||
<span>占用</span>
|
||||
</div>
|
||||
<div class="sub-menu-item" @click="handleStorageAction('release', '释放')">
|
||||
<span class="action-icon">📤</span>
|
||||
<span>释放</span>
|
||||
</div>
|
||||
<div class="sub-menu-item" @click="handleStorageAction('lock', '锁定')">
|
||||
<span class="action-icon">🔒</span>
|
||||
<span>锁定</span>
|
||||
</div>
|
||||
<div class="sub-menu-item" @click="handleStorageAction('unlock', '解锁')">
|
||||
<span class="action-icon">🔓</span>
|
||||
<span>解锁</span>
|
||||
</div>
|
||||
<div class="sub-menu-item" @click="handleStorageAction('enable', '启用')">
|
||||
<span class="action-icon">✅</span>
|
||||
<span>启用</span>
|
||||
</div>
|
||||
<div class="sub-menu-item" @click="handleStorageAction('disable', '禁用')">
|
||||
<span class="action-icon">❌</span>
|
||||
<span>禁用</span>
|
||||
</div>
|
||||
<div class="sub-menu-item" @click="handleStorageAction('set_empty_tray', '设为空托盘')">
|
||||
<span class="action-icon">📋</span>
|
||||
<span>设为空托盘</span>
|
||||
</div>
|
||||
<div class="sub-menu-item" @click="handleStorageAction('clear_empty_tray', '清除空托盘')">
|
||||
<span class="action-icon">🗑️</span>
|
||||
<span>清除空托盘</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
interface StorageLocationInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
isOccupied: boolean;
|
||||
isLocked: boolean;
|
||||
status: 'available' | 'occupied' | 'locked' | 'unknown';
|
||||
}
|
||||
import type { StorageLocationInfo } from '../services/storageApi';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
@ -56,10 +103,12 @@ interface Props {
|
||||
y?: number;
|
||||
menuType?: 'default' | 'storage' | 'storage-background';
|
||||
storageLocations?: StorageLocationInfo[];
|
||||
apiBaseUrl?: string; // 添加API基础URL
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'close'): void;
|
||||
(e: 'actionComplete', data: { action: string; location: StorageLocationInfo; success: boolean }): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@ -67,6 +116,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
y: 0,
|
||||
menuType: 'default',
|
||||
storageLocations: () => [],
|
||||
apiBaseUrl: '',
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
@ -75,6 +125,28 @@ defineOptions({
|
||||
name: 'ContextMenu',
|
||||
});
|
||||
|
||||
// 子菜单状态
|
||||
const showSubMenu = ref<string | null>(null);
|
||||
const selectedLocation = ref<StorageLocationInfo | null>(null);
|
||||
const hideTimer = ref<number | null>(null);
|
||||
|
||||
// 主菜单引用
|
||||
const mainMenuRef = ref<HTMLElement | null>(null);
|
||||
|
||||
// 监听子菜单显示状态
|
||||
watch(showSubMenu, (newValue) => {
|
||||
if (newValue) {
|
||||
selectedLocation.value = props.storageLocations.find(loc => loc.id === newValue) || null;
|
||||
// 清除之前的隐藏定时器
|
||||
if (hideTimer.value) {
|
||||
clearTimeout(hideTimer.value);
|
||||
hideTimer.value = null;
|
||||
}
|
||||
} else {
|
||||
selectedLocation.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 菜单样式计算
|
||||
const menuStyle = computed(() => {
|
||||
const style = {
|
||||
@ -84,6 +156,29 @@ const menuStyle = computed(() => {
|
||||
return style;
|
||||
});
|
||||
|
||||
// 子菜单样式计算
|
||||
const subMenuStyle = computed(() => {
|
||||
if (!showSubMenu.value) return {};
|
||||
|
||||
// 计算子菜单位置,让它与对应的库位项对齐
|
||||
const menuItemHeight = 60; // 库位项的高度
|
||||
const headerHeight = 40; // 菜单头部高度
|
||||
const itemIndex = props.storageLocations.findIndex(loc => loc.id === showSubMenu.value);
|
||||
const offsetY = headerHeight + (itemIndex * menuItemHeight);
|
||||
|
||||
// 获取主菜单的实际宽度,让子菜单紧挨着
|
||||
let mainMenuWidth = 200; // 默认宽度
|
||||
if (mainMenuRef.value) {
|
||||
mainMenuWidth = mainMenuRef.value.offsetWidth;
|
||||
}
|
||||
|
||||
const style = {
|
||||
left: `${props.x + mainMenuWidth}px`, // 紧贴主菜单右边缘
|
||||
top: `${props.y + offsetY}px`, // 与对应库位项对齐
|
||||
};
|
||||
return style;
|
||||
});
|
||||
|
||||
// 动态标题
|
||||
const headerTitle = computed(() => {
|
||||
if (props.menuType === 'storage-background') return '库位状态';
|
||||
@ -100,7 +195,76 @@ const handleRefresh = () => {
|
||||
// 选择库位
|
||||
const handleSelectStorage = (location: StorageLocationInfo) => {
|
||||
console.log('选择库位:', location);
|
||||
emit('close');
|
||||
// 不立即关闭菜单,让子菜单显示
|
||||
};
|
||||
|
||||
// 隐藏子菜单(立即隐藏)
|
||||
const hideSubMenu = () => {
|
||||
showSubMenu.value = null;
|
||||
if (hideTimer.value) {
|
||||
clearTimeout(hideTimer.value);
|
||||
hideTimer.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 延迟隐藏子菜单
|
||||
const hideSubMenuDelayed = () => {
|
||||
if (hideTimer.value) {
|
||||
clearTimeout(hideTimer.value);
|
||||
}
|
||||
hideTimer.value = window.setTimeout(() => {
|
||||
showSubMenu.value = null;
|
||||
hideTimer.value = null;
|
||||
}, 150); // 150ms延迟,给用户足够时间移动到子菜单
|
||||
};
|
||||
|
||||
// 处理库位项鼠标离开
|
||||
const handleStorageMouseLeave = () => {
|
||||
hideSubMenuDelayed();
|
||||
};
|
||||
|
||||
// 处理子菜单鼠标进入
|
||||
const handleSubMenuMouseEnter = () => {
|
||||
// 清除隐藏定时器
|
||||
if (hideTimer.value) {
|
||||
clearTimeout(hideTimer.value);
|
||||
hideTimer.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理子菜单鼠标离开
|
||||
const handleSubMenuMouseLeave = () => {
|
||||
hideSubMenu();
|
||||
};
|
||||
|
||||
// 库位操作处理
|
||||
const handleStorageAction = async (action: string, actionName: string) => {
|
||||
if (!selectedLocation.value) return;
|
||||
|
||||
try {
|
||||
// 直接调用API,不需要勾选
|
||||
const { StorageActionService } = await import('../services/storageActionService');
|
||||
await StorageActionService.handleStorageAction(action, selectedLocation.value, actionName);
|
||||
|
||||
// 发送操作完成事件
|
||||
emit('actionComplete', {
|
||||
action,
|
||||
location: selectedLocation.value,
|
||||
success: true
|
||||
});
|
||||
|
||||
// 关闭菜单
|
||||
emit('close');
|
||||
} catch (error) {
|
||||
console.error(`库位${actionName}操作失败:`, error);
|
||||
|
||||
// 发送操作失败事件
|
||||
emit('actionComplete', {
|
||||
action,
|
||||
location: selectedLocation.value,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取库位项样式类
|
||||
@ -263,4 +427,89 @@ const getStorageTooltip = (location: StorageLocationInfo) => {
|
||||
background-color: #f6ffed;
|
||||
border-left-color: #52c41a;
|
||||
}
|
||||
|
||||
/* 箭头图标 */
|
||||
.arrow-icon {
|
||||
margin-left: auto;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.storage-item:hover .arrow-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 子菜单容器样式 */
|
||||
.sub-menu-container {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* 连接区域 - 确保鼠标移动流畅 */
|
||||
.sub-menu-connector {
|
||||
width: 12px;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
pointer-events: auto;
|
||||
/* 确保连接区域覆盖可能的间隙 */
|
||||
margin-left: -6px;
|
||||
/* 添加一个微妙的背景,帮助用户理解连接关系 */
|
||||
background: linear-gradient(to right, transparent 0%, rgba(24, 144, 255, 0.05) 50%, transparent 100%);
|
||||
}
|
||||
|
||||
/* 子菜单样式 */
|
||||
.sub-menu {
|
||||
background: white;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-left: none; /* 移除左边框,与主菜单无缝连接 */
|
||||
border-radius: 0 6px 6px 0; /* 只圆角右侧,左侧与主菜单连接 */
|
||||
box-shadow: 2px 4px 12px rgba(0, 0, 0, 0.15); /* 调整阴影,避免左侧阴影 */
|
||||
padding: 4px 0;
|
||||
min-width: 160px;
|
||||
user-select: none;
|
||||
color: #000;
|
||||
pointer-events: auto;
|
||||
/* 确保与主菜单无缝连接 */
|
||||
margin-left: 0;
|
||||
/* 添加一个微妙的左边框,模拟与主菜单的连接 */
|
||||
border-left: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.sub-menu-header {
|
||||
padding: 8px 12px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 0 6px 0 0; /* 只圆角右上角,与子菜单整体设计一致 */
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.sub-menu-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.sub-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
font-size: 13px;
|
||||
color: #000;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sub-menu-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 14px;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
120
src/services/storageActionService.ts
Normal file
120
src/services/storageActionService.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { message } from '../utils/message';
|
||||
import type { StorageLocationInfo } from './storageApi';
|
||||
import { batchUpdateStorageLocationStatus } from './storageApi';
|
||||
|
||||
// 库位操作服务
|
||||
export class StorageActionService {
|
||||
/**
|
||||
* 处理库位操作
|
||||
* @param action 操作类型
|
||||
* @param location 库位信息
|
||||
* @param actionName 操作名称(用于显示)
|
||||
*/
|
||||
static async handleStorageAction(
|
||||
action: string,
|
||||
location: StorageLocationInfo,
|
||||
actionName: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 使用layer_name,如果没有则使用id或name作为替代
|
||||
const layerName = location.name;
|
||||
|
||||
if (!layerName) {
|
||||
message.error(`库位 ${location.name} 缺少必要信息,无法执行操作`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 准备请求参数
|
||||
const requestParams: any = {
|
||||
layer_names: [layerName],
|
||||
action: action,
|
||||
reason: `${actionName}操作 - ${location.name}`,
|
||||
};
|
||||
|
||||
// 如果是锁定操作,添加锁定者信息
|
||||
if (action === 'lock') {
|
||||
requestParams.locked_by = 'current_user'; // 可以从用户状态中获取
|
||||
}
|
||||
|
||||
// 调用API
|
||||
const result = await batchUpdateStorageLocationStatus(requestParams);
|
||||
|
||||
// 处理结果
|
||||
if (result.data) {
|
||||
const { failed_count } = result.data;
|
||||
|
||||
if (failed_count === 0) {
|
||||
message.success(`库位 ${location.name} ${actionName}成功`);
|
||||
} else {
|
||||
message.error(`库位 ${location.name} ${actionName}失败`);
|
||||
}
|
||||
} else {
|
||||
// 如果没有详细结果数据,显示简单成功消息
|
||||
message.success(`库位 ${location.name} ${actionName}成功`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`库位${actionName}失败:`, error);
|
||||
message.error(`库位 ${location.name} ${actionName}失败`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量处理库位操作
|
||||
* @param action 操作类型
|
||||
* @param locations 库位信息列表
|
||||
* @param actionName 操作名称(用于显示)
|
||||
*/
|
||||
static async handleBatchStorageAction(
|
||||
action: string,
|
||||
locations: StorageLocationInfo[],
|
||||
actionName: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 过滤出有效的库位(有layer_name、id或name)
|
||||
const validLocations = locations.filter(loc => loc.layer_name || loc.id || loc.name);
|
||||
|
||||
if (validLocations.length === 0) {
|
||||
message.error('没有有效的库位可以操作');
|
||||
return;
|
||||
}
|
||||
|
||||
if (validLocations.length !== locations.length) {
|
||||
message.warning(`部分库位缺少必要信息,将跳过 ${locations.length - validLocations.length} 个库位`);
|
||||
}
|
||||
|
||||
// 准备请求参数
|
||||
const requestParams: any = {
|
||||
layer_names: validLocations.map(loc => loc.layer_name || loc.id || loc.name),
|
||||
action: action,
|
||||
reason: `批量${actionName}操作`,
|
||||
};
|
||||
|
||||
// 如果是锁定操作,添加锁定者信息
|
||||
if (action === 'lock') {
|
||||
requestParams.locked_by = 'current_user'; // 可以从用户状态中获取
|
||||
}
|
||||
|
||||
// 调用API
|
||||
const result = await batchUpdateStorageLocationStatus(requestParams);
|
||||
|
||||
// 处理结果
|
||||
if (result.data) {
|
||||
const { total_count, success_count, failed_count } = result.data;
|
||||
|
||||
if (failed_count === 0) {
|
||||
message.success(`批量${actionName}完成:总计 ${total_count} 个库位,全部成功`);
|
||||
} else if (success_count > 0) {
|
||||
message.warning(`批量${actionName}完成:成功 ${success_count} 个,失败 ${failed_count} 个`);
|
||||
} else {
|
||||
message.error(`批量${actionName}失败:${failed_count} 个库位操作失败`);
|
||||
}
|
||||
} else {
|
||||
// 如果没有详细结果数据,显示简单成功消息
|
||||
message.success(`批量${actionName}成功`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`批量${actionName}失败:`, error);
|
||||
message.error(`批量${actionName}失败`);
|
||||
}
|
||||
}
|
||||
}
|
105
src/services/storageApi.ts
Normal file
105
src/services/storageApi.ts
Normal file
@ -0,0 +1,105 @@
|
||||
// 库位操作API服务
|
||||
export interface StorageLocationInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
isOccupied: boolean;
|
||||
isLocked: boolean;
|
||||
status: 'available' | 'occupied' | 'locked' | 'unknown';
|
||||
layer_name?: string;
|
||||
}
|
||||
|
||||
export interface StorageActionRequest {
|
||||
layer_names: string[];
|
||||
action: string;
|
||||
locked_by?: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface StorageActionResponse {
|
||||
data: {
|
||||
total_count: number;
|
||||
success_count: number;
|
||||
failed_count: number;
|
||||
failed_items?: Array<{
|
||||
layer_name: string;
|
||||
error: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
// 获取API基础URL
|
||||
const getApiBaseUrl = () => {
|
||||
// 使用相对路径,确保开发环境通过 Vite 代理转发到目标服务
|
||||
// 如需在生产或特定环境覆盖,可再引入对应环境变量
|
||||
return '';
|
||||
};
|
||||
|
||||
// 发送HTTP请求的通用方法
|
||||
const request = async (url: string, options: RequestInit = {}) => {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量更新库位状态
|
||||
* @param data 批量更新参数
|
||||
*/
|
||||
export const batchUpdateStorageLocationStatus = async (data: StorageActionRequest): Promise<StorageActionResponse> => {
|
||||
const apiBaseUrl = getApiBaseUrl();
|
||||
const url = `${apiBaseUrl}/vwedApi/api/vwed-operate-point/batch-status`;
|
||||
|
||||
return request(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新单个库位状态
|
||||
* @param data 更新参数
|
||||
*/
|
||||
export const updateStorageLocationStatus = async (data: {
|
||||
layer_name: string;
|
||||
action: string;
|
||||
locked_by?: string;
|
||||
reason?: string;
|
||||
}): Promise<any> => {
|
||||
const apiBaseUrl = getApiBaseUrl();
|
||||
const url = `${apiBaseUrl}/vwedApi/api/vwed-operate-point/status`;
|
||||
|
||||
return request(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取库位列表
|
||||
* @param params 查询参数
|
||||
*/
|
||||
export const getStorageLocationList = async (params: any = {}): Promise<any> => {
|
||||
const apiBaseUrl = getApiBaseUrl();
|
||||
const url = `${apiBaseUrl}/vwedApi/api/vwed-operate-point/list`;
|
||||
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
const fullUrl = queryString ? `${url}?${queryString}` : url;
|
||||
|
||||
return request(fullUrl);
|
||||
};
|
121
src/utils/message.ts
Normal file
121
src/utils/message.ts
Normal file
@ -0,0 +1,121 @@
|
||||
// 消息提示工具
|
||||
export interface MessageOptions {
|
||||
type: 'success' | 'error' | 'warning' | 'info';
|
||||
message: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
// 创建消息提示元素
|
||||
const createMessageElement = (options: MessageOptions): HTMLElement => {
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.className = `storage-message storage-message-${options.type}`;
|
||||
messageEl.textContent = options.message;
|
||||
|
||||
// 添加样式
|
||||
Object.assign(messageEl.style, {
|
||||
position: 'fixed',
|
||||
top: '20px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: '9999',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '6px',
|
||||
color: 'white',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
transition: 'all 0.3s ease',
|
||||
maxWidth: '400px',
|
||||
textAlign: 'center',
|
||||
wordWrap: 'break-word',
|
||||
});
|
||||
|
||||
// 根据类型设置背景色
|
||||
const colors = {
|
||||
success: '#52c41a',
|
||||
error: '#ff4d4f',
|
||||
warning: '#faad14',
|
||||
info: '#1890ff',
|
||||
};
|
||||
|
||||
messageEl.style.backgroundColor = colors[options.type];
|
||||
|
||||
return messageEl;
|
||||
};
|
||||
|
||||
// 显示消息
|
||||
export const showMessage = (options: MessageOptions): void => {
|
||||
const messageEl = createMessageElement(options);
|
||||
|
||||
// 添加到页面
|
||||
document.body.appendChild(messageEl);
|
||||
|
||||
// 添加进入动画
|
||||
requestAnimationFrame(() => {
|
||||
messageEl.style.opacity = '1';
|
||||
messageEl.style.transform = 'translateX(-50%) translateY(0)';
|
||||
});
|
||||
|
||||
// 自动移除
|
||||
const duration = options.duration || 3000;
|
||||
setTimeout(() => {
|
||||
messageEl.style.opacity = '0';
|
||||
messageEl.style.transform = 'translateX(-50%) translateY(-20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
if (messageEl.parentNode) {
|
||||
messageEl.parentNode.removeChild(messageEl);
|
||||
}
|
||||
}, 300);
|
||||
}, duration);
|
||||
};
|
||||
|
||||
// 便捷方法
|
||||
export const message = {
|
||||
success: (message: string, duration?: number) =>
|
||||
showMessage({ type: 'success', message, duration }),
|
||||
|
||||
error: (message: string, duration?: number) =>
|
||||
showMessage({ type: 'error', message, duration }),
|
||||
|
||||
warning: (message: string, duration?: number) =>
|
||||
showMessage({ type: 'warning', message, duration }),
|
||||
|
||||
info: (message: string, duration?: number) =>
|
||||
showMessage({ type: 'info', message, duration }),
|
||||
};
|
||||
|
||||
// 添加全局样式
|
||||
const addGlobalStyles = () => {
|
||||
if (document.getElementById('storage-message-styles')) return;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = 'storage-message-styles';
|
||||
style.textContent = `
|
||||
.storage-message {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-20px);
|
||||
}
|
||||
|
||||
.storage-message-success {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
|
||||
.storage-message-error {
|
||||
background-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.storage-message-warning {
|
||||
background-color: #faad14;
|
||||
}
|
||||
|
||||
.storage-message-info {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(style);
|
||||
};
|
||||
|
||||
// 初始化样式
|
||||
addGlobalStyles();
|
@ -50,6 +50,11 @@ export default ({ mode }: Record<string, unknown>) =>
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/vwedApi/': {
|
||||
target: 'http://192.168.189.206:8000/',
|
||||
rewrite: (path) => path.replace(/^\/vwedApi/, ''),
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/ws/': {
|
||||
target: 'ws://192.168.189.206:8080/jeecg-boot',
|
||||
rewrite: (path) => path.replace(/^\/ws/, ''),
|
||||
|
Loading…
x
Reference in New Issue
Block a user