fix: 删除旧的文件
This commit is contained in:
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 |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user