feat: 添加 PFileUpload 组件及相关类型定义,支持文件上传功能
This commit is contained in:
5
.npmrc
5
.npmrc
@ -1 +1,4 @@
|
|||||||
shamefully-hoist=true
|
registry=https://nexus.oo1.dev/repository/npm/
|
||||||
|
use-node-version=22.12.0
|
||||||
|
|
||||||
|
shamefully-hoist=true
|
@ -16,6 +16,7 @@ import { addAsteriskPlugin } from './formkit.config.plugin.addAsteriskPlugin';
|
|||||||
import { debugPlugin } from './formkit.config.plugin.debug';
|
import { debugPlugin } from './formkit.config.plugin.debug';
|
||||||
import { PDatePicker } from '@/__fk-inputs__/inputs/p-date-picker';
|
import { PDatePicker } from '@/__fk-inputs__/inputs/p-date-picker';
|
||||||
import { PCascadeSelect } from '@/__fk-inputs__/inputs/p-cascade-select';
|
import { PCascadeSelect } from '@/__fk-inputs__/inputs/p-cascade-select';
|
||||||
|
import { PFileUpload } from '@/__fk-inputs__/inputs/p-file-upload';
|
||||||
|
|
||||||
const plugins: FormKitPlugin[] = [
|
const plugins: FormKitPlugin[] = [
|
||||||
// createLibraryPlugin(fkLibrary),
|
// createLibraryPlugin(fkLibrary),
|
||||||
@ -30,6 +31,7 @@ const plugins: FormKitPlugin[] = [
|
|||||||
PSelect,
|
PSelect,
|
||||||
PDatePicker,
|
PDatePicker,
|
||||||
PCascadeSelect,
|
PCascadeSelect,
|
||||||
|
PFileUpload,
|
||||||
}),
|
}),
|
||||||
// createLibraryPlugin(
|
// createLibraryPlugin(
|
||||||
// {
|
// {
|
||||||
|
13
package.json
13
package.json
@ -16,18 +16,19 @@
|
|||||||
"@formkit/icons": "^1.6.9",
|
"@formkit/icons": "^1.6.9",
|
||||||
"@formkit/pro": "^0.127.15",
|
"@formkit/pro": "^0.127.15",
|
||||||
"@formkit/themes": "^1.6.9",
|
"@formkit/themes": "^1.6.9",
|
||||||
"@formkit/vue": "^1.6.9",
|
"@formkit/vue": "1.6.10-fix.202412201106",
|
||||||
"@formkit/zod": "^1.6.9",
|
"@formkit/zod": "^1.6.9",
|
||||||
"@headlessui/vue": "^1.7.23",
|
"@headlessui/vue": "^1.7.23",
|
||||||
"@primevue/forms": "^4.2.5",
|
"@primevue/forms": "^4.2.5",
|
||||||
"@primevue/themes": "^4.2.5",
|
"@primevue/themes": "^4.2.5",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"axios": "^1.7.9",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"i18next": "^24.2.0",
|
"i18next": "^24.2.0",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primelocale": "^1.2.2",
|
"primelocale": "^1.2.2",
|
||||||
"primevue": "^4.2.5",
|
"primevue": "4.2.6-fix.202412310330",
|
||||||
"sweetalert2": "^11.15.3",
|
"sweetalert2": "^11.15.3",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"utils4u": "^2.19.2",
|
"utils4u": "^2.19.2",
|
||||||
@ -39,9 +40,9 @@
|
|||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
"unocss": "^0.65.2",
|
"unocss": "^0.65.3",
|
||||||
"unplugin-vue-components": "^0.28.0",
|
"unplugin-vue-components": "^0.28.0",
|
||||||
"vite": "^6.0.5",
|
"vite": "^6.0.6",
|
||||||
"vue-tsc": "^2.1.10"
|
"vue-tsc": "^2.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
723
pnpm-lock.yaml
generated
723
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,74 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import TimesIcon from '@primevue/icons/times';
|
||||||
|
import type { FileUploadStatus } from './types';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { BadgeProps } from 'primevue/badge';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
url: string;
|
||||||
|
filename: string;
|
||||||
|
status?: FileUploadStatus;
|
||||||
|
progress?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
remove: () => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const bageValue = computed(() => {
|
||||||
|
switch (props.status) {
|
||||||
|
case 'pending':
|
||||||
|
return '待上传';
|
||||||
|
case 'uploading':
|
||||||
|
return '上传中';
|
||||||
|
case 'failed':
|
||||||
|
return '上传失败';
|
||||||
|
case 'uploaded':
|
||||||
|
return '已上传';
|
||||||
|
default:
|
||||||
|
return '未知状态';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const bageSeverity = computed<BadgeProps['severity']>(() => {
|
||||||
|
switch (props.status) {
|
||||||
|
case 'pending':
|
||||||
|
return 'info';
|
||||||
|
case 'uploading':
|
||||||
|
return 'warn';
|
||||||
|
case 'failed':
|
||||||
|
return 'danger';
|
||||||
|
case 'uploaded':
|
||||||
|
return 'success';
|
||||||
|
default:
|
||||||
|
return 'contrast';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="border p-2 rounded-md flex flex-wrap items-center gap-2">
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
:src="url"
|
||||||
|
class="w-10 h-10 shrink-0"
|
||||||
|
/>
|
||||||
|
<div class="break-all break-anywhere">{{ filename }}</div>
|
||||||
|
<Badge
|
||||||
|
:value="bageValue"
|
||||||
|
:severity="bageSeverity"
|
||||||
|
/>
|
||||||
|
<ProgressBar
|
||||||
|
v-if="status === 'uploading'"
|
||||||
|
:value="progress"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
text
|
||||||
|
:rounded="true"
|
||||||
|
severity="danger"
|
||||||
|
class="ml-auto"
|
||||||
|
@click="$emit('remove')"
|
||||||
|
>
|
||||||
|
<TimesIcon aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
202
src/__fk-inputs__/components/file-upload/file-upload.vue
Normal file
202
src/__fk-inputs__/components/file-upload/file-upload.vue
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormKitFrameworkContext } from '@formkit/core';
|
||||||
|
import FileUpload, { type FileUploadUploaderEvent } from 'primevue/fileupload';
|
||||||
|
import { computed, onMounted, useTemplateRef } from 'vue';
|
||||||
|
import FileUploadItem from './file-upload-item.vue';
|
||||||
|
import type { CustomRequest, FileExt, FileUploadInst, PropFilesToValue, PropValueToFiles } from './types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
context: FormKitFrameworkContext & {
|
||||||
|
fileLimit?: number;
|
||||||
|
maxFileSize?: number;
|
||||||
|
customRequest?: CustomRequest;
|
||||||
|
valueToFiles?: PropValueToFiles;
|
||||||
|
filesToValue?: PropFilesToValue;
|
||||||
|
autoUpload?: boolean;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
const formkitContext = props.context;
|
||||||
|
|
||||||
|
const customRequest = props.context.customRequest;
|
||||||
|
|
||||||
|
const fileUploadRef = useTemplateRef<FileUploadInst>('fileUploadRef');
|
||||||
|
|
||||||
|
const cmpt_disabled = computed(() => {
|
||||||
|
if (fileUploadRef.value) {
|
||||||
|
// 有上传失败的文件
|
||||||
|
if (fileUploadRef.value.uploadedFiles.some((f) => f.status === 'failed')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已上传文件数量超过限制(这里是大于*等于*)
|
||||||
|
if (fileUploadRef.value.uploadedFileCount >= (formkitContext.fileLimit ?? Infinity)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已上传和待上传文件数量超过限制(这里是*大于*)
|
||||||
|
const uploaded_and_pending_count = fileUploadRef.value.uploadedFileCount + fileUploadRef.value.files.length;
|
||||||
|
if (uploaded_and_pending_count > (formkitContext.fileLimit ?? Infinity)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const cmpt_showUploadButton = computed(() => {
|
||||||
|
if (fileUploadRef.value?.files?.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const changeModelValue = () => {
|
||||||
|
const uploadedFiles = fileUploadRef.value!.uploadedFiles.filter((f) => f.status === 'uploaded');
|
||||||
|
if (!formkitContext.filesToValue) {
|
||||||
|
console.warn('[FileUpload] filesToValue is not defined');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formkitContext.node.input(formkitContext.filesToValue(uploadedFiles));
|
||||||
|
};
|
||||||
|
const changeUploadedFiles = () => {
|
||||||
|
if (!formkitContext.valueToFiles) {
|
||||||
|
console.warn('[FileUpload] valueToFiles is not defined');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const files = formkitContext.valueToFiles(formkitContext._value);
|
||||||
|
fileUploadRef.value!.uploadedFiles = files.map((f) => ({
|
||||||
|
name: f.name,
|
||||||
|
url: f.url,
|
||||||
|
status: f.status || 'uploaded',
|
||||||
|
progress: f.progress || 100,
|
||||||
|
}));
|
||||||
|
fileUploadRef.value!.uploadedFileCount = files.length;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[FileUpload] valueToFiles error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
changeUploadedFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onUploader = (event: FileUploadUploaderEvent) => {
|
||||||
|
if (!customRequest) {
|
||||||
|
console.warn('[FileUpload] customRequest is not defined');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = event.files as FileExt[];
|
||||||
|
for (const file of files) {
|
||||||
|
fileUploadRef.value!.uploadedFiles.push({
|
||||||
|
rawFile: file,
|
||||||
|
name: file.name,
|
||||||
|
url: '',
|
||||||
|
status: 'uploading',
|
||||||
|
progress: 0,
|
||||||
|
});
|
||||||
|
const fileItem = fileUploadRef.value!.uploadedFiles[fileUploadRef.value!.uploadedFiles.length - 1];
|
||||||
|
customRequest({
|
||||||
|
file,
|
||||||
|
onProgress: (percent) => {
|
||||||
|
fileItem.progress = percent;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
fileItem.status = 'uploaded';
|
||||||
|
fileItem.url = result.url;
|
||||||
|
changeModelValue();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
fileItem.status = 'failed';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FileUpload
|
||||||
|
ref="fileUploadRef"
|
||||||
|
:auto="formkitContext.autoUpload"
|
||||||
|
:disabled="cmpt_disabled"
|
||||||
|
:showUploadButton="cmpt_showUploadButton"
|
||||||
|
:showCancelButton="false"
|
||||||
|
:customUpload="true"
|
||||||
|
mode="advanced"
|
||||||
|
:multiple="true"
|
||||||
|
accept="image/*"
|
||||||
|
:maxFileSize="formkitContext.maxFileSize"
|
||||||
|
invalidFileSizeMessage="文件 {0} 大小超过限制 {1}"
|
||||||
|
:fileLimit="formkitContext.fileLimit"
|
||||||
|
invalidFileLimitMessage="最多只能上传 {0} 个文件,请移除多余文件后点击上传"
|
||||||
|
@uploader="onUploader"
|
||||||
|
:chooseButtonProps="{ size: 'small' }"
|
||||||
|
:uploadButtonProps="{ size: 'small', severity: 'secondary' }"
|
||||||
|
:cancelButtonProps="{ size: 'small', severity: 'secondary' }"
|
||||||
|
>
|
||||||
|
<template #empty>
|
||||||
|
<Message
|
||||||
|
size="small"
|
||||||
|
severity="info"
|
||||||
|
>请上传图片</Message
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #content="{ messages, removeFileCallback, removeUploadedFileCallback }">
|
||||||
|
<Message
|
||||||
|
size="small"
|
||||||
|
v-for="msg of messages"
|
||||||
|
closable
|
||||||
|
@close="fileUploadRef!.messages = []"
|
||||||
|
:key="msg"
|
||||||
|
severity="error"
|
||||||
|
>{{ msg }}</Message
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 已上传列表(上传中、上传成功、上传失败) -->
|
||||||
|
<template
|
||||||
|
v-for="(file, index) of fileUploadRef?.uploadedFiles"
|
||||||
|
:key="file.name + file.url"
|
||||||
|
>
|
||||||
|
<FileUploadItem
|
||||||
|
:url="file.url || file.rawFile?.objectURL || ''"
|
||||||
|
:filename="file.name || file.url || '未知文件'"
|
||||||
|
@remove="
|
||||||
|
() => {
|
||||||
|
fileUploadRef!.uploadedFileCount--;
|
||||||
|
removeUploadedFileCallback(index);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
:status="file.status"
|
||||||
|
:progress="file.progress"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 待上传列表 -->
|
||||||
|
<template
|
||||||
|
v-for="(file, index) of fileUploadRef?.files"
|
||||||
|
:key="file.name + file.type + file.size"
|
||||||
|
>
|
||||||
|
<FileUploadItem
|
||||||
|
:url="file.objectURL"
|
||||||
|
:filename="file.name"
|
||||||
|
@remove="removeFileCallback(index)"
|
||||||
|
status="pending"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</FileUpload>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.p-floatlabel:has(.p-fileupload) label {
|
||||||
|
top: var(--p-floatlabel-over-active-top);
|
||||||
|
transform: translateY(0);
|
||||||
|
font-size: var(--p-floatlabel-active-font-size);
|
||||||
|
font-weight: var(--p-floatlabel-label-active-font-weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-fileupload-content .p-progressbar {
|
||||||
|
--p-fileupload-progressbar-height: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
28
src/__fk-inputs__/components/file-upload/types.ts
Normal file
28
src/__fk-inputs__/components/file-upload/types.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import type { FileUploadState } from 'primevue/fileupload';
|
||||||
|
|
||||||
|
export interface FileExt extends File {
|
||||||
|
objectURL: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FileUploadStatus = 'pending' | 'uploading' | 'uploaded' | 'failed';
|
||||||
|
|
||||||
|
export type UploadedFileInfo = {
|
||||||
|
rawFile?: FileExt;
|
||||||
|
name?: string;
|
||||||
|
url?: string;
|
||||||
|
status?: FileUploadStatus;
|
||||||
|
progress?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FileUploadInst extends FileUploadState {
|
||||||
|
files: FileExt[];
|
||||||
|
uploadedFiles: UploadedFileInfo[];
|
||||||
|
chooseDisabled?: boolean;
|
||||||
|
}
|
||||||
|
export type CustomRequest = (options: {
|
||||||
|
file: File;
|
||||||
|
onProgress: (percent: number) => void;
|
||||||
|
}) => Promise<{ url: string }>;
|
||||||
|
|
||||||
|
export type PropValueToFiles = (value: unknown) => UploadedFileInfo[];
|
||||||
|
export type PropFilesToValue = (filelist: UploadedFileInfo[]) => unknown;
|
47
src/__fk-inputs__/inputs/p-file-upload.tsx
Normal file
47
src/__fk-inputs__/inputs/p-file-upload.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import type { FormKitTypeDefinition } from '@formkit/core';
|
||||||
|
import type { FormKitInputs } from '@formkit/inputs';
|
||||||
|
import { createSection, label, outer } from '@formkit/inputs';
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
import FileUploadComponent from '../components/file-upload/file-upload.vue';
|
||||||
|
import type { CustomRequest, PropFilesToValue, PropValueToFiles } from '../components/file-upload/types';
|
||||||
|
import { floatLabel } from '../sections/floatLabel';
|
||||||
|
import { help } from '../sections/help';
|
||||||
|
import { messages } from '../sections/messages';
|
||||||
|
|
||||||
|
const input = createSection('input', () => ({
|
||||||
|
$cmp: markRaw(FileUploadComponent) as never,
|
||||||
|
bind: '$attrs',
|
||||||
|
props: {
|
||||||
|
context: '$node.context',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const PFileUpload: FormKitTypeDefinition = {
|
||||||
|
type: 'input',
|
||||||
|
schema: outer(
|
||||||
|
floatLabel(
|
||||||
|
input(), //
|
||||||
|
label('$label'),
|
||||||
|
),
|
||||||
|
help('$help'),
|
||||||
|
messages(),
|
||||||
|
),
|
||||||
|
props: ['fileLimit', 'maxFileSize', 'customRequest', 'valueToFiles', 'filesToValue', 'autoUpload'],
|
||||||
|
schemaMemoKey: 'ihcxd4qdgh7', // Math.random().toString(36).substring(2, 15)
|
||||||
|
};
|
||||||
|
|
||||||
|
declare module '@formkit/inputs' {
|
||||||
|
// https://formkit.com/essentials/custom-inputs#typescript-support
|
||||||
|
interface FormKitInputProps<Props extends FormKitInputs<Props>> {
|
||||||
|
PFileUpload: {
|
||||||
|
type: 'PFileUpload';
|
||||||
|
value?: unknown;
|
||||||
|
fileLimit?: number;
|
||||||
|
maxFileSize?: number;
|
||||||
|
customRequest?: CustomRequest;
|
||||||
|
valueToFiles?: PropValueToFiles;
|
||||||
|
filesToValue?: PropFilesToValue;
|
||||||
|
autoUpload?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import { text } from '@formkit/inputs';
|
|||||||
import Swal from 'sweetalert2';
|
import Swal from 'sweetalert2';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { arrayToTree } from 'utils4u/array';
|
import { arrayToTree } from 'utils4u/array';
|
||||||
|
import { CustomRequest } from '@/__fk-inputs__/components/file-upload/types';
|
||||||
|
|
||||||
async function submit(formData: Record<string, any>, formNode: FormKitNode) {
|
async function submit(formData: Record<string, any>, formNode: FormKitNode) {
|
||||||
console.group('submit');
|
console.group('submit');
|
||||||
@ -70,6 +71,23 @@ const promiseCascadeOptions = new Promise<typeof K_FLAT_TREE>((resolve) => {
|
|||||||
await new Promise(r => setTimeout(r, 1000))
|
await new Promise(r => setTimeout(r, 1000))
|
||||||
return K_OPTIONS;
|
return K_OPTIONS;
|
||||||
} */
|
} */
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
const customRequest: CustomRequest = (async ({ file, onProgress, }) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
return axios
|
||||||
|
.post('https://jsonplaceholder.typicode.com/posts', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
onUploadProgress: (e) => {
|
||||||
|
onProgress(Math.round((e.loaded * 100) / (e.total || 1)));
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// onProgress(100);
|
||||||
|
return { url: 'https://picsum.photos/200/300' };
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -112,6 +130,20 @@ const promiseCascadeOptions = new Promise<typeof K_FLAT_TREE>((resolve) => {
|
|||||||
validation="required"
|
validation="required"
|
||||||
>
|
>
|
||||||
</FormKit>
|
</FormKit>
|
||||||
|
<div class="border border-dashed border-gray-300 p-4 rounded-md">
|
||||||
|
<FormKit
|
||||||
|
type="PFileUpload"
|
||||||
|
name="PFileUpload"
|
||||||
|
label="文件上传"
|
||||||
|
:maxFileSize="1024 * 1024 * 2"
|
||||||
|
:customRequest
|
||||||
|
:fileLimit="99"
|
||||||
|
:autoUpload="true"
|
||||||
|
:filesToValue="(files) => JSON.stringify(files.map(f => ({ name: f.name, url: f.url })))"
|
||||||
|
:valueToFiles="(value) => JSON.parse(value as string)"
|
||||||
|
value='[{"name":"2KB图片_副本.jpeg","url":"https://picsum.photos/200/300"}]'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<FormKit
|
<FormKit
|
||||||
type="PInputPassword"
|
type="PInputPassword"
|
||||||
name="PInputPassword"
|
name="PInputPassword"
|
||||||
|
Reference in New Issue
Block a user