fix: c
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled

This commit is contained in:
2025-12-24 14:42:57 +08:00
parent 08e79c60e7
commit 7743cdc29a
49 changed files with 2179 additions and 4575 deletions

View File

@@ -25,6 +25,37 @@ export namespace FeatureApi {
api_id?: string;
name?: string;
}
export interface FeatureExampleItem {
id: number;
feature_id: number;
api_id: string;
data: string;
create_time: string;
update_time: string;
}
export interface ConfigFeatureExampleRequest {
feature_id: number;
data: string;
}
export interface ConfigFeatureExampleResponse {
success: boolean;
}
export interface GetFeatureExampleRequest {
feature_id: number;
}
export interface GetFeatureExampleResponse {
id: number;
feature_id: number;
api_id: string;
data: string;
create_time: string;
update_time: string;
}
}
/**
@@ -72,10 +103,35 @@ async function deleteFeature(id: number) {
return requestClient.delete<{ success: boolean }>(`/feature/delete/${id}`);
}
/**
* 配置功能示例数据
* @param data 示例数据配置
*/
async function configFeatureExample(
data: FeatureApi.ConfigFeatureExampleRequest,
) {
return requestClient.post<FeatureApi.ConfigFeatureExampleResponse>(
'/feature/config-example',
data,
);
}
/**
* 获取功能示例数据
* @param featureId 功能ID
*/
async function getFeatureExample(featureId: number) {
return requestClient.get<FeatureApi.GetFeatureExampleResponse>(
`/feature/example/${featureId}`,
);
}
export {
configFeatureExample,
createFeature,
deleteFeature,
getFeatureDetail,
getFeatureExample,
getFeatureList,
updateFeature,
};

View File

@@ -0,0 +1,165 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace SystemApiApi {
export interface SystemApiItem {
id: number;
role_id?: number;
api_id?: number;
api_name: string;
api_code: string;
method: string;
url: string;
status: 0 | 1;
description?: string;
create_time?: string;
update_time?: string;
}
export interface SystemApi {
list: SystemApiItem[];
total: number;
}
export interface SystemApiAllResponse {
items: SystemApiItem[];
}
export interface SystemRoleApiResponse {
items: null | SystemApiItem[];
}
export interface RoleApiItem {
id: number;
role_id: number;
api_id: number;
api_name: string;
api_code: string;
method: string;
url: string;
status: 0 | 1;
description?: string;
}
export interface RoleApi {
list: RoleApiItem[];
}
}
/**
* 获取API列表数据
*/
async function getApiList(params: Recordable<any>) {
return requestClient.get<SystemApiApi.SystemApi>('/api/list', {
params,
});
}
/**
* 获取API详情
* @param id API ID
*/
async function getApiDetail(id: number) {
return requestClient.get<SystemApiApi.SystemApiItem>(`/api/detail/${id}`);
}
/**
* 创建API
* @param data API数据
*/
async function createApi(
data: Omit<SystemApiApi.SystemApiItem, 'create_time' | 'id' | 'update_time'>,
) {
return requestClient.post('/api/create', data);
}
/**
* 更新API
* @param id API ID
* @param data API数据
*/
async function updateApi(
id: number,
data: Omit<SystemApiApi.SystemApiItem, 'create_time' | 'id' | 'update_time'>,
) {
return requestClient.put(`/api/update/${id}`, data);
}
/**
* 删除API
* @param id API ID
*/
async function deleteApi(id: number) {
return requestClient.delete(`/api/delete/${id}`);
}
/**
* 批量更新API状态
* @param data.ids API ID数组
* @param data.status 状态值
*/
async function batchUpdateApiStatus(data: { ids: number[]; status: 0 | 1 }) {
return requestClient.put('/api/batch-update-status', data);
}
/**
* 获取角色API权限列表
* @param roleId 角色ID
*/
async function getRoleApiList(roleId: number) {
return requestClient.get<SystemApiApi.SystemRoleApiResponse>(
`/role/${roleId}/api/list`,
);
}
/**
* 分配角色API权限
* @param data.api_ids API ID数组
* @param data.role_id 角色ID
*/
async function assignRoleApi(data: { api_ids: number[]; role_id: number }) {
return requestClient.post('/role/api/assign', data);
}
/**
* 移除角色API权限
* @param data.api_ids API ID数组
* @param data.role_id 角色ID
*/
async function removeRoleApi(data: { api_ids: number[]; role_id: number }) {
return requestClient.post('/role/api/remove', data);
}
/**
* 更新角色API权限全量更新
* @param data.api_ids API ID数组
* @param data.role_id 角色ID
*/
async function updateRoleApi(data: { api_ids: number[]; role_id: number }) {
return requestClient.put('/role/api/update', data);
}
/**
* 获取所有API列表用于权限分配
* @param params.status 状态过滤
*/
async function getAllApiList(params?: { status?: number }) {
return requestClient.get<SystemApiApi.SystemApiAllResponse>('/api/all', {
params,
});
}
export {
assignRoleApi,
batchUpdateApiStatus,
createApi,
deleteApi,
getAllApiList,
getApiDetail,
getApiList,
getRoleApiList,
removeRoleApi,
updateApi,
updateRoleApi,
};

View File

@@ -1,3 +1,4 @@
export * from './api';
export * from './dept';
export * from './menu';
export * from './role';

View File

@@ -51,4 +51,14 @@ async function deleteUser(id: string) {
return requestClient.delete(`/user/delete/${id}`);
}
export { createUser, deleteUser, getUserList, updateUser };
/**
* 重置用户密码
* @param id 用户 ID
* @param data 新密码数据
* @param data.password 新密码
*/
async function resetPassword(id: string, data: { password: string }) {
return requestClient.post(`/reset-password/${id}`, data);
}
export { createUser, deleteUser, getUserList, resetPassword, updateUser };

View File

@@ -61,10 +61,27 @@
"operation": "Operation",
"permissions": "Permissions",
"setPermissions": "Permissions",
"setApiPermissions": "API Permissions",
"roleCode": "Role Code",
"description": "Description"
},
"api": {
"title": "API Management",
"list": "API List",
"name": "API",
"apiName": "API Name",
"apiCode": "API Code",
"method": "Request Method",
"url": "API URL",
"status": "Status",
"description": "Description",
"createTime": "Create Time",
"operation": "Operation",
"permissions": "Permissions",
"setPermissions": "Set Permissions"
},
"user": {
"title": "User Management",
"name": "User",
"list": "User List",
"userName": "Username",
@@ -72,6 +89,11 @@
"status": "Status",
"setPermissions": "Set Permissions",
"createTime": "Create Time",
"operation": "Operation"
"operation": "Operation",
"resetPassword": "Reset Password",
"newPassword": "New Password",
"confirmPassword": "Confirm Password",
"confirmPasswordRequired": "Please confirm password",
"passwordMismatch": "The two passwords do not match"
}
}

View File

@@ -62,11 +62,28 @@
"operation": "操作",
"permissions": "权限",
"setPermissions": "授权",
"setApiPermissions": "API权限",
"roleCode": "角色编号",
"description": "描述"
},
"title": "系统管理",
"api": {
"title": "API管理",
"list": "API列表",
"name": "API",
"apiName": "API名称",
"apiCode": "API编码",
"method": "请求方法",
"url": "API地址",
"status": "状态",
"description": "描述",
"createTime": "创建时间",
"operation": "操作",
"permissions": "权限",
"setPermissions": "设置权限"
},
"user": {
"title": "用户管理",
"name": "用户",
"list": "用户列表",
"userName": "用户名",
@@ -74,6 +91,11 @@
"status": "状态",
"setPermissions": "设置权限",
"createTime": "创建时间",
"operation": "操作"
"operation": "操作",
"resetPassword": "重置密码",
"newPassword": "新密码",
"confirmPassword": "确认密码",
"confirmPasswordRequired": "请确认密码",
"passwordMismatch": "两次输入的密码不一致"
}
}

View File

@@ -0,0 +1,64 @@
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'ion:settings-outline',
order: 9997,
title: $t('system.title'),
},
name: 'System',
path: '/system',
children: [
{
path: '/system/user',
name: 'SystemUser',
meta: {
icon: 'mdi:account',
title: $t('system.user.title'),
},
component: () => import('#/views/system/user/list.vue'),
},
{
path: '/system/role',
name: 'SystemRole',
meta: {
icon: 'mdi:account-group',
title: $t('system.role.title'),
},
component: () => import('#/views/system/role/list.vue'),
},
{
path: '/system/api',
name: 'SystemApi',
meta: {
icon: 'mdi:api',
title: $t('system.api.title'),
},
component: () => import('#/views/system/api/list.vue'),
},
{
path: '/system/menu',
name: 'SystemMenu',
meta: {
icon: 'mdi:menu',
title: $t('system.menu.title'),
},
component: () => import('#/views/system/menu/list.vue'),
},
{
path: '/system/dept',
name: 'SystemDept',
meta: {
icon: 'charm:organisation',
title: $t('system.dept.title'),
},
component: () => import('#/views/system/dept/list.vue'),
},
],
},
];
export default routes;

View File

@@ -69,12 +69,26 @@ export function useColumns<T = FeatureApi.FeatureItem>(
nameTitle: '模块',
onClick: onActionClick,
},
options: [
{
code: 'edit',
text: '编辑',
},
{
code: 'delete',
text: '删除',
},
{
code: 'example',
text: '示例配置',
},
],
name: 'CellOperation',
},
field: 'operation',
fixed: 'right',
title: '操作',
width: 130,
width: 180,
},
];
}

View File

