物业代码生成

This commit is contained in:
2025-06-18 11:03:42 +08:00
commit 1262d4c745
1881 changed files with 249599 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
# @vben/hooks
用于多个 `app` 公用的 hook继承了 `@vben/hooks` 的所有能力。业务上有通用 hooks 可以放在这里。
## 用法
### 添加依赖
```bash
# 进入目标应用目录,例如 apps/xxxx-app
# cd apps/xxxx-app
pnpm add @vben/hooks
```
### 使用
```ts
import { useNamespace } from '@vben/hooks';
```

View File

@@ -0,0 +1,33 @@
{
"name": "@vben/hooks",
"version": "5.5.6",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "packages/effects/hooks"
},
"license": "MIT",
"type": "module",
"sideEffects": [
"**/*.css"
],
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"dependencies": {
"@vben-core/composables": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/stores": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:",
"watermark-js-plus": "catalog:"
}
}

View File

@@ -0,0 +1,9 @@
export * from './use-app-config';
export * from './use-content-maximize';
export * from './use-design-tokens';
export * from './use-hover-toggle';
export * from './use-pagination';
export * from './use-refresh';
export * from './use-tabs';
export * from './use-watermark';
export * from '@vben-core/composables';

View File

@@ -0,0 +1,40 @@
import type {
ApplicationConfig,
VbenAdminProAppConfigRaw,
} from '@vben/types/global';
/**
* 由 vite-inject-app-config 注入的全局配置
*/
export function useAppConfig(
env: Record<string, any>,
isProduction: boolean,
): ApplicationConfig {
// 生产环境下,直接使用 window._VBEN_ADMIN_PRO_APP_CONF_ 全局变量
const config = isProduction
? window._VBEN_ADMIN_PRO_APP_CONF_
: (env as VbenAdminProAppConfigRaw);
const {
VITE_GLOB_API_URL,
VITE_GLOB_APP_CLIENT_ID,
VITE_GLOB_ENABLE_ENCRYPT,
VITE_GLOB_RSA_PRIVATE_KEY,
VITE_GLOB_RSA_PUBLIC_KEY,
VITE_GLOB_SSE_ENABLE,
} = config;
return {
// 后端地址
apiURL: VITE_GLOB_API_URL,
// 客户端key
clientId: VITE_GLOB_APP_CLIENT_ID,
enableEncrypt: VITE_GLOB_ENABLE_ENCRYPT === 'true',
// RSA私钥
rsaPrivateKey: VITE_GLOB_RSA_PRIVATE_KEY,
// RSA公钥
rsaPublicKey: VITE_GLOB_RSA_PUBLIC_KEY,
// 是否开启sse
sseEnable: VITE_GLOB_SSE_ENABLE === 'true',
};
}

View File

@@ -0,0 +1,24 @@
import { updatePreferences, usePreferences } from '@vben/preferences';
/**
* 主体区域最大化
*/
export function useContentMaximize() {
const { contentIsMaximize } = usePreferences();
function toggleMaximize() {
const isMaximize = contentIsMaximize.value;
updatePreferences({
header: {
hidden: !isMaximize,
},
sidebar: {
hidden: !isMaximize,
},
});
}
return {
contentIsMaximize,
toggleMaximize,
};
}

View File

