chore: 初始开源发布
iotlcm-admin-ui:管理前端(基于 Vben Admin 5.5.9 二次开发)。 - 天地图 API Key → YOUR_TIANDITU_KEY - OAuth 客户端 ID → your-client-id,store 加密密钥 → 提示自行替换 - Makefile 移除明文 registry 登录口令,镜像地址改 - appIcon 指向内网图床的链接 → /favicon.ico - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位) - 真实邮箱 → example.com(署名头内的上游作者邮箱保留) - 移除 CI 触发器 - 补 MIT LICENSE(含 Vben Admin 上游声明)+ README - 统一 LF 行尾并加 .gitattributes 上游 Vben Admin 的版权与许可声明完整保留(各源文件 @Author/@Copyright 头 + LICENSE)。
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
/**
|
||||
* Ant Design Vue 基础组件统一在这里异步引入。
|
||||
*
|
||||
* adapter 层只关心组件映射关系,避免把具体 UI 库的 import 细节散落到注册文件中。
|
||||
*/
|
||||
export const AutoComplete = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/auto-complete'),
|
||||
);
|
||||
|
||||
export const Button = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/button'),
|
||||
);
|
||||
|
||||
export const Checkbox = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/checkbox'),
|
||||
);
|
||||
|
||||
export const CheckboxGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
|
||||
);
|
||||
|
||||
export const DatePicker = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/date-picker'),
|
||||
);
|
||||
|
||||
export const Divider = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/divider'),
|
||||
);
|
||||
|
||||
export const Image = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/image'),
|
||||
);
|
||||
|
||||
export const Input = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/input'),
|
||||
);
|
||||
|
||||
export const InputNumber = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/input-number'),
|
||||
);
|
||||
|
||||
export const InputPassword = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/input').then((res) => res.InputPassword),
|
||||
);
|
||||
|
||||
export const Mentions = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/mentions'),
|
||||
);
|
||||
|
||||
export const PreviewGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup),
|
||||
);
|
||||
|
||||
export const Radio = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/radio'),
|
||||
);
|
||||
|
||||
export const RadioGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
|
||||
);
|
||||
|
||||
export const RangePicker = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
|
||||
);
|
||||
|
||||
export const Rate = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/rate'),
|
||||
);
|
||||
|
||||
export const Select = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/select'),
|
||||
);
|
||||
|
||||
export const Space = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/space'),
|
||||
);
|
||||
|
||||
export const Switch = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/switch'),
|
||||
);
|
||||
|
||||
export const Textarea = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/input').then((res) => res.Textarea),
|
||||
);
|
||||
|
||||
export const TimePicker = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/time-picker'),
|
||||
);
|
||||
|
||||
export const TreeSelect = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/tree-select'),
|
||||
);
|
||||
|
||||
export const Cascader = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/cascader'),
|
||||
);
|
||||
|
||||
export const Upload = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/upload'),
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
import { globalShareState } from '@vben/common-ui';
|
||||
|
||||
import { notification } from 'ant-design-vue';
|
||||
|
||||
/**
|
||||
* 框架共享消息适配。
|
||||
*
|
||||
* common-ui 内部只声明消息场景,这里绑定到当前 UI 库的 notification 实现。
|
||||
*/
|
||||
export function defineComponentAdapterMessage() {
|
||||
globalShareState.defineMessage({
|
||||
copyPreferencesSuccess: (title, content) => {
|
||||
notification.success({
|
||||
description: content,
|
||||
message: title,
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import type { UploadFile, UploadProps } from 'ant-design-vue';
|
||||
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { h, render, unref } from 'vue';
|
||||
|
||||
import { Image, PreviewGroup } from '../antd';
|
||||
|
||||
/**
|
||||
* UploadFile 没有稳定的图片标识字段时,通过 url/name/type 组合判断是否走图片预览。
|
||||
*/
|
||||
function 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/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地待上传图片没有 url 时,需要转成 base64 才能进入 ImagePreviewGroup。
|
||||
*/
|
||||
function 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));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 Ant Design Vue ImagePreviewGroup 创建一次性预览容器。
|
||||
*
|
||||
* 预览关闭后会卸载并移除临时 DOM,避免多次预览后残留节点。
|
||||
*/
|
||||
export const previewImage = async (
|
||||
file: UploadFile,
|
||||
visible: Ref<boolean>,
|
||||
fileList: Ref<UploadProps['fileList']>,
|
||||
) => {
|
||||
// 非图片沿用文件打开行为,不进入图片预览组
|
||||
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;
|
||||
}
|
||||
|
||||
// Image 与 PreviewGroup 只有预览时才加载,保持基础表单渲染轻量
|
||||
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
|
||||
Image,
|
||||
PreviewGroup,
|
||||
]);
|
||||
|
||||
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);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { defineComponent, h, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
/**
|
||||
* 为输入类/选择类组件补齐统一占位文案,并保留原组件暴露的方法。
|
||||
*/
|
||||
export const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
componentProps: Recordable<any> = {},
|
||||
) => {
|
||||
return defineComponent({
|
||||
name: component.name,
|
||||
inheritAttrs: false,
|
||||
setup: (props: any, { attrs, expose, slots }) => {
|
||||
const placeholder =
|
||||
props?.placeholder ||
|
||||
attrs?.placeholder ||
|
||||
$t(`ui.placeholder.${type}`);
|
||||
const innerRef = ref();
|
||||
|
||||
// 透传内部组件实例方法,避免包装后影响外部通过 ref 调用 focus 等能力
|
||||
expose(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => innerRef.value?.[key],
|
||||
has: (_target, key) => key in (innerRef.value || {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
|
||||
slots,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import type {
|
||||
UploadChangeParam,
|
||||
UploadFile,
|
||||
UploadProps,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { defineComponent, h, ref, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { Button, Upload } from '../antd';
|
||||
import { previewImage } from './upload-preview';
|
||||
|
||||
/**
|
||||
* Upload 未提供自定义插槽时,补齐项目统一的默认上传按钮。
|
||||
*/
|
||||
function 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,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 适配 Ant Design Vue Upload 到 Vben Form:
|
||||
* - 使用 modelValue 与表单值同步
|
||||
* - 上传列表为空时回写 undefined
|
||||
* - 内置图片预览,非图片文件直接打开地址
|
||||
*/
|
||||
export 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;
|
||||
};
|
||||
|
||||
// 监听 formApi.setValues 等外部回填,保持 Upload 内部 fileList 同步
|
||||
watch(
|
||||
() => attrs.modelValue,
|
||||
(res) => {
|
||||
fileList.value = res;
|
||||
},
|
||||
);
|
||||
|
||||
return () =>
|
||||
h(
|
||||
Upload,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
fileList: fileList.value,
|
||||
onChange: handleChange,
|
||||
onPreview: handlePreview,
|
||||
},
|
||||
renderUploadButton(),
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { ComponentType } from './types';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
|
||||
import { HtcRegisterComponent } from '#/components/HtCustom/FormExpand/register';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { FileUpload, ImageUpload } from '#/components/upload';
|
||||
|
||||
import {
|
||||
AutoComplete,
|
||||
Button,
|
||||
Cascader,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Input,
|
||||
InputNumber,
|
||||
InputPassword,
|
||||
Mentions,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Textarea,
|
||||
TimePicker,
|
||||
TreeSelect,
|
||||
} from './antd';
|
||||
import { defineComponentAdapterMessage } from './helpers/message';
|
||||
import { withDefaultPlaceholder } from './helpers/with-default-placeholder';
|
||||
import { withPreviewUpload } from './helpers/with-preview-upload';
|
||||
|
||||
/**
|
||||
* 通用组件适配层。
|
||||
*
|
||||
* 这里只维护“组件名称 -> 真实组件”的映射关系,供 vben-form、vben-modal、
|
||||
* vben-drawer 等框架组件通过 globalShareState 复用。
|
||||
*/
|
||||
function createComponentAdapterMap(): Partial<
|
||||
Record<ComponentType, Component>
|
||||
> {
|
||||
return {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: Cascader,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
}),
|
||||
ApiSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
visibleEvent: 'onDropdownVisibleChange',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
),
|
||||
ApiTreeSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiTreeSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: TreeSelect,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
),
|
||||
AutoComplete,
|
||||
Cascader,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
// 弹窗、抽屉等框架组件会通过共享状态读取这两个按钮组件
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, type: 'default' }, slots);
|
||||
},
|
||||
Divider,
|
||||
FileUpload,
|
||||
ImageUpload,
|
||||
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
|
||||
iconSlot: 'addonAfter',
|
||||
inputComponent: Input,
|
||||
modelValueProp: 'value',
|
||||
}),
|
||||
Input: withDefaultPlaceholder(Input, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||
Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
||||
PrimaryButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
RichTextarea,
|
||||
Select: withDefaultPlaceholder(Select, 'select'),
|
||||
// SelectDivision,
|
||||
Space,
|
||||
Switch,
|
||||
Textarea: withDefaultPlaceholder(Textarea, 'input'),
|
||||
TimePicker,
|
||||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||
Upload: withPreviewUpload(),
|
||||
...HtcRegisterComponent,
|
||||
};
|
||||
}
|
||||
|
||||
async function initComponentAdapter() {
|
||||
// 先注册组件,再初始化 form 适配,确保 setupVbenForm 能读到完整组件表
|
||||
globalShareState.setComponents(createComponentAdapterMap());
|
||||
defineComponentAdapterMessage();
|
||||
}
|
||||
|
||||
export { initComponentAdapter };
|
||||
export type { ComponentType };
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { SchemaComponentType } from './types';
|
||||
|
||||
import type { HtcComponentPropsMap } from '#/components/HtCustom/FormExpand/register';
|
||||
|
||||
type ComponentPropsGetter<Props> = (
|
||||
value: Partial<Record<string, any>>,
|
||||
actions: any,
|
||||
) => Props;
|
||||
|
||||
type ComponentPropsMap = HtcComponentPropsMap;
|
||||
|
||||
type UntypedComponentProps =
|
||||
| ComponentPropsGetter<Record<string, any>>
|
||||
| Record<string, any>;
|
||||
|
||||
export type ComponentProps<T extends SchemaComponentType> =
|
||||
T extends keyof ComponentPropsMap
|
||||
? ComponentPropsGetter<ComponentPropsMap[T]> | ComponentPropsMap[T]
|
||||
: UntypedComponentProps;
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
|
||||
import type { HtcRegisterComponentName } from '#/components/HtCustom/FormExpand/register';
|
||||
|
||||
/**
|
||||
* 表单 schema 中 component 字段允许使用的组件名称。
|
||||
*
|
||||
* 新增适配组件时,需要同时在这里补充名称,并在 index.ts 中注册真实组件。
|
||||
*/
|
||||
export type AdapterComponentType =
|
||||
| 'ApiCascader'
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'AutoComplete'
|
||||
| 'Cascader'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'DefaultButton'
|
||||
| 'Divider'
|
||||
| 'FileUpload'
|
||||
| 'HtcFormSwitch'
|
||||
| 'IconPicker'
|
||||
| 'ImageUpload'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'InputPassword'
|
||||
| 'Mentions'
|
||||
| 'PrimaryButton'
|
||||
| 'Radio'
|
||||
| 'RadioGroup'
|
||||
| 'RangePicker'
|
||||
| 'Rate'
|
||||
| 'RichTextarea'
|
||||
| 'Select'
|
||||
| 'SelectDivision'
|
||||
| 'Space'
|
||||
| 'Switch'
|
||||
| 'Textarea'
|
||||
| 'TimePicker'
|
||||
| 'TreeSelect'
|
||||
| 'Upload';
|
||||
|
||||
export type BaseComponentType =
|
||||
| 'DefaultButton'
|
||||
| 'PrimaryButton'
|
||||
| 'VbenCheckbox'
|
||||
| 'VbenInput'
|
||||
| 'VbenInputPassword'
|
||||
| 'VbenPinInput'
|
||||
| 'VbenSelect';
|
||||
|
||||
export type SchemaComponentType =
|
||||
| AdapterComponentType
|
||||
| BaseComponentType
|
||||
| HtcRegisterComponentName;
|
||||
|
||||
export type ComponentType =
|
||||
| AdapterComponentType
|
||||
| BaseFormComponentType
|
||||
| HtcRegisterComponentName;
|
||||
@@ -0,0 +1,68 @@
|
||||
import type {
|
||||
VbenFormProps as BaseVbenFormProps,
|
||||
VbenFormSchema as FormSchema,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import type { ComponentType } from './component';
|
||||
import type { ComponentProps } from './component/props';
|
||||
import type { SchemaComponentType } from './component/types';
|
||||
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
async function initSetupVbenForm() {
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// ant design vue组件库默认都是 v-model:value
|
||||
baseModelPropName: 'value',
|
||||
|
||||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
RichTextarea: 'modelValue',
|
||||
Switch: 'checked',
|
||||
Upload: 'fileList',
|
||||
ImageUpload: 'value',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
// 输入项目必填国际化适配
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 选择项目必填国际化适配
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type TypedFormSchema<T extends SchemaComponentType> = Omit<
|
||||
FormSchema<T>,
|
||||
'component' | 'componentProps'
|
||||
> & {
|
||||
component: T;
|
||||
componentProps?: ComponentProps<T>;
|
||||
};
|
||||
|
||||
export type VbenFormSchema = {
|
||||
[K in SchemaComponentType]: TypedFormSchema<K>;
|
||||
}[SchemaComponentType];
|
||||
|
||||
export type VbenFormProps = Omit<BaseVbenFormProps<ComponentType>, 'schema'> & {
|
||||
schema?: VbenFormSchema[];
|
||||
};
|
||||
|
||||
const useVbenForm = (options: VbenFormProps) => {
|
||||
return useForm<ComponentType>(options as BaseVbenFormProps<ComponentType>);
|
||||
};
|
||||
|
||||
export { initSetupVbenForm, useVbenForm, z };
|
||||
@@ -0,0 +1,124 @@
|
||||
/* eslint-disable vue/no-side-effects-in-computed-properties */
|
||||
import { computed, defineComponent, h, ref, watch } from 'vue';
|
||||
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
|
||||
const component = defineComponent({
|
||||
props: {
|
||||
code: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
autodetect: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
ignoreIllegals: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const language = ref(props.language);
|
||||
const ignoreIllegals = ref(props.ignoreIllegals);
|
||||
watch(
|
||||
() => props.language,
|
||||
(newLanguage) => {
|
||||
language.value = newLanguage;
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => props.ignoreIllegals,
|
||||
(newIgnoreIllegals) => {
|
||||
ignoreIllegals.value = newIgnoreIllegals;
|
||||
},
|
||||
);
|
||||
|
||||
const autodetect = computed(() => props.autodetect || !language.value);
|
||||
const cannotDetectLanguage = computed(
|
||||
() => !autodetect.value && !hljs.getLanguage(language.value),
|
||||
);
|
||||
|
||||
const className = computed((): string => {
|
||||
return cannotDetectLanguage.value ? '' : `hljs ${language.value}`;
|
||||
});
|
||||
|
||||
const highlightedCode = computed((): string => {
|
||||
// No idea what language to use, return raw code
|
||||
if (cannotDetectLanguage.value) {
|
||||
console.warn(
|
||||
`The language "${language.value}" you specified could not be found.`,
|
||||
);
|
||||
return props.code;
|
||||
}
|
||||
|
||||
if (autodetect.value) {
|
||||
const result = hljs.highlightAuto(props.code);
|
||||
language.value = result.language ?? '';
|
||||
return result.value;
|
||||
} else {
|
||||
const result = hljs.highlight(props.code, {
|
||||
language: language.value,
|
||||
ignoreIllegals: props.ignoreIllegals,
|
||||
});
|
||||
return result.value;
|
||||
}
|
||||
});
|
||||
// const saveSelection = (e: any) => {
|
||||
// const selection = window.getSelection();
|
||||
// if (selection && selection.rangeCount && selection.rangeCount > 0) {
|
||||
// const range = selection.getRangeAt(0);
|
||||
// return range;
|
||||
// }
|
||||
// return null;
|
||||
// };
|
||||
// const restoreSelection = (range: any, el: any) => {
|
||||
// if (range) {
|
||||
// const selection = window.getSelection();
|
||||
// if (selection) {
|
||||
// selection.removeAllRanges();
|
||||
// selection.addRange(range);
|
||||
// console.log('selection range', range);
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
const updateContent = (e: any) => {
|
||||
// const range = saveSelection(e)
|
||||
const result = hljs.highlight(e.target.innerText, {
|
||||
language: language.value,
|
||||
ignoreIllegals: props.ignoreIllegals,
|
||||
});
|
||||
e.target.innerHTML = result.value;
|
||||
emit('update:code', e.target.innerText);
|
||||
// setTimeout(() => {
|
||||
// restoreSelection(range, e.target)
|
||||
// }, 0)
|
||||
};
|
||||
return {
|
||||
className,
|
||||
highlightedCode,
|
||||
updateContent,
|
||||
emit,
|
||||
};
|
||||
},
|
||||
render() {
|
||||
const updateContent = this.updateContent;
|
||||
return h('pre', {}, [
|
||||
h('code', {
|
||||
class: this.className,
|
||||
innerHTML: this.highlightedCode,
|
||||
contenteditable: true,
|
||||
// onInput(e: any) {},
|
||||
onBlur(e: any) {
|
||||
updateContent(e);
|
||||
},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
export default component;
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { Button, Image } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from './form';
|
||||
|
||||
setupVbenVxeTable({
|
||||
configVxeTable: (vxeUI) => {
|
||||
vxeUI.setConfig({
|
||||
grid: {
|
||||
align: 'center',
|
||||
border: false,
|
||||
columnConfig: {
|
||||
resizable: true,
|
||||
},
|
||||
minHeight: 180,
|
||||
formConfig: {
|
||||
// 全局禁用vxe-table的表单配置,使用formOptions
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
autoLoad: true,
|
||||
response: {
|
||||
result: 'items',
|
||||
total: 'total',
|
||||
list: 'items',
|
||||
},
|
||||
showActiveMsg: true,
|
||||
showResponseMsg: false,
|
||||
},
|
||||
round: true,
|
||||
showOverflow: true,
|
||||
size: 'small',
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderTableDefault(renderOpts, params) {
|
||||
const { props } = renderOpts;
|
||||
const { column, row } = params;
|
||||
return h(Image, { src: row[column.field], ...props });
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderTableDefault(renderOpts) {
|
||||
const { props } = renderOpts;
|
||||
return h(
|
||||
Button,
|
||||
{ size: 'small', type: 'link' },
|
||||
{ default: () => props?.text },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||
// vxeUI.formats.add
|
||||
},
|
||||
useVbenForm,
|
||||
});
|
||||
|
||||
export { useVbenVxeGrid };
|
||||
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
Reference in New Issue
Block a user