|
@@ -13,11 +13,11 @@
|
|
|
<div class="panel-shell" ref="panelShellRef">
|
|
<div class="panel-shell" ref="panelShellRef">
|
|
|
<div class="crop-header">
|
|
<div class="crop-header">
|
|
|
<div class="crop-info">
|
|
<div class="crop-info">
|
|
|
- <el-avatar class="crop-avatar" :size="24" :src="cropAvatar" />
|
|
|
|
|
|
|
+ <el-avatar class="crop-avatar" :size="24" :src="resolvedCropAvatar" />
|
|
|
<span class="crop-name">{{ currentCropName }}</span>
|
|
<span class="crop-name">{{ currentCropName }}</span>
|
|
|
<span class="crop-badge">当前作物</span>
|
|
<span class="crop-badge">当前作物</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="switch-wrap">
|
|
|
|
|
|
|
+ <div v-if="hasFarm" class="switch-wrap">
|
|
|
<el-tooltip
|
|
<el-tooltip
|
|
|
v-model:visible="cropMenuVisible"
|
|
v-model:visible="cropMenuVisible"
|
|
|
placement="bottom"
|
|
placement="bottom"
|
|
@@ -65,31 +65,40 @@
|
|
|
|
|
|
|
|
<div class="alert-list" v-show="!isBackground">
|
|
<div class="alert-list" v-show="!isBackground">
|
|
|
<div
|
|
<div
|
|
|
- v-for="(item, index) in alertCards"
|
|
|
|
|
- :key="index"
|
|
|
|
|
- class="alert-card"
|
|
|
|
|
- >
|
|
|
|
|
- <div class="alert-card__header">
|
|
|
|
|
- <div class="alert-card__title-wrap">
|
|
|
|
|
- <img class="alert-card__icon" :src="item.icon" alt="" />
|
|
|
|
|
- <span class="alert-card__title">{{ item.title }}</span>
|
|
|
|
|
|
|
+ v-if="reportGenerating"
|
|
|
|
|
+ class="alert-generating"
|
|
|
|
|
+ v-loading="true"
|
|
|
|
|
+ element-loading-text="报告生成中,请稍后..."
|
|
|
|
|
+ element-loading-background="rgba(255, 255, 255, 0.9)"
|
|
|
|
|
+ ></div>
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(item, index) in alertCards"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ class="alert-card"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="alert-card__header">
|
|
|
|
|
+ <div class="alert-card__title-wrap">
|
|
|
|
|
+ <img class="alert-card__icon" :src="item.icon" alt="" />
|
|
|
|
|
+ <span class="alert-card__title">{{ item.title }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span class="alert-card__link" @click="handleViewDetail(item)">
|
|
|
|
|
+ 查看详情
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="alert-card__content-wrap">
|
|
|
|
|
+ <span class="alert-card__content">{{ item.content }}</span>
|
|
|
|
|
+ <span
|
|
|
|
|
+ v-if="hasFarm && item.patrolTip && item.title !== '正常气象'"
|
|
|
|
|
+ class="alert-card__patrol"
|
|
|
|
|
+ @click="handlePatrolTip(item)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-icon class="alert-card__patrol-icon"><Link /></el-icon>
|
|
|
|
|
+ <span>{{ item.patrolTip }}</span>
|
|
|
|
|
+ </span>
|
|
|
</div>
|
|
</div>
|
|
|
- <span class="alert-card__link" @click="handleViewDetail(item)">
|
|
|
|
|
- 查看详情
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="alert-card__content-wrap">
|
|
|
|
|
- <span class="alert-card__content">{{ item.content }}</span>
|
|
|
|
|
- <span
|
|
|
|
|
- v-if="item.patrolTip && item.title !== '正常气象'"
|
|
|
|
|
- class="alert-card__patrol"
|
|
|
|
|
- @click="handlePatrolTip(item)"
|
|
|
|
|
- >
|
|
|
|
|
- <el-icon class="alert-card__patrol-icon"><Link /></el-icon>
|
|
|
|
|
- <span>{{ item.patrolTip }}</span>
|
|
|
|
|
- </span>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </template>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -100,7 +109,7 @@
|
|
|
<script setup>
|
|
<script setup>
|
|
|
import { FloatingPanel } from "vant";
|
|
import { FloatingPanel } from "vant";
|
|
|
import { CaretBottom, Link } from "@element-plus/icons-vue";
|
|
import { CaretBottom, Link } from "@element-plus/icons-vue";
|
|
|
-import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
|
|
|
|
|
+import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
|
import { useStore } from "vuex";
|
|
import { useStore } from "vuex";
|
|
|
import { useI18n } from "@/i18n";
|
|
import { useI18n } from "@/i18n";
|
|
|
import { useRouter } from "vue-router";
|
|
import { useRouter } from "vue-router";
|
|
@@ -115,12 +124,22 @@ const CARD_ID = {
|
|
|
STRESS: 2,
|
|
STRESS: 2,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+const COUNTY_CODE_STORAGE_KEY = "GROWTH_REPORT_COUNTY_CODE";
|
|
|
|
|
+/** 有农场预警为空时轮询:间隔 5s,最长 3 分钟 */
|
|
|
|
|
+const REPORT_POLL_INTERVAL_MS = 5000;
|
|
|
|
|
+const REPORT_POLL_MAX_MS = 3 * 60 * 1000;
|
|
|
|
|
+
|
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
|
/** 当前选中农场 id,切换农场后触发分区 / 预警重拉 */
|
|
/** 当前选中农场 id,切换农场后触发分区 / 预警重拉 */
|
|
|
farmId: {
|
|
farmId: {
|
|
|
type: [String, Number],
|
|
type: [String, Number],
|
|
|
default: "",
|
|
default: "",
|
|
|
},
|
|
},
|
|
|
|
|
+ /** 无农场时区县码,与 getHomeMapCalcByCounty 同源 */
|
|
|
|
|
+ countyCode: {
|
|
|
|
|
+ type: [String, Number],
|
|
|
|
|
+ default: "",
|
|
|
|
|
+ },
|
|
|
cropName: {
|
|
cropName: {
|
|
|
type: String,
|
|
type: String,
|
|
|
default: "",
|
|
default: "",
|
|
@@ -140,6 +159,13 @@ const activeIndex = ref(0);
|
|
|
const emit = defineEmits(["viewDetail", "heightChange", "zoneChange"]);
|
|
const emit = defineEmits(["viewDetail", "heightChange", "zoneChange"]);
|
|
|
|
|
|
|
|
const weatherIcon = require("@/assets/img/report/weather.png");
|
|
const weatherIcon = require("@/assets/img/report/weather.png");
|
|
|
|
|
+const DEFAULT_CROP_AVATAR = "https://birdseye-img.sysuimars.com/dinggou-mini/defalut-icon.png";
|
|
|
|
|
+/** 当前作物名 → 品类图标 */
|
|
|
|
|
+const CROP_ICON_MAP = {
|
|
|
|
|
+ 水稻: require("@/assets/img/type_icon/水稻.png"),
|
|
|
|
|
+ 玉米: require("@/assets/img/type_icon/玉米.png"),
|
|
|
|
|
+ 荔枝: require("@/assets/img/type_icon/荔枝.png"),
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
/** 底部 tabBar 高度(含小程序安全区),优先读 DOM / store */
|
|
/** 底部 tabBar 高度(含小程序安全区),优先读 DOM / store */
|
|
|
const resolveTabBarHeight = () => {
|
|
const resolveTabBarHeight = () => {
|
|
@@ -152,14 +178,29 @@ const resolveTabBarHeight = () => {
|
|
|
const tabBarHeight = ref(resolveTabBarHeight());
|
|
const tabBarHeight = ref(resolveTabBarHeight());
|
|
|
|
|
|
|
|
const currentCropName = ref(props.cropName || "");
|
|
const currentCropName = ref(props.cropName || "");
|
|
|
|
|
+/** 无农场时来自点位预警接口的 crop_name */
|
|
|
|
|
+const pointCropName = ref("");
|
|
|
|
|
+/** 按当前作物名匹配 type_icon,未命中用默认图 */
|
|
|
|
|
+const resolvedCropAvatar = computed(() => {
|
|
|
|
|
+ const name = String(currentCropName.value || "").trim();
|
|
|
|
|
+ if (name && CROP_ICON_MAP[name]) return CROP_ICON_MAP[name];
|
|
|
|
|
+ return props.cropAvatar || DEFAULT_CROP_AVATAR;
|
|
|
|
|
+});
|
|
|
const cropMenuVisible = ref(false);
|
|
const cropMenuVisible = ref(false);
|
|
|
const guideVisible = ref(false);
|
|
const guideVisible = ref(false);
|
|
|
|
|
|
|
|
/** 防止切换农场时旧请求回写覆盖新数据 */
|
|
/** 防止切换农场时旧请求回写覆盖新数据 */
|
|
|
let reloadToken = 0;
|
|
let reloadToken = 0;
|
|
|
|
|
+/** 有农场报告生成中:空数据轮询 */
|
|
|
|
|
+const reportGenerating = ref(false);
|
|
|
|
|
+let reportPollTimer = null;
|
|
|
|
|
+let reportPollStartedAt = 0;
|
|
|
|
|
|
|
|
-/** 展示名:优先当前分区 zone_name,其次 props / 农场缓存(新建农场尚无分区时用) */
|
|
|
|
|
|
|
+/** 展示名:有农场优先分区/农场缓存;无农场用点位接口 crop_name */
|
|
|
const resolveCropNameDisplay = () => {
|
|
const resolveCropNameDisplay = () => {
|
|
|
|
|
+ if (!resolveFarmId()) {
|
|
|
|
|
+ return pointCropName.value || "";
|
|
|
|
|
+ }
|
|
|
const zone = zoneList.value[activeIndex.value];
|
|
const zone = zoneList.value[activeIndex.value];
|
|
|
if (zone?.variety_name) return zone.variety_name;
|
|
if (zone?.variety_name) return zone.variety_name;
|
|
|
const farm = getSelectedFarm();
|
|
const farm = getSelectedFarm();
|
|
@@ -191,6 +232,16 @@ watch(
|
|
|
}
|
|
}
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
|
|
+/** 无农场:父页逆地理拿到 county_code 后再拉点位预警 */
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => props.countyCode,
|
|
|
|
|
+ (code, prev) => {
|
|
|
|
|
+ if (String(code ?? "") === String(prev ?? "")) return;
|
|
|
|
|
+ if (resolveFarmId()) return;
|
|
|
|
|
+ nextTick(() => fetchAlertCards());
|
|
|
|
|
+ }
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
const dismissGuide = () => {
|
|
const dismissGuide = () => {
|
|
|
if (!guideVisible.value) return;
|
|
if (!guideVisible.value) return;
|
|
|
guideVisible.value = false;
|
|
guideVisible.value = false;
|
|
@@ -198,6 +249,7 @@ const dismissGuide = () => {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const tryShowGuide = () => {
|
|
const tryShowGuide = () => {
|
|
|
|
|
+ if (!hasFarm.value) return;
|
|
|
if (localStorage.getItem(GUIDE_STORAGE_KEY)) return;
|
|
if (localStorage.getItem(GUIDE_STORAGE_KEY)) return;
|
|
|
guideVisible.value = true;
|
|
guideVisible.value = true;
|
|
|
};
|
|
};
|
|
@@ -340,6 +392,12 @@ const resolveFarmId = () => {
|
|
|
return id != null && id !== "" ? id : "";
|
|
return id != null && id !== "" ? id : "";
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+/** 依赖 farmId,无农场时隐藏切换作物 */
|
|
|
|
|
+const hasFarm = computed(() => {
|
|
|
|
|
+ void props.farmId;
|
|
|
|
|
+ return !!resolveFarmId();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
/** 保证有zone_id */
|
|
/** 保证有zone_id */
|
|
|
const resolveZoneId = () => {
|
|
const resolveZoneId = () => {
|
|
|
const farm = getSelectedFarm();
|
|
const farm = getSelectedFarm();
|
|
@@ -388,9 +446,39 @@ const fetchZones = async (token = reloadToken) => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+const stopReportPoll = () => {
|
|
|
|
|
+ if (reportPollTimer) {
|
|
|
|
|
+ clearTimeout(reportPollTimer);
|
|
|
|
|
+ reportPollTimer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ reportGenerating.value = false;
|
|
|
|
|
+ reportPollStartedAt = 0;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 空数据时启动/续期轮询;超时或 token 失效则停止 */
|
|
|
|
|
+const scheduleReportPoll = (token, queryParams) => {
|
|
|
|
|
+ if (token !== reloadToken) {
|
|
|
|
|
+ stopReportPoll();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!reportPollStartedAt) {
|
|
|
|
|
+ reportPollStartedAt = Date.now();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (Date.now() - reportPollStartedAt >= REPORT_POLL_MAX_MS) {
|
|
|
|
|
+ stopReportPoll();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ reportGenerating.value = true;
|
|
|
|
|
+ if (reportPollTimer) clearTimeout(reportPollTimer);
|
|
|
|
|
+ reportPollTimer = setTimeout(() => {
|
|
|
|
|
+ fetchAlertCards(token, { queryParams, fromPoll: true });
|
|
|
|
|
+ }, REPORT_POLL_INTERVAL_MS);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
/** 切换农场 / 回页:先清旧 zone,再拉分区,最后拉预警 */
|
|
/** 切换农场 / 回页:先清旧 zone,再拉分区,最后拉预警 */
|
|
|
const reloadFarmAlerts = async () => {
|
|
const reloadFarmAlerts = async () => {
|
|
|
const token = ++reloadToken;
|
|
const token = ++reloadToken;
|
|
|
|
|
+ stopReportPoll();
|
|
|
selectedZoneId.value = null;
|
|
selectedZoneId.value = null;
|
|
|
activeIndex.value = 0;
|
|
activeIndex.value = 0;
|
|
|
zoneList.value = [];
|
|
zoneList.value = [];
|
|
@@ -401,11 +489,17 @@ const reloadFarmAlerts = async () => {
|
|
|
await fetchAlertCards(token);
|
|
await fetchAlertCards(token);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-/** 接口可能返回对象或数组,统一取第一条 */
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 统一取第一条业务记录:
|
|
|
|
|
+ * - 点位接口:data.records[]
|
|
|
|
|
+ * - 农场接口:data 为对象或数组
|
|
|
|
|
+ */
|
|
|
const pickFirstRecord = (res) => {
|
|
const pickFirstRecord = (res) => {
|
|
|
const data = res?.data ?? res;
|
|
const data = res?.data ?? res;
|
|
|
|
|
+ if (!data) return null;
|
|
|
|
|
+ if (Array.isArray(data?.records)) return data.records[0] || null;
|
|
|
if (Array.isArray(data)) return data[0] || null;
|
|
if (Array.isArray(data)) return data[0] || null;
|
|
|
- if (data && typeof data === "object") return data;
|
|
|
|
|
|
|
+ if (typeof data === "object") return data;
|
|
|
return null;
|
|
return null;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -429,25 +523,78 @@ const applyWeatherStress = (record) => {
|
|
|
});
|
|
});
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const fetchAlertCards = async (token = reloadToken) => {
|
|
|
|
|
- const queryParams = getQueryParams();
|
|
|
|
|
- if (queryParams.farm_id == null || queryParams.farm_id === "") {
|
|
|
|
|
- if (token !== reloadToken) return;
|
|
|
|
|
|
|
+/** 无农场:与底图一致,用区县码查风险/胁迫 */
|
|
|
|
|
+const getCountyAlertParams = () => {
|
|
|
|
|
+ const code =
|
|
|
|
|
+ props.countyCode ||
|
|
|
|
|
+ localStorage.getItem(COUNTY_CODE_STORAGE_KEY) ||
|
|
|
|
|
+ "";
|
|
|
|
|
+ if (code == null || code === "") return null;
|
|
|
|
|
+ return { county_code: String(code) };
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * @param token 请求世代
|
|
|
|
|
+ * @param options.queryParams 轮询复用参数;options.fromPoll 是否来自空数据轮询
|
|
|
|
|
+ */
|
|
|
|
|
+const fetchAlertCards = async (token = reloadToken, options = {}) => {
|
|
|
|
|
+ const queryParams = options.queryParams || getQueryParams();
|
|
|
|
|
+ const hasFarm = queryParams.farm_id != null && queryParams.farm_id !== "";
|
|
|
|
|
+ const fromPoll = !!options.fromPoll;
|
|
|
|
|
+
|
|
|
|
|
+ let riskRes;
|
|
|
|
|
+ let stressRes;
|
|
|
|
|
+ if (hasFarm) {
|
|
|
|
|
+ pointCropName.value = "";
|
|
|
|
|
+ [riskRes, stressRes] = await Promise.allSettled([
|
|
|
|
|
+ VE_API.questionnaire.getWeatherRisk(queryParams),
|
|
|
|
|
+ VE_API.questionnaire.getWeatherStress(queryParams),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ stopReportPoll();
|
|
|
|
|
+ const countyParams = getCountyAlertParams();
|
|
|
|
|
+ if (!countyParams) {
|
|
|
|
|
+ if (token !== reloadToken) return;
|
|
|
|
|
+ applyWeatherRisk(null);
|
|
|
|
|
+ applyWeatherStress(null);
|
|
|
|
|
+ pointCropName.value = "";
|
|
|
|
|
+ syncCropNameDisplay();
|
|
|
|
|
+ nextTick(() => syncMidAnchor());
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ [riskRes, stressRes] = await Promise.allSettled([
|
|
|
|
|
+ VE_API.questionnaire.getPointRiskReport(countyParams),
|
|
|
|
|
+ VE_API.questionnaire.getPointStressReport(countyParams),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 切换农场期间若有更新请求,丢弃旧回包
|
|
|
|
|
+ if (token !== reloadToken) return;
|
|
|
|
|
+
|
|
|
|
|
+ const riskRecord = riskRes.status === "fulfilled" ? pickFirstRecord(riskRes.value) : null;
|
|
|
|
|
+ const stressRecord = stressRes.status === "fulfilled" ? pickFirstRecord(stressRes.value) : null;
|
|
|
|
|
+
|
|
|
|
|
+ // 有农场且双接口均为空:展示生成中并轮询(5s,最长 3 分钟)
|
|
|
|
|
+ if (hasFarm && !riskRecord && !stressRecord) {
|
|
|
applyWeatherRisk(null);
|
|
applyWeatherRisk(null);
|
|
|
applyWeatherStress(null);
|
|
applyWeatherStress(null);
|
|
|
|
|
+ scheduleReportPoll(token, queryParams);
|
|
|
nextTick(() => syncMidAnchor());
|
|
nextTick(() => syncMidAnchor());
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- const [riskRes, stressRes] = await Promise.allSettled([
|
|
|
|
|
- VE_API.questionnaire.getWeatherRisk(queryParams),
|
|
|
|
|
- VE_API.questionnaire.getWeatherStress(queryParams),
|
|
|
|
|
- ]);
|
|
|
|
|
- // 切换农场期间若有更新请求,丢弃旧回包
|
|
|
|
|
- if (token !== reloadToken) return;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (hasFarm || fromPoll) {
|
|
|
|
|
+ stopReportPoll();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 成功但无数据时清空旧卡片,避免切换农场后残留
|
|
// 成功但无数据时清空旧卡片,避免切换农场后残留
|
|
|
- applyWeatherRisk(riskRes.status === "fulfilled" ? pickFirstRecord(riskRes.value) : null);
|
|
|
|
|
- applyWeatherStress(stressRes.status === "fulfilled" ? pickFirstRecord(stressRes.value) : null);
|
|
|
|
|
|
|
+ applyWeatherRisk(riskRecord);
|
|
|
|
|
+ applyWeatherStress(stressRecord);
|
|
|
|
|
+
|
|
|
|
|
+ // 无农场:作物名取任一接口返回的 crop_name
|
|
|
|
|
+ if (!hasFarm) {
|
|
|
|
|
+ pointCropName.value = riskRecord?.crop_name || stressRecord?.crop_name || "";
|
|
|
|
|
+ syncCropNameDisplay();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
nextTick(() => syncMidAnchor());
|
|
nextTick(() => syncMidAnchor());
|
|
|
};
|
|
};
|
|
@@ -514,6 +661,7 @@ onActivated(() => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
onBeforeUnmount(() => {
|
|
|
|
|
+ stopReportPoll();
|
|
|
document.removeEventListener("click", handleOutsideClick, true);
|
|
document.removeEventListener("click", handleOutsideClick, true);
|
|
|
resizeObserver?.disconnect?.();
|
|
resizeObserver?.disconnect?.();
|
|
|
resizeObserver = null;
|
|
resizeObserver = null;
|
|
@@ -679,6 +827,20 @@ defineExpose({ refreshAlerts: fetchAlertCards, refreshZones: fetchZones, reloadF
|
|
|
gap: 10px;
|
|
gap: 10px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ .alert-generating {
|
|
|
|
|
+ min-height: 120px;
|
|
|
|
|
+ padding: 14px 12px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #86909c;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
.alert-card {
|
|
.alert-card {
|
|
|
padding: 12px;
|
|
padding: 12px;
|
|
|
border-radius: 12px;
|
|
border-radius: 12px;
|