@@ -5,7 +5,7 @@ import type {
} from '#/adapter/vxe-table';
import type { FeatureApi } from '#/api/product-manage';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
@@ -14,6 +14,7 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteFeature, getFeatureList } from '#/api/product-manage';
import { useColumns, useGridFormSchema } from './data';
import ExampleConfig from './modules/example-config.vue';
import Form from './modules/form.vue';
// 表单抽屉
@@ -22,6 +23,12 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
destroyOnClose: true,
});
// 示例配置弹窗
const [ExampleConfigModal, exampleConfigModalApi] = useVbenModal({
connectedComponent: ExampleConfig,
destroyOnClose: true,
});
// 表格配置
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
@@ -67,6 +74,10 @@ function onActionClick(e: OnActionClickParams<FeatureApi.FeatureItem>) {
onEdit(e.row);
break;
}
case 'example': {
onExampleConfig(e.row);
break;
}
}
}
@@ -104,11 +115,17 @@ function onRefresh() {
function onCreate() {
formDrawerApi.setData({}).open();
}
// 示例配置处理
function onExampleConfig(row: FeatureApi.FeatureItem) {
exampleConfigModalApi.setData(row).open();
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<ExampleConfigModal @success="onRefresh" />
<Grid table-title="模块列表">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">

View File

@@ -0,0 +1,151 @@
<script lang="ts" setup>
import type { FeatureApi } from '#/api/product-manage';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Input, message } from 'ant-design-vue';
import { configFeatureExample, getFeatureExample } from '#/api/product-manage';
const emit = defineEmits(['success']);
const featureData = ref<FeatureApi.FeatureItem>();
const exampleData = ref('');
const loading = ref(false);
const saving = ref(false);
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!featureData.value) return;
saving.value = true;
try {
await configFeatureExample({
feature_id: featureData.value.id,
data: exampleData.value,
});
message.success('示例配置保存成功');
emit('success');
modalApi.close();
} catch (error) {
console.error('保存示例配置失败:', error);
} finally {
saving.value = false;
}
},
onOpenChange(isOpen) {
if (isOpen) {
const data = modalApi.getData<FeatureApi.FeatureItem>();
if (data) {
featureData.value = data;
loadExampleData();
}
} else {
featureData.value = undefined;
exampleData.value = '';
}
},
});
// 加载示例数据
async function loadExampleData() {
if (!featureData.value) return;
loading.value = true;
try {
const response = await getFeatureExample(featureData.value.id);
exampleData.value = response.data || '';
} catch (error) {
console.error('获取示例数据失败:', error);
exampleData.value = '';
} finally {
loading.value = false;
}
}
// 格式化JSON
function formatJson() {
try {
if (exampleData.value) {
const parsed = JSON.parse(exampleData.value);
exampleData.value = JSON.stringify(parsed, null, 2);
}
} catch {
message.error('JSON格式错误无法格式化');
}
}
// 清空数据
function clearData() {
exampleData.value = '';
}
const modalTitle = computed(() => {
return featureData.value
? `示例配置 - ${featureData.value.name}`
: '示例配置';
});
const isJsonValid = computed(() => {
if (!exampleData.value) return true;
try {
JSON.parse(exampleData.value);
return true;
} catch {
return false;
}
});
</script>
<template>
<Modal :title="modalTitle" class="w-[1200px]">
<div class="space-y-4">
<div>
<div class="mb-2 flex items-center justify-between">
<label class="text-sm font-medium">示例数据 (JSON格式)</label>
<div class="space-x-2">
<Button size="small" @click="formatJson">格式化</Button>
<Button size="small" @click="clearData">清空</Button>
</div>
</div>
<Input.TextArea
v-model:value="exampleData"
:loading="loading"
:rows="15"
:status="isJsonValid ? undefined : 'error'"
placeholder="请输入JSON格式的示例数据..."
class="font-mono text-sm"
/>
<div v-if="!isJsonValid" class="mt-1 text-xs text-red-500">
JSON格式错误请检查语法
</div>
</div>
<div class="text-xs text-gray-500">
<p>提示</p>
<ul class="list-disc space-y-1 pl-4">
<li>如果当前功能没有配置示例数据输入框将为空</li>
<li>可以在此输入JSON格式的示例数据</li>
<li>点击"格式化"按钮可以美化JSON格式</li>
<li>保存后该示例数据将用于功能展示</li>
</ul>
</div>
</div>
<template #footer>
<div class="flex justify-end space-x-2">
<Button @click="modalApi.close">取消</Button>
<Button
type="primary"
:loading="saving"
:disabled="!isJsonValid"
@click="modalApi.onConfirm"
>
保存
</Button>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,178 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemApiApi } from '#/api';
import { $t } from '#/locales';
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'api_name',
label: $t('system.api.apiName'),
rules: 'required',
},
{
component: 'Input',
fieldName: 'api_code',
label: $t('system.api.apiCode'),
rules: 'required',
},
{
component: 'Select',
componentProps: {
allowClear: true,
class: 'w-full',
placeholder: '请选择请求方法',
options: [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
],
},
fieldName: 'method',
label: $t('system.api.method'),
rules: 'required',
},
{
component: 'Input',
fieldName: 'url',
label: $t('system.api.url'),
rules: 'required',
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: $t('common.enabled'), value: 1 },
{ label: $t('common.disabled'), value: 0 },
],
optionType: 'button',
},
defaultValue: 1,
fieldName: 'status',
label: $t('system.api.status'),
},
{
component: 'Textarea',
fieldName: 'description',
label: $t('system.api.description'),
},
];
}
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'api_name',
label: $t('system.api.apiName'),
},
{
component: 'Input',
fieldName: 'api_code',
label: $t('system.api.apiCode'),
},
{
component: 'Select',
componentProps: {
allowClear: true,
class: 'w-full',
options: [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
],
},
fieldName: 'method',
label: $t('system.api.method'),
},
{
component: 'Select',
componentProps: {
allowClear: true,
class: 'w-full',
options: [
{ label: $t('common.enabled'), value: 1 },
{ label: $t('common.disabled'), value: 0 },
],
},
fieldName: 'status',
label: $t('system.api.status'),
},
{
component: 'RangePicker',
fieldName: 'create_time',
label: $t('system.api.createTime'),
},
];
}
export function useColumns<T = SystemApiApi.SystemApiItem>(
onActionClick: OnActionClickFn<T>,
onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'api_name',
title: $t('system.api.apiName'),
width: 200,
},
{
field: 'api_code',
title: $t('system.api.apiCode'),
width: 200,
},
{
field: 'method',
title: $t('system.api.method'),
width: 100,
},
{
field: 'url',
title: $t('system.api.url'),
minWidth: 200,
},
{
cellRender: {
attrs: { beforeChange: onStatusChange },
name: onStatusChange ? 'CellSwitch' : 'CellTag',
},
field: 'status',
title: $t('system.api.status'),
width: 100,
},
{
field: 'description',
minWidth: 150,
title: $t('system.api.description'),
},
{
field: 'create_time',
title: $t('system.api.createTime'),
width: 200,
},
{
align: 'center',
cellRender: {
attrs: {
nameField: 'api_name',
nameTitle: $t('system.api.apiName'),
onClick: onActionClick,
},
name: 'CellOperation',
options: [
'edit', // 默认的编辑按钮
'delete', // 默认的删除按钮
],
},
field: 'operation',
fixed: 'right',
title: $t('system.api.operation'),
width: 130,
},
];
}

View File

@@ -0,0 +1,171 @@
<script lang="ts" setup>
import type { Recordable } from '@vben/types';
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { SystemApiApi } from '#/api';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteApi, getApiList, updateApi } from '#/api';
import { $t } from '#/locales';
import { useColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['create_time', ['startTime', 'endTime']]],
schema: useGridFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useColumns(onActionClick, onStatusChange),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getApiList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
props: {
result: 'list',
total: 'total',
},
autoLoad: true,
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
} as VxeTableGridOptions<SystemApiApi.SystemApiItem>,
});
function onActionClick(e: OnActionClickParams<SystemApiApi.SystemApiItem>) {
switch (e.code) {
case 'delete': {
onDelete(e.row);
break;
}
case 'edit': {
onEdit(e.row);
break;
}
}
}
/**
* 将Antd的Modal.confirm封装为promise方便在异步函数中调用。
* @param content 提示内容
* @param title 提示标题
*/
function confirm(content: string, title: string) {
return new Promise((reslove, reject) => {
Modal.confirm({
content,
onCancel() {
reject(new Error('已取消'));
},
onOk() {
reslove(true);
},
title,
});
});
}
/**
* 状态开关即将改变
* @param newStatus 期望改变的状态值
* @param row 行数据
* @returns 返回false则中止改变返回其他值undefined、true则允许改变
*/
async function onStatusChange(
newStatus: number,
row: SystemApiApi.SystemApiItem,
) {
const status: Recordable<string> = {
0: '禁用',
1: '启用',
};
try {
await confirm(
`你要将${row.api_name}的状态切换为 【${status[newStatus.toString()]}】 吗?`,
`切换状态`,
);
await updateApi(row.id, {
...row,
status: newStatus as 0 | 1,
});
return true;
} catch {
return false;
}
}
function onEdit(row: SystemApiApi.SystemApiItem) {
formDrawerApi.setData(row).open();
}
function onDelete(row: SystemApiApi.SystemApiItem) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.api_name]),
duration: 0,
key: 'action_process_msg',
});
deleteApi(row.id)
.then(() => {
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.api_name]),
key: 'action_process_msg',
});
onRefresh();
})
.catch(() => {
hideLoading();
});
}
function onRefresh() {
gridApi.query();
}
function onCreate() {
formDrawerApi.setData({}).open();
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<Grid :table-title="$t('system.api.list')">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', [$t('system.api.name')]) }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,77 @@
<script lang="ts" setup>
import type { SystemApiApi } from '#/api';
import { computed, ref } from 'vue';
import { useVbenDrawer, useVbenForm } from '@vben/common-ui';
import { createApi, updateApi } from '#/api';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits<{
success: [];
}>();
const formData = ref<SystemApiApi.SystemApiItem>();
const schema = useFormSchema();
const [Form, formApi] = useVbenForm({
commonConfig: {
colon: true,
formItemClass: 'col-span-2 md:col-span-1',
},
schema,
showDefaultActions: false,
wrapperClass: 'grid-cols-2 gap-x-4',
});
const [Drawer, drawerApi] = useVbenDrawer({
onConfirm: onSubmit,
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<SystemApiApi.SystemApiItem>();
if (data) {
formData.value = data;
formApi.setValues(formData.value);
} else {
formApi.resetForm();
}
}
},
});
async function onSubmit() {
const { valid } = await formApi.validate();
if (valid) {
drawerApi.lock();
const data =
await formApi.getValues<
Omit<SystemApiApi.SystemApiItem, 'create_time' | 'id' | 'update_time'>
>();
try {
await (formData.value?.id
? updateApi(formData.value.id, data)
: createApi(data));
drawerApi.close();
emit('success');
} finally {
drawerApi.unlock();
}
}
}
const getDrawerTitle = computed(() =>
formData.value?.id
? $t('ui.actionTitle.edit', [$t('system.api.name')])
: $t('ui.actionTitle.create', [$t('system.api.name')]),
);
</script>
<template>
<Drawer class="w-full max-w-[600px]" :title="getDrawerTitle">
<Form class="mx-4" />
</Drawer>
</template>

View File

@@ -80,7 +80,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
];
}
export function useColumns<T = SystemRoleApi.SystemRole>(
export function useColumns<T = SystemRoleApi.SystemRoleItem>(
onActionClick: OnActionClickFn<T>,
onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
): VxeTableGridOptions['columns'] {
@@ -123,11 +123,26 @@ export function useColumns<T = SystemRoleApi.SystemRole>(
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
text: $t('ui.actionTitle.edit', [$t('system.role.name')]),
},
{
code: 'api-permissions',
text: $t('system.role.setApiPermissions'),
},
{
code: 'delete',
text: $t('ui.actionTitle.delete', [$t('system.role.name')]),
danger: true,
},
],
},
field: 'operation',
fixed: 'right',
title: $t('system.role.operation'),
width: 130,
width: 200,
},
];
}

View File

@@ -17,6 +17,7 @@ import { deleteRole, getRoleList, updateRole } from '#/api';
import { $t } from '#/locales';
import { useColumns, useGridFormSchema } from './data';
import ApiPermissions from './modules/api-permissions.vue';
import Form from './modules/form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
@@ -24,6 +25,11 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
destroyOnClose: true,
});
const [ApiPermissionsDrawer, apiPermissionsDrawerApi] = useVbenDrawer({
connectedComponent: ApiPermissions,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['create_time', ['startTime', 'endTime']]],
@@ -56,11 +62,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
search: true,
zoom: true,
},
} as VxeTableGridOptions<SystemRoleApi.SystemRole>,
} as VxeTableGridOptions<SystemRoleApi.SystemRoleItem>,
});
function onActionClick(e: OnActionClickParams<SystemRoleApi.SystemRole>) {
function onActionClick(e: OnActionClickParams<SystemRoleApi.SystemRoleItem>) {
switch (e.code) {
case 'api-permissions': {
onApiPermissions(e.row);
break;
}
case 'delete': {
onDelete(e.row);
break;
@@ -118,11 +128,11 @@ async function onStatusChange(
}
}
function onEdit(row: SystemRoleApi.SystemRole) {
function onEdit(row: SystemRoleApi.SystemRoleItem) {
formDrawerApi.setData(row).open();
}
function onDelete(row: SystemRoleApi.SystemRole) {
function onDelete(row: SystemRoleApi.SystemRoleItem) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.role_name]),
duration: 0,
@@ -148,10 +158,15 @@ function onRefresh() {
function onCreate() {
formDrawerApi.setData({}).open();
}
function onApiPermissions(row: SystemRoleApi.SystemRoleItem) {
apiPermissionsDrawerApi.setData(row).open();
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<ApiPermissionsDrawer @success="onRefresh" />
<Grid :table-title="$t('system.role.list')">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">

View File

@@ -0,0 +1,214 @@
<script lang="ts" setup>
import type { SystemRoleApi } from '#/api';
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { Button, Checkbox, message, Spin } from 'ant-design-vue';
import { getAllApiList, getRoleApiList, updateRoleApi } from '#/api';
import { $t } from '#/locales';
const emits = defineEmits(['success']);
const loading = ref(false);
const allApiList = ref<any[]>([]);
const roleApiList = ref<any[]>([]);
const selectedApiIds = ref<number[]>([]);
const formData = ref<SystemRoleApi.SystemRoleItem>();
const roleId = ref<number>();
const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
await onSave();
},
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<SystemRoleApi.SystemRoleItem>();
if (data) {
formData.value = data;
roleId.value = data.id;
fetchRoleApiList();
}
fetchAllApiList();
}
},
});
// 计算已选中的API ID
const selectedApiIdsSet = computed(() => new Set(selectedApiIds.value));
// 获取所有API列表
async function fetchAllApiList() {
try {
loading.value = true;
const response = await getAllApiList({ status: 1 }); // 只获取启用的API
allApiList.value = response.items || [];
} catch (error) {
console.error('获取API列表失败:', error);
message.error('获取API列表失败');
} finally {
loading.value = false;
}
}
// 获取角色已分配的API权限
async function fetchRoleApiList() {
if (!roleId.value) return;
try {
loading.value = true;
const response = await getRoleApiList(roleId.value);
roleApiList.value = response.items || [];
selectedApiIds.value = roleApiList.value.map((item) => item.api_id);
} catch (error) {
console.error('获取角色API权限失败:', error);
message.error('获取角色API权限失败');
} finally {
loading.value = false;
}
}
// 全选/取消全选
function toggleSelectAll(e: any) {
const checked = e.target.checked;
selectedApiIds.value = checked
? allApiList.value.map((api) => api.api_id)
: [];
}
// 判断是否全选
const isAllSelected = computed(() => {
return (
allApiList.value.length > 0 &&
selectedApiIds.value.length === allApiList.value.length
);
});
// 判断是否部分选中
const isIndeterminate = computed(() => {
return (
selectedApiIds.value.length > 0 &&
selectedApiIds.value.length < allApiList.value.length
);
});
// 保存API权限
async function onSave() {
if (!roleId.value) return;
try {
loading.value = true;
await updateRoleApi({
role_id: roleId.value,
api_ids: selectedApiIds.value,
});
message.success('API权限保存成功');
emits('success');
drawerApi.close();
} catch (error) {
console.error('保存API权限失败:', error);
message.error('保存API权限失败');
} finally {
loading.value = false;
}
}
// 重置选择
function onReset() {
selectedApiIds.value = roleApiList.value.map((item) => item.api_id);
}
// 计算抽屉标题
const getDrawerTitle = computed(() => {
return formData.value?.role_name
? `${$t('system.role.setApiPermissions')} - ${formData.value.role_name}`
: $t('system.role.setApiPermissions');
});
</script>
<template>
<Drawer :title="getDrawerTitle">
<div class="p-4">
<div class="mb-4">
<p class="text-sm text-gray-500">
为角色分配API访问权限勾选的API将被允许访问
</p>
</div>
<Spin :spinning="loading">
<div class="space-y-4">
<!-- 全选操作 -->
<div class="flex items-center gap-4 rounded-lg bg-gray-50 p-3">
<Checkbox
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@change="toggleSelectAll"
>
全选
</Checkbox>
<span class="text-sm text-gray-500">
已选择 {{ selectedApiIds.length }} / {{ allApiList.length }} 个API
</span>
</div>
<!-- API列表 -->
<div class="max-h-96 overflow-y-auto rounded-lg border">
<div
v-for="api in allApiList"
:key="api.api_id"
class="flex items-center gap-3 border-b p-3 last:border-b-0 hover:bg-gray-50"
>
<Checkbox
:checked="selectedApiIdsSet.has(api.api_id)"
@change="
(e) => {
if (e.target.checked) {
selectedApiIds.push(api.api_id);
} else {
const index = selectedApiIds.indexOf(api.api_id);
if (index > -1) {
selectedApiIds.splice(index, 1);
}
}
}
"
/>
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="font-medium">{{ api.api_name }}</span>
<span
class="rounded px-2 py-1 text-xs"
:class="{
'bg-blue-100 text-blue-800': api.method === 'GET',
'bg-green-100 text-green-800': api.method === 'POST',
'bg-orange-100 text-orange-800': api.method === 'PUT',
'bg-red-100 text-red-800': api.method === 'DELETE',
}"
>
{{ api.method }}
</span>
</div>
<div class="mt-1 text-sm text-gray-500">
{{ api.url }}
</div>
<div v-if="api.description" class="mt-1 text-xs text-gray-400">
{{ api.description }}
</div>
</div>
</div>
</div>
</div>
</Spin>
<!-- 操作按钮 -->
<div class="mt-6 flex justify-end gap-2">
<Button @click="onReset"> 重置 </Button>
<Button type="primary" @click="onSave" :loading="loading">
保存
</Button>
</div>
</div>
</Drawer>
</template>

