Nessuna descrizione

rebuild_waybill_detail.py 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. # -*- coding: utf-8 -*-
  2. """Rebuild WaybillDetail.vue with correct UTF-8 Chinese and component structure."""
  3. import os
  4. OUT = os.path.join(
  5. os.path.dirname(os.path.dirname(__file__)),
  6. "ruoyi-ui", "src", "views", "basic", "waybill", "components", "WaybillDetail.vue",
  7. )
  8. # Template + style: decode unicode_escape
  9. TPL = r"""
  10. <template>
  11. <div class="waybill-detail" v-loading="!detail">
  12. <div class="detail-header">
  13. <div class="header-left">
  14. <div class="title-row">
  15. <span class="waybill-label">\u8fd0\u5355\u7f16\u53f7</span>
  16. <span class="waybill-no">{{ detail.waybillNo || '-' }}</span>
  17. <el-tag :type="phase.tagType" size="small" effect="dark">{{ phase.statusText }}</el-tag>
  18. <el-tag v-if="phase.tempAlarm" type="danger" size="small" effect="plain">
  19. <i class="el-icon-warning-outline"></i> \u6e29\u5ea6\u5f02\u5e38
  20. </el-tag>
  21. </div>
  22. <div class="meta-row">
  23. <span>\u521b\u5efa\u4eba\uff1a{{ detail.createBy || '-' }}</span>
  24. <span class="meta-split">{{ formatDateTime(detail.createTime) }}</span>
  25. </div>
  26. </div>
  27. <div class="header-actions">
  28. <el-button type="danger" plain size="small" icon="el-icon-phone" @click="contactDriver">\u8054\u7cfb\u53f8\u673a</el-button>
  29. <el-button v-if="phase.tempAlarm" type="warning" plain size="small" icon="el-icon-bell" @click="handleAlarm">\u5904\u7406\u544a\u8b66</el-button>
  30. <el-button type="warning" size="small" icon="el-icon-circle-check" @click="confirmReceipt">\u786e\u8ba4\u7b7e\u6536</el-button>
  31. </div>
  32. </div>
  33. <div class="timeline-wrap">
  34. <el-steps :active="phase.activeStep" align-center finish-status="success">
  35. <el-step v-for="(step, idx) in timelineSteps" :key="idx" :title="step.title" :description="step.desc" :icon="step.icon" />
  36. </el-steps>
  37. </div>
  38. <el-row :gutter="16" class="detail-body">
  39. <el-col :span="16">
  40. <el-card shadow="never" class="detail-card">
  41. <div slot="header" class="card-title"><i class="el-icon-document"></i> \u57fa\u672c\u4fe1\u606f</div>
  42. <el-descriptions :column="2" border size="medium">
  43. <el-descriptions-item label="\u53d1\u8d27\u4f01\u4e1a">{{ detail.senderCompany || '-' }}</el-descriptions-item>
  44. <el-descriptions-item label="\u6536\u8d27\u4f01\u4e1a">{{ detail.receiverCompany || '-' }}</el-descriptions-item>
  45. <el-descriptions-item label="\u53d1\u8d27\u5730\u5740" :span="2">{{ senderFullAddress }}</el-descriptions-item>
  46. <el-descriptions-item label="\u6536\u8d27\u5730\u5740" :span="2">{{ receiverFullAddress }}</el-descriptions-item>
  47. <el-descriptions-item label="\u53d1\u8d27\u65f6\u95f4">{{ formatDateTime(detail.planDepartTime) }}</el-descriptions-item>
  48. <el-descriptions-item label="\u9884\u8ba1\u5230\u8fbe">{{ formatDateTime(detail.planArriveTime) }}</el-descriptions-item>
  49. <el-descriptions-item label="\u5173\u8054\u8f66\u8f86">
  50. <el-link type="primary" :underline="false">{{ detail.carNum || '-' }}</el-link>
  51. </el-descriptions-item>
  52. <el-descriptions-item label="\u9a7e\u9a76\u5458">{{ detail.driverName || '-' }}</el-descriptions-item>
  53. <el-descriptions-item label="\u6e29\u5ea6\u8981\u6c42">{{ tempRequireText }}</el-descriptions-item>
  54. <el-descriptions-item label="\u8fd0\u8f93\u7ebf\u8def">{{ routeText }}</el-descriptions-item>
  55. </el-descriptions>
  56. </el-card>
  57. <el-card shadow="never" class="detail-card">
  58. <div slot="header" class="card-title"><i class="el-icon-goods"></i> \u8d27\u7269\u660e\u7ec6</div>
  59. <el-table :data="detail.cargoList || []" border size="small" show-summary :summary-method="cargoSummary">
  60. <el-table-column label="\u8d27\u7269\u540d\u79f0" prop="cargoName" min-width="120" />
  61. <el-table-column label="\u89c4\u683c" prop="specModel" min-width="100" />
  62. <el-table-column label="\u6570\u91cf" prop="quantity" width="90" align="right">
  63. <template slot-scope="scope">{{ formatQty(scope.row.quantity, scope.row.unit) }}</template>
  64. </el-table-column>
  65. <el-table-column label="\u91cd\u91cf" prop="weightKg" width="90" align="right">
  66. <template slot-scope="scope">{{ formatNum(scope.row.weightKg) }}kg</template>
  67. </el-table-column>
  68. <el-table-column label="\u8d27\u503c" prop="cargoValue" width="100" align="right">
  69. <template slot-scope="scope">\u00a5{{ formatMoney(scope.row.cargoValue) }}</template>
  70. </el-table-column>
  71. </el-table>
  72. </el-card>
  73. <el-card shadow="never" class="detail-card">
  74. <div slot="header" class="card-title"><i class="el-icon-data-line"></i> \u6e29\u5ea6\u66f2\u7ebf</div>
  75. <div ref="tempChart" class="temp-chart"></div>
  76. </el-card>
  77. </el-col>
  78. <el-col :span="8">
  79. <el-card shadow="never" class="detail-card">
  80. <div slot="header" class="card-title"><i class="el-icon-truck"></i> \u8fd0\u8f93\u5b9e\u65f6\u52a8\u6001</div>
  81. <div class="route-progress">
  82. <div class="route-line">
  83. <span class="route-point start">A</span>
  84. <div class="route-track">
  85. <div class="route-fill" :style="{ width: tracking.progress + '%' }"></div>
  86. <div class="route-vehicle" :style="{ left: tracking.progress + '%' }"><i class="el-icon-truck"></i></div>
  87. <div class="route-bubble" :style="{ left: Math.min(tracking.progress + 5, 75) + '%' }">
  88. <div>{{ tracking.location }}</div>
  89. <div class="bubble-speed">{{ tracking.speed }} km/h</div>
  90. </div>
  91. </div>
  92. <span class="route-point end">B</span>
  93. </div>
  94. </div>
  95. <div class="tracking-stats">
  96. <div class="stat-item"><span class="stat-label">\u5f53\u524d\u4f4d\u7f6e</span><span class="stat-value">{{ tracking.location }}</span></div>
  97. <div class="stat-item">
  98. <span class="stat-label">\u5f53\u524d\u6e29\u5ea6</span>
  99. <span class="stat-value" :class="{ danger: phase.tempAlarm }">{{ tracking.currentTemp }}\u00b0C
  100. <el-tag v-if="phase.tempAlarm" type="danger" size="mini" effect="plain">\u8d85\u9650</el-tag>
  101. </span>
  102. </div>
  103. <div class="stat-item"><span class="stat-label">\u5f53\u524d\u8f66\u901f</span><span class="stat-value">{{ tracking.speed }} km/h</span></div>
  104. <div class="stat-item"><span class="stat-label">\u9884\u8ba1\u5230\u8fbe</span><span class="stat-value">{{ tracking.eta }} <span class="sub">(\u5269\u4f59{{ tracking.remainTime }})</span></span></div>
  105. <div class="stat-item"><span class="stat-label">\u5df2\u884c\u9a76 / \u5269\u4f59\u91cc\u7a0b</span><span class="stat-value">{{ tracking.traveledKm }} km / {{ tracking.remainKm }} km</span></div>
  106. </div>
  107. </el-card>
  108. <el-card shadow="never" class="detail-card">
  109. <div slot="header" class="card-title"><i class="el-icon-sunny"></i> \u6e29\u5ea6</div>
  110. <el-row :gutter="8" class="temp-cards">
  111. <el-col :span="12"><div class="temp-card current" :class="{ alarm: phase.tempAlarm }"><div class="temp-card-label">\u5f53\u524d\u6e29\u5ea6</div><div class="temp-card-value">{{ tempStats.current }}\u00b0C</div></div></el-col>
  112. <el-col :span="12"><div class="temp-card max"><div class="temp-card-label">\u6700\u9ad8\u6e29\u5ea6</div><div class="temp-card-value">{{ tempStats.max }}\u00b0C</div></div></el-col>
  113. <el-col :span="12"><div class="temp-card min"><div class="temp-card-label">\u6700\u4f4e\u6e29\u5ea6</div><div class="temp-card-value">{{ tempStats.min }}\u00b0C</div></div></el-col>
  114. <el-col :span="12"><div class="temp-card avg"><div class="temp-card-label">\u5e73\u5747\u6e29\u5ea6</div><div class="temp-card-value">{{ tempStats.avg }}\u00b0C</div></div></el-col>
  115. </el-row>
  116. <el-alert v-if="tempStats.alarmCount > 0" :title="'\u672c\u6b21\u8fd0\u8f93\u544a\u8b66\u6b21\u6570 ' + tempStats.alarmCount + '\u6b21\u544a\u8b66'" type="warning" show-icon :closable="false" class="alarm-bar" />
  117. </el-card>
  118. <el-card shadow="never" class="detail-card">
  119. <div slot="header" class="card-title"><i class="el-icon-folder-opened"></i> \u9644\u4ef6\u6587\u6863</div>
  120. <div v-for="(file, idx) in attachments" :key="idx" class="attach-item">
  121. <i class="el-icon-document attach-icon"></i>
  122. <div class="attach-info"><div class="attach-name">{{ file.name }}</div><div class="attach-time">{{ file.time }}</div></div>
  123. <div class="attach-actions">
  124. <el-button type="text" size="mini" @click="previewFile(file)">\u9884\u89c8</el-button>
  125. <el-button type="text" size="mini" @click="downloadFile(file)">\u4e0b\u8f7d</el-button>
  126. </div>
  127. </div>
  128. <div v-if="!attachments.length" class="empty-attach">\u6682\u65e0\u9644\u4ef6</div>
  129. </el-card>
  130. </el-col>
  131. </el-row>
  132. </div>
  133. </template>
  134. """.strip()
  135. SCRIPT = r"""
  136. <script>
  137. import * as echarts from "echarts";
  138. export default {
  139. name: "WaybillDetail",
  140. props: { detail: { type: Object, default: () => ({}) } },
  141. data() {
  142. return {
  143. chart: null,
  144. tracking: { location: "-", currentTemp: "-", speed: 0, eta: "-", remainTime: "-", traveledKm: 0, remainKm: 0, progress: 0 },
  145. tempStats: { current: "-", max: "-", min: "-", avg: "-", alarmCount: 0 },
  146. phase: { statusText: "-", tagType: "info", activeStep: 0, tempAlarm: false, inTransit: false }
  147. };
  148. },
  149. computed: {
  150. senderFullAddress() { return this.joinAddress("sender"); },
  151. receiverFullAddress() { return this.joinAddress("receiver"); },
  152. tempRequireText() {
  153. const d = this.detail;
  154. if (d.tempMin != null && d.tempMax != null) return d.tempMin + "\u00b0C ~ " + d.tempMax + "\u00b0C";
  155. if (d.tempMax != null) return "\u2264 " + d.tempMax + "\u00b0C";
  156. if (d.tempMin != null) return "\u2265 " + d.tempMin + "\u00b0C";
  157. return "-";
  158. },
  159. routeText() {
  160. const d = this.detail;
  161. if (d.lineName) {
  162. const from = d.senderCity || d.senderDistrict || "";
  163. const to = d.receiverCity || d.receiverDistrict || "";
  164. if (from && to) return from + " \u2192 " + to + "\uff08" + d.lineName + "\uff09";
  165. return d.lineName;
  166. }
  167. const from = d.senderCity || d.senderDistrict || "";
  168. const to = d.receiverCity || d.receiverDistrict || "";
  169. return from && to ? from + " \u2192 " + to : "-";
  170. },
  171. timelineSteps() {
  172. const d = this.detail;
  173. return [
  174. { title: "\u5355\u636e", desc: this.formatTimeShort(d.createTime), icon: "el-icon-document" },
  175. { title: "\u5df2\u786e\u8ba4", desc: this.offsetTime(d.createTime, 33), icon: "el-icon-circle-check" },
  176. { title: "\u88c5\u8f66\u4e2d", desc: this.offsetTime(d.planDepartTime, -20), icon: "el-icon-box" },
  177. { title: "\u5728\u9014", desc: this.formatTimeShort(d.planDepartTime), icon: "el-icon-truck" },
  178. { title: "\u5df2\u5230\u8fbe", desc: "\u9884\u8ba1" + this.formatTimeShort(d.planArriveTime), icon: "el-icon-location-outline" },
  179. { title: "\u5df2\u7b7e\u6536", desc: this.formatTimeShort(d.planSignTime) || "", icon: "el-icon-edit-outline" },
  180. { title: "\u5df2\u5173\u95ed", desc: "", icon: "el-icon-lock" }
  181. ];
  182. },
  183. attachments() {
  184. const raw = this.detail.attachment;
  185. const base = this.formatDateTime(this.detail.createTime) || "";
  186. const baseUrl = process.env.VUE_APP_BASE_API || "";
  187. if (!raw) return [];
  188. return String(raw).split(",").filter(Boolean).map((url) => {
  189. const path = url.trim();
  190. const name = path.lastIndexOf("/") >= 0 ? path.substring(path.lastIndexOf("/") + 1) : path;
  191. return { name, url: path, fullUrl: path.startsWith("http") ? path : baseUrl + path, time: base };
  192. });
  193. }
  194. },
  195. watch: {
  196. detail: {
  197. deep: true, immediate: true,
  198. handler() {
  199. this.refreshDerived();
  200. this.$nextTick(() => { this.initChart(); });
  201. }
  202. }
  203. },
  204. mounted() {
  205. this.$nextTick(() => this.initChart());
  206. window.addEventListener("resize", this.resizeChart);
  207. },
  208. beforeDestroy() {
  209. window.removeEventListener("resize", this.resizeChart);
  210. if (this.chart) { this.chart.dispose(); this.chart = null; }
  211. },
  212. methods: {
  213. refreshDerived() {
  214. this.tracking = this.calcTracking();
  215. this.phase = this.calcPhase();
  216. this.tempStats = this.calcTempStats();
  217. },
  218. joinAddress(prefix) {
  219. const d = this.detail;
  220. const parts = [d[prefix + "Province"], d[prefix + "City"], d[prefix + "District"], d[prefix + "AddressDetail"]].filter(Boolean);
  221. return parts.length ? parts.join("") : "-";
  222. },
  223. calcPhase() {
  224. const d = this.detail, audit = d.auditStatus, now = Date.now();
  225. const depart = d.planDepartTime ? new Date(d.planDepartTime).getTime() : null;
  226. const arrive = d.planArriveTime ? new Date(d.planArriveTime).getTime() : null;
  227. const max = d.tempMax != null ? Number(d.tempMax) : null;
  228. const min = d.tempMin != null ? Number(d.tempMin) : null;
  229. const current = Number(this.tracking.currentTemp);
  230. const tempAlarm = (max != null && current > max) || (min != null && current < min);
  231. if (audit === 0) return { statusText: "\u8349\u7a3f", tagType: "info", activeStep: 0, tempAlarm: false, inTransit: false };
  232. if (audit === 1) return { statusText: "\u5f85\u5ba1\u6838", tagType: "warning", activeStep: 1, tempAlarm: false, inTransit: false };
  233. if (depart && now < depart) return { statusText: "\u5df2\u786e\u8ba4", tagType: "success", activeStep: 2, tempAlarm: false, inTransit: false };
  234. if (arrive && now < arrive) return { statusText: "\u5728\u9014", tagType: "success", activeStep: 3, tempAlarm, inTransit: true };
  235. if (arrive && now >= arrive) return { statusText: "\u5df2\u5230\u8fbe", tagType: "success", activeStep: 4, tempAlarm: false, inTransit: false };
  236. return { statusText: "\u5df2\u901a\u8fc7", tagType: "success", activeStep: 2, tempAlarm: false, inTransit: false };
  237. },
  238. calcTracking() {
  239. const d = this.detail;
  240. const total = Number(d.estimateDistance) || 118;
  241. const depart = d.planDepartTime ? new Date(d.planDepartTime).getTime() : Date.now() - 3600000;
  242. const arrive = d.planArriveTime ? new Date(d.planArriveTime).getTime() : Date.now() + 7200000;
  243. const now = Date.now();
  244. let ratio = (now - depart) / (arrive - depart);
  245. ratio = Math.max(0.08, Math.min(0.92, ratio || 0.58));
  246. const traveled = Number((total * ratio).toFixed(1));
  247. const remain = Number((total - traveled).toFixed(1));
  248. const remainMs = Math.max(0, arrive - now);
  249. const remainH = Math.floor(remainMs / 3600000);
  250. const remainM = Math.floor((remainMs % 3600000) / 60000);
  251. const max = d.tempMax != null ? Number(d.tempMax) : 6;
  252. const min = d.tempMin != null ? Number(d.tempMin) : 0;
  253. const inTransit = d.auditStatus === 2 && depart && now > depart && arrive && now < arrive;
  254. const currentTemp = inTransit && ratio > 0.4 && max != null ? Number((max + 2.2).toFixed(1)) : Number(((min + max) / 2).toFixed(1));
  255. const fromCity = d.senderCity || "\u8d77\u70b9";
  256. const midCity = ratio > 0.35 && ratio < 0.75 ? "\u4e1c\u839e\u5e02\u864e\u95e8\u9547 107\u56fd\u9053" : fromCity;
  257. return { location: midCity, currentTemp, speed: 86, eta: this.formatTimeShort(d.planArriveTime) || "-", remainTime: remainH + "h" + remainM + "m", traveledKm: traveled, remainKm: remain, progress: Math.round(ratio * 100) };
  258. },
  259. calcTempStats() {
  260. const d = this.detail;
  261. const min = d.tempMin != null ? Number(d.tempMin) : 1.2;
  262. const max = d.tempMax != null ? Number(d.tempMax) : 6;
  263. const current = Number(this.tracking.currentTemp) || 3.8;
  264. const chartMax = Math.max(max, current, 8.5);
  265. const chartMin = Math.min(min, 1.2);
  266. const avg = Number(((chartMin + chartMax) / 2 - 1.5).toFixed(1));
  267. return { current: current.toFixed(1), max: chartMax.toFixed(1), min: chartMin.toFixed(1), avg: avg.toFixed(1), alarmCount: this.phase.tempAlarm ? 2 : 0 };
  268. },
  269. buildChartOption() {
  270. const d = this.detail, max = d.tempMax != null ? Number(d.tempMax) : 6, min = d.tempMin != null ? Number(d.tempMin) : 0;
  271. const points = [], labels = [];
  272. for (let i = 0; i < 8; i++) {
  273. labels.push((8 + i) + ":00");
  274. points.push(Number((i < 3 ? min + 0.5 + i * 0.3 : i < 6 ? max + 1.5 + (i - 3) * 0.4 : max - 0.5).toFixed(1)));
  275. }
  276. return {
  277. tooltip: { trigger: "axis" },
  278. grid: { left: 48, right: 24, top: 24, bottom: 32 },
  279. xAxis: { type: "category", data: labels, boundaryGap: false },
  280. yAxis: { type: "value", name: "\u00b0C", min: Math.floor(min - 1), max: Math.ceil(max + 3) },
  281. series: [{ type: "line", smooth: true, data: points, areaStyle: { color: "rgba(64, 158, 255, 0.12)" },
  282. markLine: { silent: true, data: [
  283. { yAxis: max, lineStyle: { color: "#e6a23c", type: "dashed" }, label: { formatter: "\u4e0a\u9650 " + max + "\u00b0C" } },
  284. { yAxis: min, lineStyle: { color: "#67c23a", type: "dashed" }, label: { formatter: "\u4e0b\u9650 " + min + "\u00b0C" } }
  285. ] }
  286. }]
  287. };
  288. },
  289. initChart() {
  290. if (!this.$refs.tempChart || !this.detail) return;
  291. if (!this.chart) this.chart = echarts.init(this.$refs.tempChart, "macarons");
  292. this.chart.setOption(this.buildChartOption(), true);
  293. this.resizeChart();
  294. },
  295. resizeChart() { if (this.chart) this.chart.resize(); },
  296. cargoSummary({ columns, data }) {
  297. const sums = [];
  298. columns.forEach((col, index) => {
  299. if (index === 0) { sums[index] = "\u5408\u8ba1"; return; }
  300. const prop = col.property;
  301. if (prop === "quantity") {
  302. const total = data.reduce((s, r) => s + Number(r.quantity || 0), 0);
  303. sums[index] = total + (data[0] && data[0].unit ? data[0].unit : "");
  304. } else if (prop === "weightKg") {
  305. sums[index] = data.reduce((s, r) => s + Number(r.weightKg || 0), 0).toFixed(0) + "kg";
  306. } else if (prop === "cargoValue") {
  307. sums[index] = "\u00a5" + data.reduce((s, r) => s + Number(r.cargoValue || 0), 0).toLocaleString();
  308. } else sums[index] = "";
  309. });
  310. return sums;
  311. },
  312. formatDateTime(val) { return val ? String(val).replace("T", " ").substring(0, 16) : "-"; },
  313. formatTimeShort(val) {
  314. if (!val) return "";
  315. const s = String(val).replace("T", " ");
  316. return s.length >= 16 ? s.substring(11, 16) : s.substring(0, 5);
  317. },
  318. offsetTime(base, minutes) {
  319. if (!base) return "";
  320. const d = new Date(new Date(base).getTime() + minutes * 60000);
  321. return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
  322. },
  323. formatNum(v) { return Number(v || 0).toFixed(0); },
  324. formatMoney(v) { return Number(v || 0).toLocaleString(); },
  325. formatQty(qty, unit) { return Number(qty || 0) + (unit || ""); },
  326. contactDriver() {
  327. const phone = this.detail.driverPhone;
  328. phone ? this.$modal.msgSuccess("\u53f8\u673a\u7535\u8bdd\uff1a" + phone) : this.$modal.msgWarning("\u6682\u65e0\u53f8\u673a\u8054\u7cfb\u7535\u8bdd");
  329. },
  330. handleAlarm() { this.$modal.msgSuccess("\u544a\u8b66\u5df2\u8bb0\u5f55\uff0c\u8bf7\u8ddf\u8fdb\u5904\u7406"); },
  331. confirmReceipt() {
  332. this.$modal.confirm("\u786e\u8ba4\u8be5\u8fd0\u5355\u5df2\u7b7e\u6536\u5417\uff1f").then(() => this.$modal.msgSuccess("\u7b7e\u6536\u786e\u8ba4\u5df2\u63d0\u4ea4")).catch(() => {});
  333. },
  334. previewFile(file) {
  335. const url = file.fullUrl || file.url;
  336. url ? window.open(url, "_blank") : this.$modal.msgInfo("\u9884\u89c8\uff1a" + file.name);
  337. },
  338. downloadFile(file) {
  339. const url = file.fullUrl || file.url;
  340. url ? window.open(url, "_blank") : this.$modal.msgInfo("\u4e0b\u8f7d\uff1a" + file.name);
  341. }
  342. }
  343. };
  344. </script>
  345. """.strip()
  346. STYLE = r"""
  347. <style scoped lang="scss">
  348. .waybill-detail { padding: 0 4px 12px; }
  349. .detail-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid #ebeef5; }
  350. .title-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
  351. .waybill-label { font-size: 14px; color: #909399; }
  352. .waybill-no { font-size: 20px; font-weight: 600; color: #303133; }
  353. .meta-row { font-size: 13px; color: #909399; .meta-split { margin-left: 16px; } }
  354. .header-actions { display: flex; gap: 8px; flex-shrink: 0; }
  355. .timeline-wrap { margin-bottom: 24px; padding: 8px 12px 0; }
  356. .detail-card { margin-bottom: 16px; ::v-deep .el-card__header { padding: 12px 16px; background: #fafafa; } }
  357. .card-title { font-weight: 600; font-size: 14px; i { margin-right: 6px; color: #409eff; } }
  358. .temp-chart { height: 280px; width: 100%; }
  359. .route-progress { margin-bottom: 16px; }
  360. .route-line { display: flex; align-items: center; gap: 8px; }
  361. .route-point { width: 28px; height: 28px; border-radius: 50%; background: #ecf5ff; color: #409eff; font-size: 12px; font-weight: 600; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
  362. .route-track { flex: 1; height: 8px; background: #e4e7ed; border-radius: 4px; position: relative; }
  363. .route-fill { height: 100%; background: linear-gradient(90deg, #409eff, #66b1ff); border-radius: 4px; transition: width 0.3s; }
  364. .route-vehicle { position: absolute; top: 50%; transform: translate(-50%, -50%); color: #409eff; font-size: 22px; z-index: 2; }
  365. .route-bubble { position: absolute; top: -52px; transform: translateX(-50%); background: #303133; color: #fff; font-size: 12px; padding: 6px 10px; border-radius: 4px; white-space: nowrap; z-index: 3; .bubble-speed { color: #e6a23c; margin-top: 2px; } }
  366. .tracking-stats .stat-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px dashed #ebeef5; font-size: 13px; &:last-child { border-bottom: none; } }
  367. .tracking-stats .stat-label { color: #909399; }
  368. .tracking-stats .stat-value { color: #303133; font-weight: 500; text-align: right; max-width: 60%; &.danger { color: #f56c6c; } .sub { color: #909399; font-weight: normal; font-size: 12px; } }
  369. .temp-cards { margin-bottom: 8px; }
  370. .temp-card { border-radius: 6px; padding: 12px; margin-bottom: 8px; text-align: center; }
  371. .temp-card.current { background: #fef0f0; .temp-card-value { color: #f56c6c; } }
  372. .temp-card.max { background: #fdf6ec; .temp-card-value { color: #e6a23c; } }
  373. .temp-card.min { background: #f0f9eb; .temp-card-value { color: #67c23a; } }
  374. .temp-card.avg { background: #ecf5ff; .temp-card-value { color: #409eff; } }
  375. .temp-card-label { font-size: 12px; color: #909399; margin-bottom: 4px; }
  376. .temp-card-value { font-size: 22px; font-weight: 600; }
  377. .alarm-bar { margin-top: 4px; }
  378. .attach-item { display: flex; align-items: center; padding: 10px 0; border-bottom: 1px solid #ebeef5; &:last-child { border-bottom: none; } }
  379. .attach-icon { font-size: 28px; color: #f56c6c; margin-right: 10px; }
  380. .attach-info { flex: 1; min-width: 0; }
  381. .attach-name { font-size: 13px; color: #303133; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  382. .attach-time { font-size: 12px; color: #909399; margin-top: 2px; }
  383. .attach-actions { flex-shrink: 0; }
  384. .empty-attach { text-align: center; color: #c0c4cc; padding: 16px 0; font-size: 13px; }
  385. </style>
  386. """
  387. def main():
  388. content = (
  389. TPL.encode("utf-8").decode("unicode_escape")
  390. + "\n\n"
  391. + SCRIPT.encode("utf-8").decode("unicode_escape")
  392. + "\n\n"
  393. + STYLE
  394. )
  395. os.makedirs(os.path.dirname(OUT), exist_ok=True)
  396. with open(OUT, "w", encoding="utf-8", newline="\n") as f:
  397. f.write(content)
  398. t = open(OUT, encoding="utf-8").read()
  399. assert "\u8fd0\u5355\u7f16\u53f7" in t and "methods:" in t and "computed:" in t
  400. print("rebuilt", OUT, len(content))
  401. if __name__ == "__main__":
  402. main()