@@ -0,0 +1,321 @@
import { reactive, watch } from 'vue';
import { preferences, usePreferences } from '@vben/preferences';
import { convertToRgb, updateCSSVariables } from '@vben/utils';
/**
* 用于适配各个框架的设计系统
*/
export function useAntdDesignTokens() {
const rootStyles = getComputedStyle(document.documentElement);
const tokens = reactive({
borderRadius: '' as any,
colorBgBase: '',
colorBgContainer: '',
colorBgElevated: '',
colorBgLayout: '',
colorBgMask: '',
colorBorder: '',
colorBorderSecondary: '',
colorError: '',
colorInfo: '',
colorPrimary: '',
colorSuccess: '',
colorTextBase: '',
colorWarning: '',
zIndexPopupBase: 2000, // 调整基础弹层层级,避免下拉等组件被弹窗或者最大化状态下的表格遮挡
});
const getCssVariableValue = (variable: string, isColor: boolean = true) => {
const value = rootStyles.getPropertyValue(variable);
return isColor ? `hsl(${value})` : value;
};
watch(
() => preferences.theme,
() => {
tokens.colorPrimary = getCssVariableValue('--primary');
tokens.colorInfo = getCssVariableValue('--primary');
tokens.colorError = getCssVariableValue('--destructive');
tokens.colorWarning = getCssVariableValue('--warning');
tokens.colorSuccess = getCssVariableValue('--success');
tokens.colorTextBase = getCssVariableValue('--foreground');
getCssVariableValue('--primary-foreground');
tokens.colorBorderSecondary = tokens.colorBorder =
getCssVariableValue('--border');
tokens.colorBgElevated = getCssVariableValue('--popover');
tokens.colorBgContainer = getCssVariableValue('--card');
tokens.colorBgBase = getCssVariableValue('--background');
const radius = Number.parseFloat(getCssVariableValue('--radius', false));
// 1rem = 16px
tokens.borderRadius = radius * 16;
tokens.colorBgLayout = getCssVariableValue('--background-deep');
tokens.colorBgMask = getCssVariableValue('--overlay');
},
{ immediate: true },
);
return {
tokens,
};
}
export function useNaiveDesignTokens() {
const rootStyles = getComputedStyle(document.documentElement);
const commonTokens = reactive({
baseColor: '',
bodyColor: '',
borderColor: '',
borderRadius: '',
cardColor: '',
dividerColor: '',
errorColor: '',
errorColorHover: '',
errorColorPressed: '',
errorColorSuppl: '',
invertedColor: '',
modalColor: '',
popoverColor: '',
primaryColor: '',
primaryColorHover: '',
primaryColorPressed: '',
primaryColorSuppl: '',
successColor: '',
successColorHover: '',
successColorPressed: '',
successColorSuppl: '',
tableColor: '',
textColorBase: '',
warningColor: '',
warningColorHover: '',
warningColorPressed: '',
warningColorSuppl: '',
});
const getCssVariableValue = (variable: string, isColor: boolean = true) => {
const value = rootStyles.getPropertyValue(variable);
return isColor ? convertToRgb(`hsl(${value})`) : value;
};
watch(
() => preferences.theme,
() => {
commonTokens.primaryColor = getCssVariableValue('--primary');
commonTokens.primaryColorHover = getCssVariableValue('--primary-600');
commonTokens.primaryColorPressed = getCssVariableValue('--primary-700');
commonTokens.primaryColorSuppl = getCssVariableValue('--primary-800');
commonTokens.errorColor = getCssVariableValue('--destructive');
commonTokens.errorColorHover = getCssVariableValue('--destructive-600');
commonTokens.errorColorPressed = getCssVariableValue('--destructive-700');
commonTokens.errorColorSuppl = getCssVariableValue('--destructive-800');
commonTokens.warningColor = getCssVariableValue('--warning');
commonTokens.warningColorHover = getCssVariableValue('--warning-600');
commonTokens.warningColorPressed = getCssVariableValue('--warning-700');
commonTokens.warningColorSuppl = getCssVariableValue('--warning-800');
commonTokens.successColor = getCssVariableValue('--success');
commonTokens.successColorHover = getCssVariableValue('--success-600');
commonTokens.successColorPressed = getCssVariableValue('--success-700');
commonTokens.successColorSuppl = getCssVariableValue('--success-800');
commonTokens.textColorBase = getCssVariableValue('--foreground');
commonTokens.baseColor = getCssVariableValue('--primary-foreground');
commonTokens.dividerColor = commonTokens.borderColor =
getCssVariableValue('--border');
commonTokens.modalColor = commonTokens.popoverColor =
getCssVariableValue('--popover');
commonTokens.tableColor = commonTokens.cardColor =
getCssVariableValue('--card');
commonTokens.bodyColor = getCssVariableValue('--background');
commonTokens.invertedColor = getCssVariableValue('--background-deep');
commonTokens.borderRadius = getCssVariableValue('--radius', false);
},
{ immediate: true },
);
return {
commonTokens,
};
}
export function useElementPlusDesignTokens() {
const { isDark } = usePreferences();
const rootStyles = getComputedStyle(document.documentElement);
const getCssVariableValueRaw = (variable: string) => {
return rootStyles.getPropertyValue(variable);
};
const getCssVariableValue = (variable: string, isColor: boolean = true) => {
const value = getCssVariableValueRaw(variable);
return isColor ? convertToRgb(`hsl(${value})`) : value;
};
watch(
() => preferences.theme,
() => {
const background = getCssVariableValue('--background');
const border = getCssVariableValue('--border');
const accent = getCssVariableValue('--accent');
const variables: Record<string, string> = {
'--el-bg-color': background,
'--el-bg-color-overlay': getCssVariableValue('--popover'),
'--el-bg-color-page': getCssVariableValue('--background-deep'),
'--el-border-color': border,
'--el-border-color-dark': border,
'--el-border-color-extra-light': border,
'--el-border-color-hover': accent,
'--el-border-color-light': border,
'--el-border-color-lighter': border,
'--el-border-radius-base': getCssVariableValue('--radius', false),
'--el-color-danger': getCssVariableValue('--destructive-500'),
'--el-color-danger-dark-2': isDark.value
? getCssVariableValue('--destructive-400')
: getCssVariableValue('--destructive-600'),
'--el-color-danger-light-3': isDark.value
? getCssVariableValue('--destructive-600')
: getCssVariableValue('--destructive-400'),
'--el-color-danger-light-5': isDark.value
? getCssVariableValue('--destructive-700')
: getCssVariableValue('--destructive-300'),
'--el-color-danger-light-7': isDark.value
? getCssVariableValue('--destructive-800')
: getCssVariableValue('--destructive-200'),
'--el-color-danger-light-8': isDark.value
? getCssVariableValue('--destructive-900')
: getCssVariableValue('--destructive-100'),
'--el-color-danger-light-9': isDark.value
? getCssVariableValue('--destructive-950')
: getCssVariableValue('--destructive-50'),
'--el-color-error': getCssVariableValue('--destructive-500'),
'--el-color-error-dark-2': isDark.value
? getCssVariableValue('--destructive-400')
: getCssVariableValue('--destructive-600'),
'--el-color-error-light-3': isDark.value
? getCssVariableValue('--destructive-600')
: getCssVariableValue('--destructive-400'),
'--el-color-error-light-5': isDark.value
? getCssVariableValue('--destructive-700')
: getCssVariableValue('--destructive-300'),
'--el-color-error-light-7': isDark.value
? getCssVariableValue('--destructive-800')
: getCssVariableValue('--destructive-200'),
'--el-color-error-light-8': isDark.value
? getCssVariableValue('--destructive-900')
: getCssVariableValue('--destructive-100'),
'--el-color-error-light-9': isDark.value
? getCssVariableValue('--destructive-950')
: getCssVariableValue('--destructive-50'),
'--el-color-info-light-5': border,
'--el-color-info-light-8': border,
'--el-color-info-light-9': getCssVariableValue('--info'), // getCssVariableValue('--secondary'),
'--el-color-primary': getCssVariableValue('--primary-500'),
'--el-color-primary-dark-2': isDark.value
? getCssVariableValue('--primary-400')
: getCssVariableValue('--primary-600'),
'--el-color-primary-light-3': isDark.value
? getCssVariableValue('--primary-600')
: getCssVariableValue('--primary-400'),
'--el-color-primary-light-5': isDark.value
? getCssVariableValue('--primary-700')
: getCssVariableValue('--primary-300'),
'--el-color-primary-light-7': isDark.value
? getCssVariableValue('--primary-800')
: getCssVariableValue('--primary-200'),
'--el-color-primary-light-8': isDark.value
? getCssVariableValue('--primary-900')
: getCssVariableValue('--primary-100'),
'--el-color-primary-light-9': isDark.value
? getCssVariableValue('--primary-950')
: getCssVariableValue('--primary-50'),
'--el-color-success': getCssVariableValue('--success-500'),
'--el-color-success-dark-2': isDark.value
? getCssVariableValue('--success-400')
: getCssVariableValue('--success-600'),
'--el-color-success-light-3': isDark.value
? getCssVariableValue('--success-600')
: getCssVariableValue('--success-400'),
'--el-color-success-light-5': isDark.value
? getCssVariableValue('--success-700')
: getCssVariableValue('--success-300'),
'--el-color-success-light-7': isDark.value
? getCssVariableValue('--success-800')
: getCssVariableValue('--success-200'),
'--el-color-success-light-8': isDark.value
? getCssVariableValue('--success-900')
: getCssVariableValue('--success-100'),
'--el-color-success-light-9': isDark.value
? getCssVariableValue('--success-950')
: getCssVariableValue('--success-50'),
'--el-color-warning': getCssVariableValue('--warning-500'),
'--el-color-warning-dark-2': isDark.value
? getCssVariableValue('--warning-400')
: getCssVariableValue('--warning-600'),
'--el-color-warning-light-3': isDark.value
? getCssVariableValue('--warning-600')
: getCssVariableValue('--warning-400'),
'--el-color-warning-light-5': isDark.value
? getCssVariableValue('--warning-700')
: getCssVariableValue('--warning-300'),
'--el-color-warning-light-7': isDark.value
? getCssVariableValue('--warning-800')
: getCssVariableValue('--warning-200'),
'--el-color-warning-light-8': isDark.value
? getCssVariableValue('--warning-900')
: getCssVariableValue('--warning-100'),
'--el-color-warning-light-9': isDark.value
? getCssVariableValue('--warning-950')
: getCssVariableValue('--warning-50'),
'--el-fill-color': getCssVariableValue('--accent'),
'--el-fill-color-blank': background,
'--el-fill-color-light': getCssVariableValue('--accent'),
'--el-fill-color-lighter': getCssVariableValue('--accent-lighter'),
'--el-fill-color-dark': getCssVariableValue('--accent-dark'),
'--el-fill-color-darker': getCssVariableValue('--accent-darker'),
// 解决ElLoading背景色问题
'--el-mask-color': isDark.value
? 'rgba(0,0,0,.8)'
: 'rgba(255,255,255,.9)',
'--el-text-color-primary': getCssVariableValue('--foreground'),
'--el-text-color-regular': getCssVariableValue('--foreground'),
};
updateCSSVariables(variables, `__vben_design_styles__`);
},
{ immediate: true },
);
}