View File

@@ -121,12 +121,17 @@ export function useColumns<T = SystemUserApi.SystemUser>(
nameTitle: $t('system.user.userName'),
onClick: onActionClick,
},
options: [
{ code: 'edit', text: '编辑' },
{ code: 'resetPassword', text: '重置密码' },
{ code: 'delete', text: '删除' },
],
name: 'CellOperation',
},
field: 'operation',
fixed: 'right',
title: $t('system.user.operation'),
width: 130,
width: 200,
},
];
}

View File

@@ -18,12 +18,18 @@ import { $t } from '#/locales';
import { useColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import ResetPasswordForm from './modules/reset-password-form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
const [ResetPasswordDrawer, resetPasswordDrawerApi] = useVbenDrawer({
connectedComponent: ResetPasswordForm,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['create_time', ['startTime', 'endTime']]],
@@ -69,6 +75,10 @@ function onActionClick(e: OnActionClickParams<SystemUserApi.SystemUser>) {
onEdit(e.row);
break;
}
case 'resetPassword': {
onResetPassword(e.row);
break;
}
}
}
@@ -122,6 +132,10 @@ function onEdit(row: SystemUserApi.SystemUser) {
formDrawerApi.setData(row).open();
}
function onResetPassword(row: SystemUserApi.SystemUser) {
resetPasswordDrawerApi.setData(row).open();
}
function onDelete(row: SystemUserApi.SystemUser) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.username]),
@@ -152,6 +166,7 @@ function onCreate() {
<template>
<Page auto-content-height>
<FormDrawer />
<ResetPasswordDrawer @success="onRefresh" />
<Grid :table-title="$t('system.user.list')">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">

View File

@@ -0,0 +1,83 @@
<script lang="ts" setup>
import type { SystemUserApi } from '#/api/system/user';
import { computed, ref } from 'vue';
import { useVbenDrawer, z } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
import { resetPassword } from '#/api/system/user';
const emits = defineEmits(['success']);
const formData = ref<SystemUserApi.SystemUser>();
const [Form, formApi] = useVbenForm({
schema: [
{
component: 'InputPassword',
fieldName: 'password',
label: '新密码',
rules: z.string().min(1, { message: '请输入新密码' }),
},
{
component: 'InputPassword',
fieldName: 'confirmPassword',
label: '确认密码',
dependencies: {
rules(values) {
const { password } = values;
return z
.string()
.min(1, { message: '请确认密码' })
.refine((value) => value === password, {
message: '两次输入的密码不一致',
});
},
triggerFields: ['password'],
},
},
],
showDefaultActions: false,
});
const id = ref();
const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
const values = await formApi.getValues();
drawerApi.lock();
resetPassword(id.value, { password: values.password })
.then(() => {
emits('success');
drawerApi.close();
})
.catch(() => {
drawerApi.unlock();
});
},
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<SystemUserApi.SystemUser>();
formApi.resetForm();
if (data) {
formData.value = data;
id.value = data.id;
} else {
id.value = undefined;
}
}
},
});
const getDrawerTitle = computed(() => {
return '重置密码';
});
</script>
<template>
<Drawer :title="getDrawerTitle">
<Form />
</Drawer>
</template>

View File

@@ -11,7 +11,9 @@ export default defineConfig(async () => {
rewrite: (path) => path.replace(/^\/api/, ''),
// mock代理目标地址
// target: 'http://localhost:8888/api',
target: 'https://www.tianyuandb.com/api',
// target: 'https://www.tianyuandb.com/api',
// target: 'https://www.zhinengcha.cn/api',
target: 'https://www.quannengcha./api',
ws: true,
},
},

1067
cl.yaml

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +0,0 @@
import { createHash } from 'node:crypto';
import { describe, expect, it } from 'vitest';
import { generatorContentHash } from '../hash';
describe('generatorContentHash', () => {
it('should generate an MD5 hash for the content', () => {
const content = 'example content';
const expectedHash = createHash('md5')
.update(content, 'utf8')
.digest('hex');
const actualHash = generatorContentHash(content);
expect(actualHash).toBe(expectedHash);
});
it('should generate an MD5 hash with specified length', () => {
const content = 'example content';
const hashLength = 10;
const generatedHash = generatorContentHash(content, hashLength);
expect(generatedHash).toHaveLength(hashLength);
});
it('should correctly generate the hash with specified length', () => {
const content = 'example content';
const hashLength = 8;
const expectedHash = createHash('md5')
.update(content, 'utf8')
.digest('hex')
.slice(0, hashLength);
const generatedHash = generatorContentHash(content, hashLength);
expect(generatedHash).toBe(expectedHash);
});
it('should return full hash if hash length parameter is not provided', () => {
const content = 'example content';
const expectedHash = createHash('md5')
.update(content, 'utf8')
.digest('hex');
const actualHash = generatorContentHash(content);
expect(actualHash).toBe(expectedHash);
});
it('should handle empty content', () => {
const content = '';
const expectedHash = createHash('md5')
.update(content, 'utf8')
.digest('hex');
const actualHash = generatorContentHash(content);
expect(actualHash).toBe(expectedHash);
});
});

View File

@@ -1,67 +0,0 @@
// pathUtils.test.ts
import { describe, expect, it } from 'vitest';
import { toPosixPath } from '../path';
describe('toPosixPath', () => {
// 测试 Windows 风格路径到 POSIX 风格路径的转换
it('converts Windows-style paths to POSIX paths', () => {
const windowsPath = String.raw`C:\Users\Example\file.txt`;
const expectedPosixPath = 'C:/Users/Example/file.txt';
expect(toPosixPath(windowsPath)).toBe(expectedPosixPath);
});
// 确认 POSIX 风格路径不会被改变
it('leaves POSIX-style paths unchanged', () => {
const posixPath = '/home/user/file.txt';
expect(toPosixPath(posixPath)).toBe(posixPath);
});
// 测试带有多个分隔符的路径
it('converts paths with mixed separators', () => {
const mixedPath = String.raw`C:/Users\Example\file.txt`;
const expectedPosixPath = 'C:/Users/Example/file.txt';
expect(toPosixPath(mixedPath)).toBe(expectedPosixPath);
});
// 测试空字符串
it('handles empty strings', () => {
const emptyPath = '';
expect(toPosixPath(emptyPath)).toBe('');
});
// 测试仅包含分隔符的路径
it('handles path with only separators', () => {
const separatorsPath = '\\\\\\';
const expectedPosixPath = '///';
expect(toPosixPath(separatorsPath)).toBe(expectedPosixPath);
});
// 测试不包含任何分隔符的路径
it('handles path without separators', () => {
const noSeparatorPath = 'file.txt';
expect(toPosixPath(noSeparatorPath)).toBe('file.txt');
});
// 测试以分隔符结尾的路径
it('handles path ending with a separator', () => {
const endingSeparatorPath = 'C:\\Users\\Example\\';
const expectedPosixPath = 'C:/Users/Example/';
expect(toPosixPath(endingSeparatorPath)).toBe(expectedPosixPath);
});
// 测试以分隔符开头的路径
it('handles path starting with a separator', () => {
const startingSeparatorPath = String.raw`\Users\Example`;
const expectedPosixPath = '/Users/Example';
expect(toPosixPath(startingSeparatorPath)).toBe(expectedPosixPath);
});
// 测试包含非法字符的路径
it('handles path with invalid characters', () => {
const invalidCharsPath = String.raw`C:\Us*?ers\Ex<ample>|file.txt`;
const expectedPosixPath = 'C:/Us*?ers/Ex<ample>|file.txt';
expect(toPosixPath(invalidCharsPath)).toBe(expectedPosixPath);
});
});

View File

@@ -1,130 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { StorageManager } from '../storage-manager';
describe('storageManager', () => {
let storageManager: StorageManager;
beforeEach(() => {
vi.useFakeTimers();
localStorage.clear();
storageManager = new StorageManager({
prefix: 'test_',
});
});
it('should set and get an item', () => {
storageManager.setItem('user', { age: 30, name: 'John Doe' });
const user = storageManager.getItem('user');
expect(user).toEqual({ age: 30, name: 'John Doe' });
});
it('should return default value if item does not exist', () => {
const user = storageManager.getItem('nonexistent', {
age: 0,
name: 'Default User',
});
expect(user).toEqual({ age: 0, name: 'Default User' });
});
it('should remove an item', () => {
storageManager.setItem('user', { age: 30, name: 'John Doe' });
storageManager.removeItem('user');
const user = storageManager.getItem('user');
expect(user).toBeNull();
});
it('should clear all items with the prefix', () => {
storageManager.setItem('user1', { age: 30, name: 'John Doe' });
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });
storageManager.clear();
expect(storageManager.getItem('user1')).toBeNull();
expect(storageManager.getItem('user2')).toBeNull();
});
it('should clear expired items', () => {
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
vi.advanceTimersByTime(1001); // 快进时间
storageManager.clearExpiredItems();
const user = storageManager.getItem('user');
expect(user).toBeNull();
});
it('should not clear non-expired items', () => {
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
vi.advanceTimersByTime(5000); // 快进时间
storageManager.clearExpiredItems();
const user = storageManager.getItem('user');
expect(user).toEqual({ age: 30, name: 'John Doe' });
});
it('should handle JSON parse errors gracefully', () => {
localStorage.setItem('test_user', '{ invalid JSON }');
const user = storageManager.getItem('user', {
age: 0,
name: 'Default User',
});
expect(user).toEqual({ age: 0, name: 'Default User' });
});
it('should return null for non-existent items without default value', () => {
const user = storageManager.getItem('nonexistent');
expect(user).toBeNull();
});
it('should overwrite existing items', () => {
storageManager.setItem('user', { age: 30, name: 'John Doe' });
storageManager.setItem('user', { age: 25, name: 'Jane Doe' });
const user = storageManager.getItem('user');
expect(user).toEqual({ age: 25, name: 'Jane Doe' });
});
it('should handle items without expiry correctly', () => {
storageManager.setItem('user', { age: 30, name: 'John Doe' });
vi.advanceTimersByTime(5000);
const user = storageManager.getItem('user');
expect(user).toEqual({ age: 30, name: 'John Doe' });
});
it('should remove expired items when accessed', () => {
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
vi.advanceTimersByTime(1001); // 快进时间
const user = storageManager.getItem('user');
expect(user).toBeNull();
});
it('should not remove non-expired items when accessed', () => {
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
vi.advanceTimersByTime(5000); // 快进时间
const user = storageManager.getItem('user');
expect(user).toEqual({ age: 30, name: 'John Doe' });
});
it('should handle multiple items with different expiry times', () => {
storageManager.setItem('user1', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' }, 2000); // 2秒过期
vi.advanceTimersByTime(1500); // 快进时间
storageManager.clearExpiredItems();
const user1 = storageManager.getItem('user1');
const user2 = storageManager.getItem('user2');
expect(user1).toBeNull();
expect(user2).toEqual({ age: 25, name: 'Jane Doe' });
});
it('should handle items with no expiry', () => {
storageManager.setItem('user', { age: 30, name: 'John Doe' });
vi.advanceTimersByTime(10_000); // 快进时间
storageManager.clearExpiredItems();
const user = storageManager.getItem('user');
expect(user).toEqual({ age: 30, name: 'John Doe' });
});
it('should clear all items correctly', () => {
storageManager.setItem('user1', { age: 30, name: 'John Doe' });
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });
storageManager.clear();
const user1 = storageManager.getItem('user1');
const user2 = storageManager.getItem('user2');
expect(user1).toBeNull();
expect(user2).toBeNull();
});
});

