fix: unify client confirmations and inline rename UX
This commit is contained in:
@@ -172,6 +172,14 @@ function compareLooseVersion(left, right) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeReleaseNotes(rawValue) {
|
||||||
|
return String(rawValue || '')
|
||||||
|
.replace(/\\r\\n/g, '\n')
|
||||||
|
.replace(/\\n/g, '\n')
|
||||||
|
.replace(/\\r/g, '\n')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
function getDesktopUpdateConfig() {
|
function getDesktopUpdateConfig() {
|
||||||
const latestVersion = normalizeVersion(
|
const latestVersion = normalizeVersion(
|
||||||
SettingsDB.get('desktop_latest_version') || DEFAULT_DESKTOP_VERSION,
|
SettingsDB.get('desktop_latest_version') || DEFAULT_DESKTOP_VERSION,
|
||||||
@@ -183,11 +191,11 @@ function getDesktopUpdateConfig() {
|
|||||||
DEFAULT_DESKTOP_INSTALLER_URL ||
|
DEFAULT_DESKTOP_INSTALLER_URL ||
|
||||||
''
|
''
|
||||||
).trim();
|
).trim();
|
||||||
const releaseNotes = String(
|
const releaseNotes = normalizeReleaseNotes(
|
||||||
SettingsDB.get('desktop_release_notes') ||
|
SettingsDB.get('desktop_release_notes') ||
|
||||||
DEFAULT_DESKTOP_RELEASE_NOTES ||
|
DEFAULT_DESKTOP_RELEASE_NOTES ||
|
||||||
''
|
''
|
||||||
).trim();
|
);
|
||||||
const mandatory = SettingsDB.get('desktop_force_update') === 'true';
|
const mandatory = SettingsDB.get('desktop_force_update') === 'true';
|
||||||
return {
|
return {
|
||||||
latestVersion,
|
latestVersion,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { openPath, openUrl } from "@tauri-apps/plugin-opener";
|
import { openPath, openUrl } from "@tauri-apps/plugin-opener";
|
||||||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||||||
@@ -202,6 +202,22 @@ const fileDeleteDialog = reactive({
|
|||||||
loading: false,
|
loading: false,
|
||||||
file: null as FileItem | null,
|
file: null as FileItem | null,
|
||||||
});
|
});
|
||||||
|
const operationConfirmDialog = reactive({
|
||||||
|
visible: false,
|
||||||
|
loading: false,
|
||||||
|
mode: "" as "" | "kick-device" | "batch-delete",
|
||||||
|
title: "",
|
||||||
|
message: "",
|
||||||
|
confirmText: "确定",
|
||||||
|
sessionId: "",
|
||||||
|
batchItems: [] as FileItem[],
|
||||||
|
});
|
||||||
|
const inlineRename = reactive({
|
||||||
|
active: false,
|
||||||
|
originalName: "",
|
||||||
|
value: "",
|
||||||
|
saving: false,
|
||||||
|
});
|
||||||
const dropState = reactive({
|
const dropState = reactive({
|
||||||
active: false,
|
active: false,
|
||||||
uploading: false,
|
uploading: false,
|
||||||
@@ -803,6 +819,14 @@ async function initClientVersion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeReleaseNotesText(raw: string | undefined) {
|
||||||
|
return String(raw || "")
|
||||||
|
.replace(/\\r\\n/g, "\n")
|
||||||
|
.replace(/\\n/g, "\n")
|
||||||
|
.replace(/\\r/g, "\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
async function checkClientUpdate(showResultToast = true): Promise<boolean> {
|
async function checkClientUpdate(showResultToast = true): Promise<boolean> {
|
||||||
if (updateState.checking) {
|
if (updateState.checking) {
|
||||||
return false;
|
return false;
|
||||||
@@ -823,7 +847,7 @@ async function checkClientUpdate(showResultToast = true): Promise<boolean> {
|
|||||||
updateState.latestVersion = String(response.data.latestVersion || updateState.currentVersion);
|
updateState.latestVersion = String(response.data.latestVersion || updateState.currentVersion);
|
||||||
updateState.available = Boolean(response.data.updateAvailable);
|
updateState.available = Boolean(response.data.updateAvailable);
|
||||||
updateState.downloadUrl = String(response.data.downloadUrl || "");
|
updateState.downloadUrl = String(response.data.downloadUrl || "");
|
||||||
updateState.releaseNotes = String(response.data.releaseNotes || "");
|
updateState.releaseNotes = normalizeReleaseNotesText(String(response.data.releaseNotes || ""));
|
||||||
updateState.mandatory = Boolean(response.data.mandatory);
|
updateState.mandatory = Boolean(response.data.mandatory);
|
||||||
updateState.message = String(response.data.message || "");
|
updateState.message = String(response.data.message || "");
|
||||||
if (showResultToast) {
|
if (showResultToast) {
|
||||||
@@ -966,6 +990,36 @@ function formatOnlineDeviceType(value: string | undefined) {
|
|||||||
return "网页端";
|
return "网页端";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openOperationConfirmDialog(options: {
|
||||||
|
mode: "" | "kick-device" | "batch-delete";
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
confirmText?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
batchItems?: FileItem[];
|
||||||
|
}) {
|
||||||
|
if (operationConfirmDialog.loading) return;
|
||||||
|
operationConfirmDialog.mode = options.mode;
|
||||||
|
operationConfirmDialog.title = options.title;
|
||||||
|
operationConfirmDialog.message = options.message;
|
||||||
|
operationConfirmDialog.confirmText = String(options.confirmText || "确定");
|
||||||
|
operationConfirmDialog.sessionId = String(options.sessionId || "").trim();
|
||||||
|
operationConfirmDialog.batchItems = Array.isArray(options.batchItems) ? [...options.batchItems] : [];
|
||||||
|
operationConfirmDialog.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeOperationConfirmDialog(force = false) {
|
||||||
|
if (operationConfirmDialog.loading && !force) return;
|
||||||
|
operationConfirmDialog.visible = false;
|
||||||
|
operationConfirmDialog.loading = false;
|
||||||
|
operationConfirmDialog.mode = "";
|
||||||
|
operationConfirmDialog.title = "";
|
||||||
|
operationConfirmDialog.message = "";
|
||||||
|
operationConfirmDialog.confirmText = "确定";
|
||||||
|
operationConfirmDialog.sessionId = "";
|
||||||
|
operationConfirmDialog.batchItems = [];
|
||||||
|
}
|
||||||
|
|
||||||
async function loadOnlineDevices(silent = false) {
|
async function loadOnlineDevices(silent = false) {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
onlineDevices.loading = true;
|
onlineDevices.loading = true;
|
||||||
@@ -986,13 +1040,22 @@ async function loadOnlineDevices(silent = false) {
|
|||||||
onlineDevices.loading = false;
|
onlineDevices.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function kickOnlineDevice(item: OnlineDeviceItem) {
|
function requestKickOnlineDevice(item: OnlineDeviceItem) {
|
||||||
const sessionId = String(item?.session_id || "").trim();
|
const sessionId = String(item?.session_id || "").trim();
|
||||||
if (!sessionId || onlineDevices.kickingSessionId) return;
|
if (!sessionId || onlineDevices.kickingSessionId) return;
|
||||||
const tip = item?.is_current ? "确定要下线当前设备吗?下线后需要重新登录。" : "确定要强制该设备下线吗?";
|
const isCurrent = Boolean(item?.is_current || item?.is_local);
|
||||||
const confirmed = window.confirm(tip);
|
openOperationConfirmDialog({
|
||||||
if (!confirmed) return;
|
mode: "kick-device",
|
||||||
|
title: isCurrent ? "确认下线本机" : "确认踢下线设备",
|
||||||
|
message: isCurrent
|
||||||
|
? "确定要下线当前设备吗?下线后需要重新登录。"
|
||||||
|
: "确定要强制该设备下线吗?",
|
||||||
|
confirmText: isCurrent ? "确认下线" : "确认踢下线",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function kickOnlineDeviceBySessionId(sessionId: string) {
|
||||||
onlineDevices.kickingSessionId = sessionId;
|
onlineDevices.kickingSessionId = sessionId;
|
||||||
const response = await invokeBridge("api_kick_online_device", {
|
const response = await invokeBridge("api_kick_online_device", {
|
||||||
baseUrl: appConfig.baseUrl,
|
baseUrl: appConfig.baseUrl,
|
||||||
@@ -1213,6 +1276,7 @@ async function loadProfile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadFiles(targetPath = pathState.currentPath) {
|
async function loadFiles(targetPath = pathState.currentPath) {
|
||||||
|
cancelInlineRename(true);
|
||||||
pathState.loading = true;
|
pathState.loading = true;
|
||||||
pathState.error = "";
|
pathState.error = "";
|
||||||
const normalizedPath = normalizePath(targetPath);
|
const normalizedPath = normalizePath(targetPath);
|
||||||
@@ -1589,6 +1653,8 @@ async function handleLogout() {
|
|||||||
fileDeleteDialog.visible = false;
|
fileDeleteDialog.visible = false;
|
||||||
fileDeleteDialog.loading = false;
|
fileDeleteDialog.loading = false;
|
||||||
fileDeleteDialog.file = null;
|
fileDeleteDialog.file = null;
|
||||||
|
closeOperationConfirmDialog(true);
|
||||||
|
cancelInlineRename(true);
|
||||||
onlineDevices.loading = false;
|
onlineDevices.loading = false;
|
||||||
onlineDevices.kickingSessionId = "";
|
onlineDevices.kickingSessionId = "";
|
||||||
onlineDevices.items = [];
|
onlineDevices.items = [];
|
||||||
@@ -1615,28 +1681,80 @@ async function createFolder() {
|
|||||||
showToast(response.data?.message || "创建文件夹失败", "error");
|
showToast(response.data?.message || "创建文件夹失败", "error");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renameSelected(target?: FileItem | null) {
|
function isInlineRenaming(item: FileItem | null | undefined) {
|
||||||
|
if (!item) return false;
|
||||||
|
return inlineRename.active && inlineRename.originalName === item.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusInlineRenameInput() {
|
||||||
|
nextTick(() => {
|
||||||
|
const input = document.querySelector<HTMLInputElement>(".inline-rename-input[data-renaming='1']");
|
||||||
|
if (!input) return;
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelInlineRename(force = false) {
|
||||||
|
if (inlineRename.saving && !force) return;
|
||||||
|
inlineRename.active = false;
|
||||||
|
inlineRename.originalName = "";
|
||||||
|
inlineRename.value = "";
|
||||||
|
inlineRename.saving = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startInlineRename(target?: FileItem | null) {
|
||||||
const current = target || selectedFile.value;
|
const current = target || selectedFile.value;
|
||||||
if (!current) {
|
if (!current) {
|
||||||
showToast("请先选中文件或文件夹", "info");
|
showToast("请先选中文件或文件夹", "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextName = window.prompt("请输入新的名称", current.name);
|
selectedFileName.value = current.name;
|
||||||
if (!nextName || !nextName.trim() || nextName.trim() === current.name) return;
|
inlineRename.active = true;
|
||||||
|
inlineRename.originalName = current.name;
|
||||||
|
inlineRename.value = current.displayName || current.name;
|
||||||
|
inlineRename.saving = false;
|
||||||
|
focusInlineRenameInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitInlineRename(target?: FileItem | null) {
|
||||||
|
const current = target || files.value.find((item) => item.name === inlineRename.originalName) || null;
|
||||||
|
if (!current || !inlineRename.active || inlineRename.saving) return;
|
||||||
|
|
||||||
|
const nextName = String(inlineRename.value || "").trim();
|
||||||
|
if (!nextName) {
|
||||||
|
cancelInlineRename(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nextName === current.name) {
|
||||||
|
cancelInlineRename(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inlineRename.saving = true;
|
||||||
const response = await invokeBridge("api_rename_file", {
|
const response = await invokeBridge("api_rename_file", {
|
||||||
baseUrl: appConfig.baseUrl,
|
baseUrl: appConfig.baseUrl,
|
||||||
path: getItemParentPath(current),
|
path: getItemParentPath(current),
|
||||||
oldName: current.name,
|
oldName: current.name,
|
||||||
newName: nextName.trim(),
|
newName: nextName,
|
||||||
});
|
});
|
||||||
|
inlineRename.saving = false;
|
||||||
|
|
||||||
if (response.ok && response.data?.success) {
|
if (response.ok && response.data?.success) {
|
||||||
showToast("重命名成功", "success");
|
showToast("重命名成功", "success");
|
||||||
|
cancelInlineRename(true);
|
||||||
await loadFiles(pathState.currentPath);
|
await loadFiles(pathState.currentPath);
|
||||||
|
selectedFileName.value = nextName;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast(response.data?.message || "重命名失败", "error");
|
showToast(response.data?.message || "重命名失败", "error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function renameSelected(target?: FileItem | null) {
|
||||||
|
startInlineRename(target);
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteSelected(target?: FileItem | null, silent = false) {
|
async function deleteSelected(target?: FileItem | null, silent = false) {
|
||||||
const current = target || selectedFile.value;
|
const current = target || selectedFile.value;
|
||||||
if (!current) {
|
if (!current) {
|
||||||
@@ -1718,8 +1836,19 @@ function selectFile(item: FileItem) {
|
|||||||
selectedFileName.value = item.name;
|
selectedFileName.value = item.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleFileCardClick(item: FileItem) {
|
||||||
|
if (isInlineRenaming(item)) return;
|
||||||
|
selectFile(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFileCardDoubleClick(item: FileItem) {
|
||||||
|
if (isInlineRenaming(item)) return;
|
||||||
|
await openItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
async function openItem(item: FileItem) {
|
async function openItem(item: FileItem) {
|
||||||
if (batchMode.value) return;
|
if (batchMode.value) return;
|
||||||
|
if (isInlineRenaming(item)) return;
|
||||||
selectFile(item);
|
selectFile(item);
|
||||||
if (item.isDirectory || item.type === "directory") {
|
if (item.isDirectory || item.type === "directory") {
|
||||||
const nextPath = buildItemPath(item);
|
const nextPath = buildItemPath(item);
|
||||||
@@ -1743,6 +1872,9 @@ function closeContextMenu() {
|
|||||||
|
|
||||||
function openContextMenu(event: MouseEvent, item: FileItem) {
|
function openContextMenu(event: MouseEvent, item: FileItem) {
|
||||||
if (batchMode.value) return;
|
if (batchMode.value) return;
|
||||||
|
if (inlineRename.active && !isInlineRenaming(item)) {
|
||||||
|
cancelInlineRename(true);
|
||||||
|
}
|
||||||
selectFile(item);
|
selectFile(item);
|
||||||
contextMenu.item = item;
|
contextMenu.item = item;
|
||||||
const maxX = window.innerWidth - 220;
|
const maxX = window.innerWidth - 220;
|
||||||
@@ -1844,12 +1976,18 @@ async function batchDeleteSelected() {
|
|||||||
showToast("请先勾选批量文件", "info");
|
showToast("请先勾选批量文件", "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const confirmed = window.confirm(`确认批量删除 ${batchSelectedItems.value.length} 个项目吗?`);
|
openOperationConfirmDialog({
|
||||||
if (!confirmed) return;
|
mode: "batch-delete",
|
||||||
|
title: "确认批量删除",
|
||||||
|
message: `确认删除已勾选的 ${batchSelectedItems.value.length} 个项目吗?删除后将无法恢复。`,
|
||||||
|
confirmText: "确定删除",
|
||||||
|
batchItems: [...batchSelectedItems.value],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runBatchDelete(items: FileItem[]) {
|
||||||
let success = 0;
|
let success = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
const items = [...batchSelectedItems.value];
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
try {
|
try {
|
||||||
await deleteSelected(item, true);
|
await deleteSelected(item, true);
|
||||||
@@ -1865,6 +2003,31 @@ async function batchDeleteSelected() {
|
|||||||
showToast(`批量删除完成:成功 ${success} 个${failedText}`, failed > 0 ? "info" : "success");
|
showToast(`批量删除完成:成功 ${success} 个${failedText}`, failed > 0 ? "info" : "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function confirmOperationDialog() {
|
||||||
|
if (operationConfirmDialog.loading) return;
|
||||||
|
operationConfirmDialog.loading = true;
|
||||||
|
try {
|
||||||
|
if (operationConfirmDialog.mode === "kick-device") {
|
||||||
|
const sessionId = operationConfirmDialog.sessionId;
|
||||||
|
if (sessionId) {
|
||||||
|
await kickOnlineDeviceBySessionId(sessionId);
|
||||||
|
}
|
||||||
|
closeOperationConfirmDialog(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operationConfirmDialog.mode === "batch-delete") {
|
||||||
|
await runBatchDelete([...operationConfirmDialog.batchItems]);
|
||||||
|
closeOperationConfirmDialog(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeOperationConfirmDialog(true);
|
||||||
|
} catch {
|
||||||
|
operationConfirmDialog.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function batchShareSelected() {
|
async function batchShareSelected() {
|
||||||
if (!batchMode.value || batchSelectedItems.value.length === 0) {
|
if (!batchMode.value || batchSelectedItems.value.length === 0) {
|
||||||
showToast("请先勾选批量文件", "info");
|
showToast("请先勾选批量文件", "info");
|
||||||
@@ -1914,8 +2077,30 @@ function handleGlobalClick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleGlobalKey(event: KeyboardEvent) {
|
function handleGlobalKey(event: KeyboardEvent) {
|
||||||
if (event.key === "Escape" && contextMenu.visible) {
|
if (event.key === "Escape") {
|
||||||
closeContextMenu();
|
if (operationConfirmDialog.visible) {
|
||||||
|
closeOperationConfirmDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (inlineRename.active) {
|
||||||
|
cancelInlineRename();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (contextMenu.visible) {
|
||||||
|
closeContextMenu();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.key === "F2"
|
||||||
|
&& authenticated.value
|
||||||
|
&& nav.value === "files"
|
||||||
|
&& !batchMode.value
|
||||||
|
&& !inlineRename.active
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
startInlineRename();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2079,6 +2264,9 @@ async function registerNativeUploadProgressListener() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(nav, async (next) => {
|
watch(nav, async (next) => {
|
||||||
|
if (next !== "files" && inlineRename.active) {
|
||||||
|
cancelInlineRename(true);
|
||||||
|
}
|
||||||
if (next === "shares" && authenticated.value) {
|
if (next === "shares" && authenticated.value) {
|
||||||
await loadShares();
|
await loadShares();
|
||||||
return;
|
return;
|
||||||
@@ -2297,24 +2485,44 @@ onBeforeUnmount(() => {
|
|||||||
<div v-else-if="pathState.error" class="empty-tip error">{{ pathState.error }}</div>
|
<div v-else-if="pathState.error" class="empty-tip error">{{ pathState.error }}</div>
|
||||||
<div v-else-if="filteredFiles.length === 0" class="empty-tip">当前目录暂无文件</div>
|
<div v-else-if="filteredFiles.length === 0" class="empty-tip">当前目录暂无文件</div>
|
||||||
<div v-else class="icon-grid">
|
<div v-else class="icon-grid">
|
||||||
<button
|
<div
|
||||||
v-for="item in filteredFiles"
|
v-for="item in filteredFiles"
|
||||||
:key="item.name"
|
:key="item.name"
|
||||||
type="button"
|
|
||||||
class="file-card"
|
class="file-card"
|
||||||
:class="{ selected: selectedFileName === item.name, batchSelected: isBatchSelected(item.name) }"
|
:class="{ selected: selectedFileName === item.name, batchSelected: isBatchSelected(item.name), renaming: isInlineRenaming(item) }"
|
||||||
@click="selectFile(item)"
|
role="button"
|
||||||
@dblclick="openItem(item)"
|
tabindex="0"
|
||||||
|
@click="handleFileCardClick(item)"
|
||||||
|
@dblclick="handleFileCardDoubleClick(item)"
|
||||||
|
@keydown.enter.prevent="handleFileCardDoubleClick(item)"
|
||||||
|
@keydown.space.prevent="handleFileCardClick(item)"
|
||||||
@contextmenu.prevent="openContextMenu($event, item)"
|
@contextmenu.prevent="openContextMenu($event, item)"
|
||||||
>
|
>
|
||||||
<div v-if="batchMode" class="batch-check" :class="{ active: isBatchSelected(item.name) }">
|
<div v-if="batchMode" class="batch-check" :class="{ active: isBatchSelected(item.name) }">
|
||||||
{{ isBatchSelected(item.name) ? "✓" : "" }}
|
{{ isBatchSelected(item.name) ? "✓" : "" }}
|
||||||
</div>
|
</div>
|
||||||
<div class="file-icon-glyph">{{ fileIcon(item) }}</div>
|
<div class="file-icon-glyph">{{ fileIcon(item) }}</div>
|
||||||
<div class="file-name" :title="item.displayName || item.name">{{ item.displayName || item.name }}</div>
|
<div class="file-name" :title="item.displayName || item.name">
|
||||||
|
<template v-if="isInlineRenaming(item)">
|
||||||
|
<input
|
||||||
|
v-model="inlineRename.value"
|
||||||
|
class="inline-rename-input"
|
||||||
|
data-renaming="1"
|
||||||
|
:disabled="inlineRename.saving"
|
||||||
|
@click.stop
|
||||||
|
@dblclick.stop
|
||||||
|
@keydown.enter.prevent="submitInlineRename(item)"
|
||||||
|
@keydown.esc.prevent="cancelInlineRename()"
|
||||||
|
@blur="cancelInlineRename()"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ item.displayName || item.name }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<div class="file-meta">{{ fileTypeLabel(item) }} · {{ item.isDirectory ? "-" : (item.sizeFormatted || formatBytes(item.size)) }}</div>
|
<div class="file-meta">{{ fileTypeLabel(item) }} · {{ item.isDirectory ? "-" : (item.sizeFormatted || formatBytes(item.size)) }}</div>
|
||||||
<div class="file-time">{{ formatDate(item.modifiedAt) }}</div>
|
<div class="file-time">{{ formatDate(item.modifiedAt) }}</div>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="dropState.active" class="drop-overlay">
|
<div v-if="dropState.active" class="drop-overlay">
|
||||||
<div class="drop-overlay-card">
|
<div class="drop-overlay-card">
|
||||||
@@ -2512,8 +2720,8 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="action-btn danger"
|
class="action-btn danger"
|
||||||
:disabled="onlineDevices.kickingSessionId === item.session_id"
|
:disabled="onlineDevices.kickingSessionId === item.session_id || operationConfirmDialog.loading"
|
||||||
@click="kickOnlineDevice(item)"
|
@click="requestKickOnlineDevice(item)"
|
||||||
>
|
>
|
||||||
{{ onlineDevices.kickingSessionId === item.session_id ? "处理中..." : (item.is_current || item.is_local ? "下线本机" : "踢下线") }}
|
{{ onlineDevices.kickingSessionId === item.session_id ? "处理中..." : (item.is_current || item.is_local ? "下线本机" : "踢下线") }}
|
||||||
</button>
|
</button>
|
||||||
@@ -2689,6 +2897,19 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="operationConfirmDialog.visible" class="confirm-mask" @click="closeOperationConfirmDialog()">
|
||||||
|
<div class="confirm-card" @click.stop>
|
||||||
|
<h4>{{ operationConfirmDialog.title || "确认操作" }}</h4>
|
||||||
|
<p>{{ operationConfirmDialog.message || "确认继续执行该操作吗?" }}</p>
|
||||||
|
<div class="confirm-actions">
|
||||||
|
<button class="action-btn" :disabled="operationConfirmDialog.loading" @click="closeOperationConfirmDialog()">取消</button>
|
||||||
|
<button class="action-btn danger" :disabled="operationConfirmDialog.loading" @click="confirmOperationDialog()">
|
||||||
|
{{ operationConfirmDialog.loading ? "处理中..." : (operationConfirmDialog.confirmText || "确定") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="updatePrompt.visible" class="confirm-mask" @click="dismissUpdatePrompt()">
|
<div v-if="updatePrompt.visible" class="confirm-mask" @click="dismissUpdatePrompt()">
|
||||||
<div class="confirm-card" @click.stop>
|
<div class="confirm-card" @click.stop>
|
||||||
<h4>发现新版本 v{{ updateState.latestVersion || "-" }}</h4>
|
<h4>发现新版本 v{{ updateState.latestVersion || "-" }}</h4>
|
||||||
@@ -3265,6 +3486,11 @@ select:focus {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-card:focus-visible {
|
||||||
|
outline: 2px solid #79a8f0;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.file-card:hover {
|
.file-card:hover {
|
||||||
border-color: #c6d7ee;
|
border-color: #c6d7ee;
|
||||||
box-shadow: 0 6px 12px rgba(34, 72, 116, 0.08);
|
box-shadow: 0 6px 12px rgba(34, 72, 116, 0.08);
|
||||||
@@ -3281,6 +3507,11 @@ select:focus {
|
|||||||
box-shadow: 0 0 0 2px rgba(63, 132, 236, 0.14);
|
box-shadow: 0 0 0 2px rgba(63, 132, 236, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-card.renaming {
|
||||||
|
border-color: #7baaf8;
|
||||||
|
box-shadow: 0 0 0 2px rgba(29, 111, 255, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
.batch-check {
|
.batch-check {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
@@ -3328,6 +3559,24 @@ select:focus {
|
|||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-rename-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid #7baaf8;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2b3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-rename-input:focus {
|
||||||
|
outline: 0;
|
||||||
|
border-color: #3f84ec;
|
||||||
|
box-shadow: 0 0 0 2px rgba(63, 132, 236, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
.file-meta,
|
.file-meta,
|
||||||
.file-time {
|
.file-time {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
Reference in New Issue
Block a user