View File

@@ -0,0 +1,68 @@
import type { Arrayable, MaybeElementRef } from '@vueuse/core';
import type { Ref } from 'vue';
import { computed, onUnmounted, ref, unref, watch } from 'vue';
import { isFunction } from '@vben/utils';
import { useElementHover } from '@vueuse/core';
/**
* 监测鼠标是否在元素内部,如果在元素内部则返回 true否则返回 false
* @param refElement 所有需要检测的元素。如果提供了一个数组,那么鼠标在任何一个元素内部都会返回 true
* @param delay 延迟更新状态的时间
* @returns 返回一个数组,第一个元素是一个 ref表示鼠标是否在元素内部第二个元素是一个控制器可以通过 enable 和 disable 方法来控制监听器的启用和禁用
*/
export function useHoverToggle(
refElement: Arrayable<MaybeElementRef>,
delay: (() => number) | number = 500,
) {
const isHovers: Array<Ref<boolean>> = [];
const value = ref(false);
const timer = ref<ReturnType<typeof setTimeout> | undefined>();
const refs = Array.isArray(refElement) ? refElement : [refElement];
refs.forEach((refEle) => {
const eleRef = computed(() => {
const ele = unref(refEle);
return ele instanceof Element ? ele : (ele?.$el as Element);
});
const isHover = useElementHover(eleRef);
isHovers.push(isHover);
});
const isOutsideAll = computed(() => isHovers.every((v) => !v.value));
function setValueDelay(val: boolean) {
timer.value && clearTimeout(timer.value);
timer.value = setTimeout(
() => {
value.value = val;
timer.value = undefined;
},
isFunction(delay) ? delay() : delay,
);
}
const watcher = watch(
isOutsideAll,
(val) => {
setValueDelay(!val);
},
{ immediate: true },
);
const controller = {
enable() {
watcher.resume();
},
disable() {
watcher.pause();
},
};
onUnmounted(() => {
timer.value && clearTimeout(timer.value);
});
return [value, controller] as [typeof value, typeof controller];
}

