fix(ui): responsive mobile layout with auth flow refactoring for campaigns
Adds: v-cloak fix, mobile card-list views for all tables, responsive CSS breakpoints, early auth redirect, CDN script reorder to head, ElementPlusIconsVue safety check, global ElMessage reference. Moves async mounted to .then() pattern to avoid Vue 3 SFC issues. Ultraworked with Sisyphus Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
<link rel="stylesheet" href="element-plus.css">
|
<link rel="stylesheet" href="element-plus.css">
|
||||||
<link rel="stylesheet" href="theme-modern.css">
|
<link rel="stylesheet" href="theme-modern.css">
|
||||||
<style>
|
<style>
|
||||||
|
[v-cloak] { display: none; }
|
||||||
.data-table { display: block; }
|
.data-table { display: block; }
|
||||||
@media (max-width: 768px) { .data-table { display: none; } }
|
@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-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 16px; }
|
||||||
@@ -14,8 +15,18 @@
|
|||||||
.stat-card .num { font-size: 28px; font-weight: 700; color: #2563eb; }
|
.stat-card .num { font-size: 28px; font-weight: 700; color: #2563eb; }
|
||||||
.stat-card .label { font-size: 12px; color: #909399; margin-top: 4px; }
|
.stat-card .label { font-size: 12px; color: #909399; margin-top: 4px; }
|
||||||
.search-engine-tag { margin: 2px; }
|
.search-engine-tag { margin: 2px; }
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page-header { flex-direction: column; align-items: flex-start !important; gap: 8px; }
|
||||||
|
.filter-bar { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||||
|
.filter-bar .el-button { margin-left: 0 !important; }
|
||||||
|
.stat-cards { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.toolbar { flex-wrap: wrap; gap: 8px; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="uni-nav.js"></script>
|
<script src="uni-nav.js"></script>
|
||||||
|
<script src="vue.global.prod.js"></script>
|
||||||
|
<script src="element-plus.full.js"></script>
|
||||||
|
<script src="icon-components.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" v-cloak>
|
<div id="app" v-cloak>
|
||||||
@@ -89,6 +100,17 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<div class="card-list-mobile">
|
||||||
|
<div v-for="p in products" :key="p.id" class="card-item">
|
||||||
|
<div class="card-row"><span class="card-label">名称</span><span class="card-value">{{ p.name }}</span></div>
|
||||||
|
<div class="card-row"><span class="card-label">类型</span><span class="card-value">{{ typeLabel(p.type) }}</span></div>
|
||||||
|
<div class="card-row"><span class="card-label">URL</span><span class="card-value"><a :href="p.url" target="_blank" style="color:#409eff;word-break:break-all;">{{ p.url || '-' }}</a></span></div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<el-button size="small" @click="editProduct(p)">编辑</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="deleteProduct(p)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="products.length > pageSize" style="display:flex;justify-content:center;margin:12px 0;">
|
<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>
|
<el-pagination background layout="prev, pager, next" :total="products.length" :page-size="pageSize" :current-page="productPage" @current-change="productPage = $event"></el-pagination>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,6 +151,19 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<div class="card-list-mobile">
|
||||||
|
<div v-for="c in campaigns" :key="c.id" class="card-item">
|
||||||
|
<div class="card-row"><span class="card-label">活动</span><span class="card-value">{{ c.name }}</span></div>
|
||||||
|
<div class="card-row"><span class="card-label">产品</span><span class="card-value">{{ c.product_name || '-' }}</span></div>
|
||||||
|
<div class="card-row"><span class="card-label">状态</span><span class="card-value"><el-tag :type="c.status === 'active' ? 'success' : c.status === 'paused' ? 'warning' : 'info'" size="small">{{ { active: '进行中', paused: '已暂停', completed: '已完成' }[c.status] || c.status }}</el-tag></span></div>
|
||||||
|
<div class="card-row"><span class="card-label">引擎</span><span class="card-value"><el-tag v-for="e in (c.target_engines || [])" :key="e" size="small" class="search-engine-tag">{{ engineLabel(e) }}</el-tag></span></div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<el-button size="small" @click="viewCampaignKeywords(c)">关键词</el-button>
|
||||||
|
<el-button size="small" @click="editCampaign(c)">编辑</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="deleteCampaign(c)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="campaigns.length > pageSize" style="display:flex;justify-content:center;margin:12px 0;">
|
<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>
|
<el-pagination background layout="prev, pager, next" :total="campaigns.length" :page-size="pageSize" :current-page="campaignPage" @current-change="campaignPage = $event"></el-pagination>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,7 +174,8 @@
|
|||||||
关键词详情:{{ selectedCampaign.name }}
|
关键词详情:{{ selectedCampaign.name }}
|
||||||
<el-button size="small" type="primary" style="margin-left:8px;" @click="showKeywordDialog = true">添加关键词</el-button>
|
<el-button size="small" type="primary" style="margin-left:8px;" @click="showKeywordDialog = true">添加关键词</el-button>
|
||||||
</h4>
|
</h4>
|
||||||
<el-table v-if="campaignKeywords.length" :data="campaignKeywords" border stripe class="data-table" style="width:100%">
|
<template v-if="campaignKeywords.length">
|
||||||
|
<el-table :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="keyword" label="关键词" min-width="150"></el-table-column>
|
||||||
<el-table-column prop="search_volume" label="搜索量" width="80"></el-table-column>
|
<el-table-column prop="search_volume" label="搜索量" width="80"></el-table-column>
|
||||||
<el-table-column label="难度" width="70">
|
<el-table-column label="难度" width="70">
|
||||||
@@ -154,6 +190,18 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<div class="card-list-mobile">
|
||||||
|
<div v-for="kw in campaignKeywords" :key="kw.id" class="card-item">
|
||||||
|
<div class="card-row"><span class="card-label">关键词</span><span class="card-value">{{ kw.keyword }}</span></div>
|
||||||
|
<div class="card-row"><span class="card-label">搜索量</span><span class="card-value">{{ kw.search_volume || 0 }}</span></div>
|
||||||
|
<div class="card-row"><span class="card-label">难度</span><span class="card-value"><el-tag :type="(kw.difficulty || 0) > 0.7 ? 'danger' : (kw.difficulty || 0) > 0.4 ? 'warning' : 'success'" size="small">{{ kw.difficulty != null ? (kw.difficulty * 100).toFixed(0) + '%' : '-' }}</el-tag></span></div>
|
||||||
|
<div class="card-row"><span class="card-label">排名</span><span class="card-value">当前 {{ kw.current_rank || '-' }} 目标 {{ kw.target_rank || '-' }}</span></div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<el-button size="small" type="danger" @click="deleteKeyword(kw)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div v-else style="text-align:center;padding:20px;color:#909399;font-size:13px;">暂无关键词数据</div>
|
<div v-else style="text-align:center;padding:20px;color:#909399;font-size:13px;">暂无关键词数据</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,7 +225,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="排名" width="80">
|
<el-table-column label="排名" width="80">
|
||||||
<template #default="s">
|
<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>
|
<el-tag :type="s.row.rank == null ? 'info' : s.row.rank <= 10 ? 'success' : s.row.rank <= 30 ? 'warning' : 'info'" size="small">{{ s.row.rank || '-' }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="url_found" label="排名URL" min-width="200">
|
<el-table-column prop="url_found" label="排名URL" min-width="200">
|
||||||
@@ -187,6 +235,14 @@
|
|||||||
<template #default="s">{{ s.row.checked_at ? new Date(s.row.checked_at).toLocaleString('zh-CN') : '-' }}</template>
|
<template #default="s">{{ s.row.checked_at ? new Date(s.row.checked_at).toLocaleString('zh-CN') : '-' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<div class="card-list-mobile">
|
||||||
|
<div v-for="r in keywordRankings" :key="r.id" class="card-item">
|
||||||
|
<div class="card-row"><span class="card-label">关键词</span><span class="card-value">{{ r.keyword }}</span></div>
|
||||||
|
<div class="card-row"><span class="card-label">引擎</span><span class="card-value">{{ engineLabel(r.search_engine) }}</span></div>
|
||||||
|
<div class="card-row"><span class="card-label">排名</span><span class="card-value"><el-tag :type="r.rank == null ? 'info' : r.rank <= 10 ? 'success' : r.rank <= 30 ? 'warning' : 'info'" size="small">{{ r.rank || '-' }}</el-tag></span></div>
|
||||||
|
<div class="card-row"><span class="card-label">时间</span><span class="card-value">{{ r.checked_at ? new Date(r.checked_at).toLocaleString('zh-CN') : '-' }}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="keywordRankings.length > pageSize" style="display:flex;justify-content:center;margin:12px 0;">
|
<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>
|
<el-pagination background layout="prev, pager, next" :total="keywordRankings.length" :page-size="pageSize" :current-page="rankingPage" @current-change="rankingPage = $event"></el-pagination>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,6 +279,15 @@
|
|||||||
<template #default="s">{{ s.row.checked_at ? new Date(s.row.checked_at).toLocaleString('zh-CN') : '-' }}</template>
|
<template #default="s">{{ s.row.checked_at ? new Date(s.row.checked_at).toLocaleString('zh-CN') : '-' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<div class="card-list-mobile">
|
||||||
|
<div v-for="a in audits" :key="a.id" class="card-item">
|
||||||
|
<div class="card-row"><span class="card-label">产品</span><span class="card-value">{{ a.product_name || '-' }}</span></div>
|
||||||
|
<div class="card-row"><span class="card-label">总分</span><span class="card-value"><el-tag :type="(a.overall_score || 0) >= 80 ? 'success' : (a.overall_score || 0) >= 60 ? 'warning' : 'danger'" size="small">{{ a.overall_score != null ? a.overall_score.toFixed(1) : '-' }}</el-tag></span></div>
|
||||||
|
<div class="card-row"><span class="card-label">评分</span><span class="card-value" style="font-size:12px;">M{{ a.meta_score || '-' }} H{{ a.heading_score || '-' }} C{{ a.content_score || '-' }} P{{ a.perf_score || '-' }} L{{ a.links_score || '-' }} Mob{{ a.mobile_score || '-' }}</span></div>
|
||||||
|
<div class="card-row"><span class="card-label">问题数</span><span class="card-value">{{ a.issues_found || 0 }}</span></div>
|
||||||
|
<div class="card-row"><span class="card-label">时间</span><span class="card-value">{{ a.checked_at ? new Date(a.checked_at).toLocaleString('zh-CN') : '-' }}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -260,6 +325,19 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<div class="card-list-mobile">
|
||||||
|
<div v-for="t in optimizationTasks" :key="t.id" class="card-item">
|
||||||
|
<div class="card-row"><span class="card-label">严重度</span><span class="card-value"><el-tag :type="t.severity === 'high' ? 'danger' : t.severity === 'medium' ? 'warning' : 'info'" size="small">{{ { high: '高', medium: '中', low: '低' }[t.severity] || t.severity }}</el-tag></span></div>
|
||||||
|
<div class="card-row"><span class="card-label">分类</span><span class="card-value">{{ { meta: 'Meta', heading: '标题', content: '内容', performance: '性能', links: '链接', mobile: '移动端' }[t.category] || t.category }}</span></div>
|
||||||
|
<div class="card-row"><span class="card-label">问题</span><span class="card-value" style="font-size:12px;">{{ t.issue }}</span></div>
|
||||||
|
<div class="card-row"><span class="card-label">建议</span><span class="card-value" style="font-size:12px;">{{ t.recommendation }}</span></div>
|
||||||
|
<div class="card-row"><span class="card-label">状态</span><span class="card-value"><el-tag :type="t.status === 'resolved' ? 'success' : t.status === 'ignored' ? 'info' : 'warning'" size="small">{{ { open: '待处理', resolved: '已解决', ignored: '已忽略' }[t.status] || t.status }}</el-tag></span></div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<el-button v-if="t.status === 'open'" size="small" type="success" @click="updateTaskStatus(t, 'resolved')">解决</el-button>
|
||||||
|
<el-button size="small" @click="updateTaskStatus(t, 'ignored')">忽略</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -342,11 +420,13 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</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>
|
<script>
|
||||||
const { createApp, ref, computed, onMounted, nextTick } = Vue;
|
const { createApp, ref, computed, onMounted, nextTick } = Vue;
|
||||||
|
const { ElMessage } = ElementPlus;
|
||||||
|
|
||||||
|
// Auth check — redirect early if no token
|
||||||
|
const authToken = localStorage.getItem('authToken') || '';
|
||||||
|
if (!authToken) { window.location.href = '/login.html'; }
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
data() {
|
data() {
|
||||||
@@ -380,7 +460,7 @@
|
|||||||
showKeywordDialog: false,
|
showKeywordDialog: false,
|
||||||
keywordForm: { keyword: '', search_volume: 0, difficulty: 0.5, target_rank: 10 },
|
keywordForm: { keyword: '', search_volume: 0, difficulty: 0.5, target_rank: 10 },
|
||||||
|
|
||||||
token: '',
|
token: authToken,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -435,7 +515,7 @@
|
|||||||
await this.loadTasks();
|
await this.loadTasks();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElementPlus.ElMessage.error(e.message);
|
ElMessage.error(e.message);
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
@@ -464,7 +544,7 @@
|
|||||||
const r = await fetch(url, { method, headers: this.headers(), body: JSON.stringify(this.productForm) });
|
const r = await fetch(url, { method, headers: this.headers(), body: JSON.stringify(this.productForm) });
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (!d.ok) throw new Error(d.detail || '操作失败');
|
if (!d.ok) throw new Error(d.detail || '操作失败');
|
||||||
ElementPlus.ElMessage.success(this.editingProduct ? '已更新' : '已创建');
|
ElMessage.success(this.editingProduct ? '已更新' : '已创建');
|
||||||
this.showProductDialog = false;
|
this.showProductDialog = false;
|
||||||
this.editingProduct = null;
|
this.editingProduct = null;
|
||||||
this.productForm = { name: '', type: 'website', url: '', domain: '', app_id: '', account_id: '', description: '' };
|
this.productForm = { name: '', type: 'website', url: '', domain: '', app_id: '', account_id: '', description: '' };
|
||||||
@@ -473,7 +553,7 @@
|
|||||||
async deleteProduct(p) {
|
async deleteProduct(p) {
|
||||||
if (!confirm('确定删除产品「' + p.name + '」?')) return;
|
if (!confirm('确定删除产品「' + p.name + '」?')) return;
|
||||||
await this.fetchJson(this.api('/products/' + p.id), { method: 'DELETE', headers: this.headers() });
|
await this.fetchJson(this.api('/products/' + p.id), { method: 'DELETE', headers: this.headers() });
|
||||||
ElementPlus.ElMessage.success('已删除');
|
ElMessage.success('已删除');
|
||||||
this.products = await this.fetchJson(this.api('/products'));
|
this.products = await this.fetchJson(this.api('/products'));
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -502,7 +582,7 @@
|
|||||||
const r = await fetch(url, { method, headers: this.headers(), body: JSON.stringify(this.campaignForm) });
|
const r = await fetch(url, { method, headers: this.headers(), body: JSON.stringify(this.campaignForm) });
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (!d.ok) throw new Error(d.detail || '操作失败');
|
if (!d.ok) throw new Error(d.detail || '操作失败');
|
||||||
ElementPlus.ElMessage.success(this.editingCampaign ? '已更新' : '已创建');
|
ElMessage.success(this.editingCampaign ? '已更新' : '已创建');
|
||||||
this.showCampaignDialog = false;
|
this.showCampaignDialog = false;
|
||||||
this.editingCampaign = null;
|
this.editingCampaign = null;
|
||||||
this.campaignForm = { name: '', product_id: null, status: 'active', target_engines: [], keywords: [], notes: '' };
|
this.campaignForm = { name: '', product_id: null, status: 'active', target_engines: [], keywords: [], notes: '' };
|
||||||
@@ -511,18 +591,18 @@
|
|||||||
async deleteCampaign(c) {
|
async deleteCampaign(c) {
|
||||||
if (!confirm('确定删除活动「' + c.name + '」?')) return;
|
if (!confirm('确定删除活动「' + c.name + '」?')) return;
|
||||||
await this.fetchJson(this.api('/campaigns/' + c.id), { method: 'DELETE', headers: this.headers() });
|
await this.fetchJson(this.api('/campaigns/' + c.id), { method: 'DELETE', headers: this.headers() });
|
||||||
ElementPlus.ElMessage.success('已删除');
|
ElMessage.success('已删除');
|
||||||
this.campaigns = await this.fetchJson(this.api('/campaigns'));
|
this.campaigns = await this.fetchJson(this.api('/campaigns'));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Keywords
|
// Keywords
|
||||||
async saveKeyword() {
|
async saveKeyword() {
|
||||||
if (!this.selectedCampaign) { ElementPlus.ElMessage.warning('请先选择活动'); return; }
|
if (!this.selectedCampaign) { ElMessage.warning('请先选择活动'); return; }
|
||||||
this.keywordForm.campaign_id = this.selectedCampaign.id;
|
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 r = await fetch(this.api('/keywords'), { method: 'POST', headers: this.headers(), body: JSON.stringify(this.keywordForm) });
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (!d.ok) throw new Error(d.detail || '添加失败');
|
if (!d.ok) throw new Error(d.detail || '添加失败');
|
||||||
ElementPlus.ElMessage.success('已添加');
|
ElMessage.success('已添加');
|
||||||
this.showKeywordDialog = false;
|
this.showKeywordDialog = false;
|
||||||
this.keywordForm = { keyword: '', search_volume: 0, difficulty: 0.5, target_rank: 10 };
|
this.keywordForm = { keyword: '', search_volume: 0, difficulty: 0.5, target_rank: 10 };
|
||||||
this.loadCampaignKeywords(this.selectedCampaign.id);
|
this.loadCampaignKeywords(this.selectedCampaign.id);
|
||||||
@@ -530,14 +610,14 @@
|
|||||||
async deleteKeyword(kw) {
|
async deleteKeyword(kw) {
|
||||||
if (!confirm('确定删除关键词「' + kw.keyword + '」?')) return;
|
if (!confirm('确定删除关键词「' + kw.keyword + '」?')) return;
|
||||||
await this.fetchJson(this.api('/keywords/' + kw.id), { method: 'DELETE', headers: this.headers() });
|
await this.fetchJson(this.api('/keywords/' + kw.id), { method: 'DELETE', headers: this.headers() });
|
||||||
ElementPlus.ElMessage.success('已删除');
|
ElMessage.success('已删除');
|
||||||
if (this.selectedCampaign) this.loadCampaignKeywords(this.selectedCampaign.id);
|
if (this.selectedCampaign) this.loadCampaignKeywords(this.selectedCampaign.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Optimization Tasks
|
// Optimization Tasks
|
||||||
async updateTaskStatus(task, status) {
|
async updateTaskStatus(task, status) {
|
||||||
await this.fetchJson(this.api('/optimization-tasks/' + task.id), { method: 'PATCH', headers: this.headers(), body: JSON.stringify({ status: status }) });
|
await this.fetchJson(this.api('/optimization-tasks/' + task.id), { method: 'PATCH', headers: this.headers(), body: JSON.stringify({ status: status }) });
|
||||||
ElementPlus.ElMessage.success('已更新');
|
ElMessage.success('已更新');
|
||||||
this.loadTasks();
|
this.loadTasks();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -549,32 +629,23 @@
|
|||||||
window.location.href = '/login.html';
|
window.location.href = '/login.html';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
mounted() {
|
||||||
this.token = localStorage.getItem('authToken') || '';
|
const token = this.token;
|
||||||
if (!this.token) { window.location.href = '/login.html'; return; }
|
if (!token) { window.location.href = '/login.html'; return; }
|
||||||
try {
|
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
|
||||||
const r = await fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + this.token } });
|
.then(function (r) { if (!r.ok) throw new Error('unauthorized'); return r.json(); })
|
||||||
if (!r.ok) throw new Error('unauthorized');
|
.then(function (d) {
|
||||||
const d = await r.json();
|
|
||||||
this.currentUser = d.user || d;
|
this.currentUser = d.user || d;
|
||||||
this.isAdmin = this.currentUser.role === 'admin';
|
this.isAdmin = this.currentUser.role === 'admin';
|
||||||
window.installUniNav(this.$parent ? null : app).component('uni-nav', window.UniNav);
|
}.bind(this))
|
||||||
} catch (e) {
|
.catch(function () { window.location.href = '/login.html'; });
|
||||||
window.location.href = '/login.html';
|
|
||||||
}
|
|
||||||
this.loadTabData();
|
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.use(ElementPlus);
|
||||||
|
if (window.installIcons) { window.installIcons(app); }
|
||||||
|
if (window.installUniNav) { window.installUniNav(app); }
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user