8a04a23e86
Add campaigns.html with 6 tabs (Overview/Products/Campaigns/Rankings/Audits/Tasks) using Vue 3 + Element Plus CDN. Add nav entry to uni-nav.js. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
582 lines
39 KiB
HTML
582 lines
39 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>宇之然内容创作平台 - 外推营销</title>
|
|
<link rel="stylesheet" href="element-plus.css">
|
|
<link rel="stylesheet" href="theme-modern.css">
|
|
<style>
|
|
.data-table { display: block; }
|
|
@media (max-width: 768px) { .data-table { display: none; } }
|
|
.stat-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 16px; }
|
|
.stat-card { background: #f8f9ff; border-radius: 10px; padding: 16px; text-align: center; }
|
|
.stat-card .num { font-size: 28px; font-weight: 700; color: #2563eb; }
|
|
.stat-card .label { font-size: 12px; color: #909399; margin-top: 4px; }
|
|
.search-engine-tag { margin: 2px; }
|
|
</style>
|
|
<script src="uni-nav.js"></script>
|
|
</head>
|
|
<body>
|
|
<div id="app" v-cloak>
|
|
<uni-nav title="外推营销" :username="currentUser.username" :is-admin="isAdmin" current-page="campaigns" @navigate="redirectToPage" @logout="logout">
|
|
</uni-nav>
|
|
<div class="main-content">
|
|
<main class="content-area">
|
|
<div class="card page-fade">
|
|
<div class="page-header">
|
|
<h2 class="page-title"><el-icon style="vertical-align:-2px;"><IconMenu /></el-icon> 外推营销</h2>
|
|
<div class="filter-bar">
|
|
<el-button size="default" :type="activeTab === 'overview' ? 'primary' : ''" @click="switchTab('overview')">概览</el-button>
|
|
<el-button size="default" :type="activeTab === 'products' ? 'primary' : ''" @click="switchTab('products')">产品管理</el-button>
|
|
<el-button size="default" :type="activeTab === 'campaigns' ? 'primary' : ''" @click="switchTab('campaigns')">活动管理</el-button>
|
|
<el-button size="default" :type="activeTab === 'rankings' ? 'primary' : ''" @click="switchTab('rankings')">关键词排名</el-button>
|
|
<el-button size="default" :type="activeTab === 'audits' ? 'primary' : ''" @click="switchTab('audits')">SEO审计</el-button>
|
|
<el-button size="default" :type="activeTab === 'tasks' ? 'primary' : ''" @click="switchTab('tasks')">优化建议</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="activeTab === 'overview'">
|
|
<div v-if="loading" class="card-loading">加载中...</div>
|
|
<template v-else>
|
|
<div class="stat-cards">
|
|
<div class="stat-card"><div class="num">{{ overview.products || 0 }}</div><div class="label">推广产品</div></div>
|
|
<div class="stat-card"><div class="num">{{ overview.campaigns || 0 }}</div><div class="label">推广活动</div></div>
|
|
<div class="stat-card"><div class="num">{{ overview.active_campaigns || 0 }}</div><div class="label">进行中</div></div>
|
|
<div class="stat-card"><div class="num">{{ overview.audits || 0 }}</div><div class="label">SEO审计</div></div>
|
|
<div class="stat-card"><div class="num">{{ overview.keywords || 0 }}</div><div class="label">追踪关键词</div></div>
|
|
<div class="stat-card"><div class="num">{{ overview.open_tasks || 0 }}</div><div class="label">待处理建议</div></div>
|
|
</div>
|
|
<div v-if="overview.type_breakdown" style="margin-top:16px;">
|
|
<h4 style="font-size:14px;color:#303133;margin-bottom:8px;">产品类型分布</h4>
|
|
<div style="display:flex;gap:12px;flex-wrap:wrap;">
|
|
<el-tag v-for="(cnt, type) in overview.type_breakdown" :key="type" size="large">{{ typeLabel(type) }}: {{ cnt }}</el-tag>
|
|
</div>
|
|
</div>
|
|
<div style="margin-top:20px;">
|
|
<el-button type="primary" @click="switchTab('products')">管理推广产品</el-button>
|
|
<el-button @click="switchTab('campaigns')">查看活动</el-button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="activeTab === 'products'">
|
|
<div class="toolbar">
|
|
<el-button type="primary" size="small" @click="showProductDialog = true">新增产品</el-button>
|
|
<span style="font-size:13px;color:#909399;margin-left:8px;">共 {{ products.length }} 个</span>
|
|
</div>
|
|
<div v-if="loading" class="card-loading">加载中...</div>
|
|
<template v-else-if="products.length === 0">
|
|
<div class="empty-state">
|
|
<div class="empty-text">暂无推广产品,点击上方按钮添加</div>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<el-table :data="products" border stripe class="data-table" style="width:100%">
|
|
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
|
<el-table-column prop="name" label="产品名称" min-width="120"></el-table-column>
|
|
<el-table-column label="类型" width="110">
|
|
<template #default="s">{{ typeLabel(s.row.type) }}</template>
|
|
</el-table-column>
|
|
<el-table-column prop="url" label="URL" min-width="180">
|
|
<template #default="s"><a :href="s.row.url" target="_blank" style="color:#409eff;">{{ s.row.url || '-' }}</a></template>
|
|
</el-table-column>
|
|
<el-table-column prop="domain" label="域名" width="140"></el-table-column>
|
|
<el-table-column label="操作" width="150" fixed="right">
|
|
<template #default="s">
|
|
<el-button size="small" @click="editProduct(s.row)">编辑</el-button>
|
|
<el-button size="small" type="danger" @click="deleteProduct(s.row)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<div v-if="products.length > pageSize" style="display:flex;justify-content:center;margin:12px 0;">
|
|
<el-pagination background layout="prev, pager, next" :total="products.length" :page-size="pageSize" :current-page="productPage" @current-change="productPage = $event"></el-pagination>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="activeTab === 'campaigns'">
|
|
<div class="toolbar">
|
|
<el-button type="primary" size="small" @click="showCampaignDialog = true">新增活动</el-button>
|
|
<span style="font-size:13px;color:#909399;margin-left:8px;">共 {{ campaigns.length }} 个</span>
|
|
</div>
|
|
<div v-if="loading" class="card-loading">加载中...</div>
|
|
<template v-else-if="campaigns.length === 0">
|
|
<div class="empty-state"><div class="empty-text">暂无推广活动</div></div>
|
|
</template>
|
|
<template v-else>
|
|
<el-table :data="campaigns" border stripe class="data-table" style="width:100%">
|
|
<el-table-column prop="name" label="活动名称" min-width="120"></el-table-column>
|
|
<el-table-column prop="product_name" label="关联产品" width="120"></el-table-column>
|
|
<el-table-column label="状态" width="90">
|
|
<template #default="s">
|
|
<el-tag :type="s.row.status === 'active' ? 'success' : s.row.status === 'paused' ? 'warning' : 'info'" size="small">{{ { active: '进行中', paused: '已暂停', completed: '已完成' }[s.row.status] || s.row.status }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="目标引擎" min-width="140">
|
|
<template #default="s">
|
|
<el-tag v-for="e in (s.row.target_engines || [])" :key="e" size="small" class="search-engine-tag">{{ engineLabel(e) }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="关键词数" width="80">
|
|
<template #default="s">{{ (s.row.keywords || []).length }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="200" fixed="right">
|
|
<template #default="s">
|
|
<el-button size="small" @click="viewCampaignKeywords(s.row)">关键词</el-button>
|
|
<el-button size="small" @click="editCampaign(s.row)">编辑</el-button>
|
|
<el-button size="small" type="danger" @click="deleteCampaign(s.row)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<div v-if="campaigns.length > pageSize" style="display:flex;justify-content:center;margin:12px 0;">
|
|
<el-pagination background layout="prev, pager, next" :total="campaigns.length" :page-size="pageSize" :current-page="campaignPage" @current-change="campaignPage = $event"></el-pagination>
|
|
</div>
|
|
</template>
|
|
|
|
<div v-if="selectedCampaign" style="margin-top:20px;border-top:1px solid #ebeef5;padding-top:16px;">
|
|
<h4 style="font-size:14px;color:#303133;margin-bottom:8px;">
|
|
关键词详情:{{ selectedCampaign.name }}
|
|
<el-button size="small" type="primary" style="margin-left:8px;" @click="showKeywordDialog = true">添加关键词</el-button>
|
|
</h4>
|
|
<el-table v-if="campaignKeywords.length" :data="campaignKeywords" border stripe class="data-table" style="width:100%">
|
|
<el-table-column prop="keyword" label="关键词" min-width="150"></el-table-column>
|
|
<el-table-column prop="search_volume" label="搜索量" width="80"></el-table-column>
|
|
<el-table-column label="难度" width="70">
|
|
<template #default="s"><el-tag :type="(s.row.difficulty || 0) > 0.7 ? 'danger' : (s.row.difficulty || 0) > 0.4 ? 'warning' : 'success'" size="small">{{ s.row.difficulty != null ? (s.row.difficulty * 100).toFixed(0) + '%' : '-' }}</el-tag></template>
|
|
</el-table-column>
|
|
<el-table-column prop="current_rank" label="当前排名" width="80"></el-table-column>
|
|
<el-table-column prop="target_rank" label="目标排名" width="80"></el-table-column>
|
|
<el-table-column prop="best_rank" label="最佳排名" width="80"></el-table-column>
|
|
<el-table-column label="操作" width="100">
|
|
<template #default="s">
|
|
<el-button size="small" type="danger" @click="deleteKeyword(s.row)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<div v-else style="text-align:center;padding:20px;color:#909399;font-size:13px;">暂无关键词数据</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="activeTab === 'rankings'">
|
|
<div class="toolbar">
|
|
<span style="font-size:13px;color:#909399;">
|
|
共 {{ keywordRankings.length }} 条记录
|
|
<span v-if="rankingOverview.with_rank != null"> · 有排名 {{ rankingOverview.with_rank }} 条</span>
|
|
</span>
|
|
</div>
|
|
<div v-if="loading" class="card-loading">加载中...</div>
|
|
<template v-else-if="keywordRankings.length === 0">
|
|
<div class="empty-state"><div class="empty-text">暂无排名数据,创建活动和关键词后将自动追踪</div></div>
|
|
</template>
|
|
<template v-else>
|
|
<el-table :data="keywordRankings" border stripe class="data-table" style="width:100%">
|
|
<el-table-column prop="keyword" label="关键词" min-width="140"></el-table-column>
|
|
<el-table-column label="搜索引擎" width="100">
|
|
<template #default="s">{{ engineLabel(s.row.search_engine) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="排名" width="80">
|
|
<template #default="s">
|
|
<el-tag :type="s.row.rank != null && s.row.rank <= 10 ? 'success' : s.row.rank != null && s.row.rank <= 30 ? 'warning' : 'info'" size="small">{{ s.row.rank || '-' }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="url_found" label="排名URL" min-width="200">
|
|
<template #default="s"><a :href="s.row.url_found" target="_blank" style="color:#409eff;font-size:12px;">{{ s.row.url_found ? s.row.url_found.substring(0, 50) + '...' : '-' }}</a></template>
|
|
</el-table-column>
|
|
<el-table-column label="检查时间" width="150">
|
|
<template #default="s">{{ s.row.checked_at ? new Date(s.row.checked_at).toLocaleString('zh-CN') : '-' }}</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<div v-if="keywordRankings.length > pageSize" style="display:flex;justify-content:center;margin:12px 0;">
|
|
<el-pagination background layout="prev, pager, next" :total="keywordRankings.length" :page-size="pageSize" :current-page="rankingPage" @current-change="rankingPage = $event"></el-pagination>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="activeTab === 'audits'">
|
|
<div class="toolbar">
|
|
<span style="font-size:13px;color:#909399;">共 {{ audits.length }} 条审计记录</span>
|
|
</div>
|
|
<div v-if="loading" class="card-loading">加载中...</div>
|
|
<template v-else-if="audits.length === 0">
|
|
<div class="empty-state"><div class="empty-text">暂无SEO审计记录</div></div>
|
|
</template>
|
|
<template v-else>
|
|
<el-table :data="audits" border stripe class="data-table" style="width:100%">
|
|
<el-table-column prop="product_name" label="产品" width="120"></el-table-column>
|
|
<el-table-column label="审计类型" width="80">
|
|
<template #default="s">{{ s.row.audit_type === 'full' ? '全面' : '快速' }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="总分" width="80">
|
|
<template #default="s">
|
|
<el-tag :type="(s.row.overall_score || 0) >= 80 ? 'success' : (s.row.overall_score || 0) >= 60 ? 'warning' : 'danger'" size="small">{{ s.row.overall_score != null ? s.row.overall_score.toFixed(1) : '-' }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="Meta" width="65"><template #default="s">{{ s.row.meta_score != null ? s.row.meta_score.toFixed(0) : '-' }}</template></el-table-column>
|
|
<el-table-column label="标题" width="65"><template #default="s">{{ s.row.heading_score != null ? s.row.heading_score.toFixed(0) : '-' }}</template></el-table-column>
|
|
<el-table-column label="内容" width="65"><template #default="s">{{ s.row.content_score != null ? s.row.content_score.toFixed(0) : '-' }}</template></el-table-column>
|
|
<el-table-column label="性能" width="65"><template #default="s">{{ s.row.perf_score != null ? s.row.perf_score.toFixed(0) : '-' }}</template></el-table-column>
|
|
<el-table-column label="链接" width="65"><template #default="s">{{ s.row.links_score != null ? s.row.links_score.toFixed(0) : '-' }}</template></el-table-column>
|
|
<el-table-column label="移动端" width="65"><template #default="s">{{ s.row.mobile_score != null ? s.row.mobile_score.toFixed(0) : '-' }}</template></el-table-column>
|
|
<el-table-column prop="issues_found" label="问题数" width="65"></el-table-column>
|
|
<el-table-column label="审计时间" width="150">
|
|
<template #default="s">{{ s.row.checked_at ? new Date(s.row.checked_at).toLocaleString('zh-CN') : '-' }}</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="activeTab === 'tasks'">
|
|
<div class="toolbar">
|
|
<el-button :type="taskFilter === 'open' ? 'primary' : ''" size="small" @click="taskFilter = 'open'; loadTasks()">待处理</el-button>
|
|
<el-button :type="taskFilter === 'resolved' ? 'primary' : ''" size="small" @click="taskFilter = 'resolved'; loadTasks()">已解决</el-button>
|
|
<el-button :type="taskFilter === null ? 'primary' : ''" size="small" @click="taskFilter = null; loadTasks()">全部</el-button>
|
|
</div>
|
|
<div v-if="loading" class="card-loading">加载中...</div>
|
|
<template v-else-if="optimizationTasks.length === 0">
|
|
<div class="empty-state"><div class="empty-text">暂无优化建议</div></div>
|
|
</template>
|
|
<template v-else>
|
|
<el-table :data="optimizationTasks" border stripe class="data-table" style="width:100%">
|
|
<el-table-column label="严重度" width="80">
|
|
<template #default="s">
|
|
<el-tag :type="s.row.severity === 'high' ? 'danger' : s.row.severity === 'medium' ? 'warning' : 'info'" size="small">{{ { high: '高', medium: '中', low: '低' }[s.row.severity] || s.row.severity }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="分类" width="90">
|
|
<template #default="s">{{ { meta: 'Meta', heading: '标题', content: '内容', performance: '性能', links: '链接', mobile: '移动端' }[s.row.category] || s.row.category }}</template>
|
|
</el-table-column>
|
|
<el-table-column prop="issue" label="问题" min-width="200"></el-table-column>
|
|
<el-table-column prop="recommendation" label="建议" min-width="200"></el-table-column>
|
|
<el-table-column label="状态" width="90">
|
|
<template #default="s">
|
|
<el-tag :type="s.row.status === 'resolved' ? 'success' : s.row.status === 'ignored' ? 'info' : 'warning'" size="small">{{ { open: '待处理', resolved: '已解决', ignored: '已忽略' }[s.row.status] || s.row.status }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="120" fixed="right">
|
|
<template #default="s">
|
|
<el-button v-if="s.row.status === 'open'" size="small" type="success" @click="updateTaskStatus(s.row, 'resolved')">解决</el-button>
|
|
<el-button size="small" @click="updateTaskStatus(s.row, 'ignored')">忽略</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Product Dialog -->
|
|
<el-dialog v-model="showProductDialog" :title="editingProduct ? '编辑产品' : '新增产品'" width="500px">
|
|
<el-form label-width="100px">
|
|
<el-form-item label="产品名称"><el-input v-model="productForm.name" placeholder="例: 我的个人博客"></el-input></el-form-item>
|
|
<el-form-item label="产品类型">
|
|
<el-select v-model="productForm.type" style="width:100%">
|
|
<el-option label="网站" value="website"></el-option>
|
|
<el-option label="文章" value="article"></el-option>
|
|
<el-option label="公众号" value="wechat_account"></el-option>
|
|
<el-option label="小程序" value="miniprogram"></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="URL"><el-input v-model="productForm.url" placeholder="https://example.com"></el-input></el-form-item>
|
|
<el-form-item label="域名"><el-input v-model="productForm.domain" placeholder="example.com"></el-input></el-form-item>
|
|
<el-form-item v-if="productForm.type === 'miniprogram'" label="AppID"><el-input v-model="productForm.app_id" placeholder="微信小程序AppID"></el-input></el-form-item>
|
|
<el-form-item v-if="productForm.type === 'wechat_account'" label="公众号ID"><el-input v-model="productForm.account_id" placeholder="公众号ID"></el-input></el-form-item>
|
|
<el-form-item label="描述"><el-input v-model="productForm.description" type="textarea" :rows="3"></el-input></el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="showProductDialog = false">取消</el-button>
|
|
<el-button type="primary" @click="saveProduct">{{ editingProduct ? '保存' : '创建' }}</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<!-- Campaign Dialog -->
|
|
<el-dialog v-model="showCampaignDialog" :title="editingCampaign ? '编辑活动' : '新增活动'" width="500px">
|
|
<el-form label-width="100px">
|
|
<el-form-item label="活动名称"><el-input v-model="campaignForm.name" placeholder="例: 百度SEO推广"></el-input></el-form-item>
|
|
<el-form-item label="关联产品">
|
|
<el-select v-model="campaignForm.product_id" style="width:100%" placeholder="选择推广产品">
|
|
<el-option v-for="p in products" :key="p.id" :label="p.name" :value="p.id"></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="状态">
|
|
<el-select v-model="campaignForm.status" style="width:100%">
|
|
<el-option label="进行中" value="active"></el-option>
|
|
<el-option label="已暂停" value="paused"></el-option>
|
|
<el-option label="已完成" value="completed"></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="目标引擎">
|
|
<el-checkbox-group v-model="campaignForm.target_engines">
|
|
<el-checkbox label="baidu">百度</el-checkbox>
|
|
<el-checkbox label="360">360</el-checkbox>
|
|
<el-checkbox label="sogou">搜狗</el-checkbox>
|
|
<el-checkbox label="wechat">微信搜索</el-checkbox>
|
|
<el-checkbox label="bing">Bing</el-checkbox>
|
|
</el-checkbox-group>
|
|
</el-form-item>
|
|
<el-form-item label="关键词">
|
|
<el-select v-model="campaignForm.keywords" multiple filterable allow-create default-first-option style="width:100%" placeholder="输入关键词后回车添加">
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="备注"><el-input v-model="campaignForm.notes" type="textarea" :rows="2"></el-input></el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="showCampaignDialog = false">取消</el-button>
|
|
<el-button type="primary" @click="saveCampaign">{{ editingCampaign ? '保存' : '创建' }}</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<!-- Keyword Dialog -->
|
|
<el-dialog v-model="showKeywordDialog" title="添加关键词" width="450px">
|
|
<el-form label-width="90px">
|
|
<el-form-item label="关键词"><el-input v-model="keywordForm.keyword" placeholder="输入关键词"></el-input></el-form-item>
|
|
<el-form-item label="搜索量"><el-input-number v-model="keywordForm.search_volume" :min="0" style="width:100%"></el-input-number></el-form-item>
|
|
<el-form-item label="竞争难度"><el-slider v-model="keywordForm.difficulty" :min="0" :max="1" :step="0.05" show-input style="width:100%"></el-slider></el-form-item>
|
|
<el-form-item label="目标排名"><el-input-number v-model="keywordForm.target_rank" :min="1" :max="100" style="width:100%"></el-input-number></el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="showKeywordDialog = false">取消</el-button>
|
|
<el-button type="primary" @click="saveKeyword">添加</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
<script src="https://unpkg.com/element-plus"></script>
|
|
<script src="icon-components.js"></script>
|
|
<script>
|
|
const { createApp, ref, computed, onMounted, nextTick } = Vue;
|
|
|
|
const app = createApp({
|
|
data() {
|
|
return {
|
|
currentUser: {},
|
|
isAdmin: false,
|
|
activeTab: 'overview',
|
|
|
|
loading: false,
|
|
pageSize: 10, productPage: 1, campaignPage: 1, rankingPage: 1,
|
|
|
|
overview: {},
|
|
products: [],
|
|
campaigns: [],
|
|
selectedCampaign: null,
|
|
campaignKeywords: [],
|
|
keywordRankings: [],
|
|
keywordRankingOverview: {},
|
|
audits: [],
|
|
optimizationTasks: [],
|
|
taskFilter: 'open',
|
|
|
|
showProductDialog: false,
|
|
editingProduct: null,
|
|
productForm: { name: '', type: 'website', url: '', domain: '', app_id: '', account_id: '', description: '' },
|
|
|
|
showCampaignDialog: false,
|
|
editingCampaign: null,
|
|
campaignForm: { name: '', product_id: null, status: 'active', target_engines: [], keywords: [], notes: '' },
|
|
|
|
showKeywordDialog: false,
|
|
keywordForm: { keyword: '', search_volume: 0, difficulty: 0.5, target_rank: 10 },
|
|
|
|
token: '',
|
|
};
|
|
},
|
|
computed: {
|
|
paginatedProducts() {
|
|
const start = (this.productPage - 1) * this.pageSize;
|
|
return this.products.slice(start, start + this.pageSize);
|
|
},
|
|
paginatedCampaigns() {
|
|
const start = (this.campaignPage - 1) * this.pageSize;
|
|
return this.campaigns.slice(start, start + this.pageSize);
|
|
},
|
|
paginatedRankings() {
|
|
const start = (this.rankingPage - 1) * this.pageSize;
|
|
return this.keywordRankings.slice(start, start + this.pageSize);
|
|
},
|
|
},
|
|
methods: {
|
|
typeLabel(t) {
|
|
return { website: '网站', article: '文章', wechat_account: '公众号', miniprogram: '小程序' }[t] || t;
|
|
},
|
|
engineLabel(e) {
|
|
return { baidu: '百度', bing: 'Bing', sogou: '搜狗', '360': '360', wechat: '微信搜索' }[e] || e;
|
|
},
|
|
api(path) { return '/api/external' + path; },
|
|
headers() { return { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }; },
|
|
async fetchJson(url, opts) {
|
|
const r = await fetch(url, opts || { headers: this.headers() });
|
|
const d = await r.json();
|
|
if (!d.ok) throw new Error(d.detail || '请求失败');
|
|
return d.data;
|
|
},
|
|
|
|
switchTab(tab) {
|
|
this.activeTab = tab;
|
|
this.selectedCampaign = null;
|
|
this.loadTabData();
|
|
},
|
|
async loadTabData() {
|
|
this.loading = true;
|
|
try {
|
|
if (this.activeTab === 'overview') {
|
|
this.overview = await this.fetchJson(this.api('/overview'));
|
|
} else if (this.activeTab === 'products') {
|
|
this.products = await this.fetchJson(this.api('/products'));
|
|
} else if (this.activeTab === 'campaigns') {
|
|
this.campaigns = await this.fetchJson(this.api('/campaigns'));
|
|
} else if (this.activeTab === 'rankings') {
|
|
await this.loadRankings();
|
|
} else if (this.activeTab === 'audits') {
|
|
this.audits = await this.fetchJson(this.api('/audits'));
|
|
} else if (this.activeTab === 'tasks') {
|
|
await this.loadTasks();
|
|
}
|
|
} catch (e) {
|
|
ElementPlus.ElMessage.error(e.message);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
async loadRankings() {
|
|
this.keywordRankings = await this.fetchJson(this.api('/rankings'));
|
|
try { this.keywordRankingOverview = await this.fetchJson(this.api('/rankings/overview')); } catch (e) {}
|
|
},
|
|
async loadTasks() {
|
|
let url = this.api('/optimization-tasks');
|
|
if (this.taskFilter) url += '?status=' + this.taskFilter;
|
|
this.optimizationTasks = await this.fetchJson(url);
|
|
},
|
|
|
|
// Products
|
|
editProduct(p) {
|
|
this.editingProduct = p;
|
|
this.productForm = { ...p };
|
|
this.showProductDialog = true;
|
|
},
|
|
async saveProduct() {
|
|
const url = this.editingProduct
|
|
? this.api('/products/' + this.editingProduct.id)
|
|
: this.api('/products');
|
|
const method = this.editingProduct ? 'PUT' : 'POST';
|
|
const r = await fetch(url, { method, headers: this.headers(), body: JSON.stringify(this.productForm) });
|
|
const d = await r.json();
|
|
if (!d.ok) throw new Error(d.detail || '操作失败');
|
|
ElementPlus.ElMessage.success(this.editingProduct ? '已更新' : '已创建');
|
|
this.showProductDialog = false;
|
|
this.editingProduct = null;
|
|
this.productForm = { name: '', type: 'website', url: '', domain: '', app_id: '', account_id: '', description: '' };
|
|
this.products = await this.fetchJson(this.api('/products'));
|
|
},
|
|
async deleteProduct(p) {
|
|
if (!confirm('确定删除产品「' + p.name + '」?')) return;
|
|
await this.fetchJson(this.api('/products/' + p.id), { method: 'DELETE', headers: this.headers() });
|
|
ElementPlus.ElMessage.success('已删除');
|
|
this.products = await this.fetchJson(this.api('/products'));
|
|
},
|
|
|
|
// Campaigns
|
|
viewCampaignKeywords(c) {
|
|
this.selectedCampaign = c;
|
|
this.loadCampaignKeywords(c.id);
|
|
},
|
|
async loadCampaignKeywords(campaignId) {
|
|
try {
|
|
this.campaignKeywords = await this.fetchJson(this.api('/keywords?campaign_id=' + campaignId));
|
|
} catch (e) {
|
|
this.campaignKeywords = [];
|
|
}
|
|
},
|
|
editCampaign(c) {
|
|
this.editingCampaign = c;
|
|
this.campaignForm = { name: c.name, product_id: c.product_id, status: c.status, target_engines: c.target_engines || [], keywords: c.keywords || [], notes: c.notes || '' };
|
|
this.showCampaignDialog = true;
|
|
},
|
|
async saveCampaign() {
|
|
const url = this.editingCampaign
|
|
? this.api('/campaigns/' + this.editingCampaign.id)
|
|
: this.api('/campaigns');
|
|
const method = this.editingCampaign ? 'PUT' : 'POST';
|
|
const r = await fetch(url, { method, headers: this.headers(), body: JSON.stringify(this.campaignForm) });
|
|
const d = await r.json();
|
|
if (!d.ok) throw new Error(d.detail || '操作失败');
|
|
ElementPlus.ElMessage.success(this.editingCampaign ? '已更新' : '已创建');
|
|
this.showCampaignDialog = false;
|
|
this.editingCampaign = null;
|
|
this.campaignForm = { name: '', product_id: null, status: 'active', target_engines: [], keywords: [], notes: '' };
|
|
this.campaigns = await this.fetchJson(this.api('/campaigns'));
|
|
},
|
|
async deleteCampaign(c) {
|
|
if (!confirm('确定删除活动「' + c.name + '」?')) return;
|
|
await this.fetchJson(this.api('/campaigns/' + c.id), { method: 'DELETE', headers: this.headers() });
|
|
ElementPlus.ElMessage.success('已删除');
|
|
this.campaigns = await this.fetchJson(this.api('/campaigns'));
|
|
},
|
|
|
|
// Keywords
|
|
async saveKeyword() {
|
|
if (!this.selectedCampaign) { ElementPlus.ElMessage.warning('请先选择活动'); return; }
|
|
this.keywordForm.campaign_id = this.selectedCampaign.id;
|
|
const r = await fetch(this.api('/keywords'), { method: 'POST', headers: this.headers(), body: JSON.stringify(this.keywordForm) });
|
|
const d = await r.json();
|
|
if (!d.ok) throw new Error(d.detail || '添加失败');
|
|
ElementPlus.ElMessage.success('已添加');
|
|
this.showKeywordDialog = false;
|
|
this.keywordForm = { keyword: '', search_volume: 0, difficulty: 0.5, target_rank: 10 };
|
|
this.loadCampaignKeywords(this.selectedCampaign.id);
|
|
},
|
|
async deleteKeyword(kw) {
|
|
if (!confirm('确定删除关键词「' + kw.keyword + '」?')) return;
|
|
await this.fetchJson(this.api('/keywords/' + kw.id), { method: 'DELETE', headers: this.headers() });
|
|
ElementPlus.ElMessage.success('已删除');
|
|
if (this.selectedCampaign) this.loadCampaignKeywords(this.selectedCampaign.id);
|
|
},
|
|
|
|
// Optimization Tasks
|
|
async updateTaskStatus(task, status) {
|
|
await this.fetchJson(this.api('/optimization-tasks/' + task.id), { method: 'PATCH', headers: this.headers(), body: JSON.stringify({ status: status }) });
|
|
ElementPlus.ElMessage.success('已更新');
|
|
this.loadTasks();
|
|
},
|
|
|
|
redirectToPage(page) {
|
|
window.location.href = '/' + page;
|
|
},
|
|
logout() {
|
|
localStorage.removeItem('authToken');
|
|
window.location.href = '/login.html';
|
|
},
|
|
},
|
|
async mounted() {
|
|
this.token = localStorage.getItem('authToken') || '';
|
|
if (!this.token) { window.location.href = '/login.html'; return; }
|
|
try {
|
|
const r = await fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + this.token } });
|
|
if (!r.ok) throw new Error('unauthorized');
|
|
const d = await r.json();
|
|
this.currentUser = d.user || d;
|
|
this.isAdmin = this.currentUser.role === 'admin';
|
|
window.installUniNav(this.$parent ? null : app).component('uni-nav', window.UniNav);
|
|
} catch (e) {
|
|
window.location.href = '/login.html';
|
|
}
|
|
this.loadTabData();
|
|
},
|
|
});
|
|
|
|
// Register Element Plus icons
|
|
const { Edit, Delete, Plus, Search, Setting, Monitor, Document, DataBoard, Folder, Menu as IconMenu } = ElementPlusIconsVue || {};
|
|
if (ElementPlusIconsVue) {
|
|
for (const [key, comp] of Object.entries(ElementPlusIconsVue)) {
|
|
app.component(key, comp);
|
|
}
|
|
}
|
|
|
|
app.use(ElementPlus);
|
|
app.mount('#app');
|
|
</script>
|
|
</body>
|
|
</html>
|