This commit is contained in:
1 parent
339df89fcf
commit
651c36367f
33 files changed
+1420
-40
No files matched your search
@@ -22,6 +22,10 @@ jobs:
|
|||||||
node -e 'if (Number(process.versions.node.split(".")[0]) !== 22) { console.error("需要 Node 22"); process.exit(1) }'
|
node -e 'if (Number(process.versions.node.split(".")[0]) !== 22) { console.error("需要 Node 22"); process.exit(1) }'
|
||||||
- name: 安装锁定依赖
|
- name: 安装锁定依赖
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
- name: 检查代码和格式
|
||||||
|
run: |
|
||||||
|
npm run lint
|
||||||
|
npm run format:check
|
||||||
- name: 安装测试浏览器
|
- name: 安装测试浏览器
|
||||||
run: npx playwright install --with-deps chromium firefox
|
run: npx playwright install --with-deps chromium firefox
|
||||||
- name: 检查源码并构建
|
- name: 检查源码并构建
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Generated output and local-only reports are not source formatting targets.
|
||||||
|
node_modules/
|
||||||
|
.nuxt/
|
||||||
|
.output/
|
||||||
|
.data/
|
||||||
|
test-results/
|
||||||
|
tests/fixtures/runs/
|
||||||
|
docs/
|
||||||
|
.claude/skills/crashwork-ops/
|
||||||
|
|
||||||
|
# Preserve supplied static assets and npm's lockfile serialization.
|
||||||
|
public/
|
||||||
|
package-lock.json
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
@@ -21,13 +21,13 @@
|
|||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
| 层次 | 技术 |
|
| 层次 | 技术 |
|
||||||
| ---------- | --------------------------------------------------------- |
|
| ---------- | --------------------------------------------------------------- |
|
||||||
| 应用框架 | Nuxt 4、Vue 3、Vue Router,客户端 SPA(`ssr: false`) |
|
| 应用框架 | Nuxt 4、Vue 3、Vue Router,客户端 SPA(`ssr: false`) |
|
||||||
| 状态与界面 | Pinia、PrimeVue 4、PrimeUI 主题、本地 Lucide 图标 |
|
| 状态与界面 | Pinia、PrimeVue 4、PrimeUI 主题、本地 Lucide 图标 |
|
||||||
| 图表与远控 | ECharts、vue-echarts、noVNC |
|
| 图表与远控 | ECharts、vue-echarts、noVNC |
|
||||||
| 源码 | JavaScript、Vue 单文件组件、CSS,Nuxt 配置使用 TypeScript |
|
| 源码 | JavaScript、Vue 单文件组件、CSS,Nuxt 配置使用 TypeScript |
|
||||||
| 验证工具 | Node.js `node:test`、Playwright、源码检查脚本、Prettier |
|
| 验证工具 | Node.js `node:test`、Playwright、源码检查脚本、ESLint、Prettier |
|
||||||
|
|
||||||
依赖的精确版本以 `package-lock.json` 为准。
|
依赖的精确版本以 `package-lock.json` 为准。
|
||||||
|
|
||||||
@@ -70,19 +70,27 @@ npm run dev
|
|||||||
|
|
||||||
## 常用命令
|
## 常用命令
|
||||||
|
|
||||||
| 命令 | 用途 |
|
| 命令 | 用途 |
|
||||||
| ----------------- | ----------------------------------------- |
|
| ---------------------- | ----------------------------------------- |
|
||||||
| `npm run dev` | 启动本地开发服务 |
|
| `npm run dev` | 启动本地开发服务 |
|
||||||
| `npm run check` | 检查应用源码语法、导入和历史部署路径依赖 |
|
| `npm run check` | 检查应用源码语法、导入和历史部署路径依赖 |
|
||||||
| `npm run build` | 构建生产版本,输出到项目根目录 `.output/` |
|
| `npm run lint` | 检查应用、配置、脚本和测试的代码问题 |
|
||||||
| `npm run preview` | 运行已构建的 Node.js 服务 |
|
| `npm run lint:fix` | 自动修复可安全修正的 ESLint 问题 |
|
||||||
| `npm test` | 对生产构建运行浏览器端到端测试 |
|
| `npm run build` | 构建生产版本,输出到项目根目录 `.output/` |
|
||||||
| `npm run format` | 格式化应用、配置、脚本和测试代码 |
|
| `npm run preview` | 运行已构建的 Node.js 服务 |
|
||||||
|
| `npm test` | 对生产构建运行浏览器端到端测试 |
|
||||||
|
| `npm run format` | 统一格式化应用、配置、脚本、测试和文档 |
|
||||||
|
| `npm run format:check` | 只检查格式,不改写文件 |
|
||||||
|
|
||||||
|
格式规则在 `.prettierrc.json`,扫描范围由 `.prettierignore` 排除生成物、本地报告、
|
||||||
|
静态资源和 npm 管理的锁文件;不会将 `docs/` 或测试产物重新纳入提交。
|
||||||
|
|
||||||
### 构建与本地预览
|
### 构建与本地预览
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run check
|
npm run check
|
||||||
|
npm run lint
|
||||||
|
npm run format:check
|
||||||
npm run build
|
npm run build
|
||||||
NUXT_PUBLIC_API_BASE=http://127.0.0.1:8080 HOST=127.0.0.1 PORT=3000 npm run preview
|
NUXT_PUBLIC_API_BASE=http://127.0.0.1:8080 HOST=127.0.0.1 PORT=3000 npm run preview
|
||||||
```
|
```
|
||||||
@@ -137,7 +145,7 @@ CHROME_PATH=/absolute/path/to/chrome npm test
|
|||||||
测试产物保存在被忽略的 `tests/fixtures/runs/`。
|
测试产物保存在被忽略的 `tests/fixtures/runs/`。
|
||||||
详细使用方式见 [测试说明](tests/README.md)。
|
详细使用方式见 [测试说明](tests/README.md)。
|
||||||
|
|
||||||
仓库包含 `.gitea/workflows/frontend.yml`:安装锁定依赖后运行源码检查、生产构建、浏览器测试和
|
仓库包含 `.gitea/workflows/frontend.yml`:安装锁定依赖后先运行 ESLint 和格式检查,再安装测试浏览器、运行源码检查、生产构建、浏览器测试和
|
||||||
`npm audit --audit-level=moderate`,不执行部署。Git 服务需启用 Actions 并配置兼容的 Linux runner;
|
`npm audit --audit-level=moderate`,不执行部署。Git 服务需启用 Actions 并配置兼容的 Linux runner;
|
||||||
仅提交工作流不代表远端任务已经运行通过。
|
仅提交工作流不代表远端任务已经运行通过。
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ var fe = {
|
|||||||
let t = M.value.flatMap((e, t) => (e.enabled ? [t] : []));
|
let t = M.value.flatMap((e, t) => (e.enabled ? [t] : []));
|
||||||
if (!t.length) return;
|
if (!t.length) return;
|
||||||
let n = t.indexOf(N.value),
|
let n = t.indexOf(N.value),
|
||||||
r = n;
|
r;
|
||||||
if (e.key === `ArrowRight`) r = (n + 1) % t.length;
|
if (e.key === `ArrowRight`) r = (n + 1) % t.length;
|
||||||
else if (e.key === `ArrowLeft`) r = (n + t.length - 1) % t.length;
|
else if (e.key === `ArrowLeft`) r = (n + t.length - 1) % t.length;
|
||||||
else if (e.key === `Home`) r = 0;
|
else if (e.key === `Home`) r = 0;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ var f = [`src`, `width`, `height`],
|
|||||||
// 仅接收站内绝对路径或无认证信息的 HTTP(S) 地址,拒绝控制字符和反斜杠。
|
// 仅接收站内绝对路径或无认证信息的 HTTP(S) 地址,拒绝控制字符和反斜杠。
|
||||||
function m(e) {
|
function m(e) {
|
||||||
let t = String(e ?? ``).trim();
|
let t = String(e ?? ``).trim();
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:拒绝图标 URL 内的控制字符。
|
||||||
if (!t || /[\u0000-\u0020\u007f\\]/.test(t)) return ``;
|
if (!t || /[\u0000-\u0020\u007f\\]/.test(t)) return ``;
|
||||||
if (/^\/(?!\/)/.test(t)) return t;
|
if (/^\/(?!\/)/.test(t)) return t;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1002,7 +1002,7 @@ var le = {
|
|||||||
let Qe = computed(() =>
|
let Qe = computed(() =>
|
||||||
Ie.value.map((e) => {
|
Ie.value.map((e) => {
|
||||||
let t = N.value[String(e.id)],
|
let t = N.value[String(e.id)],
|
||||||
n = ``;
|
n;
|
||||||
if (e.option_type === `cascade`) {
|
if (e.option_type === `cascade`) {
|
||||||
let t = Re(e);
|
let t = Re(e);
|
||||||
n = Z(e).find((e) => e.item_id === t)?.label ?? ``;
|
n = Z(e).find((e) => e.item_id === t)?.label ?? ``;
|
||||||
|
|||||||
@@ -836,7 +836,7 @@ var ae = {
|
|||||||
let Ze = computed(() =>
|
let Ze = computed(() =>
|
||||||
(O.availableOptions?.value ?? F.value?.options ?? []).map((e) => {
|
(O.availableOptions?.value ?? F.value?.options ?? []).map((e) => {
|
||||||
let t = I.value[e.id],
|
let t = I.value[e.id],
|
||||||
n = ``;
|
n;
|
||||||
if (e.kind === `quantity`) {
|
if (e.kind === `quantity`) {
|
||||||
let r = (Ne(e) ? Le(e) : ``) || e.unit;
|
let r = (Ne(e) ? Le(e) : ``) || e.unit;
|
||||||
n = typeof t == `number` ? `${t}${r ? ` ${r}` : ``}` : ``;
|
n = typeof t == `number` ? `${t}${r ? ` ${r}` : ``}` : ``;
|
||||||
|
|||||||
@@ -632,6 +632,7 @@ function W(e, t) {
|
|||||||
return;
|
return;
|
||||||
(W(), (N.value = ``));
|
(W(), (N.value = ``));
|
||||||
let t = L,
|
let t = L,
|
||||||
|
// eslint-disable-next-line no-useless-assignment -- 边界一致性:保留 try 外 computed 求值及异常传播顺序。
|
||||||
r = B.value;
|
r = B.value;
|
||||||
try {
|
try {
|
||||||
(G(), (t = L), (r = B.value));
|
(G(), (t = L), (r = B.value));
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ function e(e) {
|
|||||||
typeof e != `string` ||
|
typeof e != `string` ||
|
||||||
!e.startsWith(`/`) ||
|
!e.startsWith(`/`) ||
|
||||||
e.startsWith(`//`) ||
|
e.startsWith(`//`) ||
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:拒绝跳转路径中的控制字符。
|
||||||
/[\\\u0000-\u0020\u007f]/.test(e)
|
/[\\\u0000-\u0020\u007f]/.test(e)
|
||||||
)
|
)
|
||||||
return `/`;
|
return `/`;
|
||||||
try {
|
try {
|
||||||
// 路径解码后再次检查,避免编码后的双斜杠或反斜杠绕过站内限制。
|
// 路径解码后再次检查,避免编码后的双斜杠或反斜杠绕过站内限制。
|
||||||
let t = decodeURIComponent(e.split(/[?#]/, 1)[0] || ``);
|
let t = decodeURIComponent(e.split(/[?#]/, 1)[0] || ``);
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:解码后仍须拒绝控制字符绕过。
|
||||||
if (t.startsWith(`//`) || /[\\\u0000-\u0020\u007f]/.test(t)) return `/`;
|
if (t.startsWith(`//`) || /[\\\u0000-\u0020\u007f]/.test(t)) return `/`;
|
||||||
} catch {
|
} catch {
|
||||||
return `/`;
|
return `/`;
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ function d(e) {
|
|||||||
if (
|
if (
|
||||||
typeof e != `string` ||
|
typeof e != `string` ||
|
||||||
!/^https?:\/\//i.test(e) ||
|
!/^https?:\/\//i.test(e) ||
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:拒绝菜单 URL 内的控制字符。
|
||||||
/[\u0000-\u0020\\]/.test(e)
|
/[\u0000-\u0020\\]/.test(e)
|
||||||
)
|
)
|
||||||
return ``;
|
return ``;
|
||||||
|
|||||||
@@ -104,10 +104,10 @@ function o(t, r, i) {
|
|||||||
[`available`, `none`, `not_applicable`].includes(
|
[`available`, `none`, `not_applicable`].includes(
|
||||||
String(s.quota_status ?? ``),
|
String(s.quota_status ?? ``),
|
||||||
),
|
),
|
||||||
x = null,
|
x,
|
||||||
S = null,
|
S,
|
||||||
C = null,
|
C = null,
|
||||||
w = ``,
|
w,
|
||||||
T = !1,
|
T = !1,
|
||||||
E = t === `kky_ecs` && !!s.start_time && !!s.end_time,
|
E = t === `kky_ecs` && !!s.start_time && !!s.end_time,
|
||||||
D = E ? a(s.incoming) : null;
|
D = E ? a(s.incoming) : null;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ var e = {
|
|||||||
/** 优先采用合法站内/HTTP(S)图片地址;否则从 e/n 的系统描述推断本地图标。 */
|
/** 优先采用合法站内/HTTP(S)图片地址;否则从 e/n 的系统描述推断本地图标。 */
|
||||||
function t(e, t, n) {
|
function t(e, t, n) {
|
||||||
let r = String(e ?? ``).trim();
|
let r = String(e ?? ``).trim();
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:拒绝模块 URL 内的控制字符。
|
||||||
if (r && !/[\u0000-\u0020\u007f\\]/.test(r)) {
|
if (r && !/[\u0000-\u0020\u007f\\]/.test(r)) {
|
||||||
if (/^\/(?!\/)/.test(r)) return r;
|
if (/^\/(?!\/)/.test(r)) return r;
|
||||||
try {
|
try {
|
||||||
|
|||||||
+3
-1
@@ -13,6 +13,7 @@ function t(t) {
|
|||||||
return [];
|
return [];
|
||||||
let n = typeof t.img == `string` ? t.img : ``,
|
let n = typeof t.img == `string` ? t.img : ``,
|
||||||
r =
|
r =
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:拒绝登录图标路径中的控制字符。
|
||||||
/^\/(?!\/)/.test(n) && !/[\\\u0000-\u0020]/.test(n)
|
/^\/(?!\/)/.test(n) && !/[\\\u0000-\u0020]/.test(n)
|
||||||
? n
|
? n
|
||||||
: backend_menu_o(n);
|
: backend_menu_o(n);
|
||||||
@@ -29,7 +30,8 @@ function t(t) {
|
|||||||
}
|
}
|
||||||
function n(t, n) {
|
function n(t, n) {
|
||||||
return typeof t == `string`
|
return typeof t == `string`
|
||||||
? /^\/(?!\/)/.test(t) && !/[\\\u0000-\u0020]/.test(t)
|
? // eslint-disable-next-line no-control-regex -- 安全边界:拒绝登录跳转地址中的控制字符。
|
||||||
|
/^\/(?!\/)/.test(t) && !/[\\\u0000-\u0020]/.test(t)
|
||||||
? new URL(t, n).href
|
? new URL(t, n).href
|
||||||
: backend_menu_o(t)
|
: backend_menu_o(t)
|
||||||
: ``;
|
: ``;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function n(e) {
|
|||||||
return (
|
return (
|
||||||
t.startsWith(`/`) &&
|
t.startsWith(`/`) &&
|
||||||
!t.startsWith(`//`) &&
|
!t.startsWith(`//`) &&
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:拒绝订单续接路径中的控制字符。
|
||||||
!/[\\\u0000-\u0020\u007f]/.test(t)
|
!/[\\\u0000-\u0020\u007f]/.test(t)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -24,6 +25,7 @@ function r(e, r) {
|
|||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
typeof e != `string` ||
|
typeof e != `string` ||
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:解码后仍须拒绝控制字符绕过。
|
||||||
/[\\\u0000-\u0020\u007f]/.test(e) ||
|
/[\\\u0000-\u0020\u007f]/.test(e) ||
|
||||||
!n(e.split(/[?#]/, 1)[0] || ``)
|
!n(e.split(/[?#]/, 1)[0] || ``)
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-2
@@ -44,7 +44,7 @@ function n(n) {
|
|||||||
a = !1,
|
a = !1,
|
||||||
o = [e[r[0].replace(/[ _-]+/g, ` `).toLowerCase()]],
|
o = [e[r[0].replace(/[ _-]+/g, ` `).toLowerCase()]],
|
||||||
s =
|
s =
|
||||||
/^[\s_-]*(x86[_-]64|64[ _-]?(?:bits?|位)|32[ _-]?(?:bits?|位)|\d+(?:\.\d+)*(?:R2)?|Server|Stream|Datacenter|Standard|Enterprise|Professional|Essentials|Education|Desktop|Minimal|Core|Pro|Home|R2|LTS|SP\d+|UEFI|BIOS|GPT|BT|ARM64|aarch64|x64|amd64|x86|x32|i386|i686|ARM|cn|en|zh(?:[ _-](?:CN|TW))?|专业版|标准版|数据中心版|企业版|家庭版|教育版|中文版|英文版)(?=$|[\s_()()\[\]【】-])/i;
|
/^[\s_-]*(x86[_-]64|64[ _-]?(?:bits?|位)|32[ _-]?(?:bits?|位)|\d+(?:\.\d+)*(?:R2)?|Server|Stream|Datacenter|Standard|Enterprise|Professional|Essentials|Education|Desktop|Minimal|Core|Pro|Home|R2|LTS|SP\d+|UEFI|BIOS|GPT|BT|ARM64|aarch64|x64|amd64|x86|x32|i386|i686|ARM|cn|en|zh(?:[ _-](?:CN|TW))?|专业版|标准版|数据中心版|企业版|家庭版|教育版|中文版|英文版)(?=$|[\s_()()[\]【】-])/i;
|
||||||
for (let e = i.match(s); e; e = i.match(s)) {
|
for (let e = i.match(s); e; e = i.match(s)) {
|
||||||
let n = t[e[1].toLowerCase()] ?? e[1];
|
let n = t[e[1].toLowerCase()] ?? e[1];
|
||||||
(/^\d+(?:\.\d+)*(?:R2)?$/i.test(n) &&
|
(/^\d+(?:\.\d+)*(?:R2)?$/i.test(n) &&
|
||||||
@@ -67,7 +67,7 @@ function r(e) {
|
|||||||
return (
|
return (
|
||||||
n(t.replace(/\.(?:iso|qcow2?|img|raw|vhdx?|vmdk)$/i, ``))
|
n(t.replace(/\.(?:iso|qcow2?|img|raw|vhdx?|vmdk)$/i, ``))
|
||||||
.replace(
|
.replace(
|
||||||
/[\s_-]*(?:[((【\[]\s*)?纯净优化版(?:\s*[))】\]])?(?=$|[\s_-])/g,
|
/[\s_-]*(?:[((【[]\s*)?纯净优化版(?:\s*[))】\]])?(?=$|[\s_-])/g,
|
||||||
``,
|
``,
|
||||||
)
|
)
|
||||||
.replace(RegExp(`[((\\[【]\\s*${r}\\s*[))\\]】]`, `gi`), ` `)
|
.replace(RegExp(`[((\\[【]\\s*${r}\\s*[))\\]】]`, `gi`), ` `)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ var e = new Set(
|
|||||||
]);
|
]);
|
||||||
// 图片额外允许有限位图类型的 base64 data URL;普通链接可用 mailto/tel,不允许脚本协议。
|
// 图片额外允许有限位图类型的 base64 data URL;普通链接可用 mailto/tel,不允许脚本协议。
|
||||||
function n(e, t = !1) {
|
function n(e, t = !1) {
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:拒绝富文本链接和图片中的控制字符。
|
||||||
if (!e || /[\u0000-\u001f\u007f]/.test(e)) return !1;
|
if (!e || /[\u0000-\u001f\u007f]/.test(e)) return !1;
|
||||||
if (
|
if (
|
||||||
t &&
|
t &&
|
||||||
|
|||||||
@@ -465,7 +465,7 @@ function C(e, t = ``) {
|
|||||||
if (!n || /[×*¥¥$€]|(?:^|[\s([{])[-+]\s*\d/.test(n)) return null;
|
if (!n || /[×*¥¥$€]|(?:^|[\s([{])[-+]\s*\d/.test(n)) return null;
|
||||||
let r = [...n.matchAll(/\d+(?:\.\d+)?\s*(?:[kmgtpe](?:i?b)?|b)(?![a-z])/gi)],
|
let r = [...n.matchAll(/\d+(?:\.\d+)?\s*(?:[kmgtpe](?:i?b)?|b)(?![a-z])/gi)],
|
||||||
i = r[0],
|
i = r[0],
|
||||||
a = ``;
|
a;
|
||||||
if (r.length === 1) a = S(/[a-z]+$/i.exec(i[0])?.[0] ?? ``);
|
if (r.length === 1) a = S(/[a-z]+$/i.exec(i[0])?.[0] ?? ``);
|
||||||
else if (r.length === 0 && S(t)) {
|
else if (r.length === 0 && S(t)) {
|
||||||
let e = [...n.matchAll(/\d+(?:\.\d+)?/g)];
|
let e = [...n.matchAll(/\d+(?:\.\d+)?/g)];
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
function e(e) {
|
function e(e) {
|
||||||
if (
|
if (
|
||||||
!/^(?:https?:\/\/|\/(?!\/)|\.\.?\/)/i.test(e) ||
|
!/^(?:https?:\/\/|\/(?!\/)|\.\.?\/)/i.test(e) ||
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:拒绝附件 URL 内的控制字符。
|
||||||
/[\u0000-\u001f\u007f\\]/.test(e)
|
/[\u0000-\u001f\u007f\\]/.test(e)
|
||||||
)
|
)
|
||||||
return !1;
|
return !1;
|
||||||
@@ -56,6 +57,7 @@ function r(t) {
|
|||||||
u = c || (l ? n(l, i) : ``),
|
u = c || (l ? n(l, i) : ``),
|
||||||
d =
|
d =
|
||||||
l ||
|
l ||
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:拒绝附件名中的控制字符。
|
||||||
(typeof o == `string` && !e(o) && !/[\\/:\u0000-\u001f]/.test(o)
|
(typeof o == `string` && !e(o) && !/[\\/:\u0000-\u001f]/.test(o)
|
||||||
? o
|
? o
|
||||||
: ``),
|
: ``),
|
||||||
|
|||||||
@@ -402,6 +402,7 @@ function o(e) {
|
|||||||
function s(e) {
|
function s(e) {
|
||||||
if (typeof e != `string`) return `#`;
|
if (typeof e != `string`) return `#`;
|
||||||
let t = e.trim();
|
let t = e.trim();
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:拒绝官网链接内的控制字符。
|
||||||
return !t || /[\u0000-\u001f\\]/.test(t)
|
return !t || /[\u0000-\u001f\\]/.test(t)
|
||||||
? `#`
|
? `#`
|
||||||
: /^(https?:\/\/|mailto:|tel:|#|\/(?!\/))/i.test(t)
|
: /^(https?:\/\/|mailto:|tel:|#|\/(?!\/))/i.test(t)
|
||||||
@@ -412,6 +413,7 @@ function s(e) {
|
|||||||
function c(e) {
|
function c(e) {
|
||||||
if (typeof e != `string`) return ``;
|
if (typeof e != `string`) return ``;
|
||||||
let t = e.trim();
|
let t = e.trim();
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:拒绝官网图片 URL 内的控制字符。
|
||||||
return !t || /[\u0000-\u001f\\]/.test(t)
|
return !t || /[\u0000-\u001f\\]/.test(t)
|
||||||
? ``
|
? ``
|
||||||
: /^(https?:\/\/|\/(?!\/))/i.test(t)
|
: /^(https?:\/\/|\/(?!\/))/i.test(t)
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ var Ce = {
|
|||||||
];
|
];
|
||||||
// 为账户设置标签提供左右方向键及 Home/End 导航,并同步实际焦点。
|
// 为账户设置标签提供左右方向键及 Home/End 导航,并同步实际焦点。
|
||||||
function Xt(e, t) {
|
function Xt(e, t) {
|
||||||
let n = t;
|
let n;
|
||||||
if (e.key === `ArrowRight`) n = (t + 1) % E.length;
|
if (e.key === `ArrowRight`) n = (t + 1) % E.length;
|
||||||
else if (e.key === `ArrowLeft`) n = (t + E.length - 1) % E.length;
|
else if (e.key === `ArrowLeft`) n = (t + E.length - 1) % E.length;
|
||||||
else if (e.key === `Home`) n = 0;
|
else if (e.key === `Home`) n = 0;
|
||||||
|
|||||||
@@ -387,6 +387,7 @@ var xe = {
|
|||||||
Tt = computed(() => {
|
Tt = computed(() => {
|
||||||
let e = R.value?.certification_auth_template_url || ``;
|
let e = R.value?.certification_auth_template_url || ``;
|
||||||
return /^(?:https?:\/\/|\/(?!\/))/i.test(e) &&
|
return /^(?:https?:\/\/|\/(?!\/))/i.test(e) &&
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:拒绝认证模板 URL 内的控制字符。
|
||||||
!/[\\\u0000-\u0020]/.test(e)
|
!/[\\\u0000-\u0020]/.test(e)
|
||||||
? e
|
? e
|
||||||
: ``;
|
: ``;
|
||||||
|
|||||||
@@ -127,9 +127,6 @@ var je = {
|
|||||||
Xe = {
|
Xe = {
|
||||||
class: `amount`,
|
class: `amount`,
|
||||||
},
|
},
|
||||||
Ze = {
|
|
||||||
class: `consumption-overview`,
|
|
||||||
},
|
|
||||||
x = _plugin_vue_export_helper_t(
|
x = _plugin_vue_export_helper_t(
|
||||||
defineComponent({
|
defineComponent({
|
||||||
__name: `index`,
|
__name: `index`,
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ var T = {
|
|||||||
);
|
);
|
||||||
// 下载地址仅允许 HTTP(S) 或站内绝对路径,拒绝反斜杠、空白控制字符和协议相对地址。
|
// 下载地址仅允许 HTTP(S) 或站内绝对路径,拒绝反斜杠、空白控制字符和协议相对地址。
|
||||||
function q(e) {
|
function q(e) {
|
||||||
|
// eslint-disable-next-line no-control-regex -- 安全边界:拒绝发票下载地址中的控制字符。
|
||||||
return typeof e != `string` || /[\u0000-\u0020\\]/.test(e)
|
return typeof e != `string` || /[\u0000-\u0020\\]/.test(e)
|
||||||
? ``
|
? ``
|
||||||
: /^https?:\/\//i.test(e) || /^\/(?!\/)/.test(e)
|
: /^https?:\/\//i.test(e) || /^\/(?!\/)/.test(e)
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ var ue = {
|
|||||||
createElementVNode(
|
createElementVNode(
|
||||||
`p`,
|
`p`,
|
||||||
ge,
|
ge,
|
||||||
`发布时间 ` +
|
`发布时间\u3000` +
|
||||||
toDisplayString(
|
toDisplayString(
|
||||||
(`formatTime` in n
|
(`formatTime` in n
|
||||||
? n.formatTime
|
? n.formatTime
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ var j = {
|
|||||||
position: K.value,
|
position: K.value,
|
||||||
item: U.value,
|
item: U.value,
|
||||||
};
|
};
|
||||||
|
return undefined;
|
||||||
}),
|
}),
|
||||||
J = ref(!1),
|
J = ref(!1),
|
||||||
Y = ref(null),
|
Y = ref(null),
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ var j = {
|
|||||||
}));
|
}));
|
||||||
// 标签按左右方向键循环切换,Home/End 跳至首尾,并同步焦点到新标签。
|
// 标签按左右方向键循环切换,Home/End 跳至首尾,并同步焦点到新标签。
|
||||||
function G(e, t) {
|
function G(e, t) {
|
||||||
let n = t;
|
let n;
|
||||||
if (e.key === `ArrowRight`) n = (t + 1) % V.length;
|
if (e.key === `ArrowRight`) n = (t + 1) % V.length;
|
||||||
else if (e.key === `ArrowLeft`) n = (t + V.length - 1) % V.length;
|
else if (e.key === `ArrowLeft`) n = (t + V.length - 1) % V.length;
|
||||||
else if (e.key === `Home`) n = 0;
|
else if (e.key === `Home`) n = 0;
|
||||||
|
|||||||
@@ -61,9 +61,6 @@ var le = {
|
|||||||
me = {
|
me = {
|
||||||
class: `ticket-title`,
|
class: `ticket-title`,
|
||||||
},
|
},
|
||||||
he = {
|
|
||||||
class: `ticket-num`,
|
|
||||||
},
|
|
||||||
ge = {
|
ge = {
|
||||||
class: `ticket-meta`,
|
class: `ticket-meta`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import ts from "typescript-eslint";
|
||||||
|
import vue from "eslint-plugin-vue";
|
||||||
|
import globals from "globals";
|
||||||
|
import prettier from "eslint-config-prettier/flat";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"node_modules/**",
|
||||||
|
".nuxt/**",
|
||||||
|
".output/**",
|
||||||
|
".data/**",
|
||||||
|
"test-results/**",
|
||||||
|
"tests/fixtures/runs/**",
|
||||||
|
"docs/**",
|
||||||
|
".claude/skills/crashwork-ops/**",
|
||||||
|
"public/**",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
linterOptions: { reportUnusedDisableDirectives: "error" },
|
||||||
|
},
|
||||||
|
js.configs.recommended,
|
||||||
|
...vue.configs["flat/essential"],
|
||||||
|
...ts.configs.recommended.map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ["**/*.{ts,mts,cts}"],
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
files: ["app/**/*.{js,vue}"],
|
||||||
|
languageOptions: { globals: globals.browser },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["*.mjs", "nuxt.config.ts", "scripts/**/*.mjs", "tests/**/*.mjs"],
|
||||||
|
languageOptions: { globals: globals.node },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Playwright 的 evaluate 回调在浏览器运行,测试文件同时使用 Node 与 DOM 全局。
|
||||||
|
files: ["tests/**/*.mjs"],
|
||||||
|
languageOptions: { globals: globals.browser },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Nuxt 路由和布局文件名由框架约定,不能要求多词组件名。
|
||||||
|
files: ["app/pages/**/*.vue", "app/layouts/**/*.vue"],
|
||||||
|
rules: { "vue/multi-word-component-names": "off" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 恢复的互操作辅助只在宿主提供 require 时访问它,不为浏览器注入 Node 全局。
|
||||||
|
files: ["app/lib/rolldown-runtime.js"],
|
||||||
|
languageOptions: { globals: { require: "readonly" } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["nuxt.config.ts"],
|
||||||
|
languageOptions: { globals: { defineNuxtConfig: "readonly" } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 最佳努力清理和存储降级允许空 catch;其他空块仍报错。
|
||||||
|
rules: {
|
||||||
|
"no-empty": ["error", { allowEmptyCatch: true }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.{js,mjs,cjs,vue}"],
|
||||||
|
rules: {
|
||||||
|
// 恢复的渲染回调保留位置参数,catch 也可不使用错误绑定;局部绑定仍检查。
|
||||||
|
"no-unused-vars": ["error", { args: "none", caughtErrors: "none" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prettier,
|
||||||
|
];
|
||||||
Generated
+1257
-1
File diff suppressed because it is too large.
Load diff
+11
-2
@@ -9,7 +9,10 @@
|
|||||||
"preview": "node .output/server/index.mjs",
|
"preview": "node .output/server/index.mjs",
|
||||||
"prepare": "nuxt prepare",
|
"prepare": "nuxt prepare",
|
||||||
"check": "node scripts/check-source.mjs",
|
"check": "node scripts/check-source.mjs",
|
||||||
"format": "prettier --write app nuxt.config.ts tests scripts",
|
"lint": "eslint . --max-warnings 0",
|
||||||
|
"lint:fix": "eslint . --fix --max-warnings 0",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
"test": "node --test tests/*.test.mjs"
|
"test": "node --test tests/*.test.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -34,9 +37,15 @@
|
|||||||
"@babel/parser": "7.29.8",
|
"@babel/parser": "7.29.8",
|
||||||
"@babel/traverse": "^7.29.0",
|
"@babel/traverse": "^7.29.0",
|
||||||
"@babel/types": "^7.29.0",
|
"@babel/types": "^7.29.0",
|
||||||
|
"@eslint/js": "10.0.1",
|
||||||
"@vue/compiler-sfc": "3.5.45",
|
"@vue/compiler-sfc": "3.5.45",
|
||||||
|
"eslint": "10.14.0",
|
||||||
|
"eslint-config-prettier": "10.1.8",
|
||||||
|
"eslint-plugin-vue": "10.12.0",
|
||||||
|
"globals": "17.14.0",
|
||||||
"playwright": "^1.64.1",
|
"playwright": "^1.64.1",
|
||||||
"prettier": "^3.8.0"
|
"prettier": "3.9.10",
|
||||||
|
"typescript-eslint": "8.72.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"glob": "14.0.3",
|
"glob": "14.0.3",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
# 浏览器端到端验收
|
# 浏览器端到端验收
|
||||||
|
|
||||||
先在项目根目录执行 `npm ci` 安装锁定依赖,执行 `npm run build`,再执行 `npm test`。
|
先在项目根目录执行 `npm ci` 安装锁定依赖,执行 `npm run check`、`npm run lint`、
|
||||||
|
`npm run format:check` 和 `npm run build`,再执行 `npm test`。
|
||||||
测试使用 Node 自带的 `node:test`。浏览器场景直接操作实际构建后的页面,不替换项目内部模块;
|
测试使用 Node 自带的 `node:test`。浏览器场景直接操作实际构建后的页面,不替换项目内部模块;
|
||||||
规则适配与随机密码另有公开模块入口的隔离失败模式测试,不能代替浏览器验收。
|
规则适配与随机密码另有公开模块入口的隔离失败模式测试,不能代替浏览器验收。
|
||||||
|
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ export async function scenario(t, options, exercise) {
|
|||||||
const screenshot = `${t.name.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}.png`;
|
const screenshot = `${t.name.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}.png`;
|
||||||
let failure;
|
let failure;
|
||||||
let observed;
|
let observed;
|
||||||
let requests = [];
|
let requests;
|
||||||
try {
|
try {
|
||||||
observed = await exercise(page);
|
observed = await exercise(page);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in new issue
Block a user