View File

@@ -1,58 +0,0 @@
import { describe, expect, it } from 'vitest';
import {
convertToHsl,
convertToHslCssVar,
convertToRgb,
isValidColor,
} from '../convert';
describe('color conversion functions', () => {
it('should correctly convert color to HSL format', () => {
const color = '#ff0000';
const expectedHsl = 'hsl(0 100% 50%)';
expect(convertToHsl(color)).toEqual(expectedHsl);
});
it('should correctly convert color with alpha to HSL format', () => {
const color = 'rgba(255, 0, 0, 0.5)';
const expectedHsl = 'hsl(0 100% 50%) 0.5';
expect(convertToHsl(color)).toEqual(expectedHsl);
});
it('should correctly convert color to HSL CSS variable format', () => {
const color = '#ff0000';
const expectedHsl = '0 100% 50%';
expect(convertToHslCssVar(color)).toEqual(expectedHsl);
});
it('should correctly convert color with alpha to HSL CSS variable format', () => {
const color = 'rgba(255, 0, 0, 0.5)';
const expectedHsl = '0 100% 50% / 0.5';
expect(convertToHslCssVar(color)).toEqual(expectedHsl);
});
it('should correctly convert color to RGB CSS variable format', () => {
const color = 'hsl(284, 100%, 50%)';
const expectedRgb = 'rgb(187, 0, 255)';
expect(convertToRgb(color)).toEqual(expectedRgb);
});
it('should correctly convert color with alpha to RGBA CSS variable format', () => {
const color = 'hsla(284, 100%, 50%, 0.92)';
const expectedRgba = 'rgba(187, 0, 255, 0.92)';
expect(convertToRgb(color)).toEqual(expectedRgba);
});
});
describe('isValidColor', () => {
it('isValidColor function', () => {
// 测试有效颜色
expect(isValidColor('blue')).toBe(true);
expect(isValidColor('#000000')).toBe(true);
// 测试无效颜色
expect(isValidColor('invalid color')).toBe(false);
expect(isValidColor()).toBe(false);
});
});

View File

@@ -1,53 +0,0 @@
import { describe, expect, it } from 'vitest';
import { diff } from '../diff';
describe('diff function', () => {
it('should return an empty object when comparing identical objects', () => {
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
expect(diff(obj1, obj2)).toEqual(undefined);
});
it('should detect simple changes in primitive values', () => {
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 3 };
expect(diff(obj1, obj2)).toEqual({ b: 3 });
});
it('should detect nested object changes', () => {
const obj1 = { a: 1, b: { c: 2, d: 4 } };
const obj2 = { a: 1, b: { c: 3, d: 4 } };
expect(diff(obj1, obj2)).toEqual({ b: { c: 3 } });
});
it('should handle array changes', () => {
const obj1 = { a: [1, 2, 3], b: 2 };
const obj2 = { a: [1, 2, 4], b: 2 };
expect(diff(obj1, obj2)).toEqual({ a: [1, 2, 4] });
});
it('should handle added keys', () => {
const obj1 = { a: 1 };
const obj2 = { a: 1, b: 2 };
expect(diff(obj1, obj2)).toEqual({ b: 2 });
});
it('should handle removed keys', () => {
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1 };
expect(diff(obj1, obj2)).toEqual(undefined);
});
it('should handle boolean value changes', () => {
const obj1 = { a: true, b: false };
const obj2 = { a: true, b: true };
expect(diff(obj1, obj2)).toEqual({ b: true });
});
it('should handle null and undefined values', () => {
const obj1 = { a: null, b: undefined };
const obj2: any = { a: 1, b: undefined };
expect(diff(obj1, obj2)).toEqual({ a: 1 });
});
});

View File

@@ -1,127 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getElementVisibleRect } from '../dom';
describe('getElementVisibleRect', () => {
// 设置浏览器视口尺寸的 mock
beforeEach(() => {
vi.spyOn(document.documentElement, 'clientHeight', 'get').mockReturnValue(
800,
);
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800);
vi.spyOn(document.documentElement, 'clientWidth', 'get').mockReturnValue(
1000,
);
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1000);
});
it('should return default rect if element is undefined', () => {
expect(getElementVisibleRect()).toEqual({
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
});
});
it('should return default rect if element is null', () => {
expect(getElementVisibleRect(null)).toEqual({
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
});
});
it('should return correct visible rect when element is fully visible', () => {
const element = {
getBoundingClientRect: () => ({
bottom: 400,
height: 300,
left: 200,
right: 600,
top: 100,
width: 400,
}),
} as HTMLElement;
expect(getElementVisibleRect(element)).toEqual({
bottom: 400,
height: 300,
left: 200,
right: 600,
top: 100,
width: 400,
});
});
it('should return correct visible rect when element is partially off-screen at the top', () => {
const element = {
getBoundingClientRect: () => ({
bottom: 200,
height: 250,
left: 100,
right: 500,
top: -50,
width: 400,
}),
} as HTMLElement;
expect(getElementVisibleRect(element)).toEqual({
bottom: 200,
height: 200,
left: 100,
right: 500,
top: 0,
width: 400,
});
});
it('should return correct visible rect when element is partially off-screen at the right', () => {
const element = {
getBoundingClientRect: () => ({
bottom: 400,
height: 300,
left: 800,
right: 1200,
top: 100,
width: 400,
}),
} as HTMLElement;
expect(getElementVisibleRect(element)).toEqual({
bottom: 400,
height: 300,
left: 800,
right: 1000,
top: 100,
width: 200,
});
});
it('should return all zeros when element is completely off-screen', () => {
const element = {
getBoundingClientRect: () => ({
bottom: 1200,
height: 300,
left: 1100,
right: 1400,
top: 900,
width: 300,
}),
} as HTMLElement;
expect(getElementVisibleRect(element)).toEqual({
bottom: 800,
height: 0,
left: 1100,
right: 1000,
top: 900,
width: 0,
});
});
});

View File

@@ -1,183 +0,0 @@
import { describe, expect, it } from 'vitest';
import {
getFirstNonNullOrUndefined,
isBoolean,
isEmpty,
isHttpUrl,
isObject,
isUndefined,
isWindow,
} from '../inference';
describe('isHttpUrl', () => {
it("should return true when given 'http://example.com'", () => {
expect(isHttpUrl('http://example.com')).toBe(true);
});
it("should return true when given 'https://example.com'", () => {
expect(isHttpUrl('https://example.com')).toBe(true);
});
it("should return false when given 'ftp://example.com'", () => {
expect(isHttpUrl('ftp://example.com')).toBe(false);
});
it("should return false when given 'example.com'", () => {
expect(isHttpUrl('example.com')).toBe(false);
});
});
describe('isUndefined', () => {
it('isUndefined should return true for undefined values', () => {
expect(isUndefined()).toBe(true);
});
it('isUndefined should return false for null values', () => {
expect(isUndefined(null)).toBe(false);
});
it('isUndefined should return false for defined values', () => {
expect(isUndefined(0)).toBe(false);
expect(isUndefined('')).toBe(false);
expect(isUndefined(false)).toBe(false);
});
it('isUndefined should return false for objects and arrays', () => {
expect(isUndefined({})).toBe(false);
expect(isUndefined([])).toBe(false);
});
});
describe('isEmpty', () => {
it('should return true for empty string', () => {
expect(isEmpty('')).toBe(true);
});
it('should return true for empty array', () => {
expect(isEmpty([])).toBe(true);
});
it('should return true for empty object', () => {
expect(isEmpty({})).toBe(true);
});
it('should return false for non-empty string', () => {
expect(isEmpty('hello')).toBe(false);
});
it('should return false for non-empty array', () => {
expect(isEmpty([1, 2, 3])).toBe(false);
});
it('should return false for non-empty object', () => {
expect(isEmpty({ a: 1 })).toBe(false);
});
it('should return true for null or undefined', () => {
expect(isEmpty(null)).toBe(true);
expect(isEmpty()).toBe(true);
});
it('should return false for number or boolean', () => {
expect(isEmpty(0)).toBe(false);
expect(isEmpty(true)).toBe(false);
});
});
describe('isWindow', () => {
it('should return true for the window object', () => {
expect(isWindow(window)).toBe(true);
});
it('should return false for other objects', () => {
expect(isWindow({})).toBe(false);
expect(isWindow([])).toBe(false);
expect(isWindow(null)).toBe(false);
});
});
describe('isBoolean', () => {
it('should return true for boolean values', () => {
expect(isBoolean(true)).toBe(true);
expect(isBoolean(false)).toBe(true);
});
it('should return false for non-boolean values', () => {
expect(isBoolean(null)).toBe(false);
expect(isBoolean(42)).toBe(false);
expect(isBoolean('string')).toBe(false);
expect(isBoolean({})).toBe(false);
expect(isBoolean([])).toBe(false);
});
});
describe('isObject', () => {
it('should return true for objects', () => {
expect(isObject({})).toBe(true);
expect(isObject({ a: 1 })).toBe(true);
});
it('should return false for non-objects', () => {
expect(isObject(null)).toBe(false);
expect(isObject(42)).toBe(false);
expect(isObject('string')).toBe(false);
expect(isObject(true)).toBe(false);
expect(isObject([1, 2, 3])).toBe(true);
expect(isObject(new Date())).toBe(true);
expect(isObject(/regex/)).toBe(true);
});
});
describe('getFirstNonNullOrUndefined', () => {
describe('getFirstNonNullOrUndefined', () => {
it('should return the first non-null and non-undefined value for a number array', () => {
expect(getFirstNonNullOrUndefined<number>(undefined, null, 0, 42)).toBe(
0,
);
expect(getFirstNonNullOrUndefined<number>(null, undefined, 42, 123)).toBe(
42,
);
});
it('should return the first non-null and non-undefined value for a string array', () => {
expect(
getFirstNonNullOrUndefined<string>(undefined, null, '', 'hello'),
).toBe('');
expect(
getFirstNonNullOrUndefined<string>(null, undefined, 'test', 'world'),
).toBe('test');
});
it('should return undefined if all values are null or undefined', () => {
expect(getFirstNonNullOrUndefined(undefined, null)).toBeUndefined();
expect(getFirstNonNullOrUndefined(null)).toBeUndefined();
});
it('should work with a single value', () => {
expect(getFirstNonNullOrUndefined(42)).toBe(42);
expect(getFirstNonNullOrUndefined()).toBeUndefined();
expect(getFirstNonNullOrUndefined(null)).toBeUndefined();
});
it('should handle mixed types correctly', () => {
expect(
getFirstNonNullOrUndefined<number | object | string>(
undefined,
null,
'test',
123,
{ key: 'value' },
),
).toBe('test');
expect(
getFirstNonNullOrUndefined<number | object | string>(
null,
undefined,
[1, 2, 3],
'string',
),
).toEqual([1, 2, 3]);
});
});
});

View File

