feat: 功能优化
This commit is contained in:
52
.claude/commands/code-simplifier.md
Normal file
52
.claude/commands/code-simplifier.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
name: code-simplifier
|
||||||
|
description: Simplifies and refines code for clarity, consistency, and maintainability while preserving all functionality. Focuses on recently modified code unless instructed otherwise.
|
||||||
|
model: opus
|
||||||
|
---
|
||||||
|
|
||||||
|
You are an expert code simplification specialist focused on enhancing code clarity, consistency, and maintainability while preserving exact functionality. Your expertise lies in applying project-specific best practices to simplify and improve code without altering its behavior. You prioritize readable, explicit code over overly compact solutions. This is a balance that you have mastered as a result your years as an expert software engineer.
|
||||||
|
|
||||||
|
You will analyze recently modified code and apply refinements that:
|
||||||
|
|
||||||
|
1. **Preserve Functionality**: Never change what the code does - only how it does it. All original features, outputs, and behaviors must remain intact.
|
||||||
|
|
||||||
|
2. **Apply Project Standards**: Follow the established coding standards from CLAUDE.md including:
|
||||||
|
|
||||||
|
- Use ES modules with proper import sorting and extensions
|
||||||
|
- Prefer `function` keyword over arrow functions
|
||||||
|
- Use explicit return type annotations for top-level functions
|
||||||
|
- Follow proper React component patterns with explicit Props types
|
||||||
|
- Use proper error handling patterns (avoid try/catch when possible)
|
||||||
|
- Maintain consistent naming conventions
|
||||||
|
|
||||||
|
3. **Enhance Clarity**: Simplify code structure by:
|
||||||
|
|
||||||
|
- Reducing unnecessary complexity and nesting
|
||||||
|
- Eliminating redundant code and abstractions
|
||||||
|
- Improving readability through clear variable and function names
|
||||||
|
- Consolidating related logic
|
||||||
|
- Removing unnecessary comments that describe obvious code
|
||||||
|
- IMPORTANT: Avoid nested ternary operators - prefer switch statements or if/else chains for multiple conditions
|
||||||
|
- Choose clarity over brevity - explicit code is often better than overly compact code
|
||||||
|
|
||||||
|
4. **Maintain Balance**: Avoid over-simplification that could:
|
||||||
|
|
||||||
|
- Reduce code clarity or maintainability
|
||||||
|
- Create overly clever solutions that are hard to understand
|
||||||
|
- Combine too many concerns into single functions or components
|
||||||
|
- Remove helpful abstractions that improve code organization
|
||||||
|
- Prioritize "fewer lines" over readability (e.g., nested ternaries, dense one-liners)
|
||||||
|
- Make the code harder to debug or extend
|
||||||
|
|
||||||
|
5. **Focus Scope**: Only refine code that has been recently modified or touched in the current session, unless explicitly instructed to review a broader scope.
|
||||||
|
|
||||||
|
Your refinement process:
|
||||||
|
|
||||||
|
1. Identify the recently modified code sections
|
||||||
|
2. Analyze for opportunities to improve elegance and consistency
|
||||||
|
3. Apply project-specific best practices and coding standards
|
||||||
|
4. Ensure all functionality remains unchanged
|
||||||
|
5. Verify the refined code is simpler and more maintainable
|
||||||
|
6. Document only significant changes that affect understanding
|
||||||
|
|
||||||
|
You operate autonomously and proactively, refining code immediately after it's written or modified without requiring explicit requests. Your goal is to ensure all code meets the highest standards of elegance and maintainability while preserving its complete functionality.
|
||||||
@@ -58,7 +58,11 @@
|
|||||||
"mcp__server-mysql__connect_db",
|
"mcp__server-mysql__connect_db",
|
||||||
"Skill(ui-ux-pro-max)",
|
"Skill(ui-ux-pro-max)",
|
||||||
"Skill(ui-ux-pro-max:*)",
|
"Skill(ui-ux-pro-max:*)",
|
||||||
"Bash(python:*)"
|
"Bash(python:*)",
|
||||||
|
"mcp__server-mysql__describe_table",
|
||||||
|
"mcp__server-mysql__execute",
|
||||||
|
"mcp__server-mysql__query",
|
||||||
|
"Bash(/d/projects/sionrui/.claude/skills/ui-ux-pro-max/python scripts/search.py:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
target/
|
target/
|
||||||
!.mvn/wrapper/maven-wrapper.jar
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
settings.local.json
|
||||||
|
|
||||||
.flattened-pom.xml
|
.flattened-pom.xml
|
||||||
|
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
# 前端上传逻辑检查报告
|
|
||||||
|
|
||||||
## 📋 代码检查结果
|
|
||||||
|
|
||||||
### ✅ 正常逻辑
|
|
||||||
|
|
||||||
1. **MaterialUploadModal.vue**
|
|
||||||
- 文件上传组件逻辑清晰
|
|
||||||
- 文件列表管理合理
|
|
||||||
- 文件大小和类型校验完善
|
|
||||||
|
|
||||||
2. **MaterialList.vue**
|
|
||||||
- 上传流程完整
|
|
||||||
- 错误处理合理
|
|
||||||
- 批量上传逻辑正确
|
|
||||||
|
|
||||||
### ⚠️ 发现的问题
|
|
||||||
|
|
||||||
#### 1. 冗余代码
|
|
||||||
|
|
||||||
**MaterialUploadModal.vue**:
|
|
||||||
- `handleDrop` 方法(第188-191行)只是打印日志,没有实际作用
|
|
||||||
```javascript
|
|
||||||
const handleDrop = (e) => {
|
|
||||||
// a-upload-dragger 会自动处理拖拽的文件,通过 change 事件触发
|
|
||||||
console.log('Drop event:', e)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**建议**:删除此方法,因为 `a-upload-dragger` 会自动处理拖拽
|
|
||||||
|
|
||||||
#### 2. 逻辑问题
|
|
||||||
|
|
||||||
**MaterialList.vue**:
|
|
||||||
- `uploadFileCategory` 固定为 'video'(第147行)
|
|
||||||
```javascript
|
|
||||||
const uploadFileCategory = ref('video') // 固定为 video,不需要用户选择
|
|
||||||
```
|
|
||||||
**问题**:用户无法选择文件分类,所有文件都上传到 video 分类
|
|
||||||
**建议**:添加文件分类选择功能,或者根据文件类型自动判断分类
|
|
||||||
|
|
||||||
#### 3. 代码优化建议
|
|
||||||
|
|
||||||
**MaterialUploadModal.vue**:
|
|
||||||
- `handleFileChange` 方法(第160-174行)逻辑可以简化
|
|
||||||
- 文件对象提取逻辑(第201-217行)虽然复杂,但是必要的(因为 Ant Design Vue 的文件对象结构复杂)
|
|
||||||
|
|
||||||
## 🎯 优化建议
|
|
||||||
|
|
||||||
### 1. 删除冗余的 handleDrop 方法
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 删除这个方法,因为 a-upload-dragger 会自动处理拖拽
|
|
||||||
// const handleDrop = (e) => {
|
|
||||||
// console.log('Drop event:', e)
|
|
||||||
// }
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 添加文件分类选择功能
|
|
||||||
|
|
||||||
在 `MaterialUploadModal.vue` 中添加分类选择:
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<!-- 文件分类选择 -->
|
|
||||||
<div class="upload-category-select">
|
|
||||||
<div class="upload-label">文件分类:</div>
|
|
||||||
<a-select
|
|
||||||
v-model="fileCategory"
|
|
||||||
placeholder="请选择文件分类"
|
|
||||||
style="width: 100%"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<a-option value="video">视频集</a-option>
|
|
||||||
<a-option value="generate">生成集</a-option>
|
|
||||||
<a-option value="audio">配音集</a-option>
|
|
||||||
<a-option value="mix">混剪集</a-option>
|
|
||||||
<a-option value="voice">声音集</a-option>
|
|
||||||
</a-select>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
然后在 `MaterialList.vue` 中接收分类参数:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const handleConfirmUpload = async (files, fileCategory) => {
|
|
||||||
// 使用传入的分类,而不是固定的 'video'
|
|
||||||
await MaterialService.uploadFile(file, fileCategory)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 总结
|
|
||||||
|
|
||||||
| 项目 | 状态 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| 上传逻辑 | ✅ 正常 | 文件上传流程完整 |
|
|
||||||
| 错误处理 | ✅ 正常 | 错误提示和异常处理完善 |
|
|
||||||
| 文件校验 | ✅ 正常 | 文件大小和类型校验合理 |
|
|
||||||
| 冗余代码 | ⚠️ 1处 | `handleDrop` 方法无实际作用 |
|
|
||||||
| 功能缺失 | ⚠️ 1处 | 缺少文件分类选择功能 |
|
|
||||||
|
|
||||||
## 🔧 建议修复
|
|
||||||
|
|
||||||
1. **删除冗余代码**:移除 `handleDrop` 方法
|
|
||||||
2. **添加分类选择**:在 `MaterialUploadModal` 中添加文件分类选择功能
|
|
||||||
3. **优化用户体验**:根据文件类型自动推荐分类(可选)
|
|
||||||
|
|
||||||
@@ -162,7 +162,21 @@ export const MaterialService = {
|
|||||||
return http.get(`${BASE_URL}/preview-url`, {
|
return http.get(`${BASE_URL}/preview-url`, {
|
||||||
params: { id: fileId, type }
|
params: { id: fileId, type }
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新文件显示名称
|
||||||
|
* @param {number} fileId - 文件编号
|
||||||
|
* @param {string} displayName - 新的显示名称
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
updateDisplayName(fileId, displayName) {
|
||||||
|
return http.post(`${BASE_URL}/update-display-name`, {
|
||||||
|
fileId,
|
||||||
|
displayName
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -42,6 +42,6 @@ import SidebarNav from '@/components/SidebarNav.vue'
|
|||||||
.content-scroll {
|
.content-scroll {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: auto; /* 右侧内容区域滚动 */
|
overflow: auto; /* 右侧内容区域滚动 */
|
||||||
padding: 16px 0px 16px 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -150,146 +150,6 @@ body { scrollbar-gutter: stable both-edges; }
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
3. 组件样式 (Component Styles)
|
|
||||||
================================ */
|
|
||||||
|
|
||||||
/* Button 组件 */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: var(--radius-button);
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
padding: var(--space-2) var(--space-6);
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--primary {
|
|
||||||
background: var(--color-slate-900);
|
|
||||||
color: white;
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
|
||||||
background: var(--color-slate-800);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--secondary {
|
|
||||||
background: white;
|
|
||||||
color: var(--color-slate-700);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
padding: var(--space-1) var(--space-4);
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
|
||||||
background: var(--color-slate-50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--gradient {
|
|
||||||
background: linear-gradient(to right, var(--color-indigo-600), var(--color-indigo-800));
|
|
||||||
color: white;
|
|
||||||
box-shadow: var(--shadow-blue);
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
|
||||||
background: linear-gradient(to right, var(--color-indigo-700), var(--color-indigo-900));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Input 组件 */
|
|
||||||
.input {
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--space-2) var(--space-4);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-button);
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
background: white;
|
|
||||||
color: var(--color-text);
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--color-border-focus);
|
|
||||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--color-slate-400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card 组件 */
|
|
||||||
.card {
|
|
||||||
background: white;
|
|
||||||
border: 1px solid var(--color-slate-200);
|
|
||||||
border-radius: var(--radius-card);
|
|
||||||
padding: var(--space-6);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table 组件 */
|
|
||||||
.table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
|
|
||||||
th {
|
|
||||||
background: var(--color-slate-50);
|
|
||||||
padding: var(--space-3) var(--space-4);
|
|
||||||
text-align: left;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-slate-500);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
border-bottom: 1px solid var(--color-slate-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: var(--space-4);
|
|
||||||
border-bottom: 1px solid var(--color-slate-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:hover {
|
|
||||||
background: var(--color-slate-50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tag 组件 */
|
|
||||||
.tag {
|
|
||||||
display: inline-block;
|
|
||||||
padding: var(--space-0-5) var(--space-2);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: var(--radius-tag);
|
|
||||||
background: var(--color-gray-100);
|
|
||||||
color: var(--color-slate-700);
|
|
||||||
|
|
||||||
&--red {
|
|
||||||
background: #fee2e2;
|
|
||||||
color: var(--color-red-800);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--yellow {
|
|
||||||
background: #fef3c7;
|
|
||||||
color: var(--color-yellow-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--vip {
|
|
||||||
color: var(--color-yellow-500);
|
|
||||||
border: 1px solid var(--color-yellow-500);
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ function handleReset() {
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
background: var(--color-bg);
|
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|||||||
@@ -365,106 +365,151 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
// ========== 公共变量 ==========
|
|
||||||
@primary-color: var(--primary-color, #1677ff);
|
|
||||||
@primary-bg-10: rgba(22, 119, 255, 0.1);
|
|
||||||
@primary-bg-15: rgba(22, 119, 255, 0.15);
|
|
||||||
@primary-bg-20: rgba(22, 119, 255, 0.2);
|
|
||||||
@primary-bg-30: rgba(22, 119, 255, 0.3);
|
|
||||||
@primary-bg-50: rgba(22, 119, 255, 0.5);
|
|
||||||
@success-color: var(--color-green-500);
|
|
||||||
@error-color: var(--color-red-500);
|
|
||||||
@warning-color: var(--color-yellow-500);
|
|
||||||
@surface-bg: var(--bg-primary);
|
|
||||||
@surface-bg-25: var(--bg-primary);
|
|
||||||
@surface-bg-30: var(--bg-secondary);
|
|
||||||
@text-primary: var(--text-primary);
|
|
||||||
@text-secondary: var(--text-secondary);
|
|
||||||
@panel-bg: var(--bg-primary);
|
|
||||||
|
|
||||||
/* ========== 页面布局 ========== */
|
/* ========== 页面布局 ========== */
|
||||||
.kling-page { padding: 0; }
|
.kling-page {
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
.kling-content {
|
.kling-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 480px 1fr;
|
||||||
gap: 24px;
|
gap: 32px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
.upload-panel,
|
|
||||||
.result-panel {
|
/* 优化左侧配置面板 */
|
||||||
background: @panel-bg;
|
.upload-panel {
|
||||||
border-radius: 12px;
|
background: var(--bg-primary);
|
||||||
padding: 24px;
|
border-radius: 16px;
|
||||||
|
padding: 28px;
|
||||||
|
height: fit-content;
|
||||||
|
position: sticky;
|
||||||
|
top: 24px;
|
||||||
|
max-height: calc(100vh - 48px);
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
/* 自定义滚动条 */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-light);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--text-secondary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 基础组件 ========== */
|
/* ========== 基础组件 ========== */
|
||||||
.section { margin-bottom: 24px; }
|
.section {
|
||||||
.section h3,
|
margin-bottom: 24px;
|
||||||
.card-content h4,
|
}
|
||||||
.audio-info h4 {
|
|
||||||
color: @text-primary;
|
.section h3 {
|
||||||
|
color: var(--text-primary);
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.section h3 { margin-bottom: 12px; }
|
|
||||||
.card-content h4, .audio-info h4 { margin-bottom: 12px; font-size: 14px; }
|
.card-content h4,
|
||||||
.card-content p, .duration-label span:first-child { color: @text-secondary; font-size: 13px; }
|
.audio-info h4 {
|
||||||
.card-content p { margin: 0; }
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content p,
|
||||||
|
.duration-label span:first-child {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ========== 表单控件 ========== */
|
/* ========== 表单控件 ========== */
|
||||||
.tts-textarea {
|
.tts-textarea {
|
||||||
background: var(--bg-primary);
|
background: var(--bg-secondary);
|
||||||
border-radius: 6px;
|
border: 1px solid var(--border-light);
|
||||||
padding: 12px;
|
border-radius: 8px;
|
||||||
color: @text-primary;
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
:deep(.ant-input) {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-hint {
|
.text-hint {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
margin-top: 8px;
|
margin-top: 12px;
|
||||||
padding: 8px 12px;
|
padding: 12px 16px;
|
||||||
background: var(--bg-secondary);
|
background: rgba(var(--color-primary), 0.1);
|
||||||
border: 1px solid var(--border-light);
|
border: 1px solid rgba(var(--color-primary), 0.3);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: @text-secondary;
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-icon {
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.hint-icon { font-size: 14px; }
|
|
||||||
|
|
||||||
/* ========== 控制面板 ========== */
|
/* ========== 控制面板 ========== */
|
||||||
.control-group { margin-bottom: 16px; }
|
.control-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.control-label {
|
.control-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: @text-primary;
|
color: var(--text-primary);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider-card {
|
.slider-card {
|
||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 10px 12px;
|
padding: 12px 16px;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider-info {
|
.slider-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: @text-secondary;
|
color: var(--text-secondary);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider-value {
|
.slider-value {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: @text-primary;
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.reset-btn {
|
.reset-btn {
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
color: @text-primary;
|
color: var(--text-secondary);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -474,37 +519,43 @@ onMounted(async () => {
|
|||||||
.video-selection-cards {
|
.video-selection-cards {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-option-card {
|
.video-option-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
padding: 20px;
|
padding: 16px;
|
||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
background: rgba(var(--color-primary), 0.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.video-option-card:hover {
|
|
||||||
border-color: @primary-color;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
}
|
|
||||||
.video-option-card.selected {
|
|
||||||
border-color: @primary-color;
|
|
||||||
background: @primary-bg-10;
|
|
||||||
}
|
|
||||||
.card-icon {
|
.card-icon {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: @primary-color;
|
color: var(--color-primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background: @primary-bg-10;
|
background: rgba(var(--color-primary), 0.1);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 视频预览 ========== */
|
/* ========== 视频预览 ========== */
|
||||||
@@ -537,7 +588,7 @@ onMounted(async () => {
|
|||||||
bottom: 4px;
|
bottom: 4px;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
background: rgba(0, 0, 0, 0.8);
|
background: rgba(0, 0, 0, 0.8);
|
||||||
color: @text-primary;
|
color: var(--text-primary);
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -548,7 +599,7 @@ onMounted(async () => {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.video-title {
|
.video-title {
|
||||||
color: @text-primary;
|
color: var(--text-primary);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
@@ -560,32 +611,81 @@ onMounted(async () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: @text-secondary;
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 上传区域 ========== */
|
/* ========== 上传区域 ========== */
|
||||||
.upload-zone {
|
.upload-zone {
|
||||||
min-height: 300px;
|
min-height: 280px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 2px dashed var(--border-light);
|
border: 2px dashed var(--border-light);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
|
||||||
|
&.drag-over {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
background: rgba(var(--color-primary), 0.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-placeholder {
|
.upload-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 40px 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: @text-secondary;
|
|
||||||
|
h3 {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
padding: 8px 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.upload-placeholder h3 { color: @text-primary; }
|
|
||||||
.video-preview {
|
.video-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: @text-primary;
|
|
||||||
|
p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 6px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-video {
|
.preview-video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 300px;
|
max-height: 280px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 验证结果 ========== */
|
/* ========== 验证结果 ========== */
|
||||||
@@ -595,59 +695,93 @@ onMounted(async () => {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.validation-result.validation-passed {
|
.validation-result.validation-passed {
|
||||||
border-color: rgba(82, 196, 26, 0.3);
|
border-color: var(--color-success);
|
||||||
background: rgba(82, 196, 26, 0.05);
|
background: rgba(var(--color-success), 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.validation-result.validation-failed {
|
.validation-result.validation-failed {
|
||||||
border-color: rgba(255, 77, 79, 0.3);
|
border-color: var(--color-error);
|
||||||
background: rgba(255, 77, 79, 0.05);
|
background: rgba(var(--color-error), 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.validation-status {
|
.validation-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
.status-icon { font-size: 18px; }
|
|
||||||
|
.status-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.status-text {
|
.status-text {
|
||||||
color: @text-primary;
|
color: var(--text-primary);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 时长对比进度条 ========== */
|
/* ========== 时长对比进度条 ========== */
|
||||||
.duration-comparison { margin-bottom: 16px; }
|
.duration-comparison {
|
||||||
.duration-bar { margin-bottom: 12px; }
|
margin-bottom: 16px;
|
||||||
.duration-bar:last-child { margin-bottom: 0; }
|
padding: 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-bar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.duration-label {
|
.duration-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 6px;
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-value {
|
.duration-value {
|
||||||
color: @text-primary;
|
color: var(--text-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: var(--bg-primary);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-fill {
|
.progress-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
transition: width 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-bar .progress-fill {
|
.audio-bar .progress-fill {
|
||||||
background: linear-gradient(90deg, @primary-color, #60A5FA);
|
background: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-bar .progress-fill.success {
|
.video-bar .progress-fill.success {
|
||||||
background: linear-gradient(90deg, @success-color, #73d13d);
|
background: var(--color-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-bar .progress-fill.error {
|
.video-bar .progress-fill.error {
|
||||||
background: linear-gradient(90deg, @error-color, #ff7875);
|
background: var(--color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 错误提示 ========== */
|
/* ========== 错误提示 ========== */
|
||||||
@@ -657,12 +791,17 @@ onMounted(async () => {
|
|||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
color: @error-color;
|
color: var(--color-error);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin: 0 0 12px 0;
|
margin: 0 0 12px 0;
|
||||||
}
|
}
|
||||||
.quick-actions { display: flex; gap: 8px; }
|
|
||||||
|
.quick-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ========== 音频生成 ========== */
|
/* ========== 音频生成 ========== */
|
||||||
.audio-generation-section {
|
.audio-generation-section {
|
||||||
@@ -672,24 +811,49 @@ onMounted(async () => {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
}
|
}
|
||||||
.generate-audio-row { margin-bottom: 16px; }
|
|
||||||
|
.generate-audio-row {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.audio-preview {
|
.audio-preview {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-info {
|
.duration-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.duration-info .label { color: @text-secondary; }
|
|
||||||
.duration-info .value { color: @text-primary; font-weight: 600; }
|
.duration-info .label {
|
||||||
.duration-info.validation-passed .value { color: @success-color; }
|
color: var(--text-secondary);
|
||||||
.duration-info.validation-failed .value { color: @error-color; }
|
}
|
||||||
.audio-player { margin: 16px 0; }
|
|
||||||
.audio-element { width: 100%; }
|
.duration-info .value {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-info.validation-passed .value {
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-info.validation-failed .value {
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-player {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-element {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.regenerate-row {
|
.regenerate-row {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
@@ -701,24 +865,49 @@ onMounted(async () => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-buttons .ant-btn[type="primary"] {
|
||||||
|
height: 48px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.generate-hint {
|
.generate-hint {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 8px;
|
padding: 12px 16px;
|
||||||
padding: 8px 12px;
|
background: rgba(var(--color-warning), 0.1);
|
||||||
background: rgba(255, 193, 7, 0.1);
|
border: 1px solid rgba(var(--color-warning), 0.3);
|
||||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: @warning-color;
|
color: var(--color-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 响应式 ========== */
|
/* ========== 响应式 ========== */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.kling-content {
|
.kling-content {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
padding: 16px;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-panel {
|
||||||
|
position: static;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-selection-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-zone {
|
||||||
|
min-height: 240px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -59,12 +59,33 @@
|
|||||||
|
|
||||||
<!-- 右侧内容区域 -->
|
<!-- 右侧内容区域 -->
|
||||||
<div class="material-content">
|
<div class="material-content">
|
||||||
<!-- 头部操作栏 -->
|
<!-- 搜索栏 -->
|
||||||
<div class="material-content__header">
|
<div class="material-content__search">
|
||||||
<h2 class="material-content__title">
|
<a-input
|
||||||
{{ getContentTitle() }}
|
v-model="searchKeyword"
|
||||||
</h2>
|
placeholder="搜索文件名"
|
||||||
|
style="width: 300px"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<SearchOutlined />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
<a-button type="primary" @click="handleSearch">搜索</a-button>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
<div class="material-content__actions">
|
<div class="material-content__actions">
|
||||||
|
<!-- 全选/取消全选 -->
|
||||||
|
<a-checkbox
|
||||||
|
:checked="selectedFileIds.length === fileList.length && fileList.length > 0"
|
||||||
|
:indeterminate="selectedFileIds.length > 0 && selectedFileIds.length < fileList.length"
|
||||||
|
@change="handleSelectAll"
|
||||||
|
>
|
||||||
|
<span v-if="selectedFileIds.length === 0">全选</span>
|
||||||
|
<span v-else>已选 {{ selectedFileIds.length }}</span>
|
||||||
|
</a-checkbox>
|
||||||
|
|
||||||
<a-button type="primary" @click="handleOpenUploadModal">
|
<a-button type="primary" @click="handleOpenUploadModal">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<UploadOutlined />
|
<UploadOutlined />
|
||||||
@@ -89,22 +110,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索栏 -->
|
|
||||||
<div class="material-content__search">
|
|
||||||
<a-input
|
|
||||||
v-model="searchKeyword"
|
|
||||||
placeholder="搜索文件名"
|
|
||||||
style="width: 300px"
|
|
||||||
allow-clear
|
|
||||||
@press-enter="handleSearch"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<SearchOutlined />
|
|
||||||
</template>
|
|
||||||
</a-input>
|
|
||||||
<a-button type="primary" @click="handleSearch">搜索</a-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 文件列表 -->
|
<!-- 文件列表 -->
|
||||||
<div class="material-content__list">
|
<div class="material-content__list">
|
||||||
<a-spin :spinning="loading" tip="加载中..." style="width: 100%; min-height: 400px;">
|
<a-spin :spinning="loading" tip="加载中..." style="width: 100%; min-height: 400px;">
|
||||||
@@ -113,6 +118,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="file in fileList"
|
v-for="file in fileList"
|
||||||
:key="file.id"
|
:key="file.id"
|
||||||
|
:data-file-id="file.id"
|
||||||
class="material-item"
|
class="material-item"
|
||||||
:class="{ 'material-item--selected': selectedFileIds.includes(file.id) }"
|
:class="{ 'material-item--selected': selectedFileIds.includes(file.id) }"
|
||||||
@click="handleFileClick(file)"
|
@click="handleFileClick(file)"
|
||||||
@@ -125,6 +131,7 @@
|
|||||||
:src="file.coverBase64"
|
:src="file.coverBase64"
|
||||||
:alt="file.fileName"
|
:alt="file.fileName"
|
||||||
@error="handleImageError"
|
@error="handleImageError"
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<div v-else class="material-item__preview-placeholder">
|
<div v-else class="material-item__preview-placeholder">
|
||||||
<FileOutlined />
|
<FileOutlined />
|
||||||
@@ -133,10 +140,27 @@
|
|||||||
|
|
||||||
<!-- 文件信息 -->
|
<!-- 文件信息 -->
|
||||||
<div class="material-item__info">
|
<div class="material-item__info">
|
||||||
<div class="material-item__name" :title="file.fileName">
|
<div
|
||||||
{{ file.fileName }}
|
class="material-item__name"
|
||||||
|
:title="file.displayName"
|
||||||
|
@click="handleEditName(file)"
|
||||||
|
>
|
||||||
|
<template v-if="editingFileId !== file.id">
|
||||||
|
{{ file.displayName }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-input
|
||||||
|
v-model:value="editingDisplayName"
|
||||||
|
size="small"
|
||||||
|
@blur="handleSaveName(file)"
|
||||||
|
@press-enter="handleSaveName(file)"
|
||||||
|
@click.stop
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="material-item__meta">
|
<div class="material-item__meta">
|
||||||
|
<span class="material-item__type">{{ getFileTypeText(file.fileName) }}</span>
|
||||||
<span class="material-item__size">{{ formatFileSize(file.fileSize) }}</span>
|
<span class="material-item__size">{{ formatFileSize(file.fileSize) }}</span>
|
||||||
<span class="material-item__time">{{ formatDate(file.createTime) }}</span>
|
<span class="material-item__time">{{ formatDate(file.createTime) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,7 +178,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<a-empty v-else description="暂无素材" />
|
<a-empty
|
||||||
|
v-else
|
||||||
|
description="暂无素材"
|
||||||
|
style="padding: 48px 0;"
|
||||||
|
>
|
||||||
|
|
||||||
|
</a-empty>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -191,9 +221,6 @@
|
|||||||
<a-form-item label="分组名称" name="name" :rules="[{ required: true, message: '请输入分组名称' }]">
|
<a-form-item label="分组名称" name="name" :rules="[{ required: true, message: '请输入分组名称' }]">
|
||||||
<a-input v-model:value="createGroupForm.name" placeholder="请输入分组名称" />
|
<a-input v-model:value="createGroupForm.name" placeholder="请输入分组名称" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="分组分类">
|
|
||||||
<a-input v-model:value="createGroupForm.category" disabled />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="分组描述">
|
<a-form-item label="分组描述">
|
||||||
<a-textarea v-model:value="createGroupForm.description" placeholder="请输入分组描述(可选)" />
|
<a-textarea v-model:value="createGroupForm.description" placeholder="请输入分组描述(可选)" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -211,7 +238,7 @@ import {
|
|||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
PlusOutlined
|
PlusOutlined
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { message } from 'ant-design-vue';
|
import { message, Modal } from 'ant-design-vue';
|
||||||
import MaterialUploadModal from '@/components/material/MaterialUploadModal.vue';
|
import MaterialUploadModal from '@/components/material/MaterialUploadModal.vue';
|
||||||
import MaterialService, { MaterialGroupService } from '@/api/material';
|
import MaterialService, { MaterialGroupService } from '@/api/material';
|
||||||
import { useUpload } from '@/composables/useUpload';
|
import { useUpload } from '@/composables/useUpload';
|
||||||
@@ -240,6 +267,10 @@ const createGroupForm = reactive({
|
|||||||
// 选中的文件ID列表
|
// 选中的文件ID列表
|
||||||
const selectedFileIds = ref([]);
|
const selectedFileIds = ref([]);
|
||||||
|
|
||||||
|
// 编辑状态
|
||||||
|
const editingFileId = ref(null);
|
||||||
|
const editingDisplayName = ref('');
|
||||||
|
|
||||||
// 分页信息
|
// 分页信息
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
current: 1,
|
current: 1,
|
||||||
@@ -258,6 +289,10 @@ const handleCategoryChange = async (category) => {
|
|||||||
|
|
||||||
|
|
||||||
const handleSelectGroup = (group) => {
|
const handleSelectGroup = (group) => {
|
||||||
|
// 如果点击的是当前已选中的分组,则不执行任何操作
|
||||||
|
if (selectedGroupId.value === group.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
selectedGroupId.value = group.id;
|
selectedGroupId.value = group.id;
|
||||||
loadFileList();
|
loadFileList();
|
||||||
};
|
};
|
||||||
@@ -351,26 +386,20 @@ const handlePageSizeChange = (current, size) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFileClick = (file) => {
|
const handleFileClick = (file) => {
|
||||||
// 切换文件选中状态
|
const index = selectedFileIds.value.indexOf(file.id);
|
||||||
const isSelected = selectedFileIds.value.includes(file.id);
|
if (index > -1) {
|
||||||
if (isSelected) {
|
selectedFileIds.value.splice(index, 1);
|
||||||
// 如果已选中,则取消选中
|
|
||||||
selectedFileIds.value = selectedFileIds.value.filter(id => id !== file.id);
|
|
||||||
} else {
|
} else {
|
||||||
// 如果未选中,则添加到选中列表
|
selectedFileIds.value.push(file.id);
|
||||||
if (!selectedFileIds.value.includes(file.id)) {
|
|
||||||
selectedFileIds.value.push(file.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileSelectChange = (fileId, checked) => {
|
const handleFileSelectChange = (fileId, checked) => {
|
||||||
if (checked) {
|
const index = selectedFileIds.value.indexOf(fileId);
|
||||||
if (!selectedFileIds.value.includes(fileId)) {
|
if (checked && index === -1) {
|
||||||
selectedFileIds.value.push(fileId);
|
selectedFileIds.value.push(fileId);
|
||||||
}
|
} else if (!checked && index > -1) {
|
||||||
} else {
|
selectedFileIds.value.splice(index, 1);
|
||||||
selectedFileIds.value = selectedFileIds.value.filter(id => id !== fileId);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -408,17 +437,74 @@ const handleFileUpload = async (filesWithCover, category, groupId) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleBatchDelete = async () => {
|
const handleSelectAll = (event) => {
|
||||||
// TODO: 实现批量删除
|
const checked = event.target.checked;
|
||||||
message.info('批量删除功能待实现');
|
if (checked) {
|
||||||
|
// 全选:选中当前页所有文件
|
||||||
|
selectedFileIds.value = fileList.value.map(file => file.id);
|
||||||
|
} else {
|
||||||
|
// 取消全选:清空选中状态
|
||||||
|
selectedFileIds.value = [];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getContentTitle = () => {
|
const handleBatchDelete = async () => {
|
||||||
if (selectedGroupId.value === null) {
|
if (selectedFileIds.value.length === 0) {
|
||||||
return activeCategory.value === 'MIX' ? '混剪素材' : '数字人素材';
|
message.warning('请先选择要删除的文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认对话框
|
||||||
|
const count = selectedFileIds.value.length;
|
||||||
|
const fileIdsToDelete = [...selectedFileIds.value]; // 保存要删除的文件ID
|
||||||
|
const confirmed = await new Promise((resolve) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除选中的 ${count} 个文件吗?此操作不可恢复。`,
|
||||||
|
okText: '确认删除',
|
||||||
|
cancelText: '取消',
|
||||||
|
okType: 'danger',
|
||||||
|
centered: true,
|
||||||
|
class: 'batch-delete-modal',
|
||||||
|
onOk() {
|
||||||
|
resolve(true);
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
resolve(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
await MaterialService.deleteFiles(fileIdsToDelete);
|
||||||
|
|
||||||
|
// 从列表中移除已删除的文件
|
||||||
|
fileList.value = fileList.value.filter(file => !fileIdsToDelete.includes(file.id));
|
||||||
|
|
||||||
|
// 更新总数
|
||||||
|
totalFileCount.value = Math.max(0, totalFileCount.value - count);
|
||||||
|
|
||||||
|
// 清空选中状态
|
||||||
|
selectedFileIds.value = [];
|
||||||
|
|
||||||
|
// 如果删除后当前页没有数据了,且不是第一页,则加载上一页
|
||||||
|
if (fileList.value.length === 0 && pagination.current > 1) {
|
||||||
|
pagination.current = pagination.current - 1;
|
||||||
|
await loadFileList();
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success(`成功删除 ${count} 个文件`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量删除失败:', error);
|
||||||
|
message.error('删除失败,请重试');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
const group = groupList.value.find(g => g.id === selectedGroupId.value);
|
|
||||||
return group ? group.name : '素材列表';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatFileSize = (size) => {
|
const formatFileSize = (size) => {
|
||||||
@@ -432,6 +518,60 @@ const formatDate = (date) => {
|
|||||||
return new Date(date).toLocaleDateString();
|
return new Date(date).toLocaleDateString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFileTypeText = (fileName) => {
|
||||||
|
if (!fileName) return '';
|
||||||
|
// 提取文件后缀,如 .mp3、.mp4、.avi 等
|
||||||
|
const ext = fileName.split('.').pop();
|
||||||
|
return ext ? `${ext.toLowerCase()}` : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditName = (file) => {
|
||||||
|
editingFileId.value = file.id;
|
||||||
|
editingDisplayName.value = file.displayName || file.fileName;
|
||||||
|
// 延迟聚焦,确保输入框已渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
const input = document.querySelector('.ant-input:focus');
|
||||||
|
if (input) {
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveName = async (file) => {
|
||||||
|
if (!editingDisplayName.value.trim()) {
|
||||||
|
message.warning('名称不能为空');
|
||||||
|
editingFileId.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingDisplayName.value === file.displayName) {
|
||||||
|
editingFileId.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await MaterialService.updateDisplayName(file.id, editingDisplayName.value.trim());
|
||||||
|
file.displayName = editingDisplayName.value.trim();
|
||||||
|
message.success('重命名成功');
|
||||||
|
|
||||||
|
// 添加成功动画效果
|
||||||
|
const nameElement = document.querySelector(`[data-file-id="${file.id}"] .material-item__name`);
|
||||||
|
if (nameElement) {
|
||||||
|
nameElement.style.transition = 'all 0.3s ease';
|
||||||
|
nameElement.style.transform = 'scale(1.05)';
|
||||||
|
setTimeout(() => {
|
||||||
|
nameElement.style.transform = 'scale(1)';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重命名失败:', error);
|
||||||
|
message.error('重命名失败,请重试');
|
||||||
|
} finally {
|
||||||
|
editingFileId.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleImageError = (e) => {
|
const handleImageError = (e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
};
|
};
|
||||||
@@ -568,6 +708,11 @@ onMounted(() => {
|
|||||||
&--active {
|
&--active {
|
||||||
background: #e6f7ff;
|
background: #e6f7ff;
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
|
|
||||||
|
// 移除active状态下的hover效果
|
||||||
|
&:hover {
|
||||||
|
background: #e6f7ff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
@@ -606,129 +751,349 @@ onMounted(() => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #fff;
|
background: #F8FAFC;
|
||||||
margin: 16px;
|
margin: 0px 16px;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
&__header {
|
&__search {
|
||||||
|
padding: 20px 32px;
|
||||||
|
border-bottom: 1px solid #E2E8F0;
|
||||||
|
background: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 24px;
|
gap: 16px;
|
||||||
border-bottom: 1px solid #e8e8e8;
|
transition: all 0.2s ease;
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
align-items: center;
|
||||||
}
|
gap: 12px;
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
&__search {
|
.ant-checkbox {
|
||||||
padding: 16px 24px;
|
display: flex;
|
||||||
border-bottom: 1px solid #e8e8e8;
|
align-items: center;
|
||||||
display: flex;
|
gap: 6px;
|
||||||
gap: 8px;
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #F1F5F9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-checkbox__inner {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-checkbox-checked {
|
||||||
|
.ant-checkbox__inner {
|
||||||
|
background-color: #3B82F6;
|
||||||
|
border-color: #3B82F6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__list {
|
&__list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 24px;
|
padding: 32px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
background: #F8FAFC;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__pagination {
|
&__pagination {
|
||||||
padding: 16px 24px;
|
padding: 20px 32px;
|
||||||
border-top: 1px solid #e8e8e8;
|
border-top: 1px solid #E2E8F0;
|
||||||
|
background: #fff;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-grid {
|
.material-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
gap: 16px;
|
gap: 24px;
|
||||||
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-item {
|
.material-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid #e8e8e8;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border: 1px solid #E2E8F0;
|
||||||
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: #1890ff;
|
border-color: #3B82F6;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--selected {
|
&--selected {
|
||||||
border-color: #1890ff;
|
border-color: #3B82F6;
|
||||||
background: #e6f7ff;
|
background: linear-gradient(to bottom, #EFF6FF, #fff);
|
||||||
|
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.2);
|
||||||
|
|
||||||
|
.material-item__select {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
padding: 12px;
|
padding: 16px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__preview {
|
&__preview {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 120px;
|
height: 160px;
|
||||||
background: #f5f5f5;
|
background: linear-gradient(135deg, #F1F5F9 0%, #E2E8F0 100%);
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 12px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(59, 130, 246, 0);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::after {
|
||||||
|
background: rgba(59, 130, 246, 0.05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__preview-placeholder {
|
&__preview-placeholder {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
color: #d9d9d9;
|
color: #94A3B8;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #3B82F6;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__name {
|
&__name {
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
|
color: #1E293B;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
line-height: 1.4;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #3B82F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 30px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-color: #3B82F6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__meta {
|
&__meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 12px;
|
align-items: center;
|
||||||
color: #999;
|
font-size: 13px;
|
||||||
|
color: #64748B;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid #F1F5F9;
|
||||||
|
|
||||||
|
.material-item__type {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #3B82F6;
|
||||||
|
background: #EFF6FF;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item__size {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item__time {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 12px;
|
||||||
|
color: #94A3B8;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__select {
|
&__select {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 12px;
|
||||||
right: 8px;
|
right: 12px;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
padding: 4px;
|
padding: 6px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
|
||||||
|
.ant-checkbox {
|
||||||
|
&:hover .ant-checkbox__inner {
|
||||||
|
border-color: #3B82F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-checked .ant-checkbox__inner {
|
||||||
|
background-color: #3B82F6;
|
||||||
|
border-color: #3B82F6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 键盘导航支持
|
||||||
|
.material-item:focus-within {
|
||||||
|
outline: 2px solid #3B82F6;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.material-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-content__search {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.material-content__actions {
|
||||||
|
margin-left: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item {
|
||||||
|
&__preview {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.material-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-content {
|
||||||
|
margin: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&__search,
|
||||||
|
&__list,
|
||||||
|
&__pagination {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无障碍支持
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除确认弹窗样式
|
||||||
|
:deep(.batch-delete-modal) {
|
||||||
|
.ant-modal-body {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-footer {
|
||||||
|
text-align: right;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-top: 1px solid #e8e8e8;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,74 +0,0 @@
|
|||||||
-- 素材列表布局重构迁移脚本
|
|
||||||
-- Change ID: refactor-material-list-layout
|
|
||||||
-- Date: 2025-12-28
|
|
||||||
-- Description: 添加分组分类功能,支持混剪素材和数字人素材
|
|
||||||
|
|
||||||
-- =====================================
|
|
||||||
-- 1. 为tik_file_group表添加category字段
|
|
||||||
-- =====================================
|
|
||||||
|
|
||||||
ALTER TABLE tik_file_group
|
|
||||||
ADD COLUMN category VARCHAR(20) NOT NULL DEFAULT 'MIX'
|
|
||||||
COMMENT '分组分类: MIX(混剪素材), DIGITAL_HUMAN(数字人素材)'
|
|
||||||
AFTER description;
|
|
||||||
|
|
||||||
-- =====================================
|
|
||||||
-- 2. 创建索引优化查询性能
|
|
||||||
-- =====================================
|
|
||||||
|
|
||||||
CREATE INDEX idx_file_group_user_category
|
|
||||||
ON tik_file_group (user_id, category, deleted);
|
|
||||||
|
|
||||||
-- =====================================
|
|
||||||
-- 3. 为tik_user_file表的groupId字段添加注释
|
|
||||||
-- =====================================
|
|
||||||
|
|
||||||
ALTER TABLE tik_user_file
|
|
||||||
MODIFY COLUMN group_id BIGINT NULL
|
|
||||||
COMMENT '关联素材分组编号(关联tik_file_group.id)';
|
|
||||||
|
|
||||||
-- =====================================
|
|
||||||
-- 4. 迁移现有数据(将所有现有分组设置为MIX分类)
|
|
||||||
-- =====================================
|
|
||||||
|
|
||||||
UPDATE tik_file_group SET category = 'MIX' WHERE category IS NULL OR category = '';
|
|
||||||
|
|
||||||
-- =====================================
|
|
||||||
-- 5. 验证迁移结果
|
|
||||||
-- =====================================
|
|
||||||
|
|
||||||
SELECT category, COUNT(*) as group_count
|
|
||||||
FROM tik_file_group
|
|
||||||
WHERE deleted = FALSE
|
|
||||||
GROUP BY category;
|
|
||||||
|
|
||||||
-- =====================================
|
|
||||||
-- 6. 创建统计视图(可选,用于性能优化)
|
|
||||||
-- =====================================
|
|
||||||
|
|
||||||
CREATE OR REPLACE VIEW v_file_group_stats AS
|
|
||||||
SELECT
|
|
||||||
g.id,
|
|
||||||
g.user_id,
|
|
||||||
g.name,
|
|
||||||
g.category,
|
|
||||||
g.sort,
|
|
||||||
COUNT(f.id) as file_count
|
|
||||||
FROM tik_file_group g
|
|
||||||
LEFT JOIN tik_user_file f ON g.id = f.group_id AND f.deleted = FALSE
|
|
||||||
WHERE g.deleted = FALSE
|
|
||||||
GROUP BY g.id, g.user_id, g.name, g.category, g.sort;
|
|
||||||
|
|
||||||
-- =====================================
|
|
||||||
-- 7. 按分类统计文件数量
|
|
||||||
-- =====================================
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
g.category,
|
|
||||||
COUNT(DISTINCT g.id) as group_count,
|
|
||||||
COUNT(f.id) as file_count
|
|
||||||
FROM tik_file_group g
|
|
||||||
LEFT JOIN tik_user_file f ON g.id = f.group_id AND f.deleted = FALSE
|
|
||||||
WHERE g.deleted = FALSE
|
|
||||||
GROUP BY g.category
|
|
||||||
ORDER BY g.category;
|
|
||||||
@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
|||||||
import cn.iocoder.yudao.module.tik.file.service.TikUserFileService;
|
import cn.iocoder.yudao.module.tik.file.service.TikUserFileService;
|
||||||
import cn.iocoder.yudao.module.tik.file.vo.app.AppTikUserFilePageReqVO;
|
import cn.iocoder.yudao.module.tik.file.vo.app.AppTikUserFilePageReqVO;
|
||||||
import cn.iocoder.yudao.module.tik.file.vo.app.AppTikUserFileRespVO;
|
import cn.iocoder.yudao.module.tik.file.vo.app.AppTikUserFileRespVO;
|
||||||
|
import cn.iocoder.yudao.module.tik.file.vo.app.UpdateDisplayNameReqVO;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -113,5 +114,12 @@ public class AppTikUserFileController {
|
|||||||
return success(userFileService.completeUpload(request));
|
return success(userFileService.completeUpload(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/update-display-name")
|
||||||
|
@Operation(summary = "更新文件显示名称")
|
||||||
|
public CommonResult<Boolean> updateDisplayName(@RequestBody @Valid UpdateDisplayNameReqVO reqVO) {
|
||||||
|
userFileService.updateDisplayName(reqVO.getFileId(), reqVO.getDisplayName());
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,5 +83,9 @@ public class TikUserFileDO extends TenantBaseDO {
|
|||||||
* 视频时长(秒)
|
* 视频时长(秒)
|
||||||
*/
|
*/
|
||||||
private Integer duration;
|
private Integer duration;
|
||||||
|
/**
|
||||||
|
* 显示名称(用户可重命名,去除文件后缀)
|
||||||
|
*/
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,5 +94,13 @@ public interface TikUserFileService {
|
|||||||
*/
|
*/
|
||||||
Object completeUpload(Object request);
|
Object completeUpload(Object request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新文件显示名称
|
||||||
|
*
|
||||||
|
* @param fileId 文件编号
|
||||||
|
* @param displayName 新的显示名称
|
||||||
|
*/
|
||||||
|
void updateDisplayName(Long fileId, String displayName);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,17 +244,20 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
|||||||
throw exception(FILE_NOT_EXISTS, "文件记录保存失败:无法获取文件ID");
|
throw exception(FILE_NOT_EXISTS, "文件记录保存失败:无法获取文件ID");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理视频封面
|
// 处理视频封面和文件名
|
||||||
String fileName = file.getOriginalFilename();
|
String fileName = file.getOriginalFilename();
|
||||||
String fileType = file.getContentType();
|
String fileType = file.getContentType();
|
||||||
String coverUrl = handleCoverUpload(coverBase64, fileName, fileType, baseDirectory);
|
String coverUrl = handleCoverUpload(coverBase64, fileName, fileType, baseDirectory);
|
||||||
|
// 处理显示名称(去除文件后缀)
|
||||||
|
String displayName = FileUtil.mainName(fileName);
|
||||||
|
|
||||||
// 9. 创建文件记录(保存完整路径,包含封面URL和Base64)
|
// 9. 创建文件记录(保存完整路径,包含封面URL和Base64)
|
||||||
TikUserFileDO userFile = new TikUserFileDO()
|
TikUserFileDO userFile = new TikUserFileDO()
|
||||||
.setUserId(userId)
|
.setUserId(userId)
|
||||||
.setFileId(infraFileId) // 关联infra_file表,用于后续通过FileService管理文件
|
.setFileId(infraFileId) // 关联infra_file表,用于后续通过FileService管理文件
|
||||||
.setFileName(file.getOriginalFilename()) // 保存原始文件名,用于展示
|
.setFileName(fileName) // 保留原始文件名(系统标识)
|
||||||
.setFileType(file.getContentType())
|
.setDisplayName(displayName) // 设置显示名称(无后缀,用户可编辑)
|
||||||
|
.setFileType(fileType)
|
||||||
.setFileCategory(fileCategory)
|
.setFileCategory(fileCategory)
|
||||||
.setFileSize(file.getSize())
|
.setFileSize(file.getSize())
|
||||||
.setFileUrl(fileUrl)
|
.setFileUrl(fileUrl)
|
||||||
@@ -668,12 +671,15 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
|||||||
// 4. 处理视频封面
|
// 4. 处理视频封面
|
||||||
String baseDirectory = ossInitService.getOssDirectoryByCategory(userId, fileCategory);
|
String baseDirectory = ossInitService.getOssDirectoryByCategory(userId, fileCategory);
|
||||||
String coverUrl = handleCoverUpload(coverBase64, fileName, fileType, baseDirectory);
|
String coverUrl = handleCoverUpload(coverBase64, fileName, fileType, baseDirectory);
|
||||||
|
// 处理显示名称(去除文件后缀)
|
||||||
|
String displayName = FileUtil.mainName(fileName);
|
||||||
|
|
||||||
// 5. 保存到 tik_user_file 表
|
// 5. 保存到 tik_user_file 表
|
||||||
TikUserFileDO userFile = new TikUserFileDO()
|
TikUserFileDO userFile = new TikUserFileDO()
|
||||||
.setUserId(userId)
|
.setUserId(userId)
|
||||||
.setFileId(infraFileId)
|
.setFileId(infraFileId)
|
||||||
.setFileName(fileName)
|
.setFileName(fileName) // 保留原始文件名(系统标识)
|
||||||
|
.setDisplayName(displayName) // 设置显示名称(无后缀,用户可编辑)
|
||||||
.setFileType(fileType)
|
.setFileType(fileType)
|
||||||
.setFileCategory(fileCategory)
|
.setFileCategory(fileCategory)
|
||||||
.setFileSize(fileSize)
|
.setFileSize(fileSize)
|
||||||
@@ -699,5 +705,24 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDisplayName(Long fileId, String displayName) {
|
||||||
|
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
|
||||||
|
// 验证文件存在且属于当前用户
|
||||||
|
TikUserFileDO fileDO = userFileMapper.selectById(fileId);
|
||||||
|
if (fileDO == null || !fileDO.getUserId().equals(userId)) {
|
||||||
|
throw exception(FILE_NOT_EXISTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新显示名称
|
||||||
|
userFileMapper.updateById(new TikUserFileDO()
|
||||||
|
.setId(fileId)
|
||||||
|
.setDisplayName(displayName));
|
||||||
|
|
||||||
|
log.info("[updateDisplayName][用户({})更新文件({})显示名称成功,新名称({})]",
|
||||||
|
userId, fileId, displayName);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ public class AppTikUserFileRespVO {
|
|||||||
@Schema(description = "文件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "test.mp4")
|
@Schema(description = "文件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "test.mp4")
|
||||||
private String fileName;
|
private String fileName;
|
||||||
|
|
||||||
|
@Schema(description = "显示名称(用户可重命名,去除文件后缀)", example = "test")
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
@Schema(description = "文件类型(video/image/document等)", example = "video")
|
@Schema(description = "文件类型(video/image/document等)", example = "video")
|
||||||
private String fileType;
|
private String fileType;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package cn.iocoder.yudao.module.tik.file.vo.app;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新文件显示名称 Request VO
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Schema(description = "用户 App - 更新文件显示名称 Request VO")
|
||||||
|
@Data
|
||||||
|
public class UpdateDisplayNameReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "文件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
@NotNull(message = "文件编号不能为空")
|
||||||
|
private Long fileId;
|
||||||
|
|
||||||
|
@Schema(description = "显示名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我的视频")
|
||||||
|
@Size(max = 50, message = "显示名称长度不能超过50个字符")
|
||||||
|
@NotNull(message = "显示名称不能为空")
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user