style: 统一Admin JSDoc规范

This commit is contained in:
sunlei 2026-07-25 09:51:27 +08:00
parent 1d07233114
commit 523f96c3b2
199 changed files with 968 additions and 4488 deletions

View File

@ -1,8 +1,3 @@
/**
* 使 adapter/form 使便使
* vben-formvben-modalvben-drawer 使,
*/
/* eslint-disable vue/one-component-per-file */
import type { UploadChangeParam, UploadFile, UploadProps } from 'antdv-next';

View File

@ -10,12 +10,6 @@ type BlogPreviewEnv = {
VITE_KT_BLOG_WEB_BASE_URL?: string;
};
/**
* Builds the public KT Blog Web URL used by the Admin preview iframe.
* @param article Article data whose slug is the preferred public post route key.
* @param articleId Admin article id retained in the query string for preview traceability.
* @returns Absolute or same-origin iframe URL for the KT Blog Web post route.
*/
export function buildKtBlogPreviewUrl(
article: BlogPreviewArticle,
articleId: string,
@ -33,11 +27,6 @@ export function buildKtBlogPreviewUrl(
return url.toString();
}
/**
* Resolves the Blog Web preview origin and refuses silent localhost fallback in production.
* @param env Vite import meta environment; tests pass a small object to cover production guards.
* @returns Configured Blog Web base URL, or the local development default outside production.
*/
export function resolveKtBlogWebBaseUrl(env: BlogPreviewEnv) {
const configured = env.VITE_KT_BLOG_WEB_BASE_URL?.trim();
if (configured) {

View File

@ -1,7 +1,6 @@
import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
export interface LoginParams {
password?: string;
username?: string;
@ -12,7 +11,6 @@ export namespace AuthApi {
username?: string;
}
/** 登录接口返回值 */
export interface LoginResult {
accessToken: string;
wordpressAvailable?: boolean;

View File

@ -28,10 +28,6 @@ const messagePushMenuNames = [
'QqBotAccountMessagePushToggle',
];
/**
* Reads the private menu whitelist's literal source array without exporting it.
* @returns Every string literal used to initialize `SUPPORTED_ADMIN_MENU_NAMES`.
*/
function getSupportedAdminMenuNameLiterals() {
const sourceFile = ts.createSourceFile(
'menu.ts',

View File

@ -549,10 +549,6 @@ const messagePushCallerNames = [
'updateMessageTemplate',
];
/**
* Type-checks this focused contract spec with a virtual request client boundary.
* @returns Deterministic TypeScript diagnostics for only this spec and `message-push.ts`.
*/
function getMessagePushContractDiagnostics() {
const appRoot = resolve('apps/web-antdv-next');
const configPath = resolve(appRoot, 'tsconfig.json');

View File

@ -181,42 +181,24 @@ export namespace QqbotMessagePushApi {
}
}
/**
* Lists registered system message sources available to the Admin UI.
* @returns Public source definitions without internal adapter details.
*/
export function getMessagePushSources() {
return requestClient.get<QqbotMessagePushApi.SystemMessageSourceDefinition[]>(
'/qqbot/message-push/sources',
);
}
/**
* Loads one public source definition by its stable source key.
* @param sourceKey - String source identity, encoded before becoming a URL segment.
* @returns The requested source definition.
*/
export function getMessagePushSourceDetail(sourceKey: string) {
return requestClient.get<QqbotMessagePushApi.SystemMessageSourceDefinition>(
`/qqbot/message-push/sources/${encodeURIComponent(sourceKey)}`,
);
}
/**
* Loads server-evaluated options for the built-in STUN port-change source.
* @returns Eligible and disabled port-forward and DDNS options.
*/
export function getStunMappingPortChangedOptions() {
return requestClient.get<QqbotMessagePushApi.StunMappingPortChangedOptionsResponse>(
'/qqbot/message-push/sources/network.stun.mapping-port-changed/options',
);
}
/**
* Pages global message subscriptions using independent list filters.
* @param params - Pagination, source, name, and enabled-state filters.
* @returns A strict `{ items, total }` subscription page.
*/
export function getMessageSubscriptionList(
params: QqbotMessagePushApi.MessageSubscriptionListQuery,
) {
@ -225,11 +207,6 @@ export function getMessageSubscriptionList(
>('/qqbot/message-push/subscriptions', { params });
}
/**
* Creates one source-scoped global message subscription.
* @param data - Complete subscription name, source, configuration, and enabled state.
* @returns The persisted subscription view.
*/
export function createMessageSubscription(
data: QqbotMessagePushApi.MessageSubscriptionInput,
) {
@ -239,12 +216,6 @@ export function createMessageSubscription(
);
}
/**
* Replaces one global subscription without coercing its string ID.
* @param id - Stable subscription ID, encoded before becoming a URL segment.
* @param data - Complete replacement subscription input.
* @returns The updated subscription view.
*/
export function updateMessageSubscription(
id: string,
data: QqbotMessagePushApi.MessageSubscriptionInput,
@ -255,12 +226,6 @@ export function updateMessageSubscription(
);
}
/**
* Changes whether a subscription matches future source events.
* @param id - Stable subscription ID, encoded before becoming a URL segment.
* @param enabled - Requested future enabled state.
* @returns The updated subscription view.
*/
export function setMessageSubscriptionEnabled(id: string, enabled: boolean) {
return requestClient.put<QqbotMessagePushApi.MessageSubscriptionView>(
`/qqbot/message-push/subscriptions/${encodeURIComponent(id)}/enabled`,
@ -268,22 +233,12 @@ export function setMessageSubscriptionEnabled(id: string, enabled: boolean) {
);
}
/**
* Soft-deletes one global message subscription.
* @param id - Stable subscription ID, encoded before becoming a URL segment.
* @returns Whether the subscription was deleted.
*/
export function deleteMessageSubscription(id: string) {
return requestClient.delete<boolean>(
`/qqbot/message-push/subscriptions/${encodeURIComponent(id)}`,
);
}
/**
* Pages global message templates using independent list filters.
* @param params - Pagination, source, name, and enabled-state filters.
* @returns A strict `{ items, total }` template page.
*/
export function getMessageTemplateList(
params: QqbotMessagePushApi.MessageTemplateListQuery,
) {
@ -292,11 +247,6 @@ export function getMessageTemplateList(
>('/qqbot/message-push/templates', { params });
}
/**
* Creates one source-scoped global message template.
* @param data - Complete template content, source, metadata, and enabled state.
* @returns The persisted template view.
*/
export function createMessageTemplate(
data: QqbotMessagePushApi.MessageTemplateInput,
) {
@ -306,12 +256,6 @@ export function createMessageTemplate(
);
}
/**
* Replaces one global message template without coercing its string ID.
* @param id - Stable template ID, encoded before becoming a URL segment.
* @param data - Complete replacement template input.
* @returns The updated template view.
*/
export function updateMessageTemplate(
id: string,
data: QqbotMessagePushApi.MessageTemplateInput,
@ -322,12 +266,6 @@ export function updateMessageTemplate(
);
}
/**
* Changes whether a template can be selected for future bindings and events.
* @param id - Stable template ID, encoded before becoming a URL segment.
* @param enabled - Requested future enabled state.
* @returns The updated template view.
*/
export function setMessageTemplateEnabled(id: string, enabled: boolean) {
return requestClient.put<QqbotMessagePushApi.MessageTemplateView>(
`/qqbot/message-push/templates/${encodeURIComponent(id)}/enabled`,
@ -335,22 +273,12 @@ export function setMessageTemplateEnabled(id: string, enabled: boolean) {
);
}
/**
* Soft-deletes one global message template when the backend permits it.
* @param id - Stable template ID, encoded before becoming a URL segment.
* @returns Whether the template was deleted.
*/
export function deleteMessageTemplate(id: string) {
return requestClient.delete<boolean>(
`/qqbot/message-push/templates/${encodeURIComponent(id)}`,
);
}
/**
* Renders a source-scoped message template using server-controlled example data.
* @param data - Template content and source identity only.
* @returns Safe rendered text and the example variables used.
*/
export function previewMessageTemplate(
data: QqbotMessagePushApi.MessageTemplatePreviewInput,
) {
@ -360,23 +288,12 @@ export function previewMessageTemplate(
);
}
/**
* Lists message-push bindings belonging to exactly one QQBot account.
* @param selfId - QQBot self ID kept as a string and encoded in the URL.
* @returns Account-scoped binding views with their configured targets.
*/
export function getAccountMessagePushBindings(selfId: string) {
return requestClient.get<
QqbotMessagePushApi.QqbotMessagePublishBindingView[]
>(`/qqbot/accounts/${encodeURIComponent(selfId)}/message-push/bindings`);
}
/**
* Creates one account-scoped binding from a global subscription and template.
* @param selfId - QQBot self ID kept as a string and encoded in the URL.
* @param data - Subscription, template, target, and enabled-state input.
* @returns The persisted account binding view.
*/
export function createAccountMessagePushBinding(
selfId: string,
data: QqbotMessagePushApi.QqbotMessagePublishBindingInput,
@ -387,13 +304,6 @@ export function createAccountMessagePushBinding(
);
}
/**
* Replaces one binding for exactly one QQBot account.
* @param selfId - QQBot self ID kept as a string and encoded in the URL.
* @param id - Stable binding ID, encoded before becoming a URL segment.
* @param data - Subscription, template, target, and enabled-state input.
* @returns The updated account binding view.
*/
export function updateAccountMessagePushBinding(
selfId: string,
id: string,
@ -405,13 +315,6 @@ export function updateAccountMessagePushBinding(
);
}
/**
* Changes whether one account binding creates future delivery work.
* @param selfId - QQBot self ID kept as a string and encoded in the URL.
* @param id - Stable binding ID, encoded before becoming a URL segment.
* @param enabled - Requested future enabled state.
* @returns The updated account binding view.
*/
export function setAccountMessagePushBindingEnabled(
selfId: string,
id: string,
@ -423,23 +326,12 @@ export function setAccountMessagePushBindingEnabled(
);
}
/**
* Soft-deletes one binding from exactly one QQBot account.
* @param selfId - QQBot self ID kept as a string and encoded in the URL.
* @param id - Stable binding ID, encoded before becoming a URL segment.
* @returns Whether the account binding was deleted.
*/
export function deleteAccountMessagePushBinding(selfId: string, id: string) {
return requestClient.delete<boolean>(
`/qqbot/accounts/${encodeURIComponent(selfId)}/message-push/bindings/${encodeURIComponent(id)}`,
);
}
/**
* Lists group and private-message target choices for exactly one QQBot account.
* @param selfId - QQBot self ID kept as a string and encoded in the URL.
* @returns Availability state plus currently discoverable string-ID targets.
*/
export function getAccountMessagePushTargets(selfId: string) {
return requestClient.get<QqbotMessagePushApi.QqbotMessagePushTargetOptionsResponse>(
`/qqbot/accounts/${encodeURIComponent(selfId)}/message-push/targets`,

View File

@ -246,9 +246,6 @@ export function getQqbotNapcatRuntimeDetail(accountId: string) {
);
}
/**
* Creates a short-lived gateway session for opening one account's NapCat WebUI.
*/
export function createQqbotNapcatWebuiSession(
data: QqbotNapcatApi.WebuiGatewaySessionCreateBody,
) {
@ -258,18 +255,12 @@ export function createQqbotNapcatWebuiSession(
);
}
/**
* Extends an active NapCat WebUI gateway session while the page is alive.
*/
export function heartbeatQqbotNapcatWebuiSession(sessionId: string) {
return requestClient.post<QqbotNapcatApi.WebuiGatewayLifecycleResult>(
`/qqbot/napcat/webui/session/${sessionId}/heartbeat`,
);
}
/**
* Revokes a NapCat WebUI gateway session when the page leaves the WebUI view.
*/
export function revokeQqbotNapcatWebuiSession(sessionId: string) {
return requestClient.post<QqbotNapcatApi.WebuiGatewayLifecycleResult>(
`/qqbot/napcat/webui/session/${sessionId}/revoke`,

View File

@ -1,6 +1,3 @@
/**
*
*/
import type { AxiosResponseHeaders, RequestClientOptions } from '@vben/request';
import { useAppConfig } from '@vben/hooks';

View File

@ -142,34 +142,18 @@ export namespace EnvironmentDashboardApi {
| 'snapshot-required';
}
/**
* Loads the aggregate Admin environment dashboard snapshot.
*
* @returns Dashboard response unwrapped by the shared Vben request client.
*/
export function getEnvironmentDashboard() {
return requestClient.get<EnvironmentDashboardApi.EnvironmentDashboardResponse>(
'/system/environment/dashboard',
);
}
/**
* Runs the API read-only self-check endpoint without exposing write actions.
*
* @returns Fresh dashboard response after backend read-only probes complete.
*/
export function runEnvironmentSelfCheck() {
return requestClient.post<EnvironmentDashboardApi.EnvironmentDashboardResponse>(
'/system/environment/self-check',
);
}
/**
* Builds the EventSource URL for API-emitted dashboard updates.
*
* @param lastEventId Optional SSE replay cursor held only in the current page instance.
* @returns Absolute API URL when the request client exposes a base URL; otherwise a relative path.
*/
export function getEnvironmentDashboardEventsUrl(lastEventId?: string) {
const query = lastEventId
? `?lastEventId=${encodeURIComponent(lastEventId)}`
@ -177,12 +161,6 @@ export function getEnvironmentDashboardEventsUrl(lastEventId?: string) {
return buildApiUrl(`/system/environment/events/stream${query}`);
}
/**
* Mirrors the existing API URL joining convention used by local SSE helpers.
*
* @param path API path produced by the environment dashboard wrapper.
* @returns Browser-ready URL that respects proxy or absolute API base configuration.
*/
function buildApiUrl(path: string) {
const getBaseUrl = (requestClient as unknown as { getBaseUrl?: () => string })
.getBaseUrl;

View File

@ -5,7 +5,6 @@ import { requestClient } from '#/api/request';
import { isSupportedAdminMenuName } from '../core/menu';
export namespace SystemMenuApi {
/** 徽标颜色集合 */
export const BadgeVariants = [
'default',
'destructive',
@ -13,9 +12,7 @@ export namespace SystemMenuApi {
'success',
'warning',
] as const;
/** 徽标类型集合 */
export const BadgeTypes = ['dot', 'normal'] as const;
/** 菜单类型集合 */
export const MenuTypes = [
'catalog',
'menu',
@ -23,73 +20,40 @@ export namespace SystemMenuApi {
'link',
'button',
] as const;
/** 系统菜单 */
export interface SystemMenu {
[key: string]: any;
/** 后端权限标识 */
authCode: string;
/** 子级 */
children?: SystemMenu[];
/** 组件 */
component?: string;
/** 菜单ID */
id: string;
/** 菜单元数据 */
meta?: {
/** 激活时显示的图标 */
activeIcon?: string;
/** 作为路由时需要激活的菜单的Path */
activePath?: string;
/** 固定在标签栏 */
affixTab?: boolean;
/** 在标签栏固定的顺序 */
affixTabOrder?: number;
/** 徽标内容(当徽标类型为normal时有效) */
badge?: string;
/** 徽标类型 */
badgeType?: (typeof BadgeTypes)[number];
/** 徽标颜色 */
badgeVariants?: (typeof BadgeVariants)[number];
/** 在菜单中隐藏下级 */
hideChildrenInMenu?: boolean;
/** 在面包屑中隐藏 */
hideInBreadcrumb?: boolean;
/** 在菜单中隐藏 */
hideInMenu?: boolean;
/** 在标签栏中隐藏 */
hideInTab?: boolean;
/** 菜单图标 */
icon?: string;
/** 内嵌Iframe的URL */
iframeSrc?: string;
/** 是否缓存页面 */
keepAlive?: boolean;
/** 外链页面的URL */
link?: string;
/** 同一个路由最大打开的标签数 */
maxNumOfOpenTab?: number;
/** 无需基础布局 */
noBasicLayout?: boolean;
/** 是否在新窗口打开 */
openInNewWindow?: boolean;
/** 菜单排序 */
order?: number;
/** 额外的路由参数 */
query?: Recordable<any>;
/** 菜单标题 */
title?: string;
};
/** 菜单名称 */
name: string;
/** 路由路径 */
path: string;
/** 父级ID */
pid: string;
/** 重定向 */
redirect?: string;
/** 排序值 */
sort?: number;
/** 菜单类型 */
type: (typeof MenuTypes)[number];
}
}

View File

@ -193,22 +193,12 @@ export namespace SystemNetworkApi {
}
}
/**
* Loads the persisted automatic-DDNS record page for either address family.
* @param params - Pagination and independent record/status filters.
* @returns DDNS rows with API-provided FQDN and source state.
*/
export function getNetworkDdnsList(params: SystemNetworkApi.DdnsRecordQuery) {
return requestClient.get<
SystemNetworkApi.PageResult<SystemNetworkApi.DdnsRecord>
>('/system/network/ddns/list', { params });
}
/**
* Loads eligible and disabled source choices for one DNS address family.
* @param recordType - A selects port-forward IPv4 sources; AAAA selects Agent IPv6.
* @returns Server-controlled source options and their current address state.
*/
export function getNetworkDdnsSourceOptions(
recordType: SystemNetworkApi.DdnsRecordType,
) {
@ -218,21 +208,12 @@ export function getNetworkDdnsSourceOptions(
);
}
/**
* Loads the redacted DNSPod runtime readiness state.
* @returns Provider name plus enabled/configured flags, never credentials.
*/
export function getNetworkDdnsProviderStatus() {
return requestClient.get<SystemNetworkApi.DdnsProviderStatus>(
'/system/network/ddns/provider-status',
);
}
/**
* Creates one generic A or AAAA automatic-DDNS binding.
* @param data - User-editable record and server-recognized source selector.
* @returns Persisted DDNS record with asynchronous synchronization state.
*/
export function createNetworkDdnsRecord(
data: SystemNetworkApi.DdnsRecordInput,
) {
@ -242,12 +223,6 @@ export function createNetworkDdnsRecord(
);
}
/**
* Updates one persisted automatic-DDNS binding.
* @param id - Stable string record ID.
* @param data - Complete editable DDNS input.
* @returns Updated record with pending synchronization state.
*/
export function updateNetworkDdnsRecord(
id: string,
data: SystemNetworkApi.DdnsRecordInput,
@ -258,33 +233,18 @@ export function updateNetworkDdnsRecord(
);
}
/**
* Deletes only the local automatic-update binding, leaving DNSPod unchanged.
* @param id - Stable string record ID.
* @returns Deleted local record state.
*/
export function deleteNetworkDdnsRecord(id: string) {
return requestClient.delete<SystemNetworkApi.DdnsRecord>(
`/system/network/ddns/${id}`,
);
}
/**
* Requests one immediate provider reconciliation for an eligible source.
* @param id - Stable string record ID.
* @returns Latest DDNS synchronization state.
*/
export function retryNetworkDdnsRecord(id: string) {
return requestClient.post<SystemNetworkApi.DdnsRecord>(
`/system/network/ddns/${id}/retry`,
);
}
/**
* Loads the persisted port-forward resource page from the API fact source.
* @param params - Pagination and independent protocol/synchronization filters.
* @returns Port-forward rows and the total record count.
*/
export function getNetworkPortForwardList(
params: SystemNetworkApi.PortForwardQuery,
) {
@ -293,11 +253,6 @@ export function getNetworkPortForwardList(
>('/system/network/port-forward/list', { params });
}
/**
* Creates a desired router mapping without exposing router credentials.
* @param data - User-editable mapping fields.
* @returns The persisted pending desired record.
*/
export function createNetworkPortForward(
data: SystemNetworkApi.PortForwardInput,
) {
@ -307,12 +262,6 @@ export function createNetworkPortForward(
);
}
/**
* Updates the editable desired fields of one active mapping.
* @param id - Stable port-forward record ID.
* @param data - Complete editable mapping fields.
* @returns The updated pending desired record.
*/
export function updateNetworkPortForward(
id: string,
data: SystemNetworkApi.PortForwardInput,
@ -323,69 +272,36 @@ export function updateNetworkPortForward(
);
}
/**
* Requests confirmed asynchronous deletion of one managed mapping.
* @param id - Stable port-forward record ID.
* @returns The deleting tombstone retained until Agent acknowledgement.
*/
export function deleteNetworkPortForward(id: string) {
return requestClient.delete<SystemNetworkApi.PortForward>(
`/system/network/port-forward/${id}`,
);
}
/**
* Raises a new reconciliation revision for a failed or conflicted mapping.
* @param id - Stable port-forward record ID.
* @returns The latest pending desired record.
*/
export function retryNetworkPortForward(id: string) {
return requestClient.post<SystemNetworkApi.PortForward>(
`/system/network/port-forward/${id}/retry`,
);
}
/**
* Enables persistent UDP STUN observation and triggers an immediate probe.
* @param id - Eligible UDP same-port mapping ID.
* @returns The latest pending desired record.
*/
export function enableNetworkPortForwardKeeper(id: string) {
return requestClient.post<SystemNetworkApi.PortForward>(
`/system/network/port-forward/${id}/keeper/enable`,
);
}
/**
* Stops future STUN renewals without deleting the router mapping.
* @param id - UDP mapping ID whose Keeper is currently desired.
* @returns The latest pending desired record.
*/
export function disableNetworkPortForwardKeeper(id: string) {
return requestClient.post<SystemNetworkApi.PortForward>(
`/system/network/port-forward/${id}/keeper/disable`,
);
}
/**
* Generates an idempotent request ID for one immediate UDP STUN cycle.
* @param id - Enabled UDP same-port mapping ID.
* @returns The latest desired record carrying the new request ID.
*/
export function probeNetworkPortForward(id: string) {
return requestClient.post<SystemNetworkApi.PortForward>(
`/system/network/port-forward/${id}/probe`,
);
}
/**
* Loads append-only endpoint transition history for one mapping.
* @param id - Stable port-forward record ID.
* @param params - History pagination values.
* @param params.pageNo - One-based history page number.
* @param params.pageSize - Maximum rows requested for the page.
* @returns Endpoint transition records and their total count.
*/
export function getNetworkPortForwardEndpointHistory(
id: string,
params: { pageNo?: number; pageSize?: number },
@ -395,21 +311,12 @@ export function getNetworkPortForwardEndpointHistory(
>(`/system/network/port-forward/${id}/endpoint-history`, { params });
}
/**
* Loads independent Agent connectivity and revision convergence state.
* @returns Current Agent status without inferring per-record failures.
*/
export function getNetworkAgentStatus() {
return requestClient.get<SystemNetworkApi.AgentStatus>(
'/system/network/agent/status',
);
}
/**
* Builds the credentialed EventSource URL for committed network-state changes.
* @param lastEventId - Optional replay cursor retained by the current route instance.
* @returns Browser-ready SSE URL using the configured API base path.
*/
export function getNetworkManagementEventsUrl(lastEventId?: string) {
const query = lastEventId
? `?lastEventId=${encodeURIComponent(lastEventId)}`
@ -417,11 +324,6 @@ export function getNetworkManagementEventsUrl(lastEventId?: string) {
return buildApiUrl(`/system/network/events/stream${query}`);
}
/**
* Joins one network API path with the request client's configured base URL.
* @param path - Relative API route for the EventSource connection.
* @returns Absolute or proxy-relative URL matching normal Admin requests.
*/
function buildApiUrl(path: string) {
const getBaseUrl = (requestClient as unknown as { getBaseUrl?: () => string })
.getBaseUrl;

View File

@ -81,15 +81,6 @@ export default defineComponent({
name: 'KtTable',
props: ktTableProps,
emits: ['register'],
/**
* KtTable API
*
* @param rawProps props register
* @param emit Vue setup context
* @param emit.emit Vue register API
* @param emit.expose Vue ref 访 API
* @param emit.slots titletoolbarbodyCellsummaryfooter
*/
setup(rawProps, { emit, expose, slots }) {
const { props, setProps } = useKtTableResolvedProps(
rawProps as KtTableProps,
@ -662,9 +653,6 @@ export default defineComponent({
loadData();
}
/**
*
*/
const renderSearchArea = () => {
const hasSearch = (formOptions.value.schema?.length || 0) > 0;
const hasFormButtons = formButtons.value.length > 0;
@ -714,11 +702,6 @@ export default defineComponent({
);
};
/**
*
*
* @param record
*/
const renderActionCell = (record: KtTableRecord) => {
const { inlineActions, overflowActions } = splitRowActions(
getVisibleRowActions(record),
@ -797,9 +780,6 @@ export default defineComponent({
return Math.max(0, Math.floor(visibleCount));
}
/**
* toolbar
*/
const renderHeaderButtons = () => {
const toolbar = slots.toolbar?.(context);
const buttons = headerButtons.value.map((button) => renderButton(button));
@ -820,9 +800,6 @@ export default defineComponent({
);
};
/**
*
*/
const renderHeaderSettings = () => {
if (!props.showTableSetting) return null;

View File

@ -39,14 +39,6 @@ export default defineComponent({
},
},
emits: ['pageChange'],
/**
*
*
* @param props
* @param emit Vue setup context
* @param emit.emit
* @param emit.slots
*/
setup(props, { emit, slots }) {
/**
* Antdv Pagination KtTable

View File

@ -35,13 +35,6 @@ export default defineComponent({
type: String,
},
},
/**
*
*
* @param props props
* @param slots Vue setup context
* @param slots.slots
*/
setup(props, { slots }) {
return () => {
const slotTitle = resolveSlotContent(slots.title?.());

View File

@ -30,14 +30,6 @@ export default defineComponent({
type: Number,
},
},
/**
*
*
* @param props
* @param attrs Vue setup context
* @param attrs.attrs Antdv Table th
* @param attrs.slots
*/
setup(props, { attrs, slots }) {
const dragging = ref(false);
const stopNextClick = ref(false);

View File

@ -33,14 +33,6 @@ export default defineComponent({
},
},
emits: ['transitionEnd', 'transitionStart'],
/**
* /
*
* @param props
* @param emit Vue setup context
* @param emit.emit
* @param emit.slots
*/
setup(props, { emit, slots }) {
const shellRef = ref<HTMLElement | null>(null);
const contentRef = ref<HTMLElement | null>(null);

View File

@ -76,13 +76,6 @@ export default defineComponent({
'sizeChange',
'visibleColumnKeysChange',
],
/**
* KtTable
*
* @param props
* @param emit Vue setup context
* @param emit.emit
*/
setup(props, { emit }) {
const draggingColumnKey = ref('');
const dragOverColumnKey = ref('');
@ -241,14 +234,6 @@ export default defineComponent({
emit('sizeChange', next);
}
/**
*
*
* @param key key Vue
* @param title
* @param icon
* @param onClick
*/
const renderIconButton = (
key: string,
title: string,
@ -271,9 +256,6 @@ export default defineComponent({
);
};
/**
*
*/
const renderColumnSetting = () => {
if (!props.setting.column) return null;

View File

@ -24,12 +24,6 @@ type RenderKtTableSummaryOptions = {
statistics: KtTableStatistic[];
};
/**
*
*
* @param item
* @param context KtTable
*/
const renderStatisticValue = (
item: KtTableStatistic,
context: KtTableContext,
@ -48,13 +42,6 @@ const renderStatisticValue = (
);
};
/**
* summary
*
* @param context KtTable
* @param statistic
* @param showDefaultLabel
*/
const renderCellContent = (
context: KtTableContext,
statistic: KtTableStatistic | undefined,
@ -68,16 +55,6 @@ const renderCellContent = (
return null;
};
/**
* KtTable summary
*
* @param options summary
* @param options.columns
* @param options.context KtTable
* @param options.customSummary summary
* @param options.showSelection
* @param options.statistics
*/
export const renderKtTableSummary = (options: RenderKtTableSummaryOptions) => {
const { columns, context, customSummary, showSelection, statistics } =
options;

View File

@ -36,11 +36,6 @@ export function useKtTable<
return tableApi;
}
/**
* props
*
* @param nextProps props props
*/
const setProps: KtTableSetProps<Row, SearchValues> = (nextProps) => {
if (tableApi) {
tableApi.setProps(nextProps);
@ -57,11 +52,6 @@ export function useKtTable<
};
};
/**
* KtTable API props
*
* @param api KtTable API
*/
const register: KtTableRegisterFn<Row, SearchValues> = (api) => {
tableApi = api;
api.setProps(pendingProps);

View File

@ -36,7 +36,6 @@ vi.mock('antdv-next', () => ({
}),
}));
/** Creates the smallest action runtime needed to exercise row rendering. */
function createActionRuntime() {
const context = {
formApi: {},

View File

@ -77,9 +77,6 @@ export function useKtTableActions(options: UseKtTableActionsOptions) {
customButtons.value.filter((button) => button.placement !== 'form'),
);
/**
* KtTable
*/
const getDefaultButtons = (): KtTableButton[] => {
if (!props.showDefaultButtons) return [];
@ -102,12 +99,6 @@ export function useKtTableActions(options: UseKtTableActionsOptions) {
];
};
/**
*
*
* @param icon
* @param targetContext 使
*/
const renderIcon = (
icon: KtTableButton['icon'],
targetContext: KtTableContext = context,
@ -205,11 +196,6 @@ export function useKtTableActions(options: UseKtTableActionsOptions) {
});
}
/**
*
*
* @param button
*/
const renderButton = (button: KtTableButton) => {
return (
<AButton
@ -226,12 +212,6 @@ export function useKtTableActions(options: UseKtTableActionsOptions) {
);
};
/**
*
*
* @param action
* @param row
*/
const renderRowAction = (action: KtTableRowAction, row: KtTableRecord) => {
const disabled =
typeof action.disabled === 'function'
@ -262,14 +242,6 @@ export function useKtTableActions(options: UseKtTableActionsOptions) {
);
};
/**
* Resolves a readable disabled reason only while a row action is disabled.
*
* @param action Current row action configuration.
* @param row Current table row.
* @param disabled Resolved disabled state for this row.
* @returns Tooltip text, or undefined when no explanation should render.
*/
function resolveDisabledReason(
action: KtTableRowAction,
row: KtTableRecord,

View File

@ -397,11 +397,6 @@ export function useKtTableColumns(options: UseKtTableColumnsOptions) {
return {
...column,
ellipsis: column.ellipsis ?? true,
/**
* header cell KtTable
*
* @param targetColumn Antdv
*/
onHeaderCell: (targetColumn: TableColumnType<KtTableRecord>) => {
const originalProps = (originalHeaderCell?.(targetColumn) ||
{}) as Record<string, any>;
@ -421,12 +416,6 @@ export function useKtTableColumns(options: UseKtTableColumnsOptions) {
width: nextWidth,
};
},
/**
* body cell KtTable
*
* @param record
* @param index
*/
onCell: (record: KtTableRecord, index?: number) => {
const originalProps = (originalCell?.(record, index) || {}) as Record<
string,

View File

@ -78,11 +78,6 @@ export function useKtTableResolvedProps(rawProps: KtTableProps) {
);
}
/**
* register props props
*
* @param nextProps props props
*/
const setProps: KtTableSetProps = (nextProps) => {
const patch =
typeof nextProps === 'function' ? nextProps({ ...props }) : nextProps;

View File

@ -23,9 +23,6 @@ type TiptapExpose = ComponentPublicInstance & {
setHtml: (value: string) => void;
};
/**
* Tiptap Editor HTML
*/
class FakeEditor {
public html = '';
@ -46,11 +43,6 @@ class FakeEditor {
public options: FakeEditorOptions;
/**
* HTML
*
* @param options
*/
constructor(options: FakeEditorOptions) {
this.options = options;
this.html = options.content || '';
@ -126,11 +118,6 @@ vi.mock('@tiptap/vue-3', () => ({
type: Object,
},
},
/**
* FakeEditor HTML使
*
* @param props EditorContent
*/
setup(props) {
return () =>
h('div', {
@ -157,15 +144,6 @@ vi.mock('antdv-next', () => ({
},
},
emits: ['click'],
/**
* button antdv-next Button attrs/disabled/click
*
* @param props disabled
* @param context Vue setup context attrs slots
* @param context.attrs button
* @param context.emit click
* @param context.slots
*/
setup(props, { attrs, emit, slots }) {
return () =>
h(
@ -182,9 +160,6 @@ vi.mock('antdv-next', () => ({
}),
Divider: defineComponent({
name: 'MockDivider',
/**
*
*/
setup() {
return () => h('span', { class: 'mock-divider' });
},
@ -198,13 +173,6 @@ vi.mock('antdv-next', () => ({
},
},
emits: ['update:value'],
/**
* input antdv-next Input value
*
* @param props 使 value
* @param context Vue setup context update:value
* @param context.emit update:value
*/
setup(props, { emit }) {
return () =>
h('input', {
@ -224,13 +192,6 @@ vi.mock('antdv-next', () => ({
},
},
emits: ['cancel', 'ok', 'update:open'],
/**
* Modal open=true
*
* @param props open
* @param context Vue setup context
* @param context.slots
*/
setup(props, { slots }) {
return () =>
props.open ? h('div', { role: 'dialog' }, slots.default?.()) : null;
@ -238,13 +199,6 @@ vi.mock('antdv-next', () => ({
}),
Space: defineComponent({
name: 'MockSpace',
/**
* div Space
*
* @param _props 使 Space props
* @param context Vue setup context slot
* @param context.slots Space
*/
setup(_props, { slots }) {
return () => h('div', slots.default?.());
},
@ -257,13 +211,6 @@ vi.mock('antdv-next', () => ({
type: String,
},
},
/**
* Tooltip slot
*
* @param _props 使 Tooltip props
* @param context Vue setup context slot
* @param context.slots Tooltip
*/
setup(_props, { slots }) {
return () => h('span', slots.default?.());
},
@ -279,11 +226,6 @@ vi.mock('@vben/icons', () => ({
type: String,
},
},
/**
*
*
* @param props data
*/
setup(props) {
return () => h('i', { 'data-icon': props.icon });
},
@ -350,9 +292,6 @@ describe('ktTiptapHtmlEditor', () => {
it('syncs setHtml changes through a v-model parent', async () => {
const Parent = defineComponent({
name: 'TiptapParentHarness',
/**
* v-model emit
*/
setup() {
const html = ref('<p>旧正文</p>');
const editorRef = ref<null | TiptapExpose>(null);

View File

@ -98,12 +98,6 @@ function readEditorHtml(editor: Editor | null | undefined, fallback: string) {
return editor?.getHTML() || fallback;
}
/**
* 使 Iconify
*
* @param icon Vben/Iconify lucide
* @returns
*/
const renderIcon = (icon: string) => (
<IconifyIcon class="kt-tiptap-editor__toolbar-icon" icon={icon} />
);
@ -138,14 +132,6 @@ export default defineComponent({
focus: () => true,
'update:modelValue': (_value: string) => true,
},
/**
* Tiptap HTML toolbar v-model expose API
*
* @param props HTML
* @param context Vue setup context
* @param context.emit HTMLchangefocus blur
* @param context.expose HTML
*/
setup(props, { emit, expose }) {
const currentHtml = ref(props.modelValue || '');
const linkModalOpen = ref(false);
@ -417,12 +403,6 @@ export default defineComponent({
},
];
/**
*
*
* @param command
* @returns
*/
const renderCommandButton = (command: ToolbarCommand): VNodeChild => {
const currentEditor = editor.value;
const active = currentEditor ? command.isActive?.(currentEditor) : false;
@ -447,9 +427,6 @@ export default defineComponent({
);
};
/**
*
*/
const renderToolbar = () => {
const groups = [
toolbarCommands.slice(0, 4),
@ -473,9 +450,6 @@ export default defineComponent({
);
};
/**
*
*/
const renderModals = () => (
<>
<AModal

View File

@ -1,10 +1,5 @@
import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description
* 使
* !!!
*/
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {

View File

@ -1,29 +1,14 @@
export const ADMIN_SSO_DEFAULT_REDIRECT = '/blog/article';
/**
* Reads a scalar router query value while tolerating Vue Router array values.
* @param value Raw route-query value.
* @returns First string value or an empty string when absent.
*/
function readQueryValue(value: unknown) {
const scalar = Array.isArray(value) ? value[0] : value;
return typeof scalar === 'string' ? scalar : '';
}
/**
* Detects the explicit cross-site Admin SSO bootstrap flag.
* @param value Raw `sso` route-query value.
* @returns Whether silent Admin-cookie restoration should be attempted.
*/
export function isAdminSsoRequest(value: unknown) {
return readQueryValue(value) === '1';
}
/**
* Resolves the fixed Blog management landing route without accepting open redirects.
* @param value Raw `redirect` route-query value, optionally URL-encoded once.
* @returns The allow-listed Blog article management route.
*/
export function resolveAdminSsoRedirect(value: unknown) {
const rawValue = readQueryValue(value);
let decodedValue = rawValue;

View File

@ -203,9 +203,7 @@ function setupAccessGuard(router: Router) {
* @param router
*/
function createRouterGuard(router: Router) {
/** 通用 */
setupCommonGuard(router);
/** 权限访问 */
setupAccessGuard(router);
}

View File

@ -9,9 +9,6 @@ import { resetStaticRoutes } from '@vben/utils';
import { createRouterGuard } from './guard';
import { routes } from './routes';
/**
* @zh_CN vue-router实例
*/
const router = createRouter({
history:
import.meta.env.VITE_ROUTER_HISTORY === 'hash'

View File

@ -7,7 +7,6 @@ import { $t } from '#/locales';
const BasicLayout = () => import('#/layouts/basic.vue');
const AuthPageLayout = () => import('#/layouts/auth.vue');
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
meta: {
@ -20,13 +19,7 @@ const fallbackNotFoundRoute: RouteRecordRaw = {
path: '/:path(.*)*',
};
/** 基本路由,这些路由是必须存在的 */
const coreRoutes: RouteRecordRaw[] = [
/**
*
* 使BasicLayout
*
*/
{
component: BasicLayout,
meta: {

View File

@ -12,27 +12,21 @@ const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
/** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
/** 外部路由列表访问这些页面可以不需要Layout可能用于内嵌在别的系统(不会显示在菜单中) */
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
const staticRoutes: RouteRecordRaw[] = [];
const externalRoutes: RouteRecordRaw[] = [];
/** 404
* */
const routes: RouteRecordRaw[] = [
...coreRoutes,
...externalRoutes,
fallbackNotFoundRoute,
];
/** 基本路由列表,这些路由不需要进入权限拦截 */
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
/** 有权限校验的路由列表,包含动态路由和静态路由 */
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
const componentKeys: string[] = Object.keys({

View File

@ -27,11 +27,6 @@ const messagePushRoutes: ExpectedMessagePushRoute[] = [
},
];
/**
* Returns the text key of a non-computed object-literal property.
* @param property - Candidate route object property from the parsed source.
* @returns The literal property key, or `undefined` when it cannot be read statically.
*/
function getPropertyName(property: ts.ObjectLiteralElementLike) {
if (!ts.isPropertyAssignment(property) || property.name === undefined) {
return undefined;
@ -42,12 +37,6 @@ function getPropertyName(property: ts.ObjectLiteralElementLike) {
: undefined;
}
/**
* Finds one directly declared property on a route object.
* @param object - Route object literal to inspect.
* @param name - Required literal property key.
* @returns The matching property assignment, if present.
*/
function getProperty(object: ts.ObjectLiteralExpression, name: string) {
return object.properties.find(
(property): property is ts.PropertyAssignment =>
@ -55,12 +44,6 @@ function getProperty(object: ts.ObjectLiteralExpression, name: string) {
);
}
/**
* Reads a required string-literal property from a route object.
* @param object - Route object literal to inspect.
* @param name - Required literal property key.
* @returns The declared string value, or `undefined` when it is not a string literal.
*/
function getStringProperty(object: ts.ObjectLiteralExpression, name: string) {
const property = getProperty(object, name);
@ -69,11 +52,6 @@ function getStringProperty(object: ts.ObjectLiteralExpression, name: string) {
: undefined;
}
/**
* Reads the literal target from a route's lazy dynamic import without invoking it.
* @param object - Route object literal to inspect.
* @returns The module path passed to `import()`.
*/
function getLazyComponentPath(object: ts.ObjectLiteralExpression) {
const component = getProperty(object, 'component');
@ -91,11 +69,6 @@ function getLazyComponentPath(object: ts.ObjectLiteralExpression) {
return (importCall.arguments[0] as ts.StringLiteral).text;
}
/**
* Locates the direct `QqBot.children` array in the route initializer.
* @param sourceFile - Parsed QQBot route module source.
* @returns The direct child route objects of the `QqBot` parent.
*/
function getQqBotChildren(sourceFile: ts.SourceFile) {
const routesDeclaration = sourceFile.statements.find(
(statement): statement is ts.VariableStatement => {
@ -139,16 +112,10 @@ function getQqBotChildren(sourceFile: ts.SourceFile) {
);
}
/**
* Collects every route object that declares one of the locked message-push names.
* @param sourceFile - Parsed QQBot route module source.
* @returns All matching objects, including accidental nested or duplicate routes.
*/
function getAllMessagePushRoutes(sourceFile: ts.SourceFile) {
const routeNames = new Set(messagePushRoutes.map((route) => route.name));
const matches: ts.ObjectLiteralExpression[] = [];
/** Visits route objects so nested copies cannot evade the direct-child assertion. */
function visit(node: ts.Node): void {
if (ts.isObjectLiteralExpression(node)) {
const routeName = getStringProperty(node, 'name');

View File

@ -178,10 +178,6 @@ export const useAuthStore = defineStore('auth', () => {
};
}
/**
* Restores the Admin client session from its origin-scoped HttpOnly refresh cookie.
* @returns Whether a fresh access token was restored; failures clear stale client identity state.
*/
async function restoreSessionFromCookie() {
if (accessStore.accessToken) return true;

View File

@ -7,12 +7,6 @@ import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('list.tsx', import.meta.url), 'utf8');
describe('blog article list preview entry', () => {
/**
* Reads a source slice between two stable markers for modal ordering guards.
* @param start Marker before the function body under assertion.
* @param end Marker after the function body under assertion.
* @returns Source slice used by regression assertions.
*/
function getSourceSlice(start: string, end: string) {
const startIndex = source.indexOf(start);
const endIndex = source.indexOf(end, startIndex + start.length);

View File

@ -393,39 +393,21 @@ export default defineComponent({
});
}
/**
* Applies editor schema and form values after the modal has opened so the modal-contained form can mount first.
* @param values Article values selected for the current modal session.
*/
async function resetArticleModalForm(values: BlogArticleFormValues) {
await setArticleEditorMode(values.editorMode || 'markdown');
await resetArticleForm(values);
}
/**
* Resets modal form state before applying create or edit values.
* @param values Article values selected for the current modal session.
*/
async function resetArticleForm(values: BlogArticleFormValues) {
await articleFormApi.resetForm();
await articleFormApi.setValues(values);
await articleFormApi.resetValidate();
}
/**
* Handles editor mode changes from the form control without clearing the current content draft.
* @param mode Editor mode selected by the user.
*/
function handleArticleEditorModeChange(mode: BlogArticleEditorMode) {
void setArticleEditorMode(mode, { preserveContent: true });
}
/**
* Switches the content field between Markdown, rich HTML, and raw WordPress HTML editing.
* @param mode Editor mode selected for the current modal session.
* @param options Whether the current content draft should be reapplied after schema replacement.
* @param options.preserveContent Reapplies the existing content value after the editor component changes.
*/
async function setArticleEditorMode(
mode: BlogArticleEditorMode,
options: { preserveContent?: boolean } = {},
@ -458,10 +440,6 @@ export default defineComponent({
await articleFormApi.setValues(nextValues);
}
/**
* Opens the article modal in Markdown authoring mode with current table filters as defaults.
* @param context Optional table context supplied by a toolbar button.
*/
async function openCreate(
context?: KtTableContext<WordpressBlogApi.Article, ArticleSearchValues>,
) {
@ -474,10 +452,6 @@ export default defineComponent({
articleModalApi.setData({ values }).open();
}
/**
* Opens the article modal using raw HTML mode for imported WordPress/Argon content.
* @param row Article row selected from the table.
*/
function openEdit(row: WordpressBlogApi.Article) {
editingId.value = `${row.id}`;
const values = getBlogArticleEditFormValues(row);
@ -496,9 +470,6 @@ export default defineComponent({
});
}
/**
* Validates and submits the article form with the active content format preserved.
*/
async function submitArticle() {
const { valid } = await articleFormApi.validate();
if (!valid) return;

View File

@ -27,9 +27,6 @@ const articleStatusOptions = [
export default defineComponent({
name: 'BlogArticlePreview',
/**
* Wires the current article route id to a full-bleed Blog Web iframe preview.
*/
setup() {
const route = useRoute();
const router = useRouter();
@ -64,16 +61,10 @@ export default defineComponent({
{ immediate: true },
);
/**
* Navigates back to the Blog article list.
*/
function goBack() {
void router.push({ name: 'BlogArticle' });
}
/**
* Opens the current public KT Blog Web preview in a browser tab for direct inspection.
*/
function openPreviewInNewWindow() {
if (!iframeUrl.value) {
return;
@ -82,9 +73,6 @@ export default defineComponent({
window.open(iframeUrl.value, '_blank', 'noopener,noreferrer');
}
/**
* Reloads the article detail and rebuilds the iframe URL for the current route id.
*/
async function loadArticlePreview() {
const articleId = routeArticleId.value;
article.value = null;
@ -108,11 +96,6 @@ export default defineComponent({
}
}
/**
* Renders the floating status card without taking layout space from the preview iframe.
*
* @returns Overlay card with article metadata and navigation actions.
*/
const renderFloatingCard = () => {
return (
<div class="blog-article-preview__floating-card">
@ -157,11 +140,6 @@ export default defineComponent({
);
};
/**
* Renders loading, error, and ready iframe states inside the fixed preview canvas.
*
* @returns Current preview body content.
*/
const renderBody = () => {
if (state.value === 'ready' && iframeUrl.value) {
return (
@ -197,11 +175,6 @@ export default defineComponent({
);
};
/**
* Renders the stable page root required by Vben route transitions.
*
* @returns Single-root preview page shell.
*/
const renderPage = () => {
return (
<div class="blog-article-preview-page">
@ -217,19 +190,11 @@ export default defineComponent({
},
});
/**
* @param value Raw vue-router route param value.
* @returns A single trimmed article id string.
*/
function normalizeRouteParam(value: unknown) {
if (Array.isArray(value)) return `${value[0] || ''}`.trim();
return `${value || ''}`.trim();
}
/**
* @param article Blog article detail returned by the Admin API.
* @returns Plain article title for metadata and iframe title attributes.
*/
function getArticleTitle(article?: null | WordpressBlogApi.Article) {
const value = article?.title;
if (typeof value === 'string') return stripHtml(value);
@ -237,10 +202,6 @@ function getArticleTitle(article?: null | WordpressBlogApi.Article) {
return stripHtml(value?.raw || value?.rendered || '');
}
/**
* @param previewUrl Iframe URL built from the KT Blog Web base URL.
* @returns Host shown in the floating card so operators can verify the embedded target.
*/
function getPreviewHost(previewUrl: string) {
if (!previewUrl) {
return '';
@ -254,10 +215,6 @@ function getPreviewHost(previewUrl: string) {
}
}
/**
* @param value HTML-ish rendered text from WordPress-compatible article fields.
* @returns Plain text safe for compact Admin metadata.
*/
function stripHtml(value: string) {
return value
.replaceAll(/<[^>]*>/g, ' ')
@ -268,10 +225,6 @@ function stripHtml(value: string) {
.trim();
}
/**
* @param state Current preview page lifecycle state.
* @returns Ant Design tag color and Chinese label for the floating card.
*/
function getStatusMeta(state: PreviewState) {
const statusMap = {
error: { color: 'error', label: '异常' },
@ -282,10 +235,6 @@ function getStatusMeta(state: PreviewState) {
return statusMap[state];
}
/**
* @param status Article publish status returned by the Admin API.
* @returns Ant Design tag color and Chinese label for article metadata.
*/
function getArticleStatusMeta(status?: string) {
return (
articleStatusOptions.find((item) => item.value === status) ||

View File

@ -21,13 +21,6 @@ const SOURCE_ONLY_HTML_PATTERN =
/\b(?:hljs-ln|hljs-control|fancybox-wrapper|lazyload|collapse-block|wp-block-(?!code\b)[\w-]+)/;
const HTML_TAG_PATTERN = /<\/?[a-z][\s\S]*>/i;
/**
* Builds the content field schema for Markdown, rich HTML, or raw WordPress HTML preservation.
* @param mode Current editor mode; source mode uses a raw textarea to avoid runtime DOM rewrites.
* @param markdownEditor Markdown editor component used for normal local article authoring.
* @param richHtmlEditor Tiptap HTML editor component used for plain HTML authoring.
* @returns Vben form schema for the article content field.
*/
export function createBlogArticleContentSchema(
mode: BlogArticleEditorMode,
markdownEditor: unknown,
@ -79,11 +72,6 @@ export function createBlogArticleContentSchema(
} as VbenFormSchema;
}
/**
* Builds the editor mode selector schema and forwards mode changes to the article list page.
* @param onChange Callback that receives the next editor mode selected in the form.
* @returns Vben form schema for selecting the article editor mode.
*/
export function createBlogArticleEditorModeSchema(
onChange?: (mode: BlogArticleEditorMode) => void,
): VbenFormSchema {
@ -107,22 +95,12 @@ export function createBlogArticleEditorModeSchema(
} as VbenFormSchema;
}
/**
* Maps the UI editor mode back to the API's persisted content format contract.
* @param mode Current editor mode from the article form.
* @returns API content format saved with the article.
*/
export function getContentFormatForEditorMode(
mode: BlogArticleEditorMode,
): BlogArticleContentFormat {
return mode === 'markdown' ? 'markdown' : 'html';
}
/**
* Chooses the editor mode for an existing article without forcing Markdown source or imported Argon HTML into the wrong editor.
* @param article Article row returned by the local blog API.
* @returns Markdown for source-backed articles, source HTML for strong runtime DOM, rich HTML for plain tags.
*/
export function getBlogArticleEditorMode(
article?: Partial<WordpressBlogApi.Article>,
): BlogArticleEditorMode {
@ -134,24 +112,12 @@ export function getBlogArticleEditorMode(
return HTML_TAG_PATTERN.test(html) ? 'html-rich' : 'markdown';
}
/**
* Chooses the persisted content format for an existing article.
* @param article Article row returned by the local blog API.
* @returns API content format derived from the detected editor mode.
*/
export function getBlogArticleContentFormat(
article?: Partial<WordpressBlogApi.Article>,
): BlogArticleContentFormat {
return getContentFormatForEditorMode(getBlogArticleEditorMode(article));
}
/**
* Builds form defaults for creating a new article from current table filters.
* @param searchValues Active table search filters used to prefill category and tag fields.
* @param searchValues.categories Active category filters copied into the create form.
* @param searchValues.tags Active tag filters copied into the create form.
* @returns Article form defaults in Markdown mode.
*/
export function getBlogArticleCreateFormDefaults(
searchValues: {
categories?: string[];
@ -172,11 +138,6 @@ export function getBlogArticleCreateFormDefaults(
};
}
/**
* Builds edit form values, preserving WordPress/Argon HTML when the article depends on runtime classes.
* @param row Article row selected from the article table.
* @returns Form values and content format for the edit modal.
*/
export function getBlogArticleEditFormValues(
row: WordpressBlogApi.Article,
): BlogArticleFormValues {
@ -201,13 +162,6 @@ export function getBlogArticleEditFormValues(
};
}
/**
* Builds the API payload while preserving the current content format selected for the modal.
* @param values Current form values from Vben Form.
* @param editingId Current article id, or undefined for create.
* @param editorMode Current editor mode.
* @returns Payload accepted by the blog article save/update API.
*/
export function buildBlogArticleSubmitPayload(
values: BlogArticleFormValues,
editingId: string | undefined,
@ -222,43 +176,22 @@ export function buildBlogArticleSubmitPayload(
};
}
/**
* Converts WordPress rendered fields or strings into readable plain text.
* @param value Rendered field or string value.
* @returns HTML-free text.
*/
export function getRenderedText(
value?: string | WordpressBlogApi.RenderedField,
) {
return stripHtml(getRenderedValue(value));
}
/**
* Reads a rendered field without stripping HTML.
* @param value Rendered field or string value.
* @returns Raw/rendered string.
*/
function getRenderedValue(value?: string | WordpressBlogApi.RenderedField) {
if (!value) return '';
if (typeof value === 'string') return value;
return value.raw || value.rendered || '';
}
/**
* Detects whether the article still owns an editable Markdown source snapshot.
* @param article Article row returned by the local blog API.
* @returns true when Admin should prefer Milkdown unless strong runtime-only HTML is present.
*/
function hasMarkdownSource(article?: Partial<WordpressBlogApi.Article>) {
return !!article?.contentMarkdown?.trim();
}
/**
* Chooses editable Markdown from an API rendered field and its explicit Markdown source.
* @param value Rendered field or string value.
* @param markdown Explicit Markdown source saved on local articles.
* @returns Markdown text for the Milkdown editor.
*/
function getEditableMarkdown(
value?: string | WordpressBlogApi.RenderedField,
markdown?: string,
@ -267,11 +200,6 @@ function getEditableMarkdown(
return getRenderedValue(value);
}
/**
* Strips simple HTML tags from Admin labels and excerpts.
* @param value HTML or text value.
* @returns Trimmed text.
*/
function stripHtml(value: string) {
return value
.replaceAll(/<[^>]+>/g, '')

View File

@ -7,12 +7,6 @@ defineProps<{
events: EnvironmentEvent[];
}>();
/**
* Maps event severity to a compact tag color.
*
* @param status Event severity from the API stream or snapshot.
* @returns Antdv tag color value.
*/
function getStatusColor(status: EnvironmentHealthStatus) {
if (status === 'ok') return 'success';
if (status === 'degraded') return 'warning';

View File

@ -21,13 +21,6 @@ defineEmits<{
selfCheck: [];
}>();
/**
* Returns evidence rows for the selected service or signal.
*
* @param service Selected topology service from the current site.
* @param signal Selected signal when the user drills into one.
* @returns Evidence list shown in the right-side panel.
*/
function getEvidenceRows(
service?: EnvironmentService,
signal?: EnvironmentSignal,
@ -38,12 +31,6 @@ function getEvidenceRows(
);
}
/**
* Labels evidence with a stable source and missing-config status.
*
* @param evidence Evidence record from the API contract.
* @returns Human-readable evidence title.
*/
function getEvidenceTitle(evidence: EnvironmentEvidence) {
return `${evidence.type || 'evidence'} · ${evidence.source}`;
}

View File

@ -12,12 +12,6 @@ defineEmits<{
selectSite: [siteId: string];
}>();
/**
* Counts service signals under a site for compact rail evidence.
*
* @param site Site record from the dashboard snapshot.
* @returns Total number of service signals in the site tree.
*/
function countSignals(site: EnvironmentSite) {
return site.nodes.reduce(
(count, node) =>
@ -30,12 +24,6 @@ function countSignals(site: EnvironmentSite) {
);
}
/**
* Counts unwired signals so missing config remains visible.
*
* @param site Site record from the dashboard snapshot.
* @returns Number of signals marked as unwired by the API.
*/
function countUnwiredSignals(site: EnvironmentSite) {
return site.nodes.reduce(
(count, node) =>
@ -51,12 +39,6 @@ function countUnwiredSignals(site: EnvironmentSite) {
);
}
/**
* Maps site status to antdv badge status.
*
* @param site Site record from the dashboard snapshot.
* @returns Badge status that preserves unknown/unwired as non-green.
*/
function getBadgeStatus(site: EnvironmentSite) {
if (site.status === 'online') return 'success';
if (site.status === 'degraded') return 'warning';

View File

@ -17,11 +17,6 @@ defineEmits<{
selfCheck: [];
}>();
/**
* Chooses the strongest global status from summary counters.
*
* @returns Status tag value used by the top dashboard bar.
*/
function getGlobalStatus(): EnvironmentHealthStatus {
const summary = props.dashboard?.summary;
if (!summary) return 'unknown';
@ -32,12 +27,6 @@ function getGlobalStatus(): EnvironmentHealthStatus {
return 'ok';
}
/**
* Maps health status to antdv tag colors without hiding unwired integrations.
*
* @param status Global or signal health status from the API.
* @returns Antdv tag color name.
*/
function getStatusColor(status: EnvironmentHealthStatus) {
if (status === 'ok') return 'success';
if (status === 'degraded') return 'warning';

View File

@ -16,12 +16,6 @@ defineEmits<{
selectService: [serviceId: string];
}>();
/**
* Maps service health to a compact tag color.
*
* @param status Service or signal status from the dashboard model.
* @returns Antdv tag color value.
*/
function getStatusColor(status: EnvironmentHealthStatus) {
if (status === 'ok') return 'success';
if (status === 'degraded') return 'warning';
@ -30,12 +24,6 @@ function getStatusColor(status: EnvironmentHealthStatus) {
return 'default';
}
/**
* Counts unwired signals in a service.
*
* @param service Service record selected from the current site.
* @returns Number of signals that are visibly missing configuration.
*/
function countUnwiredSignals(service: EnvironmentService) {
return service.signals.filter((signal) => signal.status === 'unwired').length;
}

View File

@ -20,12 +20,6 @@ export interface UseEnvironmentDashboardStreamOptions {
onSnapshotRequired: (event: StreamEvent) => void;
}
/**
* Opens one browser EventSource for Admin environment updates after the first HTTP snapshot.
*
* @param options Page-owned callbacks that merge SSE payloads into visible state.
* @returns Controls for starting and closing the stream plus connection state for display.
*/
export function useEnvironmentDashboardStream(
options: UseEnvironmentDashboardStreamOptions,
) {
@ -33,9 +27,6 @@ export function useEnvironmentDashboardStream(
const lastEventId = ref<string>();
let source: EventSource | undefined;
/**
* Starts the SSE connection once and lets the browser handle native reconnects.
*/
function start() {
if (source) return;
connectionState.value = 'connecting';
@ -53,9 +44,6 @@ export function useEnvironmentDashboardStream(
source.addEventListener('error', handleError);
}
/**
* Closes the EventSource when the route leaves or the component unmounts.
*/
function close() {
if (!source) return;
source.removeEventListener('open', handleOpen);
@ -69,18 +57,10 @@ export function useEnvironmentDashboardStream(
connectionState.value = 'closed';
}
/**
* Marks the stream as open after the browser confirms the connection.
*/
function handleOpen() {
connectionState.value = 'open';
}
/**
* Applies a backend event entry to the visible event stream.
*
* @param event Browser SSE message carrying an EnvironmentEvent JSON payload.
*/
function handleEnvironmentEvent(event: Event) {
const payload = parseStreamEvent(event);
if (!payload) return;
@ -88,11 +68,6 @@ export function useEnvironmentDashboardStream(
options.onEnvironmentEvent(payload);
}
/**
* Applies one signal-level update without reloading the dashboard snapshot.
*
* @param event Browser SSE message carrying an EnvironmentEvent JSON payload.
*/
function handleEnvironmentSignal(event: Event) {
const payload = parseStreamEvent(event);
if (!payload) return;
@ -100,11 +75,6 @@ export function useEnvironmentDashboardStream(
options.onEnvironmentSignal(payload);
}
/**
* Notifies the page that the API replay buffer requires one fresh snapshot.
*
* @param event Browser SSE message carrying the replay-gap event envelope.
*/
function handleSnapshotRequired(event: Event) {
const payload = parseStreamEvent(event);
if (!payload) return;
@ -112,20 +82,12 @@ export function useEnvironmentDashboardStream(
options.onSnapshotRequired(payload);
}
/**
* Leaves dashboard state unchanged for keepalive events.
*/
function handleHeartbeat() {
if (connectionState.value === 'connecting') {
connectionState.value = 'open';
}
}
/**
* Records stream errors without starting polling or manual reconnect loops.
*
* @param event Optional API error payload when the server sends a typed error event.
*/
function handleError(event: Event) {
connectionState.value = 'error';
const payload = parseStreamEvent(event);
@ -135,12 +97,6 @@ export function useEnvironmentDashboardStream(
}
}
/**
* Parses JSON payloads from typed SSE events.
*
* @param event Browser EventSource event that may include string data.
* @returns Environment event payload, or undefined when malformed.
*/
function parseStreamEvent(event: Event) {
const data = (event as MessageEvent<string>).data;
if (!data) return undefined;
@ -151,11 +107,6 @@ export function useEnvironmentDashboardStream(
}
}
/**
* Keeps the replay cursor scoped to the current route instance.
*
* @param event Parsed environment stream event from the API.
*/
function rememberEventId(event: StreamEvent) {
if (event.eventId) {
lastEventId.value = event.eventId;

View File

@ -131,41 +131,21 @@ class FakeEventSource {
readonly listeners = new Map<string, Set<FakeEventSourceListener>>();
readonly url: string;
/**
* Records the stream URL used by the page so tests can dispatch typed SSE messages.
*
* @param url Browser EventSource URL built from the Admin API wrapper.
*/
constructor(url: string) {
this.url = url;
FakeEventSource.instances.push(this);
}
/**
* Stores typed SSE listeners registered by the production stream composable.
*
* @param type SSE event type supplied by the backend stream.
* @param listener Component-side event handler receiving the JSON payload.
*/
addEventListener(type: string, listener: FakeEventSourceListener) {
const listeners = this.listeners.get(type) ?? new Set();
listeners.add(listener);
this.listeners.set(type, listeners);
}
/**
* Marks the connection as closed so the unmount lifecycle can be asserted.
*/
close() {
this.closed = true;
}
/**
* Delivers one typed SSE message to the mounted page.
*
* @param type SSE event name sent by the API.
* @param payload JSON-serializable event payload.
*/
dispatch(type: string, payload: unknown) {
const event = new MessageEvent(type, {
data: JSON.stringify(payload),
@ -175,20 +155,11 @@ class FakeEventSource {
}
}
/**
* Removes an SSE listener during component cleanup.
*
* @param type SSE event type supplied by the backend stream.
* @param listener Previously registered component handler.
*/
removeEventListener(type: string, listener: FakeEventSourceListener) {
this.listeners.get(type)?.delete(listener);
}
}
/**
* Flushes Vue microtasks created by API promises and reactive state updates.
*/
async function flushDashboardUpdates() {
await Promise.resolve();
await nextTick();
@ -196,11 +167,6 @@ async function flushDashboardUpdates() {
await nextTick();
}
/**
* Builds a complete four-site dashboard fixture that mirrors the backend contract.
*
* @param includeMqttEvent Whether the API snapshot should already contain a MQTT-origin event.
*/
function createDashboardFixture(
includeMqttEvent = false,
): EnvironmentDashboardApi.EnvironmentDashboardResponse {

View File

@ -51,30 +51,18 @@ const streamState = environmentStream.connectionState;
onMounted(handleMounted);
onBeforeUnmount(handleBeforeUnmount);
/**
* Starts the initial snapshot load when the Vben route component mounts.
*/
function handleMounted() {
void loadDashboardSnapshot('initial');
}
/**
* Closes the SSE stream when route transitions remove this page.
*/
function handleBeforeUnmount() {
environmentStream.close();
}
/**
* Handles explicit user refresh without creating timer-based polling.
*/
function handleManualRefresh() {
void loadDashboardSnapshot('manual');
}
/**
* Runs the read-only backend self-check and replaces visible evidence with its response.
*/
async function handleSelfCheck() {
selfChecking.value = true;
errorText.value = '';
@ -87,11 +75,6 @@ async function handleSelfCheck() {
}
}
/**
* Loads a dashboard snapshot for first render, manual refresh, or replay gap recovery.
*
* @param reason Call origin that controls loading state and snapshot-required de-duping.
*/
async function loadDashboardSnapshot(reason: SnapshotLoadReason) {
if (reason === 'snapshot-required') {
if (snapshotRequestInFlight.value) return;
@ -114,22 +97,12 @@ async function loadDashboardSnapshot(reason: SnapshotLoadReason) {
}
}
/**
* Applies a full dashboard response while preserving still-valid selections.
*
* @param next Dashboard response from GET snapshot or POST self-check.
*/
function applyDashboard(next: EnvironmentDashboard) {
dashboard.value = next;
recentEvents.value = [...(next.events ?? [])];
ensureSelection(next);
}
/**
* Preserves selected service when possible, otherwise selects the first risky service.
*
* @param next Dashboard response that becomes the new source of truth.
*/
function ensureSelection(next: EnvironmentDashboard) {
const existingSite = selectedSiteId.value
? next.sites.find((site) => site.id === selectedSiteId.value)
@ -152,11 +125,6 @@ function ensureSelection(next: EnvironmentDashboard) {
selectedSignalId.value = fallbackService?.signals[0]?.id;
}
/**
* Updates selected site and moves service selection to that site's first service.
*
* @param siteId Site ID emitted by the left rail.
*/
function handleSiteSelect(siteId: string) {
selectedSiteId.value = siteId;
const site = dashboard.value?.sites.find((item) => item.id === siteId);
@ -165,61 +133,31 @@ function handleSiteSelect(siteId: string) {
selectedSignalId.value = service?.signals[0]?.id;
}
/**
* Updates selected service inside the active site.
*
* @param serviceId Service ID emitted by the topology panel.
*/
function handleServiceSelect(serviceId: string) {
const service = findService(selectedSite.value, serviceId);
selectedServiceId.value = service?.id;
selectedSignalId.value = service?.signals[0]?.id;
}
/**
* Adds a stream event to the visible bottom event list.
*
* @param event SSE event envelope sent by the API stream.
*/
function handleEnvironmentEvent(event: EnvironmentEvent) {
addRecentEvent(event);
}
/**
* Merges one signal update into the current dashboard tree without re-fetching.
*
* @param event SSE signal envelope sent by the API stream.
*/
function handleEnvironmentSignal(event: EnvironmentEvent) {
addRecentEvent(event);
applySignalEvent(event);
}
/**
* Runs one snapshot load when the API reports a replay buffer gap.
*
* @param event Snapshot-required event envelope from the API stream.
*/
function handleSnapshotRequired(event: EnvironmentEvent) {
addRecentEvent(event);
void loadDashboardSnapshot('snapshot-required');
}
/**
* Shows stream errors as evidence without starting a polling fallback.
*
* @param event Error event envelope from the API stream.
*/
function handleStreamError(event: EnvironmentEvent) {
addRecentEvent(event);
errorText.value = event.summary;
}
/**
* Applies one service or signal-level update in-place for visible topology state.
*
* @param event Parsed SSE environment-signal payload.
*/
function applySignalEvent(event: EnvironmentEvent) {
const site = dashboard.value?.sites.find((item) => item.id === event.siteId);
if (!site || !event.serviceId) return;
@ -245,11 +183,6 @@ function applySignalEvent(event: EnvironmentEvent) {
site.status = mapSiteStatus(site.nodes.map((item) => item.status));
}
/**
* Inserts a recent event newest-first while de-duping by event ID.
*
* @param event Event from the initial snapshot or SSE stream.
*/
function addRecentEvent(event: EnvironmentEvent) {
recentEvents.value = [
event,
@ -257,52 +190,26 @@ function addRecentEvent(event: EnvironmentEvent) {
].slice(0, 30);
}
/**
* Resolves the currently selected site from reactive IDs.
*
* @returns Selected site or undefined when the dashboard has not loaded.
*/
function resolveSelectedSite() {
return dashboard.value?.sites.find(
(site) => site.id === selectedSiteId.value,
);
}
/**
* Resolves the currently selected service from the selected site.
*
* @returns Selected service or undefined when no service is selected.
*/
function resolveSelectedService() {
return findService(selectedSite.value, selectedServiceId.value);
}
/**
* Resolves the currently selected signal from the selected service.
*
* @returns Selected signal or undefined when no signal is selected.
*/
function resolveSelectedSignal() {
return findSignal(selectedService.value, selectedSignalId.value);
}
/**
* Sorts events newest-first using their observed time while keeping stable IDs.
*
* @returns Event list used by the bottom stream panel.
*/
function resolveSortedEvents() {
return recentEvents.value.toSorted(
(left, right) => Date.parse(right.observedAt) - Date.parse(left.observedAt),
);
}
/**
* Finds the first non-green service as the default operational focus.
*
* @param sites Dashboard sites from the latest snapshot.
* @returns Preferred site/service pair or undefined when all sites are empty.
*/
function findFirstAttentionService(sites: EnvironmentSite[]) {
for (const site of sites) {
for (const node of site.nodes) {
@ -319,23 +226,10 @@ function findFirstAttentionService(sites: EnvironmentSite[]) {
return undefined;
}
/**
* Finds the first service under a site.
*
* @param site Site selected by route state or fallback selection.
* @returns First service in the site tree.
*/
function getFirstService(site?: EnvironmentSite) {
return site?.nodes.flatMap((node) => node.services)[0];
}
/**
* Finds one service by ID within a site.
*
* @param site Site tree to inspect.
* @param serviceId Service ID from user selection or persisted state.
* @returns Matching service when still present.
*/
function findService(site?: EnvironmentSite, serviceId?: string) {
if (!site || !serviceId) return undefined;
for (const node of site.nodes) {
@ -345,48 +239,21 @@ function findService(site?: EnvironmentSite, serviceId?: string) {
return undefined;
}
/**
* Finds one service by ID within a single node.
*
* @param node Node that owns service records.
* @param serviceId Service ID from an SSE event or user selection.
* @returns Matching service when present.
*/
function findServiceInNode(node: EnvironmentNode, serviceId: string) {
return node.services.find((service) => service.id === serviceId);
}
/**
* Finds the node that owns a service ID.
*
* @param site Site tree to inspect.
* @param serviceId Service ID from the SSE signal envelope.
* @returns Owning node when the service exists.
*/
function findNodeByServiceId(site: EnvironmentSite, serviceId: string) {
return site.nodes.find((node) =>
node.services.some((service) => service.id === serviceId),
);
}
/**
* Finds one signal by ID within a service.
*
* @param service Service that owns signal records.
* @param signalId Signal ID from selection or an SSE event.
* @returns Matching signal when present.
*/
function findSignal(service?: EnvironmentService, signalId?: string) {
if (!service || !signalId) return undefined;
return service.signals.find((signal) => signal.id === signalId);
}
/**
* Converts event source metadata into signal source kinds supported by node badges.
*
* @param sourceKind Event source kind from the SSE envelope.
* @returns Signal source kind stored on the affected signal.
*/
function mapEventSourceToSignalSource(
sourceKind: EnvironmentEvent['sourceKind'],
): EnvironmentSignal['sourceKind'] {
@ -394,12 +261,6 @@ function mapEventSourceToSignalSource(
return sourceKind;
}
/**
* Picks the strongest status for a node after a signal update.
*
* @param statuses Service statuses under the same node.
* @returns Strongest health status according to dashboard severity rules.
*/
function pickWorstHealthStatus(
statuses: EnvironmentHealthStatus[],
): EnvironmentHealthStatus {
@ -421,12 +282,6 @@ function pickWorstHealthStatus(
return worst;
}
/**
* Maps node health back into the coarser site status union.
*
* @param statuses Node statuses under the same site.
* @returns Site-level status that never marks unknown/unwired integrations green.
*/
function mapSiteStatus(statuses: EnvironmentHealthStatus[]) {
const worst = pickWorstHealthStatus(statuses);
if (worst === 'ok') return 'online';
@ -437,12 +292,6 @@ function mapSiteStatus(statuses: EnvironmentHealthStatus[]) {
return 'unknown';
}
/**
* Normalizes unknown thrown values from request wrappers into alert text.
*
* @param error Error thrown by the request client or runtime code.
* @returns User-visible error summary.
*/
function getErrorMessage(error: unknown) {
if (error instanceof Error) return error.message;
if (typeof error === 'string') return error;

View File

@ -56,9 +56,6 @@ const cropImage = async () => {
}
};
/**
* 下载图片
*/
const downloadImage = () => {
if (!cropperImg.value) return;

View File

@ -440,9 +440,6 @@ function onSubmit(values: Record<string, any>) {
}
function handleSetFormValue() {
/**
* 设置表单值(多个)
*/
baseFormApi.setValues({
checkboxGroup: ['1'],
datePicker: dayjs('2022-01-01'),

View File

@ -46,9 +46,6 @@ vi.mock('#/components/ktTable', () => ({
KtTable: defineComponent({
name: 'MockLegacyKtTable',
inheritAttrs: false,
/**
* Renders the real parent-owned header callbacks and records legacy table props.
*/
setup(_, { attrs, slots }) {
return () => {
mocks.legacyTableProps.push(attrs);
@ -68,7 +65,6 @@ vi.mock('antdv-next', () => ({
},
Spin: defineComponent({
name: 'MockSpin',
/** Keeps the legacy table subtree mounted for regression assertions. */
setup(_, { slots }) {
return () => h('div', slots.default?.());
},
@ -80,7 +76,6 @@ vi.mock('antdv-next', () => ({
items: Array,
},
emits: ['update:activeKey'],
/** Exposes every configured tab through a deterministic clickable button. */
setup(props, { emit }) {
return () =>
h(
@ -102,7 +97,6 @@ vi.mock('antdv-next', () => ({
}),
Tag: defineComponent({
name: 'MockTag',
/** Renders tag text without changing the parent structure. */
setup(_, { slots }) {
return () => h('span', slots.default?.());
},
@ -117,7 +111,6 @@ vi.mock('./AccountMessagePushPanel', () => ({
selfId: String,
title: Function,
},
/** Renders both inherited header callbacks to prove the ownership boundary. */
setup(props) {
return () =>
h('section', { 'data-testid': 'message-push-panel' }, [
@ -128,7 +121,6 @@ vi.mock('./AccountMessagePushPanel', () => ({
}),
}));
/** Creates the current route account with an unsafe-integer string self ID. */
function createAccount(): QqbotApi.Account {
return {
connectStatus: 'online',

View File

@ -50,7 +50,6 @@ const mocks = vi.hoisted(() => {
};
});
/** Creates one fluent no-op Zod rule for schema-focused tests. */
function createRule(): any {
const rule: any = {};
for (const method of [
@ -72,7 +71,6 @@ vi.mock('#/adapter/form', () => ({
mocks.formOptions = options;
const Form = defineComponent({
name: 'MockBindingForm',
/** Renders the schema's real local target-picker component and model wiring. */
setup() {
return () => {
const targetField = mocks.formOptions.schema.find(
@ -113,7 +111,6 @@ vi.mock('@vben/common-ui', () => ({
mocks.modalOptions = options;
const Modal = defineComponent({
name: 'MockBindingModal',
/** Renders modal content while lifecycle remains API-controlled. */
setup(_, { slots }) {
return () => h('section', slots.default?.());
},
@ -134,7 +131,6 @@ vi.mock('./MessagePushTargetPicker', () => ({
value: Array,
},
emits: ['update:value'],
/** Exposes one button that exercises the Vben `modelPropName:value` path. */
setup(_, { emit }) {
return () =>
h(
@ -150,9 +146,6 @@ vi.mock('./MessagePushTargetPicker', () => ({
);
},
}),
/**
* Mirrors the production helper so modal tests retain the exact validation boundary.
*/
isValidMessagePushTargetId: (targetId: string) =>
/^[1-9]\d{4,19}$/.test(targetId),
}));
@ -162,7 +155,6 @@ vi.mock('#/api/qqbot/message-push', () => ({
updateAccountMessagePushBinding: mocks.update,
}));
/** Creates two source families to test compatible-template filtering. */
function createSubscriptions(): QqbotMessagePushApi.MessageSubscriptionView[] {
return [
{
@ -219,7 +211,6 @@ function createSubscriptions(): QqbotMessagePushApi.MessageSubscriptionView[] {
];
}
/** Creates templates in both source families. */
function createTemplates(): QqbotMessagePushApi.MessageTemplateView[] {
return [
{
@ -249,7 +240,6 @@ function createTemplates(): QqbotMessagePushApi.MessageTemplateView[] {
];
}
/** Creates an edit row whose targets retain API-provided names as strings. */
function createBinding(): QqbotMessagePushApi.QqbotMessagePublishBindingView {
return {
available: true,
@ -283,7 +273,6 @@ function createBinding(): QqbotMessagePushApi.QqbotMessagePublishBindingView {
};
}
/** Creates one manually resolved promise for async session-race assertions. */
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((done) => {
@ -292,7 +281,6 @@ function deferred<T>() {
return { promise, resolve };
}
/** Mounts the modal with one implicit publisher and page-owned metadata. */
function mountModal(selfId = '10000000000000001') {
return mount(AccountMessagePushModal, {
props: {

View File

@ -64,7 +64,6 @@ export default defineComponent({
},
},
emits: ['saved'],
/** Owns one account-scoped create/edit session without selecting another account. */
setup(props, { emit, expose }) {
const editingId = ref<string>();
const modalOpen = ref(false);
@ -75,11 +74,6 @@ export default defineComponent({
commonConfig: {
labelClass: 'w-24',
},
/**
* Keeps the selected template only while it remains source-compatible.
* @param values - Current form values after the update.
* @param fieldsChanged - Exact form fields changed by this event.
*/
async handleValuesChange(values, fieldsChanged) {
if (!fieldsChanged.includes('subscriptionId')) return;
selectedSubscriptionId.value =
@ -100,17 +94,12 @@ export default defineComponent({
showDefaultActions: false,
wrapperClass: 'grid-cols-1',
});
/** Derives the title from the current isolated binding identity. */
const modalTitle = computed(() =>
editingId.value ? '编辑消息推送' : '新增消息推送',
);
const [Modal, modalApi] = useVbenModal({
class: 'w-[760px]',
fullscreenButton: false,
/**
* Persists the active session while containing failures at ModalApi's
* non-awaited callback boundary.
*/
async onConfirm() {
try {
await submit();
@ -118,10 +107,6 @@ export default defineComponent({
// The request/form layer already presents the persistence error.
}
},
/**
* Restores only the latest session after destroy-on-close content mounts.
* @param isOpen - Whether the modal content is currently mounted.
*/
async onOpenChange(isOpen: boolean) {
modalOpen.value = isOpen;
if (!isOpen) return;
@ -138,7 +123,6 @@ export default defineComponent({
},
});
/** Opens a fresh four-field binding session for the current account path. */
function openCreate() {
editingId.value = undefined;
beginSession({
@ -149,10 +133,6 @@ export default defineComponent({
});
}
/**
* Opens an edit session with only API-provided editable binding values.
* @param row - Account binding selected from the panel-owned KtTable.
*/
function openEdit(row: QqbotMessagePushApi.QqbotMessagePublishBindingView) {
editingId.value = row.id;
beginSession({
@ -167,10 +147,6 @@ export default defineComponent({
});
}
/**
* Stores one revision-bound modal payload before opening its content.
* @param values - Exact four editable values for the new modal session.
*/
function beginSession(values: AccountMessagePushFormValues) {
sessionRevision += 1;
sessionSelfId = props.selfId;
@ -184,17 +160,12 @@ export default defineComponent({
.open();
}
/**
* Resets the mounted form before installing one isolated session.
* @param values - Exact values captured before opening the modal.
*/
async function resetForm(values: AccountMessagePushFormValues) {
await formApi.resetForm();
await formApi.setValues(values);
await formApi.resetValidate();
}
/** Validates and persists one session without putting selfId in the body. */
async function submit() {
const revision = sessionRevision;
const selfId = sessionSelfId;
@ -222,9 +193,6 @@ export default defineComponent({
}
}
/**
* Invalidates an open old-account session before its path identity changes.
*/
async function invalidateForSelfIdChange() {
sessionRevision += 1;
sessionSelfId = '';
@ -251,12 +219,6 @@ export default defineComponent({
},
});
/**
* Builds the exact account-binding schema with the local controlled picker.
* @param props - Page-owned subscriptions, templates, and target candidates.
* @param selectedSubscriptionId - Current subscription driving template options.
* @returns Four fields in subscription/template/targets/enabled order.
*/
function createFormSchema(
props: Readonly<{
selfId: string;
@ -329,11 +291,6 @@ function createFormSchema(
];
}
/**
* Formats one subscription while retaining server validity information.
* @param subscription - Global subscription available to the current account.
* @returns Select label with a stable invalid reason when applicable.
*/
function formatSubscriptionLabel(
subscription: QqbotMessagePushApi.MessageSubscriptionView,
): string {
@ -344,12 +301,6 @@ function formatSubscriptionLabel(
return `${subscription.name} · ${subscription.sourceName}${reason}`;
}
/**
* Returns templates whose source matches the selected global subscription.
* @param props - Current page-owned subscription/template collections.
* @param subscriptionId - Exact selected subscription string ID.
* @returns Source-compatible template rows in server order.
*/
function compatibleTemplates(
props: Readonly<{
subscriptions: QqbotMessagePushApi.MessageSubscriptionView[];
@ -364,13 +315,6 @@ function compatibleTemplates(
return props.templates.filter((template) => template.sourceKey === sourceKey);
}
/**
* Checks template/source compatibility without interpreting numeric IDs.
* @param props - Current subscription/template collections.
* @param subscriptionId - Selected subscription string ID.
* @param templateId - Existing template string ID.
* @returns Whether both rows exist and share the exact source key.
*/
function isTemplateCompatible(
props: Readonly<{
subscriptions: QqbotMessagePushApi.MessageSubscriptionView[];
@ -384,12 +328,6 @@ function isTemplateCompatible(
);
}
/**
* Validates the exact four-field payload before invoking the account API.
* @param props - Current source-compatible metadata.
* @param values - Form-owned binding values.
* @returns A clean API payload or undefined when any ID/target is invalid.
*/
function normalizeBindingPayload(
props: Readonly<{
subscriptions: QqbotMessagePushApi.MessageSubscriptionView[];

View File

@ -32,11 +32,9 @@ const mocks = vi.hoisted(() => {
},
tableOptions: undefined as any,
};
/** Installs the real list callback only after the rendered KtTable registers. */
state.registerTable.mockImplementation(() => {
state.registeredTableOptions = state.tableOptions;
});
/** Reloads through the registered list callback and records the applied page. */
state.tableApi.reload.mockImplementation(async () => {
if (!state.registeredTableOptions) {
throw new Error('[MockKtTable]: table is not registered yet.');
@ -86,7 +84,6 @@ vi.mock('#/components/ktTable', () => ({
KtTable: defineComponent({
name: 'MockAccountMessagePushKtTable',
emits: ['register'],
/** Registers first, then renders inherited account header callbacks. */
setup(_, { emit, slots }) {
emit('register', {});
return () =>
@ -113,7 +110,6 @@ vi.mock('./AccountMessagePushModal', () => ({
templates: Array,
},
emits: ['saved'],
/** Exposes create/edit commands plus a deterministic save event. */
setup(_, { emit, expose }) {
expose({
openCreate: mocks.modalOpenCreate,
@ -141,7 +137,6 @@ vi.mock('#/api/qqbot/message-push', () => ({
setAccountMessagePushBindingEnabled: mocks.api.toggleBinding,
}));
/** Creates one unsafe-integer string-ID binding row. */
function createBinding(
overrides: Partial<QqbotMessagePushApi.QqbotMessagePublishBindingView> = {},
): QqbotMessagePushApi.QqbotMessagePublishBindingView {
@ -171,7 +166,6 @@ function createBinding(
};
}
/** Creates one global subscription fixture for the account modal. */
function createSubscription(
id = '20000000000000001',
): QqbotMessagePushApi.MessageSubscriptionView {
@ -194,7 +188,6 @@ function createSubscription(
};
}
/** Creates one source-compatible template fixture. */
function createTemplate(
id = '50000000000000001',
): QqbotMessagePushApi.MessageTemplateView {
@ -212,7 +205,6 @@ function createTemplate(
};
}
/** Creates a manually controlled promise for stale-response assertions. */
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((done) => {
@ -221,7 +213,6 @@ function deferred<T>() {
return { promise, resolve };
}
/** Mounts the panel with visible account header callbacks. */
function mountPanel(selfId = '10000000000000001') {
return mount(AccountMessagePushPanel, {
props: {

View File

@ -66,7 +66,6 @@ export default defineComponent({
type: Function as PropType<() => VNodeChild>,
},
},
/** Owns one permission-gated account binding table and per-account option cache. */
setup(props) {
const { hasAccessByCodes } = useAccess();
const canList = hasAccessByCodes([PERMISSIONS.list]);
@ -127,10 +126,6 @@ export default defineComponent({
];
const api: KtTableApi<QqbotMessagePushApi.QqbotMessagePublishBindingView> =
{
/**
* Adapts the account binding array while returning the latest page for stale requests.
* @returns Strict KtTable page retaining every string identifier.
*/
list: async () => {
const revision = loadRevision;
const selfId = props.selfId;
@ -204,35 +199,20 @@ export default defineComponent({
size: 'small',
});
/** Opens a blank binding session for this panel's implicit account. */
function openCreate() {
modalRef.value?.openCreate();
}
/**
* Opens one existing binding without deriving another account identity.
* @param row - Binding selected from this account's KtTable.
*/
function openEdit(row: QqbotMessagePushApi.QqbotMessagePublishBindingView) {
modalRef.value?.openEdit(row);
}
/**
* Builds account-binding removal confirmation text.
* @param row - Binding awaiting removal.
* @returns Confirmation containing the global subscription name.
*/
function getDeleteConfirm(
row: QqbotMessagePushApi.QqbotMessagePublishBindingView,
): string {
return `确认解绑消息订阅「${row.subscriptionName}」吗?`;
}
/**
* Toggles one binding and reloads only the mutable binding list after success.
* @param row - Binding whose enabled state is inverted.
* @param context - Registered KtTable action context.
*/
async function handleToggle(
row: QqbotMessagePushApi.QqbotMessagePublishBindingView,
context: KtTableContext<QqbotMessagePushApi.QqbotMessagePublishBindingView>,
@ -245,11 +225,6 @@ export default defineComponent({
await context.reload();
}
/**
* Removes one binding and reloads only the mutable binding list after success.
* @param row - Binding confirmed for account-scoped removal.
* @param context - Registered KtTable action context.
*/
async function handleDelete(
row: QqbotMessagePushApi.QqbotMessagePublishBindingView,
context: KtTableContext<QqbotMessagePushApi.QqbotMessagePublishBindingView>,
@ -258,16 +233,10 @@ export default defineComponent({
await context.reload();
}
/** Reloads only bindings after one successful modal persistence. */
async function handleModalSaved() {
await tableApi.reload();
}
/**
* Loads the complete subscription/template collection in bounded 100-row pages.
* @param loader - One strict page caller receiving pageNo/pageSize.
* @returns Concatenated server rows until total is reached or progress stops.
*/
async function loadAllPages<Row>(
loader: (params: {
pageNo: number;
@ -293,11 +262,6 @@ export default defineComponent({
return rows;
}
/**
* Loads per-account modal metadata once and applies only the latest revision.
* @param selfId - Exact account identity captured for this load.
* @param revision - Monotonic revision assigned by `loadAccount`.
*/
async function loadMetadata(selfId: string, revision: number) {
targetOptionsLoading.value = canLoadTargets;
const [subscriptionResult, templateResult, targetResult] =
@ -321,10 +285,6 @@ export default defineComponent({
targetOptionsLoading.value = false;
}
/**
* Starts exactly one logical load for a new nonempty account identity.
* @param selfId - Current implicit account identity.
*/
async function loadAccount(selfId: string) {
const revision = ++loadRevision;
latestBindingPage = { items: [], total: 0 };
@ -341,23 +301,14 @@ export default defineComponent({
]);
}
/** Starts the one authorized initial account load after table registration. */
function activatePanel() {
if (canList && props.selfId) void loadAccount(props.selfId);
}
/** Invalidates any pending request when this tab's panel unmounts. */
function invalidatePendingLoad() {
loadRevision += 1;
}
/**
* Renders source, targets, availability, and enabled state.
* @param slot - KtTable body-cell slot payload.
* @param slot.column - Current data column.
* @param slot.record - Current account binding.
* @returns Custom cell content or undefined for native field rendering.
*/
function renderBodyCell(slot: {
column: TableColumnType<QqbotMessagePushApi.QqbotMessagePublishBindingView>;
record: QqbotMessagePushApi.QqbotMessagePublishBindingView;

View File

@ -11,7 +11,6 @@ import MessagePushTargetPicker, {
isValidMessagePushTargetId,
} from './MessagePushTargetPicker';
/** Creates type-scoped known group/private target candidates. */
function createOptions(): QqbotMessagePushApi.QqbotMessagePushTargetOption[] {
return [
{
@ -27,7 +26,6 @@ function createOptions(): QqbotMessagePushApi.QqbotMessagePushTargetOption[] {
];
}
/** Mounts the controlled picker with optional contract overrides. */
function mountPicker(
overrides: Partial<{
available: boolean;
@ -49,7 +47,6 @@ function mountPicker(
});
}
/** Reads the newest controlled value emitted by the picker. */
function latestValue(
wrapper: ReturnType<typeof mountPicker>,
): QqbotMessagePushApi.QqbotMessagePublishTargetInput[] {

View File

@ -27,11 +27,6 @@ export interface MessagePushTargetPickerProps {
value: QqbotMessagePushApi.QqbotMessagePublishTargetInput[];
}
/**
* Validates one QQ group/user target without numeric conversion.
* @param targetId - Trimmed target identifier supplied by the Select runtime.
* @returns Whether the identifier is 520 digits and has no leading zero.
*/
export function isValidMessagePushTargetId(targetId: string): boolean {
return /^[1-9]\d{4,19}$/.test(targetId);
}
@ -69,38 +64,23 @@ export default defineComponent({
},
},
emits: {
/**
* Emits one normalized controlled target collection.
* @param value - Group-first then private targets with string IDs only.
* @returns Whether the payload remains an array for Vue emit validation.
*/
'update:value': (
value: QqbotMessagePushApi.QqbotMessagePublishTargetInput[],
) => Array.isArray(value),
},
/** Owns only selector remount revisions; all durable values stay controlled. */
setup(props, { emit }) {
const groupRevision = ref(0);
const privateRevision = ref(0);
/** Derives the currently controlled group IDs without cloning other fields. */
const groupIds = computed(() => targetIdsForType(props.value, 'group'));
/** Derives the currently controlled private IDs independently. */
const privateIds = computed(() => targetIdsForType(props.value, 'private'));
/** Maps only server-returned group choices to raw Select values. */
const groupOptions = computed(() =>
createSelectOptions(props.options, 'group'),
);
/** Maps only server-returned private choices to raw Select values. */
const privateOptions = computed(() =>
createSelectOptions(props.options, 'private'),
);
/**
* Cleans one installed Select update and remounts only rejected input.
* @param targetType - Selector whose controlled string values changed.
* @param runtimeValue - Raw installed Select emission; may contain bad runtime values.
*/
function handleValueUpdate(targetType: TargetType, runtimeValue: unknown) {
const targetIds = normalizeTargetIds(runtimeValue);
if (containsRejectedTarget(runtimeValue, targetIds)) {
@ -164,12 +144,6 @@ export default defineComponent({
},
});
/**
* Returns controlled target IDs for exactly one target type.
* @param targets - Current API/form-controlled targets.
* @param targetType - Group or private selector identity.
* @returns Original string IDs in their controlled order.
*/
function targetIdsForType(
targets: QqbotMessagePushApi.QqbotMessagePublishTargetInput[],
targetType: TargetType,
@ -179,12 +153,6 @@ function targetIdsForType(
.map((target) => target.targetId);
}
/**
* Maps one type's API candidates to plain string-value Select options.
* @param options - Current OneBot candidate snapshot.
* @param targetType - Type allowed in the target selector.
* @returns Type-scoped candidates retaining their server labels.
*/
function createSelectOptions(
options: QqbotMessagePushApi.QqbotMessagePushTargetOption[],
targetType: TargetType,
@ -198,12 +166,6 @@ function createSelectOptions(
}));
}
/**
* Searches one known candidate by case-insensitive label and string ID.
* @param input - Search text entered into the installed Select.
* @param option - Candidate option produced by `createSelectOptions`.
* @returns Whether its label or target ID contains the query.
*/
function filterTargetOption(
input: string,
option: Record<string, unknown>,
@ -215,11 +177,6 @@ function filterTargetOption(
);
}
/**
* Sanitizes the installed tags-mode payload without coercing non-string values.
* @param runtimeValue - Raw `update:value` payload from Antdv Next.
* @returns Trimmed, valid, deduplicated string IDs in input order.
*/
function normalizeTargetIds(runtimeValue: unknown): string[] {
if (!Array.isArray(runtimeValue)) return [];
const seen = new Set<string>();
@ -234,12 +191,6 @@ function normalizeTargetIds(runtimeValue: unknown): string[] {
return result;
}
/**
* Detects internal Select state that differs from the cleaned controlled value.
* @param runtimeValue - Raw installed component emission.
* @param normalized - Accepted target IDs.
* @returns Whether the affected Select must remount to discard invalid state.
*/
function containsRejectedTarget(
runtimeValue: unknown,
normalized: string[],
@ -252,14 +203,6 @@ function containsRejectedTarget(
);
}
/**
* Rebuilds group-first/private-second targets after one selector update.
* @param current - Controlled value carrying existing API name snapshots.
* @param options - Current type-scoped candidate labels.
* @param changedType - Selector whose IDs are replaced.
* @param changedIds - Sanitized replacement IDs.
* @returns Normalized targets with no cross-type name leakage.
*/
function mergeTargets(
current: QqbotMessagePushApi.QqbotMessagePublishTargetInput[],
options: QqbotMessagePushApi.QqbotMessagePushTargetOption[],
@ -291,14 +234,6 @@ function mergeTargets(
);
}
/**
* Resolves a display name from only the same target type and identifier.
* @param current - Existing API/form values with optional name snapshots.
* @param options - Current OneBot candidates.
* @param targetType - Exact group/private namespace.
* @param targetId - Exact string target identifier.
* @returns Current candidate label, same-type existing name, or undefined.
*/
function resolveTargetName(
current: QqbotMessagePushApi.QqbotMessagePublishTargetInput[],
options: QqbotMessagePushApi.QqbotMessagePushTargetOption[],

View File

@ -529,9 +529,6 @@ export default defineComponent({
});
}
/**
* Opens the route that will own the NapCat WebUI session lifecycle.
*/
function openNapcatWebui(row: QqbotApi.Account) {
void router.push({
name: 'QqBotAccountNapcatWebui',

View File

@ -12,18 +12,9 @@ const accountRoot = resolve(
const readAccountSource = (relativePath: string) =>
readFileSync(resolve(accountRoot, relativePath), 'utf8');
/**
* Reads repository-root files that define browser-facing deployment boundaries.
*
* @param relativePath - Repository-relative path to read.
* @returns Source text for boundary assertions.
*/
const readRepoSource = (relativePath: string) =>
readFileSync(resolve(cwd(), relativePath), 'utf8');
/**
* Reads QQBot router module source for route boundary assertions.
*/
const readRouteSource = (relativePath: string) =>
readFileSync(
resolve(

View File

@ -19,9 +19,6 @@ const ATag = Tag as any;
export default defineComponent({
name: 'QqBotAccountNapcatWebui',
/**
* Wires route account identity to the page-owned NapCat WebUI session.
*/
setup() {
const route = useRoute();
const router = useRouter();
@ -48,32 +45,18 @@ export default defineComponent({
{ immediate: true },
);
/**
* Navigates back to the account list route.
*/
function goBack() {
void router.push({ name: 'QqBotAccount' });
}
/**
* Reopens the gateway session for the current route account.
*/
function reopen() {
void session.open();
}
/**
* Closes the current gateway session while staying on this page.
*/
function closeSession() {
void session.revoke();
}
/**
* Renders the floating metadata panel without taking layout space from the iframe.
*
* @returns Overlay card content for the current session metadata and actions.
*/
const renderFloatingCard = () => {
return (
<div class="qqbot-napcat-webui__floating-card">
@ -114,11 +97,6 @@ export default defineComponent({
);
};
/**
* Renders the main state area for loading, error, revoked, and ready states.
*
* @returns TSX content for the current gateway state.
*/
const renderBody = () => {
if (session.state.value === 'ready' && session.iframeUrl.value) {
return (
@ -161,11 +139,6 @@ export default defineComponent({
);
};
/**
* Renders the page root required by Vben route transitions.
*
* @returns The stable single-root page shell.
*/
const renderPage = () => {
return (
<div class="qqbot-napcat-webui-page">
@ -181,23 +154,11 @@ export default defineComponent({
},
});
/**
* Normalizes a vue-router route param into a single trimmed account id.
*
* @param value - Raw route param value.
* @returns Trimmed account id or an empty string.
*/
function normalizeRouteParam(value: unknown) {
if (Array.isArray(value)) return `${value[0] || ''}`.trim();
return `${value || ''}`.trim();
}
/**
* Formats the gateway expiry timestamp for compact page metadata.
*
* @param value - Epoch milliseconds returned by the Gateway heartbeat.
* @returns Local date-time text, or an empty string when the value is absent.
*/
function formatGatewayExpiresAt(value?: number) {
if (!value) return '';
const date = new Date(value);
@ -210,12 +171,6 @@ function formatGatewayExpiresAt(value?: number) {
)}`;
}
/**
* Maps gateway state to a compact Ant Design status tag.
*
* @param state - Current gateway lifecycle state.
* @returns Tag color and Chinese label for the page header.
*/
function getStatusMeta(state: NapcatWebuiGatewaySessionState) {
const statusMap = {
error: { color: 'error', label: '异常' },

View File

@ -134,12 +134,6 @@ vi.mock('#/api/qqbot/napcat', () => ({
revokeQqbotNapcatWebuiSession: vi.fn(),
}));
/**
* Creates a mocked gateway session payload for page lifecycle tests.
*
* @param overrides - Partial fields that should replace the default fixture.
* @returns A gateway session payload shaped like the backend response.
*/
function createSessionFixture(overrides = {}) {
return {
account: {

View File

@ -19,12 +19,6 @@ export type NapcatWebuiGatewaySessionState =
| 'ready'
| 'revoked';
/**
* Owns one NapCat WebUI gateway session for the current account page lifetime.
*
* @param accountId - Reactive account id read from the route.
* @returns Reactive gateway session state and lifecycle actions.
*/
export function useNapcatWebuiGatewaySession(accountId: Ref<string>) {
const account = ref<QqbotNapcatApi.WebuiGatewaySessionAccount>();
const container = ref<QqbotNapcatApi.WebuiGatewaySessionContainer>();
@ -42,11 +36,6 @@ export function useNapcatWebuiGatewaySession(accountId: Ref<string>) {
onBeforeUnmount(handleBeforeUnmount);
/**
* Opens a new gateway session after revoking any existing local session.
*
* @returns A promise that settles after the open attempt finishes.
*/
async function open() {
const nextAccountId = accountId.value.trim();
heartbeat.pause();
@ -89,11 +78,6 @@ export function useNapcatWebuiGatewaySession(accountId: Ref<string>) {
}
}
/**
* Revokes the current gateway session and clears local iframe credentials.
*
* @returns A promise that settles after local state has been cleared.
*/
async function revoke() {
openToken += 1;
heartbeat.pause();
@ -118,11 +102,6 @@ export function useNapcatWebuiGatewaySession(accountId: Ref<string>) {
}
}
/**
* Sends one heartbeat for the active ready session and updates its expiry.
*
* @returns A promise that settles after the heartbeat attempt finishes.
*/
async function sendHeartbeat() {
const currentSessionId = sessionId.value;
if (state.value !== 'ready' || !currentSessionId) return;
@ -142,11 +121,6 @@ export function useNapcatWebuiGatewaySession(accountId: Ref<string>) {
}
}
/**
* Applies the gateway session payload returned by the backend.
*
* @param result - Gateway session payload containing iframe URL and metadata.
*/
function applyGatewaySession(result: QqbotNapcatApi.WebuiGatewaySession) {
account.value = result.account;
container.value = result.container;
@ -155,9 +129,6 @@ export function useNapcatWebuiGatewaySession(accountId: Ref<string>) {
sessionId.value = result.sessionId;
}
/**
* Clears all local data that can keep a gateway session reachable.
*/
function clearGatewaySession() {
account.value = undefined;
container.value = undefined;
@ -166,12 +137,6 @@ export function useNapcatWebuiGatewaySession(accountId: Ref<string>) {
sessionId.value = '';
}
/**
* Revokes a session created after this composable was already invalidated.
*
* @param staleSessionId - Session id returned by an outdated open request.
* @returns A promise that ignores revoke failures because the page is leaving.
*/
async function revokeDetachedSession(staleSessionId: string) {
try {
await revokeQqbotNapcatWebuiSession(staleSessionId);
@ -180,9 +145,6 @@ export function useNapcatWebuiGatewaySession(accountId: Ref<string>) {
}
}
/**
* Marks the composable disposed and starts best-effort session revocation.
*/
function handleBeforeUnmount() {
disposed = true;
void revoke();
@ -201,13 +163,6 @@ export function useNapcatWebuiGatewaySession(accountId: Ref<string>) {
};
}
/**
* Extracts a user-facing error message from unknown rejection values.
*
* @param error - Unknown error thrown by the request client.
* @param fallback - Message to show when the error has no readable text.
* @returns A Chinese or backend-provided message for page alerts.
*/
function getErrorMessage(error: unknown, fallback: string) {
if (error instanceof Error && error.message) return error.message;
if (typeof error === 'string' && error.trim()) return error.trim();

View File

@ -39,9 +39,6 @@ export default defineComponent({
{ immediate: true },
);
/**
* Loads sanitized runtime profile evidence for the selected account.
*/
async function loadDetail() {
if (!props.account?.id) return;
loading.value = true;
@ -52,19 +49,11 @@ export default defineComponent({
}
}
/**
* Emits both drawer close contracts used by existing QQBot views.
*/
function closeDrawer() {
emit('update:open', false);
emit('close');
}
/**
* Renders compact key/value evidence rows without exposing interactive controls.
* @param label - Human readable field label.
* @param value - Runtime evidence value already sanitized by the API.
*/
const renderField = (label: string, value: unknown) => {
return (
<div class="grid grid-cols-[120px_1fr] gap-3 border-b border-solid border-border py-2 text-sm">
@ -74,11 +63,6 @@ export default defineComponent({
);
};
/**
* Renders a JSON evidence block for profile sections.
* @param title - Section title shown above the evidence block.
* @param value - Sanitized profile object returned by the API.
*/
const renderJsonBlock = (title: string, value: unknown) => {
if (!value) return null;
return (
@ -91,11 +75,6 @@ export default defineComponent({
);
};
/**
* Converts scalar runtime evidence into stable display text.
* @param value - Sanitized value from profile detail.
* @returns Display text for compact rows.
*/
function formatValue(value: unknown) {
if (value === undefined || value === null || value === '') return '-';
if (typeof value === 'object') return JSON.stringify(value);

View File

@ -30,11 +30,9 @@ const mocks = vi.hoisted(() => {
},
tableOptions: undefined as any,
};
/** Installs the list path only when the rendered KtTable emits registration. */
state.registerTable.mockImplementation(() => {
state.registeredTableOptions = state.tableOptions;
});
/** Reloads through the registered table options and returns the caller result. */
state.tableApi.reload.mockImplementation(async () => {
if (!state.registeredTableOptions) {
throw new Error('[MockKtTable]: table is not registered yet.');
@ -58,7 +56,6 @@ vi.mock('@vben/common-ui', () => ({
Page: defineComponent({
name: 'MockPage',
props: { autoContentHeight: Boolean },
/** Renders a stable marker exposing the root height contract. */
setup(props, { slots }) {
return () =>
h(
@ -76,7 +73,6 @@ vi.mock('@vben/common-ui', () => ({
vi.mock('antdv-next', () => ({
Tag: defineComponent({
name: 'MockTag',
/** Renders enabled-state tag text for the body-cell contract. */
setup(_, { slots }) {
return () => h('span', slots.default?.());
},
@ -87,7 +83,6 @@ vi.mock('#/components/ktTable', () => ({
KtTable: defineComponent({
name: 'MockKtTable',
emits: ['register'],
/** Registers the table boundary before rendering its explicit refresh control. */
setup(_, { emit, slots }) {
emit('register', {});
return () =>
@ -117,7 +112,6 @@ vi.mock('./components/MessageSubscriptionModal', () => ({
default: defineComponent({
name: 'MockMessageSubscriptionModal',
emits: ['saved'],
/** Exposes create/edit commands and one deterministic saved trigger. */
setup(_, { emit, expose }) {
expose({
openCreate: mocks.modalOpenCreate,
@ -144,7 +138,6 @@ vi.mock('#/api/qqbot/message-push', () => ({
setMessageSubscriptionEnabled: mocks.api.toggle,
}));
/** Creates one table row whose identifiers exceed JavaScript's safe integer. */
function createRow(): QqbotMessagePushApi.MessageSubscriptionView {
return {
createTime: '2026-07-24 10:00:00',

View File

@ -33,9 +33,6 @@ const AKtTable = KtTable as any;
export default defineComponent({
name: 'QqBotMessageSubscriptionList',
/**
* Owns the permission-gated subscription table, metadata cache, and row reloads.
*/
setup() {
const { hasAccessByCodes } = useAccess();
const canList = hasAccessByCodes(['QqBot:MessageSubscription:List']);
@ -85,7 +82,6 @@ export default defineComponent({
},
];
const api: KtTableApi<QqbotMessagePushApi.MessageSubscriptionView> = {
/** Passes the caller's strict `{ items, total }` page through unchanged. */
list: async (params) => await getMessageSubscriptionList(params),
};
const buttons: Array<
@ -170,35 +166,20 @@ export default defineComponent({
tableTitle: '消息订阅',
});
/** Opens a blank modal session through the page-owned exposed ref. */
function openCreate() {
modalRef.value?.openCreate();
}
/**
* Opens one row without copying page-only runtime state into the modal.
* @param row - Subscription selected from KtTable.
*/
function openEdit(row: QqbotMessagePushApi.MessageSubscriptionView) {
modalRef.value?.openEdit(row);
}
/**
* Builds KtTable's delete confirmation text for one subscription.
* @param row - Subscription awaiting delete confirmation.
* @returns Human-readable confirmation text containing the row name.
*/
function getDeleteConfirm(
row: QqbotMessagePushApi.MessageSubscriptionView,
) {
return `确认删除消息订阅「${row.name}」吗?`;
}
/**
* Toggles one row and reloads only that table context after success.
* @param row - Subscription whose enabled state is inverted.
* @param context - KtTable row-action context owning the list reload.
*/
async function handleToggle(
row: QqbotMessagePushApi.MessageSubscriptionView,
context: KtTableContext<QqbotMessagePushApi.MessageSubscriptionView>,
@ -207,11 +188,6 @@ export default defineComponent({
await context.reload();
}
/**
* Deletes one row and reloads only that table context after success.
* @param row - Subscription confirmed through KtTable's action system.
* @param context - KtTable row-action context owning the list reload.
*/
async function handleDelete(
row: QqbotMessagePushApi.MessageSubscriptionView,
context: KtTableContext<QqbotMessagePushApi.MessageSubscriptionView>,
@ -220,12 +196,10 @@ export default defineComponent({
await context.reload();
}
/** Reloads the mutable list exactly once after a successful modal save. */
async function handleModalSaved() {
await tableApi.reload();
}
/** Loads immutable source metadata once for the authorized page mount. */
async function loadMetadata() {
const [nextSources, nextStunOptions] = await Promise.all([
getMessagePushSources(),
@ -235,19 +209,11 @@ export default defineComponent({
stunOptions.value = nextStunOptions;
}
/** Starts the single authorized list and metadata load for this mount. */
async function activatePage() {
if (!canList) return;
await Promise.all([tableApi.reload(), loadMetadata()]);
}
/**
* Renders source, status, and empty-value presentation for data columns.
* @param slot - KtTable body-cell slot payload.
* @param slot.column - Column whose custom presentation is requested.
* @param slot.record - Subscription row rendered by the table.
* @returns Custom cell content or undefined for native field rendering.
*/
function renderBodyCell(slot: {
column: TableColumnType<QqbotMessagePushApi.MessageSubscriptionView>;
record: QqbotMessagePushApi.MessageSubscriptionView;

View File

@ -11,7 +11,6 @@ import { describe, expect, it } from 'vitest';
import MessageTemplateMentions from './MessageTemplateMentions';
/** Creates one complete source-variable fixture for Mentions behavior. */
function createVariable(
overrides: Partial<QqbotMessagePushApi.SystemMessageSourceVariableDefinition> = {},
): QqbotMessagePushApi.SystemMessageSourceVariableDefinition {

View File

@ -35,16 +35,9 @@ export default defineComponent({
},
},
emits: {
/**
* Keeps the wrapper controlled and forwards only the complete plain-text value.
* @param value - Mentions textarea content after an explicit user update.
* @returns Always true because string payloads are the only supported update.
*/
'update:value': (value: string) => typeof value === 'string',
},
/** Creates a request-free controlled boundary around the installed Mentions. */
setup(props, { emit }) {
/** Maps server variables to the exact `{{key}}` values required after `$`. */
const options = computed<MessageTemplateMentionOption[]>(() =>
props.variables.map((variable) => ({
label: `${variable.key} · ${variable.label} · ${variable.description} · 示例:${variable.example}`,
@ -53,12 +46,6 @@ export default defineComponent({
})),
);
/**
* Filters one option across the four server-owned variable descriptors.
* @param input - Case-insensitive query entered after the `$` prefix.
* @param option - Mentions option carrying the original variable definition.
* @returns Whether key, label, description, or example includes the query.
*/
function filterVariableOption(
input: string,
option: Record<string, unknown>,
@ -74,10 +61,6 @@ export default defineComponent({
].some((field) => field.toLocaleLowerCase().includes(query));
}
/**
* Emits the installed component's controlled value without interpreting its text.
* @param value - Literal textarea value, including CQ-looking substrings.
*/
function handleValueUpdate(value: string) {
emit('update:value', value);
}
@ -99,11 +82,6 @@ export default defineComponent({
},
});
/**
* Narrows an unknown Mentions option payload to the server variable contract.
* @param value - Candidate option metadata supplied by the installed component.
* @returns Whether all searchable variable fields are strings.
*/
function isVariableDefinition(
value: unknown,
): value is QqbotMessagePushApi.SystemMessageSourceVariableDefinition {

View File

@ -50,7 +50,6 @@ vi.mock('@vben/common-ui', () => ({
Page: defineComponent({
name: 'MockPage',
props: { autoContentHeight: Boolean },
/** Renders one stable route root and exposes the height contract. */
setup(props, { slots }) {
return () =>
h(
@ -72,7 +71,6 @@ vi.mock('@vben/icons', () => ({
vi.mock('antdv-next/dist/tag/index', () => ({
default: defineComponent({
name: 'MockTag',
/** Renders tag children as ordinary escaped text. */
setup(_, { slots }) {
return () => h('span', slots.default?.());
},
@ -92,7 +90,6 @@ vi.mock('#/components/ktTable', () => ({
KtTable: defineComponent({
name: 'MockKtTable',
emits: ['register'],
/** Emits the actual registration event before exposing refresh/list rendering. */
setup(_, { emit, slots }) {
emit('register', { registered: true });
return () =>
@ -132,7 +129,6 @@ vi.mock('./components/MessageTemplateModal', () => ({
},
},
emits: ['saved'],
/** Exposes create/edit and a saved trigger through the rendered modal marker. */
setup(props, { emit, expose }) {
expose({
openCreate: mocks.modalOpenCreate,
@ -152,7 +148,6 @@ vi.mock('./components/MessageTemplateModal', () => ({
}),
}));
/** Creates one template row while preserving unsafe-integer IDs as strings. */
function createRow(
overrides: Partial<QqbotMessagePushApi.MessageTemplateView> = {},
): QqbotMessagePushApi.MessageTemplateView {
@ -171,7 +166,6 @@ function createRow(
};
}
/** Creates the page-lifetime source directory fixture. */
function createSources(): QqbotMessagePushApi.SystemMessageSourceDefinition[] {
return [
{

View File

@ -32,7 +32,6 @@ const AKtTable = KtTable as any;
export default defineComponent({
name: 'QqBotMessageTemplateList',
/** Owns the permission-gated template list, source labels, and row mutations. */
setup() {
const { hasAccessByCodes } = useAccess();
const canList = hasAccessByCodes(['QqBot:MessageTemplate:List']);
@ -62,7 +61,6 @@ export default defineComponent({
},
];
const api: KtTableApi<QqbotMessagePushApi.MessageTemplateView> = {
/** Passes the caller's strict `{ items, total }` page through unchanged. */
list: async (params) => await getMessageTemplateList(params),
};
const buttons: Array<
@ -149,33 +147,18 @@ export default defineComponent({
tableTitle: '消息模板',
});
/** Opens a blank template session through the page-owned exposed ref. */
function openCreate() {
modalRef.value?.openCreate();
}
/**
* Opens one row without copying page-only runtime state into the modal.
* @param row - Template selected from KtTable.
*/
function openEdit(row: QqbotMessagePushApi.MessageTemplateView) {
modalRef.value?.openEdit(row);
}
/**
* Builds KtTable confirmation for one currently unreferenced template.
* @param row - Template awaiting delete confirmation.
* @returns Confirmation text containing the template name.
*/
function getDeleteConfirm(row: QqbotMessagePushApi.MessageTemplateView) {
return `确认删除消息模板「${row.name}」吗?`;
}
/**
* Explains why a referenced template cannot be deleted from the current row.
* @param row - Template whose current reference count controls the action.
* @returns Count-bearing reason, or undefined when delete is enabled.
*/
function getDeleteDisabledReason(
row: QqbotMessagePushApi.MessageTemplateView,
) {
@ -184,11 +167,6 @@ export default defineComponent({
: undefined;
}
/**
* Toggles one row and reloads only its KtTable context after success.
* @param row - Template whose enabled state is inverted.
* @param context - Row-action context owning the affected list.
*/
async function handleToggle(
row: QqbotMessagePushApi.MessageTemplateView,
context: KtTableContext<QqbotMessagePushApi.MessageTemplateView>,
@ -197,11 +175,6 @@ export default defineComponent({
await context.reload();
}
/**
* Deletes one row and reloads only its KtTable context after success.
* @param row - Unreferenced template confirmed through KtTable.
* @param context - Row-action context owning the affected list.
*/
async function handleDelete(
row: QqbotMessagePushApi.MessageTemplateView,
context: KtTableContext<QqbotMessagePushApi.MessageTemplateView>,
@ -210,29 +183,19 @@ export default defineComponent({
await context.reload();
}
/** Reloads the mutable list exactly once after one successful modal save. */
async function handleModalSaved() {
await tableApi.reload();
}
/** Loads the page-lifetime source directory once for an authorized mount. */
async function loadSources() {
sources.value = await getMessagePushSources();
}
/** Starts the only automatic list/source load for this route mount. */
async function activatePage() {
if (!canList) return;
await Promise.all([tableApi.reload(), loadSources()]);
}
/**
* Renders source, literal content summary, and enabled presentation.
* @param slot - KtTable body-cell payload.
* @param slot.column - Column requesting a custom presentation.
* @param slot.record - Template row rendered by the table.
* @returns Escaped text/status content or undefined for native rendering.
*/
function renderBodyCell(slot: {
column: TableColumnType<QqbotMessagePushApi.MessageTemplateView>;
record: QqbotMessagePushApi.MessageTemplateView;

View File

@ -412,9 +412,6 @@ export default defineComponent({
};
}
/**
* KtTable
*/
const renderHeaderControls = () => {
return (
<>
@ -439,9 +436,6 @@ export default defineComponent({
);
};
/**
* KtTable
*/
const renderPermissionModeToolbar = () => {
return (
<div class="kt-table__header-control-group">

View File

@ -42,9 +42,6 @@ export default defineComponent({
{ immediate: true },
);
/**
* Loads the latest task run records for the selected plugin task drawer.
*/
async function loadRuns() {
if (!props.task?.id) return;
loading.value = true;
@ -59,11 +56,6 @@ export default defineComponent({
}
}
/**
* Renders one scheduled-task execution record with themed evidence blocks.
*
* @param item - Task run row returned by the plugin task API.
*/
const renderRun = (item: QqbotPluginTaskApi.TaskRun) => (
<div class="border-b border-solid border-border py-3" key={item.id}>
<div class="mb-2 flex flex-wrap items-center gap-2">

View File

@ -43,11 +43,6 @@ export default defineComponent({
},
emits: ['close', 'installationAction'],
setup(props, { emit }) {
/**
* Maps plugin platform status values to readable themed status tags.
*
* @param status - Runtime, binding, or installation status from the API.
*/
const renderStatusTag = (status?: string) => {
if (!status) return <Tag color="default">-</Tag>;
const color =
@ -55,9 +50,6 @@ export default defineComponent({
return <Tag color={color}>{getQqbotStatusLabel(status)}</Tag>;
};
/**
* Renders recent runtime events with safe summaries for diagnosis.
*/
const renderEvents = () =>
props.runtimeEvents.length > 0 ? (
<div class="space-y-3">
@ -79,9 +71,6 @@ export default defineComponent({
<span></span>
);
/**
* Renders account-to-plugin binding rows for the selected platform state.
*/
const renderBindings = () =>
props.accountBindings.length > 0 ? (
<div class="space-y-3">
@ -98,9 +87,6 @@ export default defineComponent({
<span></span>
);
/**
* Renders installed plugin rows and exposes safe lifecycle actions.
*/
const renderInstallations = () =>
props.installations.length > 0 ? (
<div class="space-y-3">
@ -140,9 +126,6 @@ export default defineComponent({
<span></span>
);
/**
* Selects the drawer body according to the active platform state tab.
*/
const renderContent = () => {
if (props.mode === 'events') return renderEvents();
if (props.mode === 'bindings') return renderBindings();

View File

@ -41,7 +41,6 @@ const mocks = vi.hoisted(() => {
};
});
/** Creates a fluent no-op validation rule for the form-schema fixture. */
function createRule(): any {
const rule: any = {};
for (const method of [
@ -110,7 +109,6 @@ vi.mock('#/locales', () => ({
$t: (key: string) => key,
}));
/** Builds one server-returned DDNS row with a Snowflake string ID. */
function createDdnsRow(
overrides: Partial<SystemNetworkApi.DdnsRecord> = {},
): SystemNetworkApi.DdnsRecord {

View File

@ -43,9 +43,6 @@ const dnsLabelPattern = /^[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?$/i;
export default defineComponent({
name: 'NetworkDdnsRecordModal',
emits: ['saved'],
/**
* Owns the dual-stack DDNS CRUD form while keeping provider details server-side.
*/
setup(_, { emit, expose }) {
const editingRow = ref<SystemNetworkApi.DdnsRecord>();
const recordType = ref<SystemNetworkApi.DdnsRecordType>('A');
@ -55,11 +52,6 @@ export default defineComponent({
commonConfig: {
labelClass: 'w-24',
},
/**
* Replaces address-family sources immediately without retaining a stale mapping ID.
* @param values - Current form values after the field update.
* @param fieldsChanged - Field names changed in this form event.
*/
async handleValuesChange(values, fieldsChanged) {
if (!fieldsChanged.includes('recordType')) return;
const nextRecordType =
@ -97,10 +89,6 @@ export default defineComponent({
async onConfirm() {
await submit();
},
/**
* Restores form state only after destroy-on-close content has mounted.
* @param isOpen - Whether the modal content is now visible.
*/
async onOpenChange(isOpen: boolean) {
if (!isOpen) return;
const { values } = modalApi.getData<NetworkDdnsRecordModalData>();
@ -113,7 +101,6 @@ export default defineComponent({
},
});
/** Opens a blank IPv4 DDNS form without provider or credential fields. */
function openCreate() {
editingRow.value = undefined;
recordType.value = 'A';
@ -133,10 +120,6 @@ export default defineComponent({
.open();
}
/**
* Opens one record with only the editable DDNS contract copied into the form.
* @param row - Persisted DDNS row selected from KtTable.
*/
function openEdit(row: SystemNetworkApi.DdnsRecord) {
editingRow.value = row;
recordType.value = row.recordType;
@ -159,20 +142,12 @@ export default defineComponent({
.open();
}
/**
* Resets validation and installs only user-editable values.
* @param values - Modal session values excluding provider/runtime state.
*/
async function resetForm(values: Partial<NetworkDdnsRecordFormValues>) {
await formApi.resetForm();
await formApi.setValues(values);
await formApi.resetValidate();
}
/**
* Loads sources for the selected family while dropping stale response races.
* @param nextRecordType - Address family whose sources the server must evaluate.
*/
async function loadSourceOptions(
nextRecordType: SystemNetworkApi.DdnsRecordType,
) {
@ -187,7 +162,6 @@ export default defineComponent({
sourceOptions.value = result.items;
}
/** Persists one normalized A/AAAA binding and leaves provider work asynchronous. */
async function submit() {
const { valid } = await formApi.validate();
if (!valid) return;
@ -245,11 +219,6 @@ export default defineComponent({
},
});
/**
* Builds the approved dual-stack DDNS form schema without provider credentials.
* @param sourceOptions - Reactive API-owned source choices used by the IPv4 select.
* @returns Vben form fields for record identity, family, source and local behavior.
*/
function createFormSchema(
sourceOptions: Readonly<{
value: SystemNetworkApi.DdnsSourceOption[];
@ -333,11 +302,6 @@ function createFormSchema(
];
}
/**
* Formats one server-evaluated source without making eligibility decisions locally.
* @param source - Source option returned by the API.
* @returns Ant Design Select option with a stable string ID and disabled reason.
*/
function formatSourceOption(source: SystemNetworkApi.DdnsSourceOption) {
const endpoint =
source.protocol && source.externalPort
@ -355,22 +319,12 @@ function formatSourceOption(source: SystemNetworkApi.DdnsSourceOption) {
};
}
/**
* Formats a stable server reason code without inventing source eligibility rules.
* @param reasonCode - Optional API reason code for an ineligible source.
* @returns Localized prefix plus the stable code for diagnostics.
*/
function formatSourceDisabledReason(reasonCode?: null | string): string {
return reasonCode
? `${$t('system.network.ddnsSourceUnavailable')}: ${reasonCode}`
: $t('system.network.ddnsSourceUnavailable');
}
/**
* Validates a DNSPod zone as ASCII DNS labels with no URL or port syntax.
* @param value - Candidate zone name.
* @returns True only for a normalized multi-label DNS zone.
*/
export function isValidDdnsDomain(value: string): boolean {
const normalized = value.trim();
if (
@ -383,11 +337,6 @@ export function isValidDdnsDomain(value: string): boolean {
return normalized.split('.').every((label) => dnsLabelPattern.test(label));
}
/**
* Validates an A/AAAA host record, including DNSPod's root-record marker.
* @param value - Candidate host record.
* @returns True for `@` or one or more valid ASCII DNS labels.
*/
export function isValidDdnsSubDomain(value: string): boolean {
const normalized = value.trim();
if (normalized === '@') return true;

View File

@ -101,7 +101,6 @@ vi.mock('#/locales', () => ({
$t: (key: string) => key,
}));
/** Builds one dual-stack-capable DDNS table fixture. */
function createDdnsRow(
overrides: Partial<SystemNetworkApi.DdnsRecord> = {},
): SystemNetworkApi.DdnsRecord {

View File

@ -66,9 +66,6 @@ export interface NetworkDdnsTableExposed {
export default defineComponent({
name: 'NetworkDdnsTable',
/**
* Owns the independent DDNS KtTable while the route page owns SSE and active-tab refresh.
*/
setup(_, { expose }) {
const busyRowIds = ref<Set<string>>(new Set());
const modalRef = ref<NetworkDdnsRecordModalExposed>();
@ -116,11 +113,6 @@ export default defineComponent({
},
];
const api: KtTableApi<SystemNetworkApi.DdnsRecord> = {
/**
* Loads the latest persisted DDNS fact page.
* @param params - KtTable pagination and search values.
* @returns Server-owned DDNS rows and total.
*/
list: async (params) => await getNetworkDdnsList(params),
};
const buttons: Array<KtTableButton<SystemNetworkApi.DdnsRecord>> = [
@ -217,30 +209,18 @@ export default defineComponent({
tableTitle: $t('system.network.ddnsTitle'),
});
/** Opens the blank dual-stack DDNS modal. */
function openCreate() {
modalRef.value?.openCreate();
}
/**
* Opens one DDNS record unless another mutation owns its row.
* @param row - Persisted DDNS record.
*/
function openEdit(row: SystemNetworkApi.DdnsRecord) {
if (!isRowBusy(row)) modalRef.value?.openEdit(row);
}
/** Refreshes the table after a successful modal save. */
function handleModalSaved() {
void reload();
}
/**
* Runs one DDNS mutation with per-row duplicate suppression.
* @param row - Selected persisted record.
* @param mutation - API operation accepting the stable record ID.
* @param successMessage - Accurate asynchronous-success wording.
*/
async function runRowMutation(
row: SystemNetworkApi.DdnsRecord,
mutation: (id: string) => Promise<unknown>,
@ -257,11 +237,6 @@ export default defineComponent({
}
}
/**
* Replaces the reactive in-flight set instead of mutating it in place.
* @param id - Stable DDNS record ID.
* @param busy - Whether this row currently owns a write.
*/
function setRowBusy(id: string, busy: boolean) {
const next = new Set(busyRowIds.value);
if (busy) next.add(id);
@ -269,16 +244,10 @@ export default defineComponent({
busyRowIds.value = next;
}
/**
* Returns whether one row already owns a write request.
* @param row - Persisted DDNS record.
* @returns True while a mutation is in flight.
*/
function isRowBusy(row: SystemNetworkApi.DdnsRecord): boolean {
return busyRowIds.value.has(row.id);
}
/** Loads redacted provider readiness without translating failure into disabled. */
async function loadProviderStatus() {
try {
providerStatus.value = await getNetworkDdnsProviderStatus();
@ -288,14 +257,10 @@ export default defineComponent({
}
}
/**
* Reloads only DDNS-owned data when requested by the route or a local mutation.
*/
async function reload(): Promise<void> {
await Promise.allSettled([tableApi.reload(), loadProviderStatus()]);
}
/** Renders provider readiness without exposing credential values. */
function renderProviderStatus() {
const status = providerStatus.value;
let color = 'warning';
@ -316,13 +281,6 @@ export default defineComponent({
);
}
/**
* Renders dual-stack DDNS cells using only API-returned address and FQDN facts.
* @param cell - KtTable body-cell slot context.
* @param cell.column - Current table column.
* @param cell.record - Current DDNS record.
* @returns Cell content or undefined for default rendering.
*/
function renderBodyCell({ column, record }: any) {
const row = record as SystemNetworkApi.DdnsRecord;
if (column.key === 'identity') {
@ -389,11 +347,6 @@ export default defineComponent({
},
});
/**
* Resolves whether retry is unsafe without hiding the action.
* @param row - Persisted DDNS record with server-evaluated source eligibility.
* @returns Exact disabled reason, or undefined when retry is allowed.
*/
export function getDdnsRetryDisabledReason(
row: SystemNetworkApi.DdnsRecord,
): string | undefined {

View File

@ -62,7 +62,6 @@ vi.mock('#/locales', () => ({
$t: (key: string) => key,
}));
/** Creates the minimal row needed to scope a history request. */
function createRow(id: string): SystemNetworkApi.PortForward {
return {
desiredPresence: 'present',

View File

@ -27,7 +27,6 @@ const eventColors: Record<SystemNetworkApi.EndpointEventType, string> = {
export default defineComponent({
name: 'NetworkEndpointHistoryDrawer',
/** Creates a lazy, record-scoped history table inside a disposable drawer. */
setup(_, { expose }) {
const selectedRow = ref<SystemNetworkApi.PortForward>();
const columns: Array<
@ -64,7 +63,6 @@ export default defineComponent({
},
];
const api: KtTableApi<SystemNetworkApi.EndpointHistoryItem> = {
/** Loads only the selected mapping's append-only endpoint transitions. */
list: async (params) => {
if (!selectedRow.value) return { items: [], total: 0 };
return await getNetworkPortForwardEndpointHistory(
@ -108,10 +106,6 @@ export default defineComponent({
},
});
/**
* Opens history for one mapping and discards rows from the prior selection.
* @param row - Mapping whose append-only history should be queried.
*/
function open(row: SystemNetworkApi.PortForward) {
selectedRow.value = row;
drawerApi.open();

View File

@ -35,7 +35,6 @@ const mocks = vi.hoisted(() => {
};
});
/** Creates a fluent no-op validation rule for the form-schema fixture. */
function createRule(): any {
const rule: any = {};
for (const method of ['int', 'max', 'min', 'optional', 'or', 'trim']) {

View File

@ -31,9 +31,6 @@ const protocolOptions = [
export default defineComponent({
name: 'NetworkPortForwardModal',
emits: ['saved'],
/**
* Owns the CRUD form while keeping the NAS target and Keeper state read-only.
*/
setup(_, { emit, expose }) {
const editingRow = ref<SystemNetworkApi.PortForward>();
const targetIpv4 = ref('');
@ -57,7 +54,6 @@ export default defineComponent({
async onConfirm() {
await submit();
},
/** Resets values only after destroy-on-close content has mounted. */
onOpenChange(isOpen: boolean) {
if (!isOpen) return;
const { values } = modalApi.getData<NetworkPortForwardModalData>();
@ -65,10 +61,6 @@ export default defineComponent({
},
});
/**
* Opens a blank form for one new desired mapping.
* @param fixedTargetIpv4 - Server-controlled NAS target shown read-only.
*/
function openCreate(fixedTargetIpv4: string) {
editingRow.value = undefined;
targetIpv4.value = fixedTargetIpv4;
@ -85,10 +77,6 @@ export default defineComponent({
.open();
}
/**
* Opens an existing row for editing without copying reported/runtime fields.
* @param row - Persisted desired record selected from KtTable.
*/
function openEdit(row: SystemNetworkApi.PortForward) {
editingRow.value = row;
targetIpv4.value = row.targetIpv4;
@ -105,10 +93,6 @@ export default defineComponent({
.open();
}
/**
* Resets validation and installs only user-editable values.
* @param values - Form values excluding target IP, Keeper and secrets.
*/
async function resetForm(
values: Partial<SystemNetworkApi.PortForwardInput>,
) {
@ -117,9 +101,6 @@ export default defineComponent({
await formApi.resetValidate();
}
/**
* Persists the desired mapping and leaves all runtime effects asynchronous.
*/
async function submit() {
const { valid } = await formApi.validate();
if (!valid) return;
@ -173,10 +154,6 @@ export default defineComponent({
},
});
/**
* Builds the modal's approved editable schema without any credential field.
* @returns Vben form fields for name, protocol, ports and optional remark.
*/
function createFormSchema(): VbenFormSchema[] {
const portRule = z
.number()

View File

@ -9,18 +9,12 @@ export interface UseNetworkManagementStreamOptions {
onStateChanged: (event: SystemNetworkApi.StateChangeEvent) => void;
}
/**
* Bridges API SSE updates into the network page without exposing MQTT credentials.
* @param options - Page-owned callbacks for committed changes and replay gaps.
* @returns Idempotent start/close controls for route keep-alive lifecycle hooks.
*/
export function useNetworkManagementStream(
options: UseNetworkManagementStreamOptions,
) {
const lastEventId = ref<string>();
let source: EventSource | undefined;
/** Starts one EventSource and relies on native reconnect while the page is active. */
function start() {
if (source) return;
source = new EventSource(getNetworkManagementEventsUrl(lastEventId.value), {
@ -30,7 +24,6 @@ export function useNetworkManagementStream(
source.addEventListener('snapshot-required', handleSnapshotRequired);
}
/** Closes the active route stream and removes its typed listeners. */
function close() {
if (!source) return;
source.removeEventListener('network-state-changed', handleStateChanged);
@ -39,10 +32,6 @@ export function useNetworkManagementStream(
source = undefined;
}
/**
* Applies one committed MQTT-derived state event exactly once.
* @param event - Browser SSE message containing a safe state-change envelope.
*/
function handleStateChanged(event: Event) {
const payload = parseStateChange(event);
if (!payload || payload.eventId === lastEventId.value) return;
@ -50,16 +39,10 @@ export function useNetworkManagementStream(
options.onStateChanged(payload);
}
/** Requests one fresh snapshot only when the API cannot replay a missed topic event. */
function handleSnapshotRequired() {
options.onSnapshotRequired();
}
/**
* Validates the minimum browser payload before it reaches page state.
* @param event - Typed EventSource message with JSON data.
* @returns Parsed state event, or undefined for malformed/unexpected data.
*/
function parseStateChange(
event: Event,
): SystemNetworkApi.StateChangeEvent | undefined {

View File

@ -32,26 +32,22 @@ class FakeEventSource {
FakeEventSource.instances.push(this);
}
/** Registers one typed SSE listener for the page test. */
addEventListener(type: string, listener: FakeEventSourceListener) {
const listeners = this.listeners.get(type) || new Set();
listeners.add(listener);
this.listeners.set(type, listeners);
}
/** Closes this fake stream and prevents later dispatches. */
close() {
this.closed = true;
}
/** Dispatches one JSON SSE payload to currently registered listeners. */
dispatch(type: string, data: Record<string, unknown>) {
if (this.closed) return;
const event = new MessageEvent(type, { data: JSON.stringify(data) });
this.listeners.get(type)?.forEach((listener) => listener(event));
}
/** Removes one typed SSE listener from the page test. */
removeEventListener(type: string, listener: FakeEventSourceListener) {
this.listeners.get(type)?.delete(listener);
}
@ -224,7 +220,6 @@ vi.mock('#/locales', () => ({
$t: (key: string) => key,
}));
/** Creates a complete UDP row fixture with string revisions. */
function createRow(
overrides: Partial<SystemNetworkApi.PortForward> = {},
): SystemNetworkApi.PortForward {

View File

@ -92,9 +92,6 @@ const keeperStatusLabels: Record<SystemNetworkApi.KeeperStatus, string> = {
export default defineComponent({
name: 'SystemNetworkList',
/**
* Builds the generic persisted network-resource table and event-driven refresh stream.
*/
setup() {
const { hasAccessByCodes } = useAccess();
const canViewPortForward = hasAccessByCodes([
@ -192,7 +189,6 @@ export default defineComponent({
},
];
const api: KtTableApi<SystemNetworkApi.PortForward> = {
/** Loads the latest API fact-source page for KtTable. */
list: async (params) => await getNetworkPortForwardList(params),
};
const buttons: Array<KtTableButton<SystemNetworkApi.PortForward>> = [
@ -364,7 +360,6 @@ export default defineComponent({
tableTitle: $t('system.network.portForwardTitle'),
});
/** Opens the create modal using the server-reported fixed target address. */
function openCreate() {
const rowTarget = tableApi.getRows()[0]?.targetIpv4 || '';
void modalRef.value?.openCreate(
@ -372,24 +367,20 @@ export default defineComponent({
);
}
/** Opens one mutable row in the shared CRUD modal. */
function openEdit(row: SystemNetworkApi.PortForward) {
if (!isRowBusy(row) && !isDeleting(row)) {
void modalRef.value?.openEdit(row);
}
}
/** Opens the append-only endpoint history drawer for one row. */
function openHistory(row: SystemNetworkApi.PortForward) {
historyDrawerRef.value?.open(row);
}
/** Reloads after a modal save while preserving serialized request order. */
function handleModalSaved() {
void requestRefresh('port-forward');
}
/** Copies an API-approved current endpoint without logging it. */
async function copyEndpoint(row: SystemNetworkApi.PortForward) {
const endpoint = getCurrentEndpoint(row);
if (!endpoint) return;
@ -397,12 +388,6 @@ export default defineComponent({
message.success($t('system.network.endpointCopied'));
}
/**
* Runs one row mutation with per-row duplicate suppression and fact-source reload.
* @param row - Selected persisted mapping.
* @param mutation - API operation accepting the stable record ID.
* @param successMessage - Accurate asynchronous-success wording.
*/
async function runRowMutation(
row: SystemNetworkApi.PortForward,
mutation: (id: string) => Promise<unknown>,
@ -419,7 +404,6 @@ export default defineComponent({
}
}
/** Updates the reactive per-row in-flight set without mutating it in place. */
function setRowBusy(id: string, busy: boolean) {
const next = new Set(busyRowIds.value);
if (busy) next.add(id);
@ -427,12 +411,10 @@ export default defineComponent({
busyRowIds.value = next;
}
/** Returns whether one row already owns an in-flight write request. */
function isRowBusy(row: SystemNetworkApi.PortForward): boolean {
return busyRowIds.value.has(row.id);
}
/** Loads Agent connectivity without translating refresh failure into offline. */
async function loadAgentStatus() {
try {
agentStatus.value = await getNetworkAgentStatus();
@ -442,9 +424,6 @@ export default defineComponent({
}
}
/**
* Serializes table and Agent refreshes so stale responses cannot overtake writes.
*/
async function requestRefresh(
resource: NetworkTabKey = activeTab.value,
): Promise<void> {
@ -470,10 +449,6 @@ export default defineComponent({
}
}
/**
* Loads exactly one permission-approved resource without cross-tab requests.
* @param resource - Active or locally-mutated network resource.
*/
async function performResourceRefresh(resource: NetworkTabKey) {
if (resource === 'ddns') {
await ddnsTableRef.value?.reload();
@ -482,19 +457,10 @@ export default defineComponent({
await Promise.allSettled([tableApi.reload(), loadAgentStatus()]);
}
/**
* Checks list permission before any resource request.
* @param resource - Candidate tab resource.
* @returns True when its List permission is present.
*/
function canViewResource(resource: NetworkTabKey): boolean {
return resource === 'ddns' ? canViewDdns : canViewPortForward;
}
/**
* Refreshes only the active resource addressed by one semantic SSE event.
* @param event - Committed API event classified by network resource source.
*/
function handleStateChanged(event: SystemNetworkApi.StateChangeEvent) {
const resource = event.source === 'ddns' ? 'ddns' : 'port-forward';
if (pageActive && activeTab.value === resource) {
@ -502,22 +468,16 @@ export default defineComponent({
}
}
/** Refreshes the active resource when the API cannot replay a missed event. */
function handleSnapshotRequired() {
if (pageActive) void requestRefresh(activeTab.value);
}
/**
* Selects one allowed tab and immediately requests its first fact snapshot.
* @param key - Tab key emitted by Ant Design Tabs.
*/
function handleActiveTabChange(key: NetworkTabKey) {
if (!canViewResource(key) || activeTab.value === key) return;
activeTab.value = key;
if (pageActive) void requestRefresh(key);
}
/** Opens the stream before the one initial page snapshot to avoid a subscription gap. */
function activatePage() {
if (pageActive || tabItems.length === 0) return;
pageActive = true;
@ -525,13 +485,11 @@ export default defineComponent({
void requestRefresh(activeTab.value);
}
/** Closes the route-owned stream while preserving its replay cursor. */
function deactivatePage() {
pageActive = false;
managementStream.close();
}
/** Renders independent Agent connectivity and revision convergence controls. */
function renderAgentControls() {
const status = agentStatus.value;
return (
@ -552,7 +510,6 @@ export default defineComponent({
);
}
/** Renders cells without collapsing independent synchronization dimensions. */
function renderBodyCell({ column, record }: any) {
const row = record as SystemNetworkApi.PortForward;
if (column.key === 'protocol') {
@ -663,11 +620,6 @@ export default defineComponent({
},
});
/**
* Returns whether a mapping is immutable pending confirmed asynchronous deletion.
* @param row - Persisted port-forward row.
* @returns True for absent tombstones, deleting state, or confirmed deletion.
*/
export function isDeleting(row: SystemNetworkApi.PortForward): boolean {
return (
row.isDeleted ||
@ -676,12 +628,6 @@ export function isDeleting(row: SystemNetworkApi.PortForward): boolean {
);
}
/**
* Resolves the Keeper or probe disabled reason without consulting Agent online state.
* @param row - Persisted port-forward row.
* @param requireEnabled - Whether immediate probe additionally requires desired Keeper.
* @returns Readable reason, or undefined when the action is valid.
*/
export function getKeeperDisabledReason(
row: SystemNetworkApi.PortForward,
requireEnabled: boolean,
@ -699,11 +645,6 @@ export function getKeeperDisabledReason(
return undefined;
}
/**
* Formats the current endpoint already lease-filtered by the API.
* @param row - Persisted port-forward row.
* @returns Exact public IPv4 and port, or undefined when the API withdrew it.
*/
export function getCurrentEndpoint(
row: SystemNetworkApi.PortForward,
): string | undefined {
@ -712,12 +653,6 @@ export function getCurrentEndpoint(
: undefined;
}
/**
* Produces the row summary without treating status-refresh failure as Agent offline.
* @param row - Persisted mapping with redacted stable error fields.
* @param status - Last successfully loaded Agent status, when available.
* @returns Error, offline wait, pending wait, or an empty marker.
*/
export function getWaitingOrErrorSummary(
row: SystemNetworkApi.PortForward,
status?: SystemNetworkApi.AgentStatus,
@ -732,12 +667,6 @@ export function getWaitingOrErrorSummary(
return '-';
}
/**
* Resolves the independent Agent status color without treating refresh failure as offline.
* @param status - Last successfully loaded Agent status.
* @param unknown - Whether the latest status request failed.
* @returns Ant Design semantic color.
*/
function getAgentStatusColor(
status: SystemNetworkApi.AgentStatus | undefined,
unknown: boolean,
@ -746,12 +675,6 @@ function getAgentStatusColor(
return status?.online ? 'success' : 'warning';
}
/**
* Resolves the independent Agent status label shown in the table header.
* @param status - Last successfully loaded Agent status.
* @param unknown - Whether the latest status request failed.
* @returns Localized unknown, online, or offline label.
*/
function getAgentStatusLabel(
status: SystemNetworkApi.AgentStatus | undefined,
unknown: boolean,
@ -762,7 +685,6 @@ function getAgentStatusLabel(
: $t('system.network.agentOffline');
}
/** Returns the immutable-row explanation used by CRUD write actions. */
function getWriteDisabledReason(
row: SystemNetworkApi.PortForward,
): string | undefined {

View File

@ -64,6 +64,8 @@ function getStagedFiles() {
.filter(Boolean);
}
run(getPnpmCommand(), ['run', 'check:jsdoc']);
const files = getStagedFiles().filter((file) => {
if (!existsSync(file)) return false;
if (!CHECK_ROOTS.some((root) => file.startsWith(root))) return false;

View File

@ -0,0 +1,709 @@
#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import ts from 'typescript';
import { parse as parseVueSfc } from 'vue/compiler-sfc';
const CODE_FILE_PATTERN = /\.(?:cjs|js|mjs|ts|tsx|vue)$/u;
const HAN_CHARACTER_PATTERN = /\p{Script=Han}/u;
const JSDOC_PREFIX = '/**';
const LIFECYCLE_METHOD_NAMES = new Set([
'activated',
'beforeCreate',
'beforeDestroy',
'beforeMount',
'beforeUnmount',
'beforeUpdate',
'componentDidCatch',
'componentDidMount',
'componentDidUpdate',
'componentWillMount',
'componentWillReceiveProps',
'componentWillUnmount',
'componentWillUpdate',
'created',
'deactivated',
'destroyed',
'errorCaptured',
'getSnapshotBeforeUpdate',
'mounted',
'renderTracked',
'renderTriggered',
'serverPrefetch',
'shouldComponentUpdate',
'unmounted',
'updated',
]);
function getIsolatedGitEnvironment() {
return Object.fromEntries(
Object.entries(process.env).filter(([name]) => !name.startsWith('GIT_')),
);
}
function getLineStarts(source) {
const starts = [0];
for (let index = 0; index < source.length; index += 1) {
const character = source.codePointAt(index);
if (character === 13) {
if (source.codePointAt(index + 1) === 10) {
index += 1;
}
starts.push(index + 1);
} else if (character === 10) {
starts.push(index + 1);
}
}
return starts;
}
function getLineAndColumn(lineStarts, position) {
let low = 0;
let high = lineStarts.length - 1;
while (low <= high) {
const middle = Math.floor((low + high) / 2);
if (lineStarts[middle] <= position) {
low = middle + 1;
} else {
high = middle - 1;
}
}
const lineIndex = Math.max(0, high);
return {
column: position - lineStarts[lineIndex] + 1,
line: lineIndex + 1,
};
}
function getScriptKind(filePath, language) {
const normalizedLanguage = language?.toLowerCase();
if (normalizedLanguage === 'tsx' || filePath.endsWith('.tsx')) {
return ts.ScriptKind.TSX;
}
if (normalizedLanguage === 'jsx') {
return ts.ScriptKind.JSX;
}
if (
normalizedLanguage === 'ts' ||
filePath.endsWith('.ts') ||
filePath.endsWith('.mts') ||
filePath.endsWith('.cts')
) {
return ts.ScriptKind.TS;
}
return ts.ScriptKind.JS;
}
function getVueScriptBlocks(filePath, source) {
const { descriptor, errors } = parseVueSfc(source, {
filename: filePath,
sourceMap: false,
});
if (errors.length > 0) {
const message = errors
.map((error) => (error instanceof Error ? error.message : String(error)))
.join('; ');
throw new Error(`Vue SFC 解析失败:${message}`);
}
return [
descriptor.script
? {
content: descriptor.script.content,
label: '<script>',
language: descriptor.script.lang,
offset: descriptor.script.loc.start.offset,
}
: null,
descriptor.scriptSetup
? {
content: descriptor.scriptSetup.content,
label: '<script setup>',
language: descriptor.scriptSetup.lang,
offset: descriptor.scriptSetup.loc.start.offset,
}
: null,
]
.filter(Boolean)
.toSorted((left, right) => left.offset - right.offset);
}
function getScriptBlocks(filePath, source) {
if (filePath.endsWith('.vue')) {
return getVueScriptBlocks(filePath, source);
}
return [
{
content: source,
label: null,
language: path.extname(filePath).slice(1),
offset: 0,
},
];
}
function getNodeName(node, sourceFile) {
if (!node.name) {
return null;
}
if (
ts.isIdentifier(node.name) ||
ts.isPrivateIdentifier(node.name) ||
ts.isStringLiteralLike(node.name) ||
ts.isNumericLiteral(node.name)
) {
return node.name.text;
}
return node.name.getText(sourceFile);
}
function isSetupFunctionExpression(node, sourceFile) {
if (getNodeName(node, sourceFile) === 'setup') {
return true;
}
const parent = node.parent;
return (
ts.isPropertyAssignment(parent) &&
getNodeName(parent, sourceFile) === 'setup'
);
}
function classifyTarget(node, sourceFile) {
if (!node) {
return { allowed: false, target: 'unattached' };
}
if (ts.isFunctionDeclaration(node)) {
const name = getNodeName(node, sourceFile);
if (!name) {
return { allowed: false, target: 'anonymous-function-declaration' };
}
if (name === 'setup') {
return { allowed: false, target: 'setup' };
}
return { allowed: true, target: 'named-function-declaration' };
}
if (ts.isFunctionExpression(node)) {
const name = getNodeName(node, sourceFile);
if (!name) {
return { allowed: false, target: 'anonymous-function-expression' };
}
if (isSetupFunctionExpression(node, sourceFile)) {
return { allowed: false, target: 'setup' };
}
return { allowed: true, target: 'named-function-expression' };
}
if (
ts.isMethodDeclaration(node) ||
ts.isGetAccessorDeclaration(node) ||
ts.isSetAccessorDeclaration(node)
) {
const name = getNodeName(node, sourceFile);
if (name === 'setup') {
return { allowed: false, target: 'setup' };
}
if (name && LIFECYCLE_METHOD_NAMES.has(name)) {
return { allowed: false, target: 'lifecycle-entry' };
}
return {
allowed: Boolean(name),
target: ts.isMethodDeclaration(node) ? 'named-method' : 'accessor',
};
}
if (ts.isConstructorDeclaration(node)) {
return { allowed: false, target: 'constructor' };
}
if (ts.isArrowFunction(node)) {
return { allowed: false, target: 'arrow-function' };
}
if (ts.isInterfaceDeclaration(node)) {
return { allowed: false, target: 'interface' };
}
if (ts.isTypeAliasDeclaration(node)) {
return { allowed: false, target: 'type' };
}
if (
ts.isPropertyDeclaration(node) ||
ts.isPropertySignature(node) ||
ts.isPropertyAssignment(node) ||
ts.isShorthandPropertyAssignment(node)
) {
return { allowed: false, target: 'property' };
}
if (
ts.isVariableDeclaration(node) ||
ts.isVariableDeclarationList(node) ||
ts.isVariableStatement(node)
) {
return { allowed: false, target: 'variable' };
}
if (
ts.isMethodSignature(node) ||
ts.isCallSignatureDeclaration(node) ||
ts.isConstructSignatureDeclaration(node) ||
ts.isFunctionTypeNode(node)
) {
return { allowed: false, target: 'type' };
}
return {
allowed: false,
target: ts.SyntaxKind[node.kind] ?? 'unknown',
};
}
function getLeafTokens(sourceFile) {
const tokens = [];
function visit(node) {
if (
node.kind >= ts.SyntaxKind.FirstJSDocNode &&
node.kind <= ts.SyntaxKind.LastJSDocNode
) {
return;
}
if (
node.kind >= ts.SyntaxKind.FirstToken &&
node.kind <= ts.SyntaxKind.LastToken
) {
tokens.push(node);
return;
}
for (const child of node.getChildren(sourceFile)) {
visit(child);
}
}
visit(sourceFile);
return tokens.toSorted(
(left, right) => left.getStart(sourceFile) - right.getStart(sourceFile),
);
}
function scanTriviaGap(content, start, end) {
if (start >= end) {
return [];
}
const scanner = ts.createScanner(
ts.ScriptTarget.Latest,
false,
ts.LanguageVariant.Standard,
content.slice(start, end),
);
const comments = [];
for (
let token = scanner.scan();
token !== ts.SyntaxKind.EndOfFileToken;
token = scanner.scan()
) {
if (token !== ts.SyntaxKind.MultiLineCommentTrivia) {
continue;
}
const commentStart = start + scanner.getTokenPos();
const commentEnd = start + scanner.getTextPos();
const text = content.slice(commentStart, commentEnd);
if (!text.startsWith(JSDOC_PREFIX)) {
continue;
}
comments.push({ end: commentEnd, start: commentStart, text });
}
return comments;
}
function collectRawJsdocComments(sourceFile) {
const comments = [];
let cursor = 0;
for (const token of getLeafTokens(sourceFile)) {
const tokenStart = token.getStart(sourceFile);
comments.push(...scanTriviaGap(sourceFile.text, cursor, tokenStart));
cursor = Math.max(cursor, token.end);
}
comments.push(
...scanTriviaGap(sourceFile.text, cursor, sourceFile.text.length),
);
return comments;
}
function collectJsdocOwners(sourceFile) {
const owners = new Map();
function visit(node) {
for (const jsdoc of node.jsDoc ?? []) {
owners.set(`${jsdoc.getStart(sourceFile)}:${jsdoc.end}`, node);
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return owners;
}
function inspectScriptBlock(filePath, block, fullSource, lineStarts) {
const scriptKind = getScriptKind(filePath, block.language);
const virtualFilePath = block.label
? `${filePath}${block.label.replaceAll(/[<> ]/gu, '-')}`
: filePath;
const sourceFile = ts.createSourceFile(
virtualFilePath,
block.content,
ts.ScriptTarget.Latest,
true,
scriptKind,
);
const owners = collectJsdocOwners(sourceFile);
const rawComments = collectRawJsdocComments(sourceFile);
return rawComments.map((rawComment) => {
const owner = owners.get(`${rawComment.start}:${rawComment.end}`);
const classification = classifyTarget(owner, sourceFile);
const start = block.offset + rawComment.start;
const end = block.offset + rawComment.end;
const location = getLineAndColumn(lineStarts, start);
let rule = null;
let message = null;
if (!classification.allowed) {
rule = 'invalid-target';
message = `JSDoc 不允许用于 ${classification.target}`;
} else if (!HAN_CHARACTER_PATTERN.test(rawComment.text)) {
rule = 'non-chinese-description';
message = 'JSDoc 自然语言描述必须使用中文';
}
return {
block: block.label,
column: location.column,
end,
filePath,
line: location.line,
message,
ownerKind: owner ? ts.SyntaxKind[owner.kind] : null,
rule,
start,
target: classification.target,
text: fullSource.slice(start, end),
};
});
}
export function inspectFileSource(filePath, source) {
const normalizedFilePath = filePath.replaceAll(path.sep, '/');
const lineStarts = getLineStarts(source);
const blocks = getScriptBlocks(normalizedFilePath, source);
const comments = blocks.flatMap((block) =>
inspectScriptBlock(normalizedFilePath, block, source, lineStarts),
);
return {
comments,
isVue: normalizedFilePath.endsWith('.vue'),
scriptBlockCount: blocks.length,
violations: comments.filter((comment) => comment.rule !== null),
};
}
function collectSyntaxTokens(source, scriptKind, filePath) {
const sourceFile = ts.createSourceFile(
filePath,
source,
ts.ScriptTarget.Latest,
true,
scriptKind,
);
const tokens = [];
function visit(node) {
if (
node.kind === ts.SyntaxKind.JSDocComment ||
(node.kind >= ts.SyntaxKind.FirstJSDocNode &&
node.kind <= ts.SyntaxKind.LastJSDocNode)
) {
return;
}
const children = node.getChildren(sourceFile);
if (children.length > 0) {
for (const child of children) {
visit(child);
}
return;
}
if (node.kind === ts.SyntaxKind.EndOfFileToken) {
return;
}
const text = node.getText(sourceFile);
if (text.length > 0) {
tokens.push([node.kind, text]);
}
}
visit(sourceFile);
return tokens;
}
export function getCodeTokenSignature(filePath, source) {
const normalizedFilePath = filePath.replaceAll(path.sep, '/');
const blocks = getScriptBlocks(normalizedFilePath, source);
const tokenGroups = blocks.map((block) => ({
block: block.label,
language: block.language ?? null,
tokens: collectSyntaxTokens(
block.content,
getScriptKind(normalizedFilePath, block.language),
normalizedFilePath,
),
}));
return JSON.stringify(tokenGroups);
}
function getRemovalEdit(source, start, end) {
const lineStart = source.lastIndexOf('\n', start - 1) + 1;
const nextLineBreak = source.indexOf('\n', end);
const lineEnd = nextLineBreak === -1 ? source.length : nextLineBreak;
const prefix = source.slice(lineStart, start);
const suffix = source.slice(end, lineEnd);
if (prefix.trim() === '' && suffix.trim() === '') {
return {
end: nextLineBreak === -1 ? source.length : nextLineBreak + 1,
replacement: '',
start: lineStart,
};
}
if (suffix.trim() === '') {
let expandedStart = start;
while (
expandedStart > lineStart &&
(source[expandedStart - 1] === ' ' || source[expandedStart - 1] === '\t')
) {
expandedStart -= 1;
}
return { end: lineEnd, replacement: '', start: expandedStart };
}
if (prefix.trim() === '') {
let expandedEnd = end;
while (
expandedEnd < lineEnd &&
(source[expandedEnd] === ' ' || source[expandedEnd] === '\t')
) {
expandedEnd += 1;
}
return { end: expandedEnd, replacement: '', start: lineStart };
}
const newlines = source.slice(start, end).match(/\r\n|\r|\n/gu);
return {
end,
replacement: newlines ? newlines.join('') : ' ',
start,
};
}
function applyEdits(source, edits) {
const sortedEdits = edits.toSorted(
(left, right) => right.start - left.start || right.end - left.end,
);
let result = source;
let previousStart = source.length + 1;
for (const edit of sortedEdits) {
if (edit.end > previousStart) {
throw new Error('JSDoc 清理范围发生重叠');
}
result =
result.slice(0, edit.start) + edit.replacement + result.slice(edit.end);
previousStart = edit.start;
}
return result;
}
export function applyJsdocPolicyFix(filePath, source) {
const inspection = inspectFileSource(filePath, source);
const uniqueViolations = [
...new Map(
inspection.violations.map((violation) => [
`${violation.start}:${violation.end}`,
violation,
]),
).values(),
];
const edits = uniqueViolations.map((violation) =>
getRemovalEdit(source, violation.start, violation.end),
);
const beforeTokens = getCodeTokenSignature(filePath, source);
const fixedSource = applyEdits(source, edits);
const afterTokens = getCodeTokenSignature(filePath, fixedSource);
if (beforeTokens !== afterTokens) {
throw new Error(`${filePath}: 清理前后非注释 token 不一致`);
}
return {
removedCount: uniqueViolations.length,
source: fixedSource,
};
}
export function getTrackedCodeFiles(rootDirectory = process.cwd()) {
const output = execFileSync(
'git',
['ls-files', '--cached', '--others', '--exclude-standard', '-z'],
{
cwd: rootDirectory,
encoding: 'utf8',
env: getIsolatedGitEnvironment(),
},
);
return output
.split('\0')
.filter(Boolean)
.filter((filePath) => CODE_FILE_PATTERN.test(filePath))
.filter((filePath) => existsSync(path.join(rootDirectory, filePath)))
.toSorted();
}
function summarizeInspections(inspections) {
const summary = {
'invalid-target': 0,
'non-chinese-description': 0,
fileCount: 0,
jsdocCount: 0,
scriptBlockCount: 0,
violationCount: 0,
vueFileCount: 0,
};
for (const inspection of inspections) {
summary.fileCount += 1;
summary.jsdocCount += inspection.result.comments.length;
summary.scriptBlockCount += inspection.result.scriptBlockCount;
summary.violationCount += inspection.result.violations.length;
if (inspection.result.isVue) {
summary.vueFileCount += 1;
}
for (const violation of inspection.result.violations) {
summary[violation.rule] += 1;
}
}
return summary;
}
function inspectTrackedFiles(rootDirectory) {
return getTrackedCodeFiles(rootDirectory).map((filePath) => {
const absolutePath = path.join(rootDirectory, filePath);
const source = readFileSync(absolutePath, 'utf8');
return {
absolutePath,
filePath,
result: inspectFileSource(filePath, source),
source,
};
});
}
function formatViolation(violation) {
const blockSuffix = violation.block ? ` ${violation.block}` : '';
return `${violation.filePath}:${violation.line}:${violation.column} [${violation.rule}]${blockSuffix} ${violation.message}`;
}
function printSummary(summary, prefix = 'JSDoc 检查') {
process.stdout.write(
`${prefix}${summary.fileCount} 个受控代码文件,` +
`${summary.vueFileCount} 个 Vue 文件,` +
`${summary.scriptBlockCount} 个脚本区块,` +
`${summary.jsdocCount} 个 JSDoc` +
`${summary.violationCount} 个违规` +
`(非法位置 ${summary['invalid-target']},纯英文 ${summary['non-chinese-description']}\n`,
);
}
function runCli() {
const args = new Set(process.argv.slice(2));
const fix = args.delete('--fix');
if (args.size > 0) {
console.error(`未知参数:${[...args].join(', ')}`);
process.exitCode = 2;
return;
}
const rootDirectory = process.cwd();
const inspections = inspectTrackedFiles(rootDirectory);
const initialSummary = summarizeInspections(inspections);
if (!fix) {
for (const inspection of inspections) {
for (const violation of inspection.result.violations) {
console.error(formatViolation(violation));
}
}
printSummary(initialSummary);
if (initialSummary.violationCount > 0) {
process.exitCode = 1;
}
return;
}
let changedFileCount = 0;
let removedCount = 0;
for (const inspection of inspections) {
if (inspection.result.violations.length === 0) {
continue;
}
const fixed = applyJsdocPolicyFix(inspection.filePath, inspection.source);
writeFileSync(inspection.absolutePath, fixed.source);
changedFileCount += 1;
removedCount += fixed.removedCount;
}
const finalInspections = inspectTrackedFiles(rootDirectory);
const finalSummary = summarizeInspections(finalInspections);
process.stdout.write(
`JSDoc 清理:修改 ${changedFileCount} 个文件,删除 ${removedCount} 个违规块\n`,
);
printSummary(finalSummary, '清理后检查');
if (finalSummary.violationCount > 0) {
process.exitCode = 1;
}
}
const invokedPath = process.argv[1]
? pathToFileURL(path.resolve(process.argv[1])).href
: null;
if (invokedPath === import.meta.url) {
try {
runCli();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}

View File

@ -0,0 +1,252 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { it } from 'vitest';
import {
applyJsdocPolicyFix,
getCodeTokenSignature,
getTrackedCodeFiles,
inspectFileSource,
} from './check-jsdoc.mjs';
function runFixtureGit(repository, args) {
const environment = Object.fromEntries(
Object.entries(process.env).filter(([name]) => !name.startsWith('GIT_')),
);
execFileSync('git', args, { cwd: repository, env: environment });
}
it('allows Chinese JSDoc only on explicitly named callable declarations', () => {
const source = `
/** 读取当前配置。 */
function readConfig() {}
const loadConfig = /** 加载远端配置。 */ function loadRemoteConfig() {};
const service = {
/** 执行任务。 */
run() {},
/** 返回当前状态。 */
get status() {
return 'ready';
},
};
`;
const result = inspectFileSource('valid.ts', source);
assert.equal(result.comments.length, 4);
assert.deepEqual(result.violations, []);
});
it('rejects pure-English JSDoc on an otherwise valid named function', () => {
const result = inspectFileSource(
'english.ts',
'/** Loads the current configuration. */\nfunction loadConfig() {}\n',
);
assert.equal(result.violations.length, 1);
assert.equal(result.violations[0]?.rule, 'non-chinese-description');
assert.equal(result.violations[0]?.target, 'named-function-declaration');
});
it('rejects declarations and callables excluded by the policy', () => {
const source = `
/** 接口说明。 */
interface Options {
/** 属性说明。 */
value: string;
}
/** 类型说明。 */
type Result = string;
/** 变量说明。 */
const count = 1;
const arrow = /** 箭头函数说明。 */ () => count;
items.map(/** 匿名回调说明。 */ function () {});
class Service {
/** 构造服务。 */
constructor() {}
/** 初始化组件。 */
setup() {}
/** 组件挂载入口。 */
mounted() {}
}
/** 即使初始化器具名,注释仍属于变量。 */
const load = function loadConfig() {};
`;
const result = inspectFileSource('invalid-targets.ts', source);
assert.deepEqual(
result.violations.map(({ rule, target }) => [rule, target]),
[
['invalid-target', 'interface'],
['invalid-target', 'property'],
['invalid-target', 'type'],
['invalid-target', 'variable'],
['invalid-target', 'arrow-function'],
['invalid-target', 'anonymous-function-expression'],
['invalid-target', 'constructor'],
['invalid-target', 'setup'],
['invalid-target', 'lifecycle-entry'],
['invalid-target', 'variable'],
],
);
});
it('rejects raw JSDoc blocks that TypeScript does not attach to a node', () => {
const source = `
consume(/** 表达式内孤立注释。 */ 1);
const stringValue = '/** 字符串内容。 */';
const templateValue = \`/** 模板字符串内容。 */\`;
; /** 文件尾孤立注释。 */
`;
const result = inspectFileSource('unattached.ts', source);
assert.equal(result.comments.length, 2);
assert.deepEqual(
result.violations.map(({ rule, target }) => [rule, target]),
[
['invalid-target', 'unattached'],
['invalid-target', 'unattached'],
],
);
});
it('does not treat string, template, or TSX text content as raw JSDoc', () => {
const source = `
const stringValue = '/** 字符串内容。 */';
const templateValue = \`/** 模板字符串内容。 */\`;
const view = <div>/** TSX 文本内容。 */</div>;
consume(/** 只有这一块是真实孤立注释。 */ 1);
`;
const result = inspectFileSource('literal-content.tsx', source);
assert.equal(result.comments.length, 1);
assert.equal(result.violations[0]?.target, 'unattached');
});
it('checks Vue script and script-setup blocks without scanning template or style', () => {
const source = `
<template>
<div>/** 模板文本不属于 JSDoc。 */</div>
</template>
<script setup lang="ts">
/** setup 变量非法。 */
const count = 1;
/** 返回当前数量。 */
function readCount() {
return count;
}
</script>
<script lang="tsx">
defineComponent({
/** TSX 组件 setup 禁止使用 JSDoc。 */
setup() {
return () => <span />;
},
/** 渲染当前组件。 */
render() {
return <span />;
},
});
/** Loads remote data. */
function loadRemote() {}
</script>
<style>
/** 样式注释不属于脚本 JSDoc。 */
.root {}
</style>
`;
const result = inspectFileSource('Component.vue', source);
assert.equal(result.comments.length, 5);
assert.deepEqual(
result.violations.map(({ rule, target }) => [rule, target]),
[
['invalid-target', 'variable'],
['invalid-target', 'setup'],
['non-chinese-description', 'named-function-declaration'],
],
);
});
it('fix mode removes only violations and preserves non-comment tokens', () => {
const source = `
/** 保留有意义的中文说明。 */
function keepComment() {}
/** Remove this historical English description. */
function removeEnglishComment() {}
/** 删除变量位置的注释。 */
const value = 1;
const named = /** 保留具名函数表达式说明。 */ function namedExpression() {
return value;
};
`;
const beforeTokens = getCodeTokenSignature('cleanup.ts', source);
const fixed = applyJsdocPolicyFix('cleanup.ts', source);
assert.match(fixed.source, /保留有意义的中文说明/);
assert.match(fixed.source, /保留具名函数表达式说明/);
assert.doesNotMatch(fixed.source, /historical English/);
assert.doesNotMatch(fixed.source, /删除变量位置的注释/);
assert.equal(fixed.removedCount, 2);
assert.deepEqual(
inspectFileSource('cleanup.ts', fixed.source).violations,
[],
);
assert.equal(getCodeTokenSignature('cleanup.ts', fixed.source), beforeTokens);
});
it('tracked-file discovery also includes untracked non-ignored code', () => {
const repository = mkdtempSync(path.join(tmpdir(), 'admin-jsdoc-policy-'));
try {
runFixtureGit(repository, ['init', '--quiet']);
writeFileSync(path.join(repository, '.gitignore'), 'ignored.ts\n');
writeFileSync(path.join(repository, 'deleted.ts'), 'export {};\n');
writeFileSync(path.join(repository, 'tracked.ts'), 'export {};\n');
writeFileSync(path.join(repository, 'untracked.tsx'), 'export {};\n');
writeFileSync(path.join(repository, 'ignored.ts'), 'export {};\n');
writeFileSync(path.join(repository, 'notes.md'), '# Notes\n');
runFixtureGit(repository, [
'add',
'.gitignore',
'deleted.ts',
'tracked.ts',
]);
rmSync(path.join(repository, 'deleted.ts'));
assert.deepEqual(getTrackedCodeFiles(repository), [
'tracked.ts',
'untracked.tsx',
]);
} finally {
rmSync(repository, { force: true, recursive: true });
}
});

View File

@ -25,9 +25,6 @@ const getDefaultPwaOptions = (name: string): Partial<PwaPluginOptions> => ({
},
});
/**
* importmap CDN
*/
const defaultImportmapOptions: ImportmapPluginOptions = {
// 通过 Importmap CDN 方式引入,
// 目前只有esm.sh源兼容性好一点jspm.io对于 esm 入口要求高

View File

@ -1,6 +1,3 @@
/**
* https://github.com/jspm/vite-plugin-jspm调整为需要的功能
*/
import type { GeneratorOptions } from '@jspm/generator';
import type { Plugin } from 'vite';

View File

@ -1,4 +1,5 @@
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
// prettier-ignore
import type {
ConfigEnv,
UserConfig,
@ -7,328 +8,90 @@ import type {
import type { PluginOptions } from 'vite-plugin-dts';
import type { Options as PwaPluginOptions } from 'vite-plugin-pwa';
/**
* ImportMap
* @description
* @example
* ```typescript
* {
* imports: {
* 'vue': 'https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.js'
* },
* scopes: {
* 'https://site.com/': {
* 'vue': 'https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.js'
* }
* }
* }
* ```
*/
interface IImportMap {
/** 模块导入映射 */
imports?: Record<string, string>;
/** 作用域特定的导入映射 */
scopes?: {
[scope: string]: Record<string, string>;
};
}
/**
*
* @description
*/
interface PrintPluginOptions {
/**
*
* @description
* @example
* ```typescript
* {
* 'App Version': '1.0.0',
* 'Build Time': '2024-01-01'
* }
* ```
*/
infoMap?: Record<string, string | undefined>;
}
/**
* Nitro Mock
* @description Nitro Mock
*/
interface NitroMockPluginOptions {
/**
* Mock
* @default '@vbenjs/nitro-mock'
*/
mockServerPackage?: string;
/**
* Mock
* @default 3000
*/
port?: number;
/**
* Mock
* @default false
*/
verbose?: boolean;
}
/**
*
* @description
*/
interface ArchiverPluginOptions {
/**
*
* @default 'dist'
*/
name?: string;
/**
*
* @default '.'
*/
outputDir?: string;
}
/**
* ImportMap
* @description CDN
*/
interface ImportmapPluginOptions {
/**
* CDN
* @default 'jspm.io'
* @description esm.sh jspm.io CDN
*/
defaultProvider?: 'esm.sh' | 'jspm.io';
/**
* ImportMap
* @description CDN
* @example
* ```typescript
* [
* { name: 'vue' },
* { name: 'pinia', range: '^2.0.0' }
* ]
* ```
*/
importmap?: Array<{ name: string; range?: string }>;
/**
* ImportMap
* @description ImportMap
*/
inputMap?: IImportMap;
}
/**
*
* @description
*/
interface ConditionPlugin {
/**
*
* @description true
*/
condition?: boolean;
/**
*
* @description Promise
*/
plugins: () => PromiseLike<unknown[]> | unknown[];
}
/**
*
* @description
*/
interface CommonPluginOptions {
/**
*
* @default false
*/
devtools?: boolean;
/**
*
* @description
*/
env?: Record<string, any>;
/**
*
* @default true
*/
injectMetadata?: boolean;
/**
*
* @default false
*/
isBuild?: boolean;
/**
*
* @default 'development'
*/
mode?: string;
/**
*
* @default false
* @description 使 rollup-plugin-visualizer
*/
visualizer?: boolean | PluginVisualizerOptions;
}
/**
*
* @description
*/
interface ApplicationPluginOptions extends CommonPluginOptions {
/**
*
* @description HTML PWA
*/
appTitle?: string;
/**
*
* @default false
* @description zip
*/
archiver?: boolean;
/**
*
* @description
*/
archiverPluginOptions?: ArchiverPluginOptions;
/**
*
* @default false
* @description gzip brotli
*/
compress?: boolean;
/**
*
* @default ['gzip']
* @description
*/
compressTypes?: ('brotli' | 'gzip')[];
/**
*
* @default false
* @description
*/
extraAppConfig?: boolean;
/**
* HTML
* @default true
*/
html?: boolean;
/**
*
* @default false
*/
i18n?: boolean;
/**
* ImportMap CDN
* @default false
*/
importmap?: boolean;
/**
* ImportMap
*/
importmapOptions?: ImportmapPluginOptions;
/**
*
* @default true
*/
injectAppLoading?: boolean;
/**
* SCSS
* @default true
*/
injectGlobalScss?: boolean;
/**
*
* @default true
*/
license?: boolean;
/**
* Nitro Mock
* @default false
*/
nitroMock?: boolean;
/**
* Nitro Mock
*/
nitroMockOptions?: NitroMockPluginOptions;
/**
*
* @default false
*/
print?: boolean;
/**
*
*/
printInfoMap?: PrintPluginOptions['infoMap'];
/**
* PWA
* @default false
*/
pwa?: boolean;
/**
* PWA
*/
pwaOptions?: Partial<PwaPluginOptions>;
}
/**
*
* @description
*/
interface LibraryPluginOptions extends CommonPluginOptions {
/**
* DTS
* @default true
* @description TypeScript
*/
dts?: boolean | PluginOptions;
}
/**
*
*/
type ApplicationOptions = ApplicationPluginOptions;
/**
*
*/
type LibraryOptions = LibraryPluginOptions;
/**
*
* @description
*/
type DefineApplicationOptions = (config?: ConfigEnv) => Promise<{
/** 应用插件配置 */
application?: ApplicationOptions;
/** Vite 配置 */
vite?: UserConfig;
}>;
/**
*
* @description
*/
type DefineLibraryOptions = (config?: ConfigEnv) => Promise<{
/** 库插件配置 */
library?: LibraryOptions;
/** Vite 配置 */
vite?: UserConfig;
}>;
/**
*
* @description
*/
type DefineConfig = DefineApplicationOptions | DefineLibraryOptions;
type VbenViteConfig = Promise<UserConfig> | UserConfig | UserConfigFnPromise;

View File

@ -29,11 +29,6 @@ function getConfFiles() {
return ['.env', '.env.local', `.env.${mode}`, `.env.${mode}.local`];
}
/**
* Get the environment variables starting with the specified prefix
* @param match prefix
* @param confFiles ext
*/
async function loadEnv<T = Record<string, string>>(
match = 'VITE_GLOB_',
confFiles = getConfFiles(),

View File

@ -9,12 +9,13 @@
"build:analyze": "turbo build:analyze",
"build:antdv-next": "pnpm run build --filter=@vben/web-antdv-next",
"check": "pnpm run check:type",
"check:jsdoc": "pnpm run test:jsdoc && node internal/jsdoc-policy/check-jsdoc.mjs",
"check:type": "turbo run typecheck",
"clean": "rimraf .turbo apps/*/dist apps/*/dist.zip packages/**/dist packages/**/dist.zip internal/**/dist internal/**/dist.zip",
"dev": "pnpm run dev:antdv-next",
"dev:antdv-next": "pnpm -F @vben/web-antdv-next run dev",
"format": "eslint apps/web-antdv-next internal --cache --fix && prettier . --write --cache --log-level warn && stylelint \"**/*.{vue,css,less,scss}\" --cache --fix",
"lint": "eslint apps/web-antdv-next internal --cache && prettier . --ignore-unknown --check --cache && stylelint \"**/*.{vue,css,less,scss}\" --cache",
"lint": "pnpm run check:jsdoc && eslint apps/web-antdv-next internal --cache && prettier . --ignore-unknown --check --cache && stylelint \"**/*.{vue,css,less,scss}\" --cache",
"lint:commit": "eslint apps/web-antdv-next internal/commit/validate-commit-msg.mjs --cache",
"postinstall": "pnpm -r run stub --if-present",
"prepare": "husky",
@ -23,7 +24,8 @@
"reinstall": "pnpm run clean && rimraf pnpm-lock.yaml && pnpm install",
"verify:staged": "node internal/commit/husky-fast-check.mjs",
"verify:commit": "pnpm run lint:commit && pnpm -F @vben/web-antdv-next run typecheck",
"test:e2e": "turbo run test:e2e"
"test:e2e": "turbo run test:e2e",
"test:jsdoc": "vitest run internal/jsdoc-policy/check-jsdoc.test.mjs --environment node --reporter=dot"
},
"devDependencies": {
"@playwright/test": "catalog:",

View File

@ -1,16 +1,8 @@
/** layout content 组件的高度 */
export const CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT = `--vben-content-height`;
/** layout content 组件的宽度 */
export const CSS_VARIABLE_LAYOUT_CONTENT_WIDTH = `--vben-content-width`;
/** layout header 组件的高度 */
export const CSS_VARIABLE_LAYOUT_HEADER_HEIGHT = `--vben-header-height`;
/** layout footer 组件的高度 */
export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`;
/** 内容区域的组件ID */
export const ELEMENT_ID_MAIN_CONTENT = `__vben_main_content`;
/**
* @zh_CN
*/
export const DEFAULT_NAMESPACE = 'vben';

View File

@ -1,22 +1,10 @@
/**
* @zh_CN GITHUB
*/
export const VBEN_GITHUB_URL = 'https://github.com/vbenjs/vue-vben-admin';
/**
* @zh_CN
*/
export const VBEN_DOC_URL = 'https://doc.vben.pro';
/**
* @zh_CN Vben Logo
*/
export const VBEN_LOGO_URL =
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp';
/**
* @zh_CN Vben Admin
*/
export const VBEN_PREVIEW_URL = 'https://www.vben.pro';
export const VBEN_ANTDV_NEXT_PREVIEW_URL = 'https://antdv-next.vben.pro';

View File

@ -1,8 +1,3 @@
/**
*
* ,ssr需求
*/
interface ComponentsState {
[key: string]: any;
}

View File

@ -44,32 +44,17 @@ export function isDayjsObject(value: any): value is dayjs.Dayjs {
return dayjs.isDayjs(value);
}
/**
*
* @returns
*/
export const getSystemTimezone = () => {
return dayjs.tz.guess();
};
/**
*
*/
let currentTimezone = getSystemTimezone();
/**
*
* @param timezone
*/
export const setCurrentTimezone = (timezone?: string) => {
currentTimezone = timezone || getSystemTimezone();
dayjs.tz.setDefault(currentTimezone);
};
/**
*
* @returns
*/
export const getCurrentTimezone = () => {
return currentTimezone;
};

View File

@ -96,10 +96,6 @@ export function downloadFileFromBlobPart({
triggerDownload(url, fileName);
}
/**
* img url to base64
* @param url
*/
export function urlToBase64(url: string, mineType?: string): Promise<string> {
return new Promise((resolve, reject) => {
let canvas = document.createElement('CANVAS') as HTMLCanvasElement | null;

View File

@ -115,29 +115,6 @@ function isNumber(value: any): value is number {
return typeof value === 'number' && Number.isFinite(value);
}
/**
* Returns the first value in the provided list that is neither `null` nor `undefined`.
*
* This function iterates over the input values and returns the first one that is
* not strictly equal to `null` or `undefined`. If all values are either `null` or
* `undefined`, it returns `undefined`.
*
* @template T - The type of the input values.
* @param {...(T | null | undefined)[]} values - A list of values to evaluate.
* @returns {T | undefined} - The first value that is not `null` or `undefined`, or `undefined` if none are found.
*
* @example
* // Returns 42 because it is the first non-null, non-undefined value.
* getFirstNonNullOrUndefined(undefined, null, 42, 'hello'); // 42
*
* @example
* // Returns 'hello' because it is the first non-null, non-undefined value.
* getFirstNonNullOrUndefined(null, undefined, 'hello', 123); // 'hello'
*
* @example
* // Returns undefined because all values are either null or undefined.
* getFirstNonNullOrUndefined(undefined, null); // undefined
*/
function getFirstNonNullOrUndefined<T>(
...values: (null | T | undefined)[]
): T | undefined {

View File

@ -1,6 +1,3 @@
/**
* @zh_CN
*/
export class Stack<T> {
/**
* @zh_CN
@ -8,18 +5,9 @@ export class Stack<T> {
get size() {
return this.items.length;
}
/**
* @zh_CN
*/
private readonly dedup: boolean;
/**
* @zh_CN
*/
private items: T[] = [];
/**
* @zh_CN
*/
private readonly maxSize?: number;
constructor(dedup = true, maxSize?: number) {
@ -93,11 +81,5 @@ export class Stack<T> {
}
}
/**
* @zh_CN
* @param dedup
* @param maxSize
* @returns
*/
export const createStack = <T>(dedup = true, maxSize?: number) =>
new Stack<T>(dedup, maxSize);

View File

@ -1,8 +1,3 @@
/**
* @param { Readonly<Promise> } promise
* @param {object=} errorExt - Additional Information you can pass to the err object
* @return { Promise }
*/
export async function to<T, U = Error>(
promise: Readonly<Promise<T>>,
errorExt?: object,

View File

@ -9,12 +9,6 @@ type LayoutType =
type ThemeModeType = 'auto' | 'dark' | 'light';
/**
*
* fixed
* header
* auto
*/
type PreferencesButtonPositionType = 'auto' | 'fixed' | 'header';
type BuiltinThemeType =
@ -42,60 +36,20 @@ type ContentCompactType = 'compact' | 'wide';
type LayoutHeaderModeType = 'auto' | 'auto-scroll' | 'fixed' | 'static';
type LayoutHeaderMenuAlignType = 'center' | 'end' | 'start';
/**
*
* modal
* page
*/
type LoginExpiredModeType = 'modal' | 'page';
/**
*
* background
* normal
*/
type BreadcrumbStyleType = 'background' | 'normal';
/**
*
* backend
* frontend
* mixed
*/
type AccessModeType = 'backend' | 'frontend' | 'mixed';
/**
*
* plain
* rounded
*/
type NavigationStyleType = 'plain' | 'rounded';
/**
*
* brisk
* card
* chrome
* plain
*/
type TabsStyleType = 'brisk' | 'card' | 'chrome' | 'plain';
/**
*
*/
type PageTransitionType = 'fade' | 'fade-down' | 'fade-slide' | 'fade-up';
/**
*
* panel-center
* panel-left
* panel-right
*/
type AuthPageLayoutType = 'panel-center' | 'panel-left' | 'panel-right';
/**
*
*/
interface TimezoneOption {
label: string;
offset: number;

View File

@ -8,25 +8,10 @@ type SelectOption = BasicOption;
type TabOption = BasicOption;
interface BasicUserInfo {
/**
*
*/
avatar: string;
/**
*
*/
realName: string;
/**
*
*/
roles?: string[];
/**
* id
*/
userId: string;
/**
*
*/
username: string;
}

View File

@ -1,83 +1,41 @@
import type { ComputedRef, MaybeRef } from 'vue';
/**
*
*/
type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
/**
*
*/
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
/**
*
*/
type AnyPromiseFunction<T extends any[] = any[], R = void> = (
...arg: T
) => PromiseLike<R>;
/**
*
*/
type AnyNormalFunction<T extends any[] = any[], R = void> = (...arg: T) => R;
/**
*
*/
type AnyFunction<T extends any[] = any[], R = void> =
| AnyNormalFunction<T, R>
| AnyPromiseFunction<T, R>;
/**
* T | null
*/
type Nullable<T> = null | T;
/**
* T | Not null
*/
type NonNullable<T> = T extends null | undefined ? never : T;
/**
*
*/
type Recordable<T> = Record<string, T>;
/**
*
*/
interface ReadonlyRecordable<T = any> {
readonly [key: string]: T;
}
/**
* setTimeout
*/
type TimeoutHandle = ReturnType<typeof setTimeout>;
/**
* setInterval
*/
type IntervalHandle = ReturnType<typeof setInterval>;
/**
* ref getter
*
*/
type MaybeReadonlyRef<T> = (() => T) | ComputedRef<T>;
/**
* ref getter
*
*/
type MaybeComputedRef<T> = MaybeReadonlyRef<T> | MaybeRef<T>;
type Merge<O extends object, T extends object> = {
@ -88,18 +46,6 @@ type Merge<O extends object, T extends object> = {
: never;
};
/**
* T = [
* { name: string; age: number; },
* { sex: 'male' | 'female'; age: string }
* ]
* =>
* MergeAll<T> = {
* name: string;
* sex: 'male' | 'female';
* age: string
* }
*/
type MergeAll<
T extends object[],
R extends object = Record<string, any>,

View File

@ -1,9 +1,6 @@
import type { Component } from 'vue';
import type { RouteRecordRaw } from 'vue-router';
/**
*
*/
type ExRouteRecordRaw = RouteRecordRaw & {
parent?: string;
parents?: string[];
@ -11,65 +8,21 @@ type ExRouteRecordRaw = RouteRecordRaw & {
};
interface MenuRecordBadgeRaw {
/**
*
*/
badge?: string;
/**
*
*/
badgeType?: 'dot' | 'normal';
/**
*
*/
badgeVariants?: 'destructive' | 'primary' | string;
}
/**
*
*/
interface MenuRecordRaw extends MenuRecordBadgeRaw {
/**
*
*/
activeIcon?: string;
/**
*
*/
children?: MenuRecordRaw[];
/**
*
* @default false
*/
disabled?: boolean;
/**
*
*/
icon?: Component | string;
/**
*
*/
name: string;
/**
*
*/
order?: number;
/**
*
*/
parent?: string;
/**
*
*/
parents?: string[];
/**
* key
*/
path: string;
/**
*
* @default true
*/
show?: boolean;
}

Some files were not shown because too many files have changed in this diff Show More