feat: add kuaidi100 logistics sync
This commit is contained in:
@@ -253,6 +253,9 @@ export interface AdminOrderDetail {
|
||||
tracking_no: string;
|
||||
tracking_status: string;
|
||||
tracking_status_text: string;
|
||||
provider_status_text: string;
|
||||
sync_status_text: string;
|
||||
sync_error: string;
|
||||
latest_desc: string;
|
||||
latest_time: string;
|
||||
nodes: Array<{
|
||||
@@ -266,6 +269,9 @@ export interface AdminOrderDetail {
|
||||
tracking_no: string;
|
||||
tracking_status: string;
|
||||
tracking_status_text: string;
|
||||
provider_status_text: string;
|
||||
sync_status_text: string;
|
||||
sync_error: string;
|
||||
latest_desc: string;
|
||||
latest_time: string;
|
||||
nodes: Array<{
|
||||
@@ -1183,6 +1189,37 @@ export interface AdminExpressCompanyItem {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface AdminExpressCompanyCatalogItem {
|
||||
id: number;
|
||||
company_name: string;
|
||||
company_code: string;
|
||||
company_type: string;
|
||||
display_text: string;
|
||||
source: string;
|
||||
synced_at: string;
|
||||
}
|
||||
|
||||
export interface AdminExpressCompanyRecognitionCandidate {
|
||||
company_name: string;
|
||||
company_code: string;
|
||||
official_name?: string;
|
||||
display_text: string;
|
||||
length_pre?: number;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface AdminExpressCompanyRecognitionResult {
|
||||
input: string;
|
||||
tracking_no: string;
|
||||
company_code: string;
|
||||
company_name: string;
|
||||
status: string;
|
||||
status_text: string;
|
||||
error_message?: string;
|
||||
resolved: null | AdminExpressCompanyRecognitionCandidate;
|
||||
candidates: AdminExpressCompanyRecognitionCandidate[];
|
||||
}
|
||||
|
||||
export interface AdminExpressCompanyPayload {
|
||||
id?: number;
|
||||
company_name: string;
|
||||
@@ -2288,6 +2325,37 @@ export const adminApi = {
|
||||
};
|
||||
}>;
|
||||
},
|
||||
getExpressCompanyCatalog(params?: { keyword?: string; limit?: number }) {
|
||||
return request.get("/api/admin/express-company/catalog", { params }) as Promise<{
|
||||
code: number;
|
||||
message: string;
|
||||
data: {
|
||||
list: AdminExpressCompanyCatalogItem[];
|
||||
total: number;
|
||||
synced_at: string;
|
||||
};
|
||||
}>;
|
||||
},
|
||||
syncExpressCompanyCatalog() {
|
||||
return request.post("/api/admin/express-company/catalog/sync") as Promise<{
|
||||
code: number;
|
||||
message: string;
|
||||
data: {
|
||||
total: number;
|
||||
inserted: number;
|
||||
updated: number;
|
||||
backfilled: number;
|
||||
synced_at: string;
|
||||
};
|
||||
}>;
|
||||
},
|
||||
recognizeExpressCompany(data: { tracking_no: string; company_name?: string; company_code?: string }) {
|
||||
return request.post("/api/admin/express-company/recognize", data) as Promise<{
|
||||
code: number;
|
||||
message: string;
|
||||
data: AdminExpressCompanyRecognitionResult;
|
||||
}>;
|
||||
},
|
||||
saveExpressCompany(data: AdminExpressCompanyPayload) {
|
||||
return request.post("/api/admin/express-company/save", data) as Promise<{
|
||||
code: number;
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { Refresh } from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import {
|
||||
adminApi,
|
||||
type AdminExpressCompanyCatalogItem,
|
||||
type AdminExpressCompanyItem,
|
||||
type AdminExpressCompanyPayload,
|
||||
} from "../../api/admin";
|
||||
import OrderStatusTag from "../../components/OrderStatusTag.vue";
|
||||
|
||||
type CatalogSuggestion = AdminExpressCompanyCatalogItem & { value: string };
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const companies = ref<AdminExpressCompanyItem[]>([]);
|
||||
const defaultCompany = ref("");
|
||||
const catalogTotal = ref(0);
|
||||
const catalogSyncedAt = ref("");
|
||||
const catalogSyncing = ref(false);
|
||||
const catalogLoading = ref(false);
|
||||
|
||||
const enabledCount = computed(() => companies.value.filter((item) => item.status === "enabled").length);
|
||||
const displaySyncedAt = computed(() => (catalogSyncedAt.value && catalogSyncedAt.value !== "0" ? catalogSyncedAt.value : "-"));
|
||||
|
||||
const form = reactive<AdminExpressCompanyPayload>({
|
||||
company_name: "",
|
||||
@@ -39,6 +48,32 @@ async function fetchCompanies() {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCatalogSummary() {
|
||||
try {
|
||||
const response = await adminApi.getExpressCompanyCatalog({ limit: 1 });
|
||||
catalogTotal.value = response.data.total;
|
||||
catalogSyncedAt.value = String(response.data.synced_at || "");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function syncCatalog() {
|
||||
catalogSyncing.value = true;
|
||||
try {
|
||||
const response = await adminApi.syncExpressCompanyCatalog();
|
||||
catalogTotal.value = response.data.total;
|
||||
catalogSyncedAt.value = String(response.data.synced_at || "");
|
||||
ElMessage.success(`公司码表已同步,共 ${response.data.total} 条`);
|
||||
await fetchCompanies();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
ElMessage.error(error?.message || "快递100公司码表同步失败");
|
||||
} finally {
|
||||
catalogSyncing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog(row?: AdminExpressCompanyItem) {
|
||||
if (row) {
|
||||
form.id = row.id;
|
||||
@@ -60,6 +95,27 @@ function openDialog(row?: AdminExpressCompanyItem) {
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function resolveCatalogCode() {
|
||||
const keyword = form.company_code.trim() || form.company_name.trim();
|
||||
if (!keyword) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await adminApi.getExpressCompanyCatalog({ keyword, limit: 10 });
|
||||
const exact = response.data.list.find((item) => item.company_code === keyword || item.company_name === keyword);
|
||||
const candidate = exact || response.data.list[0];
|
||||
if (candidate) {
|
||||
form.company_code = candidate.company_code;
|
||||
if (!form.company_name.trim()) {
|
||||
form.company_name = candidate.company_name;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!form.company_name.trim()) {
|
||||
ElMessage.warning("请填写快递公司名称");
|
||||
@@ -70,6 +126,10 @@ async function submit() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9]+$/i.test(form.company_code.trim()) || form.company_code.trim().startsWith("express_")) {
|
||||
await resolveCatalogCode();
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
await adminApi.saveExpressCompany({
|
||||
@@ -89,11 +149,40 @@ async function submit() {
|
||||
}
|
||||
}
|
||||
|
||||
async function queryCatalog(queryString: string, cb: (items: CatalogSuggestion[]) => void) {
|
||||
catalogLoading.value = true;
|
||||
try {
|
||||
const response = await adminApi.getExpressCompanyCatalog({
|
||||
keyword: queryString.trim(),
|
||||
limit: 20,
|
||||
});
|
||||
const suggestions = response.data.list.map((item) => ({
|
||||
...item,
|
||||
value: item.display_text,
|
||||
}));
|
||||
cb(suggestions);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
cb([]);
|
||||
} finally {
|
||||
catalogLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCatalogSelect(item: CatalogSuggestion) {
|
||||
form.company_code = item.company_code;
|
||||
if (!form.company_name.trim()) {
|
||||
form.company_name = item.company_name;
|
||||
}
|
||||
}
|
||||
|
||||
function statusTagText(row: AdminExpressCompanyItem) {
|
||||
return row.is_default ? `${row.status_text} / 默认` : row.status_text;
|
||||
}
|
||||
|
||||
onMounted(fetchCompanies);
|
||||
onMounted(async () => {
|
||||
await Promise.all([fetchCompanies(), fetchCatalogSummary()]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -109,6 +198,16 @@ onMounted(fetchCompanies);
|
||||
<div class="metric-card__value">{{ enabledCount }}</div>
|
||||
<div class="metric-card__desc">仓管寄回下拉列表中可选择</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">官方码表</div>
|
||||
<div class="metric-card__value">{{ catalogTotal }}</div>
|
||||
<div class="metric-card__desc">快递100 官方公司编码库</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">最近同步</div>
|
||||
<div class="metric-card__value metric-card__value--text">{{ displaySyncedAt }}</div>
|
||||
<div class="metric-card__desc">官方码表最后一次同步时间</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">默认快递公司</div>
|
||||
<div class="metric-card__value metric-card__value--text">{{ defaultCompany || "-" }}</div>
|
||||
@@ -119,16 +218,18 @@ onMounted(fetchCompanies);
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<div class="filters-row" style="justify-content: space-between;">
|
||||
<div style="color: var(--admin-text-subtle);">
|
||||
维护仓管寄回时可选的快递公司。停用后不会出现在寄回下拉列表中。
|
||||
维护仓管寄回时可选的快递公司。公司码优先从快递100官方码表检索,停用后不会出现在寄回下拉列表中。
|
||||
</div>
|
||||
<el-button type="primary" @click="openDialog()">新增快递公司</el-button>
|
||||
<el-button :icon="Refresh" :loading="catalogSyncing" type="primary" @click="syncCatalog">
|
||||
同步快递100公司码表
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="panel-card orders-table" shadow="never">
|
||||
<el-table :data="companies" stripe>
|
||||
<el-table-column prop="company_name" label="快递公司" min-width="180" />
|
||||
<el-table-column prop="company_code" label="编码" min-width="160" />
|
||||
<el-table-column prop="company_code" label="快递100编码" min-width="160" />
|
||||
<el-table-column label="状态" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<OrderStatusTag :status="statusTagText(row)" />
|
||||
@@ -150,8 +251,28 @@ onMounted(fetchCompanies);
|
||||
<el-form-item label="快递公司名称">
|
||||
<el-input v-model="form.company_name" maxlength="64" placeholder="例如:顺丰速运" />
|
||||
</el-form-item>
|
||||
<el-form-item label="编码">
|
||||
<el-input v-model="form.company_code" maxlength="64" placeholder="可留空,系统自动生成" />
|
||||
<el-form-item label="快递100编码">
|
||||
<el-autocomplete
|
||||
v-model="form.company_code"
|
||||
:fetch-suggestions="queryCatalog"
|
||||
:loading="catalogLoading"
|
||||
clearable
|
||||
placeholder="输入名称或编码搜索官方码表"
|
||||
style="width: 100%"
|
||||
@select="handleCatalogSelect"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div style="display: grid; gap: 4px;">
|
||||
<div style="font-weight: 700;">{{ item.company_name }}</div>
|
||||
<div style="color: var(--admin-text-subtle); font-size: 12px;">
|
||||
{{ item.company_code }}{{ item.company_type ? ` / ${item.company_type}` : "" }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
<div style="margin-top: 6px; color: var(--admin-text-subtle); font-size: 12px;">
|
||||
支持按名称或编码检索快递100官方码表,选中后会自动回填公司码。
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
@@ -172,7 +293,7 @@ onMounted(fetchCompanies);
|
||||
<el-checkbox v-model="form.is_default">设为默认快递公司</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" maxlength="255" placeholder="内部备注,可不填" />
|
||||
<el-input v-model="form.remark" :rows="3" maxlength="255" placeholder="内部备注,可不填" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { adminApi, type AdminExpressCompanyItem, type AdminManualOrderCreatePayload, type AdminManualOrderMeta, type AdminOrderDetail, type AdminOrderListItem, type AdminOrderWarehouseOption } from "../../api/admin";
|
||||
import {
|
||||
adminApi,
|
||||
type AdminExpressCompanyItem,
|
||||
type AdminExpressCompanyRecognitionCandidate,
|
||||
type AdminManualOrderCreatePayload,
|
||||
type AdminManualOrderMeta,
|
||||
type AdminOrderDetail,
|
||||
type AdminOrderListItem,
|
||||
type AdminOrderWarehouseOption,
|
||||
} from "../../api/admin";
|
||||
import OrderStatusTag from "../../components/OrderStatusTag.vue";
|
||||
import { recognizeReturnAddress } from "../../utils/address-recognition";
|
||||
|
||||
@@ -19,6 +28,8 @@ const returnDialogVisible = ref(false);
|
||||
const returnSubmitting = ref(false);
|
||||
const returnExpressCompany = ref("");
|
||||
const returnTrackingNo = ref("");
|
||||
const returnRecognitionLoading = ref(false);
|
||||
const returnRecognitionCandidates = ref<AdminExpressCompanyRecognitionCandidate[]>([]);
|
||||
const expressCompanyLoading = ref(false);
|
||||
const expressCompanyOptions = ref<AdminExpressCompanyItem[]>([]);
|
||||
const defaultExpressCompany = ref("");
|
||||
@@ -139,6 +150,8 @@ const expressCompanySelectOptions = computed(() => {
|
||||
...expressCompanyOptions.value,
|
||||
];
|
||||
});
|
||||
let returnRecognitionTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
function createManualOrderForm(): AdminManualOrderCreatePayload {
|
||||
return {
|
||||
service_provider: "anxinyan",
|
||||
@@ -377,7 +390,53 @@ async function openReturnDialog() {
|
||||
await ensureExpressCompanyOptions();
|
||||
returnExpressCompany.value = detail.value.return_logistics?.express_company || defaultExpressCompany.value || expressCompanyOptions.value[0]?.company_name || "";
|
||||
returnTrackingNo.value = detail.value.return_logistics?.tracking_no || "";
|
||||
returnRecognitionCandidates.value = [];
|
||||
returnDialogVisible.value = true;
|
||||
if (returnTrackingNo.value) {
|
||||
scheduleReturnRecognition();
|
||||
}
|
||||
}
|
||||
|
||||
async function recognizeReturnExpressCompany() {
|
||||
const trackingNo = returnTrackingNo.value.trim();
|
||||
if (!returnDialogVisible.value || !trackingNo) {
|
||||
returnRecognitionCandidates.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
returnRecognitionLoading.value = true;
|
||||
try {
|
||||
const response = await adminApi.recognizeExpressCompany({
|
||||
tracking_no: trackingNo,
|
||||
company_name: returnExpressCompany.value.trim(),
|
||||
});
|
||||
const result = response.data;
|
||||
returnRecognitionCandidates.value = result.candidates || [];
|
||||
if (result.resolved) {
|
||||
returnExpressCompany.value = result.resolved.company_name;
|
||||
} else if (result.candidates.length === 1) {
|
||||
returnExpressCompany.value = result.candidates[0].company_name;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
returnRecognitionCandidates.value = [];
|
||||
} finally {
|
||||
returnRecognitionLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleReturnRecognition() {
|
||||
if (returnRecognitionTimer) {
|
||||
clearTimeout(returnRecognitionTimer);
|
||||
}
|
||||
returnRecognitionTimer = setTimeout(() => {
|
||||
void recognizeReturnExpressCompany();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function chooseReturnRecognitionCandidate(candidate: AdminExpressCompanyRecognitionCandidate) {
|
||||
returnExpressCompany.value = candidate.company_name;
|
||||
returnRecognitionCandidates.value = [candidate];
|
||||
}
|
||||
|
||||
async function submitReturnLogistics() {
|
||||
@@ -409,6 +468,18 @@ async function submitReturnLogistics() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(returnTrackingNo, () => {
|
||||
if (returnDialogVisible.value) {
|
||||
scheduleReturnRecognition();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (returnRecognitionTimer) {
|
||||
clearTimeout(returnRecognitionTimer);
|
||||
}
|
||||
});
|
||||
|
||||
async function markReturnReceived() {
|
||||
if (!detail.value) return;
|
||||
returnReceiveSubmitting.value = true;
|
||||
@@ -655,10 +726,18 @@ onMounted(fetchOrders);
|
||||
<div class="order-detail-item__label">物流状态</div>
|
||||
<div class="order-detail-item__value">{{ logisticsActionText }}</div>
|
||||
</div>
|
||||
<div class="order-detail-item">
|
||||
<div class="order-detail-item__label">快递100状态</div>
|
||||
<div class="order-detail-item__value">{{ detail.logistics_info.provider_status_text || detail.logistics_info.sync_status_text || "-" }}</div>
|
||||
</div>
|
||||
<div class="order-detail-item order-detail-item--full">
|
||||
<div class="order-detail-item__label">最新节点</div>
|
||||
<div class="order-detail-item__value">{{ detail.logistics_info.latest_desc || "-" }}</div>
|
||||
</div>
|
||||
<div class="order-detail-item order-detail-item--full" v-if="detail.logistics_info.sync_error">
|
||||
<div class="order-detail-item__label">同步异常</div>
|
||||
<div class="order-detail-item__value">{{ detail.logistics_info.sync_error }}</div>
|
||||
</div>
|
||||
<div class="order-detail-item order-detail-item--full" v-if="detail.logistics_info.latest_time">
|
||||
<div class="order-detail-item__label">最新更新时间</div>
|
||||
<div class="order-detail-item__value">{{ detail.logistics_info.latest_time }}</div>
|
||||
@@ -680,10 +759,18 @@ onMounted(fetchOrders);
|
||||
<div class="order-detail-item__label">物流状态</div>
|
||||
<div class="order-detail-item__value">{{ detail.return_logistics.tracking_status_text }}</div>
|
||||
</div>
|
||||
<div class="order-detail-item">
|
||||
<div class="order-detail-item__label">快递100状态</div>
|
||||
<div class="order-detail-item__value">{{ detail.return_logistics.provider_status_text || detail.return_logistics.sync_status_text || "-" }}</div>
|
||||
</div>
|
||||
<div class="order-detail-item order-detail-item--full">
|
||||
<div class="order-detail-item__label">最新节点</div>
|
||||
<div class="order-detail-item__value">{{ detail.return_logistics.latest_desc || "-" }}</div>
|
||||
</div>
|
||||
<div class="order-detail-item order-detail-item--full" v-if="detail.return_logistics.sync_error">
|
||||
<div class="order-detail-item__label">同步异常</div>
|
||||
<div class="order-detail-item__value">{{ detail.return_logistics.sync_error }}</div>
|
||||
</div>
|
||||
<div class="order-detail-item order-detail-item--full" v-if="detail.return_logistics.latest_time">
|
||||
<div class="order-detail-item__label">最新更新时间</div>
|
||||
<div class="order-detail-item__value">{{ detail.return_logistics.latest_time }}</div>
|
||||
@@ -738,6 +825,18 @@ onMounted(fetchOrders);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-card" v-if="detail.return_logistics" style="grid-column: 1 / -1">
|
||||
<div class="detail-card__title">回寄物流轨迹</div>
|
||||
<div v-if="detail.return_logistics.nodes.length" class="timeline-list" style="margin-top: 14px">
|
||||
<div v-for="item in detail.return_logistics.nodes" :key="`${item.node_time}-${item.node_desc}`" class="timeline-node">
|
||||
<div class="timeline-node__title">{{ item.node_desc }}</div>
|
||||
<div class="timeline-node__time">{{ item.node_time }}</div>
|
||||
<div class="timeline-node__desc">{{ item.node_location || "-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无回寄物流轨迹" :image-size="64" />
|
||||
</div>
|
||||
|
||||
<div class="detail-card" v-if="detail.supplement_task" style="grid-column: 1 / -1">
|
||||
<div class="detail-card__title">补图任务</div>
|
||||
<div class="detail-card__desc">
|
||||
@@ -810,6 +909,20 @@ onMounted(fetchOrders);
|
||||
<el-form-item label="回寄运单号">
|
||||
<el-input v-model="returnTrackingNo" placeholder="请输入回寄运单号" />
|
||||
</el-form-item>
|
||||
<div v-if="returnRecognitionLoading" style="margin: -8px 0 12px; color: var(--admin-text-subtle); font-size: 12px;">
|
||||
正在识别快递公司...
|
||||
</div>
|
||||
<div v-if="returnRecognitionCandidates.length" style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px;">
|
||||
<el-tag
|
||||
v-for="candidate in returnRecognitionCandidates"
|
||||
:key="`${candidate.company_code}-${candidate.company_name}`"
|
||||
effect="plain"
|
||||
style="cursor: pointer;"
|
||||
@click="chooseReturnRecognitionCandidate(candidate)"
|
||||
>
|
||||
{{ candidate.company_name }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="detail?.return_address"
|
||||
type="info"
|
||||
|
||||
@@ -10,7 +10,7 @@ const uploadingKey = ref("");
|
||||
const groups = ref<AdminSystemConfigGroupItem[]>([]);
|
||||
const groupSnapshots = ref<Record<string, Record<string, string>>>({});
|
||||
|
||||
const groupOrder = ["file_storage", "mini_program", "h5", "payment", "sms"];
|
||||
const groupOrder = ["file_storage", "mini_program", "h5", "payment", "sms", "kuaidi100"];
|
||||
|
||||
function cloneSnapshot(groupsList: AdminSystemConfigGroupItem[]) {
|
||||
return Object.fromEntries(
|
||||
|
||||
Reference in New Issue
Block a user