| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666 |
- <template>
- <Water></Water>
- <view class="detail-page" :style="{ minHeight: pageHeight }" v-if="!!data && !loading">
- <view id="detailPageTopAnchor"></view>
- <view class="detail-card detail-bg" :style="{ backgroundImage: 'url(' + bg + ')' }">
- <view class="detail-drug-title">{{ data.physicName }}</view>
- <view class="detail-row detail-traceability">
- <view class="detail-label">追溯码:</view>
- <view class="detail-value detail-wrap">{{ formatTraceCode(traceCode) }}</view>
- <view class="detail-copy-btn" @click="copy(traceCode)">
- <image src="../../../static/images/copy.png" mode="scaleToFill" :style="{ width: '30rpx', height: '30rpx' }" />
- </view>
- </view>
- <view class="detail-row">
- <view class="detail-label">生产企业:</view>
- <view
- class="detail-value detail-wrap detail-company-value"
- :class="{ 'detail-clamp': !companyExpanded }"
- :style="{ marginBottom: needCompanyToggle ? '40rpx' : undefined }"
- id="companyValue">
- <text>{{ data.produceEntName }}</text>
- </view>
- <text v-if="needCompanyToggle" class="detail-expand-tag" @click="toggleCompanyExpand">{{ companyExpanded ? "收起" : "展开" }}</text>
- </view>
- <view class="detail-row">
- <view class="detail-label">批准文号:</view>
- <view class="detail-value">{{ data.approveNo }}</view>
- </view>
- <view class="detail-row">
- <view class="detail-label">生产日期:</view>
- <view class="detail-value">{{ data.produceDate }}</view>
- </view>
- <view class="detail-row" v-show="cardExpanded">
- <view class="detail-label">产品批号:</view>
- <view class="detail-value">{{ data.produceBatchNo }}</view>
- </view>
- <view class="detail-row" v-show="cardExpanded">
- <view class="detail-label"> 剂<text :style="{ opacity: 0 }">剂型</text>型:</view>
- <view class="detail-value">{{ data.prepnDesc }}</view>
- </view>
- <view class="detail-row" v-show="cardExpanded">
- <view class="detail-label">包装规格:</view>
- <view class="detail-value">{{ data.prepnSpec }}</view>
- </view>
- <view class="detail-row" v-show="cardExpanded">
- <view class="detail-label">有效期至:</view>
- <view class="detail-value">{{ data.validEndDate }}</view>
- </view>
- <view class="detail-card-toggle" @click="toggleCardExpand">
- <uni-icons :type="cardExpanded ? 'up' : 'down'" size="18" color="#9aa4b2"></uni-icons>
- </view>
- </view>
- <view class="detail-timeline detail-bg" :style="{ backgroundImage: 'url(' + bg + ')' }">
- <view class="detail-section-header">
- <view class="detail-section-title">扫码跟踪</view>
- <view class="detail-section-actions">
- <view class="detail-section-action" @tap="toggleTimelineView">
- <uni-icons :type="timelineView === 'text' ? 'image' : 'list'" size="20" color="#59a8f2"></uni-icons>
- <text>{{ timelineView === "text" ? "查看图形" : "查看文字" }}</text>
- </view>
- <view class="detail-section-action" @tap="onSetDefaultView">
- <uni-icons type="gear" size="20" color="#59a8f2"></uni-icons>
- <text>设置默认</text>
- </view>
- </view>
- </view>
- <view class="detail-graph-panel" v-if="timelineView === 'graph'">
- <view class="graph-mode-row">
- <view class="graph-mode-left">
- <view class="graph-mode-title">货权流向模式</view>
- <view class="graph-mode-desc">开启后则会按照码的实际实物货权关系,将委托企业(若有)还原到收发货企业上并绘制流向图。</view>
- </view>
- <switch :checked="ownershipFlowMode" color="#2c69ff" @change="onOwnershipModeChange" />
- </view>
- <view
- :style="{
- width: graphViewWidth + 'px',
- height: graphViewHeight + 'px',
- }"
- v-if="isShowGraph">
- <l-echart ref="chartRef" :beforeDelay="180" :lStyle="`width:${graphViewWidth}px;height:${graphViewHeight}px;`" @finished="initChart"></l-echart>
- </view>
- <view
- :style="{
- width: graphViewWidth + 'px',
- height: graphViewHeight + 'px',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- }"
- v-else>
- 链路过长无法展示
- </view>
- </view>
- <view class="detail-timeline-toolbar" v-if="timelineView === 'text'">
- <view class="detail-toolbar-btn" @tap.stop="setAllTimelineDetail(!allTimelineDetail)">{{ !allTimelineDetail ? "全部展示详情" : "全部展示概要" }}</view>
- <!-- <view class="detail-toolbar-btn" @tap.stop="setAllTimelineDetail(false)">全部展示概要</view> -->
- </view>
- <view
- v-if="timelineView === 'text'"
- class="detail-timeline-item"
- :class="{ 'detail-timeline-item-first': i === 0 }"
- v-for="(value, i) in visibleBillDetailList"
- :key="i">
- <view class="detail-left">
- <view class="detail-dot" :class="{ 'detail-fill': false }"></view>
- <view class="detail-line" v-if="i < visibleBillDetailList.length - 1 || hasMoreTimelineItems"></view>
- </view>
- <view class="detail-right">
- <view class="detail-title-row">
- <view class="detail-title" :class="{ 'detail-bold': true }"> {{ value.billTypeName || "--" }} {{ value.billCode || "" }} </view>
- <view class="detail-item-toggle" @tap.stop="toggleTimelineItem(i)">
- {{ value._showDetail ? "概要" : "详情" }}
- <uni-icons :type="value._showDetail ? 'up' : 'down'" size="14" color="#888"></uni-icons>
- </view>
- </view>
- <view class="detail-info-row">
- <text class="detail-info-label">发货企业:</text>
- <text class="detail-info-value">{{ value.fromEntName || "--" }}</text>
- </view>
- <view class="detail-tag-row" v-if="value._showDetail && value.fromEntType !== '1'">
- <text class="detail-tag" v-if="value.fromCustomerNature">{{ value.fromCustomerNature }}</text>
- <text class="detail-tag" v-if="value.fromCustomerLevel">{{ value.fromCustomerLevel }}</text>
- <text class="detail-tag" v-if="value.fromResponsibleManager">{{ value.fromResponsibleManager }}</text>
- </view>
- <view class="detail-tag-row" v-if="value._showDetail">
- <text class="detail-tag" :style="getTagColorStyle(value.fromEntTypeName)" v-if="value.fromEntTypeName">{{ value.fromEntTypeName }}</text>
- <text class="detail-tag detail-tag-region" v-if="getRegionByPrefix(value, 'from')">{{ getRegionByPrefix(value, "from") }}</text>
- </view>
- <view class="detail-info-row">
- <text class="detail-info-label">收货企业:</text>
- <text class="detail-info-value">{{ value.toEntName || "--" }}</text>
- </view>
- <view class="detail-tag-row" v-if="value._showDetail && value.toEntType !== '1'">
- <text class="detail-tag" v-if="value.toCustomerNature">{{ value.toCustomerNature }}</text>
- <text class="detail-tag" v-if="value.toCustomerLevel">{{ value.toCustomerLevel }}</text>
- <text class="detail-tag" v-if="value.toResponsibleManager">{{ value.toResponsibleManager }}</text>
- </view>
- <view class="detail-tag-row" v-if="value._showDetail">
- <text class="detail-tag" :style="getTagColorStyle(value.toEntTypeName)" v-if="value.toEntTypeName">{{ value.toEntTypeName }}</text>
- <text class="detail-tag detail-tag-region" v-if="getRegionByPrefix(value, 'to')">{{ getRegionByPrefix(value, "to") }}</text>
- </view>
- <view class="detail-info-row">
- <text class="detail-info-label">入出库时间:</text>
- <text class="detail-info-value">{{ value.billTime || "--" }}</text>
- </view>
- <view class="detail-info-row" v-if="value._showDetail && value.uploadEntName">
- <text class="detail-info-label">上传企业:</text>
- <text class="detail-info-value">{{ value.uploadEntName }}</text>
- </view>
- <view class="detail-tag-row" v-if="value._showDetail && value.uploadEntName && value.uploadEntType !== '1'">
- <text class="detail-tag" v-if="value.uploadCustomerNature">{{ value.uploadCustomerNature }}</text>
- <text class="detail-tag" v-if="value.uploadCustomerLevel">{{ value.uploadCustomerLevel }}</text>
- <text class="detail-tag" v-if="value.uploadResponsibleManager">{{ value.uploadResponsibleManager }}</text>
- </view>
- <view class="detail-tag-row" v-if="value._showDetail && value.uploadEntName">
- <text class="detail-tag" :style="getTagColorStyle(value.uploadEntTypeName)" v-if="value.uploadEntTypeName">{{ value.uploadEntTypeName }}</text>
- <text class="detail-tag detail-tag-region" v-if="getRegionByPrefix(value, 'upload')">{{ getRegionByPrefix(value, "upload") }}</text>
- </view>
- <view class="detail-info-row" v-if="value._showDetail && value.uploadEntName">
- <text class="detail-info-label">上传时间:</text>
- <text class="detail-info-value">{{ value.uploadTime || "--" }}</text>
- </view>
- <view class="detail-info-row" v-if="value._showDetail && value.disEntName">
- <text class="detail-info-label">配送企业:</text>
- <text class="detail-info-value">{{ value.disEntName }}</text>
- </view>
- <view class="detail-tag-row" v-if="value._showDetail && value.disEntName && !value.disEntName.includes(',') && value.disEntType !== '1'">
- <text class="detail-tag" v-if="value.disCustomerNature">{{ value.disCustomerNature }}</text>
- <text class="detail-tag" v-if="value.disCustomerLevel">{{ value.disCustomerLevel }}</text>
- <text class="detail-tag" v-if="value.disResponsibleManager">{{ value.disResponsibleManager }}</text>
- </view>
- <view class="detail-tag-row" v-if="value._showDetail && value.disEntName && !value.disEntName.includes(',')">
- <text class="detail-tag" :style="getTagColorStyle(value.disEntTypeName)" v-if="value.disEntTypeName">{{ value.disEntTypeName }}</text>
- <text class="detail-tag detail-tag-region" v-if="getRegionByPrefix(value, 'dis')">{{ getRegionByPrefix(value, "dis") }}</text>
- </view>
- <view class="detail-info-row" v-if="value._showDetail && value.assEntName">
- <text class="detail-info-label">委托企业:</text>
- <text class="detail-info-value">{{ value.assEntName }}</text>
- </view>
- <view class="detail-tag-row" v-if="value._showDetail && value.assEntName && value.assEntType !== '1'">
- <text class="detail-tag" v-if="value.assCustomerNature">{{ value.assCustomerNature }}</text>
- <text class="detail-tag" v-if="value.assCustomerLevel">{{ value.assCustomerLevel }}</text>
- <text class="detail-tag" v-if="value.assResponsibleManager">{{ value.assResponsibleManager }}</text>
- </view>
- <view class="detail-tag-row" v-if="value._showDetail && value.assEntName">
- <text class="detail-tag" :style="getTagColorStyle(value.assEntTypeName)" v-if="value.assEntTypeName">{{ value.assEntTypeName }}</text>
- <text class="detail-tag detail-tag-region" v-if="getRegionByPrefix(value, 'ass')">{{ getRegionByPrefix(value, "ass") }}</text>
- </view>
- </view>
- </view>
- <view v-if="timelineView === 'text' && hasMoreTimelineItems" class="detail-timeline-loadmore">
- 上拉加载更多({{ visibleBillDetailList.length }}/{{ timelineTotalCount }})
- </view>
- </view>
- </view>
- <transition name="back-top-fade">
- <view v-if="!!data && !loading && showBackTopButton" class="detail-back-top-btn" @tap="backToTop">
- <view class="detail-back-top-arrow"></view>
- </view>
- </transition>
- <view class="detail-page-empty" :style="{ height: pageHeight }" v-if="!data && !loading">
- <image src="../../../static/images/empty.png" mode="scaleToFill" :style="{ width: '264rpx', height: '200rpx' }" />
- <text
- :style="{
- fontSize: '24rpx',
- color: '#99a1ad',
- marginTop: '60rpx',
- textAlign: 'center',
- }"
- >所查询药品所在生产企业尚未纳入扫码范围,后续将进一步扩大,敬请期待</text
- >
- </view>
- <view class="detail-page-empty" :style="{ height: pageHeight }" v-if="loading">
- <image src="../../../static/images/loading.png" mode="scaleToFill" :style="{ width: '50rpx', height: '50rpx' }" class="detail-loading" />
- </view>
- </template>
- <script>
- import bg from "../../../static/images/bg.png";
- import { formatDate } from "../../../utils/utils.js";
- import request from "../../../request/index.js";
- import Water from "@/components/water/water.vue";
- import lEchart from "../../../uni_modules/lime-echart/components/l-echart/l-echart.vue";
- import { markRaw } from "vue";
- const echarts = require("../../../static/js/echarts.min.js");
- export default {
- components: {
- Water,
- lEchart,
- },
- data() {
- return {
- data: null,
- loading: true,
- statusBarHeight: 20,
- traceCode: "",
- pageHeight: 0,
- bg,
- companyExpanded: true,
- needCompanyToggle: false,
- cardExpanded: false,
- allTimelineDetail: false,
- timelineView: "text",
- defaultTimelineViewKey: "traceabilityCodeQuery.detail.timelineDefaultView",
- imageData: [],
- graphViewWidth: 300,
- graphViewHeight: 480,
- chartInstance: null,
- chartOption: {},
- ownershipFlowMode: false,
- timelinePageSize: 20,
- timelineVisibleCount: 20,
- scrollTopValue: 0,
- scrollIntoViewId: "",
- backTopThreshold: 0,
- showBackTopButton: false,
- backTopHideTimer: null,
- };
- },
- onLoad(options) {
- const info = uni.getSystemInfoSync();
- this.statusBarHeight = info.statusBarHeight || 20;
- this.pageHeight = (info.windowHeight && info.windowHeight + "px") || "100%";
- this.backTopThreshold = info.windowHeight || 667;
- this.graphViewWidth = Math.max((info.windowWidth || 375) - 36, 280);
- this.graphViewHeight = Math.max((info.windowHeight || 667) - 200, 420);
- if (options.traceCode) {
- this.traceCode = options.traceCode;
- }
- const defaultView = uni.getStorageSync(this.defaultTimelineViewKey);
- if (defaultView === "graph" || defaultView === "text") {
- this.timelineView = defaultView;
- }
- // this.getData(options.traceCode);
- this.getData2(options.traceCode);
- },
- onReady() {
- this.$nextTick(() => {
- const info = uni.getSystemInfoSync();
- const lineHeightPx = (info.windowWidth / 750) * 40;
- this.companyExpanded = true; // 展开测量高度
- uni
- .createSelectorQuery()
- .in(this)
- .select("#companyValue")
- .boundingClientRect((rect) => {
- if (!rect || !lineHeightPx) return;
- const lines = Math.round(rect.height / lineHeightPx);
- this.needCompanyToggle = lines > 2;
- this.companyExpanded = !this.needCompanyToggle;
- })
- .exec();
- });
- },
- onReachBottom() {
- if (this.timelineView !== "text") return;
- this.loadMoreTimelineItems();
- },
- onPageScroll(e) {
- this.handlePageScroll(e);
- },
- // 页面卸载时销毁图表实例
- beforeUnmount() {
- if (this.backTopHideTimer) {
- clearTimeout(this.backTopHideTimer);
- this.backTopHideTimer = null;
- }
- if (this.$refs.chartRef) {
- this.$refs.chartRef.dispose();
- }
- },
- computed: {
- isShowGraph() {
- return this.data.billDetailList.length < 30;
- },
- timelineTotalCount() {
- if (!this.data || !Array.isArray(this.data.billDetailList)) return 0;
- return this.data.billDetailList.length;
- },
- visibleBillDetailList() {
- if (!this.data || !Array.isArray(this.data.billDetailList)) return [];
- const maxCount = Math.min(this.timelineVisibleCount, this.timelineTotalCount);
- return this.data.billDetailList.slice(0, maxCount);
- },
- hasMoreTimelineItems() {
- return this.visibleBillDetailList.length < this.timelineTotalCount;
- },
- },
- methods: {
- getTagColorStyle(tag) {
- let color = "#57bfe1";
- if (tag.includes("生产")) color = "#f5c269";
- if (tag.includes("零售")) color = "#91c76d";
- return {
- color,
- borderColor: color + "80",
- };
- },
- // 判断 imageData 中是否存在一条 source -> target 的有向连接线
- hasDirectedEdge(sourceName, targetName, list = this.imageData) {
- const sourceNode = (list || []).find((n) => n && n.name === sourceName);
- if (!sourceNode || !Array.isArray(sourceNode.to)) return false;
- return sourceNode.to.some((t) => t && t.name === targetName);
- },
- // 1) 当前节点与相邻节点是否被同一节点指向,返回数量与当前位置(1-based)
- getInboundAdjacentGroupInfo(currentIndex, list = this.imageData) {
- const nodes = Array.isArray(list) ? list : [];
- if (currentIndex < 0 || currentIndex >= nodes.length) return null;
- const currentName = nodes[currentIndex]?.name;
- if (!currentName) return null;
- const sourceNames = nodes
- .filter((node) => Array.isArray(node?.to) && node.to.some((toItem) => toItem && toItem.name === currentName))
- .map((node) => node.name);
- let best = null;
- sourceNames.forEach((sourceName) => {
- let start = currentIndex;
- let end = currentIndex;
- while (start - 1 >= 0 && this.hasDirectedEdge(sourceName, nodes[start - 1]?.name, nodes)) {
- start--;
- }
- while (end + 1 < nodes.length && this.hasDirectedEdge(sourceName, nodes[end + 1]?.name, nodes)) {
- end++;
- }
- const count = end - start + 1;
- const position = currentIndex - start + 1;
- if (!best || count > best.count) {
- best = { sourceName, count, position, start, end };
- }
- });
- return (
- best || {
- sourceName: null,
- count: 1,
- position: 1,
- start: currentIndex,
- end: currentIndex,
- }
- );
- },
- // 2) 当前节点与相邻节点是否共同指向同一节点,返回数量与当前位置(1-based)
- getOutboundAdjacentGroupInfo(currentIndex, list = this.imageData) {
- const nodes = Array.isArray(list) ? list : [];
- if (currentIndex < 0 || currentIndex >= nodes.length) return null;
- const currentNode = nodes[currentIndex] || {};
- const targetNames = Array.isArray(currentNode.to) ? currentNode.to.map((t) => t && t.name).filter(Boolean) : [];
- let best = null;
- targetNames.forEach((targetName) => {
- let start = currentIndex;
- let end = currentIndex;
- while (start - 1 >= 0 && this.hasDirectedEdge(nodes[start - 1]?.name, targetName, nodes)) {
- start--;
- }
- while (end + 1 < nodes.length && this.hasDirectedEdge(nodes[end + 1]?.name, targetName, nodes)) {
- end++;
- }
- const count = end - start + 1;
- const position = currentIndex - start + 1;
- if (!best || count > best.count) {
- best = { targetName, count, position, start, end };
- }
- });
- return (
- best || {
- targetName: null,
- count: 1,
- position: 1,
- start: currentIndex,
- end: currentIndex,
- }
- );
- },
- // 3) 当前节点与目标节点之间是否只有一条连接线(双向算多条)
- // 3) 判断当前节点与目标节点之间是否只有一条连接线
- // 说明:当前节点可能有多条连线指向目标节点(to 中出现多次),目标节点也可能反向指向当前节点,均计入总数
- getConnectionLineInfo(currentName, targetName, list = this.imageData) {
- const nodes = Array.isArray(list) ? list : [];
- const countEdges = (fromName, toName) => {
- const fromNode = nodes.find((n) => n && n.name === fromName);
- if (!fromNode || !Array.isArray(fromNode.to)) return 0;
- return fromNode.to.filter((t) => t && t.name === toName).length;
- };
- const forward = countEdges(currentName, targetName);
- const backward = countEdges(targetName, currentName);
- const edgeCount = forward + backward;
- return {
- edgeCount,
- isSingleLine: edgeCount === 1,
- forwardCount: forward,
- backwardCount: backward,
- hasForward: forward > 0,
- hasBackward: backward > 0,
- };
- },
- // 4) 当前节点与上一个/下一个节点之间是否存在连接线(任一方向都算)
- getAdjacentConnectionInfo(currentIndex, list = this.imageData) {
- const nodes = Array.isArray(list) ? list : [];
- if (currentIndex < 0 || currentIndex >= nodes.length) return null;
- const currentName = nodes[currentIndex]?.name;
- const prevName = nodes[currentIndex - 1]?.name || null;
- const nextName = nodes[currentIndex + 1]?.name || null;
- const hasPrevConnection = prevName ? this.getConnectionLineInfo(currentName, prevName, nodes).edgeCount > 0 : false;
- const hasNextConnection = nextName ? this.getConnectionLineInfo(currentName, nextName, nodes).edgeCount > 0 : false;
- return { hasPrevConnection, hasNextConnection, prevName, nextName };
- },
- buildGraphLayout(nodes = []) {
- const chartWidth = Number(this.graphViewWidth) || 320;
- const nodeSize = 86;
- const sidePadding = nodeSize / 2 + 10;
- const centerX = chartWidth / 2;
- const hGap = Math.max(140, Math.min(210, chartWidth - sidePadding * 2));
- const vGap = 150;
- const componentGap = 15;
- const edgeRecords = [];
- const nameIndexMap = {};
- nodes.forEach((node, index) => {
- if (node && node.name) nameIndexMap[node.name] = index;
- });
- nodes.forEach((node, sourceIndex) => {
- const tos = Array.isArray(node && node.to) ? node.to : [];
- tos.forEach((to, targetIndex) => {
- const targetNodeIndex = nameIndexMap[to && to.name];
- if (!to || !to.name || to.name === node.name || typeof targetNodeIndex !== "number") return;
- edgeRecords.push({
- sourceIndex,
- targetIndex,
- targetNodeIndex,
- sourceName: node.name,
- targetName: to.name,
- uniqId: `${sourceIndex}_${targetIndex}`,
- });
- });
- });
- const parent = nodes.map((_, index) => index);
- const find = (index) => {
- if (parent[index] !== index) parent[index] = find(parent[index]);
- return parent[index];
- };
- const union = (a, b) => {
- const rootA = find(a);
- const rootB = find(b);
- if (rootA !== rootB) parent[rootB] = rootA;
- };
- edgeRecords.forEach((edge) => union(edge.sourceIndex, edge.targetNodeIndex));
- const componentMap = {};
- nodes.forEach((_, index) => {
- const root = find(index);
- if (!componentMap[root]) componentMap[root] = [];
- componentMap[root].push(index);
- });
- const components = Object.values(componentMap).sort((a, b) => Math.min(...a) - Math.min(...b));
- const positions = new Array(nodes.length);
- let baseY = 100;
- components.forEach((component) => {
- const componentSet = new Set(component);
- const layers = {};
- component.forEach((index) => {
- layers[index] = 0;
- });
- for (let i = 0; i < component.length; i++) {
- let changed = false;
- edgeRecords.forEach((edge) => {
- if (!componentSet.has(edge.sourceIndex) || !componentSet.has(edge.targetNodeIndex)) return;
- const nextLayer = layers[edge.sourceIndex] + 1;
- if (nextLayer > layers[edge.targetNodeIndex]) {
- layers[edge.targetNodeIndex] = nextLayer;
- changed = true;
- }
- });
- if (!changed) break;
- }
- const grouped = {};
- component.forEach((index) => {
- const layer = layers[index] || 0;
- if (!grouped[layer]) grouped[layer] = [];
- grouped[layer].push(index);
- });
- const layerKeys = Object.keys(grouped)
- .map(Number)
- .sort((a, b) => a - b);
- layerKeys.forEach((layer) => {
- const group = grouped[layer].sort((a, b) => a - b);
- group.forEach((nodeIndex, positionIndex) => {
- positions[nodeIndex] = {
- x: centerX + (positionIndex - (group.length - 1) / 2) * hGap,
- y: baseY + layer * vGap,
- };
- });
- });
- const maxLayer = layerKeys.length ? Math.max(...layerKeys) : 0;
- baseY += (maxLayer + 1) * vGap + componentGap;
- });
- const pairMembers = {};
- edgeRecords.forEach((edge) => {
- const pairKey = [edge.sourceName, edge.targetName].sort().join("__");
- if (!pairMembers[pairKey]) pairMembers[pairKey] = [];
- pairMembers[pairKey].push(edge);
- });
- const curvenessMap = {};
- const edgeMetaMap = {};
- Object.keys(pairMembers).forEach((pairKey) => {
- const list = pairMembers[pairKey];
- const step = this.ownershipFlowMode ? (list.length === 2 ? 0.86 : 0.68) : list.length === 2 ? 0.72 : 0.56;
- list.forEach((edge, index) => {
- curvenessMap[edge.uniqId] = list.length > 1 ? (index - (list.length - 1) / 2) * step : 0;
- edgeMetaMap[edge.uniqId] = {
- count: list.length,
- index,
- offset: index - (list.length - 1) / 2,
- };
- });
- });
- return { positions, curvenessMap, edgeMetaMap, height: Math.max(520, baseY - componentGap + 70) };
- },
- // 初始化图表
- async initChart() {
- if (!this.$refs.chartRef || !this.isShowGraph || this.timelineView !== "graph") return;
- if (this._initChartLock) return;
- this._initChartLock = true;
- try {
- await this._doInitChart();
- } finally {
- this._initChartLock = false;
- }
- },
- async _doInitChart() {
- let option = {};
- const data = [];
- const links = [];
- const lineWidth = 2;
- const _nodes = this.imageData || [];
- const { positions: _positions, curvenessMap: _curvenessMap, edgeMetaMap: _edgeMetaMap } = this.buildGraphLayout(_nodes);
- const nameIndexMap = {};
- _nodes.forEach((node, nodeIndex) => {
- if (node && node.name) nameIndexMap[node.name] = nodeIndex;
- });
- this.imageData.forEach((item, index) => {
- const { name, to } = item;
- const toLen = to.length;
- const { x, y } = _positions[index] || { x: 350, y: index * 250 + 100 };
- const color = name.includes("生产") ? "#fde58f" : name.includes("零售") ? "#b5ea8e" : name.includes("消费者") ? "#e0d3fe" : "#a3e6ff";
- data.push({
- name,
- x,
- y,
- itemStyle: {
- color,
- },
- });
- to.forEach((toItem, toIndex) => {
- const { name: toName, text: toText } = toItem;
- if (name === toName) {
- const loopLabelName = `${name}__self_loop_${toIndex}`;
- data.push({
- name: loopLabelName,
- x: x + 88,
- y,
- itemStyle: {
- color: "rgba(0,0,0,0)",
- },
- label: {
- show: true,
- color: "#c6c6c6",
- formatter: toText,
- },
- tooltip: {
- show: false,
- },
- });
- links.push(
- {
- source: name,
- target: loopLabelName,
- label: {
- show: false,
- },
- lineStyle: {
- color: "#c6c6c6",
- width: lineWidth,
- curveness: 0.34,
- },
- },
- {
- source: loopLabelName,
- target: name,
- label: {
- show: false,
- },
- lineStyle: {
- color: "#c6c6c6",
- width: lineWidth,
- curveness: 0.34,
- },
- },
- );
- } else {
- const isOut = toText.includes("出库");
- const isReturn = toText.includes("退货");
- // 文案包含"退货"时整条线统一橙色;其余按出/入库区分蓝/灰
- const linkColor = isReturn ? "#ff9500" : isOut ? "#80a6fd" : "#aaaaaa";
- // 从预计算的曲率表中取值:同一对节点间多条线已均匀分配,单条线为 0(直线)
- const assignedCurveness = _curvenessMap[`${index}_${toIndex}`] || 0;
- const edgeMeta = _edgeMetaMap[`${index}_${toIndex}`] || { count: 1, offset: 0 };
- const targetPosition = _positions[nameIndexMap[toName]] || { x, y: y + 120 };
- const midX = (x + targetPosition.x) / 2;
- const midY = (y + targetPosition.y) / 2;
- const dx = targetPosition.x - x;
- const dy = targetPosition.y - y;
- const length = Math.sqrt(dx * dx + dy * dy) || 1;
- const perpX = -dy / length;
- const perpY = dx / length;
- const labelName = `${name}__edge_label_${toIndex}`;
- const labelText = toText.split(/\s+/).filter(Boolean).join("\n");
- const stagger = edgeMeta.count > 1 ? edgeMeta.offset : 0;
- const labelGap = this.ownershipFlowMode ? 68 : 54;
- const labelPerpGap = this.ownershipFlowMode ? 32 : 24;
- data.push({
- name: labelName,
- x: midX + perpX * stagger * labelGap,
- y: midY + stagger * labelGap + perpY * stagger * labelPerpGap,
- symbolSize: 1,
- itemStyle: {
- color: "rgba(0,0,0,0)",
- },
- label: {
- show: true,
- formatter: () => labelText,
- color: linkColor,
- fontSize: 12,
- lineHeight: 14,
- width: 120,
- overflow: "breakAll",
- },
- tooltip: {
- show: false,
- },
- });
- links.push({
- source: name,
- target: toName,
- label: {
- show: false,
- },
- lineStyle: {
- color: linkColor,
- width: lineWidth,
- curveness: assignedCurveness,
- },
- });
- }
- });
- });
- // console.log("this.imageData", this.imageData);
- // console.log("data", data);
- // console.log("links", links);
- option = {
- tooltip: {},
- animationDurationUpdate: 1500,
- animationEasingUpdate: "quinticInOut",
- series: [
- {
- type: "graph",
- layout: "none",
- symbolSize: 86,
- roam: true,
- label: {
- show: true,
- fontSize: 8,
- lineHeight: 10,
- formatter: (params) => {
- const text = String((params && params.name) || "");
- if (!text) return "";
- return text.split(/\s+/).filter(Boolean).join("\n");
- },
- },
- edgeSymbol: ["circle", "arrow"],
- edgeSymbolSize: [4, 10],
- data,
- links,
- lineStyle: {
- opacity: 0.9,
- width: 2,
- curveness: 0,
- },
- },
- ],
- };
- try {
- await this.$nextTick();
- // if (this.chartInstance) {
- // this.chartInstance.setOption(option, true);
- // return;
- // }
- this.chartInstance = markRaw(await this.$refs.chartRef.init(echarts));
- this.chartInstance.setOption(option, true);
- } catch (error) {
- console.error("图表初始化失败:", error);
- }
- },
- // 更新图表数据
- updateChart(newOption) {
- if (this.chartInstance) {
- this.chartInstance.setOption(newOption);
- } else if (this.$refs.chartRef) {
- this.$refs.chartRef.setOption(newOption);
- }
- },
- // 调整图表大小
- resizeChart() {
- if (this.$refs.chartRef) {
- this.$refs.chartRef.resize();
- }
- },
- formatDate(date) {
- return formatDate({ date }, "YYYY-MM-DD");
- },
- getRegion(value = {}) {
- const { provinceDesc, cityDesc, areaDesc } = value;
- return [provinceDesc, cityDesc, areaDesc].filter(Boolean).join(",") || "";
- },
- getRegionByPrefix(value = {}, prefix = "from") {
- const province = value[`${prefix}ProvinceDesc`];
- const city = value[`${prefix}CityDesc`];
- const area = value[`${prefix}AreaDesc`];
- return [province, city, area].filter(Boolean).join("-") || "";
- },
- initTimelineVisibleCount() {
- this.timelineVisibleCount = this.timelinePageSize;
- },
- handlePageScroll(e) {
- const top = Number((e && (e.scrollTop || (e.detail && e.detail.scrollTop))) || 0);
- if (top < this.backTopThreshold) {
- this.showBackTopButton = false;
- if (this.backTopHideTimer) {
- clearTimeout(this.backTopHideTimer);
- this.backTopHideTimer = null;
- }
- return;
- }
- this.showBackTopButton = true;
- if (this.backTopHideTimer) {
- clearTimeout(this.backTopHideTimer);
- }
- this.backTopHideTimer = setTimeout(() => {
- this.showBackTopButton = false;
- this.backTopHideTimer = null;
- }, 3000);
- },
- backToTop() {
- this.scrollTopValue = 0;
- this.scrollIntoViewId = "detailPageTopAnchor";
- this.showBackTopButton = false;
- if (this.backTopHideTimer) {
- clearTimeout(this.backTopHideTimer);
- this.backTopHideTimer = null;
- }
- this.$nextTick(() => {
- this.scrollIntoViewId = "";
- });
- uni.pageScrollTo({
- scrollTop: 0,
- duration: 300,
- });
- },
- onTimelineScrollToLower() {
- if (this.timelineView !== "text") return;
- this.loadMoreTimelineItems();
- },
- loadMoreTimelineItems() {
- if (!this.hasMoreTimelineItems) return;
- this.timelineVisibleCount = Math.min(this.timelineVisibleCount + this.timelinePageSize, this.timelineTotalCount);
- },
- toggleTimelineItem(index) {
- if (!this.data || !Array.isArray(this.data.billDetailList)) return;
- const item = this.data.billDetailList[index];
- if (!item) return;
- this.$set(item, "_showDetail", !item._showDetail);
- },
- setAllTimelineDetail(showDetail) {
- this.allTimelineDetail = !!showDetail;
- if (!this.data || !Array.isArray(this.data.billDetailList)) return;
- this.data.billDetailList.forEach((item) => {
- if (item) this.$set(item, "_showDetail", !!showDetail);
- });
- },
- toggleTimelineView() {
- this.timelineView = this.timelineView === "text" ? "graph" : "text";
- if (this.timelineView === "text") {
- this.initTimelineVisibleCount();
- }
- if (this.timelineView === "graph" && this.data && Array.isArray(this.data.billDetailList)) {
- this.imageData = this.ownershipFlowMode ? this.buildImageData2(this.data.billDetailList) : this.buildImageData(this.data.billDetailList);
- this.updateGraphCanvasSize();
- this.$nextTick(() => this.initChart());
- }
- },
- onOwnershipModeChange(e) {
- this.ownershipFlowMode = !!(e && e.detail && e.detail.value);
- if (!this.data || !Array.isArray(this.data.billDetailList)) return;
- this.imageData = this.ownershipFlowMode ? this.buildImageData2(this.data.billDetailList) : this.buildImageData(this.data.billDetailList);
- this.updateGraphCanvasSize();
- if (this.timelineView === "graph") {
- this.$nextTick(() => this.initChart());
- }
- },
- onSetDefaultView() {
- uni.setStorageSync(this.defaultTimelineViewKey, this.timelineView);
- uni.showToast({
- title: "已设为默认",
- icon: "none",
- });
- },
- buildImageData2(data) {
- if (!data || !Array.isArray(data)) return [];
- let list = [];
- const buildTime = (time) => {
- return time.split(" ")[0];
- };
- const buildTo = (to, item) => {
- const hasAss = !!item.assEntName;
- const isWarehousing = item.billTypeName.includes("入库");
- const hasType = item.billTypeName && item.billTypeName !== "****";
- if (item.billTypeName === "使用出库") {
- to.push({
- name: "消费者",
- text: !hasType ? "****" : buildTime(item.billTime) + " " + item.billTypeName,
- });
- } else if (item.toEntName) {
- const address = [item.toProvinceDesc, item.toCityDesc, item.toAreaDesc].filter(Boolean).join(",");
- const assAddress = [item.assProvinceDesc, item.assCityDesc, item.assAreaDesc].filter(Boolean).join(",");
- const name = item.toEntTypeName + " " + item.toEntName + " " + address;
- const assName = item.assEntTypeName + " " + item.assEntName + " " + assAddress;
- to.push({
- name: hasAss && isWarehousing ? assName : name,
- text: !hasType ? "****" : buildTime(item.billTime) + " " + item.billTypeName,
- });
- if (hasAss && hasType) {
- to[to.length - 1].text += " 被委托:" + (isWarehousing ? item.toEntName : item.fromEntName);
- }
- }
- return to;
- };
- data.forEach((item, i) => {
- const hasAss = !!item.assEntName;
- const isWarehousing = item.billTypeName.includes("入库");
- const index = list.findIndex((i) => i.name.includes(hasAss && !isWarehousing ? item.assEntName : item.fromEntName));
- let to = [];
- if (index !== -1) {
- to = list[index].to;
- }
- to = buildTo(to, item);
- const address = [item.fromProvinceDesc, item.fromCityDesc, item.fromAreaDesc].filter(Boolean).join(",");
- const assAddress = [item.assProvinceDesc, item.assCityDesc, item.assAreaDesc].filter(Boolean).join(",");
- const name = item.fromEntTypeName + " " + item.fromEntName + " " + address;
- const assName = item.assEntTypeName + " " + item.assEntName + " " + assAddress;
- if (index === -1) {
- list.push({
- num: i,
- name: hasAss && !isWarehousing ? assName : name,
- to,
- });
- } else {
- list[index].to = to;
- }
- if (item.toEntName && list.findIndex((i) => i.name.includes(item.toEntName)) === -1) {
- const address = [item.toProvinceDesc, item.toCityDesc, item.toAreaDesc].filter(Boolean).join(",");
- list.push({
- num: i + 9999,
- name: item.toEntTypeName + " " + item.toEntName + " " + address,
- to: [],
- });
- }
- });
- list = list.filter((i) => Array.isArray(i.to) && i.to.length !== 0);
- const lastItem = list[list.length - 1];
- if (lastItem?.to?.[0]?.name === "消费者") {
- list.push({
- num: 99999,
- name: "消费者",
- to: [],
- });
- }
- list.sort((a, b) => a.num - b.num);
- return list;
- },
- buildImageData(data) {
- if (!data || !Array.isArray(data)) return [];
- const list = [];
- const buildTime = (time) => {
- return time.split(" ")[0];
- };
- const buildTo = (to, item) => {
- const hasType = item.billTypeName && item.billTypeName !== "****";
- if (item.billTypeName === "使用出库") {
- to.push({
- name: "消费者",
- text: !hasType ? "****" : buildTime(item.billTime) + " " + item.billTypeName,
- });
- } else if (item.toEntName) {
- const address = [item.toProvinceDesc, item.toCityDesc, item.toAreaDesc].filter(Boolean).join(",");
- to.push({
- name: item.toEntTypeName + " " + item.toEntName + " " + address,
- text: !hasType ? "****" : buildTime(item.billTime) + " " + item.billTypeName,
- });
- if (item.assEntName && hasType) {
- to[to.length - 1].text += " 委托:" + item.assEntName;
- }
- }
- return to;
- };
- data.forEach((item, i) => {
- const index = list.findIndex((i) => i.name.includes(item.fromEntName));
- let to = [];
- if (index !== -1) {
- to = list[index].to;
- }
- to = buildTo(to, item);
- if (index === -1) {
- const address = [item.fromProvinceDesc, item.fromCityDesc, item.fromAreaDesc].filter(Boolean).join(",");
- list.push({
- num: i,
- name: item.fromEntTypeName + " " + item.fromEntName + " " + address,
- to,
- });
- } else {
- list[index].to = to;
- }
- if (item.toEntName && list.findIndex((i) => i.name.includes(item.toEntName)) === -1) {
- const address = [item.toProvinceDesc, item.toCityDesc, item.toAreaDesc].filter(Boolean).join(",");
- list.push({
- num: i + 9999,
- name: item.toEntTypeName + " " + item.toEntName + " " + address,
- to: [],
- });
- }
- });
- const lastItem = list[list.length - 1];
- if (lastItem?.to?.[0]?.name === "消费者") {
- list.push({
- num: 99999,
- name: "消费者",
- to: [],
- });
- }
- list.sort((a, b) => a.num - b.num);
- return list;
- },
- updateGraphCanvasSize() {
- if (!this.isShowGraph) {
- this.graphViewHeight = 50;
- return;
- }
- const list = this.imageData || [];
- this.graphViewHeight = this.buildGraphLayout(list).height;
- },
- getData2(traceCode) {
- request("/bills/scanCode2", {
- tracCode: traceCode,
- path: "/traceabilityCodeQuery/pages/detail/index.vue",
- }).then((res) => {
- if (res.code == 200) {
- const _data = res.data;
- this.data = _data || null;
- if (this.data && Array.isArray(this.data.billDetailList)) {
- this.data.billDetailList = this.data.billDetailList.map((item) => ({
- ...item,
- _showDetail: !!this.allTimelineDetail,
- }));
- this.initTimelineVisibleCount();
- if (this.isShowGraph) {
- this.imageData = this.ownershipFlowMode ? this.buildImageData2(this.data.billDetailList) : this.buildImageData(this.data.billDetailList);
- }
- this.updateGraphCanvasSize();
- this.initChart();
- }
- if (typeof _data === "object" && Object.values(_data).filter((i) => !!i).length == 0) {
- this.data = null;
- }
- this.loading = false;
- } else {
- uni.showToast({ title: "查询失败", icon: "none" });
- }
- });
- },
- getData(traceCode) {
- request("/bills/scanCode", {
- tracCode: traceCode,
- path: "/traceabilityCodeQuery/pages/detail/index.vue",
- }).then((res) => {
- if (res.code == 200) {
- // const _data = res.data;
- // this.data = _data || null;
- // const defaultData = [
- // {
- // billTypeName: "生产入库",
- // billCode: "SYS_IN_2025041410194699357",
- // fromEntId: "00000000000000959808",
- // fromEntName: "惠州市九惠制药股份有限公司",
- // fromEntType: "1",
- // fromEntTypeName: "生产企业",
- // fromProvinceDesc: "广东省",
- // fromCityDesc: "惠州市",
- // fromAreaDesc: "惠城区",
- // toEntId: "00000000000000959808",
- // toEntName: "惠州市九惠制药股份有限公司",
- // toEntType: "1",
- // toEntTypeName: "生产企业",
- // toProvinceDesc: "广东省",
- // toCityDesc: "惠州市",
- // toAreaDesc: "惠城区",
- // billTime: "2025-04-12 00:00:00",
- // uploadEntId: "00000000000000959808",
- // uploadEntName: "惠州市九惠制药股份有限公司",
- // uploadEntType: "1",
- // uploadEntTypeName: "生产企业",
- // uploadProvinceDesc: "广东省",
- // uploadCityDesc: "惠州市",
- // uploadAreaDesc: "惠城区",
- // uploadTime: "2025-04-14 10:19:47",
- // },
- // {
- // billTypeName: "销售出库",
- // billCode: "20250414093811017995",
- // fromEntId: "00000000000000959808",
- // fromEntName: "惠州市九惠制药股份有限公司",
- // fromEntType: "1",
- // fromEntTypeName: "生产企业",
- // fromProvinceDesc: "广东省",
- // fromCityDesc: "惠州市",
- // fromAreaDesc: "惠城区",
- // toEntId: "00000000000012618117",
- // toEntName: "深圳华润三九医药贸易有限公司",
- // toEntType: "2",
- // toEntTypeName: "批发企业",
- // toProvinceDesc: "广东省",
- // toCityDesc: "深圳市",
- // toAreaDesc: "龙华区",
- // billTime: "2025-04-14 09:38:11",
- // uploadEntId: "00000000000000592642",
- // uploadEntName: "华润三九医药股份有限公司",
- // uploadEntType: "1",
- // uploadEntTypeName: "生产企业",
- // uploadProvinceDesc: "广东省",
- // uploadCityDesc: "深圳市",
- // uploadAreaDesc: "龙华区",
- // uploadTime: "2025-10-31 17:29:36",
- // },
- // {
- // billTypeName: "销售出库",
- // billCode: "2504032G-2",
- // fromEntId: "00000000000012618117",
- // fromEntName: "深圳华润三九医药贸易有限公司",
- // fromEntType: "2",
- // fromEntTypeName: "批发企业",
- // fromProvinceDesc: "广东省",
- // fromCityDesc: "深圳市",
- // fromAreaDesc: "龙华区",
- // toEntId: "8b63aa19a9a94cb6aa472ee69d309bb5",
- // toEntName: "国药控股福建有限公司",
- // toEntType: "5",
- // toEntTypeName: "物流企业",
- // toProvinceDesc: "福建省",
- // toCityDesc: "厦门市",
- // toAreaDesc: "思明区",
- // billTime: "2025-09-27 00:00:00",
- // uploadEntId: "00000000000012618117",
- // uploadEntName: "深圳华润三九医药贸易有限公司",
- // uploadEntType: "2",
- // uploadEntTypeName: "批发企业",
- // uploadProvinceDesc: "广东省",
- // uploadCityDesc: "深圳市",
- // uploadAreaDesc: "龙华区",
- // uploadTime: "2025-09-28 10:04:12",
- // },
- // {
- // billTypeName: "采购入库",
- // billCode: "2504032G-1",
- // fromEntId: "00000000000000592642",
- // fromEntName: "华润三九医药股份有限公司",
- // fromEntType: "1",
- // fromEntTypeName: "生产企业",
- // fromProvinceDesc: "广东省",
- // fromCityDesc: "深圳市",
- // fromAreaDesc: "龙华区",
- // toEntId: "00000000000012618117",
- // toEntName: "深圳华润三九医药贸易有限公司",
- // toEntType: "2",
- // toEntTypeName: "批发企业",
- // toProvinceDesc: "广东省",
- // toCityDesc: "深圳市",
- // toAreaDesc: "龙华区",
- // billTime: "2025-09-27 00:00:00",
- // uploadEntId: "00000000000000592642",
- // uploadEntName: "华润三九医药股份有限公司",
- // uploadEntType: "1",
- // uploadEntTypeName: "生产企业",
- // uploadProvinceDesc: "广东省",
- // uploadCityDesc: "深圳市",
- // uploadAreaDesc: "龙华区",
- // uploadTime: "2025-09-28 10:04:06",
- // },
- // {
- // billTypeName: "采购入库",
- // billCode: "FJASNP2509280580393",
- // fromEntId: "00000000000012618117",
- // fromEntName: "深圳华润三九医药贸易有限公司",
- // fromEntType: "2",
- // fromEntTypeName: "批发企业",
- // fromProvinceDesc: "广东省",
- // fromCityDesc: "深圳市",
- // fromAreaDesc: "龙华区",
- // toEntId: "8b63aa19a9a94cb6aa472ee69d309bb5",
- // toEntName: "国药控股福建有限公司",
- // toEntType: "5",
- // toEntTypeName: "物流企业",
- // toProvinceDesc: "福建省",
- // toCityDesc: "厦门市",
- // toAreaDesc: "思明区",
- // billTime: "2025-10-30 00:00:00",
- // uploadEntId: "00000000000012618117",
- // uploadEntName: "深圳华润三九医药贸易有限公司",
- // uploadEntType: "2",
- // uploadEntTypeName: "批发企业",
- // uploadProvinceDesc: "广东省",
- // uploadCityDesc: "深圳市",
- // uploadAreaDesc: "龙华区",
- // uploadTime: "2025-10-30 17:05:35",
- // },
- // {
- // billTypeName: "销售出库",
- // billCode: "FJS0CFFPHL",
- // fromEntId: "8b63aa19a9a94cb6aa472ee69d309bb5",
- // fromEntName: "国药控股福建有限公司",
- // fromEntType: "5",
- // fromEntTypeName: "物流企业",
- // fromProvinceDesc: "福建省",
- // fromCityDesc: "厦门市",
- // fromAreaDesc: "思明区",
- // toEntId: "00000000000001312166",
- // toEntName: "国药控股厦门有限公司",
- // toEntType: "2",
- // toEntTypeName: "批发企业",
- // toProvinceDesc: "福建省",
- // toCityDesc: "厦门市",
- // toAreaDesc: "同安区",
- // billTime: "2025-10-31 00:00:00",
- // uploadEntId: "8b63aa19a9a94cb6aa472ee69d309bb5",
- // uploadEntName: "国药控股福建有限公司",
- // uploadEntType: "5",
- // uploadEntTypeName: "物流企业",
- // uploadProvinceDesc: "福建省",
- // uploadCityDesc: "厦门市",
- // uploadAreaDesc: "思明区",
- // uploadTime: "2025-11-01 00:03:29",
- // },
- // {
- // billTypeName: "采购入库",
- // billCode: "XMASNP2510310469890",
- // fromEntId: "8b63aa19a9a94cb6aa472ee69d309bb5",
- // fromEntName: "国药控股福建有限公司",
- // fromEntType: "5",
- // fromEntTypeName: "物流企业",
- // fromProvinceDesc: "福建省",
- // fromCityDesc: "厦门市",
- // fromAreaDesc: "思明区",
- // toEntId: "00000000000001312166",
- // toEntName: "国药控股厦门有限公司",
- // toEntType: "2",
- // toEntTypeName: "批发企业",
- // toProvinceDesc: "福建省",
- // toCityDesc: "厦门市",
- // toAreaDesc: "同安区",
- // billTime: "2025-11-03 00:00:00",
- // uploadEntId: "8b63aa19a9a94cb6aa472ee69d309bb5",
- // uploadEntName: "国药控股福建有限公司",
- // uploadEntType: "5",
- // uploadEntTypeName: "物流企业",
- // uploadProvinceDesc: "福建省",
- // uploadCityDesc: "厦门市",
- // uploadAreaDesc: "思明区",
- // uploadTime: "2025-11-03 17:14:48",
- // },
- // ];
- // this.imageData = this.buildImageData(defaultData);
- // this.updateGraphCanvasSize();
- // this.initChart();
- // if (
- // typeof _data === "object" &&
- // Object.values(_data).filter((i) => !!i).length == 0
- // ) {
- // this.data = null;
- // }
- // this.loading = false;
- // } else {
- // uni.showToast({ title: "查询失败", icon: "none" });
- // }
- }
- });
- },
- copy(text) {
- const data = String(text || this.traceCode);
- uni.setClipboardData({
- data,
- success: () => {
- console.log("packages/traceabilityCodeQuery/pages/detail/index.vue" + "storage" + "clipboardData");
- uni.showToast({
- title: "已复制追溯码",
- icon: "none",
- duration: 2000,
- });
- },
- });
- },
- toggleCompanyExpand() {
- if (!this.needCompanyToggle) return;
- this.companyExpanded = !this.companyExpanded;
- },
- toggleCardExpand() {
- this.cardExpanded = !this.cardExpanded;
- },
- formatTraceCode(traceCode) {
- return String(traceCode || "")
- .replace(/(.{5})/g, "$1 ")
- .trim();
- },
- },
- };
- </script>
- <style>
- .detail-bg {
- background-position: center top;
- background-size: 100% auto;
- }
- .detail-page {
- box-sizing: border-box;
- background: rgb(226 226 226);
- padding-bottom: 50rpx;
- }
- .detail-page-empty {
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 0 136rpx 280rpx;
- }
- .detail-back-top-btn {
- position: fixed;
- right: 28rpx;
- bottom: 110rpx;
- width: 84rpx;
- height: 84rpx;
- border-radius: 50%;
- background: #fff;
- display: flex;
- align-items: center;
- justify-content: center;
- box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.18);
- z-index: 999;
- }
- .back-top-fade-enter-active,
- .back-top-fade-leave-active {
- transition:
- opacity 0.28s ease,
- transform 0.28s ease;
- }
- .back-top-fade-enter-from,
- .back-top-fade-leave-to {
- opacity: 0;
- transform: translateY(16rpx) scale(0.92);
- }
- .detail-back-top-arrow {
- width: 20rpx;
- height: 20rpx;
- border-top: 4rpx solid #5f6773;
- border-left: 4rpx solid #5f6773;
- transform: rotate(45deg);
- margin-top: 8rpx;
- }
- .detail-card {
- padding: 40rpx;
- }
- .detail-drug-title {
- font-size: 45rpx;
- font-weight: 800;
- color: #000;
- margin-bottom: 16rpx;
- }
- .detail-row {
- position: relative;
- display: flex;
- margin-top: 12rpx;
- align-items: baseline;
- }
- .detail-label {
- flex-shrink: 0;
- display: inline-block;
- vertical-align: baseline;
- width: 150rpx;
- font-size: 28rpx;
- }
- .detail-value {
- display: inline-block;
- vertical-align: baseline;
- font-size: 28rpx;
- }
- .detail-company-value {
- line-height: 40rpx;
- }
- .detail-company-value.detail-clamp {
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
- overflow: hidden;
- }
- .detail-expand-tag {
- position: absolute;
- left: 150rpx;
- bottom: 0;
- color: #2c69ff;
- font-size: 28rpx;
- }
- .detail-value.detail-wrap {
- word-break: break-all;
- white-space: normal;
- }
- .detail-copy-btn {
- margin-left: 16rpx;
- display: flex;
- align-items: center;
- color: #2c69ff;
- }
- .detail-copy-text {
- margin-left: 6rpx;
- font-size: 26rpx;
- }
- .detail-traceability {
- font-size: 24rpx;
- margin: -10rpx 0 45rpx;
- }
- .detail-card-toggle {
- display: flex;
- justify-content: center;
- align-items: center;
- padding-top: 18rpx;
- }
- .detail-traceability .detail-label {
- width: auto;
- }
- .detail-section-header {
- padding: 30rpx 24rpx 20rpx 46rpx;
- border-bottom: 1rpx solid #55555526;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .detail-section-title {
- font-size: 35rpx;
- font-weight: 900;
- }
- .detail-section-actions {
- display: flex;
- align-items: center;
- gap: 12rpx;
- }
- .detail-section-action {
- font-size: 26rpx;
- color: #59a8f2;
- padding: 4rpx 6rpx;
- display: flex;
- align-items: center;
- gap: 6rpx;
- }
- .detail-section-action.active {
- color: #2c69ff;
- font-weight: 600;
- }
- .detail-timeline {
- margin-top: 20rpx;
- padding-bottom: 50rpx;
- }
- .detail-graph-placeholder {
- margin: 16rpx 20rpx 0;
- background: #fff;
- border-radius: 14rpx;
- font-size: 24rpx;
- text-align: center;
- color: #97a0ae;
- padding: 120rpx 20rpx;
- }
- .detail-graph-panel {
- margin: 16rpx 20rpx 0;
- background: #fff;
- border-radius: 14rpx;
- padding: 16rpx;
- }
- .graph-mode-row {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 18rpx;
- margin-bottom: 18rpx;
- padding: 4rpx 6rpx 0;
- }
- .graph-mode-left {
- flex: 1;
- }
- .graph-mode-title {
- font-size: 30rpx;
- color: #333;
- font-weight: 600;
- }
- .graph-mode-desc {
- margin-top: 8rpx;
- font-size: 24rpx;
- color: #8d96a0;
- line-height: 1.45;
- }
- .detail-graph-canvas {
- width: 100%;
- height: 920rpx;
- background: #f8fafc;
- border-radius: 10rpx;
- }
- .detail-timeline-toolbar {
- display: flex;
- gap: 16rpx;
- padding: 20rpx 32rpx 10rpx;
- }
- .detail-toolbar-btn {
- padding: 8rpx 18rpx;
- border: 1rpx solid #d8dbe0;
- border-radius: 999rpx;
- color: #666;
- font-size: 24rpx;
- background: #fff;
- }
- .detail-timeline-item {
- position: relative;
- display: flex;
- margin-left: 36rpx;
- }
- .detail-timeline-item-first {
- margin-top: 20rpx;
- }
- .detail-timeline-loadmore {
- text-align: center;
- color: #8f98a3;
- font-size: 24rpx;
- padding: 18rpx 0 8rpx;
- }
- .detail-left {
- position: absolute;
- top: 34rpx;
- left: 0;
- height: 100%;
- display: flex;
- align-items: center;
- flex-direction: column;
- }
- .detail-dot {
- width: 16rpx;
- height: 16rpx;
- border-radius: 50%;
- border: 2rpx solid #2c69ff;
- }
- .detail-dot.detail-fill {
- background: #2c69ff;
- border-color: #2c69ff;
- }
- .detail-line {
- width: 2rpx;
- flex: 1;
- background: #8face68f;
- margin-right: 2rpx;
- }
- .detail-right {
- flex: 1;
- padding: 20rpx 0;
- margin-left: 45rpx;
- border-bottom: 1rpx solid #55555526;
- }
- .detail-title-row {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 16rpx;
- }
- .detail-title {
- font-size: 32rpx;
- color: #333;
- }
- .detail-title.detail-bold {
- font-weight: 800;
- }
- .detail-item-toggle {
- flex-shrink: 0;
- padding: 6rpx 12rpx;
- border: 1rpx solid #d8dbe0;
- border-radius: 999rpx;
- font-size: 22rpx;
- color: #666;
- display: flex;
- align-items: center;
- gap: 6rpx;
- margin-right: 16rpx;
- }
- .detail-info-row {
- display: flex;
- margin-top: 10rpx;
- font-size: 28rpx;
- color: #444;
- }
- .detail-info-label {
- color: #666;
- flex-shrink: 0;
- }
- .detail-info-value {
- color: #333;
- word-break: break-all;
- }
- .detail-tag-row {
- display: flex;
- flex-wrap: wrap;
- gap: 10rpx;
- margin-top: 8rpx;
- padding-left: 130rpx;
- }
- .detail-tag {
- color: #57bfe1;
- border-radius: 8rpx;
- padding: 5rpx 14rpx;
- font-size: 22rpx;
- border: 1rpx solid #57bfe180;
- }
- .detail-tag-region {
- color: #999;
- border: 1rpx solid #d8dbe0;
- }
- .detail-text-red {
- color: red;
- }
- .detail-loading {
- animation: spin 1s linear infinite;
- }
- @keyframes spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
- }
- </style>
|