Files
douyin/app/src/pages/cok/CookieTable.vue
T

1121 lines
39 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 dayjs from 'dayjs';
import { Dayjs } from 'dayjs';
import {
EditFilled,
DeleteFilled,
SearchOutlined,
PlusOutlined,
ExclamationCircleOutlined,
StopOutlined,
ClockCircleOutlined,
DeleteOutlined,
} from '@ant-design/icons-vue';
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' },
// { title: '图文视频', dataIndex: 'imgSavePath' },
// { title: 'Cookie', dataIndex: 'cookies' },
{ title: '状态', dataIndex: 'status', width: 180 },
{ title: '操作', dataIndex: 'edit', width: 200 }, // 加宽操作列宽度
];
// 定义数组中单个对象的类型:包含uper和uid字段(可选字符串类型,根据实际需求调整是否可选)
interface UpSecUserIdItem {
uper?: string; // 若要求必填,可去掉问号 `uper: string;`
uid?: string; // 同上,必填则去掉问号
syncAll: boolean;
}
type DataItem = {
id?: string;
userName?: string;
cookies?: string;
savePath?: string;
favSavePath?: string;
secUserId?: string;
status?: number;
_isNew?: boolean;
upSecUserIdsJson?: UpSecUserIdItem[];
upSecUserIds?: string;
upSavePath?: string;
// imgSavePath?: string;
useSinglePath?: boolean; // 新增:是否全部用一个地址
useCollectFolder?: boolean;
downMix?: boolean;
downSeries?: boolean;
mixPath?: string;
seriesPath?: string;
};
const loading = ref(false);
const datas: UnwrapRef<DataItem[]> = reactive([]);
const pagination = ref({
current: 1,
defaultPageSize: 10,
total: 0,
showTotal: () => `共 ${0} 条`,
});
interface QuaryParam {
pageIndex: number;
pageSize: number;
}
const quaryData: UnwrapRef<QuaryParam> = reactive({
pageIndex: 0,
pageSize: 20,
});
const GetRecords = () => {
loading.value = true;
quaryData.pageIndex = pagination.value.current;
quaryData.pageSize = pagination.value.defaultPageSize;
useApiStore()
.CookiePageList(quaryData)
.then((res) => {
loading.value = false;
if (res.code === 0) {
dataSource.value = res.data.data;
pagination.value.current = res.data.pageIndex;
pagination.value.defaultPageSize = res.data.pageSize;
pagination.value.total = res.data.total;
pagination.value.showTotal = () => `共 ${res.data.total} 条`;
}
});
};
function addNew() {
showModal.value = true;
form._isNew = true;
}
const showModal = ref(false);
const newCookie = (cookie?: DataItem) => {
if (!cookie) {
cookie = { _isNew: true };
}
cookie.userName = undefined;
cookie.cookies = undefined;
cookie.savePath = undefined;
cookie.favSavePath = undefined;
cookie.secUserId = undefined;
cookie.status = 0; // 0=关闭,1=开启
cookie.id = '0';
cookie.upSecUserIdsJson = undefined;
cookie.upSavePath = undefined;
// cookie.imgSavePath = undefined;
cookie.useSinglePath = false; // 新增:默认不使用单一路径
cookie.useCollectFolder = false; //是否按收藏夹来下载。
cookie.downMix = false; //是否下载收藏夹的合集
cookie.downSeries = false; //是否下载短剧
cookie.mixPath = undefined; //合集存储路径
cookie.seriesPath = undefined; //短剧存储路径
return cookie;
};
const copyObject = (target: any, source?: any) => {
if (!source) {
return target;
}
Object.keys(target).forEach((key) => (target[key] = source[key]));
};
const form = reactive<DataItem>(newCookie());
// 新增:监听收藏路径变化,当启用单一路径时同步到其他路径
watch(
[() => form.savePath, () => form.useSinglePath],
([newSavePath, useSinglePath]) => {
if (useSinglePath && newSavePath) {
form.favSavePath = newSavePath;
form.upSavePath = newSavePath;
// form.imgSavePath = newSavePath;
}
},
{ immediate: true }
);
function reset() {
return newCookie(form);
}
function cancel() {
showModal.value = false;
reset();
}
const formModel = ref<FormInstance>();
const formLoading = ref(false);
function submit() {
formLoading.value = true;
formModel.value
?.validateFields()
.then((resData: DataItem) => {
if (form._isNew) {
// authors.push({ ...res });
} else {
copyObject(editRecord.value, resData);
}
useApiStore()
.UpdateConfig(resData)
.then((res) => {
loading.value = false;
if (res.code === 0) {
showModal.value = false;
message.success('修改成功,同步任务将在5-10秒按新配置运行...');
reset();
GetRecords();
}
});
})
.catch((e) => {
console.error(e);
})
.finally(() => {
formLoading.value = false;
});
}
const editRecord = ref<DataItem>();
import { Modal } from 'ant-design-vue';
const deleted = (id: string) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这条记录吗?此操作不可撤销。',
okText: '确认',
cancelText: '取消',
onOk: () => {
useApiStore()
.deleteCookie(id)
.then((res) => {
loading.value = false;
if (res.code === 0) {
showModal.value = false;
reset();
GetRecords();
}
});
},
onCancel: () => {
console.log('已取消删除');
},
});
};
function edit(record: DataItem) {
cookieId.value = record.id;
editRecord.value = record;
console.log(record);
copyObject(form, record);
// 确保useSinglePath有默认值
if (form.useSinglePath === undefined) {
form.useSinglePath = false;
}
showModal.value = true;
}
// 新增:切换同步状态方法
const switchSyncStatus = (record: DataItem) => {
// 保存原始状态,用于弹窗取消后还原
const statusText = record.status === 1 ? '开启' : '停止';
const title = `确认${statusText}同步`;
const content = `确定要${statusText}${record.userName || '该'}】Cookie的同步任务吗?`;
Modal.confirm({
title,
content,
okText: '确认',
cancelText: '取消',
onOk: () => {
loading.value = true;
useApiStore()
.SwitchCookieStatus({
id: record.id,
status: record.status,
})
.then((res) => {
loading.value = false;
if (res.code === 0) {
message.success(`${statusText}同步成功`);
GetRecords(); // 刷新列表,更新 switch 状态
} else {
message.error(`${statusText}同步失败:${res.message || '未知错误'}`);
record.status = record.status === 1 ? 0 : 1;
}
})
.catch((err) => {
loading.value = false;
console.error('切换同步状态失败:', err);
message.error('切换同步状态失败,请稍后重试');
record.status = record.status === 1 ? 0 : 1;
});
},
onCancel: () => {
console.log(`已取消${statusText}同步`);
record.status = record.status === 1 ? 0 : 1;
},
});
};
const StatusDict = {
0: '同步已停止',
1: '同步已开启',
};
const dataSource = ref(datas);
const showCookiesModal = ref(false);
let showCookiesData = '';
const showCookies = (recode: DataItem) => {
showCookiesModal.value = true;
showCookiesData = recode.cookies;
};
const showUpersModal = ref(false);
let showUpersData = '';
const showUpers = (recode: DataItem) => {
showUpersModal.value = true;
showUpersData = recode.upSecUserIds;
};
const addRow = () => {
if (!form.upSecUserIdsJson) {
form.upSecUserIdsJson = [];
}
form.upSecUserIdsJson.push({ uper: '', uid: '', syncAll: false });
};
const removeRow = (index: number) => {
if (form.upSecUserIdsJson) {
form.upSecUserIdsJson.splice(index, 1);
}
};
const rowCount = 5;
// 组件挂载时获取配置
onMounted(() => {
GetRecords();
});
// ========== 抽屉相关响应式数据 ==========
// 抽屉显示状态
const showDrawer = ref(false);
// 抽屉类型(区分收藏夹/合集/短剧)
type DrawerType = 'collect' | 'mix' | 'series';
const cookieId = ref('');
const cateType = ref(5);
const drawerType = ref<DrawerType>('collect');
// 抽屉滚动容器引用(用于监听触底分页)
const drawerScrollRef = ref<HTMLDivElement | null>(null);
// 抽屉数据列表
const drawerDataList = ref<DrawerItem[]>([]);
// 抽屉分页配置
const drawerPagination = reactive({
current: 1,
pageSize: 20,
total: 0,
loading: false,
hasMore: true, // 是否还有更多数据
});
// ========== 定义抽屉数据项接口 ==========
interface DrawerItem {
id: string; // 不显示
name: string; // 名称
saveFolder: string; // 保存文件夹
sync: boolean; // 是否同步
coverUrl: string; // 封面
cookieId: string; // 不显示
xId: string; // 不显示
total: number;
}
// ========== 3个打开抽屉的方法 ==========
const openCollectFolderSetModal = () => {
drawerType.value = 'collect';
cateType.value = 5;
openCommonDrawer();
};
const openMixDownSetModal = () => {
drawerType.value = 'mix';
cateType.value = 6;
openCommonDrawer();
};
const openSeriesDownSetModal = () => {
drawerType.value = 'series';
cateType.value = 7;
openCommonDrawer();
};
// ========== 公用抽屉打开逻辑 ==========
const openCommonDrawer = () => {
// 重置抽屉状态
drawerDataList.value = [];
drawerPagination.current = 1;
drawerPagination.total = 0;
drawerPagination.hasMore = true;
// 显示抽屉
showDrawer.value = true;
// 加载第一页数据
loadDrawerData();
// 监听滚动触底(延迟绑定,确保DOM已渲染)
nextTick(() => {
bindDrawerScrollEvent();
});
};
// ========== 绑定抽屉滚动触底事件 ==========
const bindDrawerScrollEvent = () => {
const scrollContainer = drawerScrollRef.value;
if (!scrollContainer) return;
// 先移除原有事件,避免重复绑定
scrollContainer.removeEventListener('scroll', handleDrawerScroll);
// 添加滚动事件
scrollContainer.addEventListener('scroll', handleDrawerScroll);
};
// ========== 滚动触底处理逻辑 ==========
const handleDrawerScroll = () => {
const scrollContainer = drawerScrollRef.value;
if (!scrollContainer || drawerPagination.loading || !drawerPagination.hasMore) return;
// 计算滚动位置(触底阈值20px,避免精准触底才触发)
const { scrollTop, clientHeight, scrollHeight } = scrollContainer;
const isBottom = scrollTop + clientHeight >= scrollHeight - 20;
if (isBottom) {
// 加载下一页
drawerPagination.current += 1;
loadDrawerData();
}
};
// ========== 加载抽屉数据(
const loadDrawerData = () => {
if (drawerPagination.loading || !drawerPagination.hasMore) return;
drawerPagination.loading = true;
useApiStore()
.CatePageList({
cookieId: cookieId.value,
cateType: cateType.value,
})
.then((res) => {
if (res.code === 0) {
drawerDataList.value = [...drawerDataList.value, ...res.data.data];
drawerPagination.loading = false;
// 更新分页状态
drawerPagination.total = res.data.total;
drawerPagination.loading = false;
// 判断是否还有更多数据
drawerPagination.hasMore = drawerDataList.value.length < drawerPagination.total;
} else {
message.error(res.message);
}
})
.finally(() => {
drawerPagination.loading = false;
});
};
// ========== 获取抽屉类型名称(用于页面展示) ==========
const getDrawerTypeName = () => {
switch (drawerType.value) {
case 'collect':
return '收藏夹';
case 'mix':
return '合集';
case 'series':
return '短剧';
default:
return '';
}
};
// ========== 切换同步状态(可根据需求对接真实接口) ==========
const toggleDrawerItemSync = (item: DrawerItem, index: number) => {
if (drawerDataList.value[index]) {
drawerDataList.value[index].sync = !item.sync;
console.log(`切换${getDrawerTypeName()}${item.name}】的同步状态为:${!item.sync}`);
}
};
// ========== 关闭抽屉清理资源 ==========
const closeDrawer = () => {
showDrawer.value = false;
// 移除滚动事件监听
const scrollContainer = drawerScrollRef.value;
if (scrollContainer) {
scrollContainer.removeEventListener('scroll', handleDrawerScroll);
}
};
// 2. 新增:抽屉右上角保存按钮的点击方法(仅抽屉相关,不影响其他逻辑)
const saveDrawerData = () => {
if (drawerDataList.value.length === 0) {
message.info('暂无需要保存的配置数据');
return;
}
drawerPagination.loading = true;
useApiStore()
.BatchSaveCate(drawerDataList.value)
.then((res) => {
if (res.code === 0) {
showDrawer.value = false;
message.success('保存成功');
} else {
message.error(res.message);
}
})
.finally(() => {
drawerPagination.loading = false;
});
};
</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-form-item label="Cookie名称" required name="userName">
<a-input v-model:value="form.userName" />
</a-form-item>
<a-form-item label="id" required name="id" v-show="false">
<a-input v-model:value="form.id" />
</a-form-item>
<a-form-item label="Cookie值" name="cookies">
<a-textarea v-model:value="form.cookies" :rows="rowCount" />
</a-form-item>
<a-form-item label="我的secUserId" name="secUserId">
<div style="display: flex; align-items: center; gap: 6px;">
<a-input v-model:value="form.secUserId" style="flex: 1;" placeholder="" />
<a-tooltip title="如果要同步“我喜欢”的视频和关注列表时,必填!!!">
<ExclamationCircleOutlined style="color: #faad14;font-size: 16px;" />
</a-tooltip>
</div>
</a-form-item>
<!-- 收藏的存储路径 -->
<a-form-item label="收藏的存储路径" name="savePath">
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
<a-input v-model:value="form.savePath" class="form-item-div" />
<a-alert message="不想同步收藏的视频就空着" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
</div>
</a-form-item>
<!-- 新增是否全部用一个地址开关 -->
<a-form-item v-if="form.savePath&&form.savePath.length>0" label="统一存储路径" name="useSinglePath">
<div style="display: flex; align-items: center; gap: 12px;">
<div class="form-item-div">
<a-switch v-model:checked="form.useSinglePath" :checked-value="true" :un-checked-value="false" size="default" />
<span style="margin-left:10px;">{{form.useSinglePath ?'':''}}</span>
</div>
<a-alert message="开启后,喜欢、关注、收藏都存储在一个目录,如果是容器部署:docker-compose配置,只需要映射一个路径 (合集短剧存储路径需要另外设置映射)" :type="form.useSinglePath?'error':'info'" size="small" style="flex: 1; margin-bottom: 0;" />
</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;">
<div class="form-item-div">
<a-switch v-model:checked="form.useCollectFolder" :checked-value="true" :un-checked-value="false" size="default" />
<span style="margin-left:10px;">{{ form.useCollectFolder ? '' : '' }}</span>
<span>存储路径=收藏的存储路径</span>
<a-button @click="openCollectFolderSetModal" shape="circle" type="dashed" style="margin-left:10px;" v-if="form.useCollectFolder">
<tag-outlined />
</a-button>
</div>
<a-alert message="开启后按照自定义收藏进行下载(需要开启收藏文件夹同步开关)且不再下载默认收藏夹视频" :type="form.useCollectFolder?'error':'info'" size="small" style="flex: 1; margin-bottom: 0;" />
</div>
</a-form-item>
<!-- 喜欢的存储路径 -->
<a-form-item label="喜欢的存储路径" name="favSavePath">
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
<a-input v-model:value="form.favSavePath" :disabled="form.useSinglePath" placeholder="" class="form-item-div" />
<a-alert message="不想同步喜欢的视频就空着" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
</div>
</a-form-item>
<!-- 关注的存储路径 -->
<a-form-item label="关注的存储路径" name="upSavePath">
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
<a-input v-model:value="form.upSavePath" :disabled="form.useSinglePath" placeholder="" class="form-item-div" />
<a-alert message="不想同步关注列表博主的视频就空着" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
</div>
</a-form-item>
<!-- 下载合集 - 新增name="mixPath" -->
<a-form-item label="下载合集" name="downMix">
<div style="display: flex; align-items: center; gap: 12px;">
<div class="form-item-div">
<a-switch v-model:checked="form.downMix" :checked-value="true" :un-checked-value="false" size="default" />
<span style="margin-left:10px;">{{ form.downMix ? '' : '' }}</span>
<!-- 给输入框对应的form-item添加name属性 -->
<a-form-item name="mixPath" noStyle>
<a-input v-model:value="form.mixPath" class="form-item-div-input" />
</a-form-item>
<a-button @click="openMixDownSetModal" shape="circle" type="dashed" style="margin-left:10px;" v-if="form.downMix"> <tag-outlined />
</a-button>
</div>
<a-alert message="开启后会同步下载收藏的合集(需要开启合集同步开关,不设置路径默认存储到收藏,设置后记得docker里面加映射)," :type="form.downMix?'error':'info'" size="small" style="flex: 1; margin-bottom: 0;" />
</div>
</a-form-item>
<!-- 下载短剧 - 新增name="seriesPath" -->
<a-form-item label="下载短剧" name="downSeries">
<div style="display: flex; align-items: center; gap: 12px;">
<div class="form-item-div">
<a-switch v-model:checked="form.downSeries" :checked-value="true" :un-checked-value="false" size="default" />
<span style="margin-left:10px;">{{ form.downSeries ? '' : '' }}</span>
<!-- 给输入框对应的form-item添加name属性 -->
<a-form-item name="seriesPath" noStyle>
<a-input v-model:value="form.seriesPath" class="form-item-div-input" />
</a-form-item>
<a-button @click="openSeriesDownSetModal" shape="circle" type="dashed" style="margin-left:10px;" v-if="form.downSeries"> <tag-outlined />
</a-button>
</div>
<a-alert message="开启后会同步下载收藏的短剧(需要开启短剧同步开关,不设置路径默认存储到收藏,设置后记得docker里面加映射)" :type="form.downSeries?'error':'info'" size="small" style="flex: 1; margin-bottom: 0;" />
</div>
</a-form-item>
<!--
<a-form-item v-if="form.savePath&&form.savePath.length>0" label="下载合集" name="downMix">
<div style="display: flex; align-items: center; gap: 12px;">
<div class="form-item-div">
<a-switch v-model:checked="form.downMix" :checked-value="true" :un-checked-value="false" size="default" />
<span style="margin-left:10px;">{{ form.downMix ? '是' : '否' }}</span>
<a-input v-model:value="form.mixPath" class="form-item-div-input" />
<a-button @click="openMixDownSetModal" shape="circle" type="dashed" style="margin-left:10px;" v-if="form.downMix"> <tag-outlined />
</a-button>
</div>
<a-alert message="开启后会同步下载收藏的合集(需要开启合集同步开关,不设置路径默认存储到收藏,设置后记得docker里面加映射)," :type="form.downMix?'error':'info'" size="small" style="flex: 1; margin-bottom: 0;" />
</div>
</a-form-item>
<a-form-item v-if="form.savePath&&form.savePath.length>0" label="下载短剧" name="downSeries">
<div style="display: flex; align-items: center; gap: 12px;">
<div class="form-item-div">
<a-switch v-model:checked="form.downSeries" :checked-value="true" :un-checked-value="false" size="default" />
<span style="margin-left:10px;">{{ form.downSeries ? '是' : '否' }}</span>
<a-input v-model:value="form.seriesPath" class="form-item-div-input" />
<a-button @click="openSeriesDownSetModal" shape="circle" type="dashed" style="margin-left:10px;" v-if="form.downSeries"> <tag-outlined />
</a-button>
</div>
<a-alert message="开启后会同步下载收藏的短剧(需要开启短剧同步开关,不设置路径默认存储到收藏,设置后记得docker里面加映射)" :type="form.downSeries?'error':'info'" size="small" style="flex: 1; margin-bottom: 0;" />
</div>
</a-form-item> -->
<!-- 图文的存储路径 -->
<!-- <a-form-item label="图文的存储路径" name="imgSavePath">
<div style="display: flex; align-items: center; gap: 12px; width: 100%;">
<a-input v-model:value="form.imgSavePath" :disabled="form.useSinglePath" style="width: 200px;" />
<a-alert message="如果系统配置页面开启了同步图文视频,且开启了单独存储,则必填!!!" type="info" size="small" style="flex: 1; margin-bottom: 0;" />
</div>
</a-form-item> -->
<!-- 同步状态开关 -->
<a-form-item label="同步状态" name="status">
<div style="display: flex; align-items: center; gap: 8px;">
<a-switch v-model:checked="form.status" :checked-value="1" :un-checked-value="0" size="default" />
<span>{{ form.status === 1 ? '' : '' }}</span>
</div>
</a-form-item>
</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">
<!-- 抽屉右上角添加保存按钮利用 extra 插槽不改动其他模板 -->
<template #extra>
<a-button type="primary" :loading="drawerPagination.loading" @click="saveDrawerData" class="drawer-save-btn">
<template #icon>
<SaveOutlined />
</template>
保存
</a-button>
</template>
<!-- 抽屉滚动容器绑定ref和滚动样式 -->
<div ref="drawerScrollRef" class="drawer-scroll-container">
<!-- 加载中状态 -->
<div v-if="drawerPagination.loading && drawerDataList.length === 0" class="drawer-loading">
<Spin size="large" />
</div>
<!-- 卡片网格展示 - 优化布局横向一行展示名称保存路径 -->
<a-card v-else :bordered="false" class="drawer-card-container grid-container">
<a-card-grid v-for="(item, index) in drawerDataList" :key="item.id" class="drawer-card-grid">
<!-- 竖向封面移除预览功能仅保留基础展示 -->
<div class="grid-cover vertical-cover" v-if="drawerType!='collect'">
<a-image :preview="false" :src="item.coverUrl" fit="cover" />
</div>
<!-- 名称横向一行展示标签 + 内容 在同一行 -->
<div class="grid-item horizontal-item name">
<label class="drawer-label">名称</label>
<span>{{ item.name || '未命名' }}</span>
</div>
<!-- 保存文件夹横向一行展示标签 + 输入框 在同一行 -->
<div class="grid-item horizontal-item save-folder">
<label class="drawer-label">保存</label>
<a-input v-model:value="item.saveFolder" size="small" placeholder="默认用名称作文件夹" class="save-path-input" />
</div>
<div class="grid-item horizontal-item name">
<label class="drawer-label">集数</label>
<span>{{ item.total || '0' }}</span>
</div>
<!-- 同步开关保持原有逻辑横向对齐 -->
<div class="grid-item horizontal-item sync-switch">
<label class="drawer-label">同步</label>
<a-switch :checked="item.sync" @change="() => toggleDrawerItemSync(item, index)" size="small" />
</div>
</a-card-grid>
</a-card>
<!-- 无更多数据提示 -->
<div v-if="!drawerPagination.hasMore && drawerDataList.length > 0" class="no-more-data">
<!-- 已加载全部{{ getDrawerTypeName() }}数据 -->
</div>
<!-- 加载下一页中 -->
<div v-if="drawerPagination.loading && drawerDataList.length > 0" class="loading-more">
<Spin size="small" />
<span>加载中...</span>
</div>
</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>
<a-button type="primary" @click="addNew" :loading="formLoading">
<template #icon>
<PlusOutlined />
</template>
新增
</a-button>
</div>
</template>
<template #bodyCell="{ column, text, record }">
<template v-if="column.dataIndex === 'status'">
<!-- a-switch 显示状态绑定切换逻辑 -->
<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="{
fontSize: '12px',
color: record.status === 1 ? '#52c41a' : '#ff4d4f'
}">
{{ StatusDict[record.status] }}
</span>
</div>
</template>
<template v-else-if="column.dataIndex === 'cookies'">
<a-button @click="showCookies(record)">查看</a-button>
</template>
<template v-else-if="column.dataIndex === 'upSecUserIds'">
<a-button @click="showUpers(record)">查看</a-button>
</template>
<template v-else-if="column.dataIndex === 'edit'">
<!-- 已删除开启/停止按钮仅保留编辑和删除 -->
<a-button :disabled="showModal || loading" type="link" @click="edit(record)">
<template #icon>
<EditFilled />
</template>
编辑
</a-button>
<a-button :disabled="loading" type="link" @click="deleted(record.id)" danger>
<template #icon>
<DeleteOutlined />
</template>
删除
</a-button>
</template>
<div v-else class="text-subtext">
{{ text }}
</div>
</template>
</a-table>
<!-- Cookie详情弹窗 -->
<a-modal title="Cookie 详情" :visible="showCookiesModal" style="width:1200px;" @cancel="showCookiesModal = false" @ok="showCookiesModal = false">
<div class="cookie-content">
{{ showCookiesData || '无Cookie数据' }}
</div>
</a-modal>
<!-- 博主信息弹窗 -->
<a-modal title="要同步的博主信息" :visible="showUpersModal" style="width:1200px;" @cancel="showUpersModal = false" @ok="showUpersModal = false">
<div class="cookie-content">
{{ showUpersData || '未设置' }}
</div>
</a-modal>
</template>
<style scoped lang="less">
// 原有非抽屉样式(保留,不改动)
.cookie-content {
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
overflow-y: auto;
padding: 10px;
box-sizing: border-box;
}
.ant-form-item {
margin-bottom: 10px;
}
/* 透明滚动条样式 - WebKit内核浏览器 */
.cookie-content::-webkit-scrollbar {
width: 6px; /* 更细的滚动条 */
}
.cookie-content::-webkit-scrollbar-track {
background: transparent;
}
.cookie-content::-webkit-scrollbar-thumb {
background: rgba(150, 150, 150, 0.2); /* 浅灰透明 */
border-radius: 3px;
}
.cookie-content::-webkit-scrollbar-thumb:hover {
background: rgba(150, 150, 150, 0.4); /* 略深一点的透明 */
}
.cookie-content::-webkit-scrollbar-corner {
background: transparent;
}
.form-item-div {
width: 300px;
}
.form-item-div-input {
width: 150px;
margin-left: 10px;
}
/* Firefox 透明滚动条适配 */
.cookie-content {
scrollbar-width: thin;
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
}
/* a-textarea 透明滚动条 */
:deep(.ant-input-textarea-input) {
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
}
:deep(.ant-input-textarea-input)::-webkit-scrollbar {
width: 6px;
height: 6px;
}
:deep(.ant-input-textarea-input)::-webkit-scrollbar-track {
background: transparent;
}
:deep(.ant-input-textarea-input)::-webkit-scrollbar-thumb {
background: rgba(150, 150, 150, 0.2);
border-radius: 3px;
}
:deep(.ant-input-textarea-input)::-webkit-scrollbar-thumb:hover {
background: rgba(150, 150, 150, 0.4);
}
:deep(.ant-input-textarea-input)::-webkit-scrollbar-corner {
background: transparent;
}
/* 禁用状态的输入框样式优化 */
:deep(.ant-input-disabled) {
background-color: #f5f5f5 !important;
color: #666 !important;
}
/* 修复:移除对alert的自定义样式修改,还原官方默认样式 */
.alert-wrapper {
flex: 1;
margin-bottom: 0 !important;
}
// ========== 精准匹配模板:a-card-grid 样式优化 ==========
/* 父容器:解决卡片间隙,避免换行(不改动模板,仅样式控制) */
.drawer-card-container.grid-container {
// 清除 a-card 自带内边距,避免与卡片间隙冲突
:deep(.ant-card-body) {
padding: 16px;
margin: 0;
}
// 启用 Flex 布局,实现卡片均匀间隙(核心:gap 不与组件自带样式冲突)
display: flex;
flex-wrap: wrap;
gap: 20px; /* 卡片之间的横向/纵向间隙(可自定义,不会换行) */
box-sizing: border-box;
/* 固定左对齐:单元素/多元素都保持从左往右排列 */
justify-content: flex-start;
/* 新增:兜底最小高度,避免空容器塌陷 */
min-height: 200px;
/* 新增:左内边距,让单元素和多元素视觉对齐一致 */
padding-left: 5px;
}
/* 新增:单元素样式修复 - 关键修复(仅防变形,不居中) */
.drawer-card-container.grid-container:has(:only-child) :deep(.drawer-card-grid) {
/* 单元素时保持3列布局的宽度,不拉伸,防止变形 */
width: calc(33.333% - 10.333px) !important;
/* 可选:如果觉得单元素太窄,可取消下面注释,设置固定宽度 */
/* width: 300px !important; */
min-width: 200px; /* 强制最小宽度,防止压缩变形 */
}
/* a-card-grid 核心样式:圆角 + 固定宽度 + 内边距 */
:deep(.drawer-card-grid) {
// 精确宽度计算:3列均分,减去 gap 分摊值,避免换行
width: calc(33.333% - 10.333px) !important; /* 20px gap 分摊到3列,每列减 ~13.333px */
/* 新增:最小宽度限制,防止变形(核心) */
min-width: 200px;
margin: 5px !important; /* 清除组件自带 margin,避免间隙重复 */
border-radius: 12px !important; /* 卡片圆角(可自定义大小) */
padding: 16px !important; /* 卡片内部整体内边距 */
box-sizing: border-box;
border: 1px solid #f0f0f0; /* 卡片边框,增强圆角视觉感 */
// 卡片内部竖向排列,确保每个内容块独占一行
display: flex;
flex-direction: column;
gap: 12px; /* 卡片内部元素之间的间距(可自定义) */
}
/* 图片容器:横向铺满卡片 + 自带内边距 */
:deep(.drawer-card-grid .grid-cover.vertical-cover) {
width: 100% !important; /* 横向铺满卡片 */
padding: 4px; /* 图片内部内边距(可自定义,实现图片与卡片边框的间距) */
box-sizing: border-box;
border-radius: 8px; /* 图片容器轻微圆角,与卡片圆角呼应 */
overflow: hidden;
}
/* 图片:强制铺满容器,不拉伸 */
:deep(.drawer-card-grid .vertical-cover .ant-image) {
width: 100% !important;
height: auto !important; /* 高度自适应,保持图片比例 */
display: block;
}
:deep(.drawer-card-grid .vertical-cover .ant-image-img) {
width: 100% !important;
height: 100% !important;
object-fit: cover !important; /* 保持图片比例,裁剪多余部分 */
}
/* 名称、保存路径、同步开关:独占一行样式 */
:deep(.drawer-card-grid .grid-item.horizontal-item) {
width: 100% !important; /* 独占一行,横向铺满卡片 */
display: flex;
align-items: center; /* 标签与内容垂直居中 */
margin: 0 !important; /* 清除多余间距,确保独占一行 */
padding: 4px 0; /* 上下轻微内边距,增强可读性 */
}
// 亮色模式(保留)
:deep(.drawer-label) {
flex-shrink: 0;
font-size: 12px;
color: rgba(0, 0, 0, 0.6);
}
/* 名称内容、输入框:占满剩余宽度 */
:deep(.drawer-card-grid .horizontal-item span) {
flex: 1;
font-size: 12px;
color: rgba(0, 0, 0, 0.88);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.drawer-card-grid .horizontal-item .save-path-input) {
flex: 1;
width: 100% !important;
font-size: 12px;
}
/* 同步开关:两端对齐,保持独占一行 */
:deep(.drawer-card-grid .sync-switch) {
justify-content: space-between; /* 标签左对齐,开关右对齐 */
}
/* 响应式适配:小屏幕自动调整列数,保持间隙不换行 */
@media (max-width: 768px) {
:deep(.drawer-card-grid) {
width: calc(50% - 10px) !important; /* 2列均分,20px gap 每列减10px */
/* 新增:响应式最小宽度 */
min-width: 180px;
}
/* 响应式下单元素适配(保持左对齐) */
.drawer-card-container.grid-container:has(:only-child) :deep(.drawer-card-grid) {
width: calc(50% - 10px) !important;
min-width: 180px;
}
}
@media (max-width: 480px) {
:deep(.drawer-card-grid) {
width: 100% !important; /* 1列铺满,无间距分摊 */
/* 新增:移动端最小宽度铺满 */
min-width: 100%;
}
/* 移动端单元素适配(1列铺满) */
.drawer-card-container.grid-container:has(:only-child) :deep(.drawer-card-grid) {
width: 100% !important;
min-width: 100%;
}
}
// ========== 黑暗模式样式(html.dark-mode + 链式类名,光效增强 + hover 效果) ==========
/* 卡片:深色背景 + 隐式边框 + 增强光效(默认状态) */
html.dark-mode .drawer-card-container.grid-container .drawer-card-grid {
border-color: rgba(142, 140, 140, 0.1) !important; /* 边框透明化(隐掉) */
background-color: #1a1a2e !important;
/* 增强光效:双层阴影+浅蓝紫微光,更醒目且不刺眼(核心修改) */
box-shadow: 0 6px 16px rgba(0, 20, 60, 0.4),
/* 主阴影:加深蓝紫调,增大模糊半径,光效更明显 */ 0 2px 6px rgba(100, 120, 255, 0.2),
/* 辅阴影:新增浅蓝紫微光,光效更突出 */ inset 0 1px 0 rgba(255, 255, 255, 0.05) !important; /* 内阴影:轻微高光,增强卡片质感 */
/* 可选:完全隐藏边框 */
/* border: none !important; */
transition: all 0.3s ease-in-out !important; /* 过渡动画:hover 效果平滑切换,不生硬 */
}
/* 卡片 hover 效果:悬浮时光效更突出,层次感拉满 */
html.dark-mode .drawer-card-container.grid-container .drawer-card-grid:hover {
box-shadow: 0 8px 24px rgba(0, 30, 80, 0.5),
/* 主阴影:进一步加深、扩大,立体感更强 */ 0 4px 12px rgba(120, 140, 255, 0.35),
/* 辅阴影:蓝紫微光更亮,光效更醒目 */ inset 0 1px 0 rgba(255, 255, 255, 0.08) !important; /* 内阴影:高光增强,质感更优 */
transform: translateY(-2px) !important; /* 可选:轻微上浮,增强交互感(可删除) */
}
/* label:强制纯白色(核心需求,格式不变) */
html.dark-mode .drawer-card-container.grid-container .drawer-card-grid .horizontal-item label {
color: #ffffff !important;
}
/* 名称内容:浅白色(格式不变) */
html.dark-mode .drawer-card-container.grid-container .drawer-card-grid .horizontal-item span {
color: rgba(255, 255, 255, 0.88) !important;
}
/* 输入框:深色适配(格式不变) */
html.dark-mode .drawer-card-container.grid-container .drawer-card-grid .ant-input {
background-color: #2f2f2f !important;
border-color: #404040 !important;
color: rgba(255, 255, 255, 0.88) !important;
}
</style>
<!-- 新增全局样式块确保alert样式不受scoped影响仅针对alert还原默认 -->
<style lang="less">
.full-modal {
.ant-modal {
max-width: 100%;
top: 0;
padding-bottom: 0;
margin: 0;
}
.ant-modal-content {
display: flex;
flex-direction: column;
height: calc(100vh);
}
.ant-modal-body {
flex: 1;
}
}
/* 还原Ant Design Vue alert官方默认样式,消除scoped样式的间接影响 */
.ant-alert {
box-sizing: border-box;
margin: 0;
/* padding: 16px 16px; */
color: rgba(0, 0, 0, 0.88);
font-size: 14px;
line-height: 1.5714285714;
list-style: none;
position: relative;
display: flex;
align-items: center;
border-radius: 8px;
}
.ant-alert-small {
padding: 8px 16px;
font-size: 12px;
border-radius: 4px;
}
/* 还原alert不同类型的官方默认颜色 */
.ant-alert-info {
background-color: #e6f4ff;
border: 1px solid #91caff;
}
.ant-alert-info .ant-alert-message {
color: #1677ff;
}
/* 还原alert不同类型的官方默认颜色 */
.ant-alert-success {
background-color: #daf1d3;
border: 1px solid #5bbc51;
}
.ant-alert-success .ant-alert-message {
color: #228b22;
}
/* 还原 Alert 各类型的官方默认颜色 */
/* 错误(error)类型 - 红色系 */
.ant-alert-error {
background-color: #fff1f0; /* 错误类型背景色 */
border: 1px solid #ff4d4f; /* 错误类型边框色 */
}
.ant-alert-error .ant-alert-message {
color: #f5222d; /* 错误类型文字主色 */
}
.ant-alert-warning {
background-color: #fffbe6;
border: 1px solid #ffe58f;
}
.ant-alert-warning .ant-alert-message {
color: #faad14;
}
/* 确保alert图标颜色同步官方样式 */
.ant-alert-info .ant-alert-icon {
color: #1677ff;
}
.ant-alert-warning .ant-alert-icon {
color: #faad14;
}
</style>