feat: 实现文件管理页面及其配套的 WebSocket 服务。
This commit is contained in:
parent
ec7c5a1e23
commit
3137928d0d
@ -31,6 +31,10 @@ export default {
|
||||
</button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<button class="toolbar-btn" @click="openConfigModal" title="扫描配置">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>配置</span>
|
||||
</button>
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search"></i>
|
||||
<input
|
||||
@ -124,6 +128,86 @@ export default {
|
||||
<span class="path-info">当前路径: {{ currentPath }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 配置弹窗 -->
|
||||
<div v-if="showConfigModal" class="modal-overlay" @click.self="closeConfigModal">
|
||||
<div class="modal-content config-modal-size">
|
||||
<div class="modal-header">
|
||||
<h3>文件扫描配置</h3>
|
||||
<button class="close-btn" @click="closeConfigModal">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body config-body">
|
||||
<!-- 路径设置 -->
|
||||
<section class="config-section">
|
||||
<h4>扫描目录</h4>
|
||||
<div class="form-group">
|
||||
<label>基础路径 (Base Path)</label>
|
||||
<input type="text" v-model="configBasePath" class="form-input" placeholder="例如: D:\CAD_Files">
|
||||
<p class="help-text">修改后将重新扫描该目录下的所有文件</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 扩展名设置 -->
|
||||
<section class="config-section">
|
||||
<div class="section-header">
|
||||
<h4>文件类型扩展名</h4>
|
||||
<div class="add-category-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
v-model="newCategoryName"
|
||||
placeholder="新增分类 (如: excel)"
|
||||
class="small-input"
|
||||
@keydown.enter.prevent="addNewCategory"
|
||||
>
|
||||
<button class="btn-small" @click="addNewCategory" :disabled="!newCategoryName">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="extensions-container">
|
||||
<div v-if="Object.keys(configExtensions).length === 0" class="empty-tip">
|
||||
暂无配置,请添加分类
|
||||
</div>
|
||||
<div v-for="(exts, category) in configExtensions" :key="category" class="extension-group">
|
||||
<div class="group-header">
|
||||
<span class="category-badge">{{ category }}</span>
|
||||
<button class="btn-icon-danger" @click="removeCategory(category)" title="删除分类">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tags-wrapper">
|
||||
<span v-for="(ext, idx) in exts" :key="idx" class="ext-tag">
|
||||
{{ ext }}
|
||||
<i class="fas fa-times" @click="removeExtension(category, idx)"></i>
|
||||
</span>
|
||||
<div class="add-tag-box">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="+ .ext (回车)"
|
||||
@keydown.enter.prevent="addExtension(category, $event)"
|
||||
@blur="addExtension(category, $event)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isConfigLoading" class="config-loading-overlay">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>读取配置中...</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-cancel" @click="closeConfigModal">取消</button>
|
||||
<button class="btn-confirm" @click="saveConfig">
|
||||
<i class="fas fa-save"></i> 保存配置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 重命名弹窗 -->
|
||||
<div v-if="showRenameModal" class="modal-overlay" @click.self="closeRenameModal">
|
||||
<div class="modal-content">
|
||||
@ -274,9 +358,17 @@ const renameParams = ref({
|
||||
case_sensitive: true,
|
||||
format: '%Y%m%d',
|
||||
position: 'suffix',
|
||||
position: 'suffix',
|
||||
case_type: 'lower'
|
||||
})
|
||||
|
||||
// 配置相关状态
|
||||
const showConfigModal = ref(false)
|
||||
const configBasePath = ref('')
|
||||
const configExtensions = ref({})
|
||||
const newCategoryName = ref('')
|
||||
const isConfigLoading = ref(false)
|
||||
|
||||
// 策略定义
|
||||
const strategies = ref([
|
||||
{ name: 'add_prefix', title: '添加前缀', description: '在文件名前添加字符' },
|
||||
@ -426,9 +518,13 @@ onMounted(() => {
|
||||
websocketService.on('onRenameStrategies', handleRenameStrategies)
|
||||
websocketService.on('onRenamePreview', handleRenamePreview)
|
||||
websocketService.on('onRenameResult', handleRenameResult)
|
||||
websocketService.on('onFileConfigUpdate', handleFileConfigUpdate)
|
||||
|
||||
// 初始获取
|
||||
refreshFileList()
|
||||
|
||||
// 获取初始配置
|
||||
websocketService.getFileConfig()
|
||||
websocketService.getRenameStrategies()
|
||||
})
|
||||
|
||||
@ -439,6 +535,7 @@ onUnmounted(() => {
|
||||
websocketService.off('onRenameStrategies', handleRenameStrategies)
|
||||
websocketService.off('onRenamePreview', handleRenamePreview)
|
||||
websocketService.off('onRenameResult', handleRenameResult)
|
||||
websocketService.off('onFileConfigUpdate', handleFileConfigUpdate)
|
||||
})
|
||||
|
||||
// 获取文件图标
|
||||
@ -525,6 +622,122 @@ const applyRename = () => {
|
||||
)
|
||||
}
|
||||
|
||||
// 处理配置更新
|
||||
const handleFileConfigUpdate = (data) => {
|
||||
isConfigLoading.value = false
|
||||
if (data.base_path) {
|
||||
configBasePath.value = data.base_path
|
||||
// 如果是设置成功后的回传,更新当前路径
|
||||
if (showConfigModal.value === false) {
|
||||
currentPath.value = data.base_path
|
||||
}
|
||||
}
|
||||
if (data.file_extensions) {
|
||||
configExtensions.value = JSON.parse(JSON.stringify(data.file_extensions))
|
||||
}
|
||||
}
|
||||
|
||||
// 配置模态框方法
|
||||
const openConfigModal = () => {
|
||||
isConfigLoading.value = true
|
||||
showConfigModal.value = true
|
||||
|
||||
// 尝试发送请求
|
||||
const sent = websocketService.getFileConfig()
|
||||
|
||||
if (!sent) {
|
||||
// 发送失败(未连接),直接结束加载状态
|
||||
isConfigLoading.value = false
|
||||
// 但我们仍然显示弹窗,允许用户看(虽然可能是空的)或者手动重试?
|
||||
// 更好的是提示用户
|
||||
// 这里简单处理:让它保持打开,但 loading 结束
|
||||
} else {
|
||||
// 设置超时保护 (3秒后如果还没响应,就停止loading)
|
||||
setTimeout(() => {
|
||||
if (isConfigLoading.value) {
|
||||
isConfigLoading.value = false
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
const closeConfigModal = () => {
|
||||
showConfigModal.value = false
|
||||
isConfigLoading.value = false
|
||||
}
|
||||
|
||||
const removeExtension = (category, index) => {
|
||||
if (configExtensions.value[category]) {
|
||||
configExtensions.value[category].splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const removeCategory = (category) => {
|
||||
if (confirm(`确定要删除 "${category}" 分类及其所有后缀吗?`)) {
|
||||
delete configExtensions.value[category]
|
||||
}
|
||||
}
|
||||
|
||||
const addNewCategory = () => {
|
||||
const name = newCategoryName.value.trim()
|
||||
if (!name) return
|
||||
|
||||
if (configExtensions.value[name]) {
|
||||
alert('该分类已存在')
|
||||
return
|
||||
}
|
||||
|
||||
// Use standard reactivity assignment for new property
|
||||
configExtensions.value = {
|
||||
...configExtensions.value,
|
||||
[name]: []
|
||||
}
|
||||
newCategoryName.value = ''
|
||||
}
|
||||
|
||||
const addExtension = (category, event) => {
|
||||
const val = event.target.value.trim()
|
||||
if (!val) return
|
||||
|
||||
// Ensure starts with .
|
||||
const ext = val.startsWith('.') ? val : `.${val}`
|
||||
|
||||
if (!configExtensions.value[category]) {
|
||||
configExtensions.value[category] = []
|
||||
}
|
||||
|
||||
if (!configExtensions.value[category].includes(ext)) {
|
||||
configExtensions.value[category].push(ext)
|
||||
}
|
||||
|
||||
event.target.value = '' // clear input
|
||||
}
|
||||
|
||||
const saveConfig = () => {
|
||||
// 验证: 检查是否有分类但没有后缀的情况
|
||||
for (const [category, exts] of Object.entries(configExtensions.value)) {
|
||||
if (!exts || exts.length === 0) {
|
||||
alert(`分类 "${category}" 下没有添加任何扩展名 (如 .md, .txt),将无法扫描到文件。请先添加扩展名。`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 保存路径
|
||||
if (configBasePath.value) {
|
||||
websocketService.setBasePath(configBasePath.value)
|
||||
}
|
||||
|
||||
// 保存扩展名
|
||||
websocketService.setFileExtensions(configExtensions.value)
|
||||
|
||||
closeConfigModal()
|
||||
|
||||
// 延迟刷新列表 (给后端一点时间保存和重新扫描)
|
||||
setTimeout(() => {
|
||||
refreshFileList()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -572,6 +785,23 @@ const applyRename = () => {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.config-loading-overlay {
|
||||
position: absolute;
|
||||
top: 60px; /* Skip header */
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 60px; /* Skip footer */
|
||||
background: rgba(var(--color-bg-primary-rgb), 0.7);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(2px);
|
||||
gap: var(--spacing-md);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.toolbar-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
@ -1006,4 +1236,179 @@ const applyRename = () => {
|
||||
box-shadow: 0 4px 15px var(--color-primary-rgb-3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Config Modal Styles */
|
||||
.config-modal-size {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.config-body {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
|
||||
.config-section h4 {
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
border-bottom: 2px solid var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.add-category-wrapper {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.small-input {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 12px;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 2px 6px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: 4px;
|
||||
color: var(--color-success);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-small:hover {
|
||||
border-color: var(--color-success);
|
||||
background: rgba(var(--color-success-rgb), 0.1);
|
||||
}
|
||||
|
||||
.btn-icon-danger {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--color-text-tertiary);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
padding: 4px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.btn-icon-danger:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.extensions-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: var(--color-text-tertiary);
|
||||
padding: var(--spacing-lg);
|
||||
font-size: var(--font-size-sm);
|
||||
border: 1px dashed var(--color-border-secondary);
|
||||
border-radius: var(--size-border-radius);
|
||||
}
|
||||
|
||||
.extension-group {
|
||||
background: var(--color-bg-secondary);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--size-border-radius);
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
}
|
||||
|
||||
.group-header {
|
||||
margin-bottom: var(--spacing-sm);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tags-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ext-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.ext-tag:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.ext-tag i {
|
||||
cursor: pointer;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: 11px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.ext-tag i:hover {
|
||||
color: var(--color-danger, #ef4444);
|
||||
}
|
||||
|
||||
.add-tag-box input {
|
||||
width: 60px;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--color-border-primary);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.add-tag-box input:focus {
|
||||
width: 80px;
|
||||
border-color: var(--color-primary);
|
||||
border-style: solid;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -34,7 +34,8 @@ class WebSocketService {
|
||||
onDownloadUrl: [],
|
||||
onRenameStrategies: [],
|
||||
onRenamePreview: [],
|
||||
onRenameResult: []
|
||||
onRenameResult: [],
|
||||
onFileConfigUpdate: []
|
||||
}
|
||||
|
||||
}
|
||||
@ -214,6 +215,11 @@ class WebSocketService {
|
||||
if (message.data.results) {
|
||||
this.emit('onRenameResult', message.data)
|
||||
}
|
||||
|
||||
// 文件配置相关消息 (使用 in 运算符检查属性存在性,避免空字符串/空对象判定为假)
|
||||
if ('base_path' in message.data || 'file_extensions' in message.data) {
|
||||
this.emit('onFileConfigUpdate', message.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -401,6 +407,29 @@ class WebSocketService {
|
||||
})
|
||||
}
|
||||
|
||||
// 设置基础扫描路径
|
||||
setBasePath(path) {
|
||||
return this.send({
|
||||
type: 'set_base_path',
|
||||
base_path: path
|
||||
})
|
||||
}
|
||||
|
||||
// 设置文件扩展名配置
|
||||
setFileExtensions(extensions) {
|
||||
return this.send({
|
||||
type: 'set_file_extensions',
|
||||
file_extensions: extensions
|
||||
})
|
||||
}
|
||||
|
||||
// 获取文件配置
|
||||
getFileConfig() {
|
||||
return this.send({
|
||||
type: 'get_file_config'
|
||||
})
|
||||
}
|
||||
|
||||
// 心跳检测
|
||||
startHeartbeat() {
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user