chore: release updated anxinyan version
This commit is contained in:
@@ -499,6 +499,7 @@ export interface AdminReportListItem {
|
|||||||
zhongjian_report_no: string;
|
zhongjian_report_no: string;
|
||||||
report_entry_admin_name: string;
|
report_entry_admin_name: string;
|
||||||
report_entered_at: string;
|
report_entered_at: string;
|
||||||
|
trace_info_visible: boolean;
|
||||||
product_name: string;
|
product_name: string;
|
||||||
category_name: string;
|
category_name: string;
|
||||||
brand_name: string;
|
brand_name: string;
|
||||||
@@ -525,6 +526,7 @@ export interface AdminReportDetail {
|
|||||||
report_entry_admin_id: number;
|
report_entry_admin_id: number;
|
||||||
report_entry_admin_name: string;
|
report_entry_admin_name: string;
|
||||||
report_entered_at: string;
|
report_entered_at: string;
|
||||||
|
trace_info_visible: boolean;
|
||||||
};
|
};
|
||||||
product_info: Record<string, any>;
|
product_info: Record<string, any>;
|
||||||
result_info: Record<string, any>;
|
result_info: Record<string, any>;
|
||||||
@@ -1168,6 +1170,29 @@ export interface AdminWarehousePayload {
|
|||||||
remark: string;
|
remark: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminExpressCompanyItem {
|
||||||
|
id: number;
|
||||||
|
company_name: string;
|
||||||
|
company_code: string;
|
||||||
|
status: string;
|
||||||
|
status_text: string;
|
||||||
|
is_default: boolean;
|
||||||
|
sort_order: number;
|
||||||
|
remark: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminExpressCompanyPayload {
|
||||||
|
id?: number;
|
||||||
|
company_name: string;
|
||||||
|
company_code: string;
|
||||||
|
status: string;
|
||||||
|
is_default: boolean;
|
||||||
|
sort_order: number;
|
||||||
|
remark: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminMaterialTagCode {
|
export interface AdminMaterialTagCode {
|
||||||
id: number;
|
id: number;
|
||||||
batch_id: number;
|
batch_id: number;
|
||||||
@@ -1752,6 +1777,16 @@ export const adminApi = {
|
|||||||
data: AdminPublishReportResponse & { material_tag?: AdminMaterialTagCode | null };
|
data: AdminPublishReportResponse & { material_tag?: AdminMaterialTagCode | null };
|
||||||
}>;
|
}>;
|
||||||
},
|
},
|
||||||
|
updateReportTraceVisibility(id: number, visible: boolean) {
|
||||||
|
return request.post("/api/admin/report/trace-visibility", {
|
||||||
|
id,
|
||||||
|
trace_info_visible: visible,
|
||||||
|
}) as Promise<{
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: { id: number; trace_info_visible: boolean };
|
||||||
|
}>;
|
||||||
|
},
|
||||||
saveInspectionReport(data: AdminManualInspectionPayload) {
|
saveInspectionReport(data: AdminManualInspectionPayload) {
|
||||||
return request.post("/api/admin/report/inspection/save", data) as Promise<{
|
return request.post("/api/admin/report/inspection/save", data) as Promise<{
|
||||||
code: number;
|
code: number;
|
||||||
@@ -2242,6 +2277,23 @@ export const adminApi = {
|
|||||||
data: { id: number };
|
data: { id: number };
|
||||||
}>;
|
}>;
|
||||||
},
|
},
|
||||||
|
getExpressCompanies(params?: { enabled_only?: 0 | 1 }) {
|
||||||
|
return request.get("/api/admin/express-companies", { params }) as Promise<{
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: {
|
||||||
|
list: AdminExpressCompanyItem[];
|
||||||
|
default_company: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
},
|
||||||
|
saveExpressCompany(data: AdminExpressCompanyPayload) {
|
||||||
|
return request.post("/api/admin/express-company/save", data) as Promise<{
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: { id: number };
|
||||||
|
}>;
|
||||||
|
},
|
||||||
getMaterialBatches(params?: Record<string, string>) {
|
getMaterialBatches(params?: Record<string, string>) {
|
||||||
return request.get("/api/admin/material/batches", { params }) as Promise<{
|
return request.get("/api/admin/material/batches", { params }) as Promise<{
|
||||||
code: number;
|
code: number;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const menus = [
|
|||||||
{ index: "users", label: "用户管理", icon: User, permission: "users.manage" },
|
{ index: "users", label: "用户管理", icon: User, permission: "users.manage" },
|
||||||
{ index: "customers", label: "客户管理", icon: Connection, permission: "customers.manage" },
|
{ index: "customers", label: "客户管理", icon: Connection, permission: "customers.manage" },
|
||||||
{ index: "warehouses", label: "仓库中心", icon: OfficeBuilding, permission: "warehouses.manage" },
|
{ index: "warehouses", label: "仓库中心", icon: OfficeBuilding, permission: "warehouses.manage" },
|
||||||
|
{ index: "express-companies", label: "快递公司", icon: Van, permission: "warehouses.manage" },
|
||||||
{ index: "materials", label: "物料管理", icon: Box, permission: "materials.manage" },
|
{ index: "materials", label: "物料管理", icon: Box, permission: "materials.manage" },
|
||||||
{ index: "access", label: "权限中心", icon: Lock, permission: "access.manage" },
|
{ index: "access", label: "权限中心", icon: Lock, permission: "access.manage" },
|
||||||
{ index: "content", label: "内容中心", icon: DocumentChecked, permission: "system.manage" },
|
{ index: "content", label: "内容中心", icon: DocumentChecked, permission: "system.manage" },
|
||||||
|
|||||||
@@ -745,7 +745,7 @@ function normalizedKeyPoints() {
|
|||||||
point_code: item.point_code,
|
point_code: item.point_code,
|
||||||
point_name: item.point_name,
|
point_name: item.point_name,
|
||||||
point_value: item.point_value.trim(),
|
point_value: item.point_value.trim(),
|
||||||
point_remark: item.point_remark.trim(),
|
point_remark: "",
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1577,7 +1577,7 @@ onMounted(async () => {
|
|||||||
<div class="task-form-block">
|
<div class="task-form-block">
|
||||||
<div class="task-form-block__title">鉴定模板填写</div>
|
<div class="task-form-block__title">鉴定模板填写</div>
|
||||||
<div class="task-panel__desc">
|
<div class="task-panel__desc">
|
||||||
选择品类后自动加载对应模板,按关键项逐项填写检查结论和备注。
|
选择品类后自动加载对应模板,按关键项逐项填写检查结果。
|
||||||
</div>
|
</div>
|
||||||
<div v-loading="appraisalTemplateLoading">
|
<div v-loading="appraisalTemplateLoading">
|
||||||
<div v-if="resultKeyPoints.length" class="task-key-point-list">
|
<div v-if="resultKeyPoints.length" class="task-key-point-list">
|
||||||
@@ -1619,10 +1619,6 @@ onMounted(async () => {
|
|||||||
/>
|
/>
|
||||||
<el-input v-else v-model="item.point_value" :disabled="isTaskReadonly" placeholder="请输入检查结果" />
|
<el-input v-else v-model="item.point_value" :disabled="isTaskReadonly" placeholder="请输入检查结果" />
|
||||||
</div>
|
</div>
|
||||||
<div class="task-form-field task-form-field--full">
|
|
||||||
<div class="task-form-field__label">备注</div>
|
|
||||||
<el-input v-model="item.point_remark" :disabled="isTaskReadonly" type="textarea" :rows="2" placeholder="可补充细节、依据或风险点" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
193
admin-web/src/pages/express-companies/index.vue
Normal file
193
admin-web/src/pages/express-companies/index.vue
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import {
|
||||||
|
adminApi,
|
||||||
|
type AdminExpressCompanyItem,
|
||||||
|
type AdminExpressCompanyPayload,
|
||||||
|
} from "../../api/admin";
|
||||||
|
import OrderStatusTag from "../../components/OrderStatusTag.vue";
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const submitting = ref(false);
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const companies = ref<AdminExpressCompanyItem[]>([]);
|
||||||
|
const defaultCompany = ref("");
|
||||||
|
|
||||||
|
const enabledCount = computed(() => companies.value.filter((item) => item.status === "enabled").length);
|
||||||
|
|
||||||
|
const form = reactive<AdminExpressCompanyPayload>({
|
||||||
|
company_name: "",
|
||||||
|
company_code: "",
|
||||||
|
status: "enabled",
|
||||||
|
is_default: false,
|
||||||
|
sort_order: 0,
|
||||||
|
remark: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchCompanies() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await adminApi.getExpressCompanies();
|
||||||
|
companies.value = response.data.list;
|
||||||
|
defaultCompany.value = response.data.default_company;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
ElMessage.error("快递公司加载失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDialog(row?: AdminExpressCompanyItem) {
|
||||||
|
if (row) {
|
||||||
|
form.id = row.id;
|
||||||
|
form.company_name = row.company_name;
|
||||||
|
form.company_code = row.company_code;
|
||||||
|
form.status = row.status;
|
||||||
|
form.is_default = row.is_default;
|
||||||
|
form.sort_order = row.sort_order;
|
||||||
|
form.remark = row.remark;
|
||||||
|
} else {
|
||||||
|
form.id = undefined;
|
||||||
|
form.company_name = "";
|
||||||
|
form.company_code = "";
|
||||||
|
form.status = "enabled";
|
||||||
|
form.is_default = companies.value.length === 0;
|
||||||
|
form.sort_order = companies.value.length + 1;
|
||||||
|
form.remark = "";
|
||||||
|
}
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!form.company_name.trim()) {
|
||||||
|
ElMessage.warning("请填写快递公司名称");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (form.is_default && form.status !== "enabled") {
|
||||||
|
ElMessage.warning("默认快递公司必须保持启用");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
await adminApi.saveExpressCompany({
|
||||||
|
...form,
|
||||||
|
company_name: form.company_name.trim(),
|
||||||
|
company_code: form.company_code.trim(),
|
||||||
|
remark: form.remark.trim(),
|
||||||
|
});
|
||||||
|
ElMessage.success(form.id ? "快递公司已更新" : "快递公司已创建");
|
||||||
|
dialogVisible.value = false;
|
||||||
|
await fetchCompanies();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
ElMessage.error(error?.message || "快递公司保存失败");
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusTagText(row: AdminExpressCompanyItem) {
|
||||||
|
return row.is_default ? `${row.status_text} / 默认` : row.status_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchCompanies);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-loading="loading">
|
||||||
|
<div class="metric-grid" style="margin-bottom: 18px">
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-card__label">快递公司总数</div>
|
||||||
|
<div class="metric-card__value">{{ companies.length }}</div>
|
||||||
|
<div class="metric-card__desc">当前已维护的可选快递公司</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-card__label">启用中</div>
|
||||||
|
<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 metric-card__value--text">{{ defaultCompany || "-" }}</div>
|
||||||
|
<div class="metric-card__desc">新登记寄回运单时默认选中</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card class="panel-card" shadow="never">
|
||||||
|
<div class="filters-row" style="justify-content: space-between;">
|
||||||
|
<div style="color: var(--admin-text-subtle);">
|
||||||
|
维护仓管寄回时可选的快递公司。停用后不会出现在寄回下拉列表中。
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" @click="openDialog()">新增快递公司</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 label="状态" min-width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<OrderStatusTag :status="statusTagText(row)" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="sort_order" label="排序" min-width="90" />
|
||||||
|
<el-table-column prop="remark" label="备注" min-width="240" />
|
||||||
|
<el-table-column prop="updated_at" label="更新时间" min-width="170" />
|
||||||
|
<el-table-column label="操作" fixed="right" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="openDialog(row)">编辑</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible" :title="form.id ? '编辑快递公司' : '新增快递公司'" width="560px">
|
||||||
|
<el-form label-position="top">
|
||||||
|
<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>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="form.status" style="width: 100%">
|
||||||
|
<el-option label="启用" value="enabled" />
|
||||||
|
<el-option label="停用" value="disabled" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="排序值">
|
||||||
|
<el-input v-model.number="form.sort_order" type="number" placeholder="越小越靠前" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="默认设置">
|
||||||
|
<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-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.metric-card__value--text {
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 24px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { adminApi, type AdminManualOrderCreatePayload, type AdminManualOrderMeta, type AdminOrderDetail, type AdminOrderListItem, type AdminOrderWarehouseOption } from "../../api/admin";
|
import { adminApi, type AdminExpressCompanyItem, type AdminManualOrderCreatePayload, type AdminManualOrderMeta, type AdminOrderDetail, type AdminOrderListItem, type AdminOrderWarehouseOption } from "../../api/admin";
|
||||||
import OrderStatusTag from "../../components/OrderStatusTag.vue";
|
import OrderStatusTag from "../../components/OrderStatusTag.vue";
|
||||||
|
import { recognizeReturnAddress } from "../../utils/address-recognition";
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const detailLoading = ref(false);
|
const detailLoading = ref(false);
|
||||||
@@ -18,11 +19,15 @@ const returnDialogVisible = ref(false);
|
|||||||
const returnSubmitting = ref(false);
|
const returnSubmitting = ref(false);
|
||||||
const returnExpressCompany = ref("");
|
const returnExpressCompany = ref("");
|
||||||
const returnTrackingNo = ref("");
|
const returnTrackingNo = ref("");
|
||||||
|
const expressCompanyLoading = ref(false);
|
||||||
|
const expressCompanyOptions = ref<AdminExpressCompanyItem[]>([]);
|
||||||
|
const defaultExpressCompany = ref("");
|
||||||
const manualDialogVisible = ref(false);
|
const manualDialogVisible = ref(false);
|
||||||
const manualSubmitting = ref(false);
|
const manualSubmitting = ref(false);
|
||||||
const manualMetaLoading = ref(false);
|
const manualMetaLoading = ref(false);
|
||||||
const manualMeta = ref<AdminManualOrderMeta>({ categories: [], brands: [] });
|
const manualMeta = ref<AdminManualOrderMeta>({ categories: [], brands: [] });
|
||||||
const manualForm = ref<AdminManualOrderCreatePayload>(createManualOrderForm());
|
const manualForm = ref<AdminManualOrderCreatePayload>(createManualOrderForm());
|
||||||
|
const manualAddressRecognitionText = ref("");
|
||||||
|
|
||||||
const keyword = ref("");
|
const keyword = ref("");
|
||||||
const serviceProvider = ref("");
|
const serviceProvider = ref("");
|
||||||
@@ -113,6 +118,27 @@ const logisticsActionText = computed(() => {
|
|||||||
const canSubmitReturnLogistics = computed(() => Boolean(detail.value?.order_info.can_submit_return_logistics));
|
const canSubmitReturnLogistics = computed(() => Boolean(detail.value?.order_info.can_submit_return_logistics));
|
||||||
const returnLogisticsBlockReason = computed(() => detail.value?.order_info.return_logistics_block_reason || "");
|
const returnLogisticsBlockReason = computed(() => detail.value?.order_info.return_logistics_block_reason || "");
|
||||||
const canMarkReturnReceived = computed(() => Boolean(detail.value?.order_info.can_mark_return_received));
|
const canMarkReturnReceived = computed(() => Boolean(detail.value?.order_info.can_mark_return_received));
|
||||||
|
const expressCompanySelectOptions = computed(() => {
|
||||||
|
if (!returnExpressCompany.value || expressCompanyOptions.value.some((item) => item.company_name === returnExpressCompany.value)) {
|
||||||
|
return expressCompanyOptions.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
company_name: returnExpressCompany.value,
|
||||||
|
company_code: returnExpressCompany.value,
|
||||||
|
status: "enabled",
|
||||||
|
status_text: "启用中",
|
||||||
|
is_default: false,
|
||||||
|
sort_order: 0,
|
||||||
|
remark: "",
|
||||||
|
created_at: "",
|
||||||
|
updated_at: "",
|
||||||
|
},
|
||||||
|
...expressCompanyOptions.value,
|
||||||
|
];
|
||||||
|
});
|
||||||
function createManualOrderForm(): AdminManualOrderCreatePayload {
|
function createManualOrderForm(): AdminManualOrderCreatePayload {
|
||||||
return {
|
return {
|
||||||
service_provider: "anxinyan",
|
service_provider: "anxinyan",
|
||||||
@@ -185,10 +211,40 @@ async function ensureManualMeta() {
|
|||||||
|
|
||||||
async function openManualDialog() {
|
async function openManualDialog() {
|
||||||
manualForm.value = createManualOrderForm();
|
manualForm.value = createManualOrderForm();
|
||||||
|
manualAddressRecognitionText.value = "";
|
||||||
manualDialogVisible.value = true;
|
manualDialogVisible.value = true;
|
||||||
await ensureManualMeta();
|
await ensureManualMeta();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureExpressCompanyOptions() {
|
||||||
|
if (expressCompanyOptions.value.length) return;
|
||||||
|
expressCompanyLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await adminApi.getExpressCompanies({ enabled_only: 1 });
|
||||||
|
expressCompanyOptions.value = response.data.list;
|
||||||
|
defaultExpressCompany.value = response.data.default_company;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
ElMessage.error("快递公司列表加载失败");
|
||||||
|
} finally {
|
||||||
|
expressCompanyLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRecognizedManualAddress() {
|
||||||
|
const result = recognizeReturnAddress(manualAddressRecognitionText.value);
|
||||||
|
if (!result.ok || !result.address) {
|
||||||
|
ElMessage.warning(result.message || "寄回地址识别失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
manualForm.value.return_address = {
|
||||||
|
...manualForm.value.return_address,
|
||||||
|
...result.address,
|
||||||
|
};
|
||||||
|
ElMessage.success("寄回地址已识别并填入");
|
||||||
|
}
|
||||||
|
|
||||||
function validateManualForm() {
|
function validateManualForm() {
|
||||||
const form = manualForm.value;
|
const form = manualForm.value;
|
||||||
if (!form.product_info.category_id) {
|
if (!form.product_info.category_id) {
|
||||||
@@ -312,13 +368,14 @@ async function submitWarehouseReassign() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openReturnDialog() {
|
async function openReturnDialog() {
|
||||||
if (!detail.value) return;
|
if (!detail.value) return;
|
||||||
if (!canSubmitReturnLogistics.value) {
|
if (!canSubmitReturnLogistics.value) {
|
||||||
ElMessage.warning(returnLogisticsBlockReason.value || "当前订单暂不支持登记回寄运单");
|
ElMessage.warning(returnLogisticsBlockReason.value || "当前订单暂不支持登记回寄运单");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
returnExpressCompany.value = detail.value.return_logistics?.express_company || "";
|
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 || "";
|
returnTrackingNo.value = detail.value.return_logistics?.tracking_no || "";
|
||||||
returnDialogVisible.value = true;
|
returnDialogVisible.value = true;
|
||||||
}
|
}
|
||||||
@@ -735,7 +792,20 @@ onMounted(fetchOrders);
|
|||||||
<el-dialog v-model="returnDialogVisible" title="登记回寄运单" width="520px">
|
<el-dialog v-model="returnDialogVisible" title="登记回寄运单" width="520px">
|
||||||
<el-form label-position="top">
|
<el-form label-position="top">
|
||||||
<el-form-item label="回寄快递公司">
|
<el-form-item label="回寄快递公司">
|
||||||
<el-input v-model="returnExpressCompany" placeholder="例如:顺丰速运" />
|
<el-select
|
||||||
|
v-model="returnExpressCompany"
|
||||||
|
:loading="expressCompanyLoading"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择回寄快递公司"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in expressCompanySelectOptions"
|
||||||
|
:key="`${item.id}-${item.company_name}`"
|
||||||
|
:label="item.company_name"
|
||||||
|
:value="item.company_name"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="回寄运单号">
|
<el-form-item label="回寄运单号">
|
||||||
<el-input v-model="returnTrackingNo" placeholder="请输入回寄运单号" />
|
<el-input v-model="returnTrackingNo" placeholder="请输入回寄运单号" />
|
||||||
@@ -810,6 +880,18 @@ onMounted(fetchOrders);
|
|||||||
<div class="manual-section">
|
<div class="manual-section">
|
||||||
<div class="manual-section__title">寄回信息</div>
|
<div class="manual-section__title">寄回信息</div>
|
||||||
<el-form label-position="top">
|
<el-form label-position="top">
|
||||||
|
<el-form-item label="自动识别寄回地址">
|
||||||
|
<div class="manual-address-recognition">
|
||||||
|
<el-input
|
||||||
|
v-model="manualAddressRecognitionText"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
resize="none"
|
||||||
|
placeholder="粘贴收货人、收货电话、收货地址,自动识别后填入下方字段"
|
||||||
|
/>
|
||||||
|
<el-button @click="applyRecognizedManualAddress">识别并填入</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<div class="manual-grid">
|
<div class="manual-grid">
|
||||||
<el-form-item label="收件人">
|
<el-form-item label="收件人">
|
||||||
<el-input v-model="manualForm.return_address.consignee" placeholder="用于匹配或创建用户" />
|
<el-input v-model="manualForm.return_address.consignee" placeholder="用于匹配或创建用户" />
|
||||||
@@ -985,6 +1067,15 @@ onMounted(fetchOrders);
|
|||||||
gap: 0 18px;
|
gap: 0 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.manual-address-recognition {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-address-recognition .el-button {
|
||||||
|
justify-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.manual-upload-head {
|
.manual-upload-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const drawerVisible = ref(false);
|
|||||||
const inspectionDrawerVisible = ref(false);
|
const inspectionDrawerVisible = ref(false);
|
||||||
const inspectionSubmitting = ref(false);
|
const inspectionSubmitting = ref(false);
|
||||||
const publishingId = ref<number | null>(null);
|
const publishingId = ref<number | null>(null);
|
||||||
|
const traceVisibilitySavingId = ref<number | null>(null);
|
||||||
const detailQrDataUrl = ref("");
|
const detailQrDataUrl = ref("");
|
||||||
|
|
||||||
const keyword = ref("");
|
const keyword = ref("");
|
||||||
@@ -279,6 +280,7 @@ type PublishReportTarget = Pick<AdminReportListItem, "id" | "report_status" | "r
|
|||||||
report_type: string;
|
report_type: string;
|
||||||
material_tag_bound: boolean;
|
material_tag_bound: boolean;
|
||||||
};
|
};
|
||||||
|
type ReportTraceVisibilityTarget = Pick<AdminReportListItem, "id" | "trace_info_visible">;
|
||||||
|
|
||||||
async function promptReportMaterialTagInput() {
|
async function promptReportMaterialTagInput() {
|
||||||
try {
|
try {
|
||||||
@@ -344,6 +346,46 @@ async function publishReport(row: PublishReportTarget) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchValueToBoolean(value: unknown) {
|
||||||
|
if (typeof value === "boolean") return value;
|
||||||
|
if (typeof value === "number") return value === 1;
|
||||||
|
return ["1", "true", "yes", "on"].includes(String(value).trim().toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateReportTraceVisibility(row: ReportTraceVisibilityTarget, value: unknown) {
|
||||||
|
const visible = switchValueToBoolean(value);
|
||||||
|
traceVisibilitySavingId.value = row.id;
|
||||||
|
try {
|
||||||
|
const response = await adminApi.updateReportTraceVisibility(row.id, visible);
|
||||||
|
if (response.code !== 0) {
|
||||||
|
ElMessage.error(response.message || "追溯信息开关保存失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appliedVisible = Boolean(response.data.trace_info_visible);
|
||||||
|
row.trace_info_visible = appliedVisible;
|
||||||
|
|
||||||
|
const listItem = reports.value.find((item) => item.id === row.id);
|
||||||
|
if (listItem) {
|
||||||
|
listItem.trace_info_visible = appliedVisible;
|
||||||
|
}
|
||||||
|
if (detail.value?.report_header.id === row.id) {
|
||||||
|
detail.value.report_header.trace_info_visible = appliedVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success(response.message || (appliedVisible ? "追溯信息已设为显示" : "追溯信息已隐藏"));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
ElMessage.error("追溯信息开关保存失败");
|
||||||
|
} finally {
|
||||||
|
traceVisibilitySavingId.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTraceVisibilityChange(row: ReportTraceVisibilityTarget, value: unknown) {
|
||||||
|
void updateReportTraceVisibility(row, value);
|
||||||
|
}
|
||||||
|
|
||||||
function validateInspectionForm() {
|
function validateInspectionForm() {
|
||||||
const { report_header, product_info, result_info } = inspectionForm.value;
|
const { report_header, product_info, result_info } = inspectionForm.value;
|
||||||
if (!report_header.report_title.trim()) {
|
if (!report_header.report_title.trim()) {
|
||||||
@@ -466,6 +508,18 @@ watch(
|
|||||||
<span v-else class="detail-label">不适用</span>
|
<span v-else class="detail-label">不适用</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="追溯信息" min-width="130">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-switch
|
||||||
|
:model-value="row.trace_info_visible"
|
||||||
|
:loading="traceVisibilitySavingId === row.id"
|
||||||
|
inline-prompt
|
||||||
|
active-text="显示"
|
||||||
|
inactive-text="隐藏"
|
||||||
|
@change="handleTraceVisibilityChange(row, $event)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="institution_name" label="出具机构" min-width="160" />
|
<el-table-column prop="institution_name" label="出具机构" min-width="160" />
|
||||||
<el-table-column prop="publish_time" label="发布时间" min-width="170" />
|
<el-table-column prop="publish_time" label="发布时间" min-width="170" />
|
||||||
<el-table-column label="操作" fixed="right" width="220">
|
<el-table-column label="操作" fixed="right" width="220">
|
||||||
@@ -538,6 +592,20 @@ watch(
|
|||||||
<div class="detail-label">出具机构</div>
|
<div class="detail-label">出具机构</div>
|
||||||
<div class="detail-value">{{ detail.report_header.institution_name }}</div>
|
<div class="detail-value">{{ detail.report_header.institution_name }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-card__desc">
|
||||||
|
<div class="detail-label">追溯信息</div>
|
||||||
|
<div class="detail-value report-visibility-control">
|
||||||
|
<el-switch
|
||||||
|
:model-value="detail.report_header.trace_info_visible"
|
||||||
|
:loading="traceVisibilitySavingId === detail.report_header.id"
|
||||||
|
inline-prompt
|
||||||
|
active-text="显示"
|
||||||
|
inactive-text="隐藏"
|
||||||
|
@change="handleTraceVisibilityChange(detail.report_header, $event)"
|
||||||
|
/>
|
||||||
|
<span>{{ detail.report_header.trace_info_visible ? "用户端显示追溯信息 tab" : "用户端隐藏追溯信息 tab" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-card">
|
<div class="detail-card">
|
||||||
@@ -599,7 +667,7 @@ watch(
|
|||||||
<template v-if="detail.result_info.key_points?.length">
|
<template v-if="detail.result_info.key_points?.length">
|
||||||
<div v-for="(item, index) in detail.result_info.key_points" :key="`${item.point_code || item.point_name}-${index}`" class="detail-card__desc">
|
<div v-for="(item, index) in detail.result_info.key_points" :key="`${item.point_code || item.point_name}-${index}`" class="detail-card__desc">
|
||||||
<div class="detail-label">{{ item.point_name || "鉴定项" }}</div>
|
<div class="detail-label">{{ item.point_name || "鉴定项" }}</div>
|
||||||
<div class="detail-value">{{ [item.point_value, item.point_remark].filter(Boolean).join(";") || "-" }}</div>
|
<div class="detail-value">{{ item.point_value || "-" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="detail.result_info.external_remark" class="detail-card__desc">
|
<div v-if="detail.result_info.external_remark" class="detail-card__desc">
|
||||||
@@ -987,4 +1055,17 @@ watch(
|
|||||||
.report-evidence-card__body {
|
.report-evidence-card__body {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.report-visibility-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-visibility-control span {
|
||||||
|
color: var(--admin-text-subtle);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, defineComponent, h, nextTick, ref, type PropType } from "vue";
|
import { computed, defineComponent, h, nextTick, onMounted, ref, type PropType } from "vue";
|
||||||
import { ElMessage, type InputInstance } from "element-plus";
|
import { ElMessage, type InputInstance } from "element-plus";
|
||||||
import {
|
import {
|
||||||
adminApi,
|
adminApi,
|
||||||
|
type AdminExpressCompanyItem,
|
||||||
type AdminFileAsset,
|
type AdminFileAsset,
|
||||||
type AdminReportDetail,
|
type AdminReportDetail,
|
||||||
type AdminWarehouseWorkbenchContext,
|
type AdminWarehouseWorkbenchContext,
|
||||||
@@ -20,6 +21,9 @@ const returnTagNo = ref("");
|
|||||||
const returnMaterialQr = ref("");
|
const returnMaterialQr = ref("");
|
||||||
const returnExpressCompany = ref("");
|
const returnExpressCompany = ref("");
|
||||||
const returnTrackingNo = ref("");
|
const returnTrackingNo = ref("");
|
||||||
|
const expressCompanyLoading = ref(false);
|
||||||
|
const expressCompanyOptions = ref<AdminExpressCompanyItem[]>([]);
|
||||||
|
const defaultExpressCompany = ref("");
|
||||||
const inboundAttachments = ref<AdminFileAsset[]>([]);
|
const inboundAttachments = ref<AdminFileAsset[]>([]);
|
||||||
const returnPackingAttachments = ref<AdminFileAsset[]>([]);
|
const returnPackingAttachments = ref<AdminFileAsset[]>([]);
|
||||||
|
|
||||||
@@ -47,6 +51,27 @@ const returnReportActionText = computed(() => {
|
|||||||
if (currentReturnIsZhongjian.value || returnMaterialMatched.value) return "核对报告";
|
if (currentReturnIsZhongjian.value || returnMaterialMatched.value) return "核对报告";
|
||||||
return "匹配吊牌并核对报告";
|
return "匹配吊牌并核对报告";
|
||||||
});
|
});
|
||||||
|
const expressCompanySelectOptions = computed(() => {
|
||||||
|
if (!returnExpressCompany.value || expressCompanyOptions.value.some((item) => item.company_name === returnExpressCompany.value)) {
|
||||||
|
return expressCompanyOptions.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
company_name: returnExpressCompany.value,
|
||||||
|
company_code: returnExpressCompany.value,
|
||||||
|
status: "enabled",
|
||||||
|
status_text: "启用中",
|
||||||
|
is_default: false,
|
||||||
|
sort_order: 0,
|
||||||
|
remark: "",
|
||||||
|
created_at: "",
|
||||||
|
updated_at: "",
|
||||||
|
},
|
||||||
|
...expressCompanyOptions.value,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
const OrderContextCard = defineComponent({
|
const OrderContextCard = defineComponent({
|
||||||
name: "OrderContextCard",
|
name: "OrderContextCard",
|
||||||
@@ -225,6 +250,21 @@ function removeInboundAttachment(fileUrl: string) {
|
|||||||
inboundAttachments.value = inboundAttachments.value.filter((item) => item.file_url !== fileUrl);
|
inboundAttachments.value = inboundAttachments.value.filter((item) => item.file_url !== fileUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureExpressCompanyOptions() {
|
||||||
|
if (expressCompanyOptions.value.length) return;
|
||||||
|
expressCompanyLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await adminApi.getExpressCompanies({ enabled_only: 1 });
|
||||||
|
expressCompanyOptions.value = response.data.list;
|
||||||
|
defaultExpressCompany.value = response.data.default_company;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
ElMessage.error("快递公司列表加载失败");
|
||||||
|
} finally {
|
||||||
|
expressCompanyLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function lookupZhongjian() {
|
async function lookupZhongjian() {
|
||||||
if (!zhongjianTagNo.value.trim()) {
|
if (!zhongjianTagNo.value.trim()) {
|
||||||
ElMessage.warning("请扫描内部流转码");
|
ElMessage.warning("请扫描内部流转码");
|
||||||
@@ -274,8 +314,9 @@ async function lookupReturn() {
|
|||||||
}
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
|
await ensureExpressCompanyOptions();
|
||||||
returnMaterialQr.value = "";
|
returnMaterialQr.value = "";
|
||||||
returnExpressCompany.value = "";
|
returnExpressCompany.value = defaultExpressCompany.value || expressCompanyOptions.value[0]?.company_name || "";
|
||||||
returnTrackingNo.value = "";
|
returnTrackingNo.value = "";
|
||||||
returnPackingAttachments.value = [];
|
returnPackingAttachments.value = [];
|
||||||
returnReviewReport.value = null;
|
returnReviewReport.value = null;
|
||||||
@@ -449,6 +490,10 @@ function openFile(url: string) {
|
|||||||
if (!url) return;
|
if (!url) return;
|
||||||
window.open(url, "_blank", "noopener,noreferrer");
|
window.open(url, "_blank", "noopener,noreferrer");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void ensureExpressCompanyOptions();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -570,7 +615,21 @@ function openFile(url: string) {
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="returnContext && returnConfirmed" class="return-form">
|
<div v-if="returnContext && returnConfirmed" class="return-form">
|
||||||
<el-input v-model="returnExpressCompany" size="large" placeholder="回寄快递公司,例如:顺丰速运" />
|
<el-select
|
||||||
|
v-model="returnExpressCompany"
|
||||||
|
:loading="expressCompanyLoading"
|
||||||
|
filterable
|
||||||
|
size="large"
|
||||||
|
placeholder="请选择回寄快递公司"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in expressCompanySelectOptions"
|
||||||
|
:key="`${item.id}-${item.company_name}`"
|
||||||
|
:label="item.company_name"
|
||||||
|
:value="item.company_name"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
<el-input ref="returnTrackingInputRef" v-model="returnTrackingNo" size="large" placeholder="扫描或输入回寄运单号" @keyup.enter="shipReturn" />
|
<el-input ref="returnTrackingInputRef" v-model="returnTrackingNo" size="large" placeholder="扫描或输入回寄运单号" @keyup.enter="shipReturn" />
|
||||||
<div class="packing-upload">
|
<div class="packing-upload">
|
||||||
<div class="packing-upload-head">
|
<div class="packing-upload-head">
|
||||||
|
|||||||
@@ -113,6 +113,16 @@ const adminChildren = [
|
|||||||
permission: "warehouses.manage",
|
permission: "warehouses.manage",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "express-companies",
|
||||||
|
name: "express-companies",
|
||||||
|
component: () => import("../pages/express-companies/index.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "快递公司",
|
||||||
|
desc: "维护仓管寄回时可选择的快递公司和默认项。",
|
||||||
|
permission: "warehouses.manage",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "materials",
|
path: "materials",
|
||||||
name: "materials",
|
name: "materials",
|
||||||
|
|||||||
1
admin-web/src/static/regions/pca.json
Normal file
1
admin-web/src/static/regions/pca.json
Normal file
File diff suppressed because one or more lines are too long
213
admin-web/src/utils/address-recognition.ts
Normal file
213
admin-web/src/utils/address-recognition.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import regionSource from "../static/regions/pca.json";
|
||||||
|
|
||||||
|
type RegionNode = {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
children?: RegionNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RecognizedReturnAddress = {
|
||||||
|
consignee: string;
|
||||||
|
mobile: string;
|
||||||
|
province: string;
|
||||||
|
city: string;
|
||||||
|
district: string;
|
||||||
|
detail_address: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RecognizeReturnAddressResult = {
|
||||||
|
ok: boolean;
|
||||||
|
address?: RecognizedReturnAddress;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const regionTree = regionSource as RegionNode[];
|
||||||
|
const nameLabels = ["收货人", "收件人", "姓名", "联系人", "取件人"];
|
||||||
|
const mobileLabels = ["收货电话", "联系电话", "手机号", "手机号码", "手机", "电话"];
|
||||||
|
const addressLabels = ["收货地址", "收件地址", "寄回地址", "地址"];
|
||||||
|
const allLabels = [...nameLabels, ...mobileLabels, ...addressLabels];
|
||||||
|
|
||||||
|
function normalizeLines(raw: string) {
|
||||||
|
return raw
|
||||||
|
.replace(/\r/g, "\n")
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelMatch(line: string, labels: string[]) {
|
||||||
|
for (const label of labels) {
|
||||||
|
const pattern = new RegExp(`^\\s*${label}\\s*[::]?\\s*(.*)$`);
|
||||||
|
const match = line.match(pattern);
|
||||||
|
if (match) {
|
||||||
|
return { label, value: String(match[1] || "").trim() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isKnownLabel(line: string) {
|
||||||
|
const normalized = line.replace(/\s+/g, "");
|
||||||
|
return allLabels.some((label) => normalized === label || normalized === `${label}:` || normalized === `${label}:`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractLabeledValue(lines: string[], labels: string[], block = false) {
|
||||||
|
for (let index = 0; index < lines.length; index += 1) {
|
||||||
|
const match = labelMatch(lines[index], labels);
|
||||||
|
if (!match) continue;
|
||||||
|
if (match.value) return match.value;
|
||||||
|
|
||||||
|
const values: string[] = [];
|
||||||
|
for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) {
|
||||||
|
const nextLine = lines[nextIndex];
|
||||||
|
if (labelMatch(nextLine, allLabels) && isKnownLabel(nextLine)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (labelMatch(nextLine, allLabels)?.value) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
values.push(nextLine);
|
||||||
|
if (!block) break;
|
||||||
|
}
|
||||||
|
return values.join(block ? "" : " ").trim();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMobile(value: string) {
|
||||||
|
const directMatch = value.match(/1[3-9]\d{9}/);
|
||||||
|
if (directMatch) return directMatch[0];
|
||||||
|
|
||||||
|
const digits = value.replace(/\D+/g, "");
|
||||||
|
return digits.match(/1[3-9]\d{9}/)?.[0] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripKnownAddressPrefixes(value: string) {
|
||||||
|
return value
|
||||||
|
.replace(/\s+/g, "")
|
||||||
|
.replace(/^(中国大陆|中华人民共和国|中国|大陆)+/, "")
|
||||||
|
.replace(/^(收货地址|收件地址|寄回地址|地址)[::]?/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function aliases(name: string) {
|
||||||
|
const suffixes = ["特别行政区", "壮族自治区", "回族自治区", "维吾尔自治区", "自治区", "自治州", "自治县", "地区", "省", "市", "区", "县", "旗", "盟"];
|
||||||
|
const values = [name];
|
||||||
|
for (const suffix of suffixes) {
|
||||||
|
if (name.endsWith(suffix) && name.length > suffix.length) {
|
||||||
|
values.push(name.slice(0, -suffix.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(new Set(values)).sort((a, b) => b.length - a.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function consumePrefix(text: string, names: string[]) {
|
||||||
|
for (const name of names) {
|
||||||
|
if (name && text.startsWith(name)) {
|
||||||
|
return { consumed: name.length, rest: text.slice(name.length) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDirectCity(province: RegionNode, city: RegionNode) {
|
||||||
|
return province.name === city.name || aliases(province.name).some((name) => aliases(city.name).includes(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchDistrict(city: RegionNode, text: string) {
|
||||||
|
for (const district of city.children || []) {
|
||||||
|
const match = consumePrefix(text, aliases(district.name));
|
||||||
|
if (match) {
|
||||||
|
return { district, detail: match.rest };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchRegion(addressText: string) {
|
||||||
|
const address = stripKnownAddressPrefixes(addressText);
|
||||||
|
if (!address) return null;
|
||||||
|
|
||||||
|
for (const province of regionTree) {
|
||||||
|
const provinceMatch = consumePrefix(address, aliases(province.name));
|
||||||
|
if (!provinceMatch) continue;
|
||||||
|
|
||||||
|
for (const city of province.children || []) {
|
||||||
|
const cityMatch = consumePrefix(provinceMatch.rest, aliases(city.name));
|
||||||
|
const districtSource = cityMatch ? cityMatch.rest : (isDirectCity(province, city) ? provinceMatch.rest : "");
|
||||||
|
if (!districtSource) continue;
|
||||||
|
|
||||||
|
const districtMatch = matchDistrict(city, districtSource);
|
||||||
|
if (districtMatch) {
|
||||||
|
return {
|
||||||
|
province: province.name,
|
||||||
|
city: city.name,
|
||||||
|
district: districtMatch.district.name,
|
||||||
|
detail_address: districtMatch.detail,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const province of regionTree) {
|
||||||
|
for (const city of province.children || []) {
|
||||||
|
const cityMatch = consumePrefix(address, aliases(city.name));
|
||||||
|
if (!cityMatch) continue;
|
||||||
|
|
||||||
|
const districtMatch = matchDistrict(city, cityMatch.rest);
|
||||||
|
if (districtMatch) {
|
||||||
|
return {
|
||||||
|
province: province.name,
|
||||||
|
city: city.name,
|
||||||
|
district: districtMatch.district.name,
|
||||||
|
detail_address: districtMatch.detail,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackAddressLine(lines: string[], consignee: string, mobile: string) {
|
||||||
|
return lines
|
||||||
|
.map((line) => labelMatch(line, allLabels)?.value || line)
|
||||||
|
.filter((line) => line && line !== consignee && !line.includes(mobile) && !normalizeMobile(line))
|
||||||
|
.sort((a, b) => b.length - a.length)[0] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recognizeReturnAddress(raw: string): RecognizeReturnAddressResult {
|
||||||
|
const lines = normalizeLines(raw);
|
||||||
|
if (!lines.length) {
|
||||||
|
return { ok: false, message: "请先粘贴寄回地址信息" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const consignee = extractLabeledValue(lines, nameLabels).trim();
|
||||||
|
const mobile = normalizeMobile(extractLabeledValue(lines, mobileLabels) || raw);
|
||||||
|
const addressText = extractLabeledValue(lines, addressLabels, true) || fallbackAddressLine(lines, consignee, mobile);
|
||||||
|
const region = matchRegion(addressText);
|
||||||
|
|
||||||
|
if (!consignee) {
|
||||||
|
return { ok: false, message: "未识别到收件人,请检查文本中是否包含收货人或收件人" };
|
||||||
|
}
|
||||||
|
if (!mobile) {
|
||||||
|
return { ok: false, message: "未识别到有效手机号,请检查文本中的收货电话" };
|
||||||
|
}
|
||||||
|
if (!region) {
|
||||||
|
return { ok: false, message: "未识别到省市区,请检查地址是否包含城市和区县" };
|
||||||
|
}
|
||||||
|
if (!region.detail_address.trim()) {
|
||||||
|
return { ok: false, message: "未识别到详细地址,请检查区县后的街道门牌信息" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
address: {
|
||||||
|
consignee,
|
||||||
|
mobile,
|
||||||
|
province: region.province,
|
||||||
|
city: region.city,
|
||||||
|
district: region.district,
|
||||||
|
detail_address: region.detail_address.trim(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\controller\admin;
|
||||||
|
|
||||||
|
use app\support\ExpressCompanyService;
|
||||||
|
use support\Request;
|
||||||
|
|
||||||
|
class ExpressCompaniesController
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$enabledOnly = (int)$request->input('enabled_only', 0) === 1;
|
||||||
|
|
||||||
|
return api_success([
|
||||||
|
'list' => $this->service()->list($enabledOnly),
|
||||||
|
'default_company' => $this->service()->defaultName(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(Request $request)
|
||||||
|
{
|
||||||
|
$id = (int)$request->input('id', 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$companyId = $this->service()->save([
|
||||||
|
'company_name' => $request->input('company_name', ''),
|
||||||
|
'company_code' => $request->input('company_code', ''),
|
||||||
|
'status' => $request->input('status', 'enabled'),
|
||||||
|
'is_default' => $request->input('is_default', false),
|
||||||
|
'sort_order' => $request->input('sort_order', 0),
|
||||||
|
'remark' => $request->input('remark', ''),
|
||||||
|
], $id);
|
||||||
|
} catch (\RuntimeException $e) {
|
||||||
|
return api_error($e->getMessage(), 422);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return api_error('快递公司保存失败', 500, [
|
||||||
|
'detail' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return api_success([
|
||||||
|
'id' => $companyId,
|
||||||
|
], $id > 0 ? '快递公司已更新' : '快递公司已创建');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function service(): ExpressCompanyService
|
||||||
|
{
|
||||||
|
return new ExpressCompanyService();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ class ReportsController
|
|||||||
'r.zhongjian_report_no',
|
'r.zhongjian_report_no',
|
||||||
'r.report_entry_admin_name',
|
'r.report_entry_admin_name',
|
||||||
'r.report_entered_at',
|
'r.report_entered_at',
|
||||||
|
'r.trace_info_visible',
|
||||||
'o.order_no',
|
'o.order_no',
|
||||||
'p.product_name',
|
'p.product_name',
|
||||||
'p.category_name',
|
'p.category_name',
|
||||||
@@ -83,6 +84,7 @@ class ReportsController
|
|||||||
'zhongjian_report_no' => (string)($item['zhongjian_report_no'] ?? ''),
|
'zhongjian_report_no' => (string)($item['zhongjian_report_no'] ?? ''),
|
||||||
'report_entry_admin_name' => (string)($item['report_entry_admin_name'] ?? ''),
|
'report_entry_admin_name' => (string)($item['report_entry_admin_name'] ?? ''),
|
||||||
'report_entered_at' => (string)($item['report_entered_at'] ?? ''),
|
'report_entered_at' => (string)($item['report_entered_at'] ?? ''),
|
||||||
|
'trace_info_visible' => (int)($item['trace_info_visible'] ?? 0) === 1,
|
||||||
'product_name' => $item['product_name'] ?: (string)($productSnapshot['product_name'] ?? ''),
|
'product_name' => $item['product_name'] ?: (string)($productSnapshot['product_name'] ?? ''),
|
||||||
'category_name' => $item['category_name'] ?: (string)($productSnapshot['category_name'] ?? ''),
|
'category_name' => $item['category_name'] ?: (string)($productSnapshot['category_name'] ?? ''),
|
||||||
'brand_name' => $item['brand_name'] ?: (string)($productSnapshot['brand_name'] ?? ''),
|
'brand_name' => $item['brand_name'] ?: (string)($productSnapshot['brand_name'] ?? ''),
|
||||||
@@ -169,6 +171,7 @@ class ReportsController
|
|||||||
'report_entry_admin_id' => (int)($report['report_entry_admin_id'] ?? 0),
|
'report_entry_admin_id' => (int)($report['report_entry_admin_id'] ?? 0),
|
||||||
'report_entry_admin_name' => (string)($report['report_entry_admin_name'] ?? ''),
|
'report_entry_admin_name' => (string)($report['report_entry_admin_name'] ?? ''),
|
||||||
'report_entered_at' => (string)($report['report_entered_at'] ?? ''),
|
'report_entered_at' => (string)($report['report_entered_at'] ?? ''),
|
||||||
|
'trace_info_visible' => (int)($report['trace_info_visible'] ?? 0) === 1,
|
||||||
],
|
],
|
||||||
'product_info' => $productSnapshot,
|
'product_info' => $productSnapshot,
|
||||||
'result_info' => $resultSnapshot,
|
'result_info' => $resultSnapshot,
|
||||||
@@ -188,6 +191,32 @@ class ReportsController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updateTraceVisibility(Request $request)
|
||||||
|
{
|
||||||
|
$id = (int)$request->input('id', 0);
|
||||||
|
if (!$id) {
|
||||||
|
return api_error('报告 ID 不能为空', 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$report = Db::name('reports')->where('id', $id)->find();
|
||||||
|
if (!$report) {
|
||||||
|
return api_error('报告不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$visible = $this->boolInput($request->input('trace_info_visible', false));
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
Db::name('reports')->where('id', $id)->update([
|
||||||
|
'trace_info_visible' => $visible ? 1 : 0,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return api_success([
|
||||||
|
'id' => $id,
|
||||||
|
'trace_info_visible' => $visible,
|
||||||
|
], $visible ? '追溯信息已设为显示' : '追溯信息已隐藏');
|
||||||
|
}
|
||||||
|
|
||||||
public function saveInspection(Request $request)
|
public function saveInspection(Request $request)
|
||||||
{
|
{
|
||||||
$id = (int)$request->input('id', 0);
|
$id = (int)$request->input('id', 0);
|
||||||
@@ -540,6 +569,18 @@ class ReportsController
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function boolInput(mixed $value): bool
|
||||||
|
{
|
||||||
|
if (is_bool($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
return (int)$value === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array(strtolower(trim((string)$value)), ['1', 'true', 'yes', 'on'], true);
|
||||||
|
}
|
||||||
|
|
||||||
private function loadReportContentMap(array $reportIds): array
|
private function loadReportContentMap(array $reportIds): array
|
||||||
{
|
{
|
||||||
if (!$reportIds) {
|
if (!$reportIds) {
|
||||||
|
|||||||
@@ -108,12 +108,16 @@ class ReportsController
|
|||||||
$reportMedia = [
|
$reportMedia = [
|
||||||
'images' => $this->filterAssetsByType($evidenceAttachments, 'image'),
|
'images' => $this->filterAssetsByType($evidenceAttachments, 'image'),
|
||||||
];
|
];
|
||||||
$traceInfo = $this->buildTraceInfo(
|
$traceInfoVisible = (int)($reportData['trace_info_visible'] ?? 0) === 1;
|
||||||
|
$traceInfo = $traceInfoVisible
|
||||||
|
? $this->buildTraceInfo(
|
||||||
(int)($reportData['order_id'] ?? 0),
|
(int)($reportData['order_id'] ?? 0),
|
||||||
$payload['appraisal_snapshot'],
|
$payload['appraisal_snapshot'],
|
||||||
$evidenceAttachments,
|
$evidenceAttachments,
|
||||||
$request
|
$request
|
||||||
);
|
)
|
||||||
|
: ['visible' => false, 'nodes' => []];
|
||||||
|
$traceInfo['visible'] = $traceInfoVisible;
|
||||||
$pdfUrl = $this->ensurePdfFile($request, $reportData, $content ?: [], $verify ?: [], $productDisplay, $reportMedia);
|
$pdfUrl = $this->ensurePdfFile($request, $reportData, $content ?: [], $verify ?: [], $productDisplay, $reportMedia);
|
||||||
|
|
||||||
return api_success([
|
return api_success([
|
||||||
@@ -129,6 +133,7 @@ class ReportsController
|
|||||||
'zhongjian_report_no' => (string)($reportData['zhongjian_report_no'] ?? ''),
|
'zhongjian_report_no' => (string)($reportData['zhongjian_report_no'] ?? ''),
|
||||||
'report_entry_admin_name' => (string)($reportData['report_entry_admin_name'] ?? ''),
|
'report_entry_admin_name' => (string)($reportData['report_entry_admin_name'] ?? ''),
|
||||||
'report_entered_at' => (string)($reportData['report_entered_at'] ?? ''),
|
'report_entered_at' => (string)($reportData['report_entered_at'] ?? ''),
|
||||||
|
'trace_info_visible' => $traceInfoVisible,
|
||||||
],
|
],
|
||||||
'result_info' => $payload['result_snapshot'],
|
'result_info' => $payload['result_snapshot'],
|
||||||
'product_info' => $payload['product_snapshot'],
|
'product_info' => $payload['product_snapshot'],
|
||||||
@@ -353,7 +358,7 @@ class ReportsController
|
|||||||
$items,
|
$items,
|
||||||
$label,
|
$label,
|
||||||
$this->textValue($point['point_value'] ?? '') ?: '-',
|
$this->textValue($point['point_value'] ?? '') ?: '-',
|
||||||
$this->textValue($point['point_remark'] ?? ''),
|
'',
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ class AdminAuthMiddleware implements MiddlewareInterface
|
|||||||
str_starts_with($path, '/api/admin/customers'),
|
str_starts_with($path, '/api/admin/customers'),
|
||||||
str_starts_with($path, '/api/admin/customer/') => ['customers.manage'],
|
str_starts_with($path, '/api/admin/customer/') => ['customers.manage'],
|
||||||
str_starts_with($path, '/api/admin/warehouse-workbench/') => ['warehouse_workbench.manage'],
|
str_starts_with($path, '/api/admin/warehouse-workbench/') => ['warehouse_workbench.manage'],
|
||||||
|
str_starts_with($path, '/api/admin/express-companies') && strtoupper($method) === 'GET' => ['warehouse_workbench.manage', 'orders.manage', 'warehouses.manage'],
|
||||||
|
str_starts_with($path, '/api/admin/express-company/') => ['warehouses.manage'],
|
||||||
str_starts_with($path, '/api/admin/warehouses'),
|
str_starts_with($path, '/api/admin/warehouses'),
|
||||||
str_starts_with($path, '/api/admin/warehouse/') => ['warehouses.manage'],
|
str_starts_with($path, '/api/admin/warehouse/') => ['warehouses.manage'],
|
||||||
str_starts_with($path, '/api/admin/material/') => ['materials.manage'],
|
str_starts_with($path, '/api/admin/material/') => ['materials.manage'],
|
||||||
|
|||||||
208
server-api/app/support/ExpressCompanyService.php
Normal file
208
server-api/app/support/ExpressCompanyService.php
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\support;
|
||||||
|
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
class ExpressCompanyService
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->ensureTable();
|
||||||
|
$this->bootstrapDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function list(bool $enabledOnly = false): array
|
||||||
|
{
|
||||||
|
$query = Db::name('express_companies')
|
||||||
|
->order('is_default', 'desc')
|
||||||
|
->order('sort_order', 'asc')
|
||||||
|
->order('id', 'asc');
|
||||||
|
|
||||||
|
if ($enabledOnly) {
|
||||||
|
$query->where('status', 'enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_map(fn (array $item) => $this->format($item), $query->select()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(array $payload, int $id = 0): int
|
||||||
|
{
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$companyName = trim((string)($payload['company_name'] ?? ''));
|
||||||
|
$companyCode = trim((string)($payload['company_code'] ?? ''));
|
||||||
|
$status = trim((string)($payload['status'] ?? 'enabled'));
|
||||||
|
$isDefault = !empty($payload['is_default']);
|
||||||
|
|
||||||
|
if ($companyName === '') {
|
||||||
|
throw new \RuntimeException('快递公司名称不能为空');
|
||||||
|
}
|
||||||
|
if (!in_array($status, ['enabled', 'disabled'], true)) {
|
||||||
|
throw new \RuntimeException('快递公司状态无效');
|
||||||
|
}
|
||||||
|
if ($isDefault && $status !== 'enabled') {
|
||||||
|
throw new \RuntimeException('默认快递公司必须保持启用');
|
||||||
|
}
|
||||||
|
if ($companyCode === '') {
|
||||||
|
$companyCode = $this->generateCompanyCode($companyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
$existsByName = Db::name('express_companies')
|
||||||
|
->where('company_name', $companyName)
|
||||||
|
->when($id > 0, fn ($query) => $query->where('id', '<>', $id))
|
||||||
|
->find();
|
||||||
|
if ($existsByName) {
|
||||||
|
throw new \RuntimeException('快递公司名称已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$existsByCode = Db::name('express_companies')
|
||||||
|
->where('company_code', $companyCode)
|
||||||
|
->when($id > 0, fn ($query) => $query->where('id', '<>', $id))
|
||||||
|
->find();
|
||||||
|
if ($existsByCode) {
|
||||||
|
throw new \RuntimeException('快递公司编码已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'company_name' => $companyName,
|
||||||
|
'company_code' => $companyCode,
|
||||||
|
'status' => $status,
|
||||||
|
'is_default' => $isDefault ? 1 : 0,
|
||||||
|
'sort_order' => (int)($payload['sort_order'] ?? 0),
|
||||||
|
'remark' => trim((string)($payload['remark'] ?? '')),
|
||||||
|
'updated_at' => $now,
|
||||||
|
];
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
if ($isDefault) {
|
||||||
|
Db::name('express_companies')->update([
|
||||||
|
'is_default' => 0,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($id > 0) {
|
||||||
|
Db::name('express_companies')->where('id', $id)->update($data);
|
||||||
|
$companyId = $id;
|
||||||
|
} else {
|
||||||
|
$data['created_at'] = $now;
|
||||||
|
$companyId = (int)Db::name('express_companies')->insertGetId($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ensureEnabledDefault($now);
|
||||||
|
Db::commit();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $companyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaultName(): string
|
||||||
|
{
|
||||||
|
$row = Db::name('express_companies')
|
||||||
|
->where('status', 'enabled')
|
||||||
|
->where('is_default', 1)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$row) {
|
||||||
|
$row = Db::name('express_companies')
|
||||||
|
->where('status', 'enabled')
|
||||||
|
->order('sort_order', 'asc')
|
||||||
|
->order('id', 'asc')
|
||||||
|
->find();
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim((string)($row['company_name'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function format(array $item): array
|
||||||
|
{
|
||||||
|
$status = (string)($item['status'] ?? 'enabled');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int)$item['id'],
|
||||||
|
'company_name' => (string)$item['company_name'],
|
||||||
|
'company_code' => (string)$item['company_code'],
|
||||||
|
'status' => $status,
|
||||||
|
'status_text' => $status === 'enabled' ? '启用中' : '已停用',
|
||||||
|
'is_default' => (bool)$item['is_default'],
|
||||||
|
'sort_order' => (int)$item['sort_order'],
|
||||||
|
'remark' => (string)($item['remark'] ?? ''),
|
||||||
|
'created_at' => (string)($item['created_at'] ?? ''),
|
||||||
|
'updated_at' => (string)($item['updated_at'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bootstrapDefaults(): void
|
||||||
|
{
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$exists = Db::name('express_companies')->where('company_name', '顺丰速运')->find();
|
||||||
|
if (!$exists) {
|
||||||
|
Db::name('express_companies')->insert([
|
||||||
|
'company_name' => '顺丰速运',
|
||||||
|
'company_code' => 'sf_express',
|
||||||
|
'status' => 'enabled',
|
||||||
|
'is_default' => 1,
|
||||||
|
'sort_order' => 1,
|
||||||
|
'remark' => '系统默认快递公司',
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ensureEnabledDefault($now);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureEnabledDefault(string $now): void
|
||||||
|
{
|
||||||
|
$default = Db::name('express_companies')
|
||||||
|
->where('status', 'enabled')
|
||||||
|
->where('is_default', 1)
|
||||||
|
->find();
|
||||||
|
if ($default) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$firstEnabled = Db::name('express_companies')
|
||||||
|
->where('status', 'enabled')
|
||||||
|
->order('sort_order', 'asc')
|
||||||
|
->order('id', 'asc')
|
||||||
|
->find();
|
||||||
|
if ($firstEnabled) {
|
||||||
|
Db::name('express_companies')->where('id', $firstEnabled['id'])->update([
|
||||||
|
'is_default' => 1,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCompanyCode(string $companyName): string
|
||||||
|
{
|
||||||
|
return 'express_' . substr(hash('sha256', $companyName), 0, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureTable(): void
|
||||||
|
{
|
||||||
|
Db::execute(<<<'SQL'
|
||||||
|
CREATE TABLE IF NOT EXISTS express_companies (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
company_name VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
company_code VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'enabled',
|
||||||
|
is_default TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_express_companies_name (company_name),
|
||||||
|
UNIQUE KEY uk_express_companies_code (company_code),
|
||||||
|
KEY idx_express_companies_status (status),
|
||||||
|
KEY idx_express_companies_default (is_default)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='快递公司字典'
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ use app\controller\admin\SystemConfigsController as AdminSystemConfigsController
|
|||||||
use app\controller\admin\AuthController as AdminAuthController;
|
use app\controller\admin\AuthController as AdminAuthController;
|
||||||
use app\controller\admin\CustomersController as AdminCustomersController;
|
use app\controller\admin\CustomersController as AdminCustomersController;
|
||||||
use app\controller\admin\WarehouseWorkbenchController as AdminWarehouseWorkbenchController;
|
use app\controller\admin\WarehouseWorkbenchController as AdminWarehouseWorkbenchController;
|
||||||
|
use app\controller\admin\ExpressCompaniesController as AdminExpressCompaniesController;
|
||||||
use app\controller\admin\FileUploadController as AdminFileUploadController;
|
use app\controller\admin\FileUploadController as AdminFileUploadController;
|
||||||
use app\controller\open\OrdersController as OpenOrdersController;
|
use app\controller\open\OrdersController as OpenOrdersController;
|
||||||
|
|
||||||
@@ -219,6 +220,7 @@ Route::post('/api/admin/catalog/upload-templates/save', [AdminCatalogController:
|
|||||||
Route::post('/api/admin/catalog/appraisal-templates/save', [AdminCatalogController::class, 'saveAppraisalTemplates']);
|
Route::post('/api/admin/catalog/appraisal-templates/save', [AdminCatalogController::class, 'saveAppraisalTemplates']);
|
||||||
Route::get('/api/admin/reports', [AdminReportsController::class, 'index']);
|
Route::get('/api/admin/reports', [AdminReportsController::class, 'index']);
|
||||||
Route::get('/api/admin/report/detail', [AdminReportsController::class, 'detail']);
|
Route::get('/api/admin/report/detail', [AdminReportsController::class, 'detail']);
|
||||||
|
Route::post('/api/admin/report/trace-visibility', [AdminReportsController::class, 'updateTraceVisibility']);
|
||||||
Route::post('/api/admin/report/inspection/save', [AdminReportsController::class, 'saveInspection']);
|
Route::post('/api/admin/report/inspection/save', [AdminReportsController::class, 'saveInspection']);
|
||||||
Route::post('/api/admin/report/publish', [AdminReportsController::class, 'publish']);
|
Route::post('/api/admin/report/publish', [AdminReportsController::class, 'publish']);
|
||||||
Route::get('/api/admin/appraisal-tasks', [AdminAppraisalTasksController::class, 'index']);
|
Route::get('/api/admin/appraisal-tasks', [AdminAppraisalTasksController::class, 'index']);
|
||||||
@@ -262,6 +264,8 @@ Route::post('/api/admin/customer/event/resend', [AdminCustomersController::class
|
|||||||
Route::get('/api/admin/warehouses/overview', [AdminWarehousesController::class, 'overview']);
|
Route::get('/api/admin/warehouses/overview', [AdminWarehousesController::class, 'overview']);
|
||||||
Route::get('/api/admin/warehouses', [AdminWarehousesController::class, 'index']);
|
Route::get('/api/admin/warehouses', [AdminWarehousesController::class, 'index']);
|
||||||
Route::post('/api/admin/warehouse/save', [AdminWarehousesController::class, 'save']);
|
Route::post('/api/admin/warehouse/save', [AdminWarehousesController::class, 'save']);
|
||||||
|
Route::get('/api/admin/express-companies', [AdminExpressCompaniesController::class, 'index']);
|
||||||
|
Route::post('/api/admin/express-company/save', [AdminExpressCompaniesController::class, 'save']);
|
||||||
Route::get('/api/admin/warehouse-workbench/inbound/lookup', [AdminWarehouseWorkbenchController::class, 'inboundLookup']);
|
Route::get('/api/admin/warehouse-workbench/inbound/lookup', [AdminWarehouseWorkbenchController::class, 'inboundLookup']);
|
||||||
Route::post('/api/admin/warehouse-workbench/inbound/receive', [AdminWarehouseWorkbenchController::class, 'inboundReceive']);
|
Route::post('/api/admin/warehouse-workbench/inbound/receive', [AdminWarehouseWorkbenchController::class, 'inboundReceive']);
|
||||||
Route::post('/api/admin/warehouse-workbench/inbound/evidence/upload', [AdminWarehouseWorkbenchController::class, 'uploadInboundEvidenceFile']);
|
Route::post('/api/admin/warehouse-workbench/inbound/evidence/upload', [AdminWarehouseWorkbenchController::class, 'uploadInboundEvidenceFile']);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ DROP TABLE IF EXISTS enterprise_customer_order_refs;
|
|||||||
DROP TABLE IF EXISTS enterprise_api_nonces;
|
DROP TABLE IF EXISTS enterprise_api_nonces;
|
||||||
DROP TABLE IF EXISTS enterprise_customer_apps;
|
DROP TABLE IF EXISTS enterprise_customer_apps;
|
||||||
DROP TABLE IF EXISTS enterprise_customers;
|
DROP TABLE IF EXISTS enterprise_customers;
|
||||||
|
DROP TABLE IF EXISTS express_companies;
|
||||||
DROP TABLE IF EXISTS shipping_warehouses;
|
DROP TABLE IF EXISTS shipping_warehouses;
|
||||||
DROP TABLE IF EXISTS user_api_tokens;
|
DROP TABLE IF EXISTS user_api_tokens;
|
||||||
DROP TABLE IF EXISTS sms_code_logs;
|
DROP TABLE IF EXISTS sms_code_logs;
|
||||||
@@ -729,6 +730,23 @@ CREATE TABLE order_logistics_nodes (
|
|||||||
KEY idx_order_logistics_nodes_logistics_id (logistics_id)
|
KEY idx_order_logistics_nodes_logistics_id (logistics_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='物流节点';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='物流节点';
|
||||||
|
|
||||||
|
CREATE TABLE express_companies (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
company_name VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
company_code VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'enabled',
|
||||||
|
is_default TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_express_companies_name (company_name),
|
||||||
|
UNIQUE KEY uk_express_companies_code (company_code),
|
||||||
|
KEY idx_express_companies_status (status),
|
||||||
|
KEY idx_express_companies_default (is_default)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='快递公司字典';
|
||||||
|
|
||||||
CREATE TABLE order_abnormals (
|
CREATE TABLE order_abnormals (
|
||||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
order_id BIGINT UNSIGNED NOT NULL,
|
order_id BIGINT UNSIGNED NOT NULL,
|
||||||
@@ -949,6 +967,7 @@ CREATE TABLE reports (
|
|||||||
report_entry_admin_id BIGINT UNSIGNED NULL DEFAULT NULL,
|
report_entry_admin_id BIGINT UNSIGNED NULL DEFAULT NULL,
|
||||||
report_entry_admin_name VARCHAR(64) NOT NULL DEFAULT '',
|
report_entry_admin_name VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
report_entered_at DATETIME NULL DEFAULT NULL,
|
report_entered_at DATETIME NULL DEFAULT NULL,
|
||||||
|
trace_info_visible TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
invalid_reason VARCHAR(255) NOT NULL DEFAULT '',
|
invalid_reason VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ $pdo = new PDO(
|
|||||||
);
|
);
|
||||||
|
|
||||||
$tables = [
|
$tables = [
|
||||||
|
'express_companies',
|
||||||
'shipping_warehouses',
|
'shipping_warehouses',
|
||||||
'order_shipping_targets',
|
'order_shipping_targets',
|
||||||
'material_tag_scan_logs', 'material_batch_download_logs', 'material_tag_codes', 'material_batches',
|
'material_tag_scan_logs', 'material_batch_download_logs', 'material_tag_codes', 'material_batches',
|
||||||
@@ -62,6 +63,9 @@ INSERT INTO shipping_warehouses (id, warehouse_name, warehouse_code, warehouse_t
|
|||||||
(1, '安心验鉴定中心', 'AXY-WH-DEFAULT', 'detection_center', 'anxinyan', '安心验鉴定中心', '400-800-1314', '广东省', '深圳市', '南山区', '科技园鉴定路 88 号 安心验收件中心', '周一至周日 09:30-18:30', '寄送前请确认订单信息完整,包裹内附上订单号可提升签收后的处理效率。', NULL, NULL, NULL, 'enabled', 1, 1, '默认仓库', '{$now}', '{$now}'),
|
(1, '安心验鉴定中心', 'AXY-WH-DEFAULT', 'detection_center', 'anxinyan', '安心验鉴定中心', '400-800-1314', '广东省', '深圳市', '南山区', '科技园鉴定路 88 号 安心验收件中心', '周一至周日 09:30-18:30', '寄送前请确认订单信息完整,包裹内附上订单号可提升签收后的处理效率。', NULL, NULL, NULL, 'enabled', 1, 1, '默认仓库', '{$now}', '{$now}'),
|
||||||
(2, '中检合作鉴定中心', 'ZJ-WH-DEFAULT', 'detection_center', 'zhongjian', '中检合作鉴定中心', '400-800-1314', '广东省', '深圳市', '南山区', '科技园鉴定路 88 号 安心验中检收件中心', '周一至周日 09:30-18:30', '中检鉴定订单请优先附上鉴定单号,寄出后尽快填写运单号。', NULL, NULL, NULL, 'enabled', 1, 1, '默认仓库', '{$now}', '{$now}');
|
(2, '中检合作鉴定中心', 'ZJ-WH-DEFAULT', 'detection_center', 'zhongjian', '中检合作鉴定中心', '400-800-1314', '广东省', '深圳市', '南山区', '科技园鉴定路 88 号 安心验中检收件中心', '周一至周日 09:30-18:30', '中检鉴定订单请优先附上鉴定单号,寄出后尽快填写运单号。', NULL, NULL, NULL, 'enabled', 1, 1, '默认仓库', '{$now}', '{$now}');
|
||||||
|
|
||||||
|
INSERT INTO express_companies (id, company_name, company_code, status, is_default, sort_order, remark, created_at, updated_at) VALUES
|
||||||
|
(1, '顺丰速运', 'sf_express', 'enabled', 1, 1, '系统默认快递公司', '{$now}', '{$now}');
|
||||||
|
|
||||||
INSERT INTO catalog_categories (id, name, code, sort_order, is_enabled, need_shipping, supported_service_types, created_at, updated_at) VALUES
|
INSERT INTO catalog_categories (id, name, code, sort_order, is_enabled, need_shipping, supported_service_types, created_at, updated_at) VALUES
|
||||||
(1, '奢侈品箱包', 'luxury_bag', 1, 1, 1, JSON_ARRAY('anxinyan', 'zhongjian'), '{$now}', '{$now}'),
|
(1, '奢侈品箱包', 'luxury_bag', 1, 1, 1, JSON_ARRAY('anxinyan', 'zhongjian'), '{$now}', '{$now}'),
|
||||||
(2, '潮流鞋类', 'sneaker', 2, 1, 1, JSON_ARRAY('anxinyan', 'zhongjian'), '{$now}', '{$now}'),
|
(2, '潮流鞋类', 'sneaker', 2, 1, 1, JSON_ARRAY('anxinyan', 'zhongjian'), '{$now}', '{$now}'),
|
||||||
|
|||||||
68
server-api/tools/schema_upgrade_express_companies.php
Normal file
68
server-api/tools/schema_upgrade_express_companies.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||||
|
$dotenv->safeLoad();
|
||||||
|
|
||||||
|
$dsn = sprintf(
|
||||||
|
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
|
||||||
|
$_ENV['DB_HOST'] ?? '127.0.0.1',
|
||||||
|
$_ENV['DB_PORT'] ?? '3306',
|
||||||
|
$_ENV['DB_DATABASE'] ?? '',
|
||||||
|
$_ENV['DB_CHARSET'] ?? 'utf8mb4'
|
||||||
|
);
|
||||||
|
|
||||||
|
$pdo = new PDO(
|
||||||
|
$dsn,
|
||||||
|
$_ENV['DB_USERNAME'] ?? '',
|
||||||
|
$_ENV['DB_PASSWORD'] ?? '',
|
||||||
|
[
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$pdo->exec(<<<'SQL'
|
||||||
|
CREATE TABLE IF NOT EXISTS express_companies (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
company_name VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
company_code VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'enabled',
|
||||||
|
is_default TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_express_companies_name (company_name),
|
||||||
|
UNIQUE KEY uk_express_companies_code (company_code),
|
||||||
|
KEY idx_express_companies_status (status),
|
||||||
|
KEY idx_express_companies_default (is_default)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='快递公司字典'
|
||||||
|
SQL);
|
||||||
|
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$stmt = $pdo->prepare('SELECT id FROM express_companies WHERE company_name = ? LIMIT 1');
|
||||||
|
$stmt->execute(['顺丰速运']);
|
||||||
|
$exists = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$exists) {
|
||||||
|
$insert = $pdo->prepare('INSERT INTO express_companies (company_name, company_code, status, is_default, sort_order, remark, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
|
||||||
|
$insert->execute(['顺丰速运', 'sf_express', 'enabled', 1, 1, '系统默认快递公司', $now, $now]);
|
||||||
|
echo "SEED_DEFAULT_EXPRESS_COMPANY\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultCount = (int)$pdo->query("SELECT COUNT(*) FROM express_companies WHERE status = 'enabled' AND is_default = 1")->fetchColumn();
|
||||||
|
if ($defaultCount === 0) {
|
||||||
|
$row = $pdo->query("SELECT id FROM express_companies WHERE status = 'enabled' ORDER BY sort_order ASC, id ASC LIMIT 1")->fetch();
|
||||||
|
if ($row) {
|
||||||
|
$update = $pdo->prepare('UPDATE express_companies SET is_default = 1, updated_at = ? WHERE id = ?');
|
||||||
|
$update->execute([$now, (int)$row['id']]);
|
||||||
|
echo "SET_DEFAULT_EXPRESS_COMPANY\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "SCHEMA_UPGRADE_OK\n";
|
||||||
40
server-api/tools/schema_upgrade_report_trace_visibility.php
Normal file
40
server-api/tools/schema_upgrade_report_trace_visibility.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||||
|
$dotenv->safeLoad();
|
||||||
|
|
||||||
|
$dsn = sprintf(
|
||||||
|
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
|
||||||
|
$_ENV['DB_HOST'] ?? '127.0.0.1',
|
||||||
|
$_ENV['DB_PORT'] ?? '3306',
|
||||||
|
$_ENV['DB_DATABASE'] ?? '',
|
||||||
|
$_ENV['DB_CHARSET'] ?? 'utf8mb4'
|
||||||
|
);
|
||||||
|
|
||||||
|
$pdo = new PDO(
|
||||||
|
$dsn,
|
||||||
|
$_ENV['DB_USERNAME'] ?? '',
|
||||||
|
$_ENV['DB_PASSWORD'] ?? '',
|
||||||
|
[
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
function hasColumn(PDO $pdo, string $table, string $column): bool
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?');
|
||||||
|
$stmt->execute([$table, $column]);
|
||||||
|
return (int)$stmt->fetchColumn() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasColumn($pdo, 'reports', 'trace_info_visible')) {
|
||||||
|
$pdo->exec('ALTER TABLE reports ADD COLUMN trace_info_visible TINYINT(1) NOT NULL DEFAULT 0 AFTER report_entered_at');
|
||||||
|
echo "ADD_COLUMN reports.trace_info_visible\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "SCHEMA_UPGRADE_REPORT_TRACE_VISIBILITY_OK\n";
|
||||||
@@ -357,6 +357,7 @@ export interface ReportDetailData {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
trace_info: {
|
trace_info: {
|
||||||
|
visible: boolean;
|
||||||
nodes: Array<{
|
nodes: Array<{
|
||||||
code: "inbound" | "appraisal" | "return";
|
code: "inbound" | "appraisal" | "return";
|
||||||
title: string;
|
title: string;
|
||||||
@@ -377,6 +378,7 @@ export interface ReportDetailData {
|
|||||||
zhongjian_report_no: string;
|
zhongjian_report_no: string;
|
||||||
report_entry_admin_name: string;
|
report_entry_admin_name: string;
|
||||||
report_entered_at: string;
|
report_entered_at: string;
|
||||||
|
trace_info_visible: boolean;
|
||||||
};
|
};
|
||||||
result_info: Record<string, any>;
|
result_info: Record<string, any>;
|
||||||
product_info: Record<string, any>;
|
product_info: Record<string, any>;
|
||||||
|
|||||||
@@ -390,6 +390,7 @@ export const reportDetailFallback: ReportDetailData = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
trace_info: {
|
trace_info: {
|
||||||
|
visible: false,
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
code: "inbound",
|
code: "inbound",
|
||||||
@@ -426,6 +427,7 @@ export const reportDetailFallback: ReportDetailData = {
|
|||||||
zhongjian_report_no: "ZJ-20260418-0001",
|
zhongjian_report_no: "ZJ-20260418-0001",
|
||||||
report_entry_admin_name: "王师傅",
|
report_entry_admin_name: "王师傅",
|
||||||
report_entered_at: "2026-04-18 18:20:00",
|
report_entered_at: "2026-04-18 18:20:00",
|
||||||
|
trace_info_visible: false,
|
||||||
},
|
},
|
||||||
result_info: {
|
result_info: {
|
||||||
result_status: "authentic",
|
result_status: "authentic",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { onLoad } from "@dcloudio/uni-app";
|
import { onLoad } from "@dcloudio/uni-app";
|
||||||
import { appApi, type EvidenceAttachmentAsset, type ReportDetailData } from "../../api/app";
|
import { appApi, type EvidenceAttachmentAsset, type ReportDetailData } from "../../api/app";
|
||||||
import { reportDetailFallback } from "../../mocks/app";
|
import { reportDetailFallback } from "../../mocks/app";
|
||||||
@@ -17,6 +17,9 @@ const antiModalVisible = ref(false);
|
|||||||
const antiCode = ref("");
|
const antiCode = ref("");
|
||||||
const antiVerifying = ref(false);
|
const antiVerifying = ref(false);
|
||||||
const antiResult = ref<null | { passed: boolean; message: string }>(null);
|
const antiResult = ref<null | { passed: boolean; message: string }>(null);
|
||||||
|
const imagePreviewVisible = ref(false);
|
||||||
|
const imagePreviewUrls = ref<string[]>([]);
|
||||||
|
const imagePreviewIndex = ref(0);
|
||||||
|
|
||||||
const reportImages = computed(() => {
|
const reportImages = computed(() => {
|
||||||
const images = detail.value.report_media?.images || [];
|
const images = detail.value.report_media?.images || [];
|
||||||
@@ -41,7 +44,31 @@ const productItems = computed(() => {
|
|||||||
{ label: "品牌", value: detail.value.product_info.brand_name || "-" },
|
{ label: "品牌", value: detail.value.product_info.brand_name || "-" },
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
const traceNodes = computed(() => detail.value.trace_info?.nodes || []);
|
const publishTime = computed(() => detail.value.report_header.publish_time || "-");
|
||||||
|
const resultItem = computed(() => {
|
||||||
|
const item = productItems.value.find((entry) => entry.label.includes("结论"));
|
||||||
|
return {
|
||||||
|
label: item?.label || "检测结论",
|
||||||
|
value: item?.value || detail.value.result_info.result_text || "-",
|
||||||
|
remark: item?.remark || detail.value.result_info.result_desc || "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const productSpecItems = computed(() => {
|
||||||
|
const items = productItems.value
|
||||||
|
.filter((item) => !item.label.includes("结论"))
|
||||||
|
.map((item) => ({ label: item.label, value: item.value, remark: item.remark || "" }));
|
||||||
|
if (items.length) return items;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ label: "品类", value: detail.value.product_info.category_name || "-", remark: "" },
|
||||||
|
{ label: "品牌", value: detail.value.product_info.brand_name || "-", remark: "" },
|
||||||
|
{ label: "颜色", value: detail.value.product_info.color || "-", remark: "" },
|
||||||
|
{ label: "规格/尺寸", value: detail.value.product_info.size_spec || "-", remark: "" },
|
||||||
|
{ label: "序列号/编码", value: detail.value.product_info.serial_no || "-", remark: "" },
|
||||||
|
].filter((item) => item.value && item.value !== "-");
|
||||||
|
});
|
||||||
|
const traceInfoVisible = computed(() => Boolean(detail.value.trace_info?.visible || detail.value.report_header.trace_info_visible));
|
||||||
|
const traceNodes = computed(() => (traceInfoVisible.value ? detail.value.trace_info?.nodes || [] : []));
|
||||||
const zhongjianReportFiles = computed(() => detail.value.zhongjian_report_files || []);
|
const zhongjianReportFiles = computed(() => detail.value.zhongjian_report_files || []);
|
||||||
const zhongjianImageFiles = computed(() => zhongjianReportFiles.value.filter((item) => item.file_type === "image"));
|
const zhongjianImageFiles = computed(() => zhongjianReportFiles.value.filter((item) => item.file_type === "image"));
|
||||||
const zhongjianOtherFiles = computed(() => zhongjianReportFiles.value.filter((item) => item.file_type !== "image"));
|
const zhongjianOtherFiles = computed(() => zhongjianReportFiles.value.filter((item) => item.file_type !== "image"));
|
||||||
@@ -59,9 +86,23 @@ function assetDisplayName(item: EvidenceAttachmentAsset, index: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function previewImages(files: EvidenceAttachmentAsset[], current: string) {
|
function previewImages(files: EvidenceAttachmentAsset[], current: string) {
|
||||||
const urls = files.filter((item) => item.file_type === "image").map((item) => item.file_url);
|
const urls = files
|
||||||
|
.filter((item) => item.file_type === "image" && item.file_url)
|
||||||
|
.map((item) => item.file_url);
|
||||||
if (!urls.length) return;
|
if (!urls.length) return;
|
||||||
uni.previewImage({ urls, current });
|
|
||||||
|
const currentIndex = urls.indexOf(current);
|
||||||
|
imagePreviewUrls.value = urls;
|
||||||
|
imagePreviewIndex.value = currentIndex >= 0 ? currentIndex : 0;
|
||||||
|
imagePreviewVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeImagePreview() {
|
||||||
|
imagePreviewVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImagePreviewChange(event: { detail?: { current?: number } }) {
|
||||||
|
imagePreviewIndex.value = Number(event.detail?.current || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAsset(item: EvidenceAttachmentAsset, files: EvidenceAttachmentAsset[]) {
|
function openAsset(item: EvidenceAttachmentAsset, files: EvidenceAttachmentAsset[]) {
|
||||||
@@ -209,6 +250,12 @@ function downloadPdf() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(traceInfoVisible, (visible) => {
|
||||||
|
if (!visible && activeTab.value === "trace") {
|
||||||
|
activeTab.value = "product";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onLoad(async (options) => {
|
onLoad(async (options) => {
|
||||||
const id = Number(options?.id || 0);
|
const id = Number(options?.id || 0);
|
||||||
const currentReportNo = String(options?.report_no || "");
|
const currentReportNo = String(options?.report_no || "");
|
||||||
@@ -237,7 +284,7 @@ onLoad(async (options) => {
|
|||||||
<view class="app-page report-page">
|
<view class="app-page report-page">
|
||||||
<view v-if="!pageReady && loading" class="section notice-card">
|
<view v-if="!pageReady && loading" class="section notice-card">
|
||||||
<view class="notice-card__title">正在加载报告详情</view>
|
<view class="notice-card__title">正在加载报告详情</view>
|
||||||
<view class="notice-card__desc">请稍候,我们正在同步报告正文、追溯信息与 PDF 文件。</view>
|
<view class="notice-card__desc">请稍候,我们正在同步报告正文与 PDF 文件。</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else-if="!pageReady && loadError" class="section notice-card">
|
<view v-else-if="!pageReady && loadError" class="section notice-card">
|
||||||
@@ -247,48 +294,67 @@ onLoad(async (options) => {
|
|||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<view class="report-shell">
|
<view class="report-shell">
|
||||||
<view class="report-carousel">
|
<view class="report-cover">
|
||||||
<swiper v-if="reportImages.length" class="report-carousel__swiper" indicator-dots circular>
|
<swiper v-if="reportImages.length" class="report-cover__swiper" indicator-dots circular>
|
||||||
<swiper-item v-for="item in reportImages" :key="item.file_url || item.file_id">
|
<swiper-item v-for="item in reportImages" :key="item.file_url || item.file_id">
|
||||||
<image
|
<image
|
||||||
class="report-carousel__image"
|
class="report-cover__image"
|
||||||
:src="item.thumbnail_url || item.file_url"
|
:src="item.thumbnail_url || item.file_url"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
@click="previewImages(reportImages, item.file_url)"
|
@click="previewImages(reportImages, item.file_url)"
|
||||||
/>
|
/>
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
</swiper>
|
</swiper>
|
||||||
<view v-else class="report-carousel__empty">暂无鉴定图片</view>
|
<view v-else class="report-cover__empty">暂无鉴定图片</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="report-summary">
|
<view class="report-meta">
|
||||||
<view class="report-summary__row">
|
<view class="report-meta__row">
|
||||||
<text class="report-summary__label">产品名称</text>
|
<text class="report-meta__label">产品名称</text>
|
||||||
<text class="report-summary__value">{{ productName }}</text>
|
<text class="report-meta__value">{{ productName }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="report-summary__row">
|
<view class="report-meta__row">
|
||||||
<text class="report-summary__label">检测机构</text>
|
<text class="report-meta__label">检测机构</text>
|
||||||
<text class="report-summary__value">{{ institutionName }}</text>
|
<text class="report-meta__value">{{ institutionName }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="report-summary__tools">
|
<view class="report-meta__row">
|
||||||
<text class="report-summary__chip">报告编号 {{ detail.report_header.report_no }}</text>
|
<text class="report-meta__label">报告编号</text>
|
||||||
<text class="report-summary__chip">出具日期 {{ detail.report_header.publish_time || "-" }}</text>
|
<text class="report-meta__value">{{ reportNo || "-" }}</text>
|
||||||
<text class="report-summary__download" @click="downloadPdf">{{ downloading ? "下载中..." : "下载 PDF" }}</text>
|
</view>
|
||||||
|
<view class="report-meta__row report-meta__row--date">
|
||||||
|
<text class="report-meta__label">出具日期</text>
|
||||||
|
<text class="report-meta__download" @click="downloadPdf">{{ downloading ? "下载中" : "下载PDF" }}</text>
|
||||||
|
<text class="report-meta__value">{{ publishTime }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="report-tabs">
|
<view class="report-tabs">
|
||||||
<view :class="['report-tab', activeTab === 'product' ? 'report-tab--active' : '']" @click="activeTab = 'product'">产品信息</view>
|
<view :class="['report-tab', activeTab === 'product' ? 'report-tab--active' : '']" @click="activeTab = 'product'">产品信息</view>
|
||||||
<view :class="['report-tab', activeTab === 'trace' ? 'report-tab--active' : '']" @click="activeTab = 'trace'">追溯信息</view>
|
<view v-if="traceInfoVisible" :class="['report-tab', activeTab === 'trace' ? 'report-tab--active' : '']" @click="activeTab = 'trace'">追溯信息</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="activeTab === 'product'" class="report-panel">
|
<view v-if="activeTab === 'product'" class="report-panel">
|
||||||
<view v-for="(item, index) in productItems" :key="`${item.label}-${index}`" class="product-row">
|
<view class="report-watermark" aria-hidden="true"></view>
|
||||||
<view class="product-row__label">{{ item.label }}</view>
|
|
||||||
<view class="product-row__value" :class="item.label === '检测结论' ? 'product-row__value--result' : ''">
|
<view class="report-result">
|
||||||
{{ item.value || "-" }}
|
<view class="report-result__content">
|
||||||
|
<view class="report-result__label">{{ resultItem.label }}</view>
|
||||||
|
<view class="report-result__value">{{ resultItem.value || "-" }}</view>
|
||||||
|
<view v-if="resultItem.remark" class="report-result__desc">{{ resultItem.remark }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="report-seal">
|
||||||
|
<text class="report-seal__brand">ANXINYAN</text>
|
||||||
|
<text class="report-seal__main">可信</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="product-spec">
|
||||||
|
<view v-for="(item, index) in productSpecItems" :key="`${item.label}-${index}`" class="product-spec__row">
|
||||||
|
<view class="product-spec__label">{{ item.label }}</view>
|
||||||
|
<view class="product-spec__line"></view>
|
||||||
|
<view class="product-spec__value">{{ item.value || "-" }}</view>
|
||||||
|
<view v-if="item.remark" class="product-spec__remark">{{ item.remark }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="item.remark" class="product-row__remark">{{ item.remark }}</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="zhongjianReportFiles.length" class="inline-section">
|
<view v-if="zhongjianReportFiles.length" class="inline-section">
|
||||||
@@ -317,7 +383,8 @@ onLoad(async (options) => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="report-panel">
|
<view v-else-if="traceInfoVisible" class="report-panel report-panel--trace">
|
||||||
|
<view v-if="traceNodes.length === 0" class="trace-empty">暂无追溯信息</view>
|
||||||
<view v-for="node in traceNodes" :key="node.code" class="trace-node">
|
<view v-for="node in traceNodes" :key="node.code" class="trace-node">
|
||||||
<view class="trace-node__head">
|
<view class="trace-node__head">
|
||||||
<view>
|
<view>
|
||||||
@@ -353,6 +420,24 @@ onLoad(async (options) => {
|
|||||||
<view class="btn btn--primary" @click="openAntiModal">防伪查询</view>
|
<view class="btn btn--primary" @click="openAntiModal">防伪查询</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="imagePreviewVisible" class="image-preview-mask" @click="closeImagePreview">
|
||||||
|
<view class="image-preview__close" @click.stop="closeImagePreview">×</view>
|
||||||
|
<swiper
|
||||||
|
class="image-preview__swiper"
|
||||||
|
:current="imagePreviewIndex"
|
||||||
|
:circular="imagePreviewUrls.length > 1"
|
||||||
|
@change="handleImagePreviewChange"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<swiper-item v-for="url in imagePreviewUrls" :key="url" class="image-preview__item">
|
||||||
|
<image class="image-preview__image" :src="url" mode="aspectFit" />
|
||||||
|
</swiper-item>
|
||||||
|
</swiper>
|
||||||
|
<view v-if="imagePreviewUrls.length > 1" class="image-preview__counter">
|
||||||
|
{{ imagePreviewIndex + 1 }} / {{ imagePreviewUrls.length }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view v-if="antiModalVisible" class="anti-modal-mask" @click="closeAntiModal">
|
<view v-if="antiModalVisible" class="anti-modal-mask" @click="closeAntiModal">
|
||||||
<view class="anti-modal" @click.stop>
|
<view class="anti-modal" @click.stop>
|
||||||
<view class="anti-modal__title">防伪查询</view>
|
<view class="anti-modal__title">防伪查询</view>
|
||||||
@@ -388,115 +473,121 @@ onLoad(async (options) => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.report-page {
|
.report-page {
|
||||||
padding-bottom: 148rpx;
|
width: 100vw;
|
||||||
background: #eef6ff;
|
max-width: 100vw;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 28rpx 32rpx 170rpx;
|
||||||
|
background: #f1f3f6;
|
||||||
|
color: #3c3f45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-shell {
|
.report-shell {
|
||||||
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid rgba(33, 94, 160, 0.12);
|
border-radius: 28rpx;
|
||||||
border-radius: 8rpx;
|
background: #ffffff;
|
||||||
background: rgba(255, 255, 255, 0.96);
|
box-shadow: 0 18rpx 48rpx rgba(31, 36, 48, 0.08);
|
||||||
box-shadow: 0 18rpx 42rpx rgba(30, 76, 130, 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-carousel {
|
.report-cover {
|
||||||
margin: 0 24rpx;
|
height: 356rpx;
|
||||||
height: 392rpx;
|
margin: 28rpx 28rpx 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #eaf0f6;
|
border-radius: 10rpx;
|
||||||
|
background: #e8eaee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-carousel__swiper,
|
.report-cover__swiper,
|
||||||
.report-carousel__image,
|
.report-cover__image,
|
||||||
.report-carousel__empty {
|
.report-cover__empty {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-carousel__image {
|
.report-cover__image {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-carousel__empty {
|
.report-cover__empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: var(--color-text-muted);
|
color: #8c919b;
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-summary {
|
.report-meta {
|
||||||
padding: 34rpx 40rpx 22rpx;
|
padding: 34rpx 28rpx 12rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-summary__row {
|
.report-meta__row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: flex-start;
|
||||||
gap: 20rpx;
|
gap: 18rpx;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: 48rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-summary__row + .report-summary__row {
|
.report-meta__row + .report-meta__row {
|
||||||
margin-top: 24rpx;
|
margin-top: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-summary__label {
|
.report-meta__label {
|
||||||
flex: 0 0 128rpx;
|
flex: 0 0 138rpx;
|
||||||
color: var(--color-text-muted);
|
display: block;
|
||||||
font-size: 30rpx;
|
color: #7d828a;
|
||||||
|
font-size: 28rpx;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-summary__value {
|
.report-meta__value {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
display: block;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
color: var(--color-heading);
|
color: #44474d;
|
||||||
font-size: 34rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 900;
|
font-weight: 700;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
word-break: break-word;
|
text-align: right;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-summary__tools {
|
.report-meta__row--date .report-meta__value {
|
||||||
display: flex;
|
flex: 0 1 auto;
|
||||||
flex-wrap: wrap;
|
margin-left: auto;
|
||||||
gap: 12rpx;
|
word-break: normal;
|
||||||
margin-top: 28rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-summary__chip,
|
.report-meta__download {
|
||||||
.report-summary__download {
|
flex: 0 0 auto;
|
||||||
padding: 10rpx 14rpx;
|
display: block;
|
||||||
|
min-height: 38rpx;
|
||||||
|
padding: 0 16rpx;
|
||||||
|
border: 1px solid rgba(221, 179, 47, 0.74);
|
||||||
border-radius: 6rpx;
|
border-radius: 6rpx;
|
||||||
font-size: 22rpx;
|
background: #fff9e6;
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-summary__chip {
|
|
||||||
background: #f3f7fb;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-summary__download {
|
|
||||||
border: 1px solid var(--color-accent);
|
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
font-weight: 800;
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 36rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-tabs {
|
.report-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 112rpx;
|
gap: 104rpx;
|
||||||
padding: 10rpx 40rpx 28rpx;
|
padding: 26rpx 40rpx 34rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-tab {
|
.report-tab {
|
||||||
position: relative;
|
position: relative;
|
||||||
color: #8b929d;
|
color: #8e9298;
|
||||||
font-size: 34rpx;
|
font-size: 32rpx;
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,9 +598,9 @@ onLoad(async (options) => {
|
|||||||
.report-tab--active::after {
|
.report-tab--active::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
bottom: -8rpx;
|
bottom: -10rpx;
|
||||||
width: 42rpx;
|
width: 44rpx;
|
||||||
height: 8rpx;
|
height: 6rpx;
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
background: var(--color-accent);
|
background: var(--color-accent);
|
||||||
content: "";
|
content: "";
|
||||||
@@ -517,63 +608,185 @@ onLoad(async (options) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.report-panel {
|
.report-panel {
|
||||||
|
position: relative;
|
||||||
min-height: 440rpx;
|
min-height: 440rpx;
|
||||||
padding: 18rpx 40rpx 46rpx;
|
padding: 18rpx 28rpx 48rpx;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-row {
|
.report-watermark {
|
||||||
padding: 22rpx 0;
|
position: absolute;
|
||||||
border-bottom: 1px solid rgba(104, 121, 141, 0.14);
|
top: -6rpx;
|
||||||
|
left: 50%;
|
||||||
|
width: 520rpx;
|
||||||
|
height: 430rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.46;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background:
|
||||||
|
repeating-radial-gradient(ellipse at center, rgba(230, 195, 79, 0.2) 0, rgba(230, 195, 79, 0.2) 2rpx, transparent 3rpx, transparent 17rpx),
|
||||||
|
repeating-conic-gradient(from 0deg, rgba(230, 195, 79, 0.12) 0deg 8deg, transparent 8deg 16deg);
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-row__label {
|
.report-result {
|
||||||
color: var(--color-text-muted);
|
position: relative;
|
||||||
font-size: 30rpx;
|
z-index: 1;
|
||||||
line-height: 1.4;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 24rpx;
|
||||||
|
padding: 10rpx 0 30rpx;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-row__value {
|
.report-result__content {
|
||||||
margin-top: 10rpx;
|
flex: 1;
|
||||||
color: var(--color-heading);
|
min-width: 0;
|
||||||
font-size: 32rpx;
|
}
|
||||||
|
|
||||||
|
.report-result__label {
|
||||||
|
color: #3d3f44;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-result__value {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
color: #e04135;
|
||||||
|
font-size: 38rpx;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
line-height: 1.4;
|
line-height: 1.22;
|
||||||
word-break: break-word;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-row__value--result {
|
.report-result__desc {
|
||||||
color: #d83b4c;
|
margin-top: 10rpx;
|
||||||
}
|
color: #6f747c;
|
||||||
|
|
||||||
.product-row__remark {
|
|
||||||
margin-top: 8rpx;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
line-height: 1.5;
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-seal {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 104rpx;
|
||||||
|
height: 104rpx;
|
||||||
|
margin-top: 2rpx;
|
||||||
|
border: 4rpx solid rgba(56, 164, 73, 0.8);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
color: #39a54b;
|
||||||
|
transform: rotate(-10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-seal__brand {
|
||||||
|
font-size: 16rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-seal__main {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-spec {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-spec__row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
min-height: 54rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-spec__label {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: block;
|
||||||
|
color: #858991;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-spec__line {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 32rpx;
|
||||||
|
height: 1px;
|
||||||
|
border-bottom: 1px dotted #b9bdc4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-spec__value {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
display: block;
|
||||||
|
max-width: 58%;
|
||||||
|
color: #5b5f67;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-spec__remark {
|
||||||
|
flex-basis: 100%;
|
||||||
|
display: block;
|
||||||
|
margin: -2rpx 0 12rpx 0;
|
||||||
|
color: #8b9098;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-section {
|
.inline-section {
|
||||||
margin-top: 30rpx;
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
margin-top: 32rpx;
|
||||||
|
padding-top: 24rpx;
|
||||||
|
border-top: 1px solid #ececec;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-section__title {
|
.inline-section__title {
|
||||||
color: var(--color-heading);
|
color: #3f4248;
|
||||||
font-size: 30rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 900;
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-panel--trace {
|
||||||
|
padding-top: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-empty {
|
||||||
|
padding: 58rpx 0;
|
||||||
|
color: #8b9098;
|
||||||
|
font-size: 26rpx;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-node {
|
.trace-node {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 24rpx 0 26rpx 34rpx;
|
padding: 26rpx 0 28rpx 38rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-node::before {
|
.trace-node::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 8rpx;
|
left: 9rpx;
|
||||||
top: 34rpx;
|
top: 38rpx;
|
||||||
bottom: -20rpx;
|
bottom: -22rpx;
|
||||||
width: 2rpx;
|
width: 2rpx;
|
||||||
background: #d9e5f2;
|
background: #e4e0d3;
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,7 +797,7 @@ onLoad(async (options) => {
|
|||||||
.trace-node::after {
|
.trace-node::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 34rpx;
|
top: 36rpx;
|
||||||
width: 18rpx;
|
width: 18rpx;
|
||||||
height: 18rpx;
|
height: 18rpx;
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
@@ -595,28 +808,28 @@ onLoad(async (options) => {
|
|||||||
.trace-node__head {
|
.trace-node__head {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 18rpx;
|
gap: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-node__title {
|
.trace-node__title {
|
||||||
color: var(--color-heading);
|
color: #3f4248;
|
||||||
font-size: 30rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 900;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-node__time,
|
.trace-node__time,
|
||||||
.trace-node__empty {
|
.trace-node__empty {
|
||||||
margin-top: 8rpx;
|
margin-top: 8rpx;
|
||||||
color: var(--color-text-muted);
|
color: #8b9098;
|
||||||
font-size: 24rpx;
|
font-size: 23rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-node__status {
|
.trace-node__status {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
padding: 8rpx 12rpx;
|
padding: 8rpx 12rpx;
|
||||||
border-radius: 6rpx;
|
border-radius: 6rpx;
|
||||||
background: #f3f5f8;
|
background: #f5f2e9;
|
||||||
color: var(--color-text-muted);
|
color: #9f8433;
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
@@ -629,7 +842,7 @@ onLoad(async (options) => {
|
|||||||
.asset-grid {
|
.asset-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 14rpx;
|
gap: 12rpx;
|
||||||
margin-top: 18rpx;
|
margin-top: 18rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,9 +850,9 @@ onLoad(async (options) => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid rgba(104, 121, 141, 0.16);
|
border: 1px solid rgba(127, 119, 94, 0.16);
|
||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
background: #f4f7fa;
|
background: #f5f3ee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-tile__image {
|
.asset-tile__image {
|
||||||
@@ -656,7 +869,7 @@ onLoad(async (options) => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
font-weight: 900;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-tile__play {
|
.asset-tile__play {
|
||||||
@@ -686,10 +899,10 @@ onLoad(async (options) => {
|
|||||||
gap: 16rpx;
|
gap: 16rpx;
|
||||||
padding: 18rpx;
|
padding: 18rpx;
|
||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
background: #f6f8fb;
|
background: #faf8f1;
|
||||||
color: var(--color-heading);
|
color: #3f4248;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-list__type {
|
.asset-list__type {
|
||||||
@@ -700,6 +913,92 @@ onLoad(async (options) => {
|
|||||||
.report-actions {
|
.report-actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 28rpx;
|
||||||
|
width: 100vw;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 22rpx 32rpx calc(22rpx + env(safe-area-inset-bottom));
|
||||||
|
background: rgba(241, 243, 246, 0.96);
|
||||||
|
border-top: 0;
|
||||||
|
box-shadow: 0 -10rpx 34rpx rgba(31, 36, 48, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-actions .btn {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 76rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-actions .btn--secondary {
|
||||||
|
border: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #4f535a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-actions .btn--primary {
|
||||||
|
background: #dfb733;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-mask {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 200;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.88);
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview__swiper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview__image {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview__close {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 201;
|
||||||
|
top: calc(110rpx + env(safe-area-inset-top));
|
||||||
|
right: 34rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 72rpx;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview__counter {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 201;
|
||||||
|
left: 50%;
|
||||||
|
bottom: calc(42rpx + env(safe-area-inset-bottom));
|
||||||
|
min-width: 96rpx;
|
||||||
|
padding: 10rpx 22rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(0, 0, 0, 0.42);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-align: center;
|
||||||
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.anti-modal-mask {
|
.anti-modal-mask {
|
||||||
@@ -710,19 +1009,19 @@ onLoad(async (options) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 48rpx;
|
padding: 48rpx;
|
||||||
background: rgba(13, 30, 48, 0.46);
|
background: rgba(31, 36, 48, 0.42);
|
||||||
}
|
}
|
||||||
|
|
||||||
.anti-modal {
|
.anti-modal {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 620rpx;
|
max-width: 620rpx;
|
||||||
padding: 34rpx;
|
padding: 34rpx;
|
||||||
border-radius: 8rpx;
|
border-radius: 20rpx;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.anti-modal__title {
|
.anti-modal__title {
|
||||||
color: var(--color-heading);
|
color: #2f3238;
|
||||||
font-size: 36rpx;
|
font-size: 36rpx;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
@@ -730,7 +1029,7 @@ onLoad(async (options) => {
|
|||||||
|
|
||||||
.anti-modal__desc {
|
.anti-modal__desc {
|
||||||
margin-top: 12rpx;
|
margin-top: 12rpx;
|
||||||
color: var(--color-text-muted);
|
color: #7e838b;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
@@ -742,7 +1041,7 @@ onLoad(async (options) => {
|
|||||||
.anti-result {
|
.anti-result {
|
||||||
margin-top: 20rpx;
|
margin-top: 20rpx;
|
||||||
padding: 18rpx;
|
padding: 18rpx;
|
||||||
border-radius: 8rpx;
|
border-radius: 10rpx;
|
||||||
background: #f7f8fa;
|
background: #f7f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -755,14 +1054,14 @@ onLoad(async (options) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.anti-result__title {
|
.anti-result__title {
|
||||||
color: var(--color-heading);
|
color: #2f3238;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.anti-result__desc {
|
.anti-result__desc {
|
||||||
margin-top: 8rpx;
|
margin-top: 8rpx;
|
||||||
color: var(--color-text-muted);
|
color: #7e838b;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
@@ -776,9 +1075,9 @@ onLoad(async (options) => {
|
|||||||
|
|
||||||
.anti-modal__button {
|
.anti-modal__button {
|
||||||
height: 82rpx;
|
height: 82rpx;
|
||||||
border-radius: 8rpx;
|
border-radius: 999rpx;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 900;
|
font-weight: 800;
|
||||||
line-height: 82rpx;
|
line-height: 82rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,19 @@ export interface AdminDirectUploadPolicy {
|
|||||||
expires_at?: string;
|
expires_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminExpressCompanyItem {
|
||||||
|
id: number;
|
||||||
|
company_name: string;
|
||||||
|
company_code: string;
|
||||||
|
status: string;
|
||||||
|
status_text: string;
|
||||||
|
is_default: boolean;
|
||||||
|
sort_order: number;
|
||||||
|
remark: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
function filenameFromPath(filePath: string) {
|
function filenameFromPath(filePath: string) {
|
||||||
return filePath.split(/[\\/]/).pop() || `upload-${Date.now()}`;
|
return filePath.split(/[\\/]/).pop() || `upload-${Date.now()}`;
|
||||||
}
|
}
|
||||||
@@ -475,6 +488,9 @@ export const adminApi = {
|
|||||||
getManualOrderMeta() {
|
getManualOrderMeta() {
|
||||||
return request<AdminManualOrderMeta>("/api/admin/manual-order/meta");
|
return request<AdminManualOrderMeta>("/api/admin/manual-order/meta");
|
||||||
},
|
},
|
||||||
|
getExpressCompanies(params: { enabled_only?: 0 | 1 } = { enabled_only: 1 }) {
|
||||||
|
return request<{ list: AdminExpressCompanyItem[]; default_company: string }>("/api/admin/express-companies", { params });
|
||||||
|
},
|
||||||
createManualOrder(data: AdminManualOrderCreatePayload) {
|
createManualOrder(data: AdminManualOrderCreatePayload) {
|
||||||
return request<AdminManualOrderCreateResponse>("/api/admin/manual-order/create", {
|
return request<AdminManualOrderCreateResponse>("/api/admin/manual-order/create", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ import { computed, ref } from "vue";
|
|||||||
import { onLoad } from "@dcloudio/uni-app";
|
import { onLoad } from "@dcloudio/uni-app";
|
||||||
import { adminApi, type AdminManualOrderCreatePayload, type AdminManualOrderMeta } from "../../api/admin";
|
import { adminApi, type AdminManualOrderCreatePayload, type AdminManualOrderMeta } from "../../api/admin";
|
||||||
import { showErrorToast, showInfoToast, withLoading } from "../../utils/feedback";
|
import { showErrorToast, showInfoToast, withLoading } from "../../utils/feedback";
|
||||||
import { buildRegionPickerState, updateRegionPickerIndexes } from "../../utils/regions";
|
import { recognizeReturnAddress } from "../../utils/address-recognition";
|
||||||
|
import { buildRegionPickerState, findRegionIndexes, updateRegionPickerIndexes } from "../../utils/regions";
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const meta = ref<AdminManualOrderMeta>({ categories: [], brands: [] });
|
const meta = ref<AdminManualOrderMeta>({ categories: [], brands: [] });
|
||||||
const form = ref<AdminManualOrderCreatePayload>(createForm());
|
const form = ref<AdminManualOrderCreatePayload>(createForm());
|
||||||
const regionPickerIndexes = ref<[number, number, number]>([0, 0, 0]);
|
const regionPickerIndexes = ref<[number, number, number]>([0, 0, 0]);
|
||||||
|
const addressRecognitionText = ref("");
|
||||||
|
|
||||||
const providerOptions = [
|
const providerOptions = [
|
||||||
{ label: "实物鉴定", value: "anxinyan" },
|
{ label: "实物鉴定", value: "anxinyan" },
|
||||||
@@ -110,6 +112,21 @@ function pickerText(options: Array<{ label?: string; name?: string }>, index: nu
|
|||||||
return item?.label || item?.name || fallback;
|
return item?.label || item?.name || fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyRecognizedReturnAddress() {
|
||||||
|
const result = recognizeReturnAddress(addressRecognitionText.value);
|
||||||
|
if (!result.ok || !result.address) {
|
||||||
|
showInfoToast(result.message || "寄回地址识别失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.value.return_address = {
|
||||||
|
...form.value.return_address,
|
||||||
|
...result.address,
|
||||||
|
};
|
||||||
|
regionPickerIndexes.value = findRegionIndexes(result.address);
|
||||||
|
showInfoToast("寄回地址已识别并填入");
|
||||||
|
}
|
||||||
|
|
||||||
function validateForm() {
|
function validateForm() {
|
||||||
const product = form.value.product_info;
|
const product = form.value.product_info;
|
||||||
const address = form.value.return_address;
|
const address = form.value.return_address;
|
||||||
@@ -173,6 +190,14 @@ onLoad(() => {
|
|||||||
|
|
||||||
<view class="card stack">
|
<view class="card stack">
|
||||||
<view class="card-title">寄回信息</view>
|
<view class="card-title">寄回信息</view>
|
||||||
|
<view class="address-recognition">
|
||||||
|
<textarea
|
||||||
|
v-model="addressRecognitionText"
|
||||||
|
class="textarea address-recognition__textarea"
|
||||||
|
placeholder="粘贴收货人、收货电话、收货地址,自动识别后填入下方字段"
|
||||||
|
/>
|
||||||
|
<button class="btn btn--ghost address-recognition__button" @click="applyRecognizedReturnAddress">识别并填入</button>
|
||||||
|
</view>
|
||||||
<input v-model="form.return_address.consignee" class="field" placeholder="收件人" />
|
<input v-model="form.return_address.consignee" class="field" placeholder="收件人" />
|
||||||
<input v-model="form.return_address.mobile" class="field" type="number" placeholder="手机号,用于匹配用户" />
|
<input v-model="form.return_address.mobile" class="field" type="number" placeholder="手机号,用于匹配用户" />
|
||||||
<picker
|
<picker
|
||||||
@@ -216,6 +241,19 @@ onLoad(() => {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.address-recognition {
|
||||||
|
display: grid;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-recognition__textarea {
|
||||||
|
min-height: 188rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-recognition__button {
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
.region-field {
|
.region-field {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16rpx;
|
gap: 16rpx;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const resultMetaItems = computed(() => {
|
|||||||
for (const point of normalizedKeyPoints(result.key_points)) {
|
for (const point of normalizedKeyPoints(result.key_points)) {
|
||||||
items.push({
|
items.push({
|
||||||
label: point.point_name,
|
label: point.point_name,
|
||||||
value: [point.point_value, point.point_remark].filter(Boolean).join(";") || "-",
|
value: point.point_value || "-",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ function normalizedKeyPoints(value: unknown) {
|
|||||||
point_remark: textValue(point.point_remark),
|
point_remark: textValue(point.point_remark),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((item) => item.point_value || item.point_remark);
|
.filter((item) => item.point_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isImageAsset(item: AdminFileAsset) {
|
function isImageAsset(item: AdminFileAsset) {
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { onLoad } from "@dcloudio/uni-app";
|
import { onLoad } from "@dcloudio/uni-app";
|
||||||
import { adminApi, type AdminFileAsset, type AdminWarehouseWorkbenchContext } from "../../api/admin";
|
import { adminApi, type AdminExpressCompanyItem, type AdminFileAsset, type AdminWarehouseWorkbenchContext } from "../../api/admin";
|
||||||
import { showErrorToast, showInfoToast, withLoading } from "../../utils/feedback";
|
import { showErrorToast, showInfoToast, withLoading } from "../../utils/feedback";
|
||||||
|
|
||||||
const internalTagNo = ref("");
|
const internalTagNo = ref("");
|
||||||
const expressCompany = ref("");
|
const expressCompany = ref("");
|
||||||
const trackingNo = ref("");
|
const trackingNo = ref("");
|
||||||
|
const expressCompanyOptions = ref<AdminExpressCompanyItem[]>([]);
|
||||||
|
const expressCompanyLoading = ref(false);
|
||||||
|
const defaultExpressCompany = ref("");
|
||||||
const context = ref<AdminWarehouseWorkbenchContext | null>(null);
|
const context = ref<AdminWarehouseWorkbenchContext | null>(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
@@ -16,6 +19,14 @@ const activeVideo = ref<AdminFileAsset | null>(null);
|
|||||||
const RETURN_SHIPPED_STORAGE_KEY = "warehouse_return_shipped_context";
|
const RETURN_SHIPPED_STORAGE_KEY = "warehouse_return_shipped_context";
|
||||||
|
|
||||||
const returnConfirmed = computed(() => Boolean(context.value?.transfer_flow?.return_confirmed_at));
|
const returnConfirmed = computed(() => Boolean(context.value?.transfer_flow?.return_confirmed_at));
|
||||||
|
const expressCompanyNames = computed(() => {
|
||||||
|
const names = expressCompanyOptions.value.map((item) => item.company_name);
|
||||||
|
if (expressCompany.value && !names.includes(expressCompany.value)) {
|
||||||
|
return [expressCompany.value, ...names];
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
});
|
||||||
|
const expressCompanyIndex = computed(() => Math.max(0, expressCompanyNames.value.findIndex((item) => item === expressCompany.value)));
|
||||||
const canSubmit = computed(() =>
|
const canSubmit = computed(() =>
|
||||||
returnConfirmed.value &&
|
returnConfirmed.value &&
|
||||||
Boolean(expressCompany.value.trim()) &&
|
Boolean(expressCompany.value.trim()) &&
|
||||||
@@ -53,6 +64,27 @@ async function fetchContext() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchExpressCompanies() {
|
||||||
|
expressCompanyLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await adminApi.getExpressCompanies({ enabled_only: 1 });
|
||||||
|
expressCompanyOptions.value = response.list;
|
||||||
|
defaultExpressCompany.value = response.default_company;
|
||||||
|
if (!expressCompany.value) {
|
||||||
|
expressCompany.value = response.default_company || response.list[0]?.company_name || "";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error, "快递公司列表加载失败");
|
||||||
|
} finally {
|
||||||
|
expressCompanyLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExpressCompanyChange(event: any) {
|
||||||
|
const index = Number(event.detail?.value || 0);
|
||||||
|
expressCompany.value = expressCompanyNames.value[index] || "";
|
||||||
|
}
|
||||||
|
|
||||||
function scanTrackingNo() {
|
function scanTrackingNo() {
|
||||||
uni.scanCode({
|
uni.scanCode({
|
||||||
scanType: ["barCode", "qrCode"],
|
scanType: ["barCode", "qrCode"],
|
||||||
@@ -189,6 +221,7 @@ async function submitReturnShipping() {
|
|||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
internalTagNo.value = readQueryString(options?.internal_tag_no);
|
internalTagNo.value = readQueryString(options?.internal_tag_no);
|
||||||
|
void fetchExpressCompanies();
|
||||||
void fetchContext();
|
void fetchContext();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -241,7 +274,13 @@ onLoad((options) => {
|
|||||||
<view class="card">
|
<view class="card">
|
||||||
<view class="card-title">快递单号</view>
|
<view class="card-title">快递单号</view>
|
||||||
<view class="card-desc">报告确认后登记回寄物流信息。</view>
|
<view class="card-desc">报告确认后登记回寄物流信息。</view>
|
||||||
<input v-model="expressCompany" class="field form-field" placeholder="回寄快递公司,例如:顺丰速运" />
|
<picker :range="expressCompanyNames" :value="expressCompanyIndex" @change="onExpressCompanyChange">
|
||||||
|
<view class="field picker-field form-field">
|
||||||
|
<text v-if="expressCompany" class="picker-field__value">{{ expressCompany }}</text>
|
||||||
|
<text v-else class="picker-field__placeholder">{{ expressCompanyLoading ? "正在加载快递公司" : "请选择回寄快递公司" }}</text>
|
||||||
|
<text class="picker-field__arrow"></text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
<view class="scan-control">
|
<view class="scan-control">
|
||||||
<input v-model="trackingNo" class="field scan-input" placeholder="扫描或输入回寄运单号" />
|
<input v-model="trackingNo" class="field scan-input" placeholder="扫描或输入回寄运单号" />
|
||||||
<button class="btn scan-button" @click="scanTrackingNo">扫码</button>
|
<button class="btn scan-button" @click="scanTrackingNo">扫码</button>
|
||||||
@@ -305,6 +344,38 @@ onLoad((options) => {
|
|||||||
margin-top: 18rpx;
|
margin-top: 18rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.picker-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-field__value,
|
||||||
|
.picker-field__placeholder {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-field__value {
|
||||||
|
color: var(--work-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-field__placeholder {
|
||||||
|
color: var(--work-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-field__arrow {
|
||||||
|
width: 14rpx;
|
||||||
|
height: 14rpx;
|
||||||
|
flex: 0 0 14rpx;
|
||||||
|
border-right: 3rpx solid var(--work-text-soft);
|
||||||
|
border-bottom: 3rpx solid var(--work-text-soft);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
.scan-control {
|
.scan-control {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 14rpx;
|
gap: 14rpx;
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ function templateKeyPointsPayload() {
|
|||||||
point_code: item.point_code,
|
point_code: item.point_code,
|
||||||
point_name: item.point_name,
|
point_name: item.point_name,
|
||||||
point_value: item.point_value || "",
|
point_value: item.point_value || "",
|
||||||
point_remark: item.point_remark || "",
|
point_remark: "",
|
||||||
})) || [];
|
})) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,13 +787,6 @@ onShow(() => {
|
|||||||
:placeholder="`${item.point_name} 值`"
|
:placeholder="`${item.point_name} 值`"
|
||||||
@input="updateTemplatePointFromInput(index, 'point_value', $event)"
|
@input="updateTemplatePointFromInput(index, 'point_value', $event)"
|
||||||
/>
|
/>
|
||||||
<textarea
|
|
||||||
:value="item.point_remark"
|
|
||||||
class="textarea"
|
|
||||||
:disabled="isTaskReadonly"
|
|
||||||
:placeholder="`${item.point_name} 说明`"
|
|
||||||
@input="updateTemplatePointFromInput(index, 'point_remark', $event)"
|
|
||||||
/>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -888,13 +881,6 @@ onShow(() => {
|
|||||||
:placeholder="`${item.point_name} 值`"
|
:placeholder="`${item.point_name} 值`"
|
||||||
@input="updateTemplatePointFromInput(index, 'point_value', $event)"
|
@input="updateTemplatePointFromInput(index, 'point_value', $event)"
|
||||||
/>
|
/>
|
||||||
<textarea
|
|
||||||
:value="item.point_remark"
|
|
||||||
class="textarea"
|
|
||||||
:disabled="isTaskReadonly"
|
|
||||||
:placeholder="`${item.point_name} 说明`"
|
|
||||||
@input="updateTemplatePointFromInput(index, 'point_remark', $event)"
|
|
||||||
/>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|||||||
213
work-app/src/utils/address-recognition.ts
Normal file
213
work-app/src/utils/address-recognition.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import regionSource from "../static/regions/pca.json";
|
||||||
|
|
||||||
|
type RegionNode = {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
children?: RegionNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RecognizedReturnAddress = {
|
||||||
|
consignee: string;
|
||||||
|
mobile: string;
|
||||||
|
province: string;
|
||||||
|
city: string;
|
||||||
|
district: string;
|
||||||
|
detail_address: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RecognizeReturnAddressResult = {
|
||||||
|
ok: boolean;
|
||||||
|
address?: RecognizedReturnAddress;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const regionTree = regionSource as RegionNode[];
|
||||||
|
const nameLabels = ["收货人", "收件人", "姓名", "联系人", "取件人"];
|
||||||
|
const mobileLabels = ["收货电话", "联系电话", "手机号", "手机号码", "手机", "电话"];
|
||||||
|
const addressLabels = ["收货地址", "收件地址", "寄回地址", "地址"];
|
||||||
|
const allLabels = [...nameLabels, ...mobileLabels, ...addressLabels];
|
||||||
|
|
||||||
|
function normalizeLines(raw: string) {
|
||||||
|
return raw
|
||||||
|
.replace(/\r/g, "\n")
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelMatch(line: string, labels: string[]) {
|
||||||
|
for (const label of labels) {
|
||||||
|
const pattern = new RegExp(`^\\s*${label}\\s*[::]?\\s*(.*)$`);
|
||||||
|
const match = line.match(pattern);
|
||||||
|
if (match) {
|
||||||
|
return { label, value: String(match[1] || "").trim() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isKnownLabel(line: string) {
|
||||||
|
const normalized = line.replace(/\s+/g, "");
|
||||||
|
return allLabels.some((label) => normalized === label || normalized === `${label}:` || normalized === `${label}:`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractLabeledValue(lines: string[], labels: string[], block = false) {
|
||||||
|
for (let index = 0; index < lines.length; index += 1) {
|
||||||
|
const match = labelMatch(lines[index], labels);
|
||||||
|
if (!match) continue;
|
||||||
|
if (match.value) return match.value;
|
||||||
|
|
||||||
|
const values: string[] = [];
|
||||||
|
for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) {
|
||||||
|
const nextLine = lines[nextIndex];
|
||||||
|
if (labelMatch(nextLine, allLabels) && isKnownLabel(nextLine)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (labelMatch(nextLine, allLabels)?.value) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
values.push(nextLine);
|
||||||
|
if (!block) break;
|
||||||
|
}
|
||||||
|
return values.join(block ? "" : " ").trim();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMobile(value: string) {
|
||||||
|
const directMatch = value.match(/1[3-9]\d{9}/);
|
||||||
|
if (directMatch) return directMatch[0];
|
||||||
|
|
||||||
|
const digits = value.replace(/\D+/g, "");
|
||||||
|
return digits.match(/1[3-9]\d{9}/)?.[0] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripKnownAddressPrefixes(value: string) {
|
||||||
|
return value
|
||||||
|
.replace(/\s+/g, "")
|
||||||
|
.replace(/^(中国大陆|中华人民共和国|中国|大陆)+/, "")
|
||||||
|
.replace(/^(收货地址|收件地址|寄回地址|地址)[::]?/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function aliases(name: string) {
|
||||||
|
const suffixes = ["特别行政区", "壮族自治区", "回族自治区", "维吾尔自治区", "自治区", "自治州", "自治县", "地区", "省", "市", "区", "县", "旗", "盟"];
|
||||||
|
const values = [name];
|
||||||
|
for (const suffix of suffixes) {
|
||||||
|
if (name.endsWith(suffix) && name.length > suffix.length) {
|
||||||
|
values.push(name.slice(0, -suffix.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(new Set(values)).sort((a, b) => b.length - a.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function consumePrefix(text: string, names: string[]) {
|
||||||
|
for (const name of names) {
|
||||||
|
if (name && text.startsWith(name)) {
|
||||||
|
return { consumed: name.length, rest: text.slice(name.length) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDirectCity(province: RegionNode, city: RegionNode) {
|
||||||
|
return province.name === city.name || aliases(province.name).some((name) => aliases(city.name).includes(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchDistrict(city: RegionNode, text: string) {
|
||||||
|
for (const district of city.children || []) {
|
||||||
|
const match = consumePrefix(text, aliases(district.name));
|
||||||
|
if (match) {
|
||||||
|
return { district, detail: match.rest };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchRegion(addressText: string) {
|
||||||
|
const address = stripKnownAddressPrefixes(addressText);
|
||||||
|
if (!address) return null;
|
||||||
|
|
||||||
|
for (const province of regionTree) {
|
||||||
|
const provinceMatch = consumePrefix(address, aliases(province.name));
|
||||||
|
if (!provinceMatch) continue;
|
||||||
|
|
||||||
|
for (const city of province.children || []) {
|
||||||
|
const cityMatch = consumePrefix(provinceMatch.rest, aliases(city.name));
|
||||||
|
const districtSource = cityMatch ? cityMatch.rest : (isDirectCity(province, city) ? provinceMatch.rest : "");
|
||||||
|
if (!districtSource) continue;
|
||||||
|
|
||||||
|
const districtMatch = matchDistrict(city, districtSource);
|
||||||
|
if (districtMatch) {
|
||||||
|
return {
|
||||||
|
province: province.name,
|
||||||
|
city: city.name,
|
||||||
|
district: districtMatch.district.name,
|
||||||
|
detail_address: districtMatch.detail,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const province of regionTree) {
|
||||||
|
for (const city of province.children || []) {
|
||||||
|
const cityMatch = consumePrefix(address, aliases(city.name));
|
||||||
|
if (!cityMatch) continue;
|
||||||
|
|
||||||
|
const districtMatch = matchDistrict(city, cityMatch.rest);
|
||||||
|
if (districtMatch) {
|
||||||
|
return {
|
||||||
|
province: province.name,
|
||||||
|
city: city.name,
|
||||||
|
district: districtMatch.district.name,
|
||||||
|
detail_address: districtMatch.detail,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackAddressLine(lines: string[], consignee: string, mobile: string) {
|
||||||
|
return lines
|
||||||
|
.map((line) => labelMatch(line, allLabels)?.value || line)
|
||||||
|
.filter((line) => line && line !== consignee && !line.includes(mobile) && !normalizeMobile(line))
|
||||||
|
.sort((a, b) => b.length - a.length)[0] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recognizeReturnAddress(raw: string): RecognizeReturnAddressResult {
|
||||||
|
const lines = normalizeLines(raw);
|
||||||
|
if (!lines.length) {
|
||||||
|
return { ok: false, message: "请先粘贴寄回地址信息" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const consignee = extractLabeledValue(lines, nameLabels).trim();
|
||||||
|
const mobile = normalizeMobile(extractLabeledValue(lines, mobileLabels) || raw);
|
||||||
|
const addressText = extractLabeledValue(lines, addressLabels, true) || fallbackAddressLine(lines, consignee, mobile);
|
||||||
|
const region = matchRegion(addressText);
|
||||||
|
|
||||||
|
if (!consignee) {
|
||||||
|
return { ok: false, message: "未识别到收件人,请检查文本中是否包含收货人或收件人" };
|
||||||
|
}
|
||||||
|
if (!mobile) {
|
||||||
|
return { ok: false, message: "未识别到有效手机号,请检查文本中的收货电话" };
|
||||||
|
}
|
||||||
|
if (!region) {
|
||||||
|
return { ok: false, message: "未识别到省市区,请检查地址是否包含城市和区县" };
|
||||||
|
}
|
||||||
|
if (!region.detail_address.trim()) {
|
||||||
|
return { ok: false, message: "未识别到详细地址,请检查区县后的街道门牌信息" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
address: {
|
||||||
|
consignee,
|
||||||
|
mobile,
|
||||||
|
province: region.province,
|
||||||
|
city: region.city,
|
||||||
|
district: region.district,
|
||||||
|
detail_address: region.detail_address.trim(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user