You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ocr-web/src/views/home/content/Content.vue

1026 lines
27 KiB

<script lang="ts" setup>
import {
createPackage,
getCheckDuplicateStatus,
getLastCheckNo,
getPictureList,
oneClickCheckTaskPackage,
queryPageListByCheckNo,
removeCheckDuplicate,
} from "@/api/home/main";
import avatar from "@/assets/images/avatar.jpg";
import { timeOptions, viewOptions } from "@/config/home";
import { useWindowSizeFn } from "@/hooks/event/useWindowSizeFn";
import { useConfig } from "@/store/modules/asideConfig";
import { getViewportOffset } from "@/utils/domUtils";
import { hideDownload } from "@/utils/image";
import emitter from "@/utils/mitt";
import { getImgUrl } from "@/utils/urlUtils";
import { EllipsisHorizontal, EyeOutline as EyeOutlineIcon } from "@vicons/ionicons5";
import { Download as DownloadIcon, Upload as UploadIcon } from "@vicons/tabler";
import { Icon } from "@vicons/utils";
import { useInfiniteScroll } from "@vueuse/core";
import dayjs from "dayjs";
import imagesloaded from "imagesloaded";
import { cloneDeep, debounce } from "lodash-es";
import Masonry from "masonry-layout";
import { NIcon, useMessage } from "naive-ui";
import type { Component } from "vue";
import {
computed,
h,
nextTick,
onMounted,
onUnmounted,
onUpdated,
reactive,
ref,
unref,
watch,
} from "vue";
import CheckingTaskModal from "./modal/CheckingTaskModal.vue";
import FinishPackageModal from "./modal/FinishPackageModal.vue";
import GeneratePackageModal from "./modal/GeneratePackageModal.vue";
import LoginSuccessModal from "./modal/LoginSuccessModal.vue";
import PackageSettingsModal from "./modal/PackageSettingsModal.vue";
import QueryRepeatedTasksModal from "./modal/QueryRepeatedTasksModal.vue";
import type { PictureSortParam } from "/#/api";
import defaultAvatar from "@/assets/icons/avatar.svg";
import baseImg from "@/assets/images/baseImg.png";
import axios from "axios";
const deviceHeight = ref(600);
let _masonry: null | Masonry = null;
let _imagesload: any;
const masonryRef = ref<ComponentRef>(null);
const el = ref<HTMLDivElement | null>(null);
const viewMode = ref("masonry");
const pagination = reactive({
pageNo: 0,
pageSize: 30,
});
const configStore = useConfig();
const packageModalRef = ref(null);
const generateModalRef = ref(null);
const queryRepeatedTasksModalRef = ref(null);
const LoginSuccessModalRef = ref(null);
const checkingTaskModalRef = ref(null);
const finishPackageModal = ref(null);
const loading = ref(false);
const message = useMessage();
const totalCount = ref(0);
const sortBy: PictureSortParam = {
orderbyname: "desc",
orderbyvalue: "pictureResult",
};
const imageRef = ref<ComponentElRef | null>();
const checkDuplicateNo = ref("");
const checkTaskStatus = ref(null); // 1.执行中 2.执行完毕 3.执行失败
const isRefresh = ref(true); // 生成任务包前,点击刷新数据,并将【一键查重】切换按钮为【生成任务包】
let canloadMore = true;
let filterId = null;
async function computeListHeight() {
const headEl = document.querySelector(".wrapper-content")!;
const { bottomIncludeBody } = getViewportOffset(headEl);
const height = bottomIncludeBody;
deviceHeight.value = height - 40 - 16 - 24;
}
useWindowSizeFn(computeListHeight);
const listStyle = computed(() => {
return {
height: `${deviceHeight.value}px`,
};
});
const layout = debounce(() => {
if (masonryRef.value == null || el.value == null) return;
if (_masonry !== null) (_masonry as any).addItems();
_masonry = new Masonry(masonryRef.value as any, {
itemSelector: ".grid-item",
gutter: 17,
columnWidth: 182,
percentPosition: true,
stagger: 10,
});
_imagesload = imagesloaded(".grid-item");
_imagesload.on("done", (instance) => {
(_masonry as any).layout();
if (!el.value) return;
loading.value = false;
});
_imagesload.on("fail", (instance) => {
message.error("图片错误");
loading.value = false;
});
}, 300);
useInfiniteScroll(
el as any,
() => {
loadMore();
},
{ distance: 10, canLoadMore: () => canloadMore }
);
onUpdated(() => {
layout();
});
const timeRange = ref("");
const timeLabel = computed(() => {
const item = timeOptions.find((option) => {
return option.value === timeRange.value;
});
return item?.label;
});
const viewLabel = computed(() => {
const item = viewOptions.find((option) => {
return option.value === viewMode.value;
});
return item?.label;
});
let isAllowDownload = ref(true);
let calNum = ref(0);
const searchValue = ref("");
const isInitSeaerch = ref(false); // 是否初始化搜索
configStore.$subscribe(() => {
console.log("subscribe", "configStore");
isAllowDownload.value = configStore.isAllowDownload;
calNum.value = configStore.getTimeNum;
// console.log("calNum.value----------", calNum.value);
searchValue.value = configStore.getSearchValue;
console.log(configStore.getSearchValue, "getSearchValue");
});
watch(
() => searchValue.value,
async (newVal, oldVal) => {
if (newVal) {
isInitSeaerch.value = true;
pagination.pageNo = 0;
const more = await featchList();
listData.value = more;
isInitSeaerch.value = false;
// configStore.setSearchValue("");
} else {
isInitSeaerch.value = true;
pagination.pageNo = 0;
const more = await featchList();
listData.value = more;
isInitSeaerch.value = false;
}
}
);
const listData = ref<any[]>([]);
async function featchList(userSearchId?: string) {
loading.value = true;
try {
const contentParams = {
search_month: timeRange.value,
search_history: 0,
userSearchId,
};
pagination.pageNo += 1;
const searchValue = configStore.getSearchValue; // rao
const asideParams = unref(configStore.getAsideValue);
const params = filterId ? { userSearchId: filterId } : cloneDeep(asideParams);
let result = {
pageCount: 0,
data: [],
total: 0,
};
let sortObj: any = {}; // rao start
if (sortBy.orderbyvalue == "pictureResult") {
sortObj.ordertype = sortBy.orderbyname;
} else if (sortBy.orderbyvalue == "fromuptime") {
sortObj.orderByTime = sortBy.orderbyname;
}
if (params["izsimilarity"] && typeof params["izsimilarity"] != "string") {
params["izsimilarity"] = params["izsimilarity"].join("-");
}
// rao end
if (checkTaskStatus.value === 2 && isRefresh) {
result = await queryPageListByCheckNo({
...pagination,
...contentParams,
...params,
...sortObj,
checkDuplicateNo: checkDuplicateNo.value,
upUserName: searchValue,
});
} else {
result = await getPictureList({
...pagination,
...contentParams,
...params,
...sortObj,
upUserName: searchValue,
});
}
const { data, pageCount, total } = result;
totalCount.value = total;
canloadMore = pageCount >= pagination.pageNo && pageCount > 0;
const list = data.map((item) => {
return {
imgUrl: item.imgurl,
thumburl: item.serverThumbnailUrl || item.imgurl,
upname: item.upname,
ocrPictureclass: item.ocrPictureclass,
uphead: item.uphead,
similar: item.similarityscore || -1,
imgName: item.imgname,
loadOver: false,
};
});
return list;
} catch (error) {
canloadMore = false;
return [];
}
}
async function loadMore() {
if (loading.value || el.value == null) return;
const more = await featchList();
// if(isInitSeaerch.value) {
// listData.value = [];
// isInitSeaerch.value = false;
// }
listData.value.push(...more);
}
const gridHeight = computed(() => {
let height = "";
if (viewMode.value === "masonry") {
height = "";
} else if (viewMode.value === "horizontalVersion") {
height = "145px";
} else if (viewMode.value === "verticalVersion") {
height = "300px";
} else if (viewMode.value === "3:4") {
height = "240px";
}
return height;
});
async function oneCheck() {
const asideVal = cloneDeep(configStore.getAsideValue);
asideVal.upUserName = searchValue.value;
console.log("searchValue", asideVal, searchValue.value);
if (asideVal.izyear && asideVal.izyear.length == 2) {
asideVal.izyear =
dayjs(asideVal.izyear[0]).format("YYYY/MM/DD") +
"-" +
dayjs(asideVal.izyear[1]).format("YYYY/MM/DD");
}
if (asideVal["izsimilarity"] && typeof asideVal["izsimilarity"] != "string") {
asideVal["izsimilarity"] = asideVal["izsimilarity"].join("-");
}
const tasksLoadingModal = queryRepeatedTasksModalRef.value as any;
console.log("calNum.value111111111111111", calNum.value, checkTaskStatus.value);
if (calNum.value == 0 && isRefresh.value) {
if (timer.value) {
clearInterval(timer.value);
}
timer.value = setInterval(() => {
console.log("calNum.value2222222222222", calNum.value, checkTaskStatus.value);
if (checkDuplicateNo.value) {
getCheckDuplicateStatus(checkDuplicateNo.value).then((res) => {
if (res.code === "OK") {
checkTaskStatus.value = res.data.status;
if (calNum.value < 90) {
calNum.value = calNum.value + 10;
}
configStore.setTimeNum(calNum.value);
if (checkTaskStatus.value === 2 || checkTaskStatus.value === 3) {
if (checkTaskStatus.value === 2) {
message.success("任务执行完毕,正在刷新数据...");
} else {
message.error("查询异常");
}
tasksLoadingModal.closeOnlyModal();
configStore.setTimeNum(100);
if (timer.value) {
clearInterval(timer.value);
}
setTimeout(() => {
configStore.setTimeNum(0);
}, 1000);
reset();
loadMore();
}
} else {
if (timer.value) {
clearInterval(timer.value);
}
}
});
}
}, 1000);
}
// 查重任务编号,状态不为空,或者状态执行中..
if (checkDuplicateNo.value && checkTaskStatus.value && checkTaskStatus.value === 1) {
// 暂时rao
tasksLoadingModal.showModal();
return;
}
//调用查重任务开启流程
oneClickCheckTaskPackage(asideVal).then((res) => {
if (res.code === "OK") {
checkDuplicateNo.value = res.data.checkDuplicateNo;
checkTaskStatus.value = res.data.status;
tasksLoadingModal.showModal(); // 暂时rao
} else {
message.error(res.message || "查重失败");
}
});
}
/**
* 显示添加任务包
*/
async function showAddPackage() {
const modal = packageModalRef.value as any;
modal.showModal();
}
//查重任务加载提示框,关闭回调
async function tasksLoadingCloseCallback() {
const checkingTaskModal = checkingTaskModalRef.value as any;
checkingTaskModal.showModal();
if (isRefresh.value) {
refresh(true);
}
}
async function showLoginSuccessModal() {
const modal = LoginSuccessModalRef.value as any;
// modal.showModal()
}
// 包id
const packageIdRef = ref("");
async function commitHandler(settingParam) {
const params = {
name: settingParam.packagename,
checkDuplicateNo: checkDuplicateNo.value,
};
const modal = generateModalRef.value as any;
const finishModal = finishPackageModal.value as any;
modal.showModal();
createPackage(params).then((res) => {
if (res.code === "OK") {
message.success(res.message);
packageIdRef.value = res.data.id;
modal.closeModal();
finishModal.showModal();
//清空查重任务状态和编号
checkDuplicateNo.value = "";
checkTaskStatus.value = null;
}
});
const asideVal = configStore.getAsideValue;
const finalParam = { ...asideVal };
finalParam.buessinessno = settingParam.packagename;
finalParam.search_history = settingParam.comparehistory ? 1 : 0;
}
onMounted(() => {
emitter.on("filter", refreshHandler);
// emitter.on("filter", (searchId)=>{
// console.log("emitter on filter" + searchId)
// reset();
// featchList(searchId);
// });
// 一件键重——获取任务编号
getLastCheckNo().then((res) => {
if (res.code === "OK") {
checkDuplicateNo.value = res.data;
}
});
nextTick(() => {
computeListHeight();
// 登录后展示成功
// showLoginSuccessModal()
});
});
onUnmounted(() => {
emitter.off("filter", refreshHandler);
});
watch(timeRange, () => {
refreshHandler();
});
watch(
() => configStore.asideValue,
(newVal, oldVal) => {
refreshHandler();
},
{ deep: true }
);
function reset() {
pagination.pageNo = 0;
pagination.pageSize = 30;
listData.value.length = 0;
loading.value = false;
canloadMore = true;
filterId = null;
layout();
}
async function refreshHandler(filtersearchId?: any) {
reset();
if (filtersearchId) filterId = filtersearchId;
nextTick(() => {
setTimeout(() => {
useInfiniteScroll(
el as any,
() => {
loadMore();
},
{ distance: 10, canLoadMore: () => canloadMore }
);
}, 300);
});
}
function getAvatar(url: string): string {
return url ? getImgUrl(url) : avatar;
}
function sortHandler(orderby: "pictureResult" | "fromuptime") {
sortBy.orderbyvalue = orderby;
sortBy.orderbyname = sortBy.orderbyname === "asc" ? "desc" : "asc";
refreshHandler();
}
async function downloadImage(item) {
if (!item.imgUrl) {
message.error("请输入有效的图片链接地址");
return;
}
try {
// 使用 fetch 发送 GET 请求获取图片
const response = await fetch(item.imgUrl, {
method: "GET",
mode: "cors", // 确保跨域请求被正确处理
cache: "default",
});
// 检查响应状态码,确保请求成功
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// 读取 Blob 数据
const blob = await response.blob();
// 创建一个新的a标签
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = item.imgName; // 设置下载的文件名
link.style.display = "none"; // 隐藏链接
// 将链接添加到 DOM 中
document.body.appendChild(link);
// 触发下载
link.click();
// 清理
URL.revokeObjectURL(link.href);
document.body.removeChild(link);
} catch (error) {
console.error("下载图片时发生错误:", error);
}
}
function previewHandler(index: number, event: MouseEvent) {
event.stopImmediatePropagation();
event.stopPropagation();
if (imageRef.value?.[index] && (imageRef.value[index] as any).src)
// (imageRef.value?.[index] as any).mergedOnClick();
(imageRef.value?.[index] as any).click();
}
const timer = ref();
/**
* 检查查重状态
*/
function refresh(val?: any) {
// delete asideVal.izsimilarity
const checkingTaskModal = checkingTaskModalRef.value as any;
if (checkDuplicateNo.value) {
getCheckDuplicateStatus(checkDuplicateNo.value).then((res) => {
if (res.code === "OK") {
checkTaskStatus.value = res.data.status; // 1.执行中 2.执行完毕
// if (isRefresh.value === false) {
// const modal = packageModalRef.value as any
// modal.showModal()
// return
// }
if (
(checkTaskStatus.value === 2 || checkTaskStatus.value === 3) &&
isRefresh.value
) {
configStore.setTimeNum(100);
checkingTaskModal.closeModal();
isRefresh.value = false;
if (checkTaskStatus.value === 2) {
message.success("任务执行完毕,正在刷新数据...");
} else {
message.error("查询异常");
}
if (timer.value) {
clearInterval(timer.value);
}
reset();
loadMore();
configStore.setTimeNum(0);
} else if (checkTaskStatus.value === 1) {
return;
}
}
});
return;
}
}
/**
* 取消查重任务
*/
function cancel(val) {
if (checkTaskStatus.value === 1) {
removeCheckDuplicate(checkDuplicateNo.value).then((res) => {
if (res.code === "OK") {
checkDuplicateNo.value = "";
checkTaskStatus.value = null;
message.success("查重任务取消成功");
}
});
}
}
const renderIcon = (icon: Component) => {
return () => {
return h(NIcon, null, {
default: () => h(icon),
});
};
};
const dropdownOptions = ref([
{
label: "导入任务数据",
key: "profile",
icon: renderIcon(UploadIcon),
},
{
label: "导出任务数据",
key: "editProfile",
icon: renderIcon(DownloadIcon),
},
{
label: "查看导入记录",
key: "logout",
icon: renderIcon(EyeOutlineIcon),
},
]);
const loadImgOver = (item) => {
console.log("loadImgOver", item);
setTimeout(() => (item.loadOver = true), 2000);
};
</script>
<template>
<div class="wrapper">
<div class="wrapper-header">
<div class="left">
<SvgIcon size="32" name="magnifying" />
<span class="font">AI一键查重</span>
</div>
<div class="flex-btn-icon">
<SvgIcon
v-show="checkTaskStatus !== 2"
style="cursor: pointer"
size="105"
name="yijianchachong"
@click="oneCheck"
/>
<SvgIcon
v-show="checkTaskStatus === 2"
style="cursor: pointer"
size="105"
name="shengchengrenwubao"
@click="showAddPackage"
/>
<n-dropdown trigger="hover" placement="bottom-start" :options="dropdownOptions">
<span class="expand-icon">
<Icon style="position: relative; top: 2px">
<EllipsisHorizontal style="color: #507afd" />
</Icon>
</span>
</n-dropdown>
</div>
</div>
<div class="wrapper-content">
<div style="display: flex; justify-content: space-between">
<div class="form">
<!-- <n-popselect v-model:value="timeRange" :options="timeOptions" trigger="click">
<div class="dropdown">
<span>{{ timeLabel || '请选择' }}</span>
<SvgIcon class="gap" name="arrow-botton" size="14" />
</div>
</n-popselect> -->
<n-popselect v-model:value="viewMode" :options="viewOptions" trigger="click">
<div class="dropdown">
<!-- <span>{{ viewLabel || '请选择' }}</span> -->
<span>视图</span>
<SvgIcon class="gap" name="arrow-botton" size="14" />
</div>
</n-popselect>
<div
style="margin-left: 15px; cursor: pointer; color: #323233"
@click="sortHandler('fromuptime')"
>
<span>时间排序</span>
<SvgIcon style="margin-left: 8px" name="sort" size="12" />
</div>
<div
style="margin-left: 15px; cursor: pointer; color: #323233"
@click="sortHandler('pictureResult')"
>
<span>相似度排序</span>
<SvgIcon style="margin-left: 8px" name="sort" size="12" />
</div>
</div>
<span style="font-size: 16px; color: #333"
>共
<span style="color: #507afd; font-weight: 500">{{ totalCount }}</span> 项</span
>
</div>
<n-spin :show="loading">
<div ref="el" class="scroll" :style="listStyle">
<!-- <n-scrollbar :on-scroll="scrollHandler"> -->
<div ref="masonryRef" class="grid">
<div
v-for="(item, index) in listData"
:key="index"
:style="{ height: gridHeight }"
class="grid-item"
>
<!-- <div :style="{ 'background-color': randomColor(0.2) }" class="wrapper-content-item-img" /> -->
<!-- <img
class="wrapper-content-item-img" :class="{ 'wrapper-content-item-img-fit': viewMode !== 'masonry' }"
:src="item.imgUrl"
> -->
<n-image
class="img"
:img-props="{ onClick: hideDownload }"
:class="{
'img-fit': viewMode === 'horizontalVersion',
'img-full': viewMode === '3:4' || viewMode === 'verticalVersion',
}"
:preview-src="item.imgUrl"
:src="item.thumburl"
ref="imageRef"
/>
<!-- @load="loadImgOver(item)" -->
<!-- <n-image
class="img"
:class="{
'img-fit': viewMode === 'horizontalVersion',
'img-full': viewMode === '3:4' || viewMode === 'verticalVersion',
}"
:src="baseImg"
v-show="!item.loadOver"
/> -->
<div class="percent" v-if="item.similar != -1">
<SvgIcon size="42" :name="item.similar == 100 ? 'error_tag' : 'tag'" />
<div class="val">
{{ `${item.similar}%` }}
</div>
</div>
<div class="glass" v-if="isAllowDownload">
<SvgIcon
size="16"
name="download"
style="margin-top: -6px; cursor: pointer"
@click="downloadImage(item)"
/>
</div>
<div class="info">
<div class="footer">
<div class="img-name">
<n-tooltip trigger="hover">
<template #trigger>
<span>{{ item.imgName }}</span>
</template>
{{ item.imgName }}
</n-tooltip>
</div>
<div class="icon-wrap" @click="previewHandler(index, $event)">
<SvgIcon
size="13"
name="magnifying-2"
style="cursor: pointer; color: #898481"
/>
</div>
</div>
<div class="left">
<n-avatar
:src="(item.uphead && getAvatar(item.uphead)) || defaultAvatar"
class="avatar"
round
/>
<span>{{ item.upname }}</span>
</div>
<!-- <div class="right">
<span :style="{ marginRight: '5px' }"></span>
<span>{{ item.ocrPictureclass?.classname }}</span>
</div> -->
</div>
</div>
</div>
<!-- </n-scrollbar> -->
</div>
</n-spin>
</div>
<PackageSettingsModal ref="packageModalRef" @commit="commitHandler" />
<GeneratePackageModal ref="generateModalRef" />
<QueryRepeatedTasksModal
ref="queryRepeatedTasksModalRef"
@closeCallback="tasksLoadingCloseCallback"
/>
<LoginSuccessModal ref="LoginSuccessModalRef" />
<FinishPackageModal ref="finishPackageModal" :id="packageIdRef" />
<CheckingTaskModal ref="checkingTaskModalRef" @refresh="refresh" @cancel="cancel" />
</div>
</template>
<style lang="less" scoped>
.wrapper {
display: flex;
flex: 1;
flex-direction: column;
box-sizing: border-box;
margin-left: 16px;
width: 100%;
&-header {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
padding: 0px 16px 0px 15px;
box-sizing: border-box;
height: 64px;
border: 1px solid rgb(239, 239, 245);
border-radius: 3px;
width: 100%;
.left {
display: flex;
align-items: center;
}
.font {
font-size: 18px;
font-weight: bold;
color: #333333;
line-height: 25px;
width: 89px;
height: 25px;
white-space: nowrap;
// margin-left: 12px;
}
}
&-content {
flex: 1;
padding: 16px 16px 0px 16px;
margin-top: 16px;
background: #fff;
border-radius: 3px;
border: 1px solid rgb(239, 239, 245);
.form {
display: flex;
align-items: center;
font-size: 14px;
padding-bottom: 16px;
}
.img {
border-radius: 7px;
display: block;
height: calc(100% - 25px);
}
.img-fit {
width: 100%;
overflow: hidden;
}
.img-full {
width: 100%;
overflow: hidden;
::v-deep(img) {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.info {
display: flex;
justify-content: space-between;
margin-top: 4px;
position: relative;
color: #666666;
font-size: 16px;
.left {
display: flex;
align-items: center;
}
.right {
display: flex;
align-items: center;
}
.avatar {
width: 20px;
height: 20px;
margin-right: 5px;
}
}
.dropdown {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 24px;
color: #323233;
.gap {
margin-left: 5px;
}
}
.grid-item {
width: 182px;
border-radius: 7px;
margin-bottom: 10px;
overflow: hidden;
position: relative;
transition: 0.5s;
.glass {
position: absolute;
display: none !important;
background-color: #fff;
border-radius: 6px;
padding: 3px;
top: 10px;
right: 10px;
width: 22px;
height: 22px;
}
.footer {
display: none;
width: 100%;
height: 40px;
position: absolute;
top: -45px;
justify-content: space-between;
align-items: center;
padding: 0 10px;
background: rgba(0, 0, 0, 0.35);
border-radius: 7px;
.img-name {
width: 70%;
color: #fff;
/* 设置文本溢出时的样式为省略号 */
text-overflow: ellipsis;
/* 隐藏超出容器的文本 */
overflow: hidden;
/* 确保文本不换行 */
white-space: nowrap;
}
.icon-wrap {
width: 22px;
height: 22px;
line-height: 22px;
text-align: center;
background-color: #fff;
border-radius: 6px;
}
}
&:hover {
.percent {
display: none;
}
.glass {
display: block !important;
}
.info .footer {
display: flex;
}
}
}
.percent {
position: absolute;
text-align: center;
z-index: 3;
right: 0px;
top: -6px;
color: #fff;
.val {
position: absolute;
left: 0;
top: 0;
display: block;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-family: PingFang SC, PingFang SC-Semibold;
font-weight: Semibold;
text-align: left;
color: #ffffff;
line-height: 24px;
}
}
.scroll {
overflow-y: scroll;
}
}
.flex-btn-icon {
display: flex;
align-items: center;
}
.expand-icon {
display: block;
margin-left: 10px;
width: 30px;
height: 30px;
line-height: 30px;
background: linear-gradient(
144deg,
rgba(115, 167, 249, 0.1) 0%,
rgba(62, 110, 241, 0.1) 96%
);
border-radius: 8px;
text-align: center;
}
}
</style>