build(frontend): Element Plus 2.3.1 -> 2.14.5, sass 1.58 -> 1.103, icons 1.x -> 2.3.2
Element Plus с 2.8.5 требует sass >= 1.79, поэтому обновление идёт классом:
element-plus + sass + @element-plus/icons-vue, отдельным коммитом от Vue и
typechecker — UI-регрессию так проще локализовать.
Обновление внесло единственный новый класс ошибок типов — тот самый
DefaultRow/PeerVo, который аудит ожидал увидеть сразу: до 2.14 слоты таблицы
типизировались слабее и ошибки не давали. 10 ошибок в семи колонках списка
пиров.
el-table обобщён по типу строки, но el-table-column — отдельный компонент, и
тип из :data родительской таблицы в его слот не попадает: scope всегда
{ row: DefaultRow }. Аннотировать слот нельзя — DefaultRow не сужается до PeerVo
контравариантно. Поэтому переход нужен, и вопрос только в том, где он стоит:
здесь он ровно один (peerRow), назван и объяснён, вместо семи `as any` в
шаблоне, каждый из которых глушил бы и остальное выражение.
PeerVo переведён с interface на псевдоним типа: неявную индексную сигнатуру
TypeScript даёт литеральным типам, но не интерфейсам, поэтому раньше PeerVo и
DefaultRow не были совместимы ни в одну сторону и переход требовал `as unknown
as` — утверждения, которое компилятор не проверяет вообще.
typecheck: 0, eslint: 0, build: OK.
This commit is contained in:
@@ -50,8 +50,8 @@ import type {
|
||||
type DeepRequired<T> = T extends (infer U)[]
|
||||
? DeepRequired<U>[]
|
||||
: T extends object
|
||||
? { [K in keyof T]-?: DeepRequired<NonNullable<T[K]>> }
|
||||
: T;
|
||||
? { [K in keyof T]-?: DeepRequired<NonNullable<T[K]>> }
|
||||
: T;
|
||||
|
||||
/** Конфиг Hysteria в том виде, в котором его показывает панель. */
|
||||
export type Hysteria2ServerConfigView = DeepRequired<Hysteria2ServerConfig>;
|
||||
@@ -218,9 +218,7 @@ export const defaultHysteria2ServerConfigView: Hysteria2ServerConfigView = {
|
||||
};
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return (
|
||||
typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
);
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,4 +48,3 @@ export function exportHysteria2ConfigApi(): AxiosPromise {
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,28 @@ export interface PeerSaveDto {
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface PeerVo extends IdDto {
|
||||
/**
|
||||
* Строка списка пиров.
|
||||
*
|
||||
* Объявлена через `type`, а не `interface`, и это не стилистика.
|
||||
*
|
||||
* `el-table-column` типизирует scope своего default-слота как
|
||||
* `{ row: DefaultRow, ... }`, где `DefaultRow = Record<PropertyKey, any>`:
|
||||
* колонка — отдельный компонент, и тип строки из `:data` родительской таблицы
|
||||
* в неё не попадает. Значит, где-то на пути от слота к обработчику нужен ровно
|
||||
* один переход `DefaultRow -> PeerVo` (см. `peerRow` в списке пиров).
|
||||
*
|
||||
* TypeScript даёт неявную индексную сигнатуру псевдонимам типов, но НЕ
|
||||
* интерфейсам и не пересечениям с ними. Пока `PeerVo` был `interface`, он не
|
||||
* был совместим с `DefaultRow` ни в одну сторону, и этот переход требовал
|
||||
* двойного приведения через `unknown` — то есть утверждения, которое компилятор
|
||||
* вообще не проверяет. С псевдонимом типа переход остаётся одним обычным
|
||||
* приведением между связанными типами.
|
||||
*/
|
||||
export type PeerVo = {
|
||||
// `id` объявлен здесь, а не унаследован от `IdDto`: пересечение с интерфейсом
|
||||
// неявную индексную сигнатуру не получает.
|
||||
id: number;
|
||||
name: string;
|
||||
remark: string;
|
||||
authId: string;
|
||||
@@ -39,7 +60,7 @@ export interface PeerVo extends IdDto {
|
||||
createTime: string;
|
||||
online: boolean;
|
||||
onlineDevices: number;
|
||||
}
|
||||
};
|
||||
|
||||
export interface PeerClientConfigVo {
|
||||
url: string;
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
}
|
||||
|
||||
.peg {
|
||||
box-shadow:
|
||||
0 0 10px var(--el-color-primary),
|
||||
box-shadow: 0 0 10px var(--el-color-primary),
|
||||
0 0 5px var(--el-color-primary);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,12 @@
|
||||
$t("peer.exportSettings")
|
||||
}}</el-button>
|
||||
</el-tooltip>
|
||||
<el-button class="ml-2" type="warning" plain @click="handleExportBackup">
|
||||
<el-button
|
||||
class="ml-2"
|
||||
type="warning"
|
||||
plain
|
||||
@click="handleExportBackup"
|
||||
>
|
||||
{{ $t("peer.exportBackup") }}
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -74,11 +79,11 @@
|
||||
/ {{ quotaText(scope.row.quotaBytes) }}
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="trafficPercent(scope.row)"
|
||||
:percentage="trafficPercent(peerRow(scope.row))"
|
||||
:status="
|
||||
scope.row.quotaBytes < 0
|
||||
? undefined
|
||||
: trafficPercent(scope.row) >= 90
|
||||
: trafficPercent(peerRow(scope.row)) >= 90
|
||||
? 'exception'
|
||||
: undefined
|
||||
"
|
||||
@@ -115,36 +120,45 @@
|
||||
>
|
||||
<template #default="scope">
|
||||
<div class="peer-actions">
|
||||
<el-button link type="primary" @click="openOverview(scope.row)">{{
|
||||
$t("peer.overview")
|
||||
}}</el-button>
|
||||
<el-button link type="primary" @click="copyUri(scope.row)">{{
|
||||
$t("peer.copyUri")
|
||||
}}</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openOverview(peerRow(scope.row))"
|
||||
>{{ $t("peer.overview") }}</el-button
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="copyUri(peerRow(scope.row))"
|
||||
>{{ $t("peer.copyUri") }}</el-button
|
||||
>
|
||||
<el-dropdown trigger="click" placement="bottom-end">
|
||||
<el-button text circle class="peer-more-btn" @click.stop>
|
||||
<i-ep-more-filled />
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="showQr(scope.row)">{{
|
||||
<el-dropdown-item @click="showQr(peerRow(scope.row))">{{
|
||||
$t("peer.clientQr")
|
||||
}}</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleUpdate(scope.row)">{{
|
||||
$t("common.edit")
|
||||
}}</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleResetTraffic(scope.row)">{{
|
||||
$t("common.resetTraffic")
|
||||
}}</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleKick(scope.row)">{{
|
||||
<el-dropdown-item
|
||||
@click="handleUpdate(peerRow(scope.row))"
|
||||
>{{ $t("common.edit") }}</el-dropdown-item
|
||||
>
|
||||
<el-dropdown-item
|
||||
@click="handleResetTraffic(peerRow(scope.row))"
|
||||
>{{ $t("common.resetTraffic") }}</el-dropdown-item
|
||||
>
|
||||
<el-dropdown-item @click="handleKick(peerRow(scope.row))">{{
|
||||
$t("peer.kick")
|
||||
}}</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleReleaseKick(scope.row)">{{
|
||||
$t("peer.releaseKick")
|
||||
}}</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
@click="handleReleaseKick(peerRow(scope.row))"
|
||||
>{{ $t("peer.releaseKick") }}</el-dropdown-item
|
||||
>
|
||||
<el-dropdown-item
|
||||
divided
|
||||
@click="handleDelete(scope.row)"
|
||||
@click="handleDelete(peerRow(scope.row))"
|
||||
>{{ $t("common.delete") }}</el-dropdown-item
|
||||
>
|
||||
</el-dropdown-menu>
|
||||
@@ -356,6 +370,28 @@ import {
|
||||
} from "@/api/peer/types";
|
||||
import { UploadFile, UploadRawFile, UploadRequestOptions } from "element-plus";
|
||||
|
||||
/**
|
||||
* Единственный переход от строки слота таблицы к модели пира.
|
||||
*
|
||||
* `el-table-column` — отдельный компонент, и тип строки из `:data` родительской
|
||||
* таблицы в его слот не попадает: scope всегда типизирован как
|
||||
* `{ row: DefaultRow, ... }`, где `DefaultRow = Record<PropertyKey, any>`.
|
||||
* Обобщённость `el-table` здесь не помогает, а аннотировать слот нельзя —
|
||||
* `DefaultRow` не сужается до `PeerVo` контравариантно.
|
||||
*
|
||||
* Поэтому переход нужен, и вопрос только в том, где он стоит. Здесь он ровно
|
||||
* один, назван и объяснён; альтернативой были семь `as any` прямо в шаблоне,
|
||||
* каждый из которых отключал бы проверку и всего остального выражения.
|
||||
*
|
||||
* Утверждение опирается на соседнюю строку шаблона: `:data="records"`, где
|
||||
* `records` объявлен как `Ref<PeerVo[]>`. Таблица не получает никаких других
|
||||
* данных, поэтому в `row` не может оказаться ничего другого. Смена типа
|
||||
* `records` сломает компиляцию здесь же: `PeerVo` объявлен псевдонимом типа и
|
||||
* потому совместим с `DefaultRow`, так что это обычное приведение между
|
||||
* связанными типами, а не `as unknown as`.
|
||||
*/
|
||||
const peerRow = (row: Record<PropertyKey, any>): PeerVo => row as PeerVo;
|
||||
|
||||
const { t } = useI18n();
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
|
||||
Reference in New Issue
Block a user