@@ -1,116 +0,0 @@
import { describe, expect, it } from 'vitest';
import {
capitalizeFirstLetter,
kebabToCamelCase,
toCamelCase,
toLowerCaseFirstLetter,
} from '../letter';
describe('capitalizeFirstLetter', () => {
it('should capitalize the first letter of a string', () => {
expect(capitalizeFirstLetter('hello')).toBe('Hello');
expect(capitalizeFirstLetter('world')).toBe('World');
});
it('should handle empty strings', () => {
expect(capitalizeFirstLetter('')).toBe('');
});
it('should handle single character strings', () => {
expect(capitalizeFirstLetter('a')).toBe('A');
expect(capitalizeFirstLetter('b')).toBe('B');
});
it('should not change the case of other characters', () => {
expect(capitalizeFirstLetter('hElLo')).toBe('HElLo');
});
});
describe('toLowerCaseFirstLetter', () => {
it('should convert the first letter to lowercase', () => {
expect(toLowerCaseFirstLetter('CommonAppName')).toBe('commonAppName');
expect(toLowerCaseFirstLetter('AnotherKeyExample')).toBe(
'anotherKeyExample',
);
});
it('should return the same string if the first letter is already lowercase', () => {
expect(toLowerCaseFirstLetter('alreadyLowerCase')).toBe('alreadyLowerCase');
});
it('should handle empty strings', () => {
expect(toLowerCaseFirstLetter('')).toBe('');
});
it('should handle single character strings', () => {
expect(toLowerCaseFirstLetter('A')).toBe('a');
expect(toLowerCaseFirstLetter('a')).toBe('a');
});
it('should handle strings with only one uppercase letter', () => {
expect(toLowerCaseFirstLetter('A')).toBe('a');
});
it('should handle strings with special characters', () => {
expect(toLowerCaseFirstLetter('!Special')).toBe('!Special');
expect(toLowerCaseFirstLetter('123Number')).toBe('123Number');
});
});
describe('toCamelCase', () => {
it('should return the key if parentKey is empty', () => {
expect(toCamelCase('child', '')).toBe('child');
});
it('should combine parentKey and key in camel case', () => {
expect(toCamelCase('child', 'parent')).toBe('parentChild');
});
it('should handle empty key and parentKey', () => {
expect(toCamelCase('', '')).toBe('');
});
it('should handle key with capital letters', () => {
expect(toCamelCase('Child', 'parent')).toBe('parentChild');
expect(toCamelCase('Child', 'Parent')).toBe('ParentChild');
});
});
describe('kebabToCamelCase', () => {
it('should convert kebab-case to camelCase correctly', () => {
expect(kebabToCamelCase('my-component-name')).toBe('myComponentName');
});
it('should handle multiple consecutive hyphens', () => {
expect(kebabToCamelCase('my--component--name')).toBe('myComponentName');
});
it('should trim leading and trailing hyphens', () => {
expect(kebabToCamelCase('-my-component-name-')).toBe('myComponentName');
});
it('should preserve the case of the first word', () => {
expect(kebabToCamelCase('My-component-name')).toBe('MyComponentName');
});
it('should convert a single word correctly', () => {
expect(kebabToCamelCase('component')).toBe('component');
});
it('should return an empty string if input is empty', () => {
expect(kebabToCamelCase('')).toBe('');
});
it('should handle strings with no hyphens', () => {
expect(kebabToCamelCase('mycomponentname')).toBe('mycomponentname');
});
it('should handle strings with only hyphens', () => {
expect(kebabToCamelCase('---')).toBe('');
});
it('should handle mixed case inputs', () => {
expect(kebabToCamelCase('my-Component-Name')).toBe('myComponentName');
});
});

View File

@@ -1,60 +0,0 @@
import { describe, expect, it } from 'vitest';
import { StateHandler } from '../state-handler';
describe('stateHandler', () => {
it('should resolve when condition is set to true', async () => {
const handler = new StateHandler();
// 模拟异步设置 condition 为 true
setTimeout(() => {
handler.setConditionTrue(); // 明确触发 condition 为 true
}, 10);
// 等待条件被设置为 true
await handler.waitForCondition();
expect(handler.isConditionTrue()).toBe(true);
});
it('should resolve immediately if condition is already true', async () => {
const handler = new StateHandler();
handler.setConditionTrue(); // 提前设置为 true
// 立即 resolve因为 condition 已经是 true
await handler.waitForCondition();
expect(handler.isConditionTrue()).toBe(true);
});
it('should reject when condition is set to false after waiting', async () => {
const handler = new StateHandler();
// 模拟异步设置 condition 为 false
setTimeout(() => {
handler.setConditionFalse(); // 明确触发 condition 为 false
}, 10);
// 等待过程中,期望 Promise 被 reject
await expect(handler.waitForCondition()).rejects.toThrow();
expect(handler.isConditionTrue()).toBe(false);
});
it('should reset condition to false', () => {
const handler = new StateHandler();
handler.setConditionTrue(); // 设置为 true
handler.reset(); // 重置为 false
expect(handler.isConditionTrue()).toBe(false);
});
it('should resolve when condition is set to true after reset', async () => {
const handler = new StateHandler();
handler.reset(); // 确保初始为 false
setTimeout(() => {
handler.setConditionTrue(); // 重置后设置为 true
}, 10);
await handler.waitForCondition();
expect(handler.isConditionTrue()).toBe(true);
});
});

View File

@@ -1,196 +0,0 @@
import { describe, expect, it } from 'vitest';
import { filterTree, mapTree, traverseTreeValues } from '../tree';
describe('traverseTreeValues', () => {
interface Node {
children?: Node[];
name: string;
}
type NodeValue = string;
const sampleTree: Node[] = [
{
name: 'A',
children: [
{ name: 'B' },
{
name: 'C',
children: [{ name: 'D' }, { name: 'E' }],
},
],
},
{
name: 'F',
children: [
{ name: 'G' },
{
name: 'H',
children: [{ name: 'I' }],
},
],
},
];
it('traverses tree and returns all node values', () => {
const values = traverseTreeValues<Node, NodeValue>(
sampleTree,
(node) => node.name,
{
childProps: 'children',
},
);
expect(values).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']);
});
it('handles empty tree', () => {
const values = traverseTreeValues<Node, NodeValue>([], (node) => node.name);
expect(values).toEqual([]);
});
it('handles tree with only root node', () => {
const rootNode = { name: 'A' };
const values = traverseTreeValues<Node, NodeValue>(
[rootNode],
(node) => node.name,
);
expect(values).toEqual(['A']);
});
it('handles tree with only leaf nodes', () => {
const leafNodes = [{ name: 'A' }, { name: 'B' }, { name: 'C' }];
const values = traverseTreeValues<Node, NodeValue>(
leafNodes,
(node) => node.name,
);
expect(values).toEqual(['A', 'B', 'C']);
});
});
describe('filterTree', () => {
const tree = [
{
id: 1,
children: [
{ id: 2 },
{ id: 3, children: [{ id: 4 }, { id: 5 }, { id: 6 }] },
{ id: 7 },
],
},
{ id: 8, children: [{ id: 9 }, { id: 10 }] },
{ id: 11 },
];
it('should return all nodes when condition is always true', () => {
const result = filterTree(tree, () => true, { childProps: 'children' });
expect(result).toEqual(tree);
});
it('should return only root nodes when condition is always false', () => {
const result = filterTree(tree, () => false);
expect(result).toEqual([]);
});
it('should return nodes with even id values', () => {
const result = filterTree(tree, (node) => node.id % 2 === 0);
expect(result).toEqual([{ id: 8, children: [{ id: 10 }] }]);
});
it('should return nodes with odd id values and their ancestors', () => {
const result = filterTree(tree, (node) => node.id % 2 === 1);
expect(result).toEqual([
{
id: 1,
children: [{ id: 3, children: [{ id: 5 }] }, { id: 7 }],
},
{ id: 11 },
]);
});
it('should return nodes with "leaf" in their name', () => {
const tree = [
{
name: 'root',
children: [
{ name: 'leaf 1' },
{
name: 'branch',
children: [{ name: 'leaf 2' }, { name: 'leaf 3' }],
},
{ name: 'leaf 4' },
],
},
];
const result = filterTree(
tree,
(node) => node.name.includes('leaf') || node.name === 'root',
);
expect(result).toEqual([
{
name: 'root',
children: [{ name: 'leaf 1' }, { name: 'leaf 4' }],
},
]);
});
});
describe('mapTree', () => {
it('map infinite depth tree using mapTree', () => {
const tree = [
{
id: 1,
name: 'node1',
children: [
{ id: 2, name: 'node2' },
{ id: 3, name: 'node3' },
{
id: 4,
name: 'node4',
children: [
{
id: 5,
name: 'node5',
children: [
{ id: 6, name: 'node6' },
{ id: 7, name: 'node7' },
],
},
{ id: 8, name: 'node8' },
],
},
],
},
];
const newTree = mapTree(tree, (node) => ({
...node,
name: `${node.name}-new`,
}));
expect(newTree).toEqual([
{
id: 1,
name: 'node1-new',
children: [
{ id: 2, name: 'node2-new' },
{ id: 3, name: 'node3-new' },
{
id: 4,
name: 'node4-new',
children: [
{
id: 5,
name: 'node5-new',
children: [
{ id: 6, name: 'node6-new' },
{ id: 7, name: 'node7-new' },
],
},
{ id: 8, name: 'node8-new' },
],
},
],
},
]);
});
});

View File

@@ -1,60 +0,0 @@
import { describe, expect, it } from 'vitest';
import { uniqueByField } from '../unique';
describe('uniqueByField', () => {
it('should return an array with unique items based on id field', () => {
const items = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
{ id: 1, name: 'Duplicate Item' },
];
const uniqueItems = uniqueByField(items, 'id');
expect(uniqueItems).toHaveLength(3);
expect(uniqueItems).toEqual([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
]);
});
it('should return an empty array when input array is empty', () => {
const items: any[] = []; // Empty array
const uniqueItems = uniqueByField(items, 'id');
// Assert expected results
expect(uniqueItems).toEqual([]);
});
it('should handle arrays with only one item correctly', () => {
const items = [{ id: 1, name: 'Item 1' }];
const uniqueItems = uniqueByField(items, 'id');
// Assert expected results
expect(uniqueItems).toHaveLength(1);
expect(uniqueItems).toEqual([{ id: 1, name: 'Item 1' }]);
});
it('should preserve the order of the first occurrence of each item', () => {
const items = [
{ id: 2, name: 'Item 2' },
{ id: 1, name: 'Item 1' },
{ id: 3, name: 'Item 3' },
{ id: 1, name: 'Duplicate Item' },
];
const uniqueItems = uniqueByField(items, 'id');
// Assert expected results (order of first occurrences preserved)
expect(uniqueItems).toEqual([
{ id: 2, name: 'Item 2' },
{ id: 1, name: 'Item 1' },
{ id: 3, name: 'Item 3' },
]);
});
});

View File

@@ -1,30 +0,0 @@
import { expect, it } from 'vitest';
import { updateCSSVariables } from '../update-css-variables';
it('updateCSSVariables should update CSS variables in :root selector', () => {
// 模拟初始的内联样式表内容
const initialStyleContent = ':root { --primaryColor: red; }';
document.head.innerHTML = `<style id="custom-styles">${initialStyleContent}</style>`;
// 要更新的CSS变量和它们的新值
const updatedVariables = {
fontSize: '16px',
primaryColor: 'blue',
secondaryColor: 'green',
};
// 调用函数来更新CSS变量
updateCSSVariables(updatedVariables, 'custom-styles');
// 获取更新后的样式内容
const styleElement = document.querySelector('#custom-styles');
const updatedStyleContent = styleElement ? styleElement.textContent : '';
// 检查更新后的样式内容是否包含正确的更新值
expect(
updatedStyleContent?.includes('primaryColor: blue;') &&
updatedStyleContent?.includes('secondaryColor: green;') &&
updatedStyleContent?.includes('fontSize: 16px;'),
).toBe(true);
});

View File

