fix: harden auto uploads and table layouts

This commit is contained in:
2026-09-19 10:25:10 +08:00
parent f0fb4db755
commit 8960b68ac8
15 changed files with 586 additions and 48 deletions
+3 -1
View File
@@ -2,6 +2,7 @@ import { defineConfig } from "@playwright/test";
const viewports = [
{ name: "desktop-1920", width: 1920, height: 1080 },
{ name: "desktop-1552", width: 1552, height: 761 },
{ name: "desktop-1366", width: 1366, height: 768 },
{ name: "tablet-1024", width: 1024, height: 768 },
{ name: "tablet-768", width: 768, height: 1024 },
@@ -21,7 +22,8 @@ export default defineConfig({
trace: "retain-on-failure"
},
webServer: {
command: "VITE_DISABLE_DEVTOOLS=1 npm run dev -- --host 127.0.0.1 --port 47173 --strictPort",
command: "npm run dev -- --host 127.0.0.1 --port 47173 --strictPort",
env: { VITE_DISABLE_DEVTOOLS: "1" },
url: "http://127.0.0.1:47173",
reuseExistingServer: false,
timeout: 120_000
+13 -8
View File
@@ -355,19 +355,24 @@ button { font-family: inherit; cursor: pointer; }
/* tables */
.el-table {
--el-table-border-color: transparent; --el-table-header-bg-color: transparent;
--el-table-bg-color: transparent; --el-table-row-hover-bg-color: var(--surface-hover);
--el-table-current-row-bg-color: var(--accent-soft); --el-fill-color-blank: transparent;
background: transparent;
--el-table-border-color: transparent; --el-table-header-bg-color: var(--surface-muted);
--el-table-bg-color: var(--surface); --el-table-tr-bg-color: var(--surface);
--el-table-row-hover-bg-color: var(--surface-hover);
--el-table-current-row-bg-color: var(--accent-soft); --el-fill-color-blank: var(--surface);
background: var(--surface);
}
.el-table::before, .el-table__inner-wrapper::before { display: none; }
.el-table th.el-table__cell { padding: 11px 16px; border-bottom: 1px solid var(--border-subtle); background: var(--surface-muted); }
html[data-theme="dark"] .el-table th.el-table__cell { background: var(--surface-muted); }
.el-table th.el-table__cell > .cell { color: var(--text-muted); font-size: 12px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase; }
.el-table td.el-table__cell { padding: 13px 16px; border-bottom: 1px solid var(--border-subtle); background: transparent; }
.el-table tr { background: transparent; }
.el-table tbody tr:nth-child(even) { background: rgba(15, 23, 42, 0.018); }
html[data-theme="dark"] .el-table tbody tr:nth-child(even) { background: rgba(255, 255, 255, 0.02); }
.el-table td.el-table__cell { padding: 13px 16px; border-bottom: 1px solid var(--border-subtle); background: var(--surface); }
.el-table tr { background: var(--surface); }
.el-table tbody tr:nth-child(even) > td.el-table__cell { background: color-mix(in srgb, var(--surface-strong) 24%, var(--surface)); }
.el-table tbody tr:hover > td.el-table__cell { background: var(--surface-hover); }
.el-table .el-table-fixed-column--right,
.el-table .el-table-fixed-column--left { background: inherit; }
.el-table .el-table-fixed-column--right.is-first-column { box-shadow: -10px 0 18px -16px rgba(23, 35, 58, .55); }
html[data-theme="dark"] .el-table .el-table-fixed-column--right.is-first-column { box-shadow: -10px 0 20px -15px rgba(0, 0, 0, .9); }
.premium-table { width: 100%; }
/* descriptions */
+9
View File
@@ -653,6 +653,15 @@ export interface OpenListConnectionTestResult {
success: boolean;
version?: string;
message: string;
sourcePath?: OpenListPathCheck;
destinationPath?: OpenListPathCheck;
}
export interface OpenListPathCheck {
path: string;
success: boolean;
canWrite: boolean;
message: string;
}
export interface OpenListDirectoryItem {
+72 -1
View File
@@ -53,6 +53,7 @@ const changingPassword = ref(false);
const testingEmail = ref(false);
const testingWebhook = ref(false);
const testingOpenList = ref(false);
const openListTestResult = ref<OpenListConnectionTestResult | null>(null);
const openListDirectoryLoading = ref(false);
const openListDirectoryDialogVisible = ref(false);
const openListDirectoryPickerTarget = ref<"source" | "destination">("source");
@@ -568,7 +569,9 @@ function buildOpenListConnectionPayload() {
return {
baseUrl: form.openListUpload.baseUrl,
username: form.openListUpload.username,
password: form.openListUpload.password
password: form.openListUpload.password,
sourcePath: form.openListUpload.sourcePath,
destinationPath: form.openListUpload.destinationPath
};
}
@@ -580,8 +583,10 @@ async function testOpenListConnection() {
"/settings/openlist/test",
buildOpenListConnectionPayload()
);
openListTestResult.value = data;
ElMessage[data.success ? "success" : "warning"](data.message);
} catch (error) {
openListTestResult.value = null;
ElMessage.error(getApiErrorMessage(error, "OpenList 连接测试失败"));
} finally {
testingOpenList.value = false;
@@ -1646,6 +1651,29 @@ onBeforeRouteLeave(async () => {
<el-button :loading="testingOpenList" @click="testOpenListConnection">测试连接</el-button>
</div>
<div
v-if="openListTestResult"
class="openlist-test-result"
:class="{ 'openlist-test-result--failed': !openListTestResult.success }"
>
<div class="openlist-test-result__summary">
<el-tag :type="openListTestResult.success ? 'success' : 'warning'">
{{ openListTestResult.success ? "配置可用" : "配置异常" }}
</el-tag>
<span>{{ openListTestResult.message }}</span>
</div>
<div v-if="openListTestResult.sourcePath" class="openlist-test-result__path">
<el-tag :type="openListTestResult.sourcePath.success ? 'success' : 'danger'" size="small">源目录</el-tag>
<code>{{ openListTestResult.sourcePath.path }}</code>
<span>{{ openListTestResult.sourcePath.message }}</span>
</div>
<div v-if="openListTestResult.destinationPath" class="openlist-test-result__path">
<el-tag :type="openListTestResult.destinationPath.success ? 'success' : 'danger'" size="small">目标目录</el-tag>
<code>{{ openListTestResult.destinationPath.path }}</code>
<span>{{ openListTestResult.destinationPath.message }}</span>
</div>
</div>
<el-form label-position="top">
<el-row :gutter="16">
<el-col :span="12">
@@ -1965,6 +1993,14 @@ onBeforeRouteLeave(async () => {
</el-button>
</div>
</div>
<el-alert
v-if="form.enableFileUpload && form.enableAutoUpload && form.enableSegmentCompletedScript"
class="event-script-conflict-alert"
type="warning"
:closable="false"
show-icon
title="内置自动上传与分片完成脚本会独立执行。请勿在该脚本中重复实现上传,以免重复提交或因脚本故障干扰完成事件。"
/>
<el-form-item v-if="form.segmentCompletedScriptMode === 'path'" label="脚本路径">
<el-input v-model="form.segmentCompletedScriptPath" placeholder="/app/scripts/segment-completed.sh" />
</el-form-item>
@@ -2828,6 +2864,41 @@ onBeforeRouteLeave(async () => {
color: var(--text-secondary);
}
.openlist-test-result {
display: grid;
gap: 10px;
margin-bottom: 16px;
padding: 12px 14px;
border: 1px solid color-mix(in srgb, var(--success) 30%, var(--border-subtle));
border-radius: var(--radius-sm);
background: var(--success-soft);
}
.openlist-test-result--failed {
border-color: color-mix(in srgb, var(--warning) 34%, var(--border-subtle));
background: var(--warning-soft);
}
.openlist-test-result__summary,
.openlist-test-result__path {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
color: var(--text-secondary);
font-size: 12px;
line-height: 1.55;
}
.openlist-test-result__path code {
color: var(--text-primary);
overflow-wrap: anywhere;
}
.event-script-conflict-alert {
margin-bottom: 16px;
}
.template-help {
margin-top: 8px;
padding: 16px 18px;
+54 -1
View File
@@ -377,6 +377,13 @@ test("live room table, drawer and dialog retain their final actions", async ({ p
await expect(actionHeader).toBeVisible();
const box = await actionHeader.boundingBox();
expect(box && box.x + box.width).toBeLessThanOrEqual((page.viewportSize()?.width ?? 0) + 1);
const fixedActionCell = page.locator("td.el-table-fixed-column--right").first();
await expect(fixedActionCell).toBeVisible();
const fixedBackground = await fixedActionCell.evaluate((element) =>
window.getComputedStyle(element).backgroundColor
);
expect(fixedBackground).not.toBe("transparent");
expect(fixedBackground).not.toMatch(/rgba\([^)]*,\s*0\s*\)$/);
}
await page.getByRole("button", { name: /^查看/ }).first().click();
@@ -677,7 +684,7 @@ test("recording failure recovery adapts its actions and long paths to each viewp
await expect(page.getByText(recordingFailure.filePath, { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "确认有效" })).toBeVisible();
if ((page.viewportSize()?.width ?? 0) <= 767) {
if ((page.viewportSize()?.width ?? 0) <= 768) {
await expect(page.locator(".failure-card")).toHaveCount(1);
await expect(page.getByRole("columnheader", { name: "操作" })).toHaveCount(0);
} else {
@@ -794,3 +801,49 @@ test("settings layer advanced controls without clipping the quick bar", async ({
await expectNoDocumentOverflow(page);
await capture(page, testInfo, "settings");
});
test("OpenList test distinguishes login success from a broken destination mount", async ({ page }) => {
await page.route("**/api/settings", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
enableFileUpload: true,
enableAutoUpload: true,
uploadTarget: 3,
openListUpload: {
baseUrl: "https://openlist.example.com",
username: "tester",
password: "secret",
basePath: "/archive",
sourcePath: "/source",
destinationPath: "/archive"
}
})
});
});
await page.route("**/api/settings/openlist/test", async (route) => {
const request = route.request().postDataJSON();
expect(request.sourcePath).toBe("/source");
expect(request.destinationPath).toBe("/archive");
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
success: false,
version: "4.1.10",
message: "OpenList 登录成功,但一个或多个配置目录不可用。",
sourcePath: { path: "/source", success: true, canWrite: false, message: "目录可访问。" },
destinationPath: { path: "/archive", success: false, canWrite: false, message: "provider timeout" }
})
});
});
await page.goto("/settings/upload");
await expect(page.getByRole("heading", { name: "OpenList 服务端复制" })).toBeVisible();
await page.getByRole("button", { name: "测试连接" }).click();
await expect(page.getByText("配置异常", { exact: true })).toBeVisible();
await expect(page.getByText("目录可访问。", { exact: true })).toBeVisible();
await expect(page.getByText("provider timeout", { exact: true })).toBeVisible();
await expectNoDocumentOverflow(page);
});