diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index c8703ae1..e3fc2527 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -66,9 +66,9 @@ jobs: # main push:只构 amd64(快)。tag(v*)release 才构 multi-arch 兼容 ARM 服务器。 platforms: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64' || 'linux/amd64' }} push: true - # build:fast = 只跑 vite build(typecheck 已由 pr-gate / full-ci-gate 单独门禁过) + # 与 Dockerfile 默认值一致:镜像构建同时执行 typecheck、i18n 检查和 Vite build。 build-args: | - BUILD_MODE=build:fast + BUILD_MODE=build tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/Dockerfile b/Dockerfile index f3269c2f..d1e2a5ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ COPY . . # build:fast = vite build 不跑 vue-tsc(CI 已跑过 typecheck),节省 ~20s 构建时间 # 想严格类型检查的把这里改成 npm run build -ARG BUILD_MODE=build:fast +ARG BUILD_MODE=build RUN npm run ${BUILD_MODE} # ── 文档站点(可选):跨仓 srcDir = ../../../../file-batch-system/docs ── diff --git a/e2e/a11y.spec.ts b/e2e/a11y.spec.ts index 499ce79f..6eb1ba7a 100644 --- a/e2e/a11y.spec.ts +++ b/e2e/a11y.spec.ts @@ -14,18 +14,10 @@ import type { Page } from '@playwright/test' const SEVERITY_TO_FAIL = ['critical', 'serious'] as const -// EP 上游已知问题豁免清单(每条标注理由) -const GLOBAL_DISABLE_RULES = [ - // EP el-button plain 模式浅蓝 + 浅灰背景对比度低(WCAG 1.4.3 AA 4.5:1 边界) - // 整套设计语言调色后可移除,issue: element-plus/element-plus#14523 - 'color-contrast', -] - -async function runAxe( - page: Page, - exclude: string[] = [], - extraDisable: string[] = [], -) { +// 全局不豁免规则。第三方 canvas/chart 的局部问题应在具体测试中按选择器排除。 +const GLOBAL_DISABLE_RULES: string[] = [] + +async function runAxe(page: Page, exclude: string[] = [], extraDisable: string[] = []) { let builder = new AxeBuilder({ page }) .withTags(['wcag2aa', 'best-practice']) .disableRules([...GLOBAL_DISABLE_RULES, ...extraDisable]) diff --git a/e2e/all-pages-zero-error.spec.ts b/e2e/all-pages-zero-error.spec.ts index 33ae7952..e0564e7e 100644 --- a/e2e/all-pages-zero-error.spec.ts +++ b/e2e/all-pages-zero-error.spec.ts @@ -39,8 +39,6 @@ const PAGES: PageCheck[] = [ path: '/files/list', title: '文件列表', drillFirstRow: true, - // files/summary 端点旧 jar 返回 500,FE 已 _silent 降级为 0(src/api/file.ts by-design) - allowErrors: [/\/queries\/files\/summary\b/], }, { path: '/files/templates', title: '文件模板' }, { path: '/files/arrival-groups', title: '到达组治理' }, diff --git a/e2e/business-flows.spec.ts b/e2e/business-flows.spec.ts index 3a62d285..ab85148e 100644 --- a/e2e/business-flows.spec.ts +++ b/e2e/business-flows.spec.ts @@ -291,9 +291,6 @@ test.describe('@business-flows D 档 P6 真实业务流程', () => { // 8. File — 归档 / 审计 // ─────────────────────────────────────────────────────────────── test('8. File — 归档 + 审计行操作', async ({ page, network }) => { - // files/summary 端点旧 jar 返回 500,FE 已 _silent 降级为 0(src/api/file.ts by-design), - // 不属于本流程要守的错误面 - network.ignore(/\/queries\/files\/summary\b/) await page.goto('/files/list') await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {}) const row = page.locator('tbody tr.el-table__row').first() diff --git a/e2e/global-setup.js b/e2e/global-setup.js deleted file mode 100644 index 4ae24a91..00000000 --- a/e2e/global-setup.js +++ /dev/null @@ -1,241 +0,0 @@ -// @ts-check -const { writeFileSync, mkdirSync, readFileSync, existsSync } = require('fs') -const path = require('path') -const crypto = require('crypto') - -const API_BASE = 'http://localhost:18080' -const FIXTURE_TENANT = 'ta' - -// 超时常量(ms) -const T_SHORT = 8_000 // 登录、导出等轻量接口 -const T_UPLOAD = 20_000 // 文件上传 -const T_APPLY = 30_000 // 配置包应用(事务较重) - -/** 幂等 key */ -function idempotencyKey() { - return crypto.randomUUID() -} - -/** - * 带超时的 fetch 封装。 - * @param {string} url - * @param {RequestInit & { timeoutMs?: number }} opts - */ -async function fetchWithTimeout(url, opts = {}) { - const { timeoutMs = T_SHORT, ...rest } = opts - return fetch(url, { ...rest, signal: AbortSignal.timeout(timeoutMs) }) -} - -/** - * 向指定租户上传并应用租户配置包 Excel。 - */ -async function seedTenant(token, tenantId, filePath) { - if (!existsSync(filePath)) { - console.warn(`[seed] 文件不存在,跳过 ${tenantId}: ${filePath}`) - return - } - - const commonHeaders = { - Authorization: `Bearer ${token}`, - 'X-Tenant-Id': tenantId, - } - - // ── ① 上传 ────────────────────────────────────────────────────── - let uploadToken - try { - const fileBuffer = readFileSync(filePath) - const formData = new FormData() - formData.append( - 'file', - new Blob([fileBuffer], { - type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - }), - path.basename(filePath), - ) - - const uploadRes = await fetchWithTimeout( - `${API_BASE}/api/console/config/tenant-package/excel/upload?tenantId=${encodeURIComponent(tenantId)}`, - { - method: 'POST', - headers: { ...commonHeaders, 'Idempotency-Key': idempotencyKey() }, - body: formData, - timeoutMs: T_UPLOAD, - }, - ) - - if (!uploadRes.ok) { - const text = await uploadRes.text().catch(() => '') - console.warn(`[seed] 上传失败 tenant=${tenantId} status=${uploadRes.status} ${text}`) - return - } - - const uploadJson = await uploadRes.json() - uploadToken = uploadJson?.data?.uploadToken ?? uploadJson?.uploadToken - if (!uploadToken) { - console.warn(`[seed] 响应中无 uploadToken,跳过 tenant=${tenantId}`) - return - } - console.log(`[seed] 上传成功 tenant=${tenantId} token=${uploadToken}`) - } catch (err) { - console.warn(`[seed] 上传异常 tenant=${tenantId}: ${err.message}`) - return - } - - // ── ② 预览校验 ────────────────────────────────────────────────── - // 权威 11-sheet 样本依赖 worker step_registry;本地只起 console-api 时 - // pipeline_step_definition 会因 impl_code 未注册而无效。先 preview,漂移时跳过 apply。 - try { - const previewRes = await fetchWithTimeout( - `${API_BASE}/api/console/config/tenant-package/excel/preview/${encodeURIComponent(uploadToken)}?tenantId=${encodeURIComponent(tenantId)}`, - { headers: commonHeaders, timeoutMs: T_SHORT }, - ) - if (!previewRes.ok) { - const text = await previewRes.text().catch(() => '') - console.warn(`[seed] 预览失败 tenant=${tenantId} status=${previewRes.status} ${text}`) - return - } - const previewJson = await previewRes.json() - const data = previewJson?.data ?? previewJson - if ((data?.invalidRows ?? 0) > 0) { - const issues = Array.isArray(data?.issues) ? data.issues.slice(0, 5) : [] - const summary = issues - .map((x) => `${x.sheetName ?? '?'}#${x.rowNo ?? '?'} ${x.message ?? ''}`.trim()) - .join(' | ') - console.warn( - `[seed] 预览存在无效行,跳过 apply tenant=${tenantId} invalidRows=${data.invalidRows}${summary ? `: ${summary}` : ''}`, - ) - return - } - } catch (err) { - console.warn(`[seed] 预览异常 tenant=${tenantId}: ${err.message}`) - return - } - - // ── ③ 应用 ────────────────────────────────────────────────────── - try { - const applyRes = await fetchWithTimeout( - `${API_BASE}/api/console/config/tenant-package/excel/apply/${encodeURIComponent(uploadToken)}`, - { - method: 'POST', - headers: { - ...commonHeaders, - 'Content-Type': 'application/json', - 'Idempotency-Key': idempotencyKey(), - }, - body: JSON.stringify({}), - timeoutMs: T_APPLY, - }, - ) - - if (!applyRes.ok) { - const text = await applyRes.text().catch(() => '') - console.warn(`[seed] 应用失败 tenant=${tenantId} status=${applyRes.status} ${text}`) - } else { - console.log(`[seed] ✓ tenant=${tenantId} 配置包导入完成`) - } - } catch (err) { - console.warn(`[seed] 应用异常 tenant=${tenantId}: ${err.message}`) - } -} - -/** - * 导出租户配置包作为 UI 上传测试的 fixture 文件。 - */ -async function exportTenantPackageFixture(token) { - const outPath = path.resolve(__dirname, '../test-excel-abc/tenant-package-export.xlsx') - if (existsSync(outPath)) { - console.log('[fixture] 已存在,跳过租户包导出') - return - } - try { - const res = await fetchWithTimeout( - `${API_BASE}/api/console/config/tenant-package/excel/export?tenantId=${encodeURIComponent(FIXTURE_TENANT)}`, - { headers: { Authorization: `Bearer ${token}`, 'X-Tenant-Id': FIXTURE_TENANT } }, - ) - if (!res.ok) { - console.warn(`[fixture] 租户包导出失败 status=${res.status}`) - return - } - const buf = Buffer.from(await res.arrayBuffer()) - writeFileSync(outPath, buf) - console.log(`[fixture] ✓ 导出租户包 → ${outPath}`) - } catch (err) { - console.warn(`[fixture] 租户包导出异常: ${err.message}`) - } -} - -/** - * 租户配置包 seed xlsx 的源在后端仓库下,前端仅引用不再保存副本, - * 防止两份文件漂移(权威源: - * file-batch-system/docs/test-data/test-full-coverage-import-suite/README.md)。 - */ -const SEED_SUITE_DIR = path.resolve( - __dirname, - '../../file-batch-system/docs/test-data/test-full-coverage-import-suite', -) - -const TENANT_EXCELS = [ - { tenantId: 'ta', file: path.join(SEED_SUITE_DIR, 'ta-tenant-config-package-test.xlsx') }, - { tenantId: 'tb', file: path.join(SEED_SUITE_DIR, 'tb-tenant-config-package-test.xlsx') }, - { tenantId: 'tc', file: path.join(SEED_SUITE_DIR, 'tc-tenant-config-package-test.xlsx') }, -] - -/** - * 全局 Setup: - * 1. 登录获取 JWT - * 2. 为 ta / tb / tc 三个租户导入配置包 Excel(超时即跳过,不阻断测试) - * 3. 导出 fixture 文件供 UI 上传链路测试使用 - * 4. 写入 storageState(默认测试租户为 ta) - * - * @param {import('@playwright/test').FullConfig} config - */ -async function globalSetup(config) { - const baseURL = config.projects[0].use.baseURL ?? 'http://localhost:5173' - - // ── 登录 ──────────────────────────────────────────────────────── - let token - try { - const loginRes = await fetchWithTimeout(`${API_BASE}/api/console/auth/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'X-Tenant-Id': 'system' }, - body: JSON.stringify({ username: 'admin', password: 'admin123' }), - }) - if (!loginRes.ok) throw new Error(`HTTP ${loginRes.status}`) - const loginJson = await loginRes.json() - token = loginJson?.data?.accessToken - if (!token) throw new Error('响应中无 accessToken') - console.log('[global-setup] 登录成功,expires:', loginJson?.data?.expiresAt) - } catch (err) { - console.warn(`[global-setup] 登录失败(${err.message}),跳过 seed,仅写入 storageState`) - } - - // ── 导入测试数据 ───────────────────────────────────────────────── - if (token) { - for (const { tenantId, file } of TENANT_EXCELS) { - await seedTenant(token, tenantId, file) - } - await exportTenantPackageFixture(token) - } - - // ── 写 storageState(默认测试租户 ta)─────────────────────────── - const authDir = path.resolve('e2e/.auth') - mkdirSync(authDir, { recursive: true }) - - const storageState = { - cookies: [], - origins: [ - { - origin: baseURL, - localStorage: [ - { name: 'batch-console-tenant-id', value: 'ta' }, - { name: 'token', value: token ?? '' }, - ], - }, - ], - } - - writeFileSync(path.join(authDir, 'user.json'), JSON.stringify(storageState, null, 2)) - console.log('[global-setup] storageState 已写入,默认测试租户: ta') -} - -module.exports = globalSetup diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts deleted file mode 100644 index 4c3d9733..00000000 --- a/e2e/global-setup.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { chromium, type FullConfig } from '@playwright/test' - -/** - * 全局登录:用真实账号走一次认证,将 cookie / localStorage 保存到 - * e2e/.auth/user.json,后续所有测试通过 storageState 复用该 session。 - */ -export default async function globalSetup(config: FullConfig) { - const baseURL = config.projects[0].use.baseURL ?? 'http://localhost:5173' - const browser = await chromium.launch() - const page = await browser.newPage() - - await page.goto(`${baseURL}/login`) - await page.getByPlaceholder('请输入用户名').fill('admin') - await page.getByPlaceholder('请输入密码').fill('admin123') - await page.getByRole('button', { name: /登\s*录/ }).click() - await page.waitForURL(/\/ops\/summary/, { timeout: 20_000 }) - - // 2026-05 角色重设计后:admin 登录 (tenantId='system') 不再自动落 tenant store, - // 首次登录由 FirstTenantPickerDialog 强制选业务租户。e2e 用 storageState 复用 session, - // 这里显式落一个业务租户 (ta),避免每个测试启动时被 picker dialog 拦住 sidebar 点击。 - await page.evaluate(() => localStorage.setItem('batch-console-tenant-id', 'ta')) - - await page.context().storageState({ path: 'e2e/.auth/user.json' }) - await browser.close() -} diff --git a/e2e/login-validation.spec.ts b/e2e/login-validation.spec.ts index d6885ead..f0a08858 100644 --- a/e2e/login-validation.spec.ts +++ b/e2e/login-validation.spec.ts @@ -23,10 +23,39 @@ test.describe('login form validation', () => { test('错误密码触发 error toast', async ({ page, context }) => { await context.clearCookies() await page.goto('http://localhost:5173/login', { waitUntil: 'networkidle' }) - await page.getByPlaceholder(/用户名|username/i).first().fill('admin') - await page.getByPlaceholder(/密码|password/i).first().fill('wrong-password-xyz') - await page.getByRole('button', { name: /登\s*录|Sign\s*in/i }).first().click() + await page + .getByPlaceholder(/用户名|username/i) + .first() + .fill('admin') + await page + .getByPlaceholder(/密码|password/i) + .first() + .fill('wrong-password-xyz') + await page + .getByRole('button', { name: /登\s*录|Sign\s*in/i }) + .first() + .click() const toast = page.locator('.el-message--error, .el-message--warning').first() await expect(toast).toBeVisible({ timeout: 8000 }) }) + + test('登录页语言切换同步更新表单文案', async ({ page, context }) => { + await context.clearCookies() + await page.goto('http://localhost:5173/login', { waitUntil: 'networkidle' }) + await page.evaluate(() => localStorage.setItem('batch-console:locale', 'zh-CN')) + await page.reload({ waitUntil: 'networkidle' }) + + const localeToggle = page.getByRole('button', { name: /切换语言|Switch language/i }) + await expect(localeToggle).toBeVisible({ timeout: 8000 }) + await localeToggle.click() + + await expect(page.getByRole('heading', { name: 'Sign in to the console' })).toBeVisible() + await expect(page.getByPlaceholder('Enter tenant username')).toBeVisible() + await expect(page.getByPlaceholder('Enter tenant password')).toBeVisible() + + await localeToggle.click() + await expect(page.getByRole('heading', { name: '登录控制台' })).toBeVisible() + await expect(page.getByPlaceholder('请输入租户账号')).toBeVisible() + await expect(page.getByPlaceholder('请输入租户密码')).toBeVisible() + }) }) diff --git a/e2e/workflow-designer-save-flow.spec.ts b/e2e/workflow-designer-save-flow.spec.ts index b3728cbc..7901ea1c 100644 --- a/e2e/workflow-designer-save-flow.spec.ts +++ b/e2e/workflow-designer-save-flow.spec.ts @@ -25,14 +25,6 @@ import { enterDemoApp, isVisible } from './support/app' test.describe('@workflow-designer-save 工作流设计器保存流', () => { test.beforeEach(async ({ page }) => { await enterDemoApp(page) - // 释放可能残留的设计锁(设计锁按会话持有,跨 e2e 运行不自动释放),避免撞「只读」被 skip。 - for (let id = 1; id <= 15; id++) { - await page.request - .delete(`/api/console/workflow-definitions/${id}/lock?tenantId=ta`, { - headers: { 'X-Tenant-Id': 'ta', 'Idempotency-Key': `e2e-rellock-${id}-${Date.now()}` }, - }) - .catch(() => undefined) - } }) test('进入 → 自动布局改图 → 保存 → 刷新后节点仍在', async ({ page }) => { @@ -69,6 +61,20 @@ test.describe('@workflow-designer-save 工作流设计器保存流', () => { await openBtn.click({ force: true }) await expect(page).toHaveURL(/\/workflow\/designer\/\d+/, { timeout: 10_000 }) + // 锁是按工作流 ID 持有的。历史实现只清 1..15,在真实自增 ID 下会遗留旧会话的锁, + // 让这个写入用例无意义地 skip。导航完成后精确释放当前工作流,再刷新以当前会话重新获取。 + const workflowId = new URL(page.url()).pathname.match(/\/workflow\/designer\/(\d+)/)?.[1] + if (!workflowId) { + test.skip(true, '无法从设计器路由解析工作流 ID,跳过保存断言') + return + } + await page.request + .delete(`/api/console/workflow-definitions/${workflowId}/lock?tenantId=ta`, { + headers: { 'X-Tenant-Id': 'ta', 'Idempotency-Key': `e2e-release-lock-${workflowId}-${Date.now()}` }, + }) + .catch(() => undefined) + await page.reload() + // ── 画布渲染验证(修复后不再崩溃)── await expect(page.locator('.node-palette').first()).toBeVisible({ timeout: 12_000 }) await expect(page.locator('.dag-canvas').first()).toBeVisible({ timeout: 8_000 }) diff --git a/playwright.config.cjs b/playwright.config.cjs index 7ef77b01..3ab58cad 100644 --- a/playwright.config.cjs +++ b/playwright.config.cjs @@ -1,6 +1,8 @@ // @ts-check const { defineConfig, devices } = require('@playwright/test') +const baseURL = process.env.E2E_BASE_URL || 'http://localhost:5173' + module.exports = defineConfig({ testDir: './e2e', // 每次运行前刷新 token + 上传 seed 到 ta/tb/tc(非 CI 也执行,避免 storageState 过期) @@ -27,7 +29,7 @@ module.exports = defineConfig({ ['json', { outputFile: 'playwright-report/results.json' }], ], use: { - baseURL: 'http://localhost:5173', + baseURL, storageState: 'e2e/.auth/user.json', // 4-worker 并发对 dev server + backend 的压力较大,偶发 page.goto 10s // 和 click 5s 超时都不是代码 bug;给出更稳的预算 @@ -52,7 +54,7 @@ module.exports = defineConfig({ ], webServer: { command: 'npm run dev', - url: 'http://localhost:5173', + url: baseURL, reuseExistingServer: !process.env.CI, timeout: 120_000, }, diff --git a/public/images/login-batch-pipeline.png b/public/images/login-batch-pipeline.png new file mode 100644 index 00000000..65f99259 Binary files /dev/null and b/public/images/login-batch-pipeline.png differ diff --git a/src/api/file.ts b/src/api/file.ts index b5dd9737..f67e2999 100644 --- a/src/api/file.ts +++ b/src/api/file.ts @@ -58,8 +58,7 @@ export const fileApi = { /** * 文件列表页领域汇总卡:今日到达 / 待处理 / 已处理 / 失败。 - * 当前本地 BE 的 /queries/files/summary 仍会 500;这里用稳定的 /queries/files total - * 兜底组成汇总,避免页面统计卡显示全 0 且污染浏览器控制台。 + * 通过稳定的 /queries/files 分页总数按口径组成汇总,避免依赖额外的聚合接口。 */ summary: async (tenantId = readStoredTenantId()): Promise => { const today = new Date() diff --git a/src/api/stream.test.ts b/src/api/stream.test.ts index 7153356d..31e5fc86 100644 --- a/src/api/stream.test.ts +++ b/src/api/stream.test.ts @@ -154,24 +154,24 @@ describe('createLogStream', () => { await createLogStream(42, onMessage) const es = FakeEventSource.instances[0] - es.emit(JSON.stringify({ data: { id: 42, status: 'RUNNING' } })) - es.emit(JSON.stringify({ data: { id: 99, status: 'OTHER' } })) + es.emitNamed('job-instance-updated', JSON.stringify({ data: { id: 42, status: 'RUNNING' } })) + es.emitNamed('job-instance-updated', JSON.stringify({ data: { id: 99, status: 'OTHER' } })) expect(onMessage).toHaveBeenCalledTimes(1) expect(onMessage).toHaveBeenCalledWith(expect.stringContaining('RUNNING')) }) - it('forwards non-JSON messages without filtering', async () => { + it('does not forward heartbeat messages', async () => { const onMessage = vi.fn() await createLogStream(42, onMessage) - FakeEventSource.instances[0].emit('heartbeat') - expect(onMessage).toHaveBeenCalledWith('heartbeat') + FakeEventSource.instances[0].emitNamed('heartbeat', 'heartbeat') + expect(onMessage).not.toHaveBeenCalled() }) - it('forwards messages without payload id (heartbeat/ready) without filtering', async () => { + it('forwards reset-required without an instance id', async () => { const onMessage = vi.fn() await createLogStream(42, onMessage) - FakeEventSource.instances[0].emit(JSON.stringify({ data: {} })) + FakeEventSource.instances[0].emitNamed('reset-required', JSON.stringify({ data: {} })) expect(onMessage).toHaveBeenCalledTimes(1) }) diff --git a/src/api/stream.ts b/src/api/stream.ts index 9deb73ab..f6f68e92 100644 --- a/src/api/stream.ts +++ b/src/api/stream.ts @@ -89,13 +89,8 @@ const SSE_DOMAIN_EVENT_NAMES = [ 'workflow-run-updated', ] as const -/** Spring SSE 使用命名 event;与 ConsoleRealtimeEventHub 生命周期及 job-instances 域事件对齐 */ -const JOB_INSTANCE_SSE_EVENT_NAMES = [ - 'ready', - 'heartbeat', - 'reset-required', - 'job-instance-updated', -] as const +/** 详情页只消费会使实例数据变脏的事件;ready/heartbeat 仅用于保活,不应触发重复 GET。 */ +const JOB_INSTANCE_SSE_EVENT_NAMES = ['reset-required', 'job-instance-updated'] as const function jobInstancePayloadId(data: unknown): number | undefined { if (!data || typeof data !== 'object') return undefined @@ -107,7 +102,7 @@ function jobInstancePayloadId(data: unknown): number | undefined { * 订阅作业实例实时 SSE(`/api/console/stream/job-instances/events`)。 * 命名事件会序列化为 JSON 字符串交给回调。 * - * @param instanceId 若传入,则仅在解析到 `data.id` 与该 id 一致时回调(心跳/ready 等仍原样回调)。 + * @param instanceId 若传入,则仅在解析到 `data.id` 与该 id 一致时回调;无 id 的 reset-required 仍会回调。 */ export async function createLogStream( instanceId: number, @@ -133,7 +128,6 @@ export async function createLogStream( for (const name of JOB_INSTANCE_SSE_EVENT_NAMES) { es.addEventListener(name, forward) } - es.onmessage = forward es.onerror = (e) => { onError?.(e) es.close() diff --git a/src/components/workflow/WorkflowMiniDag.vue b/src/components/workflow/WorkflowMiniDag.vue index aff69e42..0c4bac0a 100644 --- a/src/components/workflow/WorkflowMiniDag.vue +++ b/src/components/workflow/WorkflowMiniDag.vue @@ -16,8 +16,8 @@ @@ -151,7 +150,7 @@ } /* 暗色 Liquid Glass:把 inset 高光透明度调高一倍,黑底上才看得见折射 */ - :global(html.dark) .mobile-tab-bar { + :global(html.dark .mobile-tab-bar) { background: color-mix(in srgb, #10151d 88%, transparent 12%); border-top: 0.5px solid rgb(148 163 184 / 28%); box-shadow: @@ -161,7 +160,7 @@ 0 -8px 24px rgb(0 0 0 / 50%); } - :global(html.dark) .mobile-tab-bar::before { + :global(html.dark .mobile-tab-bar::before) { background: radial-gradient(circle at 0% 0%, rgb(10 132 255 / 14%) 0%, transparent 40%), radial-gradient(circle at 100% 0%, rgb(94 92 230 / 12%) 0%, transparent 40%); @@ -182,7 +181,7 @@ transition: color 0.1s ease; } - :global(html.dark) .mobile-tab { + :global(html.dark .mobile-tab) { color: rgb(235 235 245 / 60%); } @@ -217,7 +216,7 @@ box-shadow: 0 0 0 1.5px #ffffff; } - :global(html.dark) .mobile-tab__badge { + :global(html.dark .mobile-tab__badge) { box-shadow: 0 0 0 1.5px #1c1c1e; } diff --git a/src/layout/components/NotificationCenter.vue b/src/layout/components/NotificationCenter.vue index 46cb4bd4..b06b18b3 100644 --- a/src/layout/components/NotificationCenter.vue +++ b/src/layout/components/NotificationCenter.vue @@ -30,7 +30,7 @@ {{ t('notificationCenter.title') }} -

+

{{ t('notificationCenter.empty') }}

@@ -64,9 +64,7 @@ - +
  • +