This commit is contained in:
oneao committed 2026-04-28 17:29:53 +08:00
1 parent ae6ccad238
commit b2d113db38
17 files changed
+3087 -1630

No files matched your search

@@ -0,0 +1,141 @@
# G3File 左右布局改造计划
## 需求分析
用户需要将 g3-file-container 的布局从原来的垂直布局改为左右布局:
**原布局:**
```
┌─────────────────────────────────────┐
│ header │
├─────────────────────────────────────┤
│ main │
├─────────────────────────────────────┤
│ footer │
└─────────────────────────────────────┘
```
**新布局:**
```
┌─────────────────────────────────────────────────────────────┐
│ 左边区域 │ 右边区域 │
│ ┌─────────────────────────────┐ │ ┌─────────────────┐ │
│ │ header │ │ │ toolbar │ │ (10%)
│ ├─────────────────────────────┤ │ └─────────────────┘ │
│ │ main │ │ ┌─────────────────┐ │
│ ├─────────────────────────────┤ │ │ preview │ │ (90%)
│ │ footer │ │ │ area │ │
│ └─────────────────────────────┘ │ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
**Toolbar 功能需求:**
* 上一个/下一个按钮(用于切换预览)
- 当前预览的文件名
- 顺序显示(当前索引/总数)
**预览区域:**
* 预览功能暂不实现,显示测试内容即可
## 修改方案
### 1. 修改 index.js
**需要修改的内容:**
* 在 `render()` 方法中添加右侧预览区域的 HTML 结构
* 添加预览相关的状态管理(当前预览索引、已上传文件列表)
* 添加预览区域的 DOM 引用
* 添加上下按钮的事件处理
**新增状态:**
```javascript
this.previewIndex = 0; // 当前预览索引
```
**新增 DOM 引用:**
```javascript
this.$previewPanel = this.$container.find('.g3-file-preview-panel');
this.$previewToolbar = this.$container.find('.g3-preview-toolbar');
this.$previewContent = this.$container.find('.g3-preview-content');
this.$btnPrev = this.$container.find('.preview-prev');
this.$btnNext = this.$container.find('.preview-next');
this.$previewInfo = this.$container.find('.preview-info');
```
### 2. 修改 index.css
**需要修改的内容:**
* 修改 `.g3-file-container` 为横向 flex 布局
* 添加左侧区域样式 `.g3-file-left-panel`
* 添加右侧预览区域样式 `.g3-file-preview-panel`
* 添加 toolbar 样式 `.g3-preview-toolbar`
* 添加预览内容区域样式 `.g3-preview-content`
* 添加导航按钮样式
## 文件修改清单
| 文件 | 修改内容 |
| ------------------- | ---------------------------------- |
| `g3-file/index.js` | 修改 render 方法添加预览区域HTML,添加预览相关状态和事件 |
| `g3-file/index.css` | 修改容器布局,添加左右区域样式、toolbar 样式、预览区域样式 |
## 实现步骤
1. **修改 CSS 布局**
* 修改 `.g3-file-container` 为 `flex-direction: row`
* 添加左侧面板样式(占50%宽度)
* 添加右侧预览面板样式(占50%宽度)
* 添加 toolbar 和预览内容区域样式
2. **修改 JS 渲染**
* 在 render 方法中添加右侧预览区域 HTML
* 添加预览相关状态
* 添加上下按钮事件绑定
3. **添加辅助方法**
* `getUploadedFiles()` - 获取已上传的文件列表
* `updatePreview()` - 更新预览区域显示
## 风险评估
* 现有功能(文件上传、下载、删除)不受影响
* 预览功能暂不实现,仅显示测试内容
* 布局变化可能影响现有样式,需要仔细测试
## 测试要点
1. 布局是否正确显示为左右结构
2. 左侧原有功能是否正常工作
3. Toolbar 是否正确显示文件信息
4. 上下按钮是否可点击(即使预览功能未实现)
5. 预览区域是否正常显示测试内容
@@ -0,0 +1,386 @@
# G3File 预览区域布局修改计划
## 一、需求分析
根据用户需求,需要对 g3-file-container 进行以下修改:
1. **布局调整**:改为左右两栏布局
- **左侧区域**:保持现有完整结构(header + main + footer)
- **右侧区域**:新增预览区域(toolbar 10% + 预览内容区 90%)
2. **预览区域 Toolbar**:
- 文件总数(仅统计已上传文件)
- 当前预览文件名
- 顺序导航按钮(上一个/下一个)
3. **预览规则**:
- 只有 `status === 'uploaded'` 的文件才可预览
- 预览总数只计算已上传文件
4. **预览功能**:暂时不实现实际预览功能,只显示测试内容
## 二、布局结构
### 2.1 原布局
```
┌─────────────────────────────────────┐
│ Header │
├─────────────────────────────────────┤
│ Main │
│ (文件列表) │
├─────────────────────────────────────┤
│ Footer │
└─────────────────────────────────────┘
```
### 2.2 新布局
```
┌─────────────────────────────────────────────────────────────────┐
│ 左侧区域 │ 右侧预览区域 │
│ ┌─────────────────────────────────────┐ │ ┌───────────┐ │
│ │ Header │ │ │ Toolbar │ │
│ ├─────────────────────────────────────┤ │ │(10%高度) │ │
│ │ Main │ ├───────────────┤ │
│ │ (文件列表) │ │ │ │
│ ├─────────────────────────────────────┤ │ 预览内容区 │ │
│ │ Footer │ │ (90%高度) │ │
│ └─────────────────────────────────────┘ │ │ │
│ └─────────────┘ │
└───────────────────────────────────────────────────────────────┘
```
### 2.3 Toolbar 内容
```
┌─────────────────────────────────────────────────────────┐
│ [上一个] 第 1/5 个文件 当前: filename.pdf [下一个] │
└─────────────────────────────────────────────────────────┘
```
## 三、修改方案
### 3.1 修改文件
| 文件 | 修改内容 |
|------|----------|
| `g3-file/index.css` | 添加左右布局和预览区域样式 |
| `g3-file/index.js` | 修改布局结构、添加预览区域逻辑 |
### 3.2 JS 修改要点
1. **渲染方法 (`render`)**:修改 HTML 结构为左右布局
2. **状态管理**:
- `currentPreviewIndex`: 当前预览文件索引
3. **预览操作方法**:
- `getPreviewableFiles()`: 获取已上传文件列表
- `previewFile(file)`: 预览指定文件
- `previewNext()`: 预览下一个文件
- `previewPrev()`: 预览上一个文件
- `updatePreviewArea()`: 更新预览区域 UI
4. **事件绑定**:绑定预览区域按钮事件
### 3.3 CSS 修改要点
1. **整体容器**:改为横向 flex 布局
2. **左侧区域**:保持原有结构,设置宽度比例(如 50%)
3. **右侧区域**:
- 设置宽度比例(如 50%)
- 纵向 flex 布局
- toolbar 占 10% 高度
- 预览内容区占 90% 高度
4. **Toolbar 样式**:按钮、进度显示等
## 四、实施步骤
### 步骤 1:修改 CSS 样式
```css
/* 整体布局 - 横向排列 */
.g3-file-container {
height: 100%;
display: flex;
background: #ffffff;
overflow: hidden;
}
/* 左侧区域 */
.g3-file-left-panel {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid #eef0f2;
}
/* 右侧预览区域 */
.g3-file-right-panel {
flex: 1;
display: flex;
flex-direction: column;
background: #fafbfc;
}
/* 预览 Toolbar */
.g3-preview-toolbar {
height: 10%;
min-height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
background: #fff;
border-bottom: 1px solid #eef0f2;
}
/* 预览内容区域 */
.g3-preview-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
overflow: auto;
}
/* 预览空状态 */
.g3-preview-empty {
text-align: center;
color: #999;
font-size: 14px;
}
/* 预览文件信息 */
.g3-preview-file-info {
text-align: center;
}
/* 预览按钮 */
.g3-preview-btn {
padding: 6px 12px;
border: 1px solid #d9d9d9;
background: #fff;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.g3-preview-btn:hover:not(:disabled) {
border-color: #165dff;
color: #165dff;
}
.g3-preview-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 预览状态文字 */
.g3-preview-status {
font-size: 13px;
color: #666;
}
```
### 步骤 2:修改 JS 渲染方法
修改 `render()` 方法的 HTML 结构:
```javascript
render: function () {
const html = `
<div class="g3-file-container">
<!-- 左侧区域 -->
<div class="g3-file-left-panel">
<div class="header">...</div>
<div class="main" style="display: none;">main</div>
<div class="empty-tip">...</div>
<div class="footer">...</div>
</div>
<!-- 右侧预览区域 -->
<div class="g3-file-right-panel">
<div class="g3-preview-toolbar">
<button class="g3-preview-btn preview-prev" disabled>上一个</button>
<div class="g3-preview-status">
<span class="preview-counter">第 0/0 个文件</span>
<span class="preview-filename" style="margin-left: 12px;"></span>
</div>
<button class="g3-preview-btn preview-next" disabled>下一个</button>
</div>
<div class="g3-preview-content">
<div class="g3-preview-empty">请选择一个已上传的文件进行预览</div>
</div>
</div>
<input type="file" class="file-input" style="display: none;" multiple />
<div class="g3-global-menu"></div>
</div>
`;
this.$container.html(html);
// 缓存 DOM 元素
this.$leftPanel = this.$container.find(".g3-file-left-panel");
this.$rightPanel = this.$container.find(".g3-file-right-panel");
this.$fileList = this.$leftPanel.find(".main");
this.$emptyTip = this.$leftPanel.find(".empty-tip");
this.$checkAll = this.$leftPanel.find(".check-all-checkbox");
this.$btnDownload = this.$leftPanel.find(".download-all");
this.$btnDelete = this.$leftPanel.find(".delete-all");
this.$btnUpload = this.$leftPanel.find(".upload");
this.$btnSelect = this.$leftPanel.find(".select");
this.$fileInput = this.$container.find(".file-input");
this.$pendingCount = this.$leftPanel.find(".pending-number");
// 预览区域元素
this.$previewToolbar = this.$container.find(".g3-preview-toolbar");
this.$previewContent = this.$container.find(".g3-preview-content");
this.$btnPreviewPrev = this.$container.find(".preview-prev");
this.$btnPreviewNext = this.$container.find(".preview-next");
this.$previewCounter = this.$container.find(".preview-counter");
this.$previewFilename = this.$container.find(".preview-filename");
return this;
}
```
### 步骤 3:添加预览相关状态和方法
```javascript
// 构造函数中添加
this.currentPreviewIndex = -1;
// 获取可预览文件(已上传文件)
getPreviewableFiles: function () {
return this.files.filter(file => file.status === 'uploaded');
},
// 预览指定文件
previewFile: function (file) {
const previewableFiles = this.getPreviewableFiles();
const index = previewableFiles.findIndex(f => f._id === file._id);
if (index !== -1) {
this.currentPreviewIndex = index;
this.updatePreviewArea();
}
},
// 预览下一个
previewNext: function () {
const previewableFiles = this.getPreviewableFiles();
if (previewableFiles.length === 0) return;
if (this.currentPreviewIndex < previewableFiles.length - 1) {
this.currentPreviewIndex++;
this.updatePreviewArea();
}
},
// 预览上一个
previewPrev: function () {
if (this.currentPreviewIndex > 0) {
this.currentPreviewIndex--;
this.updatePreviewArea();
}
},
// 更新预览区域
updatePreviewArea: function () {
const previewableFiles = this.getPreviewableFiles();
const total = previewableFiles.length;
// 更新计数器
if (this.currentPreviewIndex >= 0 && this.currentPreviewIndex < total) {
this.$previewCounter.text(`第 ${this.currentPreviewIndex + 1}/${total} 个文件`);
this.$previewFilename.text(`当前: ${previewableFiles[this.currentPreviewIndex].name}`);
} else {
this.$previewCounter.text(`第 0/${total} 个文件`);
this.$previewFilename.text('');
}
// 更新按钮状态
this.$btnPreviewPrev.prop('disabled', this.currentPreviewIndex <= 0);
this.$btnPreviewNext.prop('disabled',
this.currentPreviewIndex >= total - 1 || total === 0);
// 更新预览内容
if (this.currentPreviewIndex >= 0 && this.currentPreviewIndex < total) {
const file = previewableFiles[this.currentPreviewIndex];
this.$previewContent.html(`
<div class="g3-preview-file-info">
<h3>${file.name}</h3>
<p>大小: ${this.formatFileSize(file.size)}</p>
<p>上传时间: ${this.formatDate(file.uploadDate)}</p>
<p>状态: 已上传</p>
<p>URL: ${file.url || '-'}</p>
</div>
`);
} else {
this.$previewContent.html('<div class="g3-preview-empty">请选择一个已上传的文件进行预览</div>');
}
}
```
### 步骤 4:绑定预览事件
```javascript
bindEvents: function () {
var self = this;
// ... 原有事件绑定 ...
// 预览下一个按钮
this.$btnPreviewNext.off("click").on("click", function () {
self.previewNext();
});
// 预览上一个按钮
this.$btnPreviewPrev.off("click").on("click", function () {
self.previewPrev();
});
// 点击文件项预览(只对已上传文件生效)
this.$fileList
.off("click", ".g3-file-item")
.on("click", ".g3-file-item", function (e) {
if ($(e.target).closest(".g3-file-action, .g3-file-checkbox, .g3-file-delete-btn").length) {
return;
}
var id = $(this).data("id");
var file = self.files.find(f => f._id === id);
if (!file) return;
// 只有已上传文件才能预览
if (file.status === 'uploaded') {
self.previewFile(file);
}
if (typeof self.options.onPreview === "function") {
self.options.onPreview([file]);
}
});
}
```
### 步骤 5:更新文件操作后的预览状态
在 `internalDelete()`、`updateFileStatus()` 等方法中调用 `updatePreviewArea()`
## 五、注意事项
1. **预览计数**:只统计 `status === 'uploaded'` 的文件
2. **预览权限**:只有已上传文件才能预览
3. **边界处理**:
- 没有可预览文件时,工具栏按钮禁用
- 处于第一个文件时,上一个按钮禁用
- 处于最后一个文件时,下一个按钮禁用
4. **预览内容**:暂时显示测试内容(文件名、大小、日期等信息)
## 六、测试验证
1. 上传文件后,预览区域显示可预览文件数量
2. 点击已上传文件,预览区域显示文件信息
3. 上下按钮正确切换预览文件
4. 删除文件后,预览区域正确更新
5. 未上传文件点击不触发预览
@@ -0,0 +1,418 @@
# G3File jQuery 到 Vue 3 迁移计划
## 1. 需求分析
用户需要将 `custom-file/index.js` 中的 jQuery 代码迁移到 `custom-file-vue/index.js`,使用 Vue 3 的 `vue.global.prod.js`。要求:
* 代码逻辑不变
* CSS 代码不变
## 2. 原 jQuery 代码结构分析
### 2.1 主要功能模块
| 模块 | 功能描述 |
| ----- | --------------------- |
| 配置与常量 | 默认配置、多语言、图标映射 |
| 工具方法 | 文件图标、图片压缩、大小格式化、日期格式化 |
| UI 渲染 | 主容器、文件列表、操作菜单、任务面板、通知 |
| 事件绑定 | 文件选择、拖拽、粘贴、按钮点击 |
| 文件处理 | 选择、下载、删除、上传 |
| 任务管理 | 批量下载/上传进度跟踪 |
| 通知系统 | 消息提示 |
| 权限控制 | 全局权限和单文件权限 |
### 2.2 核心数据结构
```javascript
// 文件对象
{
id: string,
name: string,
size: number,
uploadDate: string,
url: string,
file: File,
status: 'pending' | 'uploading' | 'uploaded' | 'failed'
}
// 选中文件集合
selectedFiles: Set<string>
// 任务状态
taskStats: { total, success, fail }
```
## 3. Vue 3 迁移方案
### 3.1 组件结构
```
G3File.vue (主组件)
├── 模板:HTML 结构
├── 脚本:响应式数据和方法
└── 样式:引用原 CSS
```
### 3.2 迁移策略
| jQuery 特性 | Vue 3 等价实现 |
| --------------- | ----------------------- |
| `$(selector)` | `ref` + `template refs` |
| `.html()` | `v-html` / 模板插值 |
| `.on('event')` | `@event` 指令 |
| `.off('event')` | Vue 自动管理 |
| `$.extend()` | 展开运算符 |
| `$.fn` 插件 | Vue 组件 |
### 3.3 关键迁移点
1. **DOM 操作 → 模板渲染**
* 主容器 HTML 使用 Vue 模板
* 文件列表使用 `v-for` 渲染
* 条件渲染使用 `v-if`/`v-show`
2. **事件处理**
* 选择文件:`@change`
* 拖拽:`@dragenter`, `@dragover`, `@dragleave`, `@drop`
* 粘贴:`@paste`
* 按钮点击:`@click`
3. **响应式数据**
* `files` → `ref([])`
* `selectedFiles` → `ref(new Set())`
* `actionMenuState` → `reactive({})`
* `tasks` → `ref([])`
4. **生命周期**
* `init()` → `onMounted()`
* `destroy()` → `onUnmounted()`
5. Setup写法
## 4. 迁移步骤
### 步骤 1:创建 Vue 组件结构
```javascript
const { createApp, ref, reactive, onMounted, onUnmounted, computed } = Vue
const G3File = {
template: `...`,
props: {
options: { type: Object, default: () => ({}) }
},
setup(props) {
// 响应式数据
const files = ref([])
const selectedFiles = ref(new Set())
const objectUrls = ref(new Map())
const actionMenuState = reactive({ open: false, fileId: null, anchorEl: null, items: [] })
const tasks = ref([])
const taskStats = reactive({ total: 0, success: 0, fail: 0 })
const notifications = ref([])
// 计算属性
const pendingCount = computed(() => files.value.filter(f => f.status === 'pending').length)
const allSelected = computed(() => files.value.length > 0 && selectedFiles.value.size === files.value.length)
// 方法
const generateUniqueId = () => 'file_' + Math.random().toString(36).slice(2) + Date.now()
const getFileIcon = (filename) => { /* ... */ }
const compressImage = async (file, options) => { /* ... */ }
const isImageFile = (filename) => { /* ... */ }
const formatFileSize = (bytes) => { /* ... */ }
const formatDate = (dateStr) => { /* ... */ }
const getUniqueFileName = (fileName, existing) => { /* ... */ }
const getFileBlob = async (url) => { /* ... */ }
// UI 更新方法
const updateUI = () => { /* ... */ }
const renderFileList = () => { /* ... */ }
// 文件操作方法
const handleFileSelect = async (event) => { /* ... */ }
const handleDownload = (fileList, options) => { /* ... */ }
const handleDelete = (fileList) => { /* ... */ }
const handleUpload = (fileList) => { /* ... */ }
// 任务管理
const addTask = (task) => { /* ... */ }
const updateTask = (taskId, updates) => { /* ... */ }
const finishTask = (taskId, status) => { /* ... */ }
const clearTasks = () => { /* ... */ }
// 通知系统
const showNotification = (options) => { /* ... */ }
const closeNotification = (notificationId) => { /* ... */ }
// 权限控制
const hasPermission = (file, action) => { /* ... */ }
// 生命周期
onMounted(() => {
// 初始化脚本加载
// 绑定全局事件(拖拽、粘贴)
})
onUnmounted(() => {
// 清理 ObjectURL
// 解绑全局事件
})
return {
files,
selectedFiles,
actionMenuState,
tasks,
taskStats,
notifications,
pendingCount,
allSelected,
// ... methods
}
}
}
```
### 步骤 2:模板结构
```html
<div class="g3-file-container" :class="{ 'drag-over': isDragOver }">
<!-- 头部 -->
<div class="header">
<label class="check-all">
<input type="checkbox" v-model="allSelected" />
<span>{{ language.selectAll }}</span>
</label>
<button class="download-all" :disabled="!canBatchDownload" @click="handleBatchDownload">
{{ staticIconMap.download }}
{{ language.downloadBatch }}
</button>
<button class="delete-all" :disabled="!canBatchDelete" @click="handleBatchDelete">
{{ staticIconMap.delete }}
{{ language.delete }}
</button>
</div>
<!-- 文件列表 -->
<div class="main" v-show="files.length > 0">
<div
v-for="file in files"
:key="file.id"
class="g3-file-item"
@click="handleFileClick(file)"
>
<input type="checkbox" v-model="selectedFiles.has(file.id)" />
<label class="g3-checkbox-label"></label>
<div class="g3-file-preview">
<!-- 图片或图标 -->
</div>
<div v-if="file.status === 'pending'" class="g3-file-pending-badge">
{{ language.pending }}
</div>
<div class="g3-file-delete-btn" @click.stop="handleDelete([file])">
{{ staticIconMap.delete }}
</div>
<div class="g3-file-info">
<span class="g3-file-name">{{ file.name }}</span>
<span>{{ formatFileSize(file.size) }}</span>
<span>{{ formatDate(file.uploadDate) }}</span>
</div>
<div class="g3-file-actions">
<div class="g3-file-action preview" @click.stop="handlePreview(file)">
{{ staticIconMap.preview }}
</div>
<div class="g3-file-action download" @click.stop="handleDownload([file], { batch: false })">
{{ staticIconMap.download }}
</div>
<div class="g3-file-action more" @click.stop="openActionMenu($event, file)">
{{ staticIconMap.more }}
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div class="empty-tip" v-show="files.length === 0">
<span>{{ language.emptyTip1 }}</span>
<span>{{ language.emptyTip2 }}</span>
<span>{{ language.emptyTip3 }}</span>
</div>
<!-- 底部 -->
<div class="footer">
<div class="pending-count">
<span>{{ language.pending }}</span>
<span class="pending-number">{{ pendingCount }}</span>
</div>
<div>
<button class="select" :disabled="!hasPermission(null, 'select')" @click="triggerFileInput">
{{ staticIconMap.select }}
{{ language.select }}
</button>
<button class="upload" :disabled="!hasPermission(null, 'upload') || pendingCount === 0" @click="handleUpload(files)">
{{ staticIconMap.upload }}
{{ language.upload }}
</button>
</div>
<input type="file" class="file-input" style="display: none;" multiple @change="handleFileSelect" ref="fileInput" />
</div>
<!-- 操作菜单 -->
<div class="g3-global-menu" :class="{ 'is-open': actionMenuState.open }">
<div
v-for="(item, index) in actionMenuState.items"
:key="index"
class="g3-action-menu-item"
:class="{ 'is-disabled': item.disabled }"
@click="handleActionMenuItemClick(item)"
>
{{ item.title }}
</div>
</div>
<!-- 任务面板 -->
<div class="task-dock" v-show="tasks.length > 0">
<div class="dock-header">
<span class="dock-title">Task Running</span>
<div class="dock-actions">
<button title="close" @click="clearTasks">×</button>
</div>
</div>
<div class="dock-body">
<div class="progress-line">
<span class="count">{{ taskStats.success + taskStats.fail }} / {{ taskStats.total }}</span>
<span class="percent">{{ Math.round(totalProgress) }}%</span>
</div>
<div class="task-name">{{ currentTask?.name }}</div>
<div class="bar">
<div class="bar-inner" :style="{ width: currentTask?.progress + '%' }"></div>
</div>
<div class="task-result">
{{ language.taskTotal }}: {{ taskStats.total }} |
{{ language.taskSuccess }}: {{ taskStats.success }} |
{{ language.taskFail }}: {{ taskStats.fail }}
</div>
</div>
</div>
<!-- 通知容器 -->
<div class="notification-container">
<div
v-for="notification in notifications"
:key="notification.id"
class="notification"
:class="[`notification-${notification.type}`, { 'notification-fade-out': notification.closing }]"
>
<div class="notification-content">
<div v-if="notification.title" class="notification-title">{{ notification.title }}</div>
<div class="notification-message">{{ notification.message }}</div>
</div>
<button v-if="notification.showClose" class="notification-close" @click="closeNotification(notification.id)">×</button>
</div>
</div>
<!-- UI Lock -->
<div class="ui-lock" v-show="isUILocked"></div>
</div>
```
### 步骤 3:工具方法迁移
所有工具方法保持逻辑不变,只是移除 jQuery 依赖:
* `generateUniqueId` - 保持不变
* `getFileIcon` - 保持不变(纯字符串处理)
* `compressImage` - 保持不变(使用原生 API)
* `isImageFile` - 保持不变
* `formatFileSize` - 保持不变
* `formatDate` - 保持不变
* `getUniqueFileName` - 保持不变
* `getFileBlob` - 保持不变(使用 fetch API)
### 步骤 4:事件处理迁移
| jQuery 事件 | Vue 事件 |
| ---------------------------------------------------- | ------------------------------------------------ |
| `$(document).on('paste')` | 在 `onMounted` 中绑定原生事件 |
| `$(document).on('click.g3file_actionmenu')` | Vue 模板事件 |
| `$container.on('dragenter dragover dragleave drop')` | `@dragenter`, `@dragover`, `@dragleave`, `@drop` |
| `$fileInput.on('change')` | `@change` |
### 步骤 5:权限控制迁移
保持原有的权限判断逻辑:
```javascript
const hasPermission = (file, action) => {
const globalPermission = props.options.globalPermission
const filePermissionResolver = props.options.filePermissionResolver
if (typeof filePermissionResolver === 'function') {
const result = filePermissionResolver({ file, action })
if (result !== undefined) return !!result
}
if (typeof globalPermission === 'function') {
return !!globalPermission({ file, action })
} else if (typeof globalPermission === 'object') {
return !!globalPermission[action]
}
return true
}
```
## 5. 文件复制
由于 CSS 代码不需要改变,需要将 `custom-file/index.css` 复制到 `custom-file-vue/index.css`(如果不存在差异)。
## 6. 测试验证
迁移完成后需要验证:
1. 文件列表显示正常
2. 文件选择功能正常(点击、拖拽、粘贴)
3. 文件上传功能正常
4. 文件下载功能正常(单个和批量)
5. 文件删除功能正常
6. 任务面板显示正常
7. 通知系统正常
8. 权限控制正常
9. 多语言支持正常
## 7. 潜在风险
1. **jQuery 插件封装**:原代码使用 `$.fn.G3File` 封装,迁移后需要提供 Vue 组件注册方式
2. **DOM 引用**:需要确保所有模板 ref 正确绑定
3. **全局事件**:需要在 `onUnmounted` 中正确清理
4. **异步操作**:确保所有异步操作(如文件上传、下载)正确处理
## 8. 输出文件
* `custom-file-vue/index.js` - Vue 3 组件实现
* `custom-file-vue/index.css` - 样式文件(保持不变)
@@ -0,0 +1,82 @@
# 批量操作进度通知重构计划
## 需求分析
用户要求修改批量操作(下载、上传、删除等)的通知显示逻辑:
1. **单一通知**:批量操作只显示一个 notification
2. **手动关闭**:通知不会自动关闭,需要用户手动关闭
3. **实时更新**:显示当前正在处理的文件名和进度
4. **结果统计**:操作完成后显示总数、成功数、失败数
5. **多语言支持**:使用现有语言配置,不写死文字
6. **通用设计**:支持批量下载、上传、删除等多种操作类型
7. **UI锁定**:操作进行时自动锁定UI,操作结束后自动解锁(与通知关闭无关)
## 修改内容
### 1. 新增任务组概念
在实例中添加 `currentTaskGroup` 来管理当前批量操作的通知状态:
```javascript
this.currentTaskGroup = null; // 当前任务组 { taskType, notificationId, total }
```
### 2. 修改 addTask 方法
- 检查是否已有同类型的任务组
- 如果没有,创建新的任务组和通知
- 如果有,复用已有的通知
### 3. 修改 updateTask 方法
- 更新通知中的当前文件名和进度百分比
- 显示格式:`正在下载:文件名 (进度%)`
### 4. 修改 finishTask 方法
- 所有任务完成后更新通知显示结果统计
- 解锁 UI(与通知关闭无关)
- 通知保持显示,等待用户手动关闭
### 5. 修改 clearTasks 方法
- 不自动关闭通知(用户手动关闭)
- 只是重置任务状态
### 6. 修改 closeNotification 方法
- 关闭通知时清理对应的任务组
### 7. 更新多语言配置
确保以下语言键存在:
- `batchDownload`: 批量下载
- `batchUpload`: 批量上传
- `batchDelete`: 批量删除
- `downloading`: 正在下载
- `uploading`: 正在上传
- `deleting`: 正在删除
- `downloadResult`: 结果统计格式
## 修改步骤
1. 在 `init` 或构造函数中添加 `currentTaskGroup` 状态
2. 修改 `addTask` 方法支持任务组概念
3. 修改 `updateTask` 方法更新进度显示
4. 修改 `finishTask` 方法显示最终结果并解锁UI
5. 修改 `clearTasks` 方法不自动关闭通知
6. 修改 `closeNotification` 方法清理任务组
7. 验证多语言支持
## 风险评估
| 风险项 | 风险等级 | 处理方式 |
|--------|----------|----------|
| 任务组状态管理 | 中 | 使用单一任务组管理同一类型的批量操作 |
| 通知重复创建 | 低 | 添加检查逻辑,避免重复创建 |
| UI锁定与通知生命周期不一致 | 中 | 分离 UI 锁定和通知关闭逻辑 |
## 兼容性
- 原有 `showNotification` 调用方式保持兼容
- 任务管理方法保持向后兼容
@@ -0,0 +1,108 @@
# Notification 重构计划
## 需求分析
用户要求将现有的 `notification` 和 `dock` 两个组件合并,统一使用 `notification` 来显示所有通知和进度信息。
### 修改目标
1. **统一使用 notification**:删除 dock 组件,所有进度显示改用 notification
2. **下载进度结构不变**:保持现有的任务进度数据结构
3. **开放 notification 内容**:支持更灵活的内容传递
4. **添加 lockUI 选项**:在 showNotification 中增加是否锁定 UI 的选项
***
## 修改内容
### 1. 删除 DOM 结构中的 dock
**文件**: `custom-file/index.js`
删除 `render` 方法中的 task-dock HTML 结构:
```html
<div class="task-dock">...</div>
<div class="ui-lock"></div>
```
### 2. 删除 dock 相关变量
删除 `render` 方法中的:
* `this.$taskDock = this.$container.find(".task-dock");`
* `this.$uiLock = this.$container.find(".ui-lock");`
### 3. 修改 showNotification 方法
增加以下 options:
* `lockUI`: 是否锁定 UI(默认 false)
* `progress`: 进度百分比(0-100),用于显示进度条
* `customContent`: 自定义内容(支持 HTML)
### 4. 修改任务管理方法
修改以下方法使用 notification 显示进度:
* `addTask`: 创建任务时显示通知
* `updateTask`: 更新任务进度时更新通知
* `finishTask`: 完成任务时更新通知状态
* `clearTasks`: 清空任务时关闭通知
### 5. 删除 dock 相关方法
删除以下方法:
* `showTaskDock`
* `hideTaskDock`
* `updateTaskDock`
* `bindTaskDockEvents`
* `lockUI`(改为在 showNotification 中处理)
* `unlockUI`(改为在通知关闭时处理)
* `isUILocked`
### 6. 修改批量下载逻辑
修改 `internalDownloadBatch` 方法,使用 notification 显示下载进度。
***
## 风险评估
| 风险项 | 风险等级 | 处理方式 |
| --------- | ---- | ---------------------------- |
| 任务进度状态丢失 | 低 | 保留 tasks 数组用于状态管理 |
| UI 锁定逻辑变化 | 中 | 在 notification 中实现 lockUI 选项 |
| 通知样式需要调整 | 低 | 保持原有样式,增加进度条样式 |
***
## 兼容性
* 原有 `showNotification` 调用方式保持兼容
* 新增参数为可选参数,不影响现有代码
***
## 实施步骤
1. 修改 `render` 方法,删除 dock 和 ui-lock 的 DOM 结构
2. 修改 `showNotification` 方法,增加 lockUI、progress、customContent 选项
3. 修改任务管理方法,使用 notification 显示进度
4. 删除 dock 相关方法
5. 修改 `internalDownloadBatch` 使用新的通知机制
@@ -0,0 +1,211 @@
# UI 提示系统重构计划
## 需求分析
用户要求重新设计并实现一个轻量 UI 提示系统,包含:
1. **Toast(右上角)** - 瞬时提示,无状态设计
2. **Task Center(右下角任务中心)** - 长任务状态系统,有状态
核心要求:
- 两者完全独立,不共享逻辑
- Task 必须通用化,不绑定具体业务类型
- 实现 UI Lock 机制
- 支持多语言国际化
---
## 架构设计
### 1. 整体架构
```
┌─────────────────────────────────────────────────────────────┐
│ UI 提示系统 │
├───────────────────────────┬─────────────────────────────────┤
│ Toast 系统 │ Task Center 系统 │
│ (右上角,无状态) │ (右下角,有状态) │
├───────────────────────────┼─────────────────────────────────┤
│ • 自动消失 │ • 手动关闭 │
│ • 多条堆叠 │ • 单一实例 │
│ • 类型:success/error/ │ • 生命周期独立 │
│ info/warning │ • 进度追踪 │
└───────────────────────────┴─────────────────────────────────┘
```
### 2. Toast API
```js
showToast({
title, // 标题
message, // 消息内容
type: "success | error | info | warning",
duration // 显示时长(毫秒)
});
```
### 3. Task API
```js
// 创建任务
startTask({
id, // 任务ID
title, // 任务标题(国际化key)
total, // 总数量
current, // 当前进度
currentItem // 当前处理项
});
// 更新任务
updateTask({
id, // 任务ID
current, // 当前进度
currentItem // 当前处理项
});
// 完成任务
finishTask({
id, // 任务ID
total, // 总数量
success, // 成功数量
failed // 失败数量
});
// 手动关闭
closeTask(id);
```
---
## 修改内容
### 1. 修改 CSS 文件 (`custom-file/index.css`)
**新增样式:**
- Toast 容器样式(右上角固定定位)
- Toast 单条样式(支持不同类型)
- Toast 动画(进入/退出)
- Task Center 容器样式(右下角固定定位)
- Task Center 进度条样式
- Task Center 结果展示样式
- UI Lock 样式
**删除旧样式:**
- 删除 `.task-dock` 相关样式(如仍存在)
### 2. 修改 JS 文件 (`custom-file/index.js`)
**新增方法:**
| 方法名 | 功能 |
|--------|------|
| `showToast` | 显示 Toast 提示 |
| `startTask` | 创建并启动任务 |
| `updateTask` | 更新任务进度 |
| `finishTask` | 完成任务 |
| `closeTask` | 关闭任务 UI |
| `lockUI` | 锁定 UI |
| `unlockUI` | 解锁 UI |
| `isUILocked` | 检查 UI 是否锁定 |
**修改方法:**
- `render` - 添加 Toast 和 Task Center 的 DOM 结构
- `internalDownloadBatch` - 使用新的 Task API
**删除方法:**
- `addTask`(旧版本)
- `updateTask`(旧版本)
- `finishTask`(旧版本)
- `clearTasks`(旧版本)
- `updateTaskStats`(旧版本)
### 3. 新增多语言配置
```js
languages: {
"zh-CN": {
// ... 现有配置
toast: {
success: "成功",
error: "错误",
info: "提示",
warning: "警告"
},
task: {
title: {
processing: "处理中"
},
total: "总计",
success: "成功",
failed: "失败",
currentItem: "当前项",
close: "关闭"
}
},
"en-US": {
// ... 现有配置
toast: {
success: "Success",
error: "Error",
info: "Info",
warning: "Warning"
},
task: {
title: {
processing: "Processing"
},
total: "Total",
success: "Success",
failed: "Failed",
currentItem: "Current",
close: "Close"
}
}
}
```
---
## 实施步骤
| 步骤 | 任务 | 文件 | 状态 |
|------|------|------|------|
| 1 | 添加 Toast 和 Task Center 的 CSS 样式 | `custom-file/index.css` | 待执行 |
| 2 | 更新多语言配置 | `custom-file/index.js` | 待执行 |
| 3 | 添加 Toast 相关方法 | `custom-file/index.js` | 待执行 |
| 4 | 添加 Task Center 相关方法 | `custom-file/index.js` | 待执行 |
| 5 | 添加 UI Lock 机制 | `custom-file/index.js` | 待执行 |
| 6 | 修改 render 方法添加 DOM 结构 | `custom-file/index.js` | 待执行 |
| 7 | 修改 internalDownloadBatch 使用新 API | `custom-file/index.js` | 待执行 |
| 8 | 删除旧的任务管理方法 | `custom-file/index.js` | 待执行 |
---
## 风险评估
| 风险项 | 风险等级 | 处理方式 |
|--------|----------|----------|
| Task 状态与 UI 不同步 | 中 | 确保 task 对象独立于 UI 存在 |
| Toast 堆叠过多 | 低 | 限制最大显示数量(如5条) |
| UI Lock 状态异常 | 中 | 在任务完成时强制解锁 |
| 多语言文案遗漏 | 低 | 统一使用 `this.language.xxx` 模式 |
---
## 兼容性
- 原有 API 调用保持兼容
- 新增 API 为可选参数
- 不影响现有文件管理功能
---
## 最终目标
实现一个 **"通用任务系统 + Toast 提示系统 + UI Lock + i18n"** 的轻量 UI 架构,支持未来扩展:
- 批量上传
- 批量下载
- 批量删除
- 任意异步任务
无需修改核心结构,仅通过 task 数据驱动。
@@ -1,624 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Global Feedback Dock Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
margin-bottom: 20px;
color: #2c3e50;
}
.demo-section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h2 {
margin-bottom: 15px;
color: #34495e;
font-size: 18px;
}
.button-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
button:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-success {
background-color: #2ecc71;
color: white;
}
.btn-danger {
background-color: #e74c3c;
color: white;
}
.btn-warning {
background-color: #f39c12;
color: white;
}
.btn-info {
background-color: #1abc9c;
color: white;
}
/* 全局反馈 Dock 样式 */
.g3-feedback-dock {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 10000;
background: white;
border-bottom: 1px solid #e0e0e0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transform: translateY(-100%);
transition: transform 0.3s ease;
}
.g3-feedback-dock.show {
transform: translateY(0);
}
.g3-feedback-dock.toast-mode {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.dock-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
border-bottom: 1px solid #f0f0f0;
}
.dock-title {
font-weight: 600;
font-size: 14px;
color: #2c3e50;
}
.dock-actions {
display: flex;
gap: 8px;
}
.dock-actions button {
padding: 4px 8px;
font-size: 12px;
background: #f5f5f5;
color: #666;
border-radius: 4px;
}
.dock-actions button:hover {
background: #e0e0e0;
}
.dock-body {
padding: 16px 20px;
}
.progress-line {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 13px;
color: #666;
}
.task-name {
margin-bottom: 12px;
font-size: 13px;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bar {
height: 6px;
background: #f0f0f0;
border-radius: 3px;
overflow: hidden;
margin-bottom: 12px;
}
.bar-inner {
height: 100%;
background: #3498db;
transition: width 0.3s ease;
border-radius: 3px;
}
.task-result {
font-size: 12px;
color: #666;
}
/* Toast 样式 */
.toast-content {
display: flex;
align-items: center;
gap: 12px;
}
.toast-icon {
font-size: 18px;
}
.toast-message {
flex: 1;
font-size: 14px;
}
/* Toast 类型样式 */
.g3-feedback-dock.toast-mode.success {
border-left: 4px solid #2ecc71;
}
.g3-feedback-dock.toast-mode.error {
border-left: 4px solid #e74c3c;
}
.g3-feedback-dock.toast-mode.warning {
border-left: 4px solid #f39c12;
}
.g3-feedback-dock.toast-mode.info {
border-left: 4px solid #3498db;
}
/* UI 锁定遮罩 */
.ui-lock {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255,255,255,0.7);
z-index: 9999;
display: none;
}
/* 响应式设计 */
@media (max-width: 768px) {
.g3-feedback-dock {
font-size: 13px;
}
.dock-header,
.dock-body {
padding: 10px 16px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>Global Feedback Dock Demo</h1>
<div class="demo-section">
<h2>1. 任务模式(Task Mode)</h2>
<p>用于展示批量任务进度,如下载、上传等</p>
<div class="button-group">
<button class="btn-primary" onclick="startDownloadTask()">开始批量下载</button>
<button class="btn-success" onclick="startUploadTask()">开始批量上传</button>
</div>
</div>
<div class="demo-section">
<h2>2. 提示模式(Toast Mode)</h2>
<p>用于单次操作反馈,如操作成功、失败等</p>
<div class="button-group">
<button class="btn-success" onclick="showSuccessToast()">成功提示</button>
<button class="btn-danger" onclick="showErrorToast()">错误提示</button>
<button class="btn-warning" onclick="showWarningToast()">警告提示</button>
<button class="btn-info" onclick="showInfoToast()">信息提示</button>
</div>
</div>
<div class="demo-section">
<h2>3. 单个文件下载失败示例</h2>
<p>模拟单个文件下载失败的场景</p>
<div class="button-group">
<button class="btn-danger" onclick="simulateDownloadFailure()">模拟下载失败</button>
</div>
</div>
</div>
<!-- 全局反馈 Dock 容器 -->
<div class="g3-feedback-dock" id="feedbackDock">
<div class="dock-header">
<span class="dock-title">任务进行中</span>
<div class="dock-actions">
<button title="close" onclick="feedbackDock.clear()">×</button>
</div>
</div>
<div class="dock-body">
<!-- 任务模式内容 -->
<div class="task-content">
<div class="progress-line">
<span class="count">0 / 0</span>
<span class="percent">0%</span>
</div>
<div class="task-name"></div>
<div class="bar">
<div class="bar-inner"></div>
</div>
<div class="task-result">
总计: 0 | 成功: 0 | 失败: 0
</div>
</div>
<!-- Toast 模式内容 -->
<div class="toast-content" style="display: none;">
<div class="toast-icon"></div>
<div class="toast-message"></div>
</div>
</div>
</div>
<!-- UI 锁定遮罩 -->
<div class="ui-lock" id="uiLock"></div>
<script>
// 全局反馈 Dock 组件
class FeedbackDock {
constructor() {
this.dock = document.getElementById('feedbackDock');
this.uiLock = document.getElementById('uiLock');
this.tasks = [];
this.taskStats = {
total: 0,
success: 0,
fail: 0
};
this.toastQueue = [];
this.isToastShowing = false;
this.currentMode = 'task'; // 'task' or 'toast'
}
// 生成唯一ID
generateUniqueId() {
return 'feedback_' + Math.random().toString(36).slice(2) + Date.now();
}
// 开始任务
startTask(taskConfig) {
const taskId = this.generateUniqueId();
const newTask = {
id: taskId,
name: taskConfig.name,
type: taskConfig.type || 'custom',
progress: 0,
status: 'running'
};
this.tasks.push(newTask);
this.updateTaskStats();
this.switchMode('task');
this.show();
this.lockUI();
return taskId;
}
// 更新任务
updateTask(taskId, progress) {
const task = this.tasks.find(t => t.id === taskId);
if (task) {
task.progress = progress;
this.updateTaskDock();
}
}
// 完成任务
finishTask(taskId, status) {
const task = this.tasks.find(t => t.id === taskId);
if (task) {
task.status = status || 'success';
task.progress = 100;
this.updateTaskStats();
this.updateTaskDock();
const allCompleted = this.tasks.every(
t => t.status === 'success' || t.status === 'fail'
);
if (allCompleted) {
this.unlockUI();
// 3秒后自动隐藏
setTimeout(() => {
this.clear();
}, 3000);
}
}
}
// 显示 Toast
toast(options) {
const toastConfig = {
type: options.type || 'info',
message: options.message || '',
duration: options.duration || 3000
};
this.toastQueue.push(toastConfig);
if (!this.isToastShowing) {
this.showNextToast();
}
}
// 显示下一个 Toast
showNextToast() {
if (this.toastQueue.length === 0) {
this.isToastShowing = false;
return;
}
this.isToastShowing = true;
const toastConfig = this.toastQueue.shift();
this.switchMode('toast');
this.updateToast(toastConfig);
this.show();
// 自动隐藏
setTimeout(() => {
this.hide();
setTimeout(() => {
this.showNextToast();
}, 300);
}, toastConfig.duration);
}
// 切换模式
switchMode(mode) {
this.currentMode = mode;
const taskContent = this.dock.querySelector('.task-content');
const toastContent = this.dock.querySelector('.toast-content');
if (mode === 'task') {
this.dock.classList.remove('toast-mode', 'success', 'error', 'warning', 'info');
taskContent.style.display = 'block';
toastContent.style.display = 'none';
} else {
this.dock.classList.add('toast-mode');
taskContent.style.display = 'none';
toastContent.style.display = 'flex';
}
}
// 更新任务面板
updateTaskDock() {
if (this.tasks.length === 0) return;
// 获取当前正在运行的任务,或者最后一个任务
const currentTask = this.tasks.find(t => t.status === 'running') || this.tasks[this.tasks.length - 1];
if (currentTask) {
const totalProgress = this.tasks.reduce((sum, task) => sum + task.progress, 0) / this.tasks.length;
this.dock.querySelector('.dock-title').textContent = currentTask.type === 'upload' ? '上传任务' : '下载任务';
this.dock.querySelector('.progress-line .count').textContent = `${this.taskStats.success + this.taskStats.fail} / ${this.taskStats.total}`;
this.dock.querySelector('.progress-line .percent').textContent = `${Math.round(totalProgress)}%`;
this.dock.querySelector('.task-name').textContent = currentTask.name;
this.dock.querySelector('.bar .bar-inner').style.width = `${currentTask.progress}%`;
this.dock.querySelector('.task-result').textContent = `总计: ${this.taskStats.total} | 成功: ${this.taskStats.success} | 失败: ${this.taskStats.fail}`;
}
}
// 更新 Toast
updateToast(config) {
const iconMap = {
success: '✓',
error: '✗',
warning: '!',
info: 'i'
};
this.dock.classList.remove('success', 'error', 'warning', 'info');
this.dock.classList.add(config.type);
this.dock.querySelector('.dock-title').textContent = this.getToastTitle(config.type);
this.dock.querySelector('.toast-icon').textContent = iconMap[config.type];
this.dock.querySelector('.toast-message').textContent = config.message;
}
// 获取 Toast 标题
getToastTitle(type) {
const titles = {
success: '操作成功',
error: '操作失败',
warning: '警告',
info: '信息'
};
return titles[type] || '信息';
}
// 更新任务统计
updateTaskStats() {
this.taskStats.total = this.tasks.length;
this.taskStats.success = this.tasks.filter(t => t.status === 'success').length;
this.taskStats.fail = this.tasks.filter(t => t.status === 'fail').length;
}
// 显示 Dock
show() {
this.dock.classList.add('show');
}
// 隐藏 Dock
hide() {
this.dock.classList.remove('show');
}
// 清空所有任务和 Toast
clear() {
this.tasks = [];
this.taskStats = { total: 0, success: 0, fail: 0 };
this.toastQueue = [];
this.isToastShowing = false;
this.hide();
this.unlockUI();
}
// 锁定 UI
lockUI() {
this.uiLock.style.display = 'block';
}
// 解锁 UI
unlockUI() {
this.uiLock.style.display = 'none';
}
}
// 初始化反馈 Dock
const feedbackDock = new FeedbackDock();
// 示例函数
function startDownloadTask() {
// 模拟批量下载任务
const files = ['file1.pdf', 'file2.jpg', 'file3.docx', 'file4.png', 'file5.txt'];
const taskIds = [];
files.forEach((file, index) => {
const taskId = feedbackDock.startTask({
name: file,
type: 'download'
});
taskIds.push(taskId);
// 模拟下载进度
let progress = 0;
const interval = setInterval(() => {
progress += 10;
if (progress <= 100) {
feedbackDock.updateTask(taskId, progress);
} else {
clearInterval(interval);
// 模拟90%成功率
const status = Math.random() > 0.1 ? 'success' : 'fail';
feedbackDock.finishTask(taskId, status);
}
}, 300);
});
}
function startUploadTask() {
// 模拟批量上传任务
const files = ['image1.png', 'document1.pdf', 'video1.mp4'];
const taskIds = [];
files.forEach((file, index) => {
const taskId = feedbackDock.startTask({
name: file,
type: 'upload'
});
taskIds.push(taskId);
// 模拟上传进度
let progress = 0;
const interval = setInterval(() => {
progress += 5;
if (progress <= 100) {
feedbackDock.updateTask(taskId, progress);
} else {
clearInterval(interval);
// 模拟100%成功率
feedbackDock.finishTask(taskId, 'success');
}
}, 200);
});
}
function showSuccessToast() {
feedbackDock.toast({
type: 'success',
message: '操作成功!'
});
}
function showErrorToast() {
feedbackDock.toast({
type: 'error',
message: '操作失败,请重试!'
});
}
function showWarningToast() {
feedbackDock.toast({
type: 'warning',
message: '请注意,操作可能会影响其他功能!'
});
}
function showInfoToast() {
feedbackDock.toast({
type: 'info',
message: '这是一条信息提示。'
});
}
function simulateDownloadFailure() {
// 模拟单个文件下载失败
const fileName = 'important-document.pdf';
// 模拟下载过程
setTimeout(() => {
// 下载失败,显示错误 Toast
feedbackDock.toast({
type: 'error',
message: `文件 ${fileName} 下载失败`
});
}, 1000);
}
</script>
</body>
</html>
View File
Whitespace-only changes.
View File
Whitespace-only changes.
@@ -1,32 +1,34 @@
/* ============================================================================
* G3File 样式文件
* ============================================================================ */
/* 整体布局 */
.g3-file-container {
height: 100%;
display: flex;
flex-direction: column;
background: #ffffff;
overflow: hidden;
border: 1px solid #eef0f2;
border-radius: 8px;
}
/* 拖拽时的样式 */
.g3-file-container.drag-over {
background: rgba(22, 93, 255, 0.05);
border: 2px dashed #165dff;
}
/* 头部样式 */
.g3-file-left-panel {
width: 50%;
display: flex;
flex-direction: column;
border-right: 1px solid #eef0f2;
min-width: 300px;
}
.g3-file-container .header {
display: flex;
align-items: center;
padding: 10px 14px;
height: 50px;
padding: 0px 14px;
background: #fafbfc;
border-bottom: 1px solid #eef0f2;
}
/* 主要内容区域 */
.g3-file-container .main {
flex: 1;
min-height: 0;
@@ -35,15 +37,12 @@
display: flex;
flex-wrap: wrap;
align-content: flex-start;
border-left: 1px solid #eef0f2;
border-right: 1px solid #eef0f2;
}
.g3-file-container .main > * {
margin: 10px;
}
/* 空状态提示 */
.g3-file-container .empty-tip {
flex: 1;
min-height: 0;
@@ -55,7 +54,6 @@
color: #999;
}
/* 底部样式 */
.g3-file-container .footer {
display: flex;
align-items: center;
@@ -84,7 +82,6 @@
margin-left: 4px;
}
/* 全选 */
.g3-file-container .check-all {
display: flex;
align-items: center;
@@ -103,7 +100,6 @@
color: #333;
}
/* 按钮 */
.g3-file-container button {
font-size: 13px;
border-radius: 4px;
@@ -121,7 +117,7 @@
justify-content: center;
}
.g3-file-container button svg {
.g3-file-container .g3-file-left-panel button svg {
margin-right: 4px;
}
@@ -146,7 +142,6 @@
background: #ff4d4f;
}
/* 文件项样式 */
.g3-file-container .g3-file-item {
width: calc(50% - 26px);
position: relative;
@@ -161,7 +156,6 @@
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
}
/* 文件预览 */
.g3-file-item .g3-file-preview {
height: 100px;
width: 100%;
@@ -181,7 +175,6 @@
width: 100%;
}
/* 图片容器样式 */
.g3-file-item .g3-file-preview .g3-img-container {
width: 100%;
height: 100%;
@@ -191,7 +184,6 @@
justify-content: center;
}
/* 图片错误状态 */
.g3-file-item .g3-file-preview .g3-img-container.is-error .g3-img-error {
display: flex;
align-items: center;
@@ -216,7 +208,6 @@
color: #999;
}
/* 文件信息 */
.g3-file-item .g3-file-info {
padding: 8px;
font-size: 12px;
@@ -246,7 +237,6 @@
color: #000000;
}
/* 文件操作按钮 */
.g3-file-item .g3-file-actions {
display: flex;
align-items: center;
@@ -282,8 +272,13 @@
cursor: not-allowed;
}
.g3-file-item .g3-file-action:first-child {
border-radius: 0 0 0 8px;
}
.g3-file-item .g3-file-action:last-child {
border-right: none;
border-radius: 0 0 8px 0;
}
.g3-file-item .g3-file-action svg {
@@ -292,7 +287,6 @@
color: #666;
}
/* 复选框样式 */
.g3-file-item .g3-file-checkbox {
position: absolute;
top: 8px;
@@ -304,7 +298,6 @@
z-index: 10;
}
/* 自定义复选框样式 */
.g3-file-item .g3-file-checkbox + .g3-checkbox-label {
position: absolute;
top: 8px;
@@ -346,7 +339,6 @@
cursor: not-allowed;
}
/* 未上传标记 */
.g3-file-item .g3-file-pending-badge {
position: absolute;
top: 6px;
@@ -359,7 +351,6 @@
z-index: 1;
}
/* 上传中标记 */
.g3-file-item .g3-file-uploading-badge {
position: absolute;
top: 6px;
@@ -372,7 +363,6 @@
z-index: 1;
}
/* 上传失败标记 */
.g3-file-item .g3-file-failed-badge {
position: absolute;
top: 6px;
@@ -385,7 +375,6 @@
z-index: 1;
}
/* 上传成功标记 */
.g3-file-item .g3-file-uploaded-badge {
position: absolute;
top: 6px;
@@ -398,7 +387,6 @@
z-index: 1;
}
/* 浮动删除按钮 */
.g3-file-container .g3-file-delete-btn {
position: absolute;
top: -8px;
@@ -429,7 +417,192 @@
cursor: not-allowed;
}
/* 全局菜单 */
.g3-file-preview-panel {
flex: 1;
display: flex;
flex-direction: column;
background: #fafbfc;
min-width: 300px;
}
.g3-preview-toolbar {
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
background: #ffffff;
border-bottom: 1px solid #eef0f2;
flex-shrink: 0;
}
.g3-preview-toolbar .preview-nav {
display: flex;
align-items: center;
gap: 8px;
}
.g3-preview-toolbar .preview-btn {
width: 32px;
height: 32px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: #ffffff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
padding: 0;
}
.g3-preview-toolbar .preview-btn:hover:not(:disabled) {
border-color: #165dff;
background: rgba(22, 93, 255, 0.05);
}
.g3-preview-toolbar .preview-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.g3-preview-toolbar .preview-btn svg {
width: 16px;
height: 16px;
color: #666;
}
.g3-preview-toolbar .preview-btn:hover:not(:disabled) svg {
color: #165dff;
}
.g3-preview-toolbar .preview-info {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.g3-preview-toolbar .preview-info .preview-name {
font-size: 13px;
color: #333;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
.g3-preview-toolbar .preview-info .preview-counter {
font-size: 12px;
color: #999;
}
.g3-preview-content {
flex: 1;
height: 90%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
overflow: auto;
}
.g3-preview-content .preview-placeholder {
text-align: center;
color: #999;
}
.g3-preview-content .preview-placeholder svg {
width: 80px;
height: 80px;
margin-bottom: 16px;
opacity: 0.5;
}
.g3-preview-content .preview-placeholder h3 {
font-size: 14px;
margin-bottom: 8px;
color: #666;
}
.g3-preview-content .preview-placeholder p {
font-size: 12px;
line-height: 1.5;
}
.g3-preview-content .preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.g3-preview-content .preview-icon svg {
width: 120px;
height: 120px;
color: #ccc;
}
.g3-preview-content .preview-iframe {
width: 100%;
height: 100%;
border: none;
margin: 0;
padding: 0;
background: #ffffff;
}
.g3-preview-content .preview-file-info {
text-align: left;
margin-top: 16px;
padding: 16px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.g3-preview-content .preview-file-info .info-label {
font-weight: 500;
color: #666;
}
.g3-preview-content .preview-file-info .status-uploaded {
color: #52c41a;
font-weight: 500;
}
.g3-preview-content .preview-file-info .status-pending {
color: #ff9800;
font-weight: 500;
}
.g3-preview-content .preview-file-info .cannot-preview {
margin-top: 12px;
padding: 8px 12px;
background: rgba(255, 77, 79, 0.08);
color: #ff4d4f;
border-radius: 4px;
font-size: 12px;
}
.g3-preview-content .preview-file-info .upload-tip {
margin-top: 12px;
padding: 8px 12px;
background: rgba(255, 152, 0, 0.08);
color: #ff9800;
border-radius: 4px;
font-size: 12px;
}
.g3-preview-content .url-error {
margin-top: 12px;
padding: 8px 12px;
background: rgba(255, 77, 79, 0.08);
color: #ff4d4f;
border-radius: 4px;
font-size: 12px;
}
.g3-global-menu {
position: absolute;
z-index: 9999;
@@ -448,7 +621,6 @@
pointer-events: auto;
}
/* 更多菜单项样式 */
.g3-global-menu .g3-action-menu-item {
padding: 8px 16px;
font-size: 14px;
@@ -465,22 +637,164 @@
background-color: #e8e8e8;
}
/* Task Dock 样式 */
.task-dock {
.g3-toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 12px;
}
.g3-toast {
display: flex;
align-items: flex-start;
padding: 12px 16px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 280px;
max-width: 400px;
animation: toastSlideIn 0.3s ease-out;
border-left: 4px solid #e8e8e8;
}
@keyframes toastSlideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes toastSlideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
.g3-toast.closing {
animation: toastSlideOut 0.3s ease-out forwards;
}
.g3-toast .toast-icon {
flex-shrink: 0;
width: 20px;
height: 20px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.g3-toast.toast-success {
border-left-color: #52c41a;
}
.g3-toast.toast-success .toast-icon {
background: rgba(82, 196, 26, 0.1);
color: #52c41a;
}
.g3-toast.toast-success .toast-title {
color: #52c41a;
}
.g3-toast.toast-error {
border-left-color: #ff4d4f;
}
.g3-toast.toast-error .toast-icon {
background: rgba(255, 77, 79, 0.1);
color: #ff4d4f;
}
.g3-toast.toast-error .toast-title {
color: #ff4d4f;
}
.g3-toast.toast-info {
border-left-color: #165dff;
}
.g3-toast.toast-info .toast-icon {
background: rgba(22, 93, 255, 0.1);
color: #165dff;
}
.g3-toast.toast-info .toast-title {
color: #165dff;
}
.g3-toast.toast-warning {
border-left-color: #ff9800;
}
.g3-toast.toast-warning .toast-icon {
background: rgba(255, 152, 0, 0.1);
color: #ff9800;
}
.g3-toast.toast-warning .toast-title {
color: #ff9800;
}
.g3-toast .toast-content {
flex: 1;
min-width: 0;
}
.g3-toast .toast-title {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.g3-toast .toast-message {
font-size: 12px;
color: #666;
line-height: 1.4;
}
.g3-toast .toast-close {
flex-shrink: 0;
margin-left: 12px;
width: 20px;
height: 20px;
border: none;
background: transparent;
color: #999;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.g3-toast .toast-close:hover {
background: #f0f2f5;
color: #333;
}
.g3-task-center {
position: fixed;
bottom: 20px;
right: 20px;
width: 320px;
width: 340px;
background: #ffffff;
border: 1px solid #eef0f2;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 9999;
display: none;
animation: slideIn 0.3s ease-out;
animation: taskCenterSlideIn 0.3s ease-out;
}
@keyframes slideIn {
@keyframes taskCenterSlideIn {
from {
transform: translateX(100%) translateY(100%);
opacity: 0;
@@ -491,7 +805,11 @@
}
}
.task-dock .dock-header {
.g3-task-center.is-visible {
display: block;
}
.g3-task-center .task-header {
display: flex;
align-items: center;
justify-content: space-between;
@@ -501,18 +819,13 @@
border-radius: 8px 8px 0 0;
}
.task-dock .dock-header .dock-title {
.g3-task-center .task-header .task-title {
font-size: 14px;
font-weight: 500;
color: #333;
}
.task-dock .dock-actions {
display: flex;
gap: 8px;
}
.task-dock .dock-actions button {
.g3-task-center .task-header .task-close {
width: 24px;
height: 24px;
border: none;
@@ -527,35 +840,25 @@
transition: all 0.2s ease;
}
.task-dock .dock-actions button:hover {
.g3-task-center .task-header .task-close:hover {
background: #f0f2f5;
color: #333;
}
.task-dock .dock-body {
.g3-task-center .task-body {
padding: 16px;
}
.task-dock .progress-line {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 14px;
.g3-task-center .current-item {
font-size: 13px;
color: #666;
}
.task-dock .task-name {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
margin-bottom: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.task-dock .bar {
.g3-task-center .progress-bar {
width: 100%;
height: 6px;
background: #f0f2f5;
@@ -564,125 +867,69 @@
margin-bottom: 12px;
}
.task-dock .bar .bar-inner {
.g3-task-center .progress-bar .progress-inner {
height: 100%;
background: #165dff;
border-radius: 3px;
transition: width 0.3s ease;
}
.task-dock .task-result {
.g3-task-center .progress-text {
font-size: 12px;
color: #666;
color: #999;
text-align: right;
margin-bottom: 16px;
}
/* 通知容器 */
.notification-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 9999;
}
/* 通知样式 */
.notification {
min-width: 300px;
max-width: 400px;
background: #ffffff;
border: 1px solid #eef0f2;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 16px;
.g3-task-center .result-section {
display: flex;
align-items: flex-start;
justify-content: space-between;
pointer-events: auto;
transition: all 0.3s ease;
justify-content: space-around;
padding-top: 12px;
border-top: 1px solid #f0f2f5;
}
/* 通知内容 */
.notification-content {
flex: 1;
.g3-task-center .result-item {
text-align: center;
}
/* 通知标题 */
.notification-title {
font-size: 14px;
.g3-task-center .result-item .result-value {
font-size: 18px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
/* 通知消息 */
.notification-message {
font-size: 14px;
color: #666;
line-height: 1.4;
.g3-task-center .result-item .result-label {
font-size: 12px;
color: #999;
margin-top: 4px;
}
/* 关闭按钮 */
.notification-close {
width: 24px;
height: 24px;
.g3-task-center .result-item.result-success .result-value {
color: #52c41a;
}
.g3-task-center .result-item.result-failed .result-value {
color: #ff4d4f;
}
.g3-task-center .complete-button {
width: 100%;
margin-top: 12px;
padding: 8px;
border: none;
background: transparent;
color: #666;
font-size: 16px;
cursor: pointer;
background: #165dff;
color: #fff;
font-size: 13px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
margin-left: 12px;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
}
.notification-close:hover {
background: #f0f2f5;
color: #333;
.g3-task-center .complete-button:hover {
filter: brightness(1.1);
}
/* 通知类型样式 */
.notification-success {
border-left: 4px solid #52c41a;
}
.notification-warning {
border-left: 4px solid #faad14;
}
.notification-error {
border-left: 4px solid #ff4d4f;
}
.notification-info {
border-left: 4px solid #165dff;
}
/* 通知动画 */
@keyframes slideIn {
from {
transform: translateX(100%) translateY(100%);
opacity: 0;
}
to {
transform: translateX(0) translateY(0);
opacity: 1;
}
}
.notification-fade-out {
opacity: 0;
transform: translateY(-10px);
}
/* UI Lock 样式 */
.ui-lock {
.g3-ui-lock {
position: fixed;
top: 0;
left: 0;
@@ -691,4 +938,8 @@
background: rgba(0, 0, 0, 0.05);
z-index: 9998;
display: none;
}
}
.g3-ui-lock.is-locked {
display: block;
}
File diff suppressed because it is too large. Load diff
Whitespace-only changes.
+21 -11
View File
@@ -5,8 +5,10 @@
<title>文件预览测试</title>
<script type="text/javascript" src="./js/jquery-easyui-1.4/jquery.min.js"></script>
<script type="text/javascript" src="./js/jquery-easyui-1.4/jquery.easyui.min.js"></script>
<script type="text/javascript" src="./custom-file/index.js"></script>
<link rel="stylesheet" href="./custom-file/index.css" />
<script type="text/javascript" src="./g3-file/index.js"></script>
<script type="text/javascript" src="./g3-file/libs/FileSaver.min.js"></script>
<script type="text/javascript" src="./g3-file/libs/jszip.min.js"></script>
<link rel="stylesheet" href="./g3-file/index.css" />
<style>
* {
@@ -14,18 +16,24 @@
padding: 0;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
#app {
width: 100%;
height: 100%;
background: #f5f7fa;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.button-group {
margin-bottom: 20px;
padding: 10px 20px;
background: #ffffff;
border-bottom: 1px solid #eef0f2;
display: flex;
gap: 10px;
}
@@ -43,6 +51,11 @@
.button-group button:hover {
background: #4a6fcf;
}
#fileContainer {
flex: 1;
min-height: 0;
}
</style>
</head>
@@ -57,7 +70,7 @@
<button id="destroyBtn">销毁组件</button>
</div>
<div style="height: 600px;width: 500px;">
<div style="height: 600px;width: 800px;">
<div id="fileContainer"></div>
</div>
</div>
@@ -100,8 +113,9 @@
fetchPermissions().then(() => {
const file = $('#fileContainer').G3File({
language: 'zh-CN',
previewUrl:'https://sxox.g3soft.cn:8012/onlinePreview?url=',
data: [
{ name: 'test.jar', size: 1024000, uploadDate: '2024-01-15T10:30:00', url: 'http://118.89.70.199:9000/test/XI26033384.rar' },
{ name: 'logo.png', size: 1024000, uploadDate: '2024-01-15T10:30:00', url: 'https://g3hdzjsj.oss-cn-hangzhou.aliyuncs.com/g3hd/2026/04/28/ccb964f4-124d-4169-8981-20333bea2587.docx' },
],
uploadConfig: {
aliyun: {
@@ -169,10 +183,6 @@
callback(false);
}
},
onPreview: function (files) {
console.log('预览文件:', files);
alert('预览文件: ' + files[0].name);
},
onSelect: function (files) {
console.log('选择文件:', files);
},
+13
View File
@@ -0,0 +1,13 @@
/**
* vue v3.5.33
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/var Vue=function(e){"use strict";var t,n,r;let i,l,s,o,a,c,u,h,d,p,f,g,m;function y(e){let t=Object.create(null);for(let n of e.split(","))t[n]=1;return e=>e in t}let b={},_=[],S=()=>{},x=()=>!1,C=e=>111===e.charCodeAt(0)&&110===e.charCodeAt(1)&&(e.charCodeAt(2)>122||97>e.charCodeAt(2)),k=e=>e.startsWith("onUpdate:"),T=Object.assign,w=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},N=Object.prototype.hasOwnProperty,A=(e,t)=>N.call(e,t),E=Array.isArray,I=e=>"function"==typeof e,R=e=>"string"==typeof e,O=e=>"symbol"==typeof e,M=e=>null!==e&&"object"==typeof e,P=e=>(M(e)||I(e))&&I(e.then)&&I(e.catch),F=Object.prototype.toString,L=e=>R(e)&&"NaN"!==e&&"-"!==e[0]&&""+parseInt(e,10)===e,$=y(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),D=y("bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo"),V=e=>{let t=Object.create(null);return n=>t[n]||(t[n]=e(n))},B=/-\w/g,j=V(e=>e.replace(B,e=>e.slice(1).toUpperCase())),U=/\B([A-Z])/g,H=V(e=>e.replace(U,"-$1").toLowerCase()),q=V(e=>e.charAt(0).toUpperCase()+e.slice(1)),W=V(e=>e?`on${q(e)}`:""),K=(e,t)=>!Object.is(e,t),z=(e,...t)=>{for(let n=0;n<e.length;n++)e[n](...t)},J=(e,t,n,r=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},G=e=>{let t=parseFloat(e);return isNaN(t)?e:t},X=e=>{let t=R(e)?Number(e):NaN;return isNaN(t)?e:t},Q=()=>i||(i="u">typeof globalThis?globalThis:"u">typeof self?self:"u">typeof window?window:"u">typeof global?global:{}),Z=y("Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol");function Y(e){if(E(e)){let t={};for(let n=0;n<e.length;n++){let r=e[n],i=R(r)?er(r):Y(r);if(i)for(let e in i)t[e]=i[e]}return t}if(R(e)||M(e))return e}let ee=/;(?![^(]*\))/g,et=/:([^]+)/,en=/\/\*[^]*?\*\//g;function er(e){let t={};return e.replace(en,"").split(ee).forEach(e=>{if(e){let n=e.split(et);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function ei(e){let t="";if(R(e))t=e;else if(E(e))for(let n=0;n<e.length;n++){let r=ei(e[n]);r&&(t+=r+" ")}else if(M(e))for(let n in e)e[n]&&(t+=n+" ");return t.trim()}let el=y("html,body,base,head,link,meta,style,title,address,article,aside,footer,header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot"),es=y("svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistantLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view"),eo=y("annotation,annotation-xml,maction,maligngroup,malignmark,math,menclose,merror,mfenced,mfrac,mfraction,mglyph,mi,mlabeledtr,mlongdiv,mmultiscripts,mn,mo,mover,mpadded,mphantom,mprescripts,mroot,mrow,ms,mscarries,mscarry,msgroup,msline,mspace,msqrt,msrow,mstack,mstyle,msub,msubsup,msup,mtable,mtd,mtext,mtr,munder,munderover,none,semantics"),ea=y("area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr"),ec=y("itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly");function eu(e,t){let n,r;if(e===t)return!0;let i="[object Date]"===(n=e,F.call(n)),l="[object Date]"===(r=t,F.call(r));if(i||l)return!!i&&!!l&&e.getTime()===t.getTime();if(i=O(e),l=O(t),i||l)return e===t;if(i=E(e),l=E(t),i||l)return!!i&&!!l&&function(e,t){if(e.length!==t.length)return!1;let n=!0;for(let r=0;n&&r<e.length;r++)n=eu(e[r],t[r]);return n}(e,t);if(i=M(e),l=M(t),i||l){if(!i||!l||Object.keys(e).length!==Object.keys(t).length)return!1;for(let n in e){let r=e.hasOwnProperty(n),i=t.hasOwnProperty(n);if(r&&!i||!r&&i||!eu(e[n],t[n]))return!1}}return String(e)===String(t)}function eh(e,t){return e.findIndex(e=>eu(e,t))}let ed=e=>!!(e&&!0===e.__v_isRef),ep=e=>R(e)?e:null==e?"":E(e)||M(e)&&(e.toString===F||!I(e.toString))?ed(e)?ep(e.value):JSON.stringify(e,ef,2):String(e),ef=(e,t)=>{let n;if(ed(t))return ef(e,t.value);if("[object Map]"===(n=t,F.call(n)))return{[`Map(${t.size})`]:[...t.entries()].reduce((e,[t,n],r)=>(e[eg(t,Line truncated
`===n[0]&&("PRE"===e.tagName||"TEXTAREA"===e.tagName)&&(n=n.slice(1));let{textContent:r}=e;r!==n&&r!==n.replace(/\r\n|\r/g,`
`)&&(nD(e,0)||nO(),e.textContent=t.children)}if(c){if(g||!s||48&u){let t=e.tagName.includes("-");for(let i in c)(g&&(i.endsWith("value")||"indeterminate"===i)||C(i)&&!$(i)||"."===i[0]||t&&!$(i))&&r(e,i,null,c[i],void 0,n)}else if(c.onClick)r(e,"onClick",null,c.onClick,void 0,n);else if(4&u&&td(c.style))for(let e in c.style)c.style[e]}(a=c&&c.onVnodeBeforeMount)&&iC(a,n,t),p&&t6(t,null,n,"beforeMount"),((a=c&&c.onVnodeMounted)||p||b)&&r3(()=>{a&&iC(a,n,t),b&&f.enter(e),p&&t6(t,null,n,"mounted")},i)}return e.nextSibling},d=(e,t,r,s,o,c,h)=>{h=h||!!t.dynamicChildren;let d=t.children,p=d.length;for(let t=0;t<p;t++){let f=h?d[t]:d[t]=ib(d[t]),g=f.type===r8;e?(g&&!h&&t+1<p&&ib(d[t+1]).type===r8&&(a(i(e.data.slice(f.children.length)),r,l(e)),e.data=f.children),e=u(e,f,s,o,c,h)):g&&!f.children?a(f.el=i(""),r):(nD(r,1)||nO(),n(null,f,r,null,s,o,nM(r),c))}return e},p=(e,t,n,r,i,o)=>{let{slotScopeIds:u}=t;u&&(i=i?i.concat(u):u);let h=s(e),p=d(l(e),t,h,n,r,i,o);return p&&nP(p)&&"]"===p.data?l(t.anchor=p):(nO(),a(t.anchor=c("]"),h,p),p)},f=(e,t,r,i,a,c)=>{if(nD(e.parentElement,1)||nO(),t.el=null,c){let t=g(e);for(;;){let n=l(e);if(n&&n!==t)o(n);else break}}let u=l(e),h=s(e);return o(e),n(null,t,h,u,r,i,nM(h),a),r&&(r.vnode.el=t.el,rR(r,t.el)),u},g=(e,t="[",n="]")=>{let r=0;for(;e;)if((e=l(e))&&nP(e)&&(e.data===t&&r++,e.data===n))if(0===r)return l(e);else r--;return e},m=(e,t,n)=>{let r=t.parentNode;r&&r.replaceChild(e,t);let i=n;for(;i;)i.vnode.el===t&&(i.vnode.el=i.subTree.el=e),i=i.parent},y=e=>1===e.nodeType&&"TEMPLATE"===e.tagName;return[(e,t)=>{if(!t.hasChildNodes()){n(null,e,t),tZ(),t._vnode=e;return}u(t.firstChild,e,null,null,null),tZ(),t._vnode=e},u]}let nL="data-allow-mismatch",n$={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function nD(e,t){if(0===t||1===t)for(;e&&!e.hasAttribute(nL);)e=e.parentElement;let n=e&&e.getAttribute(nL);if(null==n)return!1;{if(""===n)return!0;let e=n.split(",");return!!(0===t&&e.includes("children"))||e.includes(n$[t])}}let nV=Q().requestIdleCallback||(e=>setTimeout(e,1)),nB=Q().cancelIdleCallback||(e=>clearTimeout(e)),nj=e=>!!e.type.__asyncLoader;function nU(e,t){let{ref:n,props:r,children:i,ce:l}=t.vnode,s=ip(e,r,i);return s.ref=n,s.ce=l,delete t.vnode.ce,s}let nH=e=>e.type.__isKeepAlive;function nq(e,t){let n;if(E(e))return e.some(e=>nq(e,t));if(R(e))return e.split(",").includes(t);return"[object RegExp]"===(n=e,F.call(n))&&(e.lastIndex=0,e.test(t))}function nW(e,t){nz(e,"a",t)}function nK(e,t){nz(e,"da",t)}function nz(e,t,n=iw){let r=e.__wdc||(e.__wdc=()=>{let t=n;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()});if(nX(t,r,n),n){let e=n.parent;for(;e&&e.parent;)nH(e.parent.vnode)&&function(e,t,n,r){let i=nX(t,e,r,!0);n3(()=>{w(r[t],i)},n)}(r,t,n,e),e=e.parent}}function nJ(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function nG(e){return 128&e.shapeFlag?e.ssContent:e}function nX(e,t,n=iw,r=!1){if(n){let i=n[e]||(n[e]=[]),l=t.__weh||(t.__weh=(...r)=>{eE();let i=iA(n),l=tD(t,n,e,r);return i(),eI(),l});return r?i.unshift(l):i.push(l),l}}let nQ=e=>(t,n=iw)=>{iR&&"sp"!==e||nX(e,(...e)=>t(...e),n)},nZ=nQ("bm"),nY=nQ("m"),n0=nQ("bu"),n1=nQ("u"),n2=nQ("bum"),n3=nQ("um"),n6=nQ("sp"),n4=nQ("rtg"),n8=nQ("rtc");function n5(e,t=iw){nX("ec",e,t)}let n9="components",n7=Symbol.for("v-ndc");function re(e,t,n=!0,r=!1){let i=t0||iw;if(i){let n=i.type;if(e===n9){let e=iD(n,!1);if(e&&(e===t||e===j(t)||e===q(j(t))))return n}let l=rt(i[e]||n[e],t)||rt(i.appContext[e],t);return!l&&r?n:l}}function rt(e,t){return e&&(e[t]||e[j(t)]||e[q(j(t))])}let rn=e=>e?iI(e)?i$(e):rn(e.parent):null,rr=T(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>rn(e.parent),$root:e=>rn(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>rh(e),$forceUpdate:e=>e.f||(e.f=()=>{tJ(e.update)}),$nextTick:e=>e.n||(e.n=tz.bind(e.proxy)),$watch:e=>ne.bind(e)}),ri=(e,t)=>e!==b&&!e.__isScriptSetup&&A(e,t),rl={get({_:e},t){let n,r;if("__v_skip"===t)return!0;let{ctx:i,setupState:l,data:s,props:o,accessCache:a,type:c,appContext:u}=e;if("$"!==t[0]){let e=a[t];if(void 0!==e)switch(e){case 1:return l[t];case 2:return s[t];case 4:return i[t];case 3:return o[t]}else{if(ri(l,t))return a[t]=1,l[t];if(s!==b&&A(s,t))return a[t]=2,s[t];if(A(o,t))return a[t]=3,o[t];if(i!==b&&A(i,t))return a[t]=4,i[t];rc&&(a[t]=0)}}let h=rr[t];return h?("$attrs"===t&&eV(e.attrs,"get",""),h(e)):(n=c.__cssModules)&&(n=n[t])?n:i!==b&&A(i,t)?(a[t]=4,i[t]):A(r=u.config.globalProperties,t)?r[t]:void 0},set({_:e},t,n){let{data:r,setupState:i,ctx:l}=e;return ri(i,t)?(i[t]=n,!0):r!==b&&A(r,t)?(r[t]=n,!0):!A(e.props,t)&&!("$"===t[0]&&t.slice(1)in e)&&(l[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:i,props:l,type:s}},o){let a;return!!(n[o]||e!==b&&"$"!==o[0]&&A(e,o)||ri(t,o)||A(l,o)||A(r,o)||A(rr,o)||A(i.config.globalProperties,o)||(a=s.__cssModules)&&a[o])},defineProperty(e,t,n){return null!=n.get?e._.accessCache[t]=0:A(n,"value")&&this.set(e,t,n.valu Line truncated
`);else if(om(i.content)){let l=e[r-1]&&e[r-1].type,s=e[r+1]&&e[r+1].type;!l||!s||t&&(3===l&&(3===s||1===s)||1===l&&(3===s||1===s&&function(e){for(let t=0;t<e.length;t++){let n=e.charCodeAt(t);if(10===n||13===n)return!0}return!1}(i.content)))?(n=!0,e[r]=null):i.content=" "}else t&&(i.content=oH(i.content))}return n?e.filter(Boolean):e}function oH(e){let t="",n=!1;for(let r=0;r<e.length;r++)sQ(e.charCodeAt(r))?n||(t+=" ",n=!0):(t+=e[r],n=!1);return t}function oq(e){(oR[0]||oS).children.push(e)}function oW(e,t){return{start:oO.getPos(e),end:null==t?t:oO.getPos(t),source:null==t?t:oF(e,t)}}function oK(e,t){e.end=oO.getPos(t),e.source=oF(e.start.offset,t)}function oz(e,t=!1,n,r=0,i=0){return sU(e,t,n,r)}function oJ(e,t,n){o_.onError(s3(e,oW(t,t)))}function oG(e){let t=e.children.filter(e=>3!==e.type);return 1!==t.length||1!==t[0].type||ou(t[0])?null:t[0]}function oX(e,t){let{constantCache:n}=t;switch(e.type){case 1:if(0!==e.tagType)return 0;let r=n.get(e);if(void 0!==r)return r;let i=e.codegenNode;if(13!==i.type||i.isBlock&&"svg"!==e.tag&&"foreignObject"!==e.tag&&"math"!==e.tag)return 0;if(void 0!==i.patchFlag)return n.set(e,0),0;{let r=3,c=oZ(e,t);if(0===c)return n.set(e,0),0;c<r&&(r=c);for(let i=0;i<e.children.length;i++){let l=oX(e.children[i],t);if(0===l)return n.set(e,0),0;l<r&&(r=l)}if(r>1)for(let i=0;i<e.props.length;i++){let l=e.props[i];if(7===l.type&&"bind"===l.name&&l.exp){let i=oX(l.exp,t);if(0===i)return n.set(e,0),0;i<r&&(r=i)}}if(i.isBlock){var l,s,o,a;for(let t=0;t<e.props.length;t++)if(7===e.props[t].type)return n.set(e,0),0;t.removeHelper(sn),t.removeHelper((l=t.inSSR,s=i.isComponent,l||s?sr:si)),i.isBlock=!1,t.helper((o=t.inSSR,a=i.isComponent,o||a?sl:ss))}return n.set(e,r),r}case 2:case 3:return 3;case 9:case 11:case 10:default:return 0;case 5:case 12:return oX(e.content,t);case 4:return e.constType;case 8:let c=3;for(let n=0;n<e.children.length;n++){let r=e.children[n];if(R(r)||O(r))continue;let i=oX(r,t);if(0===i)return 0;i<c&&(c=i)}return c;case 20:return 2}}let oQ=new Set([s_,sS,sx,sC]);function oZ(e,t){let n=3,r=oY(e);if(r&&15===r.type){let{properties:e}=r;for(let r=0;r<e.length;r++){let i,{key:l,value:s}=e[r],o=oX(l,t);if(0===o)return o;if(o<n&&(n=o),0===(i=4===s.type?oX(s,t):14===s.type?function e(t,n){if(14===t.type&&!R(t.callee)&&oQ.has(t.callee)){let r=t.arguments[0];if(4===r.type)return oX(r,n);if(14===r.type)return e(r,n)}return 0}(s,t):0))return i;i<n&&(n=i)}}return n}function oY(e){let t=e.codegenNode;if(13===t.type)return t.props}function o0(e,t){t.currentNode=e;let{nodeTransforms:n}=t,r=[];for(let i=0;i<n.length;i++){let l=n[i](e,t);if(l&&(E(l)?r.push(...l):r.push(l)),!t.currentNode)return;e=t.currentNode}switch(e.type){case 3:t.ssr||t.helper(so);break;case 5:t.ssr||t.helper(sy);break;case 9:for(let n=0;n<e.branches.length;n++)o0(e.branches[n],t);break;case 10:case 11:case 1:case 0:var i=e;let l=0,s=()=>{l--};for(;l<i.children.length;l++){let e=i.children[l];R(e)||(t.grandParent=t.parent,t.parent=i,t.childIndex=l,t.onNodeRemoved=s,o0(e,t))}}t.currentNode=e;let o=r.length;for(;o--;)r[o]()}function o1(e,t){let n=R(e)?t=>t===e:t=>e.test(t);return(e,r)=>{if(1===e.type){let{props:i}=e;if(3===e.tagType&&i.some(oa))return;let l=[];for(let s=0;s<i.length;s++){let o=i[s];if(7===o.type&&n(o.name)){i.splice(s,1),s--;let n=t(e,o,r);n&&l.push(n)}}return l}}}let o2="/*@__PURE__*/",o3=e=>`${sL[e]}: _${sL[e]}`;function o6(e,t,{helper:n,push:r,newline:i,isTS:l}){let s=n("component"===t?su:sd);for(let n=0;n<e.length;n++){let o=e[n],a=o.endsWith("__self");a&&(o=o.slice(0,-6)),r(`const ${of(o,t)} = ${s}(${JSON.stringify(o)}${a?", true":""})${l?"!":""}`),n<e.length-1&&i()}}function o4(e,t){let n=e.length>3;t.push("["),n&&t.indent(),o8(e,t,n),n&&t.deindent(),t.push("]")}function o8(e,t,n=!1,r=!0){let{push:i,newline:l}=t;for(let s=0;s<e.length;s++){let o=e[s];R(o)?i(o,-3):E(o)?o4(o,t):o5(o,t),s<e.length-1&&(n?(r&&i(","),l()):r&&i(", "))}}function o5(e,t){var n,r,i;if(R(e))return void t.push(e,-3);if(O(e))return void t.push(t.helper(e));switch(e.type){case 1:case 9:case 11:case 12:o5(e.codegenNode,t);break;case 2:n=e,t.push(JSON.stringify(n.content),-3,n);break;case 4:o9(e,t);break;case 5:!function(e,t){let{push:n,helper:r,pure:i}=t;i&&n(o2),n(`${r(sy)}(`),o5(e.content,t),n(")")}(e,t);break;case 8:o7(e,t);break;case 3:!function(e,t){let{push:n,helper:r,pure:i}=t;i&&n(o2),n(`${r(so)}(${JSON.stringify(e.content)})`,-3,e)}(e,t);break;case 13:!function(e,t){var n,r;let i,{push:l,helper:s,pure:o}=t,{tag:a,props:c,children:u,patchFlag:h,dynamicProps:d,directives:p,isBlock:f,disableTracking:g,isComponent:m}=e;h&&(i=String(h)),p&&l(s(sf)+"("),f&&l(`(${s(sn)}(${g?"true":""}), `),o&&l(o2),l(s(f?(n=t.inSSR,n||m?sr:si):(r=t.inSSR,r||m?sl:ss))+"(",-2,e),o8(function(e){let t=e.length;for(;t--&&null==e[t];);return e.slice(0,t+1).map(e=>e||"null")}([a,c,u,i,d]),t),l(")"),f&&l(")"),p&&(l(", "),o5(p,t),l(")"))}(e,t);break;case 14:!function(e,t){let{push:n,helper:r,pure:i}=t,l=R(e.callee)?e.callee:r(e.callee);i&&n(o2 Line truncated
`+" ".repeat(e),0)}return p}(e,t);t.onContextCreated&&t.onContextCreated(n);let{mode:r,push:i,prefixIdentifiers:l,indent:s,deindent:o,newline:a,ssr:c}=n,u=Array.from(e.helpers),h=u.length>0,d=!l&&"module"!==r;!function(e,t){let{push:n,newline:r,runtimeGlobalName:i}=t,l=Array.from(e.helpers);if(l.length>0&&(n(`const _Vue = ${i}
`,-1),e.hoists.length)){let e=[sl,ss,so,sa,sc].filter(e=>l.includes(e)).map(o3).join(", ");n(`const { ${e} } = _Vue
`,-1)}(function(e,t){if(!e.length)return;t.pure=!0;let{push:n,newline:r}=t;r();for(let i=0;i<e.length;i++){let l=e[i];l&&(n(`const _hoisted_${i+1} = `),o5(l,t),r())}t.pure=!1})(e.hoists,t),r(),n("return ")}(e,n);let p=(c?["_ctx","_push","_parent","_attrs"]:["_ctx","_cache"]).join(", ");if(i(`function ${c?"ssrRender":"render"}(${p}) {`),s(),d&&(i("with (_ctx) {"),s(),h&&(i(`const { ${u.map(o3).join(", ")} } = _Vue
`,-1),a())),e.components.length&&(o6(e.components,"component",n),(e.directives.length||e.temps>0)&&a()),e.directives.length&&(o6(e.directives,"directive",n),e.temps>0&&a()),e.temps>0){i("let ");for(let t=0;t<e.temps;t++)i(`${t>0?", ":""}_temp${t}`)}return(e.components.length||e.directives.length||e.temps)&&(i(`
`,0),a()),c||i("return "),e.codegenNode?o5(e.codegenNode,n):i("null"),d&&(o(),i("}")),o(),i("}"),{ast:e,code:n.code,preamble:"",map:n.map?n.map.toJSON():void 0}}(o,s)}(e,T({},a$,t,{nodeTransforms:[aH,...aq,...t.nodeTransforms||[]],directiveTransforms:T({},aW,t.directiveTransforms||{}),transformHoist:null}))}(e,i),s=Function(l)();return s._rc=!0,aK[n]=s}return iM(az),e.BaseTransition=ny,e.BaseTransitionPropsValidators=ng,e.Comment=r5,e.DeprecationTypes=null,e.EffectScope=em,e.ErrorCodes={SETUP_FUNCTION:0,0:"SETUP_FUNCTION",RENDER_FUNCTION:1,1:"RENDER_FUNCTION",NATIVE_EVENT_HANDLER:5,5:"NATIVE_EVENT_HANDLER",COMPONENT_EVENT_HANDLER:6,6:"COMPONENT_EVENT_HANDLER",VNODE_HOOK:7,7:"VNODE_HOOK",DIRECTIVE_HOOK:8,8:"DIRECTIVE_HOOK",TRANSITION_HOOK:9,9:"TRANSITION_HOOK",APP_ERROR_HANDLER:10,10:"APP_ERROR_HANDLER",APP_WARN_HANDLER:11,11:"APP_WARN_HANDLER",FUNCTION_REF:12,12:"FUNCTION_REF",ASYNC_COMPONENT_LOADER:13,13:"ASYNC_COMPONENT_LOADER",SCHEDULER:14,14:"SCHEDULER",COMPONENT_UPDATE:15,15:"COMPONENT_UPDATE",APP_UNMOUNT_CLEANUP:16,16:"APP_UNMOUNT_CLEANUP"},e.ErrorTypeStrings=null,e.Fragment=r4,e.KeepAlive={name:"KeepAlive",__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){let n=iN(),r=n.ctx,i=new Map,l=new Set,s=null,o=n.suspense,{renderer:{p:a,m:c,um:u,o:{createElement:h}}}=r,d=h("div");function p(e){nJ(e),u(e,n,o,!0)}function f(e){i.forEach((t,n)=>{let r=iD(nj(t)?t.type.__asyncResolved||{}:t.type);r&&!e(r)&&g(n)})}function g(e){let t=i.get(e);!t||s&&ic(t,s)?s&&nJ(s):p(t),i.delete(e),l.delete(e)}r.activate=(e,t,n,r,i)=>{let l=e.component;c(e,t,n,0,o),a(l.vnode,e,t,n,l,o,r,e.slotScopeIds,i),rq(()=>{l.isDeactivated=!1,l.a&&z(l.a);let t=e.props&&e.props.onVnodeMounted;t&&iC(t,l.parent,e)},o)},r.deactivate=e=>{let t=e.component;rQ(t.m),rQ(t.a),c(e,d,null,1,o),rq(()=>{t.da&&z(t.da);let n=e.props&&e.props.onVnodeUnmounted;n&&iC(n,t.parent,e),t.isDeactivated=!0},o)},t7(()=>[e.include,e.exclude],([e,t])=>{e&&f(t=>nq(e,t)),t&&f(e=>!nq(t,e))},{flush:"post",deep:!0});let m=null,y=()=>{null!=m&&(rZ(n.subTree.type)?rq(()=>{i.set(m,nG(n.subTree))},n.subTree.suspense):i.set(m,nG(n.subTree)))};return nY(y),n1(y),n2(()=>{i.forEach(e=>{let{subTree:t,suspense:r}=n,i=nG(t);if(e.type===i.type&&e.key===i.key){nJ(i);let e=i.component.da;e&&rq(e,r);return}p(e)})}),()=>{if(m=null,!t.default)return s=null;let n=t.default(),r=n[0];if(n.length>1)return s=null,n;if(!ia(r)||!(4&r.shapeFlag)&&!(128&r.shapeFlag))return s=null,r;let o=nG(r);if(o.type===r5)return s=null,o;let a=o.type,c=iD(nj(o)?o.type.__asyncResolved||{}:a),{include:u,exclude:h,max:d}=e;if(u&&(!c||!nq(u,c))||h&&c&&nq(h,c))return o.shapeFlag&=-257,s=o,r;let p=null==o.key?a:o.key,f=i.get(p);return o.el&&(o=im(o),128&r.shapeFlag&&(r.ssContent=o)),m=p,f?(o.el=f.el,o.component=f.component,o.transition&&nC(o,o.transition),o.shapeFlag|=512,l.delete(p),l.add(p)):(l.add(p),d&&l.size>parseInt(d,10)&&g(l.values().next().value)),o.shapeFlag|=256,s=o,rZ(r.type)?r:o}}},e.ReactiveEffect=ey,e.Static=r9,e.Suspense={name:"Suspense",__isSuspense:!0,process(e,t,n,r,i,l,s,o,a,c){if(null==e)!function(e,t,n,r,i,l,s,o,a){let{p:c,o:{createElement:u}}=a,h=u("div"),d=e.suspense=r1(e,i,r,t,h,n,l,s,o,a);c(null,d.pendingBranch=e.ssContent,h,null,r,d,l,s),d.deps>0?(r0(e,"onPending"),r0(e,"onFallback"),c(null,e.ssFallback,t,n,r,null,l,s),r6(d,e.ssFallback)):d.resolve(!1,!0)}(t,n,r,i,l,s,o,a,c);else{if(l&&l.deps>0&&!e.suspense.isInFallback){t.suspense=e.suspense,t.suspense.vnode=t,t.el=e.el;return}!function(e,t,n,r,i,l,s,o,{p:a,um:c,o:{createElement:u}}){let h=t.suspense=e.suspense;h.vnode=t,t.el=e.el;let d=t.ssContent,p=t.ssFallback,{activeBranch:f,pendingBranch:g,isInFallback:m,isHydrating:y}=h;if(g)h.pendingBranch=d,ic(g,d)?(a(g,d,h.hiddenContainer,null,i,h,l,s,o),h.deps<=0?h.resolve():m&&!y&&(a(f,p,n,r,i,null,l,s,o),r6(h,p))):(h.pendingId=rY++,y?(h.isHydrating=!1,h.activeBranch=g):c(g,i,h),h.deps=0,h.effects.length=0,h.hiddenContainer=u("div"),m?(a(null,d,h.hiddenContainer,null,i,h,l,s,o),h.deps<=0?h.resolve():(a(f,p,n,r,i,null,l,s,o),r6(h,p))):f&&ic(f,d)?(a(f,d,n,r,i,h,l,s,o),h.resolve(!0)):(a(null,d,h.hiddenContainer,null,i,h,l,s,o),h.deps<=0&&h.resolve()));else if(f&&ic(f,d))a(f,d,n,r,i,h,l,s,o),r6(h,d);else if(r0(t,"onPending"),h.pendingBranch=d,512&d.shapeFlag?h.pendingId=d.component.suspenseId:h.pendingId=rY++,a(null,d,h.hiddenContainer,null,i,h,l,s,o),h.deps<=0)h.resolve();else{let{timeout:e,pendingId:t}=h;e>0?setTimeout(()=>{h.pendingId===t&&h.fallback(p)},e):0===e&&h.fallback(p)}}(e,t,n,r,i,s,o,a,c)}},hydrate:function(e,t,n,r,i,l,s,o,a){let c=t.suspense=r1(t,r,n,e.parentNode,document.createElement("div"),null,i,l,s,o,!0),u=a(e,c.pendingBranch=t.ssContent,n,c,l,s);return 0===c.deps&&c.resolve(!1,!0),u},normalize:function(e){let{shapeFlag:t,children:n}=e,r=32&t;e.ssContent=r2(r?n.default:n),e.ssFallback=r?r2(n.fallback):ip(r5)}},e.Teleport={name:"Teleport",__isTeleport:!0,process(e,t,n,r,i,l,s,o,a,c){let{mc:u,pc:h,pbc:d,o:{insert Line truncated
+473 -173
View File
@@ -1,205 +1,505 @@
<!DOCTYPE html>
<html lang="en">
<%@ page language="java" import="java.util.*,com.g3.general.*" pageEncoding="utf-8" %>
<%@ include file="../ini.jsp" %>
<% if (orgid.equals("")) { return; } String id=request.getParameter("id"); String
mx_moduleid=request.getParameter("mx_moduleid"); String sh_state=request.getParameter("sh_state"); String
iread=request.getParameter("iread"); String tree_where=request.getParameter("tree_where"); String
mx_secondfather=request.getParameter("mx_secondfather"); if (mx_secondfather==null) { mx_secondfather="" ; }
String mx_secondfather_fz=request.getParameter("mx_secondfather_fz"); if (mx_secondfather_fz==null) {
mx_secondfather_fz="" ; } String default_click=request.getParameter("default_click"); if (default_click==null) {
default_click="" ; } %>
<head>
<meta charset="UTF-8" />
<title>Download Progress UI Comparison</title>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<style>
/* =========================
Base Layout
========================= */
body {
margin: 0;
font-family: Arial;
background: #f5f5f5;
}
<head>
<meta http-equiv="Cache-Control" content="no-cache" />
<base href="<%=basePath%>">
<title>采购下单浏览</title>
<jsp:include page="../inc.jsp"></jsp:include>
.page {
min-height: 100vh;
display: flex;
flex-direction: column;
}
<script type="text/javascript" src="<%=basePath%>/js/jquery-easyui-1.4/jquery.min.js"></script>
<link rel="stylesheet" type="text/css" href="<%=basePath%>/js/jquery-easyui-1.4/themes/default/easyui.css" />
<link rel="stylesheet" type="text/css" href="<%=basePath%>/js/jquery-easyui-1.4/themes/icon.css" />
<script type="text/javascript" src="<%=basePath%>/js/jquery-easyui-1.4/jquery.easyui.min.js"></script>
<script type="text/javascript" src="<%=basePath%>/js/jquery-easyui-1.4/locale/easyui-lang-zh_CN.js"></script>
.header,
.footer {
padding: 12px;
background: #fff;
border-bottom: 1px solid #eee;
}
<script type="text/javascript" src="<%=basePath%>/js/g3-file/libs/FileSaver.min.js"></script>
<script type="text/javascript" src="<%=basePath%>/js/g3-file/libs/jszip.min.js"></script>
<script type="text/javascript" src="<%=basePath%>/js/g3-file/index.js"></script>
<link rel="stylesheet" type="text/css" href="<%=basePath%>/js/g3-file/index.css" />
.footer {
border-top: 1px solid #eee;
margin-top: auto;
}
<script type="text/javascript" src="g3Funlib.js?5"></script>
.content {
flex: 1;
padding: 16px;
}
<script type="text/javascript" id="testScript" columns_viewgrid="v_xc_consite_wx"
changLang_table="v_xc_consite_wx" columns_editgrid="" src="language/language_new_yardyw.js"></script>
<script type="text/javascript" src="language/languagebtn.js"></script>
/* =========================
🔴 方案 A:嵌入式进度条
========================= */
.embedded-bar {
background: #fff7e6;
border: 1px solid #ffe58f;
padding: 10px;
border-radius: 6px;
margin-bottom: 12px;
}
<style type="text/css">
* {
margin: 0;
padding: 0;
}
.embedded-bar .row {
display: flex;
gap: 10px;
align-items: center;
font-size: 13px;
}
html,
body {
height: 100%;
margin: 0;
}
.embedded-bar .actions {
margin-top: 8px;
display: flex;
gap: 8px;
}
.g3-layout {
height: 100%;
display: flex;
overflow: hidden;
}
.embedded-bar button {
border: 1px solid #ddd;
background: #fff;
padding: 4px 8px;
cursor: pointer;
}
.g3-layout .pane {
height: 100%;
flex: 0 0 auto;
overflow: auto;
}
/* =========================
🟢 方案 B:浮层 Dock(推荐)
========================= */
.download-dock {
position: fixed;
right: 20px;
bottom: 20px;
width: 320px;
.g3-layout .split {
width: 1px;
position: relative;
background: #95b8e7;
}
background: #fff;
border-radius: 10px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
.g3-layout .split::before {
content: "";
position: absolute;
left: -6px;
right: -6px;
top: 0;
bottom: 0;
cursor: ew-resize;
}
overflow: hidden;
z-index: 9999;
}
#fileContainer {
height: 100%;
display: flex;
flex-direction: column;
}
.dock-header {
display: flex;
justify-content: space-between;
padding: 10px 12px;
background: #f6f6f6;
border-bottom: 1px solid #eee;
font-size: 14px;
}
#previewContainer {
height: 100%;
}
</style>
</head>
.dock-actions button {
border: none;
background: transparent;
cursor: pointer;
font-size: 16px;
}
<body>
<div class="g3-layout" id="dropZone" style="position: relative;">
<div class="pane" data-pane="left" style="width: 10%;min-width: 150px;">
<ul id="otherbaseTree" class="easyui-tree"></ul>
</div>
.dock-body {
padding: 12px;
}
<div class="split" id="split1"></div>
.progress-line {
display: flex;
justify-content: space-between;
font-size: 13px;
margin-bottom: 8px;
}
<div class="pane" data-pane="middle" style="width: 30%;min-width: 450px;">
<div id="fileContainer"></div>
</div>
.current-file {
font-size: 12px;
color: #666;
margin-bottom: 10px;
}
<div class="split" id="split2"></div>
.bar {
height: 6px;
background: #eee;
border-radius: 3px;
overflow: hidden;
}
.bar-inner {
width: 45%;
height: 100%;
background: #faad14;
}
</style>
</head>
<body>
<div class="page">
<div class="header">File Manager Header</div>
<div class="content">
<!-- =========================
🔴 方案 A:嵌入式
========================== -->
<div class="embedded-bar">
<div class="row">
<span>Downloading</span>
<span>3 / 10</span>
<span>report.pdf</span>
<div class="pane" data-pane="right" style="flex:1">
<div id="previewContainer">文件预览位置</div>
</div>
</div>
<div class="actions">
<button>Hide</button>
<button>Cancel</button>
</div>
</div>
<script type="text/javascript">
/** 全局变量 */
var vid1 = "<%=id%>";
var mx_moduleid = "<%=mx_moduleid%>";
var sh_state = "<%=sh_state%>";
var iread = "<%=iread%>";
var orgid = "<%=orgid%>";
var tree_where = "<%=tree_where%>";
var mx_secondfather = "<%=mx_secondfather%>";
var mx_secondfather_fz = "<%=mx_secondfather_fz%>";
var default_click = "<%=default_click%>";
var Fmoduleid = "<%=mx_moduleid%>";
var modid = "g3hdzjsj";
var mx_orderid = '';
var jpowerqx = null;
var g3FileInstance = null;
var userid = "<%=userid%>";
var tablename = "bf_files";
<div>
<p>File list area...</p>
<p>Scroll content...</p>
<p>Scroll content...</p>
<p>Scroll content...</p>
</div>
/** 多语言文本 */
var tabtitle = '所有类别';
var xztitle = '下载';
var viewtitle = '查看';
var confirmOk = '是';
var confirmCancel = '否';
var alertCannotDelete = '已审核不能删除';
var alertSelectFile = '请选择要删除的图片';
var alertDeleteSuccess = '删除成功';
var alertDeleteFail = '删除失败';
var alertDeletePartial = '部分删除失败:成功 {0} 条,失败 {1} 条';
var confirmTitle = '操作提示';
var confirmMessage = '您确定删除选中的 {0} 条数据吗?';
</div>
/**
* 初始化多语言配置
*/
function initLanguage() {
var language = getCookie("languageCook");
if (language == 1) {
tabtitle = 'All Categories';
xztitle = 'Download';
viewtitle = 'View';
confirmOk = 'Yes';
confirmCancel = 'No';
alertCannotDelete = 'Cannot delete after review';
alertSelectFile = 'Please select files to delete';
alertDeleteSuccess = 'Delete success';
alertDeleteFail = 'Delete failed';
alertDeletePartial = 'Partial delete failed: {0} success, {1} failed';
confirmTitle = 'Confirmation';
confirmMessage = 'Are you sure to delete {0} selected items?';
} else if (language == 2) {
tabtitle = 'Все категории';
xztitle = 'Скачать';
viewtitle = 'Просмотр';
}
}
<div class="footer">Footer</div>
/**
* 绑定分割条拖拽事件
* @param {string} splitId - 分割条元素ID
* @param {HTMLElement} leftEl - 左侧面板元素
*/
function bindSplit(splitId, leftEl) {
const split = document.getElementById(splitId);
let startX = 0, startWidth = 0, dragging = false;
</div>
split.addEventListener('mousedown', function (e) {
if (e.button !== 0) return;
dragging = true;
startX = e.clientX;
startWidth = leftEl.offsetWidth;
document.body.style.userSelect = 'none';
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
<!-- =========================
🟢 方案 B:浮层 Dock
========================= -->
<div class="download-dock">
<div class="dock-header">
<span>Downloading</span>
function onMouseMove(e) {
if (!dragging) return;
let newWidth = startWidth + (e.clientX - startX);
if (newWidth < 100) newWidth = 150;
leftEl.style.width = newWidth + 'px';
}
<div class="dock-actions">
<button title="minimize">−</button>
<button title="close">×</button>
</div>
</div>
function onMouseUp() {
dragging = false;
document.body.style.userSelect = '';
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
}
<div class="dock-body">
<div class="progress-line">
<span>3 / 10</span>
<span>30%</span>
</div>
/**
* 初始化文件管理组件
*/
function initG3File() {
if (g3FileInstance) return;
<div class="current-file">
report.pdf
</div>
var langCode = getCookie("languageCook") == 1 ? "en-US" : "zh-CN";
<div class="bar">
<div class="bar-inner"></div>
</div>
</div>
</div>
g3FileInstance = $('#fileContainer').G3File({
language: langCode,
uploadConfig: {
aliyun: {
getAliyunKey: function () {
return new Promise(function (resolve, reject) {
$.ajax({
url: "jattachfiletype/getaliyunkey.jsp",
type: "GET",
data: {
orgid: orgid,
bucket: modid
},
success: function (res) {
try {
var json = typeof res === "string" ? JSON.parse(res) : res;
resolve({
accessid: json.accessid,
policy: json.policy,
signature: json.signature,
callback: json.callback,
host: json.host,
dir: json.dir,
expire: json.expire
});
} catch (e) {
reject(e);
}
},
error: function (err) {
reject(err);
}
});
});
}
}
},
onDelete: handleDelete
});
}
</body>
/**
* 处理文件删除操作,删除成功后刷新文件列表
* @param {Array} files - 待删除文件列表
* @param {Function} callback - 删除完成回调函数
*/
function handleDelete(files, callback) {
if (sh_state == 0) {
alert(alertCannotDelete);
callback(false);
return;
}
</html>
if (!files || files.length === 0) {
alert(alertSelectFile);
callback(false);
return;
}
$.messager.defaults = {
ok: confirmOk,
cancel: confirmCancel
};
$.messager.confirm(confirmTitle, confirmMessage.replace('{0}', files.length), function (ok) {
if (!ok) {
callback(false);
return;
}
var ids = files.map(item => item.subid);
if (ids.length === 0) {
alert(alertSelectFile);
callback(false);
return;
}
$.ajax({
type: 'post',
url: 'jattachfiletype/DelSubRecordBath.jsp',
data: {
module: tablename,
bucketName: modid,
ids: ids.join(",")
},
success: function (res) {
var last = res[res.length - 1];
if (last.result) {
alert(alertDeleteSuccess);
} else {
alert(alertDeletePartial.replace('{0}', last.success).replace('{1}', last.fail));
}
callback(true);
refreshFileList();
},
error: function () {
alert(alertDeleteFail);
callback(false);
}
});
});
}
/**
* 刷新文件列表(删除后重新请求数据)
*/
function refreshFileList() {
var queryWhere = buildQueryWhere(mx_orderid);
searchData(queryWhere);
}
/**
* 构建查询条件
* @param {string} orderId - 订单ID
* @returns {string} - 查询条件字符串
*/
function buildQueryWhere(orderId) {
var queryWhere = "mx_moduleid='" + Fmoduleid + "' and father='" + vid1 + "'";
if (tree_where && tree_where !== '' && tree_where != 'null') {
queryWhere += " and mx_orderid='" + tree_where + "'";
}
if (orderId && orderId !== '') {
queryWhere = "mx_moduleid='" + Fmoduleid + "' and father='" + vid1 + "' and mx_orderid='" + orderId + "'";
}
if (mx_secondfather && mx_secondfather !== '' && mx_secondfather != 'null') {
queryWhere += " and mx_secondfather='" + mx_secondfather + "'";
}
return queryWhere;
}
/**
* 搜索文件数据
* @param {string} condition - 查询条件
*/
function searchData(condition) {
if (!g3FileInstance) {
initG3File();
}
$.ajax({
url: "jattachfiletype/loaddata_attach.jsp",
type: "post",
async: true,
data: {
viewname: "bf_files",
searchcondition: condition,
orderfield: '',
soption: ''
},
success: function (res) {
var fileList = [];
for (var i = 0; i < res.length; i++) {
fileList.push({
name: res[i].mx_filename,
size: res[i].mx_filesize,
uploadDate: res[i].mx_addtime,
url: res[i].mx_mapfilename,
...res[i]
});
}
g3FileInstance.setFiles(fileList);
},
error: function () {
g3FileInstance.setFiles([]);
}
});
}
/**
* 创建文件类型树
*/
function createTree() {
$('#otherbaseTree').tree('append', {
data: [{ id: '', text: tabtitle }]
});
var node = $('#otherbaseTree').tree('find', '');
var queryWhere = '';
if (tree_where !== '' && tree_where != 'null') {
queryWhere = "and b_id='" + tree_where + "'";
}
$.ajax({
type: 'post',
url: 'jsp/searchdata.jsp',
cache: false,
async: false,
data: {
soption: 'main',
searchcondition: "2>1 and b_module_id='" + Fmoduleid + "'" + queryWhere,
viewname: 'bf_files_type',
orderfield: 'b_xh'
},
dataType: 'json',
success: function (result) {
for (var i in result) {
$('#otherbaseTree').tree('append', {
parent: node.target,
data: [{ id: result[i].b_id, text: result[i].b_name }]
});
}
if (default_click && default_click != 'null') {
setTimeout(function () {
var node = $('#otherbaseTree').tree('find', default_click);
if (node) {
$('#otherbaseTree').tree('select', node.target);
onClickNode(node);
}
}, 200);
}
}
});
}
/**
* 加载子节点数据
* @param {Object} node - 树节点对象
*/
function loadChildNodes(node) {
$.ajax({
type: 'post',
url: 'jsp/searchdata.jsp',
cache: false,
async: false,
data: {
soption: 'main',
searchcondition: "b_module_id ='" + node.id + "'",
viewname: 'bf_files_type',
orderfield: 'b_xh'
},
dataType: 'json',
success: function (result) {
for (var i in result) {
$('#otherbaseTree').tree('append', {
parent: node.target,
data: [{ id: result[i].b_id, text: result[i].b_name }]
});
}
}
});
}
/**
* 树节点点击事件处理
* @param {Object} node - 被点击的树节点
*/
function onClickNode(node) {
mx_orderid = node.id;
var queryWhere = buildQueryWhere(node.id);
if ($('#otherbaseTree').tree('getLevel', node.target) > 0 && $('#otherbaseTree').tree('isLeaf', node.target)) {
loadChildNodes(node);
}
searchData(queryWhere);
}
/**
* 初始化权限控制
*/
function initPowerControl() {
if (orgid != 'demo') {
jpowerqx = getUserPowerQX('bf_files', userid);
for (var key in jpowerqx) {
if (jpowerqx[key] != '1') {
$("a[tag='" + key + "']").linkbutton('disable');
$("button[tag='" + key + "']").attr('disabled', true);
$("input[tag='" + key + "']").attr('disabled', true);
}
}
}
if (getModulePowerByTag(Fmoduleid, userid, '4') != '1') {
sh_state = 0;
}
}
/**
* 页面初始化入口
*/
$(function () {
initLanguage();
bindSplit('split1', document.querySelector('[data-pane="left"]'));
bindSplit('split2', document.querySelector('[data-pane="middle"]'));
createTree();
$('#otherbaseTree').tree({
onClick: onClickNode
});
initPowerControl();
});
</script>
</body>
</html>