feat: add fnOS packaging, storage workflows and release pipeline

This commit is contained in:
2026-08-11 18:05:49 +08:00
parent c5922f9b08
commit 95932f0199
181 changed files with 24024 additions and 1164 deletions
+322 -64
View File
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { getBase64 } from '@/utils/file';
import { FormInstance } from 'ant-design-vue';
import { reactive, ref, onMounted, UnwrapRef, watch, nextTick } from 'vue';
import { computed, reactive, ref, onMounted, onBeforeUnmount, UnwrapRef, watch, nextTick } from 'vue';
import dayjs from 'dayjs';
import { Dayjs } from 'dayjs';
import {
@@ -18,20 +18,22 @@ import { useApiStore } from '@/store';
import { message } from 'ant-design-vue';
import { StarOutlined, StarFilled, StarTwoTone } from '@ant-design/icons-vue';
const columns = ref([]);
columns.value = [
{
title: 'Cookie名称',
dataIndex: 'userName',
width: 180,
},
{ title: 'Cookie状态', dataIndex: 'statusMsg' },
{ title: '收藏路径', dataIndex: 'savePath' },
{ title: '喜欢路径', dataIndex: 'favSavePath' },
{ title: '博主路径', dataIndex: 'upSavePath' },
const storageType = ref(0);
const isRemoteStorage = computed(() => storageType.value === 1 || storageType.value === 2);
const columns = computed(() => [
{ title: 'Cookie名称', dataIndex: 'userName', width: 180 },
{ title: 'Cookie状态', dataIndex: 'statusMsg', width: 120 },
{ title: '收藏路径', dataIndex: isRemoteStorage.value ? 'webDavCollectPath' : 'savePath', width: 240 },
{ title: '喜欢路径', dataIndex: isRemoteStorage.value ? 'webDavFavoritePath' : 'favSavePath', width: 240 },
{ title: '博主路径', dataIndex: isRemoteStorage.value ? 'webDavFollowPath' : 'upSavePath', width: 240 },
{ title: '状态', dataIndex: 'status', width: 180 },
{ title: '操作', dataIndex: 'edit', width: 200 },
];
]);
const isMobile = ref(window.innerWidth <= 768);
const updateViewport = () => (isMobile.value = window.innerWidth <= 768);
const drawerWidth = computed(() => (isMobile.value ? '100%' : '800px'));
interface UpSecUserIdItem {
uper?: string;
@@ -47,6 +49,8 @@ type DataItem = {
favSavePath?: string;
secUserId?: string;
status?: number;
statusMsg?: string;
statusCode?: number;
_isNew?: boolean;
upSecUserIdsJson?: UpSecUserIdItem[];
upSecUserIds?: string;
@@ -59,6 +63,16 @@ type DataItem = {
downCollect?: boolean;
downFavorite?: boolean;
downFollowd?: boolean;
webDavCollectPath?: string;
webDavFavoritePath?: string;
webDavFollowPath?: string;
webDavMixPath?: string;
webDavSeriesPath?: string;
sourceCooldownUntil?: string;
sourceRequiresAuthorization?: boolean;
sourceProbePending?: boolean;
lastSourceStatusCode?: number;
lastSourceError?: string;
};
const loading = ref(false);
@@ -125,6 +139,11 @@ const newCookie = (cookie?: DataItem) => {
cookie.downCollect = false;
cookie.downFavorite = false;
cookie.downFollowd = false;
cookie.webDavCollectPath = undefined;
cookie.webDavFavoritePath = undefined;
cookie.webDavFollowPath = undefined;
cookie.webDavMixPath = undefined;
cookie.webDavSeriesPath = undefined;
return cookie;
};
@@ -258,7 +277,7 @@ const switchSyncStatus = (record: DataItem) => {
},
});
};
const StatusDict = {
const StatusDict: Record<number, string> = {
0: '同步已停止',
1: '同步已开启',
};
@@ -277,9 +296,25 @@ const removeRow = (index: number) => {
}
};
const rowCount = 10;
onMounted(() => {
const handlePageChange = (page: number, pageSize: number) => {
pagination.value.current = page;
pagination.value.defaultPageSize = pageSize;
GetRecords();
};
onMounted(async () => {
window.addEventListener('resize', updateViewport);
try {
const res = await useApiStore().StorageConfig();
if (res.code === 0) storageType.value = Number(res.data.storageType ?? 0);
} catch {
message.warning('读取存储模式失败,路径表单暂按本地存储显示');
}
GetRecords();
});
onBeforeUnmount(() => {
window.removeEventListener('resize', updateViewport);
});
const showDrawer = ref(false);
@@ -475,8 +510,8 @@ const switchdownCollect = (e: any) => {
</script>
<template>
<a-modal :title="form._isNew ? '新增' : '编辑'" v-model:visible="showModal" @ok="submit" @cancel="cancel" width="100%" wrap-class-name="full-modal">
<a-form ref="formModel" :model="form" :labelCol="{ span: 3 }" :wrapperCol="{ span: 20 }">
<a-modal :title="form._isNew ? '新增授权' : '编辑授权'" v-model:visible="showModal" @ok="submit" @cancel="cancel" width="100%" wrap-class-name="full-modal">
<a-form ref="formModel" :model="form" :labelCol="isMobile ? { span: 24 } : { span: 3 }" :wrapperCol="isMobile ? { span: 24 } : { span: 20 }">
<a-form-item label="Cookie名称" required name="userName">
<a-input v-model:value="form.userName" />
</a-form-item>
@@ -497,99 +532,105 @@ const switchdownCollect = (e: any) => {
</a-form-item>
<a-form-item label="下载收藏视频" name="downCollect">
<div style="display: flex; align-items: center; gap: 12px;">
<div class="sync-option-row">
<div class="form-item-div">
<a-switch v-model:checked="form.downCollect" @change="switchdownCollect" :checked-value="true" :un-checked-value="false" size="default" />
<a-form-item-rest v-if="form.downCollect">
<a-form-item name="savePath" noStyle>
<a-input v-model:value="form.savePath" placeholder='请输入容器路径' class="form-item-div-input" />
<a-form-item :name="isRemoteStorage ? 'webDavCollectPath' : 'savePath'" noStyle>
<a-input v-if="isRemoteStorage" v-model:value="form.webDavCollectPath" placeholder="OpenList 相对路径,留空自动生成" class="form-item-div-input" />
<a-input v-else v-model:value="form.savePath" placeholder="请输入容器路径" class="form-item-div-input" />
</a-form-item>
</a-form-item-rest>
</div>
<a-alert message="开启后自动下载默认收藏夹视频,记得填写映射路径(容器内部路径)" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
<a-alert :message="isRemoteStorage ? '开启后写入目标基础目录下的这个 OpenList 相对路径;留空保存时会自动生成。' : '开启后自动下载默认收藏夹视频,记得填写映射路径(容器内部路径)。'" type="info" size="small" class="path-alert" />
</div>
</a-form-item>
<a-form-item v-if="form.savePath && form.savePath.length>0" label="自定义收藏夹" name="useCollectFolder">
<div style="display: flex; align-items: center; gap: 12px;">
<a-form-item v-if="isRemoteStorage || (form.savePath && form.savePath.length>0)" label="自定义收藏夹" name="useCollectFolder">
<div class="sync-option-row">
<div class="form-item-div">
<a-switch v-model:checked="form.useCollectFolder" :checked-value="true" :un-checked-value="false" size="default" />
<a-form-item-rest v-if="form.useCollectFolder">
<a-input v-model:value="form.savePath" :disabled="form.useCollectFolder&&form.downCollect" placeholder="" class="form-item-div-input" />
<a-input v-if="isRemoteStorage" v-model:value="form.webDavCollectPath" :disabled="form.useCollectFolder&&form.downCollect" placeholder="与收藏路径一致" class="form-item-div-input" />
<a-input v-else v-model:value="form.savePath" :disabled="form.useCollectFolder&&form.downCollect" placeholder="" class="form-item-div-input" />
<a-button @click="openCollectFolderSetModal" shape="circle" type="dashed" style="margin-left:5px;" v-if="form.useCollectFolder">
<star-outlined />
</a-button>
</a-form-item-rest>
</div>
<a-alert message="开启后自动下载自定义分类后的收藏夹,开启后不在下载默认收藏夹视频,存储路径与默认收藏夹存储路径一致" :type="form.useCollectFolder?'error':'info'" size="small" style="flex: 1; margin-bottom: 0;" />
<a-alert message="开启后按收藏夹分类同步,存储路径与默认收藏路径一致" :type="form.useCollectFolder?'error':'info'" size="small" class="path-alert" />
</div>
</a-form-item>
<a-form-item label="下载喜欢视频" name="downFavorite">
<div style="display: flex; align-items: center; gap: 12px;">
<div class="sync-option-row">
<div class="form-item-div">
<a-switch v-model:checked="form.downFavorite" :checked-value="true" :un-checked-value="false" size="default" />
<a-form-item-rest v-if="form.downFavorite">
<a-form-item name="favSavePath" noStyle>
<a-input v-model:value="form.favSavePath" placeholder='请输入容器路径' class="form-item-div-input" />
<a-form-item :name="isRemoteStorage ? 'webDavFavoritePath' : 'favSavePath'" noStyle>
<a-input v-if="isRemoteStorage" v-model:value="form.webDavFavoritePath" placeholder="OpenList 相对路径,留空自动生成" class="form-item-div-input" />
<a-input v-else v-model:value="form.favSavePath" placeholder="请输入容器路径" class="form-item-div-input" />
</a-form-item>
<a-button shape="circle" @click="()=>{message.success('别点了,这只是为了好看的😄')}" type="dashed" style="margin-left:5px;">
<like-outlined />
</a-button>
</a-form-item-rest>
</div>
<a-alert message="开启后自动下载喜欢(点赞)的视频,记得填写映射路径(容器内部路径)" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
<a-alert :message="isRemoteStorage ? '喜欢的视频会写入目标基础目录下的这个 OpenList 相对路径。' : '开启后自动下载喜欢(点赞)的视频,记得填写映射路径(容器内部路径)。'" type="info" size="small" class="path-alert" />
</div>
</a-form-item>
<a-form-item label="下载关注视频" name="downFollowd">
<div style="display: flex; align-items: center; gap: 12px;">
<div class="sync-option-row">
<div class="form-item-div">
<a-switch v-model:checked="form.downFollowd" :checked-value="true" :un-checked-value="false" size="default" />
<a-form-item-rest v-if="form.downFollowd">
<a-form-item name="upSavePath" noStyle>
<a-input v-model:value="form.upSavePath" placeholder='请输入容器路径' class="form-item-div-input" />
<a-form-item :name="isRemoteStorage ? 'webDavFollowPath' : 'upSavePath'" noStyle>
<a-input v-if="isRemoteStorage" v-model:value="form.webDavFollowPath" placeholder="OpenList 相对路径,留空自动生成" class="form-item-div-input" />
<a-input v-else v-model:value="form.upSavePath" placeholder="请输入容器路径" class="form-item-div-input" />
</a-form-item>
<a-button shape="circle" @click="()=>{message.success('别点了,这只是为了好看的😄')}" type="dashed" style="margin-left:5px;">
<heart-outlined />
</a-button>
</a-form-item-rest>
</div>
<a-alert message="开启后自动下载关注的博主视频,记得填写映射路径(容器内部路径)" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
<a-alert :message="isRemoteStorage ? '关注博主的视频会写入目标基础目录下的这个 OpenList 相对路径。' : '开启后自动下载关注的博主视频,记得填写映射路径(容器内部路径)。'" type="info" size="small" class="path-alert" />
</div>
</a-form-item>
<a-form-item label="下载合集视频" name="downMix">
<div style="display: flex; align-items: center; gap: 12px;">
<div class="sync-option-row">
<div class="form-item-div">
<a-switch v-model:checked="form.downMix" :checked-value="true" :un-checked-value="false" size="default" />
<a-form-item-rest v-if="form.downMix">
<a-form-item name="mixPath" noStyle>
<a-input v-model:value="form.mixPath" class="form-item-div-input" placeholder='默认使用收藏夹路径' />
<a-form-item :name="isRemoteStorage ? 'webDavMixPath' : 'mixPath'" noStyle>
<a-input v-if="isRemoteStorage" v-model:value="form.webDavMixPath" class="form-item-div-input" placeholder="OpenList 相对路径,留空自动生成" />
<a-input v-else v-model:value="form.mixPath" class="form-item-div-input" placeholder="默认使用收藏夹路径" />
</a-form-item>
<a-button @click="openMixDownSetModal" shape="circle" type="dashed" style="margin-left:5px;">
<gift-outlined />
</a-button>
</a-form-item-rest>
</div>
<a-alert message="开启后自动下载收藏的合集视频(还需要开启合集同步开关,不填目录径默认存储到收藏目录,设置后记得docker里面加映射,注意:付费视频下载后无法播放" :type="form.downMix?'error':'info'" size="small" style="flex: 1; margin-bottom: 0;" />
<a-alert :message="isRemoteStorage ? '收藏的合集会同步到该 OpenList 相对路径;付费视频下载后可能无法播放。' : '开启后自动下载收藏的合集视频;不填路径默认使用收藏目录,付费视频下载后可能无法播放。'" :type="form.downMix?'error':'info'" size="small" class="path-alert" />
</div>
</a-form-item>
<a-form-item label="下载短剧视频" name="downSeries">
<div style="display: flex; align-items: center; gap: 12px;">
<div class="sync-option-row">
<div class="form-item-div">
<a-switch v-model:checked="form.downSeries" :checked-value="true" :un-checked-value="false" size="default" />
<a-form-item-rest v-if="form.downSeries">
<a-form-item name="seriesPath" noStyle>
<a-input v-model:value="form.seriesPath" placeholder='默认使用收藏夹路径' class="form-item-div-input" />
<a-form-item :name="isRemoteStorage ? 'webDavSeriesPath' : 'seriesPath'" noStyle>
<a-input v-if="isRemoteStorage" v-model:value="form.webDavSeriesPath" placeholder="OpenList 相对路径,留空自动生成" class="form-item-div-input" />
<a-input v-else v-model:value="form.seriesPath" placeholder="默认使用收藏夹路径" class="form-item-div-input" />
</a-form-item>
<a-button @click="openSeriesDownSetModal" shape="circle" type="dashed" style="margin-left:5px;">
<fire-outlined />
</a-button>
</a-form-item-rest>
</div>
<a-alert message="开启后自动下载收藏的短剧视频(还需要开启短剧同步开关,不填目录径默认存储到收藏目录,设置后记得docker里面加映射,注意:付费视频下载后无法播放" :type="form.downSeries?'error':'info'" size="small" style="flex: 1; margin-bottom: 0;" />
<a-alert :message="isRemoteStorage ? '收藏的短剧会同步到该 OpenList 相对路径;付费视频下载后可能无法播放。' : '开启后自动下载收藏的短剧视频;不填路径默认使用收藏目录,付费视频下载后可能无法播放。'" :type="form.downSeries?'error':'info'" size="small" class="path-alert" />
</div>
</a-form-item>
@@ -602,7 +643,7 @@ const switchdownCollect = (e: any) => {
</a-form>
</a-modal>
<a-drawer :title="getDrawerTypeName()+'配置'" v-model:visible="showDrawer" placement="right" width="800px" :z-index="10010" :mask-z-index="10009" @close="closeDrawer" class="common-drawer">
<a-drawer :title="getDrawerTypeName()+'配置'" v-model:visible="showDrawer" placement="right" :width="drawerWidth" :z-index="10010" :mask-z-index="10009" @close="closeDrawer" class="common-drawer">
<template #extra>
<a-button type="primary" :loading="drawerPagination.loading" @click="saveDrawerData" class="drawer-save-btn">
<template #icon>
@@ -654,26 +695,29 @@ const switchdownCollect = (e: any) => {
</div>
</a-drawer>
<a-table v-bind="$attrs" :columns="columns" :dataSource="dataSource" :pagination="false">
<template #title>
<div class="flex justify-end pr-4">
<a-button type="primary" @click="GetRecords()" :loading="formLoading" class="mr-2">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<div class="cookie-toolbar">
<a-button @click="GetRecords()" :loading="loading">
<template #icon><SearchOutlined /></template>
刷新
</a-button>
<a-button type="primary" @click="addNew" :loading="formLoading">
<template #icon><PlusOutlined /></template>
新增授权
</a-button>
</div>
<a-button type="primary" @click="addNew" :loading="formLoading">
<template #icon>
<PlusOutlined />
</template>
新增
</a-button>
</div>
</template>
<a-table v-if="!isMobile" v-bind="$attrs" :columns="columns" :dataSource="dataSource" :pagination="false" :scroll="{ x: isRemoteStorage ? 1400 : 1200 }">
<template #bodyCell="{ column, text, record }">
<template v-if="column.dataIndex === 'status'">
<template v-if="column.dataIndex === 'statusMsg'">
<a-tooltip v-if="record.sourceRequiresAuthorization" :title="record.lastSourceError || '请编辑并保存新的 Cookie'">
<a-tag color="error">需要重新授权</a-tag>
</a-tooltip>
<a-tooltip v-else-if="record.sourceCooldownUntil || record.sourceProbePending" :title="record.lastSourceError || '等待自动探测'">
<a-tag color="warning">{{ record.sourceCooldownUntil ? `冷却至 ${dayjs(record.sourceCooldownUntil).format('HH:mm:ss')}` : '等待来源探测' }}</a-tag>
</a-tooltip>
<a-tag v-else :color="record.statusCode === 0 ? 'success' : 'default'">{{ record.statusMsg || '未知' }}</a-tag>
</template>
<template v-else-if="column.dataIndex === 'status'">
<div style="display: flex; align-items: center; gap: 8px;">
<a-switch v-model:checked="record.status" :checked-value="1" :un-checked-value="0" size="small" :disabled="loading" @change="() => switchSyncStatus(record)" />
<span :style="{
@@ -704,6 +748,54 @@ const switchdownCollect = (e: any) => {
</div>
</template>
</a-table>
<a-spin v-else :spinning="loading">
<a-empty v-if="!loading && dataSource.length === 0" description="暂无抖音授权" />
<div class="mobile-cookie-list">
<article v-for="record in dataSource" :key="record.id" class="mobile-cookie-card">
<div class="mobile-cookie-header">
<div>
<h3>{{ record.userName || '未命名授权' }}</h3>
<span :class="record.status === 1 ? 'status-on' : 'status-off'">{{ record.statusMsg || StatusDict[record.status || 0] }}</span>
<a-tag v-if="record.sourceRequiresAuthorization" color="error">需要重新授权</a-tag>
<a-tag v-else-if="record.sourceCooldownUntil || record.sourceProbePending" color="warning">
{{ record.sourceCooldownUntil ? `冷却至 ${dayjs(record.sourceCooldownUntil).format('HH:mm:ss')}` : '等待来源探测' }}
</a-tag>
</div>
<a-switch v-model:checked="record.status" :checked-value="1" :un-checked-value="0" size="small" :disabled="loading" @change="() => switchSyncStatus(record)" />
</div>
<dl class="mobile-cookie-paths">
<template v-if="isRemoteStorage">
<div><dt>收藏</dt><dd>{{ record.webDavCollectPath || '未设置' }}</dd></div>
<div><dt>喜欢</dt><dd>{{ record.webDavFavoritePath || '未设置' }}</dd></div>
<div><dt>关注</dt><dd>{{ record.webDavFollowPath || '未设置' }}</dd></div>
<div><dt>合集</dt><dd>{{ record.webDavMixPath || '未设置' }}</dd></div>
<div><dt>短剧</dt><dd>{{ record.webDavSeriesPath || '未设置' }}</dd></div>
</template>
<template v-else>
<div><dt>收藏</dt><dd>{{ record.savePath || '未设置' }}</dd></div>
<div><dt>喜欢</dt><dd>{{ record.favSavePath || '未设置' }}</dd></div>
<div><dt>关注</dt><dd>{{ record.upSavePath || '未设置' }}</dd></div>
</template>
</dl>
<div class="mobile-cookie-actions">
<a-button :disabled="showModal || loading" @click="edit(record)"><EditFilled />编辑</a-button>
<a-button :disabled="loading || !record.id" danger @click="record.id && deleted(record.id)"><DeleteOutlined />删除</a-button>
</div>
</article>
</div>
</a-spin>
<a-pagination
v-if="pagination.total > 0"
class="cookie-pagination"
:simple="isMobile"
:current="pagination.current"
:page-size="pagination.defaultPageSize"
:total="pagination.total"
:show-size-changer="false"
@change="handlePageChange"
/>
</template>
<style scoped lang="less">
@@ -711,12 +803,100 @@ const switchdownCollect = (e: any) => {
margin-bottom: 10px;
}
.form-item-div {
width: 300px;
width: 430px;
display: flex;
align-items: center;
}
.form-item-div-input {
width: 180px;
width: 310px;
margin-left: 10px;
}
.sync-option-row {
display: flex;
align-items: center;
gap: 12px;
}
.path-alert {
flex: 1;
margin-bottom: 0;
}
.cookie-toolbar {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 8px 12px 12px;
}
.cookie-pagination {
display: flex;
justify-content: flex-end;
margin: 16px 12px 4px;
}
.mobile-cookie-list {
display: grid;
gap: 12px;
padding: 0 8px;
}
.mobile-cookie-card {
padding: 14px;
border: 1px solid #e8e8e8;
border-radius: 12px;
background: #fff;
box-shadow: 0 3px 14px rgba(15, 23, 42, 0.06);
}
.mobile-cookie-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.mobile-cookie-header h3 {
margin: 0 0 3px;
color: #1f2937;
font-size: 16px;
}
.mobile-cookie-header .status-on {
color: #52c41a;
}
.mobile-cookie-header .status-off {
color: #ff4d4f;
}
.mobile-cookie-paths {
margin: 12px 0;
}
.mobile-cookie-paths > div {
display: grid;
grid-template-columns: 44px minmax(0, 1fr);
gap: 8px;
padding: 3px 0;
font-size: 12px;
}
.mobile-cookie-paths dt {
color: #8c8c8c;
}
.mobile-cookie-paths dd {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
color: #334155;
}
.mobile-cookie-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
:deep(.ant-input-textarea-input) {
overflow-y: auto;
@@ -835,6 +1015,68 @@ const switchdownCollect = (e: any) => {
}
@media (max-width: 768px) {
.cookie-toolbar {
position: sticky;
z-index: 20;
top: 0;
padding: 10px;
margin-bottom: 10px;
border: 1px solid var(--mobile-border, #e5e7eb);
border-radius: 16px;
background: var(--mobile-card, #fff);
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.05);
}
.cookie-toolbar .ant-btn {
flex: 1;
}
.cookie-pagination {
justify-content: center;
}
.sync-option-row {
align-items: stretch;
flex-direction: column;
gap: 8px;
}
.form-item-div {
width: 100%;
}
.form-item-div-input {
width: 100%;
max-width: none;
}
.mobile-cookie-list { padding: 0; }
.mobile-cookie-card {
border-color: var(--mobile-border, #e8e8e8);
border-radius: 16px;
background: var(--mobile-card, #fff);
}
.mobile-cookie-header h3 { color: var(--mobile-text, #1f2937); }
.mobile-cookie-paths dd { color: var(--mobile-text, #334155); }
:deep(.ant-modal-footer) {
position: sticky;
z-index: 5;
bottom: 0;
padding-bottom: max(10px, env(safe-area-inset-bottom));
background: var(--mobile-card, #fff);
}
:deep(.ant-form-item-label) {
padding-bottom: 4px;
text-align: left;
}
:deep(.ant-form-item-label > label) {
height: auto;
}
:deep(.drawer-card-grid) {
width: calc(50% - 10px) !important;
min-width: 180px;
@@ -901,6 +1143,22 @@ html.dark-mode .drawer-card-container.grid-container .drawer-card-grid .ant-inpu
}
.ant-modal-body {
flex: 1;
overflow-y: auto;
padding: 16px;
}
}
@media (max-width: 768px) {
.full-modal {
.ant-modal-header,
.ant-modal-footer {
padding-left: 16px;
padding-right: 16px;
}
.ant-modal-content {
height: 100dvh;
}
}
}
@@ -1015,4 +1273,4 @@ html.dark-mode .drawer-card-container.grid-container .drawer-card-grid .ant-inpu
color: rgba(255, 255, 255, 0.45);
}
}
</style>
</style>