feat: 实现文件管理页面及其配套的 WebSocket 服务。

This commit is contained in:
sladro 2026-02-04 15:18:21 +08:00
parent ec7c5a1e23
commit 3137928d0d
2 changed files with 435 additions and 1 deletions

View File

@ -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 {
// (3loading)
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>

View File

@ -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(() => {