View File

@@ -0,0 +1,58 @@
import type { Ref } from 'vue';
import { computed, ref, unref } from 'vue';
/**
* Paginates an array of items
* @param list The array to paginate
* @param pageNo The current page number (1-based)
* @param pageSize Number of items per page
* @returns Paginated array slice
* @throws {Error} If pageNo or pageSize are invalid
*/
function pagination<T = any>(list: T[], pageNo: number, pageSize: number): T[] {
if (pageNo < 1) throw new Error('Page number must be positive');
if (pageSize < 1) throw new Error('Page size must be positive');
const offset = (pageNo - 1) * Number(pageSize);
const ret =
offset + pageSize >= list.length
? list.slice(offset)
: list.slice(offset, offset + pageSize);
return ret;
}
export function usePagination<T = any>(list: Ref<T[]>, pageSize: number) {
const currentPage = ref(1);
const pageSizeRef = ref(pageSize);
const totalPages = computed(() =>
Math.ceil(unref(list).length / unref(pageSizeRef)),
);
const paginationList = computed(() => {
return pagination(unref(list), unref(currentPage), unref(pageSizeRef));
});
const total = computed(() => {
return unref(list).length;
});
function setCurrentPage(page: number) {
if (page < 1 || page > unref(totalPages)) {
throw new Error('Invalid page number');
}
currentPage.value = page;
}
function setPageSize(pageSize: number) {
if (pageSize < 1) {
throw new Error('Page size must be positive');
}
pageSizeRef.value = pageSize;
// Reset to first page to prevent invalid state
currentPage.value = 1;
}
return { setCurrentPage, total, setPageSize, paginationList };
}