@@ -1,156 +0,0 @@
import { describe, expect, it } from 'vitest';
import { bindMethods, getNestedValue } from '../util';
class TestClass {
public value: string;
constructor(value: string) {
this.value = value;
bindMethods(this); // 调用通用方法
}
getValue() {
return this.value;
}
setValue(newValue: string) {
this.value = newValue;
}
}
describe('bindMethods', () => {
it('should bind methods to the instance correctly', () => {
const instance = new TestClass('initial');
// 解构方法
const { getValue } = instance;
// 检查 getValue 是否能正确调用,并且 this 绑定了 instance
expect(getValue()).toBe('initial');
});
it('should bind multiple methods', () => {
const instance = new TestClass('initial');
const { getValue, setValue } = instance;
// 检查 getValue 和 setValue 方法是否正确绑定了 this
setValue('newValue');
expect(getValue()).toBe('newValue');
});
it('should not bind non-function properties', () => {
const instance = new TestClass('initial');
// 检查普通属性是否保持原样
expect(instance.value).toBe('initial');
});
it('should not bind constructor method', () => {
const instance = new TestClass('test');
// 检查 constructor 是否没有被绑定
expect(instance.constructor.name).toBe('TestClass');
});
it('should not bind getter/setter properties', () => {
class TestWithGetterSetter {
get value() {
return this._value;
}
set value(newValue: string) {
this._value = newValue;
}
private _value: string = 'test';
constructor() {
bindMethods(this);
}
}
const instance = new TestWithGetterSetter();
const { value } = instance;
// Getter 和 setter 不应被绑定
expect(value).toBe('test');
});
});
describe('getNestedValue', () => {
interface UserProfile {
age: number;
name: string;
}
interface UserSettings {
theme: string;
}
interface Data {
user: {
profile: UserProfile;
settings: UserSettings;
};
}
const data: Data = {
user: {
profile: {
age: 25,
name: 'Alice',
},
settings: {
theme: 'dark',
},
},
};
it('should get a nested value when the path is valid', () => {
const result = getNestedValue(data, 'user.profile.name');
expect(result).toBe('Alice');
});
it('should return undefined for non-existent property', () => {
const result = getNestedValue(data, 'user.profile.gender');
expect(result).toBeUndefined();
});
it('should return undefined when accessing a non-existent deep path', () => {
const result = getNestedValue(data, 'user.nonexistent.field');
expect(result).toBeUndefined();
});
it('should return undefined if a middle level is undefined', () => {
const result = getNestedValue({ user: undefined }, 'user.profile.name');
expect(result).toBeUndefined();
});
it('should return the correct value for a nested setting', () => {
const result = getNestedValue(data, 'user.settings.theme');
expect(result).toBe('dark');
});
it('should work for a single-level path', () => {
const result = getNestedValue({ a: 1, b: 2 }, 'b');
expect(result).toBe(2);
});
it('should return the entire object if path is empty', () => {
expect(() => getNestedValue(data, '')()).toThrow();
});
it('should handle paths with array indexes', () => {
const complexData = { list: [{ name: 'Item1' }, { name: 'Item2' }] };
const result = getNestedValue(complexData, 'list.1.name');
expect(result).toBe('Item2');
});
it('should return undefined when accessing an out-of-bounds array index', () => {
const complexData = { list: [{ name: 'Item1' }] };
const result = getNestedValue(complexData, 'list.2.name');
expect(result).toBeUndefined();
});
});

View File

@@ -1,33 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { openWindow } from '../window';
describe('openWindow', () => {
// 保存原始的 window.open 函数
let originalOpen: typeof window.open;
beforeEach(() => {
originalOpen = window.open;
});
afterEach(() => {
window.open = originalOpen;
});
it('should call window.open with correct arguments', () => {
const url = 'https://example.com';
const options = { noopener: true, noreferrer: true, target: '_blank' };
window.open = vi.fn();
// 调用函数
openWindow(url, options);
// 验证 window.open 是否被正确地调用
expect(window.open).toHaveBeenCalledWith(
url,
options.target,
'noopener=yes,noreferrer=yes',
);
});
});

View File

@@ -1,48 +0,0 @@
import type { SortableOptions } from 'sortablejs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useSortable } from '../use-sortable';
describe('useSortable', () => {
beforeEach(() => {
vi.mock('sortablejs/modular/sortable.complete.esm.js', () => ({
default: {
create: vi.fn(),
},
}));
});
it('should call Sortable.create with the correct options', async () => {
// Create a mock element
const mockElement = document.createElement('div') as HTMLDivElement;
// Define custom options
const customOptions: SortableOptions = {
group: 'test-group',
sort: false,
};
// Use the useSortable function
const { initializeSortable } = useSortable(mockElement, customOptions);
// Initialize sortable
await initializeSortable();
// Import sortablejs to access the mocked create function
const Sortable = await import(
'sortablejs/modular/sortable.complete.esm.js'
);
// Verify that Sortable.create was called with the correct parameters
expect(Sortable.default.create).toHaveBeenCalledTimes(1);
expect(Sortable.default.create).toHaveBeenCalledWith(
mockElement,
expect.objectContaining({
animation: 300,
delay: 400,
delayOnTouchOnly: true,
...customOptions,
}),
);
});
});

View File

@@ -1,122 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`defaultPreferences immutability test > should not modify the config object 1`] = `
{
"app": {
"accessMode": "frontend",
"authPageLayout": "panel-right",
"checkUpdatesInterval": 1,
"colorGrayMode": false,
"colorWeakMode": false,
"compact": false,
"contentCompact": "wide",
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
"dynamicTitle": true,
"enableCheckUpdates": true,
"enablePreferences": true,
"enableRefreshToken": false,
"isMobile": false,
"layout": "sidebar-nav",
"locale": "zh-CN",
"loginExpiredMode": "page",
"name": "Vben Admin",
"preferencesButtonPosition": "auto",
"watermark": false,
},
"breadcrumb": {
"enable": true,
"hideOnlyOne": false,
"showHome": false,
"showIcon": true,
"styleType": "normal",
},
"copyright": {
"companyName": "Vben",
"companySiteLink": "https://www.vben.pro",
"date": "2024",
"enable": true,
"icp": "",
"icpLink": "",
"settingShow": true,
},
"footer": {
"enable": false,
"fixed": false,
},
"header": {
"enable": true,
"hidden": false,
"menuAlign": "start",
"mode": "fixed",
},
"logo": {
"enable": true,
"source": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp",
},
"navigation": {
"accordion": true,
"split": true,
"styleType": "rounded",
},
"shortcutKeys": {
"enable": true,
"globalLockScreen": true,
"globalLogout": true,
"globalPreferences": true,
"globalSearch": true,
},
"sidebar": {
"autoActivateChild": false,
"collapsed": false,
"collapsedButton": true,
"collapsedShowTitle": false,
"enable": true,
"expandOnHover": true,
"extraCollapse": false,
"fixedButton": true,
"hidden": false,
"width": 224,
},
"tabbar": {
"draggable": true,
"enable": true,
"height": 38,
"keepAlive": true,
"maxCount": 0,
"middleClickToClose": false,
"persist": true,
"showIcon": true,
"showMaximize": true,
"showMore": true,
"styleType": "chrome",
"wheelable": true,
},
"theme": {
"builtinType": "default",
"colorDestructive": "hsl(348 100% 61%)",
"colorPrimary": "hsl(212 100% 45%)",
"colorSuccess": "hsl(144 57% 58%)",
"colorWarning": "hsl(42 84% 61%)",
"mode": "dark",
"radius": "0.5",
"semiDarkHeader": false,
"semiDarkSidebar": false,
},
"transition": {
"enable": true,
"loading": true,
"name": "fade-slide",
"progress": true,
},
"widget": {
"fullscreen": true,
"globalSearch": true,
"languageToggle": true,
"lockScreen": true,
"notification": true,
"refresh": true,
"sidebarToggle": true,
"themeToggle": true,
},
}
`;

View File

@@ -1,10 +0,0 @@
import { describe, expect, it } from 'vitest';
import { defaultPreferences } from '../src/config';
describe('defaultPreferences immutability test', () => {
// 创建快照,确保默认配置对象不被修改
it('should not modify the config object', () => {
expect(defaultPreferences).toMatchSnapshot();
});
});

View File

@@ -1,253 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { defaultPreferences } from '../src/config';
import { PreferenceManager } from '../src/preferences';
import { isDarkTheme } from '../src/update-css-variables';
describe('preferences', () => {
let preferenceManager: PreferenceManager;
// 模拟 window.matchMedia 方法
vi.stubGlobal(
'matchMedia',
vi.fn().mockImplementation((query) => ({
addEventListener: vi.fn(),
addListener: vi.fn(), // Deprecated
dispatchEvent: vi.fn(),
matches: query === '(prefers-color-scheme: dark)',
media: query,
onchange: null,
removeEventListener: vi.fn(),
removeListener: vi.fn(), // Deprecated
})),
);
beforeEach(() => {
preferenceManager = new PreferenceManager();
});
it('loads default preferences if no saved preferences found', () => {
const preferences = preferenceManager.getPreferences();
expect(preferences).toEqual(defaultPreferences);
});
it('initializes preferences with overrides', async () => {
const overrides: any = {
app: {
locale: 'en-US',
},
};
await preferenceManager.initPreferences({
namespace: 'testNamespace',
overrides,
});
// 等待防抖动操作完成
// await new Promise((resolve) => setTimeout(resolve, 300)); // 等待100毫秒
const expected = {
...defaultPreferences,
app: {
...defaultPreferences.app,
...overrides.app,
},
};
expect(preferenceManager.getPreferences()).toEqual(expected);
});
it('updates theme mode correctly', () => {
preferenceManager.updatePreferences({
theme: {
mode: 'light',
},
});
expect(preferenceManager.getPreferences().theme.mode).toBe('light');
});
it('updates color modes correctly', () => {
preferenceManager.updatePreferences({
app: { colorGrayMode: true, colorWeakMode: true },
});
expect(preferenceManager.getPreferences().app.colorGrayMode).toBe(true);
expect(preferenceManager.getPreferences().app.colorWeakMode).toBe(true);
});
it('resets preferences to default', () => {
// 先更新一些偏好设置
preferenceManager.updatePreferences({
theme: {
mode: 'light',
},
});
// 然后重置偏好设置
preferenceManager.resetPreferences();
expect(preferenceManager.getPreferences()).toEqual(defaultPreferences);
});
it('updates isMobile correctly', () => {
// 模拟移动端状态
vi.stubGlobal(
'matchMedia',
vi.fn().mockImplementation((query) => ({
addEventListener: vi.fn(),
addListener: vi.fn(),
dispatchEvent: vi.fn(),
matches: query === '(max-width: 768px)',
media: query,
onchange: null,
removeEventListener: vi.fn(),
removeListener: vi.fn(),
})),
);
preferenceManager.updatePreferences({
app: { isMobile: true },
});
expect(preferenceManager.getPreferences().app.isMobile).toBe(true);
});
it('updates the locale preference correctly', () => {
preferenceManager.updatePreferences({
app: { locale: 'en-US' },
});
expect(preferenceManager.getPreferences().app.locale).toBe('en-US');
});
it('updates the sidebar width correctly', () => {
preferenceManager.updatePreferences({
sidebar: { width: 200 },
});
expect(preferenceManager.getPreferences().sidebar.width).toBe(200);
});
it('updates the sidebar collapse state correctly', () => {
preferenceManager.updatePreferences({
sidebar: { collapsed: true },
});
expect(preferenceManager.getPreferences().sidebar.collapsed).toBe(true);
});
it('updates the navigation style type correctly', () => {
preferenceManager.updatePreferences({
navigation: { styleType: 'flat' },
} as any);
expect(preferenceManager.getPreferences().navigation.styleType).toBe(
'flat',
);
});
it('resets preferences to default correctly', () => {
// 先更新一些偏好设置
preferenceManager.updatePreferences({
app: { locale: 'en-US' },
sidebar: { collapsed: true, width: 200 },
theme: {
mode: 'light',
},
});
// 然后重置偏好设置
preferenceManager.resetPreferences();
expect(preferenceManager.getPreferences()).toEqual(defaultPreferences);
});
it('does not update undefined preferences', () => {
const originalPreferences = preferenceManager.getPreferences();
preferenceManager.updatePreferences({
app: { nonexistentField: 'value' },
} as any);
expect(preferenceManager.getPreferences()).toEqual(originalPreferences);
});
it('reverts to default when a preference field is deleted', () => {
preferenceManager.updatePreferences({
app: { locale: 'en-US' },
});
preferenceManager.updatePreferences({
app: { locale: undefined },
});
expect(preferenceManager.getPreferences().app.locale).toBe('en-US');
});
it('ignores updates with invalid preference value types', () => {
const originalPreferences = preferenceManager.getPreferences();
preferenceManager.updatePreferences({
app: { isMobile: 'true' as unknown as boolean }, // 错误类型
});
expect(preferenceManager.getPreferences()).toEqual(originalPreferences);
});
it('merges nested preference objects correctly', () => {
preferenceManager.updatePreferences({
app: { name: 'New App Name' },
});
const expected = {
...defaultPreferences,
app: {
...defaultPreferences.app,
name: 'New App Name',
},
};
expect(preferenceManager.getPreferences()).toEqual(expected);
});
it('applies updates immediately after initialization', async () => {
const overrides: any = {
app: {
locale: 'en-US',
},
};
await preferenceManager.initPreferences(overrides);
preferenceManager.updatePreferences({
theme: { mode: 'light' },
});
expect(preferenceManager.getPreferences().theme.mode).toBe('light');
});
});
describe('isDarkTheme', () => {
it('should return true for dark theme', () => {
expect(isDarkTheme('dark')).toBe(true);
});
it('should return false for light theme', () => {
expect(isDarkTheme('light')).toBe(false);
});
it('should return system preference for auto theme', () => {
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
addEventListener: vi.fn(),
addListener: vi.fn(), // Deprecated
dispatchEvent: vi.fn(),
matches: query === '(prefers-color-scheme: dark)',
media: query,
onchange: null,
removeEventListener: vi.fn(),
removeListener: vi.fn(), // Deprecated
}));
expect(isDarkTheme('auto')).toBe(true);
expect(window.matchMedia).toHaveBeenCalledWith(
'(prefers-color-scheme: dark)',
);
});
});

