diff --git a/public/api/workspace.php b/public/api/workspace.php index bfc4247..9121828 100644 --- a/public/api/workspace.php +++ b/public/api/workspace.php @@ -218,7 +218,15 @@ function validateWorkspaceContent($content) { ]); } elseif ($action === 'list') { + // 从权限表获取文件 $fileIds = getUserAccessibleFiles($dbPermissions, $username); + + // 兼容旧数据:同时从用户文件列表获取文件 + $userFilesKey = sanitizeDbKey($username . '_files'); + $legacyFileIds = $dbUsers->get_array($userFilesKey); + if ($legacyFileIds && is_array($legacyFileIds)) { + $fileIds = array_unique(array_merge($fileIds ?: [], $legacyFileIds)); + } $list = []; if ($fileIds && is_array($fileIds)) { @@ -232,6 +240,15 @@ function validateWorkspaceContent($content) { $fileData = json_decode($fileRaw, true); if (!$fileData || !isset($fileData['metadata'])) continue; + // 兼容旧数据:自动迁移权限记录 + $permKey = sanitizeDbKey("perm_{$fileId}_{$username}"); + if ($dbPermissions->get($permKey) === null) { + // 检查文件作者是否是当前用户 + if (isset($fileData['metadata']['author']) && $fileData['metadata']['author'] === $username) { + grantFilePermission($dbPermissions, $fileId, $username, 'owner'); + } + } + $list[] = [ 'fileId' => $fileId, 'metadata' => $fileData['metadata'] @@ -258,11 +275,6 @@ function validateWorkspaceContent($content) { respond(['success' => false, 'message' => '文件ID格式无效']); } - // 检查读权限 - if (!hasFilePermission($dbPermissions, $fileId, $username, 'read')) { - respond(['success' => false, 'message' => '无权访问该文件'], 403); - } - // 消毒文件 ID $sanitizedFileId = sanitizeDbKey($fileId); @@ -276,6 +288,20 @@ function validateWorkspaceContent($content) { respond(['success' => false, 'message' => '文件格式损坏']); } + // 检查读权限(兼容旧数据:如果没有权限记录,检查文件作者) + $permKey = sanitizeDbKey("perm_{$fileId}_{$username}"); + if ($dbPermissions->get($permKey) === null) { + // 没有权限记录,检查文件作者 + if (isset($fileData['metadata']['author']) && $fileData['metadata']['author'] === $username) { + // 自动迁移:创建权限记录 + grantFilePermission($dbPermissions, $fileId, $username, 'owner'); + } else { + respond(['success' => false, 'message' => '无权访问该文件'], 403); + } + } elseif (!hasFilePermission($dbPermissions, $fileId, $username, 'read')) { + respond(['success' => false, 'message' => '无权访问该文件'], 403); + } + respond([ 'success' => true, 'data' => $fileData @@ -291,15 +317,26 @@ function validateWorkspaceContent($content) { respond(['success' => false, 'message' => '文件ID格式无效']); } - // 检查写权限 - if (!hasFilePermission($dbPermissions, $fileId, $username, 'write')) { - respond(['success' => false, 'message' => '无权删除该文件'], 403); - } - // 消毒文件 ID $sanitizedFileId = sanitizeDbKey($fileId); $fileRaw = $dbFiles->get($sanitizedFileId); + + // 检查写权限(兼容旧数据:如果没有权限记录,检查文件作者) + $permKey = sanitizeDbKey("perm_{$fileId}_{$username}"); + if ($dbPermissions->get($permKey) === null) { + // 没有权限记录,检查文件作者 + $fileData = $fileRaw ? json_decode($fileRaw, true) : null; + if ($fileData && isset($fileData['metadata']['author']) && $fileData['metadata']['author'] === $username) { + // 自动迁移:创建权限记录 + grantFilePermission($dbPermissions, $fileId, $username, 'owner'); + } else { + respond(['success' => false, 'message' => '无权删除该文件'], 403); + } + } elseif (!hasFilePermission($dbPermissions, $fileId, $username, 'write')) { + respond(['success' => false, 'message' => '无权删除该文件'], 403); + } + if ($fileRaw !== null) { $dbFiles->delete($sanitizedFileId); diff --git a/src/App.vue b/src/App.vue index 0863d16..aa252b1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -71,14 +71,14 @@ onMounted(async () => { if (isCtrl && e.key === 'z' && !e.shiftKey) { e.preventDefault() - if (canUndo()) { + if (canUndo.value) { undo() } } if (isCtrl && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) { e.preventDefault() - if (canRedo()) { + if (canRedo.value) { redo() } } diff --git a/src/assets/main.css b/src/assets/main.css index e53adc7..b1aba3d 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -131,11 +131,11 @@ --color-bg-hover: #2a3f5c; --color-bg-overlay: rgba(0, 0, 0, 0.7); - /* 文字颜色 - 提升禁用文字对比度 */ + /* 文字颜色 - 优化深色模式可读性 */ --color-text-primary: #f1f5f9; --color-text-secondary: #cbd5e1; - --color-text-muted: #94a3b8; - --color-text-disabled: #7b8fa3; + --color-text-muted: #a0aec0; + --color-text-disabled: #8899aa; --color-text-inverse: #ffffff; /* 边框颜色 - 提升可见度 */ @@ -143,7 +143,7 @@ --color-border-strong: #536578; --color-border-hover: #536578; --color-border-light: #253347; - --color-border-dark: #7b8fa3; + --color-border-dark: #8899aa; /* 次要背景色 */ --color-bg-secondary: #151d2e; @@ -250,15 +250,15 @@ --color-text-primary: #f1f5f9; --color-text-secondary: #cbd5e1; - --color-text-muted: #94a3b8; - --color-text-disabled: #7b8fa3; + --color-text-muted: #a0aec0; + --color-text-disabled: #8899aa; --color-text-inverse: #ffffff; --color-border: #3d4f63; --color-border-strong: #536578; --color-border-hover: #536578; --color-border-light: #253347; - --color-border-dark: #7b8fa3; + --color-border-dark: #8899aa; --color-bg-secondary: #151d2e; diff --git a/src/components/auth/LoginDialog.vue b/src/components/auth/LoginDialog.vue index 42cd5e8..6892fc0 100644 --- a/src/components/auth/LoginDialog.vue +++ b/src/components/auth/LoginDialog.vue @@ -247,7 +247,7 @@ const handleSubmit = async () => { left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.5); + background: var(--color-bg-overlay); display: flex; align-items: center; justify-content: center; diff --git a/src/components/auth/SyncSettingsDialog.vue b/src/components/auth/SyncSettingsDialog.vue index 33597f6..aea36c2 100644 --- a/src/components/auth/SyncSettingsDialog.vue +++ b/src/components/auth/SyncSettingsDialog.vue @@ -214,7 +214,7 @@ const handleSubmit = async () => { left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.5); + background: var(--color-bg-overlay); display: flex; align-items: center; justify-content: center; diff --git a/src/components/docs/RuleUsageGuide.vue b/src/components/docs/RuleUsageGuide.vue index ab4464d..e2262f7 100644 --- a/src/components/docs/RuleUsageGuide.vue +++ b/src/components/docs/RuleUsageGuide.vue @@ -305,7 +305,7 @@ padding: 10px 12px; background: var(--color-info-bg); border-radius: 8px; - border: 1px solid #bfdbfe; + border: 1px solid var(--color-border); font-size: 12px; color: var(--color-info); line-height: 1.5; diff --git a/src/components/layout/AppHeader.vue b/src/components/layout/AppHeader.vue index 1b5168d..df2440b 100644 --- a/src/components/layout/AppHeader.vue +++ b/src/components/layout/AppHeader.vue @@ -67,8 +67,6 @@ @@ -80,6 +78,7 @@ import { Cloud, FolderOpen, Github, LogIn, Monitor, Moon, Settings, Sun, User } import { useAuth } from '@/composables/useAuth' import { useCloudWorkspaceDialog } from '@/composables/useCloudWorkspaceDialog' import { useGlobalSettings } from '@/composables/useGlobalSettings' +import { useSettingsDialog } from '@/composables/useSettingsDialog' const UnifiedSettingsDialog = defineAsyncComponent(() => import('../settings/UnifiedSettingsDialog.vue')) @@ -88,9 +87,9 @@ const emit = defineEmits(['open-login']) const { currentUser, webdavConfig, isLoggedIn, logout, authType, initAuth } = useAuth() const { openCloudDialog } = useCloudWorkspaceDialog() const { settings, saveToLocalStorage, applyColorScheme, applyThemeColor } = useGlobalSettings() +const { visible: showUnifiedSettings, openSettings, closeSettings } = useSettingsDialog() const showDropdown = ref(false) -const showUnifiedSettings = ref(false) const menuContainer = ref(null) const hasRetiehe = computed(() => !!currentUser.value) @@ -122,7 +121,7 @@ const openWorkspaceManagement = () => { } const openUnifiedSettings = () => { - showUnifiedSettings.value = true + openSettings() showDropdown.value = false } @@ -148,7 +147,7 @@ onMounted(() => { onBeforeUnmount(() => { document.removeEventListener('click', closeDropdownOnOutsideClick) - showUnifiedSettings.value = false + closeSettings() }) @@ -162,7 +161,7 @@ onBeforeUnmount(() => { width: 100%; background: var(--color-primary); height: 100px; - color: white; + color: var(--color-text-inverse); padding: 0 30px; box-shadow: var(--shadow-md); gap: 20px; @@ -196,7 +195,7 @@ onBeforeUnmount(() => { background: rgba(255, 255, 255, 0.12); backdrop-filter: blur(8px); border: 1px solid rgba(255, 255, 255, 0.2); - color: white; + color: var(--color-text-inverse); padding: 8px 16px; border-radius: 24px; font-size: 14px; @@ -334,7 +333,7 @@ onBeforeUnmount(() => { .theme-btn.active { background: rgba(255, 255, 255, 0.2); - color: white; + color: var(--color-text-inverse); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); } diff --git a/src/components/layout/EditorPanel.vue b/src/components/layout/EditorPanel.vue index 361e546..3d0d0ec 100644 --- a/src/components/layout/EditorPanel.vue +++ b/src/components/layout/EditorPanel.vue @@ -104,7 +104,6 @@ const candidatePanelStyle = computed(() => { flex: 1; min-height: 0; background: var(--color-surface); - border-bottom: 2px solid var(--color-border); overflow: hidden; display: flex; flex-direction: column; diff --git a/src/components/layout/ExportPreview.vue b/src/components/layout/ExportPreview.vue index 9ed6d5f..a66714b 100644 --- a/src/components/layout/ExportPreview.vue +++ b/src/components/layout/ExportPreview.vue @@ -316,7 +316,7 @@