init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
@@ -0,0 +1,618 @@
|
||||
<template>
|
||||
<div class="fa-full-height">
|
||||
<FaSearchBar
|
||||
v-show="showSearchBar"
|
||||
ref="searchBarRef"
|
||||
v-model="searchForm"
|
||||
:items="CrossCombinationSearchItems"
|
||||
:rules="searchBarRules"
|
||||
:is-expand="false"
|
||||
:show-expand="true"
|
||||
:show-reset="true"
|
||||
:show-search="true"
|
||||
:disabled-search="false"
|
||||
:default-expanded="false"
|
||||
include-audit
|
||||
@search="handleSearch"
|
||||
@reset="onResetSearch"
|
||||
/>
|
||||
|
||||
<ElCard class="fa-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
|
||||
<FaTableHeader
|
||||
v-model:columns="columnChecks"
|
||||
v-model:showSearchBar="showSearchBar"
|
||||
:loading="loading"
|
||||
@refresh="refreshData"
|
||||
>
|
||||
<template #left>
|
||||
<FaTableHeaderLeft
|
||||
:remove-ids="selectedIds"
|
||||
:perm-create="['module_bre:cross_combination:create']"
|
||||
:perm-import="['module_bre:cross_combination:import']"
|
||||
:perm-export="['module_bre:cross_combination:export']"
|
||||
:perm-delete="['module_bre:cross_combination:delete']"
|
||||
:delete-loading="batchDeleting"
|
||||
:create-loading="createLoading"
|
||||
@add="handleAdd"
|
||||
@import="openImport"
|
||||
@export="openExport"
|
||||
@delete="handleBatchDelete"
|
||||
/>
|
||||
</template>
|
||||
</FaTableHeader>
|
||||
|
||||
<FaTable
|
||||
ref="faTableRef"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@selection-change="onTableSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
/>
|
||||
</ElCard>
|
||||
|
||||
<FaDialog
|
||||
v-model="dialogVisible.visible"
|
||||
:title="dialogVisible.title"
|
||||
width="920px"
|
||||
dialog-class="crud-embed-dialog"
|
||||
modal-class="crud-embed-dialog"
|
||||
:form-mode="dialogVisible.type"
|
||||
:confirm-loading="submitLoading"
|
||||
:close-on-click-modal="false"
|
||||
@cancel="crud.handleCloseDialog"
|
||||
@close="crud.handleCloseDialog"
|
||||
@confirm="crud.handleSubmit"
|
||||
>
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<FaDescriptions
|
||||
:column="4"
|
||||
:data="detailFormData"
|
||||
:items="CrossCombinationDetailItems"
|
||||
max-height="70vh"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<FaForm
|
||||
:key="CrossCombinationFormRenderKey"
|
||||
scrollbar
|
||||
max-height="70vh"
|
||||
ref="dataFormRef"
|
||||
v-model="formData"
|
||||
:items="CrossCombinationDialogFormItems"
|
||||
:rules="rules"
|
||||
label-suffix=":"
|
||||
:label-width="120"
|
||||
label-position="right"
|
||||
:span="12"
|
||||
:gutter="16"
|
||||
:show-reset="false"
|
||||
:show-submit="false"
|
||||
class="crud-dialog-art-form"
|
||||
/>
|
||||
</template>
|
||||
</FaDialog>
|
||||
|
||||
<FaImportDialog
|
||||
v-model="importVisible"
|
||||
:content-config="CrossCombinationImportContentConfig"
|
||||
default-template-file-name="cross_combination_import_template.xlsx"
|
||||
@upload="handleCrudImportUpload"
|
||||
/>
|
||||
|
||||
<FaExportDialog
|
||||
v-model="exportVisible"
|
||||
:content-config="CrossCombinationExportContentConfig"
|
||||
:query-params="exportQueryParams"
|
||||
:page-data="data"
|
||||
:selection-data="selectedRows"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TableOperationAction } from "@/utils/table";
|
||||
import { renderTableOperationCell, stripPaginationParams, toCrudCols } from "@utils";
|
||||
import { useCrudForm } from "@/hooks/core/useCrudForm";
|
||||
import { ResultEnum } from "@/enums/api/result.enum";
|
||||
import type { IContentConfig, IObject } from "@/components/modal/types";
|
||||
import type { AuditSearchFormParams } from "@/components/forms/fa-search-bar/auditSearchFormItems";
|
||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||
import TargetAPI, { type TargetOption } from "@/api/module_bre/target";
|
||||
import GermplasmAPI, { type GermplasmOption } from "@/api/module_bre/germplasm";
|
||||
import CrossCombinationAPI, { type CrossCombinationForm, type CrossCombinationPageQuery, type CrossCombinationTable } from "@/api/module_bre/cross_combination";
|
||||
import type { ColumnOption } from "@/types/component";
|
||||
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
|
||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "CrossCombination",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const targetOptions = ref<TargetOption[]>([]);
|
||||
const germplasmOptions = ref<GermplasmOption[]>([]);
|
||||
|
||||
const targetOptionsMap = computed(() => {
|
||||
const m: Record<number, string> = {};
|
||||
for (const o of targetOptions.value) m[o.value] = o.label;
|
||||
return m;
|
||||
});
|
||||
const germplasmOptionsMap = computed(() => {
|
||||
const m: Record<number, string> = {};
|
||||
for (const o of germplasmOptions.value) m[o.value] = o.label;
|
||||
return m;
|
||||
});
|
||||
|
||||
async function loadTargetOptions() {
|
||||
try {
|
||||
const res = await TargetAPI.getTargetOptions();
|
||||
if (res.data.code === ResultEnum.SUCCESS) {
|
||||
targetOptions.value = res.data.data ?? [];
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (import.meta.env.DEV) console.error("[CrossCombination] loadTargetOptions", error);
|
||||
}
|
||||
}
|
||||
async function loadGermplasmOptions() {
|
||||
try {
|
||||
const res = await GermplasmAPI.getGermplasmOptions();
|
||||
if (res.data.code === ResultEnum.SUCCESS) {
|
||||
germplasmOptions.value = res.data.data ?? [];
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (import.meta.env.DEV) console.error("[CrossCombination] loadGermplasmOptions", error);
|
||||
}
|
||||
}
|
||||
|
||||
const createInitialFormData = (): CrossCombinationForm => ({
|
||||
id: undefined,
|
||||
combination_code: undefined,
|
||||
cross_year: undefined,
|
||||
bre_target_id: undefined,
|
||||
female_parent_id: undefined,
|
||||
male_parent_id: undefined,
|
||||
cross_method: undefined,
|
||||
cross_date: undefined,
|
||||
seed_count: undefined,
|
||||
remark: undefined,
|
||||
});
|
||||
|
||||
type CrossCombinationSearchFormParams = {
|
||||
combination_code?: string;
|
||||
cross_year?: number;
|
||||
bre_target_id?: number;
|
||||
cross_method?: string;
|
||||
} & AuditSearchFormParams;
|
||||
|
||||
const searchForm = ref<CrossCombinationSearchFormParams>({
|
||||
combination_code: undefined,
|
||||
cross_year: undefined,
|
||||
bre_target_id: undefined,
|
||||
cross_method: undefined,
|
||||
created_id: undefined,
|
||||
updated_id: undefined,
|
||||
created_time: [],
|
||||
updated_time: [],
|
||||
});
|
||||
|
||||
const showSearchBar = ref(true);
|
||||
const searchBarRef = ref<{ validate: () => Promise<boolean> } | null>(null);
|
||||
const searchBarRules: Record<string, unknown> = {};
|
||||
|
||||
const CrossCombinationSearchItems = computed(() => [
|
||||
{
|
||||
label: "组合编号",
|
||||
key: "combination_code",
|
||||
type: "input",
|
||||
placeholder: "请输入组合编号",
|
||||
clearable: true,
|
||||
span: 8,
|
||||
},
|
||||
{
|
||||
label: "杂交年份",
|
||||
key: "cross_year",
|
||||
type: "input",
|
||||
placeholder: "请输入杂交年份",
|
||||
clearable: true,
|
||||
span: 8,
|
||||
},
|
||||
{
|
||||
label: "育种目标",
|
||||
key: "bre_target_id",
|
||||
type: "select",
|
||||
props: {
|
||||
placeholder: "请选择育种目标",
|
||||
options: targetOptions.value,
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
},
|
||||
span: 8,
|
||||
},
|
||||
{
|
||||
label: "杂交方式",
|
||||
key: "cross_method",
|
||||
type: "radiogroup",
|
||||
props: {
|
||||
placeholder: "请选择杂交方式",
|
||||
options: [{ label: "人工杂交", value: "人工杂交" }, { label: "自然授粉", value: "自然授粉" }, { label: "回交", value: "回交" }],
|
||||
clearable: true,
|
||||
},
|
||||
span: 8,
|
||||
},
|
||||
]);
|
||||
|
||||
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
|
||||
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } = useTableSelection<CrossCombinationTable>();
|
||||
|
||||
const createLoading = ref(false);
|
||||
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
pagination,
|
||||
searchParams,
|
||||
getData,
|
||||
replaceSearchParams,
|
||||
resetSearchParams,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData,
|
||||
refreshCreate,
|
||||
refreshUpdate,
|
||||
refreshRemove,
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: CrossCombinationAPI.getCrossCombinationList,
|
||||
apiParams: {
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
},
|
||||
columnsFactory: (): ColumnOption<CrossCombinationTable>[] => [
|
||||
{ type: "globalIndex", width: 56, label: "序号" },
|
||||
{ type: "selection", width: 48, fixed: "left" },
|
||||
{ prop: "combination_code", label: "组合编号", minWidth: 140, showOverflowTooltip: true },
|
||||
{ prop: "cross_year", label: "杂交年份", minWidth: 100 },
|
||||
{ prop: "bre_target_name", label: "育种目标", minWidth: 140, showOverflowTooltip: true },
|
||||
{ prop: "female_parent_name", label: "母本", minWidth: 140, showOverflowTooltip: true },
|
||||
{ prop: "male_parent_name", label: "父本", minWidth: 140, showOverflowTooltip: true },
|
||||
{ prop: "cross_method", label: "杂交方式", minWidth: 140, showOverflowTooltip: true },
|
||||
{ prop: "cross_date", label: "杂交日期", minWidth: 140, showOverflowTooltip: true },
|
||||
{ prop: "seed_count", label: "获种数", minWidth: 100 },
|
||||
{
|
||||
prop: "created_time",
|
||||
label: "创建时间",
|
||||
width: 168,
|
||||
sortable: true,
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
prop: "updated_time",
|
||||
label: "更新时间",
|
||||
width: 168,
|
||||
sortable: true,
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
prop: "created_by",
|
||||
label: "创建人",
|
||||
minWidth: 100,
|
||||
formatter: (row: CrossCombinationTable) => row.created_by?.name ?? "—",
|
||||
},
|
||||
{
|
||||
prop: "updated_by",
|
||||
label: "更新人",
|
||||
minWidth: 100,
|
||||
formatter: (row: CrossCombinationTable) => row.updated_by?.name ?? "—",
|
||||
},
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 180,
|
||||
fixed: "right",
|
||||
align: "center",
|
||||
formatter: (row: CrossCombinationTable) => formatCrossCombinationOperationCell(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const CrossCombinationCrudCols = toCrudCols(columns);
|
||||
|
||||
const exportQueryParams = computed(() => {
|
||||
return stripPaginationParams(searchParams as Record<string, unknown>);
|
||||
});
|
||||
|
||||
const CrossCombinationImportContentConfig = computed<IContentConfig>(() => ({
|
||||
permPrefix: "module_bre:cross_combination",
|
||||
cols: CrossCombinationCrudCols.value,
|
||||
indexAction: async () => ({}),
|
||||
importTemplate: () => CrossCombinationAPI.downloadTemplateCrossCombination(),
|
||||
}));
|
||||
|
||||
const CrossCombinationExportContentConfig = computed(() => ({
|
||||
permPrefix: "module_bre:cross_combination",
|
||||
cols: CrossCombinationCrudCols.value,
|
||||
exportsBlobAction: async (params: IObject) => {
|
||||
const merged = {
|
||||
...(exportQueryParams.value as unknown as Record<string, unknown>),
|
||||
...params,
|
||||
} as unknown as CrossCombinationPageQuery;
|
||||
const res = await CrossCombinationAPI.exportCrossCombination(merged);
|
||||
return res.data as Blob;
|
||||
},
|
||||
}));
|
||||
|
||||
const { dialogVisible } = useCrudDialog();
|
||||
|
||||
const detailFormData = ref<CrossCombinationTable>({});
|
||||
|
||||
const CrossCombinationDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] = [
|
||||
{ label: "组合编号", prop: "combination_code" },
|
||||
{ label: "杂交年份", prop: "cross_year" },
|
||||
{ label: "育种目标", prop: "bre_target_name" },
|
||||
{ label: "母本", prop: "female_parent_name" },
|
||||
{ label: "父本", prop: "male_parent_name" },
|
||||
{ label: "杂交方式", prop: "cross_method" },
|
||||
{ label: "杂交日期", prop: "cross_date" },
|
||||
{ label: "获种数", prop: "seed_count" },
|
||||
{ label: "备注", prop: "remark" },
|
||||
{ label: "UUID", prop: "uuid" },
|
||||
{ label: "创建人", prop: "created_by.name" },
|
||||
{ label: "更新人", prop: "updated_by.name" },
|
||||
{ label: "创建时间", prop: "created_time" },
|
||||
{ label: "更新时间", prop: "updated_time" },
|
||||
];
|
||||
|
||||
const CrossCombinationDialogFormItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
key: "combination_code",
|
||||
label: "组合编号",
|
||||
type: "input",
|
||||
props: { placeholder: "请输入组合编号", maxlength: 100 },
|
||||
span: 12,
|
||||
},
|
||||
{
|
||||
key: "cross_year",
|
||||
label: "杂交年份",
|
||||
type: "number",
|
||||
props: { placeholder: "请输入杂交年份" },
|
||||
span: 12,
|
||||
},
|
||||
{
|
||||
key: "bre_target_id",
|
||||
label: "育种目标",
|
||||
type: "select",
|
||||
props: {
|
||||
placeholder: "请选择育种目标",
|
||||
options: targetOptions.value,
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
},
|
||||
span: 12,
|
||||
},
|
||||
{
|
||||
key: "female_parent_id",
|
||||
label: "母本",
|
||||
type: "select",
|
||||
props: {
|
||||
placeholder: "请选择母本",
|
||||
options: germplasmOptions.value,
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
},
|
||||
span: 12,
|
||||
},
|
||||
{
|
||||
key: "male_parent_id",
|
||||
label: "父本",
|
||||
type: "select",
|
||||
props: {
|
||||
placeholder: "请选择父本",
|
||||
options: germplasmOptions.value,
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
},
|
||||
span: 12,
|
||||
},
|
||||
{
|
||||
key: "cross_method",
|
||||
label: "杂交方式",
|
||||
type: "radiogroup",
|
||||
props: {
|
||||
placeholder: "请选择杂交方式",
|
||||
options: [{ label: "人工杂交", value: "人工杂交" }, { label: "自然授粉", value: "自然授粉" }, { label: "回交", value: "回交" }],
|
||||
clearable: true,
|
||||
},
|
||||
span: 12,
|
||||
},
|
||||
{
|
||||
key: "cross_date",
|
||||
label: "杂交日期",
|
||||
type: "date",
|
||||
props: {
|
||||
type: "date",
|
||||
placeholder: "请选择杂交日期",
|
||||
},
|
||||
span: 12,
|
||||
},
|
||||
{
|
||||
key: "seed_count",
|
||||
label: "获种数",
|
||||
type: "number",
|
||||
props: { placeholder: "请输入获种数" },
|
||||
span: 12,
|
||||
},
|
||||
{
|
||||
key: "remark",
|
||||
label: "备注",
|
||||
type: "input",
|
||||
props: { type: "textarea", rows: 3, placeholder: "请输入备注" },
|
||||
span: 24,
|
||||
},
|
||||
]);
|
||||
|
||||
const formData = ref<CrossCombinationForm>(createInitialFormData());
|
||||
|
||||
const rules = reactive({
|
||||
combination_code: [{ required: true, message: "请输入组合编号", trigger: "blur" }],
|
||||
bre_target_id: [{ required: true, message: "请选择育种目标", trigger: "change" }],
|
||||
});
|
||||
|
||||
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
|
||||
const CrossCombinationFormRenderKey = ref(0);
|
||||
|
||||
const crud = useCrudForm<CrossCombinationForm>({
|
||||
formData,
|
||||
initialFormData: createInitialFormData(),
|
||||
dialogVisible,
|
||||
dataFormRef,
|
||||
formRenderKey: CrossCombinationFormRenderKey,
|
||||
detailApi: CrossCombinationAPI.getCrossCombinationDetail,
|
||||
createApi: CrossCombinationAPI.createCrossCombination,
|
||||
updateApi: CrossCombinationAPI.updateCrossCombination,
|
||||
titles: { create: "新增杂交组合", update: "修改杂交组合", detail: "杂交组合详情" },
|
||||
detailFormData,
|
||||
onCreateSuccess: async () => {
|
||||
await refreshCreate();
|
||||
},
|
||||
onUpdateSuccess: async () => {
|
||||
await refreshUpdate();
|
||||
},
|
||||
});
|
||||
|
||||
const { submitLoading } = crud;
|
||||
|
||||
const { importVisible, exportVisible, openImport, openExport } = useImportExport();
|
||||
|
||||
const handleSearch = async (params: CrossCombinationSearchFormParams) => {
|
||||
await searchBarRef.value?.validate();
|
||||
replaceSearchParams({
|
||||
combination_code: params.combination_code,
|
||||
cross_year: params.cross_year,
|
||||
bre_target_id: params.bre_target_id,
|
||||
cross_method: params.cross_method,
|
||||
created_id: params.created_id ?? undefined,
|
||||
updated_id: params.updated_id ?? undefined,
|
||||
created_time:
|
||||
Array.isArray(params.created_time) && params.created_time.length === 2
|
||||
? params.created_time
|
||||
: undefined,
|
||||
updated_time:
|
||||
Array.isArray(params.updated_time) && params.updated_time.length === 2
|
||||
? params.updated_time
|
||||
: undefined,
|
||||
} as Record<string, unknown>);
|
||||
await getData();
|
||||
};
|
||||
|
||||
const onResetSearch = async () => {
|
||||
searchForm.value = {
|
||||
combination_code: undefined,
|
||||
cross_year: undefined,
|
||||
bre_target_id: undefined,
|
||||
cross_method: undefined,
|
||||
created_id: undefined,
|
||||
updated_id: undefined,
|
||||
created_time: [],
|
||||
updated_time: [],
|
||||
};
|
||||
await resetSearchParams();
|
||||
};
|
||||
|
||||
function buildCrossCombinationRowActions(row: CrossCombinationTable): TableOperationAction[] {
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
artType: "view",
|
||||
perm: "module_bre:cross_combination:detail",
|
||||
run: () => void crud.handleOpenDialog("detail", row.id),
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
artType: "edit",
|
||||
icon: "ri:edit-2-line",
|
||||
perm: "module_bre:cross_combination:update",
|
||||
run: () => void crud.handleOpenDialog("update", row.id),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
artType: "delete",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
perm: "module_bre:cross_combination:delete",
|
||||
run: () => deleteCrossCombinationRow(row),
|
||||
},
|
||||
];
|
||||
return all;
|
||||
}
|
||||
|
||||
function formatCrossCombinationOperationCell(row: CrossCombinationTable) {
|
||||
return renderTableOperationCell(buildCrossCombinationRowActions(row), {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1",
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
createLoading.value = true;
|
||||
try {
|
||||
await crud.handleOpenDialog("create");
|
||||
} finally {
|
||||
createLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const deleteCrossCombinationRow = async (row: CrossCombinationTable) => {
|
||||
if (!row.id) return;
|
||||
try {
|
||||
await confirmDelete(`确定删除「${row.combination_code ?? row.id}」吗?此操作不可恢复!`);
|
||||
await CrossCombinationAPI.deleteCrossCombination([row.id!]);
|
||||
faTableRef.value?.elTableRef?.clearSelection();
|
||||
await refreshRemove();
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
};
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedIds.value;
|
||||
if (ids.length === 0) return;
|
||||
try {
|
||||
await confirmBatchDelete(ids.length);
|
||||
batchDeleting.value = true;
|
||||
await CrossCombinationAPI.deleteCrossCombination(ids);
|
||||
faTableRef.value?.elTableRef?.clearSelection();
|
||||
await refreshRemove();
|
||||
} catch {
|
||||
// 用户取消
|
||||
} finally {
|
||||
batchDeleting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCrudImportUpload(uploadFormData: FormData) {
|
||||
try {
|
||||
const res = await CrossCombinationAPI.importCrossCombination(uploadFormData);
|
||||
if (res.data.code === ResultEnum.SUCCESS) {
|
||||
ElMessage.success(res.data.msg || "导入成功");
|
||||
importVisible.value = false;
|
||||
await refreshData();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (import.meta.env.DEV) console.error("[Import]", error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTargetOptions();
|
||||
loadGermplasmOptions();
|
||||
loadGermplasmOptions();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user