修复深色说明样式归属与实名认证上传字段标签

This commit is contained in:
cieanyau committed 2026-11-06 03:12:03 +08:00
1 parent 56188874cb
commit 21821ec6fa
4 files changed
+383 -17

No files matched your search

@@ -10,18 +10,19 @@
line-height: 1.8;
}
.description-panel[data-v-8a6cfb44] {
background: #fff;
border-top: 1px solid #e2e9f3;
border-bottom: 1px solid #dfe6f0;
/* 面板与文字使用同一主题协议,紧凑预览和完整说明都不能固定为浅色配色。 */
background: var(--p-content-background);
border-top: 1px solid var(--p-content-border-color);
border-bottom: 1px solid var(--p-content-border-color);
padding: 0;
}
.description-header[data-v-8a6cfb44] {
background: #f5f8fe;
border-bottom: 1px solid #dfe6f0;
background: var(--p-content-hover-background);
border-bottom: 1px solid var(--p-content-border-color);
padding: 8px clamp(20px, 8.6%, 110px);
}
.description-header h2[data-v-8a6cfb44] {
color: #657286;
color: var(--p-text-muted-color);
margin: 0;
font-size: 21px;
font-weight: 500;
@@ -42,7 +43,7 @@
display: grid;
}
.description-field dt[data-v-8a6cfb44] {
color: #708096;
color: var(--p-text-muted-color);
align-items: center;
gap: 16px;
min-width: 0;
@@ -56,7 +57,7 @@
font-size: 18px;
}
.description-field dd[data-v-8a6cfb44] {
color: #394658;
color: var(--p-text-color);
white-space: pre-line;
min-width: 0;
margin: 0;
@@ -89,13 +89,13 @@
.calc-row.total > b[data-v-e4be3b52] {
color: var(--p-text-color);
}
/* 应用深色主题覆盖,保持在基础颜色之后;主题切换只改变外观,不改变业务状态。 */
.app-dark .notice {
/* 深色提示仅属于本弹窗;首页公告也使用 notice-title,不能让覆盖跨越组件作用域。 */
.app-dark .notice[data-v-e4be3b52] {
background: #172554;
border-color: #1e3a8a;
}
/* 应用深色主题覆盖,保持在基础颜色之后;主题切换只改变外观,不改变业务状态。 */
.app-dark .notice ul,
.app-dark .notice-title {
.app-dark .notice ul[data-v-e4be3b52],
.app-dark .notice-title[data-v-e4be3b52] {
color: #bfdbfe;
}
+16 -5
View File
@@ -4,6 +4,7 @@ import "~/assets/styles/UploadField.BbTCTv6C.css";
import { toDisplayString } from "vue";
import { mergeModels } from "vue";
import { useModel } from "vue";
import { useId } from "vue";
import { ref } from "vue";
import { createCommentVNode } from "vue";
import { createElementBlock } from "vue";
@@ -18,13 +19,10 @@ import { t as _plugin_vue_export_helper_t } from "~/lib/_plugin-vue_export-helpe
var m = {
class: `field`,
},
h = {
key: 0,
},
g = {
class: `row`,
},
_ = [`accept`, `disabled`],
_ = [`id`, `accept`, `disabled`],
v = {
key: 0,
class: `state`,
@@ -62,6 +60,8 @@ var m = {
let p = useModel(t, `modelValue`),
S = u,
C = useApi(),
// 每个实例有独立关联 ID,多张证件同时上传时标签仍指向各自的原生输入框。
L = useId(),
w = ref(!1),
T = ref(``),
E = ref(``);
@@ -90,13 +90,20 @@ var m = {
createElementBlock(`div`, m, [
t.label
? (openBlock(),
createElementBlock(`label`, h, toDisplayString(t.label), 1))
createElementBlock(
`label`,
{ key: 0, for: L },
toDisplayString(t.label),
9,
[`for`],
))
: createCommentVNode(``, !0),
createElementVNode(`div`, g, [
createElementVNode(
`input`,
{
type: `file`,
id: L,
accept: t.accept,
disabled: unref(w),
onChange: D,
@@ -147,6 +154,7 @@ var m = {
{
type: `button`,
class: `rm`,
"aria-label": `移除${t.label || `已上传文件`}`,
onClick: O,
},
[
@@ -155,12 +163,15 @@ var m = {
`i`,
{
class: `pi pi-times`,
"aria-hidden": `true`,
},
null,
-1,
),
]),
],
8,
[`aria-label`],
),
],
64,
+354
View File
@@ -0,0 +1,354 @@
/** 主题和字段回归经真实商品、首页、实名认证与实例入口验证;只替换外部 HTTP 响应。 */
import assert from "node:assert/strict";
import test from "node:test";
import { backendOrigin, login, scenario } from "./helpers/console-browser.mjs";
async function boundary(page, responses) {
const requests = [];
await page
.context()
.route(`${backendOrigin}/console/v1/**`, async (route) => {
const request = route.request();
const path = new URL(request.url()).pathname.replace(
/^\/console\/v1/,
"",
);
if (!Object.hasOwn(responses, path)) return route.fallback();
requests.push({
path,
authenticated: Boolean(request.headers().authorization),
multipart:
request
.headers()
["content-type"]?.startsWith("multipart/form-data; boundary=") ===
true,
});
await route.fulfill({
json: { status: 200, msg: "", data: responses[path] },
headers: { "Access-Control-Allow-Origin": "*" },
});
});
return requests;
}
async function textPresentation(locator) {
return locator.evaluate((element) => {
const style = getComputedStyle(element);
const rgb = (value) => value.match(/[\d.]+/g).map(Number);
let background = [255, 255, 255];
const layers = [];
for (let owner = element; owner; owner = owner.parentElement)
layers.push(rgb(getComputedStyle(owner).backgroundColor));
for (const layer of layers.reverse()) {
const alpha = layer[3] ?? 1;
background = background.map(
(channel, index) => channel * (1 - alpha) + layer[index] * alpha,
);
}
const foreground = rgb(style.color);
const luminance = (color) =>
color
.slice(0, 3)
.map((channel) => {
const value = channel / 255;
return value <= 0.04045
? value / 12.92
: ((value + 0.055) / 1.055) ** 2.4;
})
.reduce(
(total, value, index) =>
total + value * [0.2126, 0.7152, 0.0722][index],
0,
);
const light = luminance(foreground);
const dark = luminance(background);
const tokenColor = new Option().style;
tokenColor.color = style.getPropertyValue("--p-text-color");
return {
color: style.color,
textColor: tokenColor.color,
background: background.map(Math.round),
contrast: (Math.max(light, dark) + 0.05) / (Math.min(light, dark) + 0.05),
};
});
}
test(
"dark structured catalog descriptions keep readable field text",
{ timeout: 25000 },
(t) =>
scenario(t, {}, async (page) => {
await page.emulateMedia({ colorScheme: "dark" });
await boundary(page, {
"/product/group/first": { list: [{ id: 1, name: "云产品" }] },
"/product/group/second": {
list: [{ id: 2, parent_id: 1, name: "测试分类", description: "" }],
},
"/product": {
list: [
{
id: 123,
name: "布局验收商品",
module: "idcsmart_common",
res_module: "idcsmart_common",
pay_type: "recurring_prepayment",
price: "10.00",
billing_cycle: "monthly",
stock_control: 0,
qty: 10,
order_allowed: true,
description: "配置:4GB 内存\n线路:BGP",
},
],
count: 1,
},
});
await login(page, "/products?g=1&sg=2");
const value = page
.locator(".description-field dd")
.filter({ hasText: "4GB" });
const label = page
.locator(".description-field dt")
.filter({ hasText: "配置" });
await value.waitFor();
await label.waitFor();
assert.ok(
(await textPresentation(value)).contrast >= 4.5,
"Structured description values must remain readable in dark mode",
);
assert.ok(
(await textPresentation(label)).contrast >= 4.5,
"Structured description labels must remain readable in dark mode",
);
return { structuredDescriptionReadable: true };
}),
);
test(
"dark withdrawal notices do not recolor the home announcement title",
{ timeout: 25000 },
(t) =>
scenario(t, {}, async (page) => {
await page.emulateMedia({ colorScheme: "dark" });
await boundary(page, {
"/announcement": {
list: [{ id: 1, title: "本地公告", create_time: 1700000000 }],
count: 1,
},
"/withdraw/rule/credit": {
status: 1,
method: [{ id: 0, name: "账户余额", no_account: true }],
min: "1.00",
max: "100.00",
withdraw_fee_type: "fixed",
withdraw_fee: "0.00",
},
});
await login(page, "/");
const announcement = page
.locator(".home-row-title")
.filter({ hasText: "本地公告" });
await announcement.waitFor();
const home = await textPresentation(announcement);
assert.equal(
home.color,
home.textColor,
"The announcement title owns its normal text color",
);
await page.getByRole("button", { name: "提现", exact: true }).click();
const dialog = page.getByRole("dialog", { name: "提现", exact: true });
const notice = dialog.locator(".notice-title");
await notice.waitFor();
const ownNotice = await textPresentation(notice);
assert.ok(
ownNotice.contrast >= 4.5,
"Withdrawal instructions must remain readable",
);
assert.notEqual(
ownNotice.color,
home.color,
"The withdrawal notice retains its own emphasis color",
);
assert.equal((await textPresentation(announcement)).color, home.color);
return {
homeAnnouncementColorOwned: true,
withdrawalNoticeReadable: true,
};
}),
);
test(
"real-name upload fields expose distinct labels and named removal actions",
{ timeout: 25000 },
(t) =>
scenario(t, {}, async (page) => {
const requests = await boundary(page, {
"/certification/info": {
certification_open: 1,
certification_company_open: 1,
certification_upload: 1,
certification_company_need_person: 0,
is_certification: 0,
person: { status: 0 },
company: { status: 0 },
},
"/upload": { save_name: "identity-front.png" },
});
await login(page, "/account/realname/personal");
const front = page.getByLabel("身份证正面照 *", { exact: true });
const back = page.getByLabel("身份证反面照 *", { exact: true });
await front.waitFor();
await back.waitFor();
assert.notEqual(
await front.getAttribute("id"),
await back.getAttribute("id"),
);
const file = {
name: "identity-front.png",
mimeType: "image/png",
buffer: Buffer.from("local-e2e-upload-not-identity-data"),
};
await front.setInputFiles(file);
const remove = page.getByRole("button", {
name: "移除身份证正面照 *",
exact: true,
});
await remove.waitFor();
assert.deepEqual(
requests.filter((request) => request.path === "/upload"),
[{ path: "/upload", authenticated: true, multipart: true }],
);
await remove.click();
assert.equal(await remove.count(), 0);
await front.setInputFiles(file);
await remove.waitFor();
return {
separateFileInputNames: true,
namedRemoval: true,
sameFileReselection: true,
authenticatedMultipartUpload: true,
};
}),
);
test(
"normal kky upgrade plans and disks fit the narrow dialog container",
{ timeout: 30000 },
(t) =>
scenario(t, {}, async (page) => {
await boundary(page, {
"/host/5": {
host: {
id: 5,
product_id: 123,
module: "kky_ecs",
name: "本地弹性云",
status: "Active",
billing_cycle: "monthly",
upgrade_allowed: true,
renew_amount: "10.00",
},
},
"/kky_ecs/5": {
cpu: 2,
memory: 4,
bw: 10,
protect: 0,
ip: "192.0.2.5",
username: "root",
port: 22,
image: { name: "Ubuntu 24.04" },
},
"/kky_ecs/5/status": { status: "on", rescue: false },
"/kky_ecs/5/traffic": {
mode: "bandwidth",
monthly_quota: 100,
traffic_show: 0,
},
"/kky_ecs/disk/5": {
list: [
{
id: "data-1",
name: "数据盘",
size: 40,
type2: "data",
fs_type: "SSD",
},
],
},
"/kky_ecs/monitor/5": {
sample_info: [],
cpu: [],
memory: [],
bandwidth: [],
disk: [],
},
"/host/5/renew/auto": { status: 0 },
"/refund": { allow_refund: 0, reasons: [] },
"/refund/host/5/refund": { refund: null },
"/promo_code/host/5/promo_code": { promo_code: [] },
"/kky_ecs/upgrade/5/ins_upgrade_config": [
{
cpu_num: 4,
mem_list: [
{ id: 1, memory: 8, is_sell_out: 0, description: "常规升级规格" },
],
},
],
"/kky_ecs/upgrade/5/add_disk_config": {
min: 10,
max: 1000,
step: 10,
type: ["SSD"],
remaining_num: 0,
},
});
await login(page, "/servers/server?id=5");
await page.getByRole("tab", { name: "计费", exact: true }).click();
await page.getByRole("button", { name: "升降级", exact: true }).click();
const dialog = page.getByRole("dialog", { name: "升降级", exact: true });
await dialog.locator(".plans .plan").waitFor();
const widths = [];
const assertFits = async (gridName, cardName) => {
const measured = await dialog
.locator(gridName)
.evaluate((grid, childSelector) => {
const bounds = grid.getBoundingClientRect();
const card = grid
.querySelector(childSelector)
.getBoundingClientRect();
return {
width: bounds.width,
scroll: grid.scrollWidth,
client: grid.clientWidth,
rightOverflow: card.right - bounds.right,
};
}, cardName);
assert.ok(measured.width > 0);
assert.ok(
measured.rightOverflow <= 1 && measured.scroll <= measured.client + 1,
`${gridName} must shrink to its actual dialog container: ${JSON.stringify(measured)}`,
);
widths.push({
viewport: page.viewportSize().width,
grid: gridName,
width: Math.round(measured.width),
fits: true,
});
};
for (const width of [375, 320]) {
await page.setViewportSize({ width, height: 900 });
await assertFits(".plans", ".plan");
}
await dialog.getByRole("button", { name: /硬盘/ }).click();
await dialog
.getByRole("button", { name: "扩容已有盘", exact: true })
.click();
await dialog.locator(".disks .disk").waitFor();
for (const width of [375, 320]) {
await page.setViewportSize({ width, height: 900 });
await assertFits(".disks", ".disk");
}
return { grids: widths };
}),
);