增加了手机操作端
This commit is contained in:
@@ -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>;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
10
user-app/src/static/regions/README.md
Normal file
10
user-app/src/static/regions/README.md
Normal 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`.
|
||||
1
user-app/src/static/regions/pca.json
Normal file
1
user-app/src/static/regions/pca.json
Normal file
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
106
user-app/src/utils/regions.ts
Normal file
106
user-app/src/utils/regions.ts
Normal 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];
|
||||
}
|
||||
Reference in New Issue
Block a user