View File

@@ -1,189 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FormApi } from '../src/form-api';
describe('formApi', () => {
let formApi: FormApi;
beforeEach(() => {
formApi = new FormApi();
});
it('should initialize with default state', () => {
expect(formApi.state).toEqual(
expect.objectContaining({
actionWrapperClass: '',
collapsed: false,
collapsedRows: 1,
commonConfig: {},
handleReset: undefined,
handleSubmit: undefined,
layout: 'horizontal',
resetButtonOptions: {},
schema: [],
showCollapseButton: false,
showDefaultActions: true,
submitButtonOptions: {},
wrapperClass: 'grid-cols-1',
}),
);
expect(formApi.isMounted).toBe(false);
});
it('should mount form actions', async () => {
const formActions: any = {
meta: {},
resetForm: vi.fn(),
setFieldValue: vi.fn(),
setValues: vi.fn(),
submitForm: vi.fn(),
validate: vi.fn(),
values: { name: 'test' },
};
await formApi.mount(formActions);
expect(formApi.isMounted).toBe(true);
expect(formApi.form).toEqual(formActions);
});
it('should get values from form', async () => {
const formActions: any = {
meta: {},
values: { name: 'test' },
};
await formApi.mount(formActions);
const values = await formApi.getValues();
expect(values).toEqual({ name: 'test' });
});
it('should set field value', async () => {
const setFieldValueMock = vi.fn();
const formActions: any = {
meta: {},
setFieldValue: setFieldValueMock,
values: { name: 'test' },
};
await formApi.mount(formActions);
await formApi.setFieldValue('name', 'new value');
expect(setFieldValueMock).toHaveBeenCalledWith(
'name',
'new value',
undefined,
);
});
it('should reset form', async () => {
const resetFormMock = vi.fn();
const formActions: any = {
meta: {},
resetForm: resetFormMock,
values: { name: 'test' },
};
await formApi.mount(formActions);
await formApi.resetForm();
expect(resetFormMock).toHaveBeenCalled();
});
it('should call handleSubmit on submit', async () => {
const handleSubmitMock = vi.fn();
const formActions: any = {
meta: {},
submitForm: vi.fn().mockResolvedValue(true),
values: { name: 'test' },
};
const state = {
handleSubmit: handleSubmitMock,
};
formApi.setState(state);
await formApi.mount(formActions);
const result = await formApi.submitForm();
expect(formActions.submitForm).toHaveBeenCalled();
expect(handleSubmitMock).toHaveBeenCalledWith({ name: 'test' });
expect(result).toEqual({ name: 'test' });
});
it('should unmount form and reset state', () => {
formApi.unmount();
expect(formApi.isMounted).toBe(false);
});
it('should validate form', async () => {
const validateMock = vi.fn().mockResolvedValue(true);
const formActions: any = {
meta: {},
validate: validateMock,
};
await formApi.mount(formActions);
const isValid = await formApi.validate();
expect(validateMock).toHaveBeenCalled();
expect(isValid).toBe(true);
});
});
describe('updateSchema', () => {
let instance: FormApi;
beforeEach(() => {
instance = new FormApi();
instance.state = {
schema: [
{ component: 'text', fieldName: 'name' },
{ component: 'number', fieldName: 'age', label: 'Age' },
],
};
});
it('should update the schema correctly when fieldName matches', () => {
const newSchema = [
{ component: 'text', fieldName: 'name' },
{ component: 'number', fieldName: 'age', label: 'Age' },
];
instance.updateSchema(newSchema);
expect(instance.state?.schema?.[0]?.component).toBe('text');
expect(instance.state?.schema?.[1]?.label).toBe('Age');
});
it('should log an error if fieldName is missing in some items', () => {
const newSchema: any[] = [
{ component: 'textarea', fieldName: 'name' },
{ component: 'number' },
];
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
instance.updateSchema(newSchema);
expect(consoleErrorSpy).toHaveBeenCalledWith(
'All items in the schema array must have a valid `fieldName` property to be updated',
);
});
it('should not update schema if fieldName does not match', () => {
const newSchema = [{ component: 'textarea', fieldName: 'unknown' }];
instance.updateSchema(newSchema);
expect(instance.state?.schema?.[0]?.component).toBe('text');
expect(instance.state?.schema?.[1]?.component).toBe('number');
});
it('should not update schema if updatedMap is empty', () => {
const newSchema: any[] = [{ component: 'textarea' }];
instance.updateSchema(newSchema);
expect(instance.state?.schema?.[0]?.component).toBe('text');
expect(instance.state?.schema?.[1]?.component).toBe('number');
});
});

View File

@@ -1,116 +0,0 @@
import type { DrawerState } from '../drawer';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { DrawerApi } from '../drawer-api';
// 模拟 Store 类
vi.mock('@vben-core/shared/store', () => {
return {
isFunction: (fn: any) => typeof fn === 'function',
Store: class {
get state() {
return this._state;
}
private _state: DrawerState;
private options: any;
constructor(initialState: DrawerState, options: any) {
this._state = initialState;
this.options = options;
}
batch(cb: () => void) {
cb();
}
setState(fn: (prev: DrawerState) => DrawerState) {
this._state = fn(this._state);
this.options.onUpdate();
}
},
};
});
describe('drawerApi', () => {
let drawerApi: DrawerApi;
let drawerState: DrawerState;
beforeEach(() => {
drawerApi = new DrawerApi();
drawerState = drawerApi.store.state;
});
it('should initialize with default state', () => {
expect(drawerState.isOpen).toBe(false);
expect(drawerState.cancelText).toBe(undefined);
expect(drawerState.confirmText).toBe(undefined);
});
it('should open the drawer', () => {
drawerApi.open();
expect(drawerApi.store.state.isOpen).toBe(true);
});
it('should close the drawer if onBeforeClose allows it', () => {
drawerApi.close();
expect(drawerApi.store.state.isOpen).toBe(false);
});
it('should not close the drawer if onBeforeClose returns false', () => {
const onBeforeClose = vi.fn(() => false);
const drawerApiWithHook = new DrawerApi({ onBeforeClose });
drawerApiWithHook.open();
drawerApiWithHook.close();
expect(drawerApiWithHook.store.state.isOpen).toBe(true);
expect(onBeforeClose).toHaveBeenCalled();
});
it('should trigger onCancel and keep drawer open if onCancel is provided', () => {
const onCancel = vi.fn();
const drawerApiWithHook = new DrawerApi({ onCancel });
drawerApiWithHook.open();
drawerApiWithHook.onCancel();
expect(onCancel).toHaveBeenCalled();
expect(drawerApiWithHook.store.state.isOpen).toBe(true); // 关闭逻辑不在 onCancel 内
});
it('should update shared data correctly', () => {
const testData = { key: 'value' };
drawerApi.setData(testData);
expect(drawerApi.getData()).toEqual(testData);
});
it('should set state correctly using an object', () => {
drawerApi.setState({ title: 'New Title' });
expect(drawerApi.store.state.title).toBe('New Title');
});
it('should set state correctly using a function', () => {
drawerApi.setState((prev) => ({ ...prev, confirmText: 'Yes' }));
expect(drawerApi.store.state.confirmText).toBe('Yes');
});
it('should call onOpenChange when state changes', () => {
const onOpenChange = vi.fn();
const drawerApiWithHook = new DrawerApi({ onOpenChange });
drawerApiWithHook.open();
expect(onOpenChange).toHaveBeenCalledWith(true);
});
it('should call onClosed callback when provided', () => {
const onClosed = vi.fn();
const drawerApiWithHook = new DrawerApi({ onClosed });
drawerApiWithHook.onClosed();
expect(onClosed).toHaveBeenCalled();
});
it('should call onOpened callback when provided', () => {
const onOpened = vi.fn();
const drawerApiWithHook = new DrawerApi({ onOpened });
drawerApiWithHook.open();
drawerApiWithHook.onOpened();
expect(onOpened).toHaveBeenCalled();
});
});

View File

@@ -1,117 +0,0 @@
import type { ModalState } from '../modal';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ModalApi } from '../modal-api';
vi.mock('@vben-core/shared/store', () => {
return {
isFunction: (fn: any) => typeof fn === 'function',
Store: class {
get state() {
return this._state;
}
private _state: ModalState;
private options: any;
constructor(initialState: ModalState, options: any) {
this._state = initialState;
this.options = options;
}
batch(cb: () => void) {
cb();
}
setState(fn: (prev: ModalState) => ModalState) {
this._state = fn(this._state);
this.options.onUpdate();
}
},
};
});
describe('modalApi', () => {
let modalApi: ModalApi;
// 使用 modalState 而不是 state
let modalState: ModalState;
beforeEach(() => {
modalApi = new ModalApi();
// 获取 modalApi 内的 state
modalState = modalApi.store.state;
});
it('should initialize with default state', () => {
expect(modalState.isOpen).toBe(false);
expect(modalState.cancelText).toBe(undefined);
expect(modalState.confirmText).toBe(undefined);
});
it('should open the modal', () => {
modalApi.open();
expect(modalApi.store.state.isOpen).toBe(true);
});
it('should close the modal if onBeforeClose allows it', () => {
modalApi.close();
expect(modalApi.store.state.isOpen).toBe(false);
});
it('should not close the modal if onBeforeClose returns false', () => {
const onBeforeClose = vi.fn(() => false);
const modalApiWithHook = new ModalApi({ onBeforeClose });
modalApiWithHook.open();
modalApiWithHook.close();
expect(modalApiWithHook.store.state.isOpen).toBe(true);
expect(onBeforeClose).toHaveBeenCalled();
});
it('should trigger onCancel and close the modal if no onCancel hook is provided', () => {
const onCancel = vi.fn();
const modalApiWithHook = new ModalApi({ onCancel });
modalApiWithHook.open();
modalApiWithHook.onCancel();
expect(onCancel).toHaveBeenCalled();
expect(modalApiWithHook.store.state.isOpen).toBe(true);
});
it('should update shared data correctly', () => {
const testData = { key: 'value' };
modalApi.setData(testData);
expect(modalApi.getData()).toEqual(testData);
});
it('should set state correctly using an object', () => {
modalApi.setState({ title: 'New Title' });
expect(modalApi.store.state.title).toBe('New Title');
});
it('should set state correctly using a function', () => {
modalApi.setState((prev) => ({ ...prev, confirmText: 'Yes' }));
expect(modalApi.store.state.confirmText).toBe('Yes');
});
it('should call onOpenChange when state changes', () => {
const onOpenChange = vi.fn();
const modalApiWithHook = new ModalApi({ onOpenChange });
modalApiWithHook.open();
expect(onOpenChange).toHaveBeenCalledWith(true);
});
it('should call onClosed callback when provided', () => {
const onClosed = vi.fn();
const modalApiWithHook = new ModalApi({ onClosed });
modalApiWithHook.onClosed();
expect(onClosed).toHaveBeenCalled();
});
it('should call onOpened callback when provided', () => {
const onOpened = vi.fn();
const modalApiWithHook = new ModalApi({ onOpened });
modalApiWithHook.open();
modalApiWithHook.onOpened();
expect(onOpened).toHaveBeenCalled();
});
});

View File

@@ -1,89 +0,0 @@
import { mount } from '@vue/test-utils';
import { describe, expect, it } from 'vitest';
import { Page } from '..';
describe('page.vue', () => {
it('renders title when passed', () => {
const wrapper = mount(Page, {
props: {
title: 'Test Title',
},
});
expect(wrapper.text()).toContain('Test Title');
});
it('renders description when passed', () => {
const wrapper = mount(Page, {
props: {
description: 'Test Description',
},
});
expect(wrapper.text()).toContain('Test Description');
});
it('renders default slot content', () => {
const wrapper = mount(Page, {
slots: {
default: '<p>Default Slot Content</p>',
},
});
expect(wrapper.html()).toContain('<p>Default Slot Content</p>');
});
it('renders footer slot when showFooter is true', () => {
const wrapper = mount(Page, {
props: {
showFooter: true,
},
slots: {
footer: '<p>Footer Slot Content</p>',
},
});
expect(wrapper.html()).toContain('<p>Footer Slot Content</p>');
});
it('applies the custom contentClass', () => {
const wrapper = mount(Page, {
props: {
contentClass: 'custom-class',
},
});
const contentDiv = wrapper.find('.p-4');
expect(contentDiv.classes()).toContain('custom-class');
});
it('does not render title slot if title prop is provided', () => {
const wrapper = mount(Page, {
props: {
title: 'Test Title',
},
slots: {
title: '<p>Title Slot Content</p>',
},
});
expect(wrapper.text()).toContain('Title Slot Content');
expect(wrapper.html()).not.toContain('Test Title');
});
it('does not render description slot if description prop is provided', () => {
const wrapper = mount(Page, {
props: {
description: 'Test Description',
},
slots: {
description: '<p>Description Slot Content</p>',
},
});
expect(wrapper.text()).toContain('Description Slot Content');
expect(wrapper.html()).not.toContain('Test Description');
});
});

