fix: refine V3 business detail views

V3.0
M先生 3 months ago
parent 2b7394d0b4
commit 107a56cca4
  1. 10
      src/components/mCountCard/index.vue
  2. 2
      src/components/mCountTo/vue-countTo.vue
  3. 9
      src/components/mSvglineAnimation/index.vue
  4. 2
      src/services/pastureApi.js
  5. 9
      src/services/request.js
  6. 2
      src/services/resourceManagementApi.js
  7. 9
      src/services/yakApi.js
  8. 3
      src/views/admin/OperationLogs.vue
  9. 64
      src/views/forestGrassWet/components/FeatureInfoPanel.vue
  10. 2
      src/views/forestGrassWet/index.vue
  11. 4
      src/views/grassLivestockBalance/BalanceAnalysisWorkbench.vue
  12. 16
      src/views/pastureManagement/index.vue
  13. 5
      src/views/yakManagement/YakOlMap.vue
  14. 62
      src/views/yakManagement/index.vue

@ -38,11 +38,11 @@ const props = defineProps({
type: Object,
default: () => {
return {
icon: "xiaoshoujine",
zh: "2023年销售金额",
en: "Sales amount in 2023",
value: 9500,
unit: "万元",
icon: "",
zh: "",
en: "",
value: 0,
unit: "",
decimals: 0,
}
},

@ -15,7 +15,7 @@ export default {
endVal: {
type: Number,
required: false,
default: 2017,
default: 0,
},
duration: {
type: Number,

@ -39,6 +39,8 @@
* @property {Number} strokeWidth 路径的粗细
* @property {String} color 路径的颜色
*/
let svgLineUid = 0
export default {
name: "SvglineAnimation",
props: {
@ -80,17 +82,12 @@ export default {
},
},
data() {
let uid = 1
const uid = ++svgLineUid
return {
maskId: "svgline-" + uid,
radialGradientId: "radialGradient-" + uid,
}
},
mounted() {
let uid = this._.uid
this.maskId = "svgline-" + uid
this.radialGradientId = "radialGradient-" + uid
},
}
</script>

@ -66,7 +66,7 @@ export function getPastureList() {
export async function getPastureProfile() {
const [dashboard, list] = await Promise.all([
getV3("/screen2/pasture-management/dashboard", { year: 2024 }),
getV3("/screen2/pasture-management/dashboard"),
getPastureList(),
])

@ -15,9 +15,15 @@ function appendParams(url, params = {}) {
}
export async function requestJson(url, options = {}) {
const { params, timeout = DEFAULT_TIMEOUT, ...fetchOptions } = options
const { params, timeout = DEFAULT_TIMEOUT, signal: externalSignal, ...fetchOptions } = options
const controller = new AbortController()
let timedOut = false
const abortFromExternalSignal = () => controller.abort()
if (externalSignal?.aborted) {
controller.abort()
} else {
externalSignal?.addEventListener?.("abort", abortFromExternalSignal, { once: true })
}
const timer = window.setTimeout(() => {
timedOut = true
controller.abort()
@ -60,6 +66,7 @@ export async function requestJson(url, options = {}) {
throw error
} finally {
window.clearTimeout(timer)
externalSignal?.removeEventListener?.("abort", abortFromExternalSignal)
}
}

@ -6,7 +6,7 @@ export async function getResourceManagementOptions() {
return normalizeResponseData(await requestJson(`${BASE_URL}/options`)) || {}
}
export async function getResourceDashboard(topicKey, year = 2023) {
export async function getResourceDashboard(topicKey, year) {
return normalizeResponseData(await requestJson(`${BASE_URL}/dashboard`, {
params: { topic: topicKey, year },
})) || createEmptyDashboard(topicKey)

@ -10,9 +10,8 @@ function joinBase(base, path) {
function unwrapResponse(response) {
const failed =
response?.success === false &&
response?.code !== undefined &&
![0, 200, "0", "200"].includes(response.code)
response?.success === false ||
(response?.code !== undefined && ![0, 200, "0", "200"].includes(response.code))
if (failed) {
throw new Error(response?.msg || response?.message || "接口请求失败")
@ -117,6 +116,10 @@ export function getYakEpidemicAnalysis(params = {}) {
return get(OPEN_API_BASE, "/screen2/yak-management/epidemic/analysis", params).then((data) => data || {})
}
export function getYakEpidemicOptions() {
return get(OPEN_API_BASE, "/screen2/yak-management/epidemic/options").then((data) => data || {})
}
export function getYakEpidemicRecords(params = {}) {
return get(OPEN_API_BASE, "/screen2/yak-management/epidemic/records", params).then((data) => data || {})
}

@ -46,7 +46,7 @@
</template>
<script setup>
import { reactive, ref, watch } from "vue"
import { reactive, ref } from "vue"
import { message } from "ant-design-vue"
import { ReloadOutlined, SearchOutlined } from "@ant-design/icons-vue"
import { fetchOperationLogs } from "@/services/admin"
@ -63,7 +63,6 @@ const columns = [
const query = reactive({ title: "", operName: "", businessType: undefined, status: undefined })
const dateRange = ref([]), rows = ref([]), loading = ref(false), detailOpen = ref(false), detail = ref(null)
const pagination = reactive({ current: 1, pageSize: 20, total: 0, showSizeChanger: true, showQuickJumper: true, showTotal: (total) => `共 ${total} 条` })
watch(dateRange, () => {}, { deep: true })
async function loadRows() { loading.value = true; try { const result = await fetchOperationLogs({ ...query, beginTime: dateRange.value?.[0], endTime: dateRange.value?.[1], pageNum: pagination.current, pageSize: pagination.pageSize }); rows.value = result.rows || []; pagination.total = Number(result.total || 0) } catch (error) { message.error(error.message) } finally { loading.value = false } }
function search() { pagination.current = 1; loadRows() }

@ -228,7 +228,7 @@ const topicBusinessFields = {
[["management_control_requirement"], "管控要求"],
[["reserve_level", "natural_protected_area_level"], "保护地级别"],
[["reserve_type", "natural_protected_area_type"], "保护地类型"],
[["area_source_mj", "area_mu", "area_m2"], "图斑面积"],
[["area_mu", "area_m2"], "图斑面积"],
[["original_area_m2"], "原始面积"],
[["adjusted_area_m2"], "调整面积"],
[["remarks"], "备注"],
@ -527,7 +527,6 @@ const fieldLabels = {
redline_type: "红线类型",
ecosystem_service_type: "生态系统服务类型",
management_control_requirement: "管控要求",
area_source_mj: "图斑面积",
area_m2: "图斑面积",
original_area_m2: "原始面积",
adjusted_area_m2: "调整面积",
@ -670,7 +669,6 @@ const patchAreaFieldKeys = new Set([
"source_area_mu",
"area_m2",
"acreage",
"area_source_mj",
"original_area_m2",
"adjusted_area_m2",
"mj",
@ -685,7 +683,6 @@ const patchAreaFieldKeys = new Set([
const townshipNamesByLength = [...hongyuanTownshipDisplayOrder].sort((a, b) => b.length - a.length)
const propsData = computed(() => props.feature?.properties || {})
const featureGeometry = computed(() => props.feature?.feature?.geometry || props.feature?.geometry || null)
const externalDisplayRows = computed(() =>
(Array.isArray(props.rows) ? props.rows : [])
.map((row, index) => ({
@ -745,10 +742,6 @@ const displayRows = computed(() => {
used.add(row.sourceKey)
}
})
const areaLabel = getTopicAreaLabel(activeTopicKey.value)
if (!isArtificialGrassland.value && !isEngineeringSuitabilityFeature.value && !rows.some((row) => row.label === areaLabel || isGenericPatchAreaLabel(row.label))) {
addStaticRow(rows, used, areaLabel, formatGeometryArea(featureGeometry.value), "geometry-area")
}
if (useCompactGrassland) return rows.slice(0, 10)
Object.entries(propsData.value).forEach(([key, value]) => {
if (used.has(key) || shouldHideFallbackField(key, value)) return
@ -865,17 +858,6 @@ function getRowPriority(row, index) {
return 150 + index
}
function addStaticRow(rows, used, label, value, sourceKey) {
if (!hasValue(value) || rows.some((row) => row.label === label || row.value === value)) return
rows.push({
key: `static-${sourceKey}`,
sourceKey,
label,
value,
})
used.add(sourceKey)
}
function resolveDisplayLabel(label, keys = []) {
if (activeTopicKey.value === "ecological" && isEcologicalScopeFeature.value && isEcologicalAreaLabel(label, keys)) {
return "图斑面积"
@ -939,7 +921,6 @@ function formatValue(key, value) {
if (patchAreaFieldKeys.has(key) && number === 0) return ""
if (number === 0 && zeroHiddenKeys.has(key)) return ""
const formatted = Number.isInteger(number) ? `${number}` : `${Number(number.toFixed(2))}`
if (key === "area_source_mj") return `${formatAreaWanMu(number * 15)} 万亩`
if (activeTopicKey.value === "ecological" && isEcologicalScopeFeature.value && ["area_mu", "acreage", "mj"].includes(key)) return `${formatAreaWanMu(number)} 万亩`
if (activeTopicKey.value === "ecological" && isEcologicalScopeFeature.value && ["area_m2", "original_area_m2", "adjusted_area_m2"].includes(key)) return `${formatAreaWanMu(number / 666.6667)} 万亩`
if (["xbmj", "tbdlmj", "geomj", "area_square_meter"].includes(key)) return `${formatAreaMu(number / 666.6667)} 亩`
@ -1099,49 +1080,6 @@ function isUnmappedCodeField(key, value) {
].includes(key)
}
function formatGeometryArea(geometry) {
const squareMeters = getGeometryAreaSquareMeters(geometry)
if (!squareMeters) return ""
const mu = squareMeters / 666.6667
if (activeTopicKey.value === "ecological" && isEcologicalScopeFeature.value) return `${formatAreaWanMu(mu)} 万亩`
return `${formatAreaMu(mu)} 亩`
}
function getGeometryAreaSquareMeters(geometry) {
if (!geometry?.coordinates) return 0
if (geometry.type === "Polygon") return getPolygonAreaSquareMeters(geometry.coordinates)
if (geometry.type === "MultiPolygon") {
return geometry.coordinates.reduce((sum, polygon) => sum + getPolygonAreaSquareMeters(polygon), 0)
}
return 0
}
function getPolygonAreaSquareMeters(rings = []) {
if (!rings.length) return 0
const [outer, ...holes] = rings
const outerArea = Math.abs(getRingAreaSquareMeters(outer))
const holesArea = holes.reduce((sum, ring) => sum + Math.abs(getRingAreaSquareMeters(ring)), 0)
return Math.max(0, outerArea - holesArea)
}
function getRingAreaSquareMeters(ring = []) {
if (ring.length < 3) return 0
const averageLat = ring.reduce((sum, point) => sum + Number(point?.[1] || 0), 0) / ring.length
const metersPerDegree = 111320
const xScale = metersPerDegree * Math.cos((averageLat * Math.PI) / 180)
let area = 0
for (let index = 0; index < ring.length; index++) {
const current = ring[index]
const next = ring[(index + 1) % ring.length]
const x1 = Number(current?.[0] || 0) * xScale
const y1 = Number(current?.[1] || 0) * metersPerDegree
const x2 = Number(next?.[0] || 0) * xScale
const y2 = Number(next?.[1] || 0) * metersPerDegree
area += x1 * y2 - x2 * y1
}
return area / 2
}
function normalizeFieldLabel(key) {
return String(key)
.replace(/_/g, " ")

@ -636,7 +636,7 @@ async function loadBusinessStats(topicKey, layerKey = activeLayerKey.value) {
coreLoading.value = true
coreStatus.value = "loading"
try {
const result = await getResourceDashboard(topicKey, 2023)
const result = await getResourceDashboard(topicKey)
if (destroyed || requestId !== businessRequestId) return
if (topicKey !== activeTopicKey.value) return
if (topicKey !== "woodland" && (businessLayerKey || "") !== (activeLayerKey.value || "")) return

@ -242,13 +242,13 @@ const verdictLabel = computed(() => {
return text === "基本实现草畜平衡" ? "基本平衡" : text
})
const adviceText = computed(() => props.balance?.adviceRows?.[0] || props.balance?.advice || "保持动态监测,结合牧草产量及时调整载畜规模。")
const childStatusLabel = computed(() => props.balance?.areaLevel === "village" ? "空间明细" : "下级区域")
const childStatusLabel = computed(() => props.balance?.areaLevel === "village" ? "类型分析" : "下级区域")
const childStatusValue = computed(() => {
if (!props.balance) return "--"
if (props.balance.areaLevel === "village") {
if (props.analysisState === "idle") return "待分析"
if (props.analysisState === "running" && props.progress < 40) return "核验中"
return detailAvailable.value ? "图斑已归属" : "待归属"
return detailAvailable.value ? "已完成" : "待归集"
}
const suffix = props.balance.areaLevel === "county" ? "个乡镇" : "个村"
return `${Number(props.balance.childCount) || 0}${suffix}`

@ -1214,9 +1214,7 @@ function handlePastureFeatureInfo(payload = {}) {
if (!pickedFeature || !layerKey.startsWith("pasture-focus")) return
const sourceProperties = pickedFeature.properties || pickedFeature.feature?.properties || {}
const recognizedYakCount = pickValue(sourceProperties, ["recognizedYakCount", "recognized_yak_count"])
const acreage = pickValue(sourceProperties, ["acreage", "areaMu", "area_mu", "grasslandArea", "grassland_area"])
const grasslandMuPerYak = pickValue(sourceProperties, ["grasslandMuPerYak", "grassland_mu_per_yak"])
?? calculateGrasslandMuPerYak(acreage, recognizedYakCount)
const properties = {
...sourceProperties,
recognizedYakCount,
@ -1234,13 +1232,6 @@ function handlePastureFeatureInfo(payload = {}) {
grasslandFeatureLoading.value = false
}
function calculateGrasslandMuPerYak(acreage, recognizedYakCount) {
const area = Number(acreage)
const yakCount = Number(recognizedYakCount)
if (!Number.isFinite(area) || !Number.isFinite(yakCount) || yakCount <= 0) return null
return Number((area / yakCount).toFixed(2))
}
function formatPastureDetailValue(value, unit, fractionDigits) {
if (value === undefined || value === null || value === "") return ""
const number = Number(value)
@ -1514,14 +1505,11 @@ function normalizeOnlineRows(rows = []) {
const registered = Number(pickValue(item, ["registeredMuhuCount", "registered_muhu_count", "registeredCount"]))
const online = Number(pickValue(item, ["muhuCount", "muhu_count", "onlineCount"]))
const rawValue = pickValue(item, ["onlinePercent", "uplineRate", "upline_rate", "onlineRate", "value", "percent", "rate"])
const value = rawValue !== undefined && rawValue !== null && rawValue !== ""
? rawValue
: registered > 0 ? (online / registered) * 100 : undefined
if (!name || value === undefined || value === null || value === "") return null
if (!name || rawValue === undefined || rawValue === null || rawValue === "") return null
return {
key: item.code || item.areaCode || item.area_code || name,
name,
value: Number(String(value).replace("%", "")) || 0,
value: Number(String(rawValue).replace("%", "")) || 0,
unit: "%",
extra: Number.isFinite(online) && Number.isFinite(registered) && registered > 0 ? `${online}/${registered}户` : item.extra,
}

@ -50,6 +50,7 @@ import RegularShape from "ol/style/RegularShape.js"
import Stroke from "ol/style/Stroke.js"
import Style from "ol/style/Style.js"
import Text from "ol/style/Text.js"
import { HONGYUAN_COUNTY_CENTER } from "@/config/layers"
import { hongyuanTownshipLabelPoints, hongyuanTownshipsGeoJson } from "@/config/townshipBoundaries"
const props = defineProps({
@ -63,7 +64,7 @@ const props = defineProps({
},
center: {
type: Array,
default: () => [103.07984, 33.029058],
default: () => [...HONGYUAN_COUNTY_CENTER],
},
minZoom: {
type: Number,
@ -115,7 +116,7 @@ const props = defineProps({
const emit = defineEmits(["ready", "tile-status", "layer-status", "map-click"])
const HONGYUAN_CENTER = [103.07984, 33.029058]
const HONGYUAN_CENTER = [...HONGYUAN_COUNTY_CENTER]
const INITIAL_ZOOM = 21
const TOWN_BOUNDARY_GLOW_Z_INDEX = 232
const TOWN_BOUNDARY_LINE_Z_INDEX = 233

@ -213,7 +213,8 @@
</m-card>
<m-card class="yak-side-card left-card" title="预测因子雷达" :width="sideCardWidth" :height="sideCardHeight">
<v-chart class="yak-chart" :option="forecastFactorOption" :autoresize="true" />
<v-chart v-if="hasForecastFactorData" class="yak-chart" :option="forecastFactorOption" :autoresize="true" />
<div v-else class="yak-chart-empty">暂无预测因子数据</div>
</m-card>
</template>
@ -756,6 +757,7 @@ import {
import {
getYakEpidemicAnalysis,
getYakEpidemicDetail,
getYakEpidemicOptions,
getYakEpidemicRecords,
getYakEpidemicStats,
getYakEpidemicVillageStats,
@ -843,7 +845,7 @@ const viewTopics = [
const activeView = ref(resolveYakViewKey(props.viewKey))
const activeYakLayerKeys = ref([])
const activePeriodKey = ref("2027")
const activePeriodKey = ref("")
const activeForecastGranularity = ref("month")
const activeFoundationPage = ref(0)
const apiTownRows = ref([])
@ -851,8 +853,8 @@ const apiStructureRows = ref([])
const yakProfileRevision = ref(0)
const structureLoading = ref(false)
const structureError = ref("")
const epidemicYears = [2025, 2026]
const activeEpidemicYear = ref(2026)
const epidemicYears = ref([])
const activeEpidemicYear = ref(null)
const epidemicAnalysis = ref({ metrics: {} })
const epidemicStats = ref({ rows: [], year: null })
const epidemicVillageStats = ref({ rows: [] })
@ -1978,6 +1980,11 @@ const forecastFactorOption = computed(() => {
}
})
const hasForecastFactorData = computed(() => {
yakProfileRevision.value
return forecastFactorRows.length >= 3
})
const forecastTownOption = computed(() => buildHorizontalBarOption(forecastTownRows.value, "forecast", "头"))
const forecastStructureOption = computed(() => {
@ -3041,8 +3048,8 @@ function normalizeEpidemicTownName(name) {
function resolveEpidemicYear(year) {
const numericYear = Number(year)
if (Number.isFinite(numericYear) && epidemicYears.includes(numericYear)) return numericYear
return epidemicYears[epidemicYears.length - 1]
if (Number.isFinite(numericYear) && epidemicYears.value.includes(numericYear)) return numericYear
return epidemicYears.value[epidemicYears.value.length - 1] ?? null
}
function getEpidemicYearRange(year = activeEpidemicYear.value) {
@ -3678,6 +3685,12 @@ async function loadRemoteRows() {
])
if (profileResult.status === "fulfilled") {
configureYakManagementProfile(profileResult.value)
const defaultPeriodKey = String(profileResult.value?.defaultPeriod || "")
const currentPeriodExists = forecastPeriods.some((item) => item.key === activePeriodKey.value)
const defaultPeriodExists = forecastPeriods.some((item) => item.key === defaultPeriodKey)
if (!currentPeriodExists) {
activePeriodKey.value = defaultPeriodExists ? defaultPeriodKey : forecastPeriods[0]?.key || ""
}
yakProfileRevision.value += 1
} else {
structureError.value = "牦牛管理基础数据接口异常"
@ -3865,11 +3878,30 @@ function locateTradeRecord(row) {
}
async function loadEpidemicData(preferredYear = activeEpidemicYear.value) {
epidemicLoading.value = true
epidemicError.value = ""
if (!epidemicYears.value.length) {
try {
const options = await getYakEpidemicOptions()
epidemicYears.value = normalizeTradeYears(options?.years)
const latestYear = Number(options?.latestYear ?? options?.latest_year)
activeEpidemicYear.value = epidemicYears.value.includes(latestYear)
? latestYear
: epidemicYears.value[epidemicYears.value.length - 1] ?? null
} catch (error) {
epidemicError.value = "防疫年份接口异常"
epidemicLoading.value = false
return
}
}
const targetYear = resolveEpidemicYear(preferredYear)
if (!Number.isFinite(targetYear)) {
epidemicError.value = "暂无可用防疫年份"
epidemicLoading.value = false
return
}
const yearRange = getEpidemicYearRange(targetYear)
activeEpidemicYear.value = targetYear
epidemicLoading.value = true
epidemicError.value = ""
const [analysisResult, recordsResult, statsResult, villageStatsResult] = await Promise.allSettled([
getYakEpidemicAnalysis(yearRange),
getYakEpidemicRecords({
@ -4307,9 +4339,8 @@ function buildForecastKLineRows(series) {
const close = Number(series.stock?.[index] || open)
const birth = Math.max(0, Number(series.births?.[index] || 0) - Number(series.births?.[Math.max(index - 1, 0)] || 0))
const outflow = Math.max(0, Number(series.outflow?.[index] || 0) - Number(series.outflow?.[Math.max(index - 1, 0)] || 0))
const wickPadding = Math.max(120, Math.round((birth + outflow) * 0.18))
const high = Math.max(open, close) + Math.max(wickPadding, Math.round(birth * 0.32))
const low = Math.max(0, Math.min(open, close) - Math.max(wickPadding, Math.round(outflow * 0.32)))
const high = Math.max(open, close)
const low = Math.min(open, close)
return {
label,
open,
@ -4904,6 +4935,15 @@ function formatSignedWan(value) {
box-sizing: border-box;
}
.yak-chart-empty {
display: grid;
place-items: center;
width: 100%;
height: 100%;
color: rgba(180, 235, 242, 0.68);
font-size: 12px;
}
.forecast-trend-card {
position: relative;
width: 100%;

Loading…
Cancel
Save