View File

@@ -0,0 +1,16 @@
import { useRouter } from 'vue-router';
import { useTabbarStore } from '@vben/stores';
export function useRefresh() {
const router = useRouter();
const tabbarStore = useTabbarStore();
async function refresh() {
await tabbarStore.refresh(router);
}
return {
refresh,
};
}

View File

@@ -0,0 +1,132 @@
import type { ComputedRef } from 'vue';
import type { RouteLocationNormalized } from 'vue-router';
import { useTabbarStore } from '@vben/stores';
import { useRoute, useRouter } from 'vue-router';
export function useTabs() {
const router = useRouter();
const route = useRoute();
const tabbarStore = useTabbarStore();
async function closeLeftTabs(tab?: RouteLocationNormalized) {
await tabbarStore.closeLeftTabs(tab || route);
}
async function closeAllTabs() {
await tabbarStore.closeAllTabs(router);
}
async function closeRightTabs(tab?: RouteLocationNormalized) {
await tabbarStore.closeRightTabs(tab || route);
}
async function closeOtherTabs(tab?: RouteLocationNormalized) {
await tabbarStore.closeOtherTabs(tab || route);
}
async function closeCurrentTab(tab?: RouteLocationNormalized) {
await tabbarStore.closeTab(tab || route, router);
}
async function pinTab(tab?: RouteLocationNormalized) {
await tabbarStore.pinTab(tab || route);
}
async function unpinTab(tab?: RouteLocationNormalized) {
await tabbarStore.unpinTab(tab || route);
}
async function toggleTabPin(tab?: RouteLocationNormalized) {
await tabbarStore.toggleTabPin(tab || route);
}
async function refreshTab(name?: string) {
await tabbarStore.refresh(name || router);
}
async function openTabInNewWindow(tab?: RouteLocationNormalized) {
await tabbarStore.openTabInNewWindow(tab || route);
}
async function closeTabByKey(key: string) {
await tabbarStore.closeTabByKey(key, router);
}
/**
* 设置当前标签页的标题
*
* @description 支持设置静态标题字符串或动态计算标题
* @description 动态标题会在每次渲染时重新计算,适用于多语言或状态相关的标题
*
* @param title - 标题内容
* - 静态标题: 直接传入字符串
* - 动态标题: 传入 ComputedRef
*
* @example
* // 静态标题
* setTabTitle('标签页')
*
* // 动态标题(多语言)
* setTabTitle(computed(() => t('page.title')))
*/
async function setTabTitle(title: ComputedRef<string> | string) {
tabbarStore.setUpdateTime();
await tabbarStore.setTabTitle(route, title);
}
async function resetTabTitle() {
tabbarStore.setUpdateTime();
await tabbarStore.resetTabTitle(route);
}
/**
* 获取操作是否禁用
* @param tab
*/
function getTabDisableState(tab: RouteLocationNormalized = route) {
const tabs = tabbarStore.getTabs;
const affixTabs = tabbarStore.affixTabs;
const index = tabs.findIndex((item) => item.path === tab.path);
const disabled = tabs.length <= 1;
const { meta } = tab;
const affixTab = meta?.affixTab ?? false;
const isCurrentTab = route.path === tab.path;
// 当前处于最左侧或者减去固定标签页的数量等于0
const disabledCloseLeft =
index === 0 || index - affixTabs.length <= 0 || !isCurrentTab;
const disabledCloseRight = !isCurrentTab || index === tabs.length - 1;
const disabledCloseOther =
disabled || !isCurrentTab || tabs.length - affixTabs.length <= 1;
return {
disabledCloseAll: disabled,
disabledCloseCurrent: !!affixTab || disabled,
disabledCloseLeft,
disabledCloseOther,
disabledCloseRight,
disabledRefresh: !isCurrentTab,
};
}
return {
closeAllTabs,
closeCurrentTab,
closeLeftTabs,
closeOtherTabs,
closeRightTabs,
closeTabByKey,
getTabDisableState,
openTabInNewWindow,
pinTab,
refreshTab,
resetTabTitle,
setTabTitle,
toggleTabPin,
unpinTab,
};
}

