You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

926 lines
26 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="order-detail" v-loading="loading">
<el-page-header :icon="ArrowLeft" :content="$t('common.back')" @back="goBack" class="page-header">
<template #content>
<span class="page-title">{{ $t('modal.detail') }}</span>
</template>
</el-page-header>
<el-card shadow="never" v-if="detail.id" class="detail-card">
<template #header>
<div class="card-header">
<span class="title">{{ detail.title }}</span>
<el-tag :type="priorityType(detail.priority)" size="small">{{ detail.priority }}</el-tag>
<el-tag :type="statusType(detail.status)" size="small">{{ detail.status }}</el-tag>
<el-button
v-if="detail.status !== '已完成'"
type="primary"
size="small"
style="margin-left:auto"
@click="openEdit"
>
{{ $t('btn.edit') }}
</el-button>
</div>
</template>
<el-descriptions :column="3" border>
<el-descriptions-item :label="$t('table.id')">{{ detail.orderNo }}</el-descriptions-item>
<el-descriptions-item :label="$t('table.hospital')">{{ detail.hospitalName }}</el-descriptions-item>
<el-descriptions-item :label="$t('table.type')">{{ detail.serviceType }}</el-descriptions-item>
<el-descriptions-item :label="$t('table.submitter')">{{ detail.submitter }}</el-descriptions-item>
<el-descriptions-item :label="$t('label.dept')">{{ detail.department }}</el-descriptions-item>
<el-descriptions-item :label="$t('table.time')">{{ detail.createdAt?.split('T').join(' ') }}</el-descriptions-item>
</el-descriptions>
<div class="section">
<div class="section-title">{{ $t('label.description') }}</div>
<div class="html-content" v-html="detail.description || '—'" />
</div>
<div class="section">
<div class="section-title">{{ $t('label.attachment') }}</div>
<el-empty v-if="!detail.attachments?.length" :description="$t('orderDetail.noAttachment')" :image-size="80" />
<div v-else class="files">
<el-button
v-for="(f, i) in detail.attachments"
:key="i"
link
type="primary"
class="file-item"
@click="viewReport(f.filePath, f.fileName)"
>
<el-icon><Document /></el-icon>{{ f.fileName }}
</el-button>
</div>
</div>
<div class="section">
<div class="section-title">{{ $t('orderDetail.statusChange') }}</div>
<el-timeline>
<el-timeline-item
v-for="(log, i) in detail.statusLogs || []"
:key="i"
:timestamp="log.createdAt"
placement="top"
>
<div class="log-title">{{ log.toStatus }} · {{ log.operator }}</div>
<div class="log-remark" v-html="log.remark"></div>
</el-timeline-item>
</el-timeline>
</div>
</el-card>
<!-- 编辑工单弹窗 -->
<el-dialog
v-model="editVisible"
:title="$t('modal.edit')"
width="640px"
:close-on-click-modal="false"
@closed="onEditClosed"
>
<el-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-width="100px"
v-loading="editLoading"
>
<el-form-item :label="$t('label.hospitalName')" prop="hospitalId">
<el-select
v-model="editForm.hospitalId"
:placeholder="$t('msg.pleaseSelectHospital')"
filterable
style="width: 100%"
>
<el-option
v-for="h in hospitals"
:key="h.id"
:label="h.name"
:value="h.id"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('label.title')" prop="title">
<div class="title-row">
<el-input
v-model="editForm.title"
:placeholder="$t('placeholder.title')"
maxlength="100"
show-word-limit
/>
<div class="color-tag-picker" ref="editColorPickerRef">
<div
class="color-tag-preview"
:style="{ background: editForm.colorTag || '#dcdfe6' }"
:class="{ 'is-empty': !editForm.colorTag }"
@click.stop="toggleEditColorPicker"
></div>
<div
v-show="editColorPickerVisible"
class="color-tag-dropdown"
@click.stop
>
<div class="color-tag-title">{{ $t("label.colorTag") }}</div>
<div class="color-tag-options">
<div
v-for="c in colorOptions"
:key="c.value"
class="color-tag-option"
:class="{ active: editForm.colorTag === c.value }"
:style="{ background: c.value }"
:title="c.label"
@click="selectEditColor(c.value)"
></div>
</div>
</div>
</div>
</div>
</el-form-item>
<el-form-item :label="$t('label.type')" prop="serviceType">
<el-select
v-model="editForm.serviceType"
:placeholder="$t('placeholder.type')"
clearable
>
<el-option label="故障问题" value="故障问题" />
<el-option label="使用咨询" value="使用咨询" />
<el-option label="功能需求" value="功能需求" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label-width="0">
<div class="inline-row">
<el-form-item :label="$t('label.priority')" prop="priority" class="inline-item">
<el-radio-group v-model="editForm.priority">
<el-radio-button label="高">高</el-radio-button>
<el-radio-button label="中">中</el-radio-button>
<el-radio-button label="低">低</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('label.feedbackchannel')" prop="feedbackchannel" class="inline-item">
<el-select
v-model="editForm.feedbackchannel"
:placeholder="$t('placeholder.feedbackchannel')"
clearable
style="width: 160px"
>
<el-option label="微信群" value="微信群" />
<el-option label="微信" value="微信" />
<el-option label="电话" value="电话" />
<el-option label="现场" value="现场" />
<el-option label="钉钉群" value="钉钉群" />
<el-option label="钉钉" value="钉钉" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
</div>
</el-form-item>
<el-form-item :label="$t('label.dept')" prop="department">
<el-input
v-model="editForm.department"
:placeholder="$t('placeholder.dept')"
/>
</el-form-item>
<el-form-item :label="$t('label.name')" prop="submitter">
<el-input
v-model="editForm.submitter"
:placeholder="$t('placeholder.name')"
/>
</el-form-item>
<el-form-item :label="$t('label.createat')" prop="createdat">
<el-date-picker
v-model="editForm.createdat"
type="datetime"
:placeholder="$t('placeholder.createat')"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
<el-form-item :label="$t('label.description')" prop="description">
<div class="rich-editor">
<Toolbar
:editor="editorRef"
:default-config="toolbarConfig"
mode="default"
style="border-bottom: 1px solid #dcdfe6"
/>
<Editor
v-model="editForm.description"
:default-config="editorConfig"
mode="default"
style="height: 200px; overflow-y: auto"
@on-created="handleEditorCreated"
@custom-paste="handleEditorPasteImage"
/>
</div>
</el-form-item>
<el-form-item :label="$t('label.attachment')">
<el-upload
v-model:file-list="editForm.files"
:auto-upload="false"
:limit="1"
:on-exceed="handleAttachmentExceed"
:on-change="handleAttachmentChange"
:on-remove="handleAttachmentRemove"
>
<el-button type="primary" plain>{{ $t("btn.upload") }}</el-button>
<template #tip>
<div class="el-upload__tip">
{{ $t("label.attachmentTip") }}
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editVisible = false">{{ $t("common.cancel") }}</el-button>
<el-button
type="primary"
:loading="editSubmitting"
@click="submitEdit"
>{{ $t("common.save") }}</el-button>
</template>
</el-dialog>
<el-dialog
v-model="previewVisible"
:title="$t('btn.preview')"
width="80%"
top="5vh"
destroy-on-close
:before-close="closePreview"
>
<iframe
v-if="previewType === 'pdf' && previewUrl"
:src="previewUrl"
class="preview-iframe"
/>
<img
v-else-if="previewType === 'image' && previewUrl"
:src="previewUrl"
class="preview-image"
/>
</el-dialog>
</div>
</template>
<script setup>
import dayjs from "dayjs";
import {
nextTick,
onBeforeUnmount,
onMounted,
reactive,
ref,
shallowRef,
} from "vue";
import { useRoute, useRouter } from "vue-router";
import { ArrowLeft, Document } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import "@wangeditor/editor/dist/css/style.css";
import { useI18n } from "vue-i18n";
import {
getAdminOrderDetail,
getAdminHospitals,
getUploadFile,
updateAdminOrder,
} from "@/service/modular/admin";
const { t: $t } = useI18n();
const route = useRoute();
const router = useRouter();
const loading = ref(false);
const detail = ref({});
const priorityType = (p) => (p === "高" ? "danger" : p === "中" ? "warning" : "info");
const statusType = (s) =>
({ 待处理: "warning", 处理中: "primary", 已完成: "success", 已取消: "info" })[s] || "info";
const loadDetail = async () => {
loading.value = true;
try {
detail.value = await getAdminOrderDetail(route.params.id);
} finally {
loading.value = false;
}
};
const goBack = () => router.back();
const previewVisible = ref(false);
const previewUrl = ref("");
const previewType = ref(""); // 'pdf' | 'image' | ''
const cleanupPreviewUrl = () => {
if (previewUrl.value) {
URL.revokeObjectURL(previewUrl.value);
previewUrl.value = "";
}
previewType.value = "";
};
const closePreview = () => {
previewVisible.value = false;
cleanupPreviewUrl();
};
// 根据文件名/扩展名+blob 的 mime 判断文件类型
const getFileType = (fileName, blob) => {
const name = String(fileName || "");
const ext = name.split(".").pop()?.toLowerCase() || "";
const mime = blob?.type || "";
if (ext === "pdf" || mime === "application/pdf") return "pdf";
if (
["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(ext) ||
(mime && mime.startsWith("image/"))
) {
return "image";
}
return "other";
};
// 浏览器直接下载 blob
const downloadBlob = (blob, fileName) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName || "download";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 0);
};
const viewReport = async (attachmentPath, originalName) => {
if (!attachmentPath) return;
const path = String(attachmentPath);
const downloadName = originalName || path;
try {
const blob = await getUploadFile(path);
const type = getFileType(downloadName, blob);
if (type === "pdf" || type === "image") {
cleanupPreviewUrl();
previewUrl.value = URL.createObjectURL(blob);
previewType.value = type;
previewVisible.value = true;
} else {
downloadBlob(blob, downloadName);
}
} catch (e) {
ElMessage.error(e?.message || $t('msg.failed'));
}
};
// ============== 编辑工单 ==============
const hospitals = ref([]);
// 颜色标签可选值(颜色 Hex 取自设计原型)
const colorOptions = [
{ label: "蓝", value: "#667eea" },
{ label: "绿", value: "#52c41a" },
{ label: "青", value: "#13c2c2" },
{ label: "紫", value: "#722ed1" },
{ label: "橙", value: "#fa8c16" },
{ label: "红", value: "#ff4d4f" },
];
const loadHospitals = async () => {
try {
const res = await getAdminHospitals({ page: 1, pageSize: 1000 });
hospitals.value = res?.list || [];
} catch (_) {
hospitals.value = [];
}
};
const editVisible = ref(false);
const editLoading = ref(false);
const editSubmitting = ref(false);
const editFormRef = ref();
// 已加载的原始附件 filePath 集合(用于判断用户是否需要保留原附件)
const editOriginalAttachments = ref([]);
// 颜色选择器
const editColorPickerVisible = ref(false);
const editColorPickerRef = ref();
const toggleEditColorPicker = () => {
editColorPickerVisible.value = !editColorPickerVisible.value;
};
const selectEditColor = (val) => {
editForm.colorTag = val;
editColorPickerVisible.value = false;
};
const onDocClick = (e) => {
if (!editColorPickerVisible.value) return;
const el = editColorPickerRef.value;
if (el && !el.contains(e.target)) {
editColorPickerVisible.value = false;
}
};
const editForm = reactive({
hospitalId: undefined,
title: "",
serviceType: "",
priority: "中",
feedbackchannel: "",
department: "",
colorTag: "#667eea",
description: "",
submitter: "",
createdat: "",
files: [],
});
const MAX_FILE_SIZE = 50 * 1024 * 1024;
const MAX_PASTE_IMAGE_SIZE = 5 * 1024 * 1024;
// el-upload 事件:选文件后立即校验大小
const handleAttachmentChange = (file) => {
if (file?.size > MAX_FILE_SIZE) {
ElMessage.warning("文件不能超过 50MB");
editForm.files = editForm.files.filter(
(f) => f.uid !== file.uid || f.size <= MAX_FILE_SIZE,
);
}
};
const handleAttachmentExceed = () => {
ElMessage.warning("只能上传 1 个文件");
};
const handleAttachmentRemove = () => {
// el-upload 会自动从 v-model:file-list 移除
};
const editRules = {
hospitalId: [
{
required: true,
message: () => $t("msg.pleaseSelectHospital"),
trigger: "change",
},
],
title: [
{
required: true,
message: () => $t("msg.pleaseInputTitle"),
trigger: "blur",
},
],
serviceType: [
{
required: true,
message: () => $t("msg.pleaseSelectType"),
trigger: "blur",
},
],
priority: [
{
required: true,
message: () => $t("msg.pleaseSelectPriority"),
trigger: "change",
},
],
feedbackchannel: [
{
required: true,
message: () => $t("msg.pleaseSelectFeedbackChannel"),
trigger: "change",
},
],
department: [
{
required: true,
whitespace: true,
message: () => $t("msg.pleaseInputDept"),
trigger: "blur",
},
],
submitter: [
{
required: true,
whitespace: true,
message: () => $t("msg.pleaseInputName"),
trigger: "blur",
},
],
description: [
{
required: true,
validator: (_, value, callback) => {
const hasImage = /<img\s/i.test(value || "");
const text = (value || "")
.replace(/<[^>]*>/g, "")
.replace(/&nbsp;/gi, "")
.trim();
if (!text && !hasImage)
return callback(new Error($t("msg.pleaseInputDescription")));
callback();
},
trigger: "change",
},
],
};
// 富文本编辑器配置
const editorRef = shallowRef();
const toolbarConfig = {
excludeKeys: [
"uploadImage",
"insertVideo",
"uploadVideo",
"group-video",
"emotion",
"group-emoji",
],
};
// 从剪贴板中捞出图片文件(同时兼容 items 和 files 两种来源)
const collectPastedImages = (clipboardData) => {
const files = [];
const items = clipboardData?.items;
if (items) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === "file" && item.type?.startsWith("image/")) {
const file = item.getAsFile();
if (file) files.push(file);
}
}
}
if (files.length === 0 && clipboardData?.files) {
for (let i = 0; i < clipboardData.files.length; i++) {
const file = clipboardData.files[i];
if (file.type?.startsWith("image/")) files.push(file);
}
}
return files;
};
const handleEditorPasteImage = (editor, event) => {
if (event.__wpsImageHandled) return;
const imageFiles = collectPastedImages(event.clipboardData);
if (imageFiles.length === 0) return;
event.__wpsImageHandled = true;
event.preventDefault();
if (typeof event.stopPropagation === "function") event.stopPropagation();
if (typeof event.stopImmediatePropagation === "function")
event.stopImmediatePropagation();
if (typeof editor.focus === "function") editor.focus();
const savedSelection = editor.selection;
imageFiles.forEach((file) => {
if (file.size > MAX_PASTE_IMAGE_SIZE) {
ElMessage.warning("粘贴的图片不能超过 5MB");
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const dataUrl = e.target?.result;
if (!dataUrl) {
ElMessage.error("图片读取失败");
return;
}
try {
if (savedSelection) editor.select(savedSelection);
} catch (_) {
if (typeof editor.focus === "function") editor.focus();
}
if (typeof editor.focus === "function") editor.focus();
editor.dangerouslyInsertHtml(
`<img src="${dataUrl}" style="max-width:100%;height:auto;" />`,
);
if (typeof editor.updateView === "function") editor.updateView();
};
reader.onerror = () => {
ElMessage.error("图片处理失败");
};
reader.readAsDataURL(file);
});
return false;
};
const editorConfig = {
placeholder: $t("placeholder.description"),
};
let pasteHandlerRef = null;
const handleEditorCreated = (editor) => {
editorRef.value = editor;
nextTick(() => {
const root = typeof editor.$el === "function" ? editor.$el() : editor.$el;
if (!root) return;
const editable = root.querySelector?.('[contenteditable="true"]') || root;
if (pasteHandlerRef) {
editable.removeEventListener("paste", pasteHandlerRef, { capture: true });
}
pasteHandlerRef = (event) => handleEditorPasteImage(editor, event);
editable.addEventListener("paste", pasteHandlerRef, { capture: true });
});
};
// 从带路径分隔符的地址中截取最后的文件名
const getAttachmentFileName = (path) => {
if (!path) return "";
return String(path).replace(/\\/g, "/").split("/").pop() || "";
};
const resetEditForm = () => {
Object.assign(editForm, {
hospitalId: undefined,
title: "",
serviceType: "",
priority: "中",
feedbackchannel: "",
department: "",
colorTag: "#667eea",
description: "",
submitter: "",
createdat: "",
files: [],
});
editOriginalAttachments.value = [];
editFormRef.value?.clearValidate();
};
const openEdit = () => {
resetEditForm();
editLoading.value = true;
editVisible.value = true;
// 延迟一帧等弹窗挂载,避免富文本在未挂载时设置 v-model 报错
nextTick(async () => {
const d = detail.value || {};
editForm.hospitalId = hospitals.value.find((h) => h.name === d.hospitalName)?.id || undefined;
editForm.title = d.title || "";
editForm.serviceType = d.serviceType || "";
editForm.priority = d.priority || "中";
editForm.feedbackchannel = d.feedbackChannel || d.feedbackchannel || "";
editForm.department = d.department || "";
editForm.colorTag = d.colorTag || "#667eea";
editForm.description = d.description || "";
editForm.submitter = d.submitter || "";
// 后端返回时间可能含 T统一格式化为 YYYY-MM-DD HH:mm:ss 供 date-picker 回显
editForm.createdat = d.createdAt
? dayjs(d.createdAt).format("YYYY-MM-DD HH:mm:ss")
: "";
// 附件回显:把详情里的第一个附件作为 el-upload 的"已上传"项展示
const atts = d.attachments || [];
editOriginalAttachments.value = atts.map((a) => a.filePath).filter(Boolean);
if (atts.length) {
const first = atts[0];
const fileName = first.fileName || getAttachmentFileName(first.filePath);
if (fileName) {
editForm.files = [
{
uid: `attachment-${route.params.id}-0`,
name: fileName,
status: "success",
},
];
}
}
editLoading.value = false;
});
};
const onEditClosed = () => {
resetEditForm();
};
const submitEdit = async () => {
editForm.department = (editForm.department || "").trim();
editForm.submitter = (editForm.submitter || "").trim();
await editFormRef.value.validate(async (valid) => {
if (!valid) return;
editSubmitting.value = true;
try {
const formData = new FormData();
const { files, ...rest } = editForm;
for (const [k, v] of Object.entries(rest)) {
if (v !== undefined && v !== null && v !== "") {
formData.append(k, v);
}
}
// 仅在用户上传了新文件时才把 files 追加到 formData
// 这样后端看到 files 就视为替换附件,未带 files 则保留原附件
const item = (files || [])[0];
if (item) {
const file = item.raw || (typeof item.size === "number" ? item : null);
if (file) formData.append("files", file, item.name);
}
await updateAdminOrder(route.params.id, formData);
ElMessage.success($t("msg.editSuccess"));
editVisible.value = false;
loadDetail();
} finally {
editSubmitting.value = false;
}
});
};
const removePasteListener = () => {
const editor = editorRef.value;
if (!editor || !pasteHandlerRef) return;
const root = typeof editor.$el === "function" ? editor.$el() : editor.$el;
if (!root) return;
const editable = root.querySelector?.('[contenteditable="true"]') || root;
editable.removeEventListener("paste", pasteHandlerRef, { capture: true });
};
onMounted(() => {
loadDetail();
loadHospitals();
document.addEventListener("click", onDocClick);
});
onBeforeUnmount(() => {
document.removeEventListener("click", onDocClick);
removePasteListener();
pasteHandlerRef = null;
const editor = editorRef.value;
if (editor == null) return;
editor.destroy();
});
</script>
<style scoped lang="scss">
.page-header {
margin-bottom: 12px;
.page-title {
font-size: 16px;
font-weight: 600;
}
}
.detail-card {
.card-header {
display: flex;
align-items: center;
gap: 8px;
.title {
font-size: 16px;
font-weight: 600;
margin-right: 8px;
}
}
.section {
margin-top: 20px;
.section-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 10px;
}
}
.html-content {
background: #fafafa;
padding: 12px;
border-radius: 4px;
min-height: 60px;
line-height: 1.6;
}
.files {
display: flex;
flex-direction: column;
gap: 8px;
.file-item {
display: inline-flex;
align-items: center;
gap: 4px;
}
}
.log-title {
font-weight: 600;
}
.log-remark {
color: #606266;
margin-top: 4px;
}
}
.preview-iframe {
width: 100%;
height: 80vh;
border: 0;
}
.preview-image {
display: block;
max-width: 100%;
max-height: 80vh;
margin: 0 auto;
object-fit: contain;
}
.rich-editor {
width: 100%;
background: #fff;
border-radius: 4px;
// box-shadow wangEditor border
// box-shadow: 0 0 0 1px #dcdfe6 inset;
border: 1px solid #dcdfe6;
overflow: hidden;
}
.rich-editor :deep(.w-e-toolbar) {
background: #fafafa;
border-bottom: 1px solid #dcdfe6;
}
.rich-editor :deep(.w-e-text-container) {
background: #fff;
}
// +
.title-row {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.title-row .el-input {
flex: 1;
}
// +
.inline-row {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 0 24px;
width: 100%;
}
.inline-row .inline-item {
flex: 1 1 0;
min-width: 0;
margin-bottom: 0 !important;
}
.inline-row .inline-item :deep(.el-form-item__content) {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.color-tag-picker {
position: relative;
flex-shrink: 0;
}
.color-tag-preview {
width: 28px;
height: 28px;
border-radius: 6px;
cursor: pointer;
border: 2px solid #fff;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.color-tag-preview:hover {
transform: scale(1.08);
}
.color-tag-preview.is-empty {
background-image: linear-gradient(
135deg,
#f5f5f5 25%,
transparent 25%,
transparent 50%,
#f5f5f5 50%,
#f5f5f5 75%,
transparent 75%
);
background-size: 8px 8px;
box-shadow: 0 0 0 1px #dcdfe6;
}
.color-tag-dropdown {
position: absolute;
top: calc(100% + 8px);
left: 0;
z-index: 10;
background: #fff;
border-radius: 10px;
padding: 12px 14px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
min-width: 180px;
}
.color-tag-title {
font-size: 13px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.color-tag-options {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.color-tag-option {
width: 24px;
height: 24px;
border-radius: 6px;
cursor: pointer;
border: 2px solid transparent;
transition:
transform 0.2s ease,
border-color 0.2s ease;
}
.color-tag-option:hover {
transform: scale(1.12);
}
.color-tag-option.active {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
</style>