View File

@@ -19,6 +19,8 @@
"deleteConfirm": "确定删除 {0} 吗?",
"deleting": "正在删除 {0} ...",
"deleteSuccess": "{0} 删除成功",
"createSuccess": "{0} 创建成功",
"updateSuccess": "{0} 更新成功",
"operationSuccess": "操作成功",
"operationFailed": "操作失败"
},
@@ -62,7 +64,7 @@
"http": {
"requestTimeout": "请求超时,请稍后再试。",
"networkError": "网络异常,请检查您的网络连接后重试。",
"badRequest": "请求错误。请检查您的输入并重试。",
"badRequest": "请求错误。请稍后再试。",
"unauthorized": "登录认证过期,请重新登录后继续。",
"forbidden": "禁止访问, 您没有权限访问此资源。",
"notFound": "未找到, 请求的资源不存在。",

View File

@@ -1,88 +0,0 @@
import { describe, expect, it } from 'vitest';
import { findMenuByPath, findRootMenuByPath } from '../find-menu-by-path';
// 示例菜单数据
const menus: any[] = [
{ path: '/', children: [] },
{ path: '/about', children: [] },
{
path: '/contact',
children: [
{ path: '/contact/email', children: [] },
{ path: '/contact/phone', children: [] },
],
},
{
path: '/services',
children: [
{ path: '/services/design', children: [] },
{
path: '/services/development',
children: [{ path: '/services/development/web', children: [] }],
},
],
},
];
describe('menu Finder Tests', () => {
it('finds a top-level menu', () => {
const menu = findMenuByPath(menus, '/about');
expect(menu).toBeDefined();
expect(menu?.path).toBe('/about');
});
it('finds a nested menu', () => {
const menu = findMenuByPath(menus, '/services/development/web');
expect(menu).toBeDefined();
expect(menu?.path).toBe('/services/development/web');
});
it('returns null for a non-existent path', () => {
const menu = findMenuByPath(menus, '/non-existent');
expect(menu).toBeNull();
});
it('handles empty menus list', () => {
const menu = findMenuByPath([], '/about');
expect(menu).toBeNull();
});
it('handles menu items without children', () => {
const menu = findMenuByPath(
[{ path: '/only', children: undefined }] as any[],
'/only',
);
expect(menu).toBeDefined();
expect(menu?.path).toBe('/only');
});
it('finds root menu by path', () => {
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
menus,
'/services/development/web',
);
expect(findMenu).toBeDefined();
expect(rootMenu).toBeUndefined();
expect(rootMenuPath).toBeUndefined();
expect(findMenu?.path).toBe('/services/development/web');
});
it('returns null for undefined or empty path', () => {
const menuUndefinedPath = findMenuByPath(menus);
const menuEmptyPath = findMenuByPath(menus, '');
expect(menuUndefinedPath).toBeNull();
expect(menuEmptyPath).toBeNull();
});
it('checks for root menu when path does not exist', () => {
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
menus,
'/non-existent',
);
expect(findMenu).toBeNull();
expect(rootMenu).toBeUndefined();
expect(rootMenuPath).toBeUndefined();
});
});

View File

@@ -1,236 +0,0 @@
import type { Router, RouteRecordRaw } from 'vue-router';
import { createRouter, createWebHistory } from 'vue-router';
import { describe, expect, it, vi } from 'vitest';
import { generateMenus } from '../generate-menus';
// Nested route setup to test child inclusion and hideChildrenInMenu functionality
describe('generateMenus', () => {
// 模拟路由数据
const mockRoutes = [
{
meta: { icon: 'home-icon', title: '首页' },
name: 'home',
path: '/home',
},
{
meta: { hideChildrenInMenu: true, icon: 'about-icon', title: '关于' },
name: 'about',
path: '/about',
children: [
{
path: 'team',
name: 'team',
meta: { icon: 'team-icon', title: '团队' },
},
],
},
] as RouteRecordRaw[];
// 模拟 Vue 路由器实例
const mockRouter = {
getRoutes: vi.fn(() => [
{ name: 'home', path: '/home' },
{ name: 'about', path: '/about' },
{ name: 'team', path: '/about/team' },
]),
};
it('the correct menu list should be generated according to the route', async () => {
const expectedMenus = [
{
badge: undefined,
badgeType: undefined,
badgeVariants: undefined,
icon: 'home-icon',
name: '首页',
order: undefined,
parent: undefined,
parents: undefined,
path: '/home',
show: true,
children: [],
},
{
badge: undefined,
badgeType: undefined,
badgeVariants: undefined,
icon: 'about-icon',
name: '关于',
order: undefined,
parent: undefined,
parents: undefined,
path: '/about',
show: true,
children: [],
},
];
const menus = await generateMenus(mockRoutes, mockRouter as any);
expect(menus).toEqual(expectedMenus);
});
it('includes additional meta properties in menu items', async () => {
const mockRoutesWithMeta = [
{
meta: { icon: 'user-icon', order: 1, title: 'Profile' },
name: 'profile',
path: '/profile',
},
] as RouteRecordRaw[];
const menus = await generateMenus(mockRoutesWithMeta, mockRouter as any);
expect(menus).toEqual([
{
badge: undefined,
badgeType: undefined,
badgeVariants: undefined,
icon: 'user-icon',
name: 'Profile',
order: 1,
parent: undefined,
parents: undefined,
path: '/profile',
show: true,
children: [],
},
]);
});
it('handles dynamic route parameters correctly', async () => {
const mockRoutesWithParams = [
{
meta: { icon: 'details-icon', title: 'User Details' },
name: 'userDetails',
path: '/users/:userId',
},
] as RouteRecordRaw[];
const menus = await generateMenus(mockRoutesWithParams, mockRouter as any);
expect(menus).toEqual([
{
badge: undefined,
badgeType: undefined,
badgeVariants: undefined,
icon: 'details-icon',
name: 'User Details',
order: undefined,
parent: undefined,
parents: undefined,
path: '/users/:userId',
show: true,
children: [],
},
]);
});
it('processes routes with redirects correctly', async () => {
const mockRoutesWithRedirect = [
{
name: 'redirectedRoute',
path: '/old-path',
redirect: '/new-path',
},
{
meta: { icon: 'path-icon', title: 'New Path' },
name: 'newPath',
path: '/new-path',
},
] as RouteRecordRaw[];
const menus = await generateMenus(
mockRoutesWithRedirect,
mockRouter as any,
);
expect(menus).toEqual([
// Assuming your generateMenus function excludes redirect routes from the menu
{
badge: undefined,
badgeType: undefined,
badgeVariants: undefined,
icon: undefined,
name: 'redirectedRoute',
order: undefined,
parent: undefined,
parents: undefined,
path: '/old-path',
show: true,
children: [],
},
{
badge: undefined,
badgeType: undefined,
badgeVariants: undefined,
icon: 'path-icon',
name: 'New Path',
order: undefined,
parent: undefined,
parents: undefined,
path: '/new-path',
show: true,
children: [],
},
]);
});
const routes: any = [
{
meta: { order: 2, title: 'Home' },
name: 'home',
path: '/',
},
{
meta: { order: 1, title: 'About' },
name: 'about',
path: '/about',
},
];
const router: Router = createRouter({
history: createWebHistory(),
routes,
});
it('should generate menu list with correct order', async () => {
const menus = await generateMenus(routes, router);
const expectedMenus = [
{
badge: undefined,
badgeType: undefined,
badgeVariants: undefined,
icon: undefined,
name: 'About',
order: 1,
parent: undefined,
parents: undefined,
path: '/about',
show: true,
children: [],
},
{
badge: undefined,
badgeType: undefined,
badgeVariants: undefined,
icon: undefined,
name: 'Home',
order: 2,
parent: undefined,
parents: undefined,
path: '/',
show: true,
children: [],
},
];
expect(menus).toEqual(expectedMenus);
});
it('should handle empty routes', async () => {
const emptyRoutes: any[] = [];
const menus = await generateMenus(emptyRoutes, router);
expect(menus).toEqual([]);
});
});

View File

@@ -1,105 +0,0 @@
import type { RouteRecordRaw } from 'vue-router';
import { describe, expect, it } from 'vitest';
import {
generateRoutesByFrontend,
hasAuthority,
} from '../generate-routes-frontend';
// Mock 路由数据
const mockRoutes = [
{
meta: {
authority: ['admin', 'user'],
hideInMenu: false,
},
path: '/dashboard',
children: [
{
path: '/dashboard/overview',
meta: { authority: ['admin'], hideInMenu: false },
},
{
path: '/dashboard/stats',
meta: { authority: ['user'], hideInMenu: true },
},
],
},
{
meta: { authority: ['admin'], hideInMenu: false },
path: '/settings',
},
{
meta: { hideInMenu: false },
path: '/profile',
},
] as RouteRecordRaw[];
describe('hasAuthority', () => {
it('should return true if there is no authority defined', () => {
expect(hasAuthority(mockRoutes[2], ['admin'])).toBe(true);
});
it('should return true if the user has the required authority', () => {
expect(hasAuthority(mockRoutes[0], ['admin'])).toBe(true);
});
it('should return false if the user does not have the required authority', () => {
expect(hasAuthority(mockRoutes[1], ['user'])).toBe(false);
});
});
describe('generateRoutesByFrontend', () => {
it('should handle routes without children', async () => {
const generatedRoutes = await generateRoutesByFrontend(mockRoutes, [
'user',
]);
expect(generatedRoutes).toEqual(
expect.arrayContaining([
expect.objectContaining({
path: '/profile', // This route has no children and should be included
}),
]),
);
});
it('should handle empty roles array', async () => {
const generatedRoutes = await generateRoutesByFrontend(mockRoutes, []);
expect(generatedRoutes).toEqual(
expect.arrayContaining([
// Only routes without authority should be included
expect.objectContaining({
path: '/profile',
}),
]),
);
expect(generatedRoutes).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
path: '/dashboard',
}),
expect.objectContaining({
path: '/settings',
}),
]),
);
});
it('should handle missing meta fields', async () => {
const routesWithMissingMeta = [
{ path: '/path1' }, // No meta
{ meta: {}, path: '/path2' }, // Empty meta
{ meta: { authority: ['admin'] }, path: '/path3' }, // Only authority
];
const generatedRoutes = await generateRoutesByFrontend(
routesWithMissingMeta as RouteRecordRaw[],
['admin'],
);
expect(generatedRoutes).toEqual([
{ path: '/path1' },
{ meta: {}, path: '/path2' },
{ meta: { authority: ['admin'] }, path: '/path3' },
]);
});
});

View File

@@ -1,68 +0,0 @@
import type { RouteRecordRaw } from 'vue-router';
import type { RouteModuleType } from '../merge-route-modules';
import { describe, expect, it } from 'vitest';
import { mergeRouteModules } from '../merge-route-modules';
describe('mergeRouteModules', () => {
it('should merge route modules correctly', () => {
const routeModules: Record<string, RouteModuleType> = {
'./dynamic-routes/about.ts': {
default: [
{
component: () => Promise.resolve({ template: '<div>About</div>' }),
name: 'About',
path: '/about',
},
],
},
'./dynamic-routes/home.ts': {
default: [
{
component: () => Promise.resolve({ template: '<div>Home</div>' }),
name: 'Home',
path: '/',
},
],
},
};
const expectedRoutes: RouteRecordRaw[] = [
{
component: expect.any(Function),
name: 'About',
path: '/about',
},
{
component: expect.any(Function),
name: 'Home',
path: '/',
},
];
const mergedRoutes = mergeRouteModules(routeModules);
expect(mergedRoutes).toEqual(expectedRoutes);
});
it('should handle empty modules', () => {
const routeModules: Record<string, RouteModuleType> = {};
const expectedRoutes: RouteRecordRaw[] = [];
const mergedRoutes = mergeRouteModules(routeModules);
expect(mergedRoutes).toEqual(expectedRoutes);
});
it('should handle modules with no default export', () => {
const routeModules: Record<string, RouteModuleType> = {
'./dynamic-routes/empty.ts': {
default: [],
},
};
const expectedRoutes: RouteRecordRaw[] = [];
const mergedRoutes = mergeRouteModules(routeModules);
expect(mergedRoutes).toEqual(expectedRoutes);
});
});

1598
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff