fix: 删除旧的文件

This commit is contained in:
Edan's Win11
2025-06-03 21:24:37 +08:00
parent 2962fb7c5c
commit 476ebc26b8
18 changed files with 0 additions and 6779 deletions
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.3 KiB

-548
View File
@@ -1,548 +0,0 @@
<template>
<div class="account-view">
<h1>账号管理</h1>
<div class="account-container">
<!-- 平台选项卡 -->
<div class="platform-tabs">
<div v-for="platform in platforms" :key="platform.id" class="platform-tab"
:class="{ active: activePlatform === platform.id }" @click="activePlatform = platform.id">
{{ platform.name }}
</div>
</div>
<div class="filter-bar">
<div class="filter-item">
<select v-model="statusFilter">
<option value="">账号状态</option>
<option value="正常">正常</option>
<option value="异常">异常</option>
<!-- <option value="已封禁">已封禁</option> -->
</select>
</div>
<!-- <div class="filter-item">
<select>
<option>分组</option>
<option>默认分组</option>
<option>自媒体</option>
<option>电商</option>
</select>
</div> -->
<div class="search-box">
<input type="text" placeholder="输入账号名称搜索" v-model="searchQuery" />
<button><i class="search-icon"></i></button>
</div>
<button class="refresh-btn" @click="refreshAccounts" title="刷新账号列表">
<i class="refresh-icon"></i>
</button>
<button class="btn-add" @click="showAddAccountModal">添加账号</button>
</div>
<div class="account-list">
<!-- 加载状态 -->
<div v-if="isLoading" class="loading-state">
<div class="loading-spinner"></div>
<p>正在加载账号列表...</p>
</div>
<!-- 无数据提示 -->
<div v-else-if="filteredAccounts.length === 0" class="empty-state">
<p>暂无账号数据</p>
<button class="btn-add-small" @click="showAddAccountModal">添加账号</button>
</div>
<!-- 账号列表 -->
<div v-else v-for="account in filteredAccounts" :key="account.id" class="account-card">
<div class="account-avatar">
<img :src="account.avatar" alt="账号头像" />
</div>
<div class="account-info">
<div class="account-name">{{ account.name }}</div>
<div class="account-stats">
<div class="stat-item">
<span class="stat-label">粉丝</span>
<span class="stat-value">{{ account.followers }}</span>
</div>
<div class="stat-item">
<span class="stat-label">获赞</span>
<span class="stat-value">{{ account.likes }}</span>
</div>
<div class="stat-item">
<span class="stat-label">播放量</span>
<span class="stat-value">{{ account.views }}</span>
</div>
</div>
<div class="account-status" :class="'status-' + account.statusType">{{ account.status }}</div>
</div>
</div>
</div>
</div>
<!-- 添加账号模态窗口 -->
<AddAccountModal :visible="isAddAccountModalVisible" :platformId="activePlatform" @close="closeAddAccountModal"
@login="handleAccountLogin" @account-added="handleAccountAdded" />
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import AddAccountModal from './AddAccountModal.vue';
import { API_BASE_URL, globalAccountsCache } from '../config';
import axios from 'axios'
import tx from '@assets/fengbaobao.png'
// 平台数据
const platforms = [
{ id: 'douyin', name: '抖音' },
{ id: 'kuaishou', name: '快手' },
{ id: 'xiaohongshu', name: '小红书' },
{ id: 'shipinhao', name: '视频号' },
];
// 当前选中的平台
const activePlatform = ref('douyin');
// 搜索关键词
const searchQuery = ref('');
// 状态筛选
const statusFilter = ref('');
// 账号数据
const accountsData = ref({
douyin: [],
kuaishou: [],
xiaohongshu: [],
shipinhao: []
});
// 加载状态
const isLoading = ref(false);
// 获取账号列表
const fetchAccounts = async (forceRefresh = false) => {
// 如果全局缓存有效且不强制刷新,则使用缓存数据
if (!forceRefresh && globalAccountsCache.isValid()) {
console.log("使用缓存的账号数据");
processAccountsData(globalAccountsCache.accounts);
return;
}
isLoading.value = true;
try {
console.log("getValidAccounts请求前",`/api/getValidAccounts`);
const response = await axios.get(`/api/getValidAccounts`);
console.log("getValidAccounts", response);
if (response.data.code === 200 && response.data.data) {
// 更新全局缓存
globalAccountsCache.updateCache(response.data.data);
// 处理账号数据
processAccountsData(response.data.data);
}
} catch (error) {
console.error('获取账号列表失败:', error);
// 如果获取失败,使用空数组
accountsData.value = {
douyin: [],
xiaohongshu: [],
shipinhao: []
};
} finally {
isLoading.value = false;
}
};
// 处理账号数据
const processAccountsData = (data) => {
// 清空所有平台的账号数据
accountsData.value = {
douyin: [],
xiaohongshu: [],
shipinhao: []
};
// 根据type值将账号分配到对应平台
data.forEach(account => {
const formattedAccount = {
id: account[0]|| Math.random().toString(36).substr(2, 9),
name: account[3] || '未命名账号',
avatar: tx,
followers: formatNumber(account.followers || 0),
likes: formatNumber(account.likes || 0),
views: formatNumber(account.views || 0),
status: account[4] === 1 ? '正常' : '异常',
statusType: account[4] === 1 ? 'normal' : 'warning',
cookiejson:account[2],
userData: account
};
// 根据type值分配到对应平台
switch (account[1]) {
case 3: // 抖音
accountsData.value.douyin.push(formattedAccount);
break;
case 4: // 新增抖音平台
accountsData.value.kuaishou.push(formattedAccount);
break;
case 1: // 小红书
accountsData.value.xiaohongshu.push(formattedAccount);
break;
case 2: // 视频号
accountsData.value.shipinhao.push(formattedAccount);
break;
default:
// 如果type不匹配,默认放入当前选中的平台
accountsData.value[activePlatform.value].push(formattedAccount);
break;
}
});
};
// 强制刷新账号列表
const refreshAccounts = () => {
fetchAccounts(true);
};
// 格式化数字
const formatNumber = (num) => {
if (num >= 100000000) {
return (num / 100000000).toFixed(1) + '亿';
} else if (num >= 10000) {
return (num / 10000).toFixed(1) + '万';
}
return num.toString();
};
// 获取平台ID数字
const getPlatformIdNumber = (platform) => {
switch (platform) {
case 'douyin': return 3;
case 'douyin2': return 4; // 新增抖音平台,编号为4
case 'xiaohongshu': return 1;
case 'shipinhao': return 2;
default: return 0;
}
};
// 根据当前选中平台和搜索关键词过滤账号列表
const filteredAccounts = computed(() => {
const accounts = accountsData.value[activePlatform.value] || [];
let filtered = accounts;
// 按名称搜索过滤
if (searchQuery.value) {
filtered = filtered.filter(account =>
account.name.toLowerCase().includes(searchQuery.value.toLowerCase())
);
}
// 按状态过滤
if (statusFilter.value) {
filtered = filtered.filter(account => account.status === statusFilter.value);
}
return filtered;
});
// 在组件挂载时获取账号列表
onMounted(() => {
fetchAccounts();
});
// 监听平台切换,重新获取账号列表
watch(activePlatform, () => {
// 平台切换时不需要强制刷新,可以使用缓存
fetchAccounts(false);
});
// 添加账号模态窗口控制
const isAddAccountModalVisible = ref(false);
const showAddAccountModal = () => {
console.log("showAddAccountModal");
// 确保每次打开都是全新的弹窗状态
isAddAccountModalVisible.value = true;
};
const closeAddAccountModal = () => {
isAddAccountModalVisible.value = false;
// 弹窗关闭后,确保下次打开是全新状态
console.log('弹窗已关闭,状态已重置');
};
const handleAccountAdded = () => {
// 账号添加成功后,不再自动刷新列表
// 用户可以点击刷新按钮手动刷新
console.log('账号添加成功,可点击刷新按钮更新列表');
};
const handleAccountLogin = (data) => {
console.log('账号登录状态:', data);
// 登录成功后不再自动刷新账号列表
// 用户可以点击刷新按钮手动刷新
};
</script>
<style scoped>
.account-view {
padding: 20px;
}
h1 {
margin-bottom: 20px;
font-size: 24px;
}
.account-container {
background-color: #fff;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
width: 100%;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 15px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
width: 100%;
color: #666;
}
.empty-state p {
margin-bottom: 15px;
font-size: 16px;
}
.btn-add-small {
padding: 8px 16px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.btn-add-small:hover {
background-color: #40a9ff;
}
.filter-bar {
display: flex;
margin-bottom: 20px;
align-items: center;
}
.filter-item {
margin-right: 15px;
}
.filter-item select {
padding: 8px 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
}
.search-box {
display: flex;
margin-right: 15px;
flex: 1;
}
.search-box input {
flex: 1;
padding: 8px 15px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
}
.search-box button {
padding: 8px 15px;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-left: none;
border-radius: 0 4px 4px 0;
}
.refresh-btn {
background-color: #f5f5f5;
color: #666;
border: 1px solid #ddd;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.refresh-btn:hover {
background-color: #e6e6e6;
}
.refresh-icon {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #666;
border-radius: 50%;
border-top-color: transparent;
position: relative;
}
.refresh-icon:before {
content: '';
position: absolute;
top: -5px;
right: 0;
width: 0;
height: 0;
border-style: solid;
border-width: 0 0 6px 6px;
border-color: transparent transparent #666 transparent;
}
.btn-add {
background-color: #6366f1;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
}
.account-list {
margin-top: 20px;
}
.account-card {
display: flex;
align-items: center;
padding: 15px;
border: 1px solid #eee;
border-radius: 8px;
margin-bottom: 10px;
}
.account-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
overflow: hidden;
margin-right: 15px;
}
.account-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.account-info {
display: flex;
flex-direction: column;
flex: 1;
}
.account-name {
font-weight: bold;
margin-bottom: 5px;
font-size: 16px;
}
.account-stats {
display: flex;
margin-bottom: 5px;
}
.stat-item {
margin-right: 15px;
display: flex;
align-items: center;
}
.stat-label {
color: #666;
font-size: 12px;
margin-right: 5px;
}
.stat-value {
font-weight: 500;
font-size: 14px;
}
.account-status {
font-size: 14px;
}
.status-normal {
color: #10b981;
}
.status-warning {
color: #f59e0b;
}
.status-danger {
color: #ef4444;
}
.platform-tabs {
display: flex;
border-bottom: 1px solid #eee;
margin-bottom: 20px;
}
.platform-tab {
padding: 10px 20px;
cursor: pointer;
border-bottom: 2px solid transparent;
font-weight: 500;
transition: all 0.2s;
}
.platform-tab:hover {
color: #6366f1;
}
.platform-tab.active {
color: #6366f1;
border-bottom-color: #6366f1;
}
</style>
@@ -1,362 +0,0 @@
<template>
<div class="modal-overlay" v-if="visible">
<div class="modal-container">
<div class="modal-header">
<h2>添加{{ getPlatformName() }}账号</h2>
<button class="close-btn" @click="closeModal">×</button>
</div>
<div class="modal-body">
<div class="input-group">
<label for="accountId">账号ID</label>
<input type="text" id="accountId" v-model="accountId" placeholder="请输入账号ID"
:class="{ 'error': showError }" />
<div class="error-message" v-if="showError">请输入账号ID</div>
</div>
<div class="qr-code-container" v-if="showQrCode">
<h3>请扫描二维码登录</h3>
<img :src="qrCodeSrc" alt="登录二维码" class="qr-code" />
</div>
<div class="message-container" v-if="message">
<h3>登录状态</h3>
<p>{{ message }}</p>
</div>
</div>
<div class="modal-footer">
<button class="cancel-btn" @click="closeModal">取消</button>
<button class="add-btn" @click="addAccount" :disabled="isLoading">
{{ isLoading ? '处理中...' : '添加账号' }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import {
ref,
defineProps,
defineEmits,
watch
} from 'vue';
import {
API_BASE_URL
} from '../config';
const props = defineProps({
visible: {
type: Boolean,
default: false
},
platformId: {
type: String,
default: 'douyin'
}
});
const emit = defineEmits(['close', 'login', 'account-added']);
// 表单数据
const accountId = ref('');
const showError = ref(false);
const isLoading = ref(false);
// SSE 连接相关
const showQrCode = ref(false);
const qrCodeSrc = ref('');
const message = ref('');
let eventSource = null;
// 监听visible变化,重置表单
watch(() => props.visible, (newValue) => {
if (!newValue) {
resetForm();
}
});
// 获取平台名称
const getPlatformName = () => {
const platformMap = {
'douyin': '抖音',
'douyin2': '抖音',
'xiaohongshu': '小红书',
'shipinhao': '视频号'
};
return platformMap[props.platformId] || '';
};
// 关闭模态窗口
const closeModal = () => {
// 如果有活跃的 EventSource 连接,关闭它
if (eventSource) {
eventSource.close();
eventSource = null;
}
emit('close');
};
// 重置表单
const resetForm = () => {
accountId.value = '';
showError.value = false;
isLoading.value = false;
showQrCode.value = false;
qrCodeSrc.value = '';
message.value = '';
// 确保关闭 EventSource 连接
if (eventSource) {
eventSource.close();
eventSource = null;
}
};
// 添加账号
const addAccount = () => {
// 表单验证
if (!accountId.value.trim()) {
showError.value = true;
return;
}
showError.value = false;
isLoading.value = true;
// 获取平台对应的type值
const platformTypeMap = {
'douyin': 3,
'kuaishou': 4, // 新增抖音平台,编号为4
'xiaohongshu': 1,
'shipinhao': 2
};
const platformType = platformTypeMap[props.platformId] || 3; // 默认使用抖音type=3
// 创建 EventSource 连接,使用正确的平台type
const url = `api/login?type=${platformType}&id=${accountId.value}`;
eventSource = new EventSource(url);
let count = 0;
// 接收事件流中的消息
eventSource.onmessage = (event) => {
console.log("onmessage");
count++;
const data = event.data;
if (count === 1) {
// 第一次收到的是 base64 图片(二维码)
console.log('收到二维码:', data);
qrCodeSrc.value = `${data}`;
showQrCode.value = true;
// 通知父组件登录过程开始
emit('login', {
status: 'qrcode',
platformId: props.platformId,
accountId: accountId.value
});
} else if (count === 2) {
// 第二次收到普通消息(登录结果)
console.log('收到消息:', data);
message.value = data;
isLoading.value = false;
// 主动关闭连接
eventSource.close();
eventSource = null;
// 通知父组件账号添加成功
emit('account-added', {
status: 'success',
platformId: props.platformId,
accountId: accountId.value,
message: data
});
// 显示成功提示并关闭弹窗
ElMessage({
message: `账号添加成功,请手动刷新: ${data}`,
type: 'success',
duration: 3000, // 显示时长,单位毫秒
showClose: true, // 是否显示关闭按钮
});
closeModal();
}
};
// 错误处理
eventSource.onerror = (err) => {
console.error('EventSource 发生错误:', err);
message.value = '连接异常中断';
isLoading.value = false;
if (eventSource) {
eventSource.close();
eventSource = null;
}
// 通知父组件登录失败
emit('login', {
status: 'error',
platformId: props.platformId,
accountId: accountId.value,
error: '连接异常中断'
});
// 显示错误提示并关闭弹窗
alert('登录失败: 连接异常中断');
closeModal();
};
};
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-container {
background-color: white;
border-radius: 8px;
width: 500px;
max-width: 90%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.modal-header h2 {
margin: 0;
font-size: 18px;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
}
.modal-body {
padding: 20px;
}
.input-group {
margin-bottom: 20px;
}
.input-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
input {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: #1890ff;
}
input.error {
border-color: #ff4d4f;
}
.error-message {
color: #ff4d4f;
font-size: 12px;
margin-top: 4px;
}
.modal-footer {
padding: 16px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.cancel-btn {
padding: 8px 16px;
background-color: #f7f7f7;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: #666;
}
.add-btn {
padding: 8px 16px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.add-btn:disabled {
background-color: #bae7ff;
cursor: not-allowed;
}
.qr-code-container {
margin-top: 20px;
text-align: center;
}
.qr-code {
width: 200px;
height: 200px;
margin: 10px auto;
display: block;
}
.message-container {
margin-top: 20px;
padding: 12px;
background-color: #f6f6f6;
border-radius: 4px;
}
.message-container h3 {
margin-top: 0;
margin-bottom: 8px;
font-size: 16px;
color: #333;
}
.message-container p {
margin: 0;
color: #666;
}
</style>
@@ -1,131 +0,0 @@
<template>
<div class="article-publish-view">
<h1>发布图文</h1>
<!-- 标题 -->
<div class="form-group">
<label>标题:</label>
<div class="input-wrapper">
<input type="text" class="form-input" placeholder="请输入标题" />
<span class="character-count">0/30</span>
</div>
</div>
<!-- 图片 -->
<div class="form-group">
<label>图片:</label>
<div class="image-upload-area">
<div class="upload-box">
<div class="plus-icon">+</div>
</div>
</div>
</div>
<!-- 活动 -->
<div class="form-group">
<label>活动:</label>
<div class="input-wrapper">
<input type="text" class="form-input" placeholder="请输入活动..." />
</div>
</div>
<!-- 按钮区域 -->
<div class="action-buttons">
<button class="btn btn-preview">预览内容</button>
<button class="btn btn-publish">零点发布</button>
</div>
</div>
</template>
<script setup>
// 这里可以添加组件逻辑
</script>
<style scoped>
.article-publish-view {
padding: 20px;
}
h1 {
margin-bottom: 30px;
font-size: 24px;
font-weight: 500;
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 10px;
font-weight: 500;
}
.input-wrapper {
position: relative;
}
.form-input {
width: 100%;
padding: 10px 15px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
outline: none;
}
.character-count {
position: absolute;
right: 10px;
top: 10px;
color: #999;
font-size: 12px;
}
.image-upload-area {
display: flex;
gap: 15px;
}
.upload-box {
width: 150px;
height: 150px;
border: 1px dashed #ccc;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background-color: #f9f9f9;
}
.plus-icon {
font-size: 40px;
color: #ccc;
}
.action-buttons {
display: flex;
gap: 15px;
margin-top: 30px;
}
.btn {
padding: 10px 20px;
border-radius: 4px;
border: none;
font-size: 14px;
cursor: pointer;
}
.btn-preview {
background-color: #fff;
border: 1px solid #e0e0e0;
}
.btn-publish {
background-color: #6366f1;
color: white;
}
</style>
-26
View File
@@ -1,26 +0,0 @@
<template>
<div class="data-view">
<h1>数据</h1>
<div class="data-container">
<p>这里是数据分析的内容</p>
</div>
</div>
</template>
<style scoped>
.data-view {
padding: 20px;
}
h1 {
margin-bottom: 20px;
font-size: 24px;
}
.data-container {
background-color: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
</style>
-209
View File
@@ -1,209 +0,0 @@
<template>
<div v-if="visible" class="login-modal-overlay" @click.self="closeModal">
<div class="login-modal">
<div class="login-header">
<h2>用户登录</h2>
<button class="close-btn" @click="closeModal">×</button>
</div>
<div class="login-body">
<div class="form-group">
<label for="username">用户名</label>
<input
type="text"
id="username"
v-model="username"
:class="{'error': errors.username}"
placeholder="请输入用户名"
/>
<span class="error-message" v-if="errors.username">{{ errors.username }}</span>
</div>
<div class="form-group">
<label for="password">密码</label>
<input
type="password"
id="password"
v-model="password"
:class="{'error': errors.password}"
placeholder="请输入密码"
/>
<span class="error-message" v-if="errors.password">{{ errors.password }}</span>
</div>
<div class="form-actions">
<button class="login-btn" @click="handleLogin">登录</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
const props = defineProps();
const emit = defineEmits(['close', 'login-success']);
const username = ref('');
const password = ref('');
const errors = reactive({
username: '',
password: ''
});
const validateForm = () => {
let isValid = true;
// 重置错误信息
errors.username = '';
errors.password = '';
// 验证用户名
if (!username.value) {
errors.username = '请输入用户名';
isValid = false;
} else if (username.value.length < 3) {
errors.username = '用户名长度不能少于3个字符';
isValid = false;
}
// 验证密码
if (!password.value) {
errors.password = '请输入密码';
isValid = false;
} else if (password.value.length < 6) {
errors.password = '密码长度不能少于6个字符';
isValid = false;
}
return isValid;
};
const handleLogin = () => {
if (validateForm()) {
// 模拟登录成功
setTimeout(() => {
// 这里可以添加实际的登录逻辑
console.log('登录成功', { username: username.value, password: password.value });
// 发送登录成功事件
emit('login-success', { username: username.value });
// 关闭模态窗
closeModal();
// 清空表单
username.value = '';
password.value = '';
}, 500);
}
};
const closeModal = () => {
emit('close');
};
</script>
<style scoped>
.login-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.login-modal {
background-color: white;
border-radius: 8px;
width: 400px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.login-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.login-header h2 {
margin: 0;
font-size: 18px;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
}
.login-body {
padding: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: #333;
}
.form-group input {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #4e6ef2;
}
.form-group input.error {
border-color: #f56c6c;
}
.error-message {
display: block;
color: #f56c6c;
font-size: 12px;
margin-top: 4px;
}
.form-actions {
margin-top: 24px;
}
.login-btn {
width: 100%;
background-color: #4e6ef2;
color: white;
border: none;
border-radius: 4px;
padding: 12px 0;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.login-btn:hover {
background-color: #3c5ae4;
}
</style>
@@ -1,688 +0,0 @@
<template>
<div class="material-library-container">
<h1>素材库</h1>
<!-- 操作栏 -->
<div class="action-bar">
<button class="upload-btn" @click="showUploadModal">
<i class="upload-icon">↑</i> 上传素材
</button>
<button class="batch-btn">
批量操作
</button>
<!-- 添加搜索框 -->
<div class="search-container">
<input
type="text"
v-model="searchQuery"
placeholder="搜索素材名称"
class="search-input"
@input="searchMaterials"
/>
<i class="search-icon">🔍</i>
</div>
</div>
<!-- 素材列表 -->
<div v-if="filteredMaterials.length > 0" class="materials-grid">
<div v-for="material in filteredMaterials" :key="material.file_name" class="material-card">
<!-- 视频预览 -->
<div class="material-preview">
<img src="@/assets/bofang.png" alt="播放图标" class="play-icon">
<button class="delete-btn" @click="deleteMaterial(material)" title="删除素材">×</button>
</div>
<!-- 素材信息 -->
<div class="material-info">
<div class="material-name">{{ material.filename }}</div>
<div class="material-meta">
<!-- <span>{{ material.filename }}</span> -->
<span>{{ formatFileSize(material.filesize) }}</span>
<span>{{ formatDate(material.upload_time) }}</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else class="empty-state">
<div class="empty-image"></div>
<p class="empty-text">暂无素材,点击上传素材按钮添加</p>
</div>
<!-- 上传素材弹窗 -->
<div class="upload-modal" v-if="isUploadModalVisible">
<div class="upload-modal-content">
<div class="modal-header">
<h3>上传素材</h3>
<button class="close-btn" @click="hideUploadModal">×</button>
</div>
<div class="modal-body">
<div class="filename-input">
<input
type="text"
v-model="customFilename"
placeholder="输入文件名称(可选)"
class="filename-field"
/>
</div>
<div class="upload-area" @click="triggerFileInput">
<div v-if="!selectedFile" class="upload-placeholder">
<span>点击上传素材</span>
<div class="upload-hint">图片最大支持20M</div>
</div>
<div v-else class="preview-container">
<video v-if="isVideo(selectedFile.name)" controls class="file-preview">
<source :src="previewUrl" type="video/mp4">
</video>
<img v-else :src="previewUrl" alt="预览" class="file-preview">
<button class="remove-file-btn" @click.stop="removeSelectedFile">×</button>
</div>
</div>
<div class="modal-footer">
<button class="cancel-btn" @click="hideUploadModal">取消</button>
<button
class="upload-submit-btn"
:disabled="!selectedFile || isUploading"
@click="uploadMaterial"
>
{{ isUploading ? '上传中...' : '上传' }}
</button>
</div>
</div>
</div>
</div>
<!-- 隐藏的文件输入 -->
<input
type="file"
ref="fileInput"
style="display: none"
@change="handleFileSelected"
accept="image/*,video/*"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';
// 素材列表
const materials = ref([]);
const filteredMaterials = ref([]);
const searchQuery = ref('');
// 上传弹窗状态
const isUploadModalVisible = ref(false);
const isUploading = ref(false);
const selectedFile = ref(null);
const previewUrl = ref('');
const customFilename = ref('');
const fileInput = ref(null);
// 显示上传弹窗
const showUploadModal = () => {
isUploadModalVisible.value = true;
selectedFile.value = null;
previewUrl.value = '';
customFilename.value = '';
// 重置文件输入框,确保每次打开弹窗都能正常选择文件
if (fileInput.value) {
fileInput.value.value = '';
}
};
// 隐藏上传弹窗
const hideUploadModal = () => {
isUploadModalVisible.value = false;
// 清理预览URL
if (previewUrl.value) {
URL.revokeObjectURL(previewUrl.value);
previewUrl.value = '';
}
};
// 移除点击弹窗外部关闭的函数,不再需要
// const closeModalOnOutsideClick = (event) => {
// if (event.target.classList.contains('upload-modal')) {
// hideUploadModal();
// }
// };
// 触发文件选择
const triggerFileInput = () => {
fileInput.value?.click();
};
// 处理文件选择
const handleFileSelected = (event) => {
const file = event.target.files[0];
if (file) {
selectedFile.value = file;
previewUrl.value = URL.createObjectURL(file);
// 如果没有自定义文件名,使用原始文件名
if (!customFilename.value) {
customFilename.value = file.name;
}
}
};
// 移除已选择的文件
const removeSelectedFile = () => {
if (previewUrl.value) {
URL.revokeObjectURL(previewUrl.value);
}
selectedFile.value = null;
previewUrl.value = '';
if (fileInput.value) {
fileInput.value.value = '';
}
};
// 上传素材
const uploadMaterial = async () => {
if (!selectedFile.value) return;
isUploading.value = true;
try {
const formData = new FormData();
formData.append('file', selectedFile.value);
console.log('上传文件:', selectedFile.value);
console.log('上传文件类型:', selectedFile.value.type);
// 使用自定义文件名或原始文件名
const filename = customFilename.value || selectedFile.value.name;
formData.append('filename', filename);
console.log('上传文件名:', filename);
const response = await axios.post('/api/uploadSave', formData);
console.log('上传响应:', response.data);
if (response.data.code === 200) {
// 上传成功,刷新素材列表
fetchMaterials();
hideUploadModal();
} else {
alert('上传失败: ' + (response.data.message || '未知错误'));
}
} catch (error) {
console.error('上传素材失败:', error);
alert('上传失败: ' + (error.response?.data?.message || error.message || '未知错误'));
} finally {
isUploading.value = false;
}
};
// 搜索素材
const searchMaterials = () => {
if (!searchQuery.value) {
filteredMaterials.value = materials.value;
return;
}
const query = searchQuery.value.toLowerCase();
filteredMaterials.value = materials.value.filter(material => {
return material.filename && material.filename.toLowerCase().includes(query);
});
};
// 获取素材列表
const fetchMaterials = async () => {
try {
const response = await axios.get(`/api/getFiles`);
console.log('获取素材列表响应:', response.data);
if (response.data.code === 200) {
// 处理文件路径,添加完整的请求地址
const materialItems = (response.data.data || []).map(item => {
return {
...item,
file_path: `/api${item.file_path}` // 添加API前缀到文件路径
};
});
// 对每个素材调用getFile接口获取视频数据
const materialPromises = materialItems.map(async (item) => {
try {
// 从file_path中提取文件名
const filename = item.file_path.split('/').pop();
if (!filename) return item;
// 调用getFile接口获取视频数据
const fileResponse = await axios.get(`/api/getFile`, {
params: { filename: filename }
});
console.log(`获取素材 ${filename} 数据:`, fileResponse.data);
if (fileResponse.data.code === 200) {
// 将获取的视频数据合并到素材对象中
return {
...item,
videoData: fileResponse.data.data
};
}
return item;
} catch (error) {
console.error(`获取素材 ${item.filename || item.file_name} 数据失败:`, error);
return item;
}
});
// 等待所有getFile请求完成
materials.value = await Promise.all(materialPromises);
// 初始化过滤后的素材列表
filteredMaterials.value = materials.value;
console.log('处理后的素材列表:', materials.value);
} else {
console.error('获取素材列表失败:', response.data.message);
}
} catch (error) {
console.error('获取素材列表失败:', error);
}
};
// 删除素材
const deleteMaterial = async (material) => {
console.log('要删除的素材:', material);
if (confirm(`确定要删除素材 ${material.filename || material.file_name} 吗?`)) {
try {
// 调用删除API
const response = await axios.get(`/api/deleteFile?id=${material.id}`);
console.log('删除素材响应:', response.data);
if (response.data.code === 200) {
// 删除成功,刷新素材列表
fetchMaterials();
alert('删除成功');
} else {
alert('删除失败: ' + (response.data.message || '未知错误'));
}
} catch (error) {
console.error('删除素材失败:', error);
alert('删除失败: ' + (error.response?.data?.message || error.message || '未知错误'));
}
}
};
// 格式化文件大小
const formatFileSize = (bytes) => {
if (!bytes || bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
};
// 格式化日期
const formatDate = (timestamp) => {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
// 判断是否为视频文件
const isVideo = (filename) => {
if (!filename) return false;
const videoExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv'];
return videoExtensions.some(ext => filename.toLowerCase().endsWith(ext));
};
// 组件挂载时获取素材列表
onMounted(() => {
fetchMaterials();
});
</script>
<style scoped>
.material-library-container {
padding: 20px;
}
h1 {
margin-bottom: 20px;
font-size: 24px;
font-weight: 500;
}
/* 操作栏样式 */
.action-bar {
display: flex;
justify-content: flex-start;
margin-bottom: 20px;
gap: 15px;
align-items: center;
}
.upload-btn {
background-color: #6366f1;
color: white;
border: none;
border-radius: 4px;
padding: 8px 15px;
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
}
.upload-icon {
margin-right: 5px;
font-size: 16px;
}
.batch-btn {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 8px 15px;
cursor: pointer;
font-size: 14px;
}
/* 素材网格样式 */
.materials-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.material-card {
background-color: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.material-card:hover {
transform: translateY(-5px);
}
.material-preview {
position: relative;
height: 150px;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.material-preview video,
.material-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.delete-btn {
position: absolute;
top: 5px;
right: 5px;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 16px;
opacity: 0;
transition: opacity 0.2s;
}
.material-card:hover .delete-btn {
opacity: 1;
}
.material-info {
padding: 10px;
}
.material-name {
font-size: 14px;
font-weight: 500;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.material-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #666;
}
/* 空状态样式 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100px 0;
background-color: #fff;
border-radius: 8px;
margin-top: 20px;
}
.empty-image {
width: 120px;
height: 120px;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23d1d5db" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>');
background-repeat: no-repeat;
background-position: center;
background-size: contain;
margin-bottom: 20px;
opacity: 0.5;
}
.empty-text {
color: #9ca3af;
font-size: 14px;
}
/* 上传弹窗样式 */
.upload-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.upload-modal-content {
background-color: white;
border-radius: 8px;
width: 500px;
max-width: 90%;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #eee;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.close-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #999;
}
.modal-body {
padding: 20px;
}
.filename-input {
margin-bottom: 15px;
}
.filename-field {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.upload-area {
border: 2px dashed #ddd;
border-radius: 8px;
padding: 30px;
text-align: center;
cursor: pointer;
margin-bottom: 20px;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.upload-area:hover {
border-color: #6366f1;
}
.upload-placeholder {
color: #666;
}
.upload-hint {
margin-top: 10px;
font-size: 12px;
color: #999;
}
.preview-container {
position: relative;
width: 100%;
height: 100%;
min-height: 150px;
display: flex;
align-items: center;
justify-content: center;
}
.file-preview {
max-width: 100%;
max-height: 200px;
object-fit: contain;
}
.remove-file-btn {
position: absolute;
top: 5px;
right: 5px;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 16px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.cancel-btn {
padding: 8px 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
cursor: pointer;
}
.upload-submit-btn {
padding: 8px 15px;
border: none;
border-radius: 4px;
background-color: #6366f1;
color: white;
cursor: pointer;
}
.upload-submit-btn:disabled {
background-color: #a5a6f6;
cursor: not-allowed;
}
/* 搜索框样式 */
.search-container {
position: relative;
flex-grow: 1;
max-width: 300px;
}
.search-input {
width: 100%;
padding: 8px 12px 8px 30px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
outline: none;
}
.search-input:focus {
border-color: #6366f1;
}
.search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 14px;
color: #999;
}
</style>
File diff suppressed because it is too large Load Diff
-375
View File
@@ -1,375 +0,0 @@
<template>
<div class="publish-view">
<h1>发布记录</h1>
<!-- 筛选栏 -->
<div class="filter-bar">
<div class="filter-items">
<div class="filter-item">
<select class="filter-select" v-model="selectedPublisher">
<option value="">全部发布人</option>
<option v-for="account in accountsList" :key="account.id" :value="account.id">
{{ account.name }}
</option>
</select>
</div>
<div class="filter-item">
<select class="filter-select">
<option value="">全部发布类型</option>
<option value="video">视频</option>
<option value="image">图文</option>
</select>
</div>
<!-- <div class="filter-item">
<select class="filter-select">
<option value="">全部推送状态</option>
<option value="success">成功</option>
<option value="failed">失败</option>
<option value="pending">待发布</option>
</select>
</div> -->
</div>
<div class="action-buttons">
<!-- 新增发布按钮及下拉菜单 -->
<div class="dropdown"
@mouseenter="showDropdown"
@mouseleave="hideDropdown">
<button class="btn btn-add">
<i class="add-icon">+</i> 新增发布
</button>
<div class="dropdown-menu" :class="{ 'show': dropdownVisible }">
<div class="dropdown-item" @click="addVideoPublishRoute">
<div class="item-icon video-icon"></div>
<span>视频</span>
</div>
<div class="dropdown-item">
<div class="item-icon image-icon"></div>
<span>图文</span>
</div>
<div class="dropdown-item" @click="goToMultiPublish">
<div class="item-icon multi-icon"></div>
<span>多平台发布</span>
</div>
<!-- <div class="dropdown-item">
<div class="item-icon article-icon"></div>
<span>文章</span>
</div>
<div class="dropdown-item">
<div class="item-icon wechat-icon"></div>
<span>公众号</span>
</div> -->
</div>
</div>
</div>
</div>
<!-- 空状态提示 -->
<div class="empty-state">
<div class="empty-image"></div>
<p class="empty-text">暂无发布记录,点击新增发布任务吧~</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { addVideoRoute } from '@/router';
import { globalAccountsCache } from '../config';
import axios from 'axios';
import tx from '@assets/fengbaobao.png'
const router = useRouter();
// 控制下拉菜单的显示状态
const dropdownVisible = ref(false);
// 选中的发布人
const selectedPublisher = ref('');
// 账号列表
const accountsList = ref([]);
// 显示下拉菜单
const showDropdown = () => {
dropdownVisible.value = true;
};
// 隐藏下拉菜单
const hideDropdown = () => {
dropdownVisible.value = false;
};
// 添加视频发布路由
const addVideoPublishRoute = () => {
const newRoute = addVideoRoute();
router.push(newRoute.path);
hideDropdown();
};
// 跳转到多平台发布页面
const goToMultiPublish = () => {
router.push('/multi-publish');
hideDropdown();
};
// 获取账号列表
const fetchAccounts = async () => {
try {
// 如果全局缓存有效,则使用缓存数据
if (globalAccountsCache.isValid()) {
processAccountsData(globalAccountsCache.accounts);
return;
}
// 否则从API获取数据
const response = await axios.get(`/api/getValidAccounts`);
if (response.data.code === 200 && response.data.data) {
// 更新全局缓存
globalAccountsCache.updateCache(response.data.data);
// 处理账号数据
processAccountsData(response.data.data);
}
} catch (error) {
console.error('获取账号列表失败:', error);
accountsList.value = [];
}
};
// 处理账号数据
const processAccountsData = (data) => {
// 提取所有账号的昵称信息
accountsList.value = data.map(account => ({
id: account[0] || Math.random().toString(36).substr(2, 9),
name: account[3] || '未命名账号',
type: account[1]
}));
};
// 组件挂载时获取账号列表
onMounted(() => {
fetchAccounts();
});
</script>
<style scoped>
.publish-view {
padding: 20px;
}
h1 {
margin-bottom: 20px;
font-size: 24px;
font-weight: 500;
}
/* 筛选栏样式 */
.filter-bar {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.filter-items {
display: flex;
gap: 15px;
}
.filter-select {
padding: 8px 15px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #fff;
min-width: 150px;
font-size: 14px;
outline: none;
}
/* 按钮样式 */
.action-buttons {
display: flex;
gap: 15px;
}
.btn {
padding: 8px 15px;
border-radius: 4px;
border: none;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.btn-export, .btn-delete {
background-color: #fff;
border: 1px solid #e0e0e0;
}
.btn-add {
background-color: #6366f1;
color: white;
position: relative;
}
.add-icon {
margin-right: 5px;
font-size: 16px;
}
/* 下拉菜单样式 */
.dropdown {
position: relative;
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
width: 200px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 5px;
z-index: 10;
overflow: hidden;
display: none;
padding-top: 5px; /* 添加顶部内边距 */
}
/* 添加一个看不见的连接区域 */
.dropdown::after {
content: '';
position: absolute;
height: 10px;
width: 100%;
bottom: -10px;
left: 0;
}
.dropdown-menu.show {
display: block;
}
.dropdown-item {
padding: 12px 15px;
display: flex;
align-items: center;
cursor: pointer;
}
.dropdown-item:hover {
background-color: #f5f5f5;
}
.item-icon {
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.video-icon {
background-color: #e0f2fe;
position: relative;
}
.video-icon::before {
content: '';
width: 10px;
height: 10px;
background-color: #0ea5e9;
border-radius: 50%;
}
.image-icon {
background-color: #dcfce7;
position: relative;
}
.image-icon::before {
content: '';
width: 10px;
height: 10px;
background-color: #22c55e;
border-radius: 50%;
}
.multi-icon {
background-color: #f0e7fe;
position: relative;
}
.multi-icon::before {
content: '';
width: 10px;
height: 10px;
background-color: #8b5cf6;
border-radius: 50%;
}
.article-icon {
background-color: #fef2f2;
position: relative;
}
.article-icon::before {
content: '';
width: 10px;
height: 10px;
background-color: #ef4444;
border-radius: 50%;
}
.wechat-icon {
background-color: #ecfdf5;
position: relative;
}
.wechat-icon::before {
content: '';
width: 10px;
height: 10px;
background-color: #10b981;
border-radius: 50%;
}
/* 空状态样式 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100px 0;
background-color: #fff;
border-radius: 8px;
margin-top: 20px;
}
.empty-image {
width: 120px;
height: 120px;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23d1d5db" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>');
background-repeat: no-repeat;
background-position: center;
background-size: contain;
margin-bottom: 20px;
opacity: 0.5;
}
.empty-text {
color: #9ca3af;
font-size: 14px;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -1,26 +0,0 @@
<template>
<div class="website-view">
<h1>网站</h1>
<div class="website-container">
<p>这里是网站管理的内容</p>
</div>
</div>
</template>
<style scoped>
.website-view {
padding: 20px;
}
h1 {
margin-bottom: 20px;
font-size: 24px;
}
.website-container {
background-color: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
</style>
@@ -1,245 +0,0 @@
<template>
<div class="main-layout">
<!-- 顶部导航栏 -->
<header class="header">
<div class="logo">
<img src="@/assets/favicon.ico" alt="Logo" class="logo-img" />
<span>呃呃呃呃呃</span>
</div>
<div class="header-tabs">
<div class="tab-item" v-for="tab in tabs" :key="tab.id">{{ tab.name }}</div>
</div>
<div class="user-info">
<button class="btn-upgrade">联系客服</button>
<div class="avatar">
<img src="@/assets/tudong.jpg" alt="Avatar" />
</div>
</div>
</header>
<!-- 主体内容区 -->
<div class="content-container">
<!-- 修改后的左侧导航栏 -->
<aside class="sidebar">
<nav>
<router-link
v-for="route in routes"
:key="route.path"
:to="route.path"
class="nav-item"
active-class="active"
>
<i :class="route.meta.icon"></i>
<span>{{ route.meta.title }}</span>
</router-link>
</nav>
</aside>
<!-- 右侧内容区 -->
<main class="main-content">
<router-view></router-view>
</main>
</div>
</div>
</template>
<script setup>
import { computed, ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
// 修改后的响应式路由列表
const routes = computed(() => {
return router.getRoutes()
.filter(route =>
route.meta?.title && // 必须有标题
!route.meta.hidden && // 过滤隐藏路由
route.path !== '/' && // 排除根重定向
!route.redirect // 排除其他重定向路由
)
.sort((a, b) => { // 保持路由顺序稳定
const order = ['/account', '/publish', '/website', '/data'];
return order.indexOf(a.path) - order.indexOf(b.path);
});
});
const filteredRoutes = computed(() => routes.value.filter(route => route.path !== '/video-publish'));
// 控制视频发布路由的显示
const showVideoPublishRoute = ref(false);
// 在组件挂载时检查本地存储
onMounted(() => {
const shouldShow = localStorage.getItem('showVideoPublishRoute');
if (shouldShow === 'true') {
showVideoPublishRoute.value = true;
}
});
// 移除视频发布路由
const removeVideoPublishRoute = () => {
showVideoPublishRoute.value = false;
localStorage.removeItem('showVideoPublishRoute');
// 如果当前在视频发布页面,则导航到发布中心
if (router.currentRoute.value.path === '/video-publish') {
router.push('/publish');
}
};
const tabs = [
{ id: 1, name: '抖音' },
{ id: 2, name: '小红书' },
{ id: 3, name: '微信视频号' },
{ id: 4, name: '快手' },
{ id: 5, name: '视频号小店' },
{ id: 6, name: 'TikTok' }
];
</script>
<style scoped>
.main-layout {
display: flex;
flex-direction: column;
height: 100vh;
}
.header {
height: 60px;
background-color: #fff;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
padding: 0 20px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}
.logo {
display: flex;
align-items: center;
font-weight: bold;
font-size: 18px;
margin-right: 40px;
}
.logo-img {
width: 30px;
height: 30px;
margin-right: 10px;
}
.header-tabs {
display: flex;
flex: 1;
}
.tab-item {
padding: 0 15px;
cursor: pointer;
height: 60px;
display: flex;
align-items: center;
}
.tab-item:hover {
color: #4e6ef2;
}
.user-info {
display: flex;
align-items: center;
}
.btn-upgrade {
background-color: #f0f2ff;
color: #4e6ef2;
border: none;
padding: 6px 12px;
border-radius: 4px;
margin-right: 15px;
cursor: pointer;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
overflow: hidden;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.content-container {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 200px;
background-color: #f8f9fc;
border-right: 1px solid #eee;
padding: 20px 0;
}
.nav-item {
display: flex;
align-items: center;
padding: 12px 20px;
color: #333;
text-decoration: none;
margin-bottom: 5px;
}
.nav-item i {
margin-right: 10px;
font-size: 18px;
}
.nav-item.active {
background-color: #e6f7ff;
color: #4e6ef2;
border-right: 3px solid #4e6ef2;
}
.nav-divider {
height: 1px;
background-color: #e0e0e0;
margin: 10px 20px;
}
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.nav-item-with-close {
position: relative;
display: flex;
align-items: center;
}
.close-btn {
position: absolute;
right: 10px;
background: none;
border: none;
color: #999;
font-size: 16px;
cursor: pointer;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #333;
}
</style>
-8
View File
@@ -1,8 +0,0 @@
<template>
</template>
<script>
</script>
<style>
</style>
-70
View File
@@ -1,70 +0,0 @@
<template>
<div style="padding: 20px;">
<button @click="openInput">添加账号</button>
<div v-if="showQrCode" style="margin-top: 20px;">
<h3>收到的二维码:</h3>
<img :src="qrCodeSrc" alt="二维码" style="width: 200px;" />
</div>
<div v-if="message" style="margin-top: 20px;">
<h3>收到的消息:</h3>
<p>{{ message }}</p>
</div>
</div>
</template>
<script setup>
import {
ref
} from 'vue'
const showQrCode = ref(false)
const qrCodeSrc = ref('')
const message = ref(null)
let eventSource = null
// 弹出输入框并直接建立 EventSource 连接
const openInput = () => {
const id = prompt('请输入ID:')
if (!id) return
// 直接创建 EventSource 连接,带上参数 type=2&id=xxx
const url = `http://localhost:5409/login?type=2&id=${id}&_=${Date.now()}`
eventSource = new EventSource(url)
let count = 0
// 接收事件流中的消息
eventSource.onmessage = (event) => {
count++
const data = event.data
if (count === 1) {
// 第一次收到的是 base64 图片
console.log('收到二维码:', data)
qrCodeSrc.value = `${data}`
showQrCode.value = true
} else if (count === 2) {
// 第二次收到普通消息
console.log('收到消息:', data)
message.value = data
// 主动关闭连接
eventSource.close()
alert('已完成操作,连接已关闭')
}
}
// 错误处理
eventSource.onerror = (err) => {
console.error('EventSource 发生错误:', err)
eventSource.close()
alert('连接异常中断')
}
}
</script>
<style scoped>
/* 可根据需要添加样式 */
</style>
-27
View File
@@ -1,27 +0,0 @@
// 全局配置文件
// API基础URL
export const API_BASE_URL = 'http://localhost:5409';
// 全局账号列表缓存
export const globalAccountsCache = {
accounts: null, // 缓存的账号数据
lastFetchTime: null, // 最后一次获取数据的时间
// 检查缓存是否有效
isValid() {
return this.accounts !== null;
},
// 更新缓存
updateCache(accounts) {
this.accounts = accounts;
this.lastFetchTime = new Date().getTime();
},
// 清除缓存
clearCache() {
this.accounts = null;
this.lastFetchTime = null;
}
};