mirror of
https://gitee.com/yudaocode/yudao-ui-admin-vben.git
synced 2025-12-30 10:32:25 +00:00
14
README.md
14
README.md
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
## 🐶 新手必读
|
## 🐶 新手必读
|
||||||
|
|
||||||
- nodejs > 20.12.0 && pnpm > 10.14.0 (强制使用pnpm)
|
- nodejs > 20.12.0 && pnpm > 10.22.0 (强制使用pnpm)
|
||||||
- 演示地址【Vue3 + element-plus】:<http://dashboard-vue3.yudao.iocoder.cn>
|
- 演示地址【Vue3 + element-plus】:<http://dashboard-vue3.yudao.iocoder.cn>
|
||||||
- 演示地址【Vue3 + vben5(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
|
- 演示地址【Vue3 + vben5(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
|
||||||
- 演示地址【Vue2 + element-ui】:<http://dashboard.yudao.iocoder.cn>
|
- 演示地址【Vue2 + element-ui】:<http://dashboard.yudao.iocoder.cn>
|
||||||
@@ -41,22 +41,22 @@
|
|||||||
|
|
||||||
| 框架 | 说明 | 版本 |
|
| 框架 | 说明 | 版本 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| [Vue](https://staging-cn.vuejs.org/) | vue框架 | 3.5.17 |
|
| [Vue](https://staging-cn.vuejs.org/) | vue框架 | 3.5.24 |
|
||||||
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 7.1.2 |
|
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 7.2.2 |
|
||||||
| [Ant Design Vue](https://www.antdv.com/) | Ant Design Vue | 4.2.6 |
|
| [Ant Design Vue](https://www.antdv.com/) | Ant Design Vue | 4.2.6 |
|
||||||
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.10.2 |
|
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.10.2 |
|
||||||
| [Naive UI](https://www.naiveui.com/) | Naive UI | 2.42.0 |
|
| [Naive UI](https://www.naiveui.com/) | Naive UI | 2.42.0 |
|
||||||
| [TDesign](https://tdesign.tencent.com/) | TDesign | 1.17.1 |
|
| [TDesign](https://tdesign.tencent.com/) | TDesign | 1.17.1 |
|
||||||
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 超集 | 5.8.3 |
|
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 超集 | 5.9.3 |
|
||||||
| [pinia](https://pinia.vuejs.org/) | Vue 存储库替代 vuex5 | 3.0.3 |
|
| [pinia](https://pinia.vuejs.org/) | Vue 存储库替代 vuex5 | 3.0.3 |
|
||||||
| [vueuse](https://vueuse.org/) | 常用工具集 | 13.4.0 |
|
| [vueuse](https://vueuse.org/) | 常用工具集 | 13.4.0 |
|
||||||
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 11.1.7 |
|
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 11.1.7 |
|
||||||
| [vue-router](https://router.vuejs.org/) | Vue 路由 | 4.5.1 |
|
| [vue-router](https://router.vuejs.org/) | Vue 路由 | 4.5.1 |
|
||||||
| [Tailwind CSS](https://tailwindcss.com/) | 原子 CSS | 3.4.17 |
|
| [Tailwind CSS](https://tailwindcss.com/) | 原子 CSS | 3.4.18 |
|
||||||
| [Iconify](https://iconify.design/) | 图标组件 | 5.0.0 |
|
| [Iconify](https://iconify.design/) | 图标组件 | 5.0.0 |
|
||||||
| [Iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.354 |
|
| [Iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.406 |
|
||||||
| [TinyMCE](https://www.tiny.cloud/) | 富文本编辑器 | 6.1.0 |
|
| [TinyMCE](https://www.tiny.cloud/) | 富文本编辑器 | 6.1.0 |
|
||||||
| [Echarts](https://echarts.apache.org/) | 图表库 | 5.6.0 |
|
| [Echarts](https://echarts.apache.org/) | 图表库 | 6.0.0 |
|
||||||
| [axios](https://axios-http.com/) | http客户端 | 1.10.0 |
|
| [axios](https://axios-http.com/) | http客户端 | 1.10.0 |
|
||||||
| [dayjs](https://day.js.org/) | 日期处理库 | 1.11.13 |
|
| [dayjs](https://day.js.org/) | 日期处理库 | 1.11.13 |
|
||||||
| [vee-validate](https://vee-validate.logaretm.com/) | 表单验证 | 4.15.1 |
|
| [vee-validate](https://vee-validate.logaretm.com/) | 表单验证 | 4.15.1 |
|
||||||
|
|||||||
@@ -3,15 +3,31 @@
|
|||||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Component } from 'vue';
|
import type {
|
||||||
|
UploadChangeParam,
|
||||||
|
UploadFile,
|
||||||
|
UploadProps,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import type { Component, Ref } from 'vue';
|
||||||
|
|
||||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||||
import type { Recordable } from '@vben/types';
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
import {
|
||||||
|
defineAsyncComponent,
|
||||||
|
defineComponent,
|
||||||
|
h,
|
||||||
|
ref,
|
||||||
|
render,
|
||||||
|
unref,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
import { notification } from 'ant-design-vue';
|
import { notification } from 'ant-design-vue';
|
||||||
|
|
||||||
@@ -22,9 +38,6 @@ const AutoComplete = defineAsyncComponent(
|
|||||||
() => import('ant-design-vue/es/auto-complete'),
|
() => import('ant-design-vue/es/auto-complete'),
|
||||||
);
|
);
|
||||||
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
|
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
|
||||||
const Cascader = defineAsyncComponent(
|
|
||||||
() => import('ant-design-vue/es/cascader'),
|
|
||||||
);
|
|
||||||
const Checkbox = defineAsyncComponent(
|
const Checkbox = defineAsyncComponent(
|
||||||
() => import('ant-design-vue/es/checkbox'),
|
() => import('ant-design-vue/es/checkbox'),
|
||||||
);
|
);
|
||||||
@@ -68,7 +81,14 @@ const TimeRangePicker = defineAsyncComponent(() =>
|
|||||||
const TreeSelect = defineAsyncComponent(
|
const TreeSelect = defineAsyncComponent(
|
||||||
() => import('ant-design-vue/es/tree-select'),
|
() => import('ant-design-vue/es/tree-select'),
|
||||||
);
|
);
|
||||||
|
const Cascader = defineAsyncComponent(
|
||||||
|
() => import('ant-design-vue/es/cascader'),
|
||||||
|
);
|
||||||
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
|
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
|
||||||
|
const Image = defineAsyncComponent(() => import('ant-design-vue/es/image'));
|
||||||
|
const PreviewGroup = defineAsyncComponent(() =>
|
||||||
|
import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup),
|
||||||
|
);
|
||||||
|
|
||||||
const withDefaultPlaceholder = <T extends Component>(
|
const withDefaultPlaceholder = <T extends Component>(
|
||||||
component: T,
|
component: T,
|
||||||
@@ -104,12 +124,223 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const withPreviewUpload = () => {
|
||||||
|
return defineComponent({
|
||||||
|
name: Upload.name,
|
||||||
|
emits: ['change', 'update:modelValue'],
|
||||||
|
setup: (
|
||||||
|
props: any,
|
||||||
|
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
|
||||||
|
) => {
|
||||||
|
const previewVisible = ref<boolean>(false);
|
||||||
|
|
||||||
|
const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`);
|
||||||
|
|
||||||
|
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
|
||||||
|
|
||||||
|
const fileList = ref<UploadProps['fileList']>(
|
||||||
|
attrs?.fileList || attrs?.['file-list'] || [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = async (event: UploadChangeParam) => {
|
||||||
|
fileList.value = event.fileList;
|
||||||
|
emit('change', event);
|
||||||
|
emit(
|
||||||
|
'update:modelValue',
|
||||||
|
event.fileList?.length ? fileList.value : undefined,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreview = async (file: UploadFile) => {
|
||||||
|
previewVisible.value = true;
|
||||||
|
await previewImage(file, previewVisible, fileList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderUploadButton = (): any => {
|
||||||
|
const isDisabled = attrs.disabled;
|
||||||
|
|
||||||
|
// 如果禁用,不渲染上传按钮
|
||||||
|
if (isDisabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则渲染默认上传按钮
|
||||||
|
return isEmpty(slots)
|
||||||
|
? createDefaultSlotsWithUpload(listType, placeholder)
|
||||||
|
: slots;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 可以监听到表单API设置的值
|
||||||
|
watch(
|
||||||
|
() => attrs.modelValue,
|
||||||
|
(res) => {
|
||||||
|
fileList.value = res;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
h(
|
||||||
|
Upload,
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
...attrs,
|
||||||
|
fileList: fileList.value,
|
||||||
|
onChange: handleChange,
|
||||||
|
onPreview: handlePreview,
|
||||||
|
},
|
||||||
|
renderUploadButton(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDefaultSlotsWithUpload = (
|
||||||
|
listType: string,
|
||||||
|
placeholder: string,
|
||||||
|
) => {
|
||||||
|
switch (listType) {
|
||||||
|
case 'picture-card': {
|
||||||
|
return {
|
||||||
|
default: () => placeholder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return {
|
||||||
|
default: () =>
|
||||||
|
h(
|
||||||
|
Button,
|
||||||
|
{
|
||||||
|
icon: h(IconifyIcon, {
|
||||||
|
icon: 'ant-design:upload-outlined',
|
||||||
|
class: 'mb-1 size-4',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
() => placeholder,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewImage = async (
|
||||||
|
file: UploadFile,
|
||||||
|
visible: Ref<boolean>,
|
||||||
|
fileList: Ref<UploadProps['fileList']>,
|
||||||
|
) => {
|
||||||
|
// 检查是否为图片文件的辅助函数
|
||||||
|
const isImageFile = (file: UploadFile): boolean => {
|
||||||
|
const imageExtensions = new Set([
|
||||||
|
'bmp',
|
||||||
|
'gif',
|
||||||
|
'jpeg',
|
||||||
|
'jpg',
|
||||||
|
'png',
|
||||||
|
'webp',
|
||||||
|
]);
|
||||||
|
if (file.url) {
|
||||||
|
const ext = file.url?.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? imageExtensions.has(ext) : false;
|
||||||
|
}
|
||||||
|
if (!file.type) {
|
||||||
|
const ext = file.name?.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? imageExtensions.has(ext) : false;
|
||||||
|
}
|
||||||
|
return file.type.startsWith('image/');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果当前文件不是图片,直接打开
|
||||||
|
if (!isImageFile(file)) {
|
||||||
|
if (file.url) {
|
||||||
|
window.open(file.url, '_blank');
|
||||||
|
} else if (file.preview) {
|
||||||
|
window.open(file.preview, '_blank');
|
||||||
|
} else {
|
||||||
|
console.warn('无法打开文件,没有可用的URL或预览地址');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于图片文件,继续使用预览组
|
||||||
|
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
|
||||||
|
Image,
|
||||||
|
PreviewGroup,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getBase64 = (file: File) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.addEventListener('load', () => resolve(reader.result));
|
||||||
|
reader.addEventListener('error', (error) => reject(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// 从fileList中过滤出所有图片文件
|
||||||
|
const imageFiles = (unref(fileList) || []).filter((element) =>
|
||||||
|
isImageFile(element),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 为所有没有预览地址的图片生成预览
|
||||||
|
for (const imgFile of imageFiles) {
|
||||||
|
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
|
||||||
|
imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const container: HTMLElement | null = document.createElement('div');
|
||||||
|
document.body.append(container);
|
||||||
|
|
||||||
|
// 用于追踪组件是否已卸载
|
||||||
|
let isUnmounted = false;
|
||||||
|
|
||||||
|
const PreviewWrapper = {
|
||||||
|
setup() {
|
||||||
|
return () => {
|
||||||
|
if (isUnmounted) return null;
|
||||||
|
return h(
|
||||||
|
PreviewGroupComponent,
|
||||||
|
{
|
||||||
|
class: 'hidden',
|
||||||
|
preview: {
|
||||||
|
visible: visible.value,
|
||||||
|
// 设置初始显示的图片索引
|
||||||
|
current: imageFiles.findIndex((f) => f.uid === file.uid),
|
||||||
|
onVisibleChange: (value: boolean) => {
|
||||||
|
visible.value = value;
|
||||||
|
if (!value) {
|
||||||
|
// 延迟清理,确保动画完成
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isUnmounted && container) {
|
||||||
|
isUnmounted = true;
|
||||||
|
render(null, container);
|
||||||
|
container.remove();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
// 渲染所有图片文件
|
||||||
|
imageFiles.map((imgFile) =>
|
||||||
|
h(ImageComponent, {
|
||||||
|
key: imgFile.uid,
|
||||||
|
src: imgFile.url || imgFile.preview,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(h(PreviewWrapper), container);
|
||||||
|
};
|
||||||
|
|
||||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||||
export type ComponentType =
|
export type ComponentType =
|
||||||
| 'ApiCascader'
|
| 'ApiCascader'
|
||||||
| 'ApiSelect'
|
| 'ApiSelect'
|
||||||
| 'ApiTreeSelect'
|
| 'ApiTreeSelect'
|
||||||
| 'AutoComplete'
|
| 'AutoComplete'
|
||||||
|
| 'Cascader'
|
||||||
| 'Checkbox'
|
| 'Checkbox'
|
||||||
| 'CheckboxGroup'
|
| 'CheckboxGroup'
|
||||||
| 'DatePicker'
|
| 'DatePicker'
|
||||||
@@ -143,21 +374,13 @@ async function initComponentAdapter() {
|
|||||||
// 如果你的组件体积比较大,可以使用异步加载
|
// 如果你的组件体积比较大,可以使用异步加载
|
||||||
// Button: () =>
|
// Button: () =>
|
||||||
// import('xxx').then((res) => res.Button),
|
// import('xxx').then((res) => res.Button),
|
||||||
ApiCascader: withDefaultPlaceholder(
|
ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||||
{
|
component: Cascader,
|
||||||
...ApiComponent,
|
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||||
name: 'ApiCascader',
|
loadingSlot: 'suffixIcon',
|
||||||
},
|
modelPropName: 'value',
|
||||||
'select',
|
visibleEvent: 'onVisibleChange',
|
||||||
{
|
}),
|
||||||
component: Cascader,
|
|
||||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
|
||||||
loadingSlot: 'suffixIcon',
|
|
||||||
modelPropName: 'value',
|
|
||||||
optionsPropName: 'treeData',
|
|
||||||
visibleEvent: 'onVisibleChange',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ApiSelect: withDefaultPlaceholder(
|
ApiSelect: withDefaultPlaceholder(
|
||||||
{
|
{
|
||||||
...ApiComponent,
|
...ApiComponent,
|
||||||
@@ -187,6 +410,7 @@ async function initComponentAdapter() {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
AutoComplete,
|
AutoComplete,
|
||||||
|
Cascader,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
CheckboxGroup,
|
CheckboxGroup,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
@@ -221,6 +445,7 @@ async function initComponentAdapter() {
|
|||||||
TimeRangePicker,
|
TimeRangePicker,
|
||||||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||||
Upload,
|
Upload,
|
||||||
|
PreviewUpload: withPreviewUpload(),
|
||||||
FileUpload,
|
FileUpload,
|
||||||
ImageUpload,
|
ImageUpload,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -84,9 +84,10 @@ setupVbenVxeTable({
|
|||||||
|
|
||||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||||
vxeUI.renderer.add('CellImage', {
|
vxeUI.renderer.add('CellImage', {
|
||||||
renderTableDefault(_renderOpts, params) {
|
renderTableDefault(renderOpts, params) {
|
||||||
|
const { props } = renderOpts;
|
||||||
const { column, row } = params;
|
const { column, row } = params;
|
||||||
return h(Image, { src: row[column.field] });
|
return h(Image, { src: row[column.field], ...props });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import { DictTag } from '#/components/dict-tag';
|
|
||||||
import { getDevicePage } from '#/api/iot/device/device';
|
import { getDevicePage } from '#/api/iot/device/device';
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
products: any[];
|
products: any[];
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import { DictTag } from '#/components/dict-tag';
|
|
||||||
import { getProductPage } from '#/api/iot/product/product';
|
import { getProductPage } from '#/api/iot/product/product';
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
categoryList: any[];
|
categoryList: any[];
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
rowConfig: {
|
rowConfig: {
|
||||||
keyField: 'id',
|
keyField: 'id',
|
||||||
isHover: true,
|
isHover: true,
|
||||||
height: type.value === UploadType.Image ? 220 : 'auto',
|
},
|
||||||
|
cellConfig: {
|
||||||
|
height: type.value === UploadType.Image ? 220 : undefined,
|
||||||
},
|
},
|
||||||
toolbarConfig: {
|
toolbarConfig: {
|
||||||
refresh: true,
|
refresh: true,
|
||||||
@@ -101,8 +103,10 @@ async function onTabChange() {
|
|||||||
rowConfig: {
|
rowConfig: {
|
||||||
keyField: 'id',
|
keyField: 'id',
|
||||||
isHover: true,
|
isHover: true,
|
||||||
height: type.value === UploadType.Image ? 220 : 'auto',
|
|
||||||
},
|
},
|
||||||
|
cellConfig: {
|
||||||
|
height: type.value === UploadType.Image ? 220 : undefined,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
await gridApi.reload();
|
await gridApi.reload();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ setupVbenVxeTable({
|
|||||||
},
|
},
|
||||||
previewTeleported: true,
|
previewTeleported: true,
|
||||||
});
|
});
|
||||||
|
// return h(ElImage, { src, previewSrcList: [src], ...props });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -66,9 +66,10 @@ setupVbenVxeTable({
|
|||||||
|
|
||||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||||
vxeUI.renderer.add('CellImage', {
|
vxeUI.renderer.add('CellImage', {
|
||||||
renderTableDefault(_renderOpts, params) {
|
renderTableDefault(renderOpts, params) {
|
||||||
|
const { props } = renderOpts;
|
||||||
const { column, row } = params;
|
const { column, row } = params;
|
||||||
return h(NImage, { src: row[column.field] });
|
return h(NImage, { src: row[column.field], ...props });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -72,9 +72,10 @@ setupVbenVxeTable({
|
|||||||
|
|
||||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||||
vxeUI.renderer.add('CellImage', {
|
vxeUI.renderer.add('CellImage', {
|
||||||
renderTableDefault(_renderOpts, params) {
|
renderTableDefault(renderOpts, params) {
|
||||||
|
const { props } = renderOpts;
|
||||||
const { column, row } = params;
|
const { column, row } = params;
|
||||||
return h(Image, { src: row[column.field] });
|
return h(Image, { src: row[column.field], ...props });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { registerLoadingDirective } from '@vben/common-ui/es/loading';
|
|||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
import { initStores } from '@vben/stores';
|
import { initStores } from '@vben/stores';
|
||||||
import '@vben/styles';
|
import '@vben/styles';
|
||||||
|
// import '@vben/styles/antd';
|
||||||
|
// 引入组件库的少量全局样式变量
|
||||||
|
|
||||||
import { useTitle } from '@vueuse/core';
|
import { useTitle } from '@vueuse/core';
|
||||||
|
|
||||||
@@ -15,8 +17,6 @@ import { initSetupVbenForm } from './adapter/form';
|
|||||||
import App from './app.vue';
|
import App from './app.vue';
|
||||||
import { router } from './router';
|
import { router } from './router';
|
||||||
|
|
||||||
// import '@vben/styles/antd';
|
|
||||||
// 引入组件库的少量全局样式变量
|
|
||||||
import 'tdesign-vue-next/es/style/index.css';
|
import 'tdesign-vue-next/es/style/index.css';
|
||||||
|
|
||||||
async function bootstrap(namespace: string) {
|
async function bootstrap(namespace: string) {
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ if (!import.meta.env.SSR) {
|
|||||||
|
|
||||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||||
vxeUI.renderer.add('CellImage', {
|
vxeUI.renderer.add('CellImage', {
|
||||||
renderTableDefault(_renderOpts, params) {
|
renderTableDefault(renderOpts, params) {
|
||||||
|
const { props } = renderOpts;
|
||||||
const { column, row } = params;
|
const { column, row } = params;
|
||||||
return h(Image, { src: row[column.field] });
|
return h(Image, { src: row[column.field], ...props });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ apps/web-naive
|
|||||||
|
|
||||||
## 演示代码精简
|
## 演示代码精简
|
||||||
|
|
||||||
如果你不需要演示代码,你可以直接删除的`playground`文件夹。
|
如果你不需要演示代码,你可以直接删除 `playground` 文件夹。
|
||||||
|
|
||||||
## 文档精简
|
## 文档精简
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ pnpm install
|
|||||||
|
|
||||||
- 在应用的 `src/router/routes` 文件中,你可以删除不需要的路由。其中 `core` 文件夹内,如果只需要登录和忘记密码,你可以删除其他路由,如忘记密码、注册等。路由删除后,你可以删除对应的页面文件,在 `src/views/_core` 文件夹中。
|
- 在应用的 `src/router/routes` 文件中,你可以删除不需要的路由。其中 `core` 文件夹内,如果只需要登录和忘记密码,你可以删除其他路由,如忘记密码、注册等。路由删除后,你可以删除对应的页面文件,在 `src/views/_core` 文件夹中。
|
||||||
|
|
||||||
- 在应用的 `src/router/routes` 文件中,你可以按需求删除不需要的路由,如`demos`、`vben` 目录等。路由删除后,你可以删除对应的页面文件,在 `src/views` 文件夹中。
|
- 在应用的 `src/router/routes` 文件中,你可以按需求删除不需要的路由,如`demos`、`vben` 目录等。路由删除后,你可以在 `src/views` 文件夹中删除对应的页面文件。
|
||||||
|
|
||||||
### 删除不需要的组件
|
### 删除不需要的组件
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@apply text-foreground bg-background font-sans text-[100%];
|
@apply text-foreground bg-background font-sans;
|
||||||
|
|
||||||
|
font-size: var(--font-size-base, 16px);
|
||||||
font-variation-settings: normal;
|
font-variation-settings: normal;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
text-size-adjust: 100%;
|
text-size-adjust: 100%;
|
||||||
|
|||||||
@@ -93,6 +93,7 @@
|
|||||||
|
|
||||||
/* 基本文字大小 */
|
/* 基本文字大小 */
|
||||||
--font-size-base: 16px;
|
--font-size-base: 16px;
|
||||||
|
--menu-font-size: calc(var(--font-size-base) * 0.875);
|
||||||
|
|
||||||
/* =============component & UI============= */
|
/* =============component & UI============= */
|
||||||
|
|
||||||
|
|||||||
@@ -208,4 +208,39 @@ function treeToString(tree: any[], nodeId: number | string) {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { filterTree, handleTree, mapTree, traverseTreeValues, treeToString };
|
/**
|
||||||
|
* 对树形结构数据进行递归排序
|
||||||
|
* @param treeData - 树形数据数组
|
||||||
|
* @param sortFunction - 排序函数,用于定义排序规则
|
||||||
|
* @param options - 配置选项,包括子节点属性名
|
||||||
|
* @returns 排序后的树形数据
|
||||||
|
*/
|
||||||
|
function sortTree<T extends Record<string, any>>(
|
||||||
|
treeData: T[],
|
||||||
|
sortFunction: (a: T, b: T) => number,
|
||||||
|
options?: TreeConfigOptions,
|
||||||
|
): T[] {
|
||||||
|
const { childProps } = options || {
|
||||||
|
childProps: 'children',
|
||||||
|
};
|
||||||
|
|
||||||
|
return treeData.toSorted(sortFunction).map((item) => {
|
||||||
|
const children = item[childProps];
|
||||||
|
if (children && Array.isArray(children) && children.length > 0) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
[childProps]: sortTree(children, sortFunction, options),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
filterTree,
|
||||||
|
handleTree,
|
||||||
|
mapTree,
|
||||||
|
sortTree,
|
||||||
|
traverseTreeValues,
|
||||||
|
treeToString,
|
||||||
|
};
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
|||||||
"colorPrimary": "hsl(212 100% 45%)",
|
"colorPrimary": "hsl(212 100% 45%)",
|
||||||
"colorSuccess": "hsl(144 57% 58%)",
|
"colorSuccess": "hsl(144 57% 58%)",
|
||||||
"colorWarning": "hsl(42 84% 61%)",
|
"colorWarning": "hsl(42 84% 61%)",
|
||||||
|
"fontSize": 16,
|
||||||
"mode": "dark",
|
"mode": "dark",
|
||||||
"radius": "0.5",
|
"radius": "0.5",
|
||||||
"semiDarkHeader": false,
|
"semiDarkHeader": false,
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ const defaultPreferences: Preferences = {
|
|||||||
colorWarning: 'hsl(42 84% 61%)',
|
colorWarning: 'hsl(42 84% 61%)',
|
||||||
mode: 'dark',
|
mode: 'dark',
|
||||||
radius: '0.5',
|
radius: '0.5',
|
||||||
|
fontSize: 16,
|
||||||
semiDarkHeader: false,
|
semiDarkHeader: false,
|
||||||
semiDarkSidebar: false,
|
semiDarkSidebar: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -141,7 +141,10 @@ class PreferenceManager {
|
|||||||
private handleUpdates(updates: DeepPartial<Preferences>) {
|
private handleUpdates(updates: DeepPartial<Preferences>) {
|
||||||
const themeUpdates = updates.theme || {};
|
const themeUpdates = updates.theme || {};
|
||||||
const appUpdates = updates.app || {};
|
const appUpdates = updates.app || {};
|
||||||
if (themeUpdates && Object.keys(themeUpdates).length > 0) {
|
if (
|
||||||
|
(themeUpdates && Object.keys(themeUpdates).length > 0) ||
|
||||||
|
Reflect.has(themeUpdates, 'fontSize')
|
||||||
|
) {
|
||||||
updateCSSVariables(this.state);
|
updateCSSVariables(this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -239,6 +239,8 @@ interface ThemePreferences {
|
|||||||
colorSuccess: string;
|
colorSuccess: string;
|
||||||
/** 警告色 */
|
/** 警告色 */
|
||||||
colorWarning: string;
|
colorWarning: string;
|
||||||
|
/** 字体大小(单位:px) */
|
||||||
|
fontSize: number;
|
||||||
/** 当前主题 */
|
/** 当前主题 */
|
||||||
mode: ThemeModeType;
|
mode: ThemeModeType;
|
||||||
/** 圆角 */
|
/** 圆角 */
|
||||||
|
|||||||
@@ -66,6 +66,19 @@ function updateCSSVariables(preferences: Preferences) {
|
|||||||
if (Reflect.has(theme, 'radius')) {
|
if (Reflect.has(theme, 'radius')) {
|
||||||
document.documentElement.style.setProperty('--radius', `${radius}rem`);
|
document.documentElement.style.setProperty('--radius', `${radius}rem`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新字体大小
|
||||||
|
if (Reflect.has(theme, 'fontSize')) {
|
||||||
|
const fontSize = theme.fontSize;
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
'--font-size-base',
|
||||||
|
`${fontSize}px`,
|
||||||
|
);
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
'--menu-font-size',
|
||||||
|
`calc(${fontSize}px * 0.875)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ $namespace: vben;
|
|||||||
padding: var(--menu-item-padding-y) var(--menu-item-padding-x);
|
padding: var(--menu-item-padding-y) var(--menu-item-padding-x);
|
||||||
margin: 0 var(--menu-item-margin-x) var(--menu-item-margin-y)
|
margin: 0 var(--menu-item-margin-x) var(--menu-item-margin-y)
|
||||||
var(--menu-item-margin-x);
|
var(--menu-item-margin-x);
|
||||||
font-size: var(--menu-font-size);
|
font-size: var(--menu-font-size) !important;
|
||||||
color: var(--menu-item-color);
|
color: var(--menu-item-color);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -433,6 +433,7 @@ $namespace: vben;
|
|||||||
max-width: var(--menu-title-width);
|
max-width: var(--menu-title-width);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
font-size: var(--menu-font-size) !important;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@@ -444,7 +445,7 @@ $namespace: vben;
|
|||||||
.#{$namespace}-menu__popup-container,
|
.#{$namespace}-menu__popup-container,
|
||||||
.#{$namespace}-menu {
|
.#{$namespace}-menu {
|
||||||
--menu-title-width: 140px;
|
--menu-title-width: 140px;
|
||||||
--menu-item-icon-size: 16px;
|
--menu-item-icon-size: var(--font-size-base, 16px);
|
||||||
--menu-item-height: 38px;
|
--menu-item-height: 38px;
|
||||||
--menu-item-padding-y: 21px;
|
--menu-item-padding-y: 21px;
|
||||||
--menu-item-padding-x: 12px;
|
--menu-item-padding-x: 12px;
|
||||||
@@ -458,7 +459,6 @@ $namespace: vben;
|
|||||||
--menu-item-collapse-margin-x: 0px;
|
--menu-item-collapse-margin-x: 0px;
|
||||||
--menu-item-radius: 0px;
|
--menu-item-radius: 0px;
|
||||||
--menu-item-indent: 16px;
|
--menu-item-indent: 16px;
|
||||||
--menu-font-size: 14px;
|
|
||||||
|
|
||||||
&.is-dark {
|
&.is-dark {
|
||||||
--menu-background-color: hsl(var(--menu));
|
--menu-background-color: hsl(var(--menu));
|
||||||
@@ -752,7 +752,7 @@ $namespace: vben;
|
|||||||
}
|
}
|
||||||
.#{$namespace}-menu__icon {
|
.#{$namespace}-menu__icon {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 20px !important;
|
font-size: calc(var(--font-size-base, 16px) * 1.25) !important;
|
||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,7 +760,7 @@ $namespace: vben;
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-size: 12px;
|
font-size: calc(var(--font-size-base, 16px) * 0.75);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
@@ -785,7 +785,7 @@ $namespace: vben;
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 var(--menu-item-padding-x);
|
padding: 0 var(--menu-item-padding-x);
|
||||||
font-size: var(--menu-font-size);
|
font-size: var(--menu-font-size) !important;
|
||||||
line-height: var(--menu-item-height);
|
line-height: var(--menu-item-height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -812,9 +812,14 @@ $namespace: vben;
|
|||||||
|
|
||||||
.#{$namespace}-sub-menu-content {
|
.#{$namespace}-sub-menu-content {
|
||||||
height: var(--menu-item-height);
|
height: var(--menu-item-height);
|
||||||
|
font-size: var(--menu-font-size) !important;
|
||||||
|
|
||||||
@include menu-item;
|
@include menu-item;
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-size: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
&__icon-arrow {
|
&__icon-arrow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ $namespace: vben;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.#{$namespace}-normal-menu__icon {
|
.#{$namespace}-normal-menu__icon {
|
||||||
font-size: 20px;
|
font-size: calc(var(--font-size-base, 16px) * 1.25);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,14 +146,14 @@ $namespace: vben;
|
|||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
max-height: 20px;
|
max-height: 20px;
|
||||||
font-size: 20px;
|
font-size: calc(var(--font-size-base, 16px) * 1.25);
|
||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__name {
|
&__name {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-size: 12px;
|
font-size: calc(var(--font-size-base, 16px) * 0.75);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ interface Props {
|
|||||||
childrenField?: string;
|
childrenField?: string;
|
||||||
/** value字段名 */
|
/** value字段名 */
|
||||||
valueField?: string;
|
valueField?: string;
|
||||||
|
/** disabled字段名 */
|
||||||
|
disabledField?: string;
|
||||||
/** 组件接收options数据的属性名 */
|
/** 组件接收options数据的属性名 */
|
||||||
optionsPropName?: string;
|
optionsPropName?: string;
|
||||||
/** 是否立即调用api */
|
/** 是否立即调用api */
|
||||||
@@ -75,6 +77,7 @@ defineOptions({ name: 'ApiComponent', inheritAttrs: false });
|
|||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
labelField: 'label',
|
labelField: 'label',
|
||||||
valueField: 'value',
|
valueField: 'value',
|
||||||
|
disabledField: 'disabled',
|
||||||
childrenField: '',
|
childrenField: '',
|
||||||
optionsPropName: 'options',
|
optionsPropName: 'options',
|
||||||
resultField: '',
|
resultField: '',
|
||||||
@@ -108,17 +111,25 @@ const isFirstLoaded = ref(false);
|
|||||||
const hasPendingRequest = ref(false);
|
const hasPendingRequest = ref(false);
|
||||||
|
|
||||||
const getOptions = computed(() => {
|
const getOptions = computed(() => {
|
||||||
const { labelField, valueField, childrenField, numberToString } = props;
|
const {
|
||||||
|
labelField,
|
||||||
|
valueField,
|
||||||
|
disabledField,
|
||||||
|
childrenField,
|
||||||
|
numberToString,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const refOptionsData = unref(refOptions);
|
const refOptionsData = unref(refOptions);
|
||||||
|
|
||||||
function transformData(data: OptionsItem[]): OptionsItem[] {
|
function transformData(data: OptionsItem[]): OptionsItem[] {
|
||||||
return data.map((item) => {
|
return data.map((item) => {
|
||||||
const value = get(item, valueField);
|
const value = get(item, valueField);
|
||||||
|
const disabled = get(item, disabledField);
|
||||||
return {
|
return {
|
||||||
...objectOmit(item, [labelField, valueField, childrenField]),
|
...objectOmit(item, [labelField, valueField, disabled, childrenField]),
|
||||||
label: get(item, labelField),
|
label: get(item, labelField),
|
||||||
value: numberToString ? `${value}` : value,
|
value: numberToString ? `${value}` : value,
|
||||||
|
disabled: get(item, disabledField),
|
||||||
...(childrenField && item[childrenField]
|
...(childrenField && item[childrenField]
|
||||||
? { children: transformData(item[childrenField]) }
|
? { children: transformData(item[childrenField]) }
|
||||||
: {}),
|
: {}),
|
||||||
|
|||||||
@@ -15,5 +15,6 @@ export { default as GlobalShortcutKeys } from './shortcut-keys/global.vue';
|
|||||||
export { default as SwitchItem } from './switch-item.vue';
|
export { default as SwitchItem } from './switch-item.vue';
|
||||||
export { default as BuiltinTheme } from './theme/builtin.vue';
|
export { default as BuiltinTheme } from './theme/builtin.vue';
|
||||||
export { default as ColorMode } from './theme/color-mode.vue';
|
export { default as ColorMode } from './theme/color-mode.vue';
|
||||||
|
export { default as FontSize } from './theme/font-size.vue';
|
||||||
export { default as Radius } from './theme/radius.vue';
|
export { default as Radius } from './theme/radius.vue';
|
||||||
export { default as Theme } from './theme/theme.vue';
|
export { default as Theme } from './theme/theme.vue';
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import {
|
||||||
|
NumberField,
|
||||||
|
NumberFieldContent,
|
||||||
|
NumberFieldDecrement,
|
||||||
|
NumberFieldIncrement,
|
||||||
|
NumberFieldInput,
|
||||||
|
} from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'PreferenceFontSize',
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelValue = defineModel<number>({
|
||||||
|
default: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
const min = 15;
|
||||||
|
const max = 22;
|
||||||
|
const step = 1;
|
||||||
|
|
||||||
|
// 限制输入值在 min 和 max 之间
|
||||||
|
watch(
|
||||||
|
modelValue,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue < min) {
|
||||||
|
modelValue.value = min;
|
||||||
|
} else if (newValue > max) {
|
||||||
|
modelValue.value = max;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex w-full flex-col gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<NumberField
|
||||||
|
v-model="modelValue"
|
||||||
|
:max="max"
|
||||||
|
:min="min"
|
||||||
|
:step="step"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<NumberFieldContent>
|
||||||
|
<NumberFieldDecrement />
|
||||||
|
<NumberFieldInput />
|
||||||
|
<NumberFieldIncrement />
|
||||||
|
</NumberFieldContent>
|
||||||
|
</NumberField>
|
||||||
|
<span class="text-muted-foreground whitespace-nowrap text-xs">px</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground text-xs">
|
||||||
|
{{ $t('preferences.theme.fontSizeTip') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
ColorMode,
|
ColorMode,
|
||||||
Content,
|
Content,
|
||||||
Copyright,
|
Copyright,
|
||||||
|
FontSize,
|
||||||
Footer,
|
Footer,
|
||||||
General,
|
General,
|
||||||
GlobalShortcutKeys,
|
GlobalShortcutKeys,
|
||||||
@@ -85,6 +86,7 @@ const themeColorPrimary = defineModel<string>('themeColorPrimary');
|
|||||||
const themeBuiltinType = defineModel<BuiltinThemeType>('themeBuiltinType');
|
const themeBuiltinType = defineModel<BuiltinThemeType>('themeBuiltinType');
|
||||||
const themeMode = defineModel<ThemeModeType>('themeMode');
|
const themeMode = defineModel<ThemeModeType>('themeMode');
|
||||||
const themeRadius = defineModel<string>('themeRadius');
|
const themeRadius = defineModel<string>('themeRadius');
|
||||||
|
const themeFontSize = defineModel<number>('themeFontSize');
|
||||||
const themeSemiDarkSidebar = defineModel<boolean>('themeSemiDarkSidebar');
|
const themeSemiDarkSidebar = defineModel<boolean>('themeSemiDarkSidebar');
|
||||||
const themeSemiDarkHeader = defineModel<boolean>('themeSemiDarkHeader');
|
const themeSemiDarkHeader = defineModel<boolean>('themeSemiDarkHeader');
|
||||||
|
|
||||||
@@ -328,6 +330,9 @@ async function handleReset() {
|
|||||||
<Block :title="$t('preferences.theme.radius')">
|
<Block :title="$t('preferences.theme.radius')">
|
||||||
<Radius v-model="themeRadius" />
|
<Radius v-model="themeRadius" />
|
||||||
</Block>
|
</Block>
|
||||||
|
<Block :title="$t('preferences.theme.fontSize')">
|
||||||
|
<FontSize v-model="themeFontSize" />
|
||||||
|
</Block>
|
||||||
<Block :title="$t('preferences.other')">
|
<Block :title="$t('preferences.other')">
|
||||||
<ColorMode
|
<ColorMode
|
||||||
v-model:app-color-gray-mode="appColorGrayMode"
|
v-model:app-color-gray-mode="appColorGrayMode"
|
||||||
|
|||||||
@@ -13,10 +13,28 @@ function parseSvg(svgData: string): IconifyIconStructure {
|
|||||||
const xmlDoc = parser.parseFromString(svgData, 'image/svg+xml');
|
const xmlDoc = parser.parseFromString(svgData, 'image/svg+xml');
|
||||||
const svgElement = xmlDoc.documentElement;
|
const svgElement = xmlDoc.documentElement;
|
||||||
|
|
||||||
|
// 提取 SVG 根元素的关键样式属性
|
||||||
|
const getAttrs = (el: Element, attrs: string[]) =>
|
||||||
|
attrs
|
||||||
|
.map((attr) =>
|
||||||
|
el.hasAttribute(attr) ? `${attr}="${el.getAttribute(attr)}"` : '',
|
||||||
|
)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
const rootAttrs = getAttrs(svgElement, [
|
||||||
|
'fill',
|
||||||
|
'stroke',
|
||||||
|
'fill-rule',
|
||||||
|
'stroke-width',
|
||||||
|
]);
|
||||||
|
|
||||||
const svgContent = [...svgElement.childNodes]
|
const svgContent = [...svgElement.childNodes]
|
||||||
.filter((node) => node.nodeType === Node.ELEMENT_NODE)
|
.filter((node) => node.nodeType === Node.ELEMENT_NODE)
|
||||||
.map((node) => new XMLSerializer().serializeToString(node))
|
.map((node) => new XMLSerializer().serializeToString(node))
|
||||||
.join('');
|
.join('');
|
||||||
|
// 若根有属性,用一个 g 标签包裹内容并继承属性
|
||||||
|
const body = rootAttrs ? `<g ${rootAttrs}>${svgContent}</g>` : svgContent;
|
||||||
|
|
||||||
const viewBoxValue = svgElement.getAttribute('viewBox') || '';
|
const viewBoxValue = svgElement.getAttribute('viewBox') || '';
|
||||||
const [left, top, width, height] = viewBoxValue.split(' ').map((val) => {
|
const [left, top, width, height] = viewBoxValue.split(' ').map((val) => {
|
||||||
@@ -25,7 +43,7 @@ function parseSvg(svgData: string): IconifyIconStructure {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
body: svgContent,
|
body,
|
||||||
height,
|
height,
|
||||||
left,
|
left,
|
||||||
top,
|
top,
|
||||||
|
|||||||
@@ -120,6 +120,8 @@
|
|||||||
"theme": {
|
"theme": {
|
||||||
"title": "Theme",
|
"title": "Theme",
|
||||||
"radius": "Radius",
|
"radius": "Radius",
|
||||||
|
"fontSize": "Font Size",
|
||||||
|
"fontSizeTip": "Adjust global font size with real-time preview",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"darkSidebar": "Semi Dark Sidebar",
|
"darkSidebar": "Semi Dark Sidebar",
|
||||||
|
|||||||
@@ -120,6 +120,8 @@
|
|||||||
"theme": {
|
"theme": {
|
||||||
"title": "主题",
|
"title": "主题",
|
||||||
"radius": "圆角",
|
"radius": "圆角",
|
||||||
|
"fontSize": "字体大小",
|
||||||
|
"fontSizeTip": "调整全局字体大小,实时预览效果",
|
||||||
"light": "浅色",
|
"light": "浅色",
|
||||||
"dark": "深色",
|
"dark": "深色",
|
||||||
"darkSidebar": "深色侧边栏",
|
"darkSidebar": "深色侧边栏",
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ import type {
|
|||||||
RouteRecordStringComponent,
|
RouteRecordStringComponent,
|
||||||
} from '@vben-core/typings';
|
} from '@vben-core/typings';
|
||||||
|
|
||||||
import { filterTree, isHttpUrl, mapTree } from '@vben-core/shared/utils';
|
import {
|
||||||
|
filterTree,
|
||||||
|
isHttpUrl,
|
||||||
|
mapTree,
|
||||||
|
sortTree,
|
||||||
|
} from '@vben-core/shared/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据 routes 生成菜单列表
|
* 根据 routes 生成菜单列表
|
||||||
@@ -83,7 +88,7 @@ function generateMenus(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 对菜单进行排序,避免order=0时被替换成999的问题
|
// 对菜单进行排序,避免order=0时被替换成999的问题
|
||||||
menus = menus.toSorted((a, b) => (a?.order ?? 999) - (b?.order ?? 999));
|
menus = sortTree(menus, (a, b) => (a?.order ?? 999) - (b?.order ?? 999));
|
||||||
|
|
||||||
// 过滤掉隐藏的菜单项
|
// 过滤掉隐藏的菜单项
|
||||||
return filterTree(menus, (menu) => !!menu.show);
|
return filterTree(menus, (menu) => !!menu.show);
|
||||||
@@ -111,7 +116,7 @@ function convertServerMenuToRouteRecordStringComponent(
|
|||||||
hideInMenu: !menu.visible,
|
hideInMenu: !menu.visible,
|
||||||
icon: menu.icon,
|
icon: menu.icon,
|
||||||
link: menu.path,
|
link: menu.path,
|
||||||
orderNo: menu.sort,
|
order: menu.sort,
|
||||||
title: menu.name,
|
title: menu.name,
|
||||||
},
|
},
|
||||||
name: menu.name,
|
name: menu.name,
|
||||||
@@ -155,7 +160,7 @@ function convertServerMenuToRouteRecordStringComponent(
|
|||||||
hideInMenu: !menu.visible,
|
hideInMenu: !menu.visible,
|
||||||
icon: menu.icon,
|
icon: menu.icon,
|
||||||
keepAlive: menu.keepAlive,
|
keepAlive: menu.keepAlive,
|
||||||
orderNo: menu.sort,
|
order: menu.sort,
|
||||||
title: menu.name,
|
title: menu.name,
|
||||||
},
|
},
|
||||||
name: finalName,
|
name: finalName,
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { BasicOption } from '@vben/types';
|
|
||||||
|
|
||||||
import type { VbenFormSchema } from '#/adapter/form';
|
|
||||||
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
|
||||||
|
|
||||||
import { ProfileBaseSetting } from '@vben/common-ui';
|
|
||||||
|
|
||||||
import { getUserInfoApi } from '#/api';
|
|
||||||
|
|
||||||
const profileBaseSettingRef = ref();
|
|
||||||
|
|
||||||
const MOCK_ROLES_OPTIONS: BasicOption[] = [
|
|
||||||
{
|
|
||||||
label: '管理员',
|
|
||||||
value: 'super',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '用户',
|
|
||||||
value: 'user',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '测试',
|
|
||||||
value: 'test',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const formSchema = computed((): VbenFormSchema[] => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
fieldName: 'realName',
|
|
||||||
component: 'Input',
|
|
||||||
label: '姓名',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'username',
|
|
||||||
component: 'Input',
|
|
||||||
label: '用户名',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'roles',
|
|
||||||
component: 'Select',
|
|
||||||
componentProps: {
|
|
||||||
mode: 'tags',
|
|
||||||
options: MOCK_ROLES_OPTIONS,
|
|
||||||
},
|
|
||||||
label: '角色',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'introduction',
|
|
||||||
component: 'Textarea',
|
|
||||||
label: '个人简介',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const data = await getUserInfoApi();
|
|
||||||
profileBaseSettingRef.value.getFormApi().setValues(data);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<ProfileBaseSetting ref="profileBaseSettingRef" :form-schema="formSchema" />
|
|
||||||
</template>
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
import { Profile } from '@vben/common-ui';
|
|
||||||
import { useUserStore } from '@vben/stores';
|
|
||||||
|
|
||||||
import ProfileBase from './base-setting.vue';
|
|
||||||
import ProfileNotificationSetting from './notification-setting.vue';
|
|
||||||
import ProfilePasswordSetting from './password-setting.vue';
|
|
||||||
import ProfileSecuritySetting from './security-setting.vue';
|
|
||||||
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
const tabsValue = ref<string>('basic');
|
|
||||||
|
|
||||||
const tabs = ref([
|
|
||||||
{
|
|
||||||
label: '基本设置',
|
|
||||||
value: 'basic',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '安全设置',
|
|
||||||
value: 'security',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '修改密码',
|
|
||||||
value: 'password',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '新消息提醒',
|
|
||||||
value: 'notice',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<Profile
|
|
||||||
v-model:model-value="tabsValue"
|
|
||||||
title="个人中心"
|
|
||||||
:user-info="userStore.userInfo"
|
|
||||||
:tabs="tabs"
|
|
||||||
>
|
|
||||||
<template #content>
|
|
||||||
<ProfileBase v-if="tabsValue === 'basic'" />
|
|
||||||
<ProfileSecuritySetting v-if="tabsValue === 'security'" />
|
|
||||||
<ProfilePasswordSetting v-if="tabsValue === 'password'" />
|
|
||||||
<ProfileNotificationSetting v-if="tabsValue === 'notice'" />
|
|
||||||
</template>
|
|
||||||
</Profile>
|
|
||||||
</template>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
import { ProfileNotificationSetting } from '@vben/common-ui';
|
|
||||||
|
|
||||||
const formSchema = computed(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
value: true,
|
|
||||||
fieldName: 'accountPassword',
|
|
||||||
label: '账户密码',
|
|
||||||
description: '其他用户的消息将以站内信的形式通知',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: true,
|
|
||||||
fieldName: 'systemMessage',
|
|
||||||
label: '系统消息',
|
|
||||||
description: '系统消息将以站内信的形式通知',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: true,
|
|
||||||
fieldName: 'todoTask',
|
|
||||||
label: '待办任务',
|
|
||||||
description: '待办任务将以站内信的形式通知',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<ProfileNotificationSetting :form-schema="formSchema" />
|
|
||||||
</template>
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { VbenFormSchema } from '#/adapter/form';
|
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
|
|
||||||
import { ProfilePasswordSetting, z } from '@vben/common-ui';
|
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
|
||||||
|
|
||||||
const profilePasswordSettingRef = ref();
|
|
||||||
|
|
||||||
const formSchema = computed((): VbenFormSchema[] => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
fieldName: 'oldPassword',
|
|
||||||
label: '旧密码',
|
|
||||||
component: 'VbenInputPassword',
|
|
||||||
componentProps: {
|
|
||||||
placeholder: '请输入旧密码',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'newPassword',
|
|
||||||
label: '新密码',
|
|
||||||
component: 'VbenInputPassword',
|
|
||||||
componentProps: {
|
|
||||||
passwordStrength: true,
|
|
||||||
placeholder: '请输入新密码',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'confirmPassword',
|
|
||||||
label: '确认密码',
|
|
||||||
component: 'VbenInputPassword',
|
|
||||||
componentProps: {
|
|
||||||
passwordStrength: true,
|
|
||||||
placeholder: '请再次输入新密码',
|
|
||||||
},
|
|
||||||
dependencies: {
|
|
||||||
rules(values) {
|
|
||||||
const { newPassword } = values;
|
|
||||||
return z
|
|
||||||
.string({ required_error: '请再次输入新密码' })
|
|
||||||
.min(1, { message: '请再次输入新密码' })
|
|
||||||
.refine((value) => value === newPassword, {
|
|
||||||
message: '两次输入的密码不一致',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
triggerFields: ['newPassword'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleSubmit() {
|
|
||||||
message.success('密码修改成功');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<ProfilePasswordSetting
|
|
||||||
ref="profilePasswordSettingRef"
|
|
||||||
class="w-1/3"
|
|
||||||
:form-schema="formSchema"
|
|
||||||
@submit="handleSubmit"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
import { ProfileSecuritySetting } from '@vben/common-ui';
|
|
||||||
|
|
||||||
const formSchema = computed(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
value: true,
|
|
||||||
fieldName: 'accountPassword',
|
|
||||||
label: '账户密码',
|
|
||||||
description: '当前密码强度:强',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: true,
|
|
||||||
fieldName: 'securityPhone',
|
|
||||||
label: '密保手机',
|
|
||||||
description: '已绑定手机:138****8293',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: true,
|
|
||||||
fieldName: 'securityQuestion',
|
|
||||||
label: '密保问题',
|
|
||||||
description: '未设置密保问题,密保问题可有效保护账户安全',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: true,
|
|
||||||
fieldName: 'securityEmail',
|
|
||||||
label: '备用邮箱',
|
|
||||||
description: '已绑定邮箱:ant***sign.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: false,
|
|
||||||
fieldName: 'securityMfa',
|
|
||||||
label: 'MFA 设备',
|
|
||||||
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<ProfileSecuritySetting :form-schema="formSchema" />
|
|
||||||
</template>
|
|
||||||
3860
pnpm-lock.yaml
generated
3860
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user