增加了手机操作端

This commit is contained in:
wushumin
2026-05-15 14:01:36 +08:00
parent 9aac78b8da
commit dd56e0861b
107 changed files with 23547 additions and 346 deletions

View File

@@ -343,6 +343,7 @@ export interface ReportListItem {
export interface ReportDetailData {
evidence_attachments: EvidenceAttachmentAsset[];
zhongjian_report_files: EvidenceAttachmentAsset[];
report_header: {
report_id: number;
report_no: string;
@@ -352,6 +353,9 @@ export interface ReportDetailData {
service_provider: string;
institution_name: string;
publish_time: string;
zhongjian_report_no: string;
report_entry_admin_name: string;
report_entered_at: string;
};
result_info: Record<string, any>;
product_info: Record<string, any>;

View File

@@ -367,6 +367,7 @@ export const reportsFallback: ReportListItem[] = [
export const reportDetailFallback: ReportDetailData = {
evidence_attachments: [],
zhongjian_report_files: [],
report_header: {
report_id: 1,
report_no: "AXY-R-20260420-0001",
@@ -376,6 +377,9 @@ export const reportDetailFallback: ReportDetailData = {
service_provider: "zhongjian",
institution_name: "中检合作机构",
publish_time: "2026-04-18 18:26:00",
zhongjian_report_no: "ZJ-20260418-0001",
report_entry_admin_name: "王师傅",
report_entered_at: "2026-04-18 18:20:00",
},
result_info: {
result_status: "authentic",
@@ -406,9 +410,9 @@ export const reportDetailFallback: ReportDetailData = {
risk_notice_text: "本报告基于送检商品及当前提交资料出具。若商品状态或所附资料发生变化,报告结论可能不再适用。",
verify_info: {
report_no: "AXY-R-20260420-0001",
verify_status: "valid",
verify_url: "/#/pages/verify/result?report_no=AXY-R-20260420-0001",
verify_qrcode_url: "/#/pages/report/detail?report_no=AXY-R-20260420-0001",
verify_status: "",
verify_url: "",
verify_qrcode_url: "",
},
file_info: {
pdf_url: "http://127.0.0.1:8787/uploads/reports/20260418/AXY-R-20260420-0001.pdf",

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import { ref } from "vue";
import { computed, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { appApi } from "../../api/app";
import { showErrorToast, showInfoToast, withLoading } from "../../utils/feedback";
import { buildRegionPickerState, findRegionIndexes, updateRegionPickerIndexes } from "../../utils/regions";
const addressId = ref(0);
const saving = ref(false);
@@ -18,6 +19,15 @@ const form = ref({
is_default: false,
});
const regionPickerIndexes = ref<[number, number, number]>([0, 0, 0]);
const selectedRegion = computed(() => {
const { province, city, district } = form.value;
return province && city && district ? [province, city, district] : [];
});
const selectedRegionText = computed(() => selectedRegion.value.join(" / "));
const regionPickerState = computed(() => buildRegionPickerState(regionPickerIndexes.value));
function goBack() {
uni.navigateBack();
}
@@ -26,6 +36,34 @@ function handleDefaultChange(event: any) {
form.value.is_default = !!event?.detail?.value;
}
function syncRegionPickerIndexes() {
regionPickerIndexes.value = findRegionIndexes({
province: form.value.province,
city: form.value.city,
district: form.value.district,
});
}
function applyRegionSelection(selection: string[]) {
const [province = "", city = "", district = ""] = selection;
form.value.province = province;
form.value.city = city;
form.value.district = district;
}
function handleRegionColumnChange(event: any) {
regionPickerIndexes.value = updateRegionPickerIndexes(regionPickerState.value.indexes, {
column: event?.detail?.column || 0,
value: event?.detail?.value || 0,
});
}
function handleRegionChange(event: any) {
const indexes = event?.detail?.value || regionPickerState.value.indexes;
regionPickerIndexes.value = indexes;
applyRegionSelection(buildRegionPickerState(indexes).selection);
}
async function saveAddress() {
const payload = form.value;
if (!payload.consignee.trim() || !payload.mobile.trim() || !payload.province.trim() || !payload.city.trim() || !payload.district.trim() || !payload.detail_address.trim()) {
@@ -79,6 +117,7 @@ onLoad(async (options) => {
detail_address: data.detail_address,
is_default: data.is_default,
};
syncRegionPickerIndexes();
} catch (error) {
showErrorToast(error, "地址详情加载失败");
}
@@ -111,23 +150,22 @@ onLoad(async (options) => {
</view>
<view class="form-group">
<view class="form-group__label">省份</view>
<view class="field-box">
<input v-model="form.province" class="field-input" maxlength="20" placeholder="例如:广东省" />
</view>
</view>
<view class="form-group">
<view class="form-group__label">城市</view>
<view class="field-box">
<input v-model="form.city" class="field-input" maxlength="20" placeholder="例如:深圳市" />
</view>
</view>
<view class="form-group">
<view class="form-group__label">区县</view>
<view class="field-box">
<input v-model="form.district" class="field-input" maxlength="20" placeholder="例如:南山区" />
<view class="form-group__label">所在地区</view>
<view class="field-box field-box--picker">
<picker
class="region-picker"
mode="multiSelector"
:range="regionPickerState.columns"
:value="regionPickerState.indexes"
@columnchange="handleRegionColumnChange"
@change="handleRegionChange"
>
<view class="region-picker__content">
<text v-if="selectedRegionText" class="field-box__value">{{ selectedRegionText }}</text>
<text v-else class="field-box__placeholder">请选择省 / / 区县</text>
<text class="field-box__arrow"></text>
</view>
</picker>
</view>
</view>

View File

@@ -14,21 +14,33 @@ const loadError = ref("");
const qrImageSource = computed(() =>
resolveQrImageSource(detail.value.verify_info.verify_qrcode_url, detail.value.verify_info.verify_url),
);
const isZhongjianReport = computed(() => detail.value.report_header.service_provider === "zhongjian");
const imageEvidenceAttachments = computed(() =>
detail.value.evidence_attachments.filter((item) => item.file_type === "image"),
);
const otherEvidenceAttachments = computed(() =>
detail.value.evidence_attachments.filter((item) => item.file_type !== "image"),
);
const zhongjianReportImageAttachments = computed(() =>
(detail.value.zhongjian_report_files || []).filter((item) => item.file_type === "image"),
);
const zhongjianReportOtherAttachments = computed(() =>
(detail.value.zhongjian_report_files || []).filter((item) => item.file_type !== "image"),
);
function goVerify() {
if (isZhongjianReport.value) {
uni.showToast({ title: "中检报告不使用平台验真吊牌", icon: "none" });
return;
}
uni.navigateTo({ url: `/pages/verify/result?report_no=${detail.value.report_header.report_no}` });
}
function previewEvidenceImage(current: string) {
if (!imageEvidenceAttachments.value.length) return;
const urls = [...imageEvidenceAttachments.value, ...zhongjianReportImageAttachments.value].map((item) => item.file_url);
if (!urls.length) return;
uni.previewImage({
urls: imageEvidenceAttachments.value.map((item) => item.file_url),
urls,
current,
});
}
@@ -186,7 +198,7 @@ onLoad(async (options) => {
<view class="page-title" style="margin-top: 20rpx; font-size: var(--font-size-2xl); color: var(--color-heading);">
{{ detail.report_header.report_title }}
</view>
<view class="section__desc">正式结果凭证支持编号与二维码验真</view>
<view class="section__desc">{{ isZhongjianReport ? "正式结果凭证,中检报告文件可在下方查看。" : "正式结果凭证,支持编号与二维码验真。" }}</view>
<view class="certificate-header__meta">
<text class="certificate-meta-chip">报告编号 {{ detail.report_header.report_no }}</text>
<text class="certificate-meta-chip">出具日期 {{ detail.report_header.publish_time }}</text>
@@ -230,6 +242,16 @@ onLoad(async (options) => {
<text class="report-meta__label">鉴定师</text>
<text class="report-meta__value">{{ detail.appraisal_info.appraiser_name }}</text>
</view>
<template v-if="isZhongjianReport">
<view class="report-meta__row">
<text class="report-meta__label">中检报告编号</text>
<text class="report-meta__value">{{ detail.report_header.zhongjian_report_no || "-" }}</text>
</view>
<view class="report-meta__row">
<text class="report-meta__label">报告录入人</text>
<text class="report-meta__value">{{ detail.report_header.report_entry_admin_name || "-" }}</text>
</view>
</template>
</view>
<view class="section section-card">
@@ -244,7 +266,7 @@ onLoad(async (options) => {
</view>
</view>
<view class="section section-card">
<view v-if="!isZhongjianReport" class="section section-card">
<view class="section__title">报告凭证</view>
<view class="credential-box">
<view class="credential-box__qr">
@@ -261,6 +283,34 @@ onLoad(async (options) => {
</view>
</view>
<view v-if="isZhongjianReport" class="section section-card">
<view class="section__title">中检报告文件</view>
<view class="section__desc">中检订单不使用平台验真吊牌请以中检报告编号与报告文件为准</view>
<view v-if="zhongjianReportImageAttachments.length" class="task-files" style="margin-top: 20rpx;">
<view
v-for="item in zhongjianReportImageAttachments"
:key="item.file_id"
class="task-file"
@click="previewEvidenceImage(item.file_url)"
>
<image class="task-file__img" :src="item.thumbnail_url || item.file_url" mode="aspectFill" />
</view>
</view>
<view v-if="zhongjianReportOtherAttachments.length" style="margin-top: 20rpx;">
<view
v-for="(item, index) in zhongjianReportOtherAttachments"
:key="item.file_id"
class="info-list__row"
@click="openEvidenceAttachment(item)"
>
<text class="info-list__label">{{ evidenceDisplayName(item, index) }}</text>
<text class="info-list__value">{{ evidenceTypeText(item.file_type) }}</text>
</view>
</view>
</view>
<view v-if="detail.evidence_attachments.length" class="section section-card">
<view class="section__title">证据附件</view>
<view class="section__desc">以下附件为本次报告留存的证据材料可点击查看原图视频或 PDF</view>
@@ -298,7 +348,7 @@ onLoad(async (options) => {
<view class="fixed-action-bar">
<view class="btn btn--secondary" @click="downloadPdf">{{ downloading ? "下载中..." : "下载 PDF" }}</view>
<view class="btn btn--primary" @click="goVerify">去验真</view>
<view v-if="!isZhongjianReport" class="btn btn--primary" @click="goVerify">去验真</view>
</view>
</template>
</view>

View File

@@ -0,0 +1,10 @@
# Regions Data
`pca.json` stores province/city/district data for the address picker.
- Source package: `lcn@7.2.2`
- Upstream source: 2024 Ministry of Civil Affairs county-level-and-above administrative division codes
- Data scope: 34 province-level entries, 342 prefecture-level entries, 2849 county-level entries
- License: MIT, inherited from `lcn`
To update this file later, replace `pca.json` with the latest `lcn` `data/pca.json` output and rerun `npm run type-check` plus `npm run build:h5`.

File diff suppressed because one or more lines are too long

View File

@@ -457,6 +457,33 @@ picker {
font-size: var(--font-size-sm);
}
.field-box--picker {
padding: 0;
}
.region-picker {
width: 100%;
min-height: var(--input-height);
}
.region-picker__content {
min-height: var(--input-height);
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.field-box__arrow {
width: 16rpx;
height: 16rpx;
border-right: 2rpx solid var(--color-muted);
border-bottom: 2rpx solid var(--color-muted);
transform: rotate(45deg);
flex-shrink: 0;
}
.chip-list {
display: flex;
flex-wrap: wrap;

View File

@@ -1,24 +1,10 @@
const LOCAL_API_BASE_URL = "http://127.0.0.1:8787";
function isLocalLikeHostname(hostname: string) {
return (
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname === "0.0.0.0" ||
/^10\./.test(hostname) ||
/^192\.168\./.test(hostname) ||
/^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname)
);
}
const PRODUCTION_API_BASE_URL = "https://api.anxinjianyan.com";
export function resolveApiBaseUrl() {
if (import.meta.env.DEV) {
return LOCAL_API_BASE_URL;
return import.meta.env.VITE_API_BASE_URL || LOCAL_API_BASE_URL;
}
if (typeof window !== "undefined" && isLocalLikeHostname(window.location.hostname)) {
return LOCAL_API_BASE_URL;
}
return import.meta.env.VITE_API_BASE_URL || LOCAL_API_BASE_URL;
return import.meta.env.VITE_API_BASE_URL || PRODUCTION_API_BASE_URL;
}

View File

@@ -0,0 +1,106 @@
import regionSource from "../static/regions/pca.json";
export type RegionNode = {
code: string;
name: string;
children?: RegionNode[];
};
export type RegionSelection = [string, string, string];
export type RegionColumnIndex = 0 | 1 | 2;
export type RegionColumnChange = {
column: RegionColumnIndex;
value: number;
};
export type RegionPickerState = {
columns: [string[], string[], string[]];
indexes: [number, number, number];
selection: RegionSelection;
};
export const regionTree = regionSource as RegionNode[];
function getChildren(node?: RegionNode) {
return node?.children || [];
}
function clampIndex(index: number, length: number) {
if (length <= 0) {
return 0;
}
if (!Number.isFinite(index)) {
return 0;
}
return Math.min(Math.max(Math.trunc(index), 0), length - 1);
}
function names(nodes: RegionNode[]) {
return nodes.map((item) => item.name);
}
function firstAvailableSelection(province: RegionNode, city?: RegionNode, district?: RegionNode): RegionSelection {
const cityName = city?.name || province.name;
const districtName = district?.name || city?.name || province.name;
return [province.name, cityName, districtName];
}
export function findRegionIndexes(selection: Partial<Record<"province" | "city" | "district", string>>): [number, number, number] {
const provinceIndex = Math.max(
0,
regionTree.findIndex((province) => province.name === selection.province),
);
const province = regionTree[provinceIndex] || regionTree[0];
const cities = getChildren(province);
const cityIndex = Math.max(
0,
cities.findIndex((city) => city.name === selection.city),
);
const city = cities[cityIndex];
const districts = getChildren(city);
const districtIndex = Math.max(
0,
districts.findIndex((district) => district.name === selection.district),
);
return [provinceIndex, cityIndex, districtIndex];
}
export function buildRegionPickerState(indexes: [number, number, number]): RegionPickerState {
const provinceIndex = clampIndex(indexes[0], regionTree.length);
const province = regionTree[provinceIndex] || regionTree[0];
const cities = getChildren(province);
const cityIndex = clampIndex(indexes[1], cities.length);
const city = cities[cityIndex];
const districts = getChildren(city);
const districtIndex = clampIndex(indexes[2], districts.length);
const district = districts[districtIndex];
return {
columns: [
names(regionTree),
cities.length ? names(cities) : [province.name],
districts.length ? names(districts) : [city?.name || province.name],
],
indexes: [
provinceIndex,
cities.length ? cityIndex : 0,
districts.length ? districtIndex : 0,
],
selection: firstAvailableSelection(province, city, district),
};
}
export function updateRegionPickerIndexes(
indexes: [number, number, number],
change: RegionColumnChange,
): [number, number, number] {
if (change.column === 0) {
return [change.value, 0, 0];
}
if (change.column === 1) {
return [indexes[0], change.value, 0];
}
return [indexes[0], indexes[1], change.value];
}