View File

@@ -0,0 +1,84 @@
import type { Watermark, WatermarkOptions } from 'watermark-js-plus';
import { nextTick, onUnmounted, readonly, ref } from 'vue';
const watermark = ref<Watermark>();
const unmountedHooked = ref<boolean>(false);
const cachedOptions = ref<Partial<WatermarkOptions>>({
advancedStyle: {
colorStops: [
{
color: 'gray',
offset: 0,
},
{
color: 'gray',
offset: 1,
},
],
type: 'linear',
},
// fontSize: '20px',
content: '',
contentType: 'multi-line-text',
globalAlpha: 0.25,
gridLayoutOptions: {
cols: 2,
gap: [20, 20],
matrix: [
[1, 0],
[0, 1],
],
rows: 2,
},
height: 200,
layout: 'grid',
rotate: 30,
width: 160,
});
export function useWatermark() {
async function initWatermark(options: Partial<WatermarkOptions>) {
const { Watermark } = await import('watermark-js-plus');
cachedOptions.value = {
...cachedOptions.value,
...options,
};
watermark.value = new Watermark(cachedOptions.value);
await watermark.value?.create();
}
async function updateWatermark(options: Partial<WatermarkOptions>) {
if (watermark.value) {
await nextTick();
await watermark.value?.changeOptions({
...cachedOptions.value,
...options,
});
} else {
await initWatermark(options);
}
}
function destroyWatermark() {
if (watermark.value) {
watermark.value.destroy();
watermark.value = undefined;
}
}
// 只在第一次调用时注册卸载钩子,防止重复注册以致于在路由切换时销毁了水印
if (!unmountedHooked.value) {
unmountedHooked.value = true;
onUnmounted(() => {
destroyWatermark();
});
}
return {
destroyWatermark,
updateWatermark,
watermark: readonly(watermark),
};
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"compilerOptions": {
"types": ["vite/client", "@vben/types/global"]
},
"include": ["src"],
"exclude": ["node_modules"]
}