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;
|
||||
Reference in New Issue
Block a user