v0.5.4: 全面修复 — template literal URL, Cookie验证, 用户默认is_active, 默认账号路由, 空间信息, 密钥清理, promoForm修复
修复:
- quark-share.ts/storage.ts: 9处template literal ${}缺失导致fetch URL写死
- user/routes.ts: testCloudConnectionWithCookie缺await + 按cloudType分发驱动
- credential.service.ts: INSERT缺?参数 (9values/10cols)
- user/routes.ts: 用户新增网盘默认is_active=0
- admin.routes.ts: 新增PUT /admin/cloud-configs/:id/primary路由
- database.ts: is_primary列迁移
- UserDashboard.vue: 保存时传递storage_used/storage_total
- SystemConfig.vue: promoForm const重赋值bug
- config/index.ts: 移除泄露的默认密钥token
This commit is contained in:
@@ -383,6 +383,38 @@ export async function changePassword(
|
||||
return data
|
||||
}
|
||||
|
||||
// ===== 推广平台管理 =====
|
||||
export interface PromotionPlatform {
|
||||
id: number
|
||||
name: string
|
||||
join_url: string
|
||||
sort_order: number
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
export async function getPromotionPlatforms(): Promise<PromotionPlatform[]> {
|
||||
const { data } = await axios.get('/api/promotion-platforms')
|
||||
return data
|
||||
}
|
||||
|
||||
export async function getAdminPromotionPlatforms(): Promise<PromotionPlatform[]> {
|
||||
const { data } = await api.get('/admin/promotion-platforms')
|
||||
return data
|
||||
}
|
||||
|
||||
export async function createPromotionPlatform(params: { name: string; join_url: string; sort_order?: number }): Promise<PromotionPlatform> {
|
||||
const { data } = await api.post('/admin/promotion-platforms', params)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function updatePromotionPlatform(id: number, params: { name: string; join_url: string; sort_order?: number }): Promise<void> {
|
||||
await api.put(`/admin/promotion-platforms/${id}`, params)
|
||||
}
|
||||
|
||||
export async function deletePromotionPlatform(id: number): Promise<void> {
|
||||
await api.delete(`/admin/promotion-platforms/${id}`)
|
||||
}
|
||||
|
||||
export default api
|
||||
export { query as searchQuery }
|
||||
|
||||
|
||||
@@ -307,7 +307,6 @@ async function switchTrendDays(d: number) {
|
||||
}
|
||||
|
||||
// Menu selection
|
||||
const activeMenu = ref('dashboard')
|
||||
const activeSystemSection = ref('')
|
||||
|
||||
// ── Recent saves ──
|
||||
@@ -381,6 +380,7 @@ const sysSectionTitles: Record<string, string> = {
|
||||
'sys-validation': '链接验证配置',
|
||||
'sys-filter': '搜索标题过滤规则',
|
||||
'sys-password': '修改管理员密码',
|
||||
'sys-platforms': '推广平台管理',
|
||||
}
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
<el-menu-item index="sys-password">🔑 修改密码</el-menu-item>
|
||||
<el-menu-item index="sys-notify">📬 消息推送</el-menu-item>
|
||||
<el-menu-item index="sys-daily-report">📊 每日汇报</el-menu-item>
|
||||
<el-menu-item index="sys-platforms">👥 推广平台管理</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item index="save-records">
|
||||
@@ -102,6 +103,7 @@ const pageTitles: Record<string, string> = {
|
||||
'sys-password': '修改管理员密码',
|
||||
'sys-notify': '消息推送',
|
||||
'sys-daily-report': '每日汇报',
|
||||
'sys-platforms': '推广平台管理',
|
||||
'save-records': '转存日志',
|
||||
}
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="白名单目录">
|
||||
<div style="width: 100%;">
|
||||
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px;" v-if="whitelistDirs.length">
|
||||
<el-tag v-for="(dir, i) in whitelistDirs" :key="i" closable size="small" @close="removeWhitelistDir(i)">{{ dir }}</el-tag>
|
||||
</div>
|
||||
<div style="display: flex; gap: 6px;">
|
||||
<el-input v-model="newWhitelistDir" placeholder="输入目录名" size="small" style="width: 160px" @keyup.enter="addWhitelistDir" />
|
||||
<el-button type="primary" size="small" @click="addWhitelistDir">添加</el-button>
|
||||
</div>
|
||||
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px;" v-if="whitelistDirs.length">
|
||||
<el-tag v-for="(dir, i) in whitelistDirs" :key="i" closable size="small" @close="removeWhitelistDir(i)">{{ dir }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
@@ -754,6 +754,57 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
|
||||
<!-- 👥 推广平台管理 -->
|
||||
<el-card id="section-sys-platforms" v-show="!activeSection || activeSection === 'sys-platforms'">
|
||||
<template #header>
|
||||
<span>👥 推广平台管理</span>
|
||||
</template>
|
||||
<div class="form-tip" style="margin-bottom: 12px;">配置注册页面可选的推广平台,每个平台需提供邀请链接(将自动生成二维码供用户扫码加入)</div>
|
||||
<div style="margin-bottom: 12px;">
|
||||
<label style="font-size: 13px; color: #606266; display: block; margin-bottom: 4px;">二维码标题</label>
|
||||
<el-input v-model="configs.promotion_qr_title" placeholder="扫码加入推广团队" style="max-width: 360px" />
|
||||
<div class="form-tip">注册页面二维码区域的标题文字,保存后生效</div>
|
||||
</div>
|
||||
<el-table :data="promoPlatforms" stripe size="small" empty-text="暂无推广平台">
|
||||
<el-table-column prop="name" label="平台名称" width="160" />
|
||||
<el-table-column prop="join_url" label="邀请链接" min-width="300" show-overflow-tooltip />
|
||||
<el-table-column prop="sort_order" label="排序" width="80" align="center" />
|
||||
<el-table-column label="操作" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" text type="primary" @click="editPromoPlatform(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除该平台?" @confirm="deletePromoPlatform(row.id)">
|
||||
<template #reference>
|
||||
<el-button size="small" text type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="margin-top: 12px;">
|
||||
<el-button size="small" type="primary" @click="showPromoDialog = true; editingPromoId = null; promoForm.name = ''; promoForm.join_url = ''; promoForm.sort_order = 0">新增平台</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 推广平台弹窗 -->
|
||||
<el-dialog v-model="showPromoDialog" :title="editingPromoId ? '编辑推广平台' : '新增推广平台'" width="480px">
|
||||
<el-form :model="promoForm" label-width="100px">
|
||||
<el-form-item label="平台名称">
|
||||
<el-input v-model="promoForm.name" placeholder="如:蜂小推" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邀请链接">
|
||||
<el-input v-model="promoForm.join_url" placeholder="https://..." />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="promoForm.sort_order" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showPromoDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="promoSaving" @click="savePromoPlatform">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="save-bar">
|
||||
<el-button type="primary" size="large" :loading="saving" @click="handleSave">
|
||||
@@ -768,7 +819,7 @@ import { ref, reactive, onMounted, computed, watch } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { ElForm } from 'element-plus'
|
||||
import { getSystemConfigs, updateSystemConfigs, changePassword as changePasswordApi, uploadFallbackImage, uploadLogo, updateSetting, getDbStatus, testRedisConnection, testExternalService, testNotifyChannel, getAllNotifierProviders, getCloudConfigs } from "../../api"
|
||||
import { getSystemConfigs, updateSystemConfigs, changePassword as changePasswordApi, uploadFallbackImage, uploadLogo, updateSetting, getDbStatus, testRedisConnection, testExternalService, testNotifyChannel, getAllNotifierProviders, getCloudConfigs, getAdminPromotionPlatforms, createPromotionPlatform, updatePromotionPlatform, deletePromotionPlatform } from "../../api"
|
||||
import { Upload, Loading } from "@element-plus/icons-vue"
|
||||
|
||||
|
||||
@@ -814,6 +865,12 @@ const ipGeoTesting = ref(false)
|
||||
const pansouInfo = ref<any>(null)
|
||||
const pansouInfoLoading = ref(true)
|
||||
const pansouUpdating = ref(false)
|
||||
|
||||
const promoPlatforms = ref<any[]>([])
|
||||
const showPromoDialog = ref(false)
|
||||
const editingPromoId = ref<number | null>(null)
|
||||
const promoForm = reactive({ name: '', join_url: '', sort_order: 0 })
|
||||
const promoSaving = ref(false)
|
||||
const proxyEnabled = computed({
|
||||
get: () => String(configs.search_proxy_enabled) === 'true',
|
||||
set: (val: boolean) => { configs.search_proxy_enabled = val ? 'true' : 'false' },
|
||||
@@ -1263,6 +1320,7 @@ const passwordRules = {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadPromoPlatforms();
|
||||
try {
|
||||
rawConfigs.value = await getSystemConfigs()
|
||||
for (const cfg of rawConfigs.value) {
|
||||
@@ -1618,6 +1676,11 @@ async function handleSave() {
|
||||
key: cfg.key,
|
||||
value: String(configs[cfg.key] ?? cfg.value),
|
||||
}))
|
||||
// Always save promotion QR title
|
||||
if (configs.promotion_qr_title !== undefined) {
|
||||
const hasQrTitle = entries.some((e: any) => e.key === 'promotion_qr_title')
|
||||
if (!hasQrTitle) entries.push({ key: 'promotion_qr_title', value: String(configs.promotion_qr_title || '') })
|
||||
}
|
||||
await saveDailyReportConfig()
|
||||
// Add global_notify_config as JSON entry
|
||||
entries.push({
|
||||
@@ -1632,6 +1695,27 @@ async function handleSave() {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
async function loadPromoPlatforms() {
|
||||
try { promoPlatforms.value = await getAdminPromotionPlatforms() } catch { /* */ }
|
||||
}
|
||||
function editPromoPlatform(row: any) {
|
||||
editingPromoId.value = row.id; promoForm.name = row.name; promoForm.join_url = row.join_url; promoForm.sort_order = row.sort_order; showPromoDialog.value = true;
|
||||
}
|
||||
async function savePromoPlatform() {
|
||||
if (!promoForm.name || !promoForm.join_url) { ElMessage.warning("平台名称和邀请链接不能为空"); return; }
|
||||
promoSaving.value = true;
|
||||
try {
|
||||
if (editingPromoId.value) { await updatePromotionPlatform(editingPromoId.value, { ...promoForm }); ElMessage.success("更新成功"); }
|
||||
else { await createPromotionPlatform({ ...promoForm }); ElMessage.success("添加成功"); }
|
||||
showPromoDialog.value = false; editingPromoId.value = null; loadPromoPlatforms();
|
||||
} catch (e: any) { ElMessage.error(e.response?.data?.error || "保存失败"); }
|
||||
finally { promoSaving.value = false; }
|
||||
}
|
||||
async function deletePromoPlatform(id: number) {
|
||||
try { await deletePromotionPlatform(id); ElMessage.success("已删除"); loadPromoPlatforms(); }
|
||||
catch (e: any) { ElMessage.error(e.response?.data?.error || "删除失败"); }
|
||||
}
|
||||
|
||||
|
||||
async function handleChangePassword() {
|
||||
const valid = await passwordFormRef.value?.validate().catch(() => false)
|
||||
|
||||
561
source_clean/frontend-src/src/pages/user/UserDashboard.vue
Normal file
561
source_clean/frontend-src/src/pages/user/UserDashboard.vue
Normal file
@@ -0,0 +1,561 @@
|
||||
<template>
|
||||
<div class="user-layout">
|
||||
<!-- Header -->
|
||||
<div class="user-header">
|
||||
<span class="user-title">CloudSearch 用户中心</span>
|
||||
<div class="header-right">
|
||||
<span class="user-account">{{ account }}</span>
|
||||
<el-button size="small" @click="handleLogout">退出登录</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-body">
|
||||
<!-- Sidebar -->
|
||||
<div class="user-sidebar">
|
||||
<div
|
||||
v-for="item in menuItems"
|
||||
:key="item.key"
|
||||
:class="['sidebar-item', { active: activeMenu === item.key }]"
|
||||
@click="activeMenu = item.key"
|
||||
>
|
||||
<span>{{ item.icon }} {{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="user-content">
|
||||
<!-- 📋 转存日志 -->
|
||||
<div v-show="activeMenu === 'records'">
|
||||
<el-card shadow="never">
|
||||
<template #header><span>📋 转存日志</span></template>
|
||||
<el-table :data="records" stripe empty-text="暂无转存记录" size="small">
|
||||
<el-table-column prop="source_title" label="资源名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="source_type" label="来源" width="80" />
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'success' ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 'success' ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="file_size" label="大小" width="90" />
|
||||
<el-table-column label="耗时" width="90">
|
||||
<template #default="{ row }">
|
||||
{{ row.duration_ms ? (row.duration_ms / 1000).toFixed(1) + 's' : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时间" width="170">
|
||||
<template #default="{ row }">{{ row.created_at }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-if="recordsTotal > 0"
|
||||
v-model:current-page="recordsPage"
|
||||
:page-size="recordsPageSize"
|
||||
:total="recordsTotal"
|
||||
layout="prev, pager, next"
|
||||
small
|
||||
style="margin-top: 16px; justify-content: center;"
|
||||
@current-change="loadRecords"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 💾 网盘管理 — 复刻 admin CloudConfig 布局 -->
|
||||
<div v-show="activeMenu === 'drives'">
|
||||
<el-card shadow="never">
|
||||
<template #header><span>💾 我的网盘</span></template>
|
||||
<div style="margin-bottom: 12px;">
|
||||
<el-button type="primary" size="small" @click="openAddDrive">新增配置</el-button>
|
||||
</div>
|
||||
<el-table :data="drives" stripe empty-text="暂无网盘配置" size="small">
|
||||
<el-table-column label="网盘类型" width="110">
|
||||
<template #default="{ row }">
|
||||
<CloudBadge :cloud_type="row.cloud_type" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="nickname" label="昵称" width="140">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.nickname">{{ row.nickname }}</span>
|
||||
<span v-else style="color:#909399;font-size:12px;">未设置</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="promotion_account" label="推广账号" width="160">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.promotion_account">{{ row.promotion_account }}</span>
|
||||
<span v-else style="color:#909399;font-size:12px;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="验证" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.verification_status === 'valid'" type="success" size="small">有效</el-tag>
|
||||
<el-tag v-else-if="row.verification_status === 'invalid'" type="danger" size="small">无效</el-tag>
|
||||
<el-tag v-else type="info" size="small">未验证</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="空间" width="200">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.storage_total && row.storage_total !== '-'">
|
||||
<div>{{ row.storage_used || '计算中...' }} / {{ row.storage_total }}</div>
|
||||
</template>
|
||||
<span v-else style="color:#909399">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="转存数" width="80" align="center">
|
||||
<template #default="{ row }">{{ row.total_saves > 0 ? row.total_saves + '次' : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" text type="primary" @click="openEditDrive(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除该配置?" @confirm="handleDeleteDrive(row.id)">
|
||||
<template #reference>
|
||||
<el-button size="small" text type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 🔔 消息推送 — 复刻 admin SystemConfig notify 布局 -->
|
||||
<div v-show="activeMenu === 'notify'">
|
||||
<el-card shadow="never">
|
||||
<template #header><span>🔔 消息推送配置</span></template>
|
||||
<el-form label-width="120px" size="small">
|
||||
<el-form-item label="推送通道">
|
||||
<el-select v-model="notifyChannel" style="width: 200px" placeholder="选择推送通道">
|
||||
<el-option label="Feishu Bot" value="feishu" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<template v-if="notifyChannel === 'feishu'">
|
||||
<el-form-item label="Webhook 地址">
|
||||
<el-input v-model="notifyWebhook" placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/xxx" style="width: 420px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="推送事件">
|
||||
<el-checkbox-group v-model="notifyEvents">
|
||||
<el-checkbox label="on_save_success">转存成功</el-checkbox>
|
||||
<el-checkbox label="on_save_fail">转存失败</el-checkbox>
|
||||
<el-checkbox label="on_cookie_expire">Cookie过期</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="savingNotify" @click="saveNotify">保存推送配置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑网盘弹窗 — 复刻 admin CloudConfig dialog -->
|
||||
<el-dialog v-model="showAddDrive" :title="editingDrive ? '编辑网盘配置' : '新增网盘配置'" width="560px">
|
||||
<el-form ref="driveFormRef" :model="driveForm" :rules="driveRules" label-width="120px">
|
||||
<el-form-item label="网盘类型" prop="cloud_type">
|
||||
<el-select v-model="driveForm.cloud_type" style="width: 100%" :disabled="!!editingDrive" @change="onDriveTypeChange">
|
||||
<el-option
|
||||
v-for="ct in enabledCloudTypes"
|
||||
:key="ct.type"
|
||||
:label="ct.label"
|
||||
:value="ct.type"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Cookie" prop="cookie">
|
||||
<el-input
|
||||
v-model="driveForm.cookie"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||
:placeholder="driveCookiePlaceholder"
|
||||
input-style="font-family: monospace; font-size: 12px;"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- Cookie 获取教程(根据网盘类型切换) -->
|
||||
<el-form-item label=" " v-if="driveForm.cloud_type && driveForm.cloud_type !== ''" class="cookie-tips-item">
|
||||
<div class="cookie-tips" :class="`cookie-tips-${driveForm.cloud_type}`">
|
||||
<div class="cookie-tips-header">
|
||||
<span class="cookie-tips-title">📖 {{ driveCloudTypeLabel }} Cookie 获取教程</span>
|
||||
</div>
|
||||
<ol class="cookie-tips-steps" v-html="driveCookieTutorialHtml"></ol>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showAddDrive = false; editingDrive = null">取消</el-button>
|
||||
<el-button type="primary" :loading="adding" @click="handleSaveDrive">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
import { CLOUD_LABELS } from '../../types'
|
||||
import type { CloudType } from '../../types'
|
||||
import CloudBadge from '../../components/CloudBadge.vue'
|
||||
import type { ElForm } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const account = ref('')
|
||||
|
||||
function userApi() {
|
||||
const token = localStorage.getItem('user_token')
|
||||
return axios.create({
|
||||
baseURL: '/api/user',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const token = localStorage.getItem('user_token')
|
||||
if (!token) { router.push('/user/login'); return }
|
||||
try {
|
||||
const { data } = await userApi().get('/profile')
|
||||
account.value = data.account
|
||||
} catch {
|
||||
localStorage.removeItem('user_token')
|
||||
localStorage.removeItem('user_account')
|
||||
router.push('/user/login')
|
||||
}
|
||||
})
|
||||
|
||||
function handleLogout() {
|
||||
localStorage.removeItem('user_token')
|
||||
localStorage.removeItem('user_account')
|
||||
router.push('/user/login')
|
||||
}
|
||||
|
||||
// ── Menu ──
|
||||
const activeMenu = ref('records')
|
||||
const menuItems = [
|
||||
{ key: 'records', icon: '📋', label: '转存日志' },
|
||||
{ key: 'drives', icon: '💾', label: '网盘管理' },
|
||||
{ key: 'notify', icon: '🔔', label: '消息推送' },
|
||||
]
|
||||
|
||||
// ── 转存日志 ──
|
||||
const records = ref<any[]>([])
|
||||
const recordsTotal = ref(0)
|
||||
const recordsPage = ref(1)
|
||||
const recordsPageSize = ref(20)
|
||||
|
||||
async function loadRecords() {
|
||||
try {
|
||||
const { data } = await userApi().get('/save-records', { params: { page: recordsPage.value, pageSize: recordsPageSize.value } })
|
||||
records.value = data.records
|
||||
recordsTotal.value = data.total
|
||||
} catch { /* */ }
|
||||
}
|
||||
|
||||
// ── 网盘管理 ──
|
||||
const drives = ref<any[]>([])
|
||||
const showAddDrive = ref(false)
|
||||
const editingDrive = ref<any>(null)
|
||||
const adding = ref(false)
|
||||
const driveFormRef = ref<InstanceType<typeof ElForm>>()
|
||||
|
||||
interface EnabledCloudType { type: string; label: string; icon: string; enabled: boolean }
|
||||
const enabledCloudTypes = ref<EnabledCloudType[]>([])
|
||||
|
||||
const defaultDriveForm = () => ({
|
||||
cloud_type: '' as CloudType | '',
|
||||
cookie: '',
|
||||
})
|
||||
|
||||
const driveForm = reactive(defaultDriveForm())
|
||||
|
||||
const driveRules = computed(() => ({
|
||||
cloud_type: [{ required: true, message: '请选择网盘类型', trigger: 'change' }],
|
||||
}))
|
||||
|
||||
const driveCookiePlaceholder = computed(() => {
|
||||
if (!driveForm.cloud_type) return '请先选择网盘类型'
|
||||
const t = driveForm.cloud_type
|
||||
if (t === 'quark' || t === 'baidu') return `请输入 ${CLOUD_LABELS[t] || t} 的完整 Cookie`
|
||||
return editingDrive.value ? '留空则保持原有' : '输入完整 Cookie'
|
||||
})
|
||||
|
||||
const driveCloudTypeLabel = computed(() => {
|
||||
return CLOUD_LABELS[driveForm.cloud_type as CloudType] || driveForm.cloud_type || ''
|
||||
})
|
||||
|
||||
/** Cookie 获取教程 HTML(根据不同网盘类型) */
|
||||
const driveCookieTutorialHtml = computed(() => {
|
||||
const t = driveForm.cloud_type
|
||||
if (!t) return ''
|
||||
const tutorials: Record<string, string> = {
|
||||
quark: `<li>在电脑上打开 <a href="https://pan.quark.cn" target="_blank">pan.quark.cn</a> 并登录你的夸克账号</li>
|
||||
<li>按 <code>F12</code> 打开开发者工具 → 切换到 <strong>网络 (Network)</strong> 选项卡</li>
|
||||
<li>刷新页面,在请求列表中点击任意一个请求(如 <code>account/info</code>)</li>
|
||||
<li>在右侧 <strong>请求头 (Request Headers)</strong> 中找到 <code>Cookie</code> 字段</li>
|
||||
<li>复制整个 Cookie 值(<b>从开头到结束的完整内容</b>),粘贴到上方输入框</li>
|
||||
<li>点击「<b>保存</b>」按钮提交配置</li>
|
||||
<div class="cookie-tips-note">⚠️ 必须包含 <code>__st=s%...</code> 字段!请复制浏览器请求头的 <b>整个 Cookie</b>(F12 → Network → 请求头 → Cookie 项),不要只复制部分。</div>`,
|
||||
|
||||
baidu: `<li>在电脑上打开 <a href="https://pan.baidu.com" target="_blank">pan.baidu.com</a> 并登录你的百度账号</li>
|
||||
<li>按 <code>F12</code> 打开开发者工具 → 切换到 <strong>网络 (Network)</strong> 选项卡</li>
|
||||
<li>刷新页面,在请求列表中点击任意一个请求</li>
|
||||
<li>在右侧 <strong>请求头 (Request Headers)</strong> 中找到 <code>Cookie</code> 字段</li>
|
||||
<li>复制整个 Cookie 值,粘贴到上方输入框</li>
|
||||
<li>点击「<b>保存</b>」按钮提交配置</li>
|
||||
<div class="cookie-tips-note">💡 需要包含 <code>BDUSS</code> 和 <code>STOKEN</code></div>`,
|
||||
|
||||
aliyun: `<li>在电脑上打开 <a href="https://www.aliyundrive.com" target="_blank">aliyundrive.com</a> 并登录</li>
|
||||
<li>按 <code>F12</code> 打开开发者工具 → <strong>网络 (Network)</strong></li>
|
||||
<li>刷新页面,找到任意请求 → 复制 <code>Cookie</code></li>
|
||||
<li>粘贴到上方输入框,点击「保存」提交</li>
|
||||
<div class="cookie-tips-note">💡 需包含 <code>token</code> 等有效字段</div>`,
|
||||
|
||||
'115': `<li>在电脑上打开 <a href="https://115.com" target="_blank">115.com</a> 并登录</li>
|
||||
<li>按 <code>F12</code> 打开开发者工具 → <strong>网络 (Network)</strong></li>
|
||||
<li>刷新页面,找到任意请求 → 复制 <code>Cookie</code></li>
|
||||
<li>粘贴到上方输入框,点击「保存」提交</li>
|
||||
<div class="cookie-tips-note">💡 需包含 <code>UID</code>、<code>CID</code>、<code>SEID</code> 等字段</div>`,
|
||||
|
||||
tianyi: `<li>在电脑上打开 <a href="https://cloud.189.cn" target="_blank">cloud.189.cn</a> 并登录</li>
|
||||
<li>按 <code>F12</code> 打开开发者工具 → <strong>网络 (Network)</strong></li>
|
||||
<li>刷新页面,找到任意请求 → 复制 <code>Cookie</code></li>
|
||||
<li>粘贴到上方输入框,点击「保存」提交</li>
|
||||
<div class="cookie-tips-note">💡 需包含 <code>COOKIE_LOGIN_USER</code>、<code>SESSION</code> 等字段</div>`,
|
||||
|
||||
'123pan': `<li>在电脑上打开 <a href="https://www.123pan.com" target="_blank">123pan.com</a> 并登录</li>
|
||||
<li>按 <code>F12</code> 打开开发者工具 → <strong>网络 (Network)</strong></li>
|
||||
<li>刷新页面,找到任意请求 → 复制 <code>Cookie</code></li>
|
||||
<li>粘贴到上方输入框,点击「保存」提交</li>`,
|
||||
|
||||
uc: `<li>在电脑上打开 <a href="https://drive.uc.cn" target="_blank">drive.uc.cn</a> 并登录</li>
|
||||
<li>按 <code>F12</code> 打开开发者工具 → <strong>网络 (Network)</strong></li>
|
||||
<li>刷新页面,找到任意请求 → 复制 <code>Cookie</code></li>
|
||||
<li>粘贴到上方输入框,点击「保存」提交</li>`,
|
||||
|
||||
xunlei: `<li>在电脑上打开 <a href="https://pan.xunlei.com" target="_blank">pan.xunlei.com</a> 并登录</li>
|
||||
<li>按 <code>F12</code> 打开开发者工具 → <strong>网络 (Network)</strong></li>
|
||||
<li>刷新页面,找到任意请求 → 复制 <code>Cookie</code></li>
|
||||
<li>粘贴到上方输入框,点击「保存」提交</li>`,
|
||||
|
||||
pikpak: `<li>在电脑上打开 <a href="https://www.mypikpak.com" target="_blank">mypikpak.com</a> 并登录</li>
|
||||
<li>按 <code>F12</code> 打开开发者工具 → <strong>网络 (Network)</strong></li>
|
||||
<li>刷新页面,找到任意请求 → 复制 <code>Cookie</code></li>
|
||||
<li>粘贴到上方输入框,点击「保存」提交</li>`,
|
||||
}
|
||||
return tutorials[t] || `<li>在电脑上打开该网盘网站并登录</li>
|
||||
<li>按 <code>F12</code> 打开开发者工具 → <strong>网络 (Network)</strong></li>
|
||||
<li>刷新页面,复制任意请求的 <code>Cookie</code></li>
|
||||
<li>粘贴到上方输入框,点击「保存」提交</li>`
|
||||
})
|
||||
|
||||
async function loadEnabledCloudTypes() {
|
||||
try {
|
||||
const { data } = await userApi().get('/enabled-cloud-types')
|
||||
enabledCloudTypes.value = data
|
||||
} catch { enabledCloudTypes.value = [] }
|
||||
}
|
||||
|
||||
async function loadDrives() {
|
||||
try {
|
||||
const { data } = await userApi().get('/cloud-configs')
|
||||
drives.value = data
|
||||
} catch { /* */ }
|
||||
}
|
||||
|
||||
function openAddDrive() {
|
||||
editingDrive.value = null
|
||||
Object.assign(driveForm, defaultDriveForm())
|
||||
showAddDrive.value = true
|
||||
}
|
||||
|
||||
function openEditDrive(row: any) {
|
||||
editingDrive.value = row
|
||||
driveForm.cloud_type = row.cloud_type
|
||||
driveForm.cookie = ''
|
||||
showAddDrive.value = true
|
||||
}
|
||||
|
||||
function onDriveTypeChange() {
|
||||
// placeholder auto-updates via computed
|
||||
}
|
||||
|
||||
async function handleSaveDrive() {
|
||||
const valid = await driveFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
adding.value = true
|
||||
try {
|
||||
let verifyNickname = ''
|
||||
let verifyStorageUsed = ''
|
||||
let verifyStorageTotal = ''
|
||||
// 如果有 Cookie,先验证并自动获取昵称和空间信息
|
||||
if (driveForm.cookie) {
|
||||
try {
|
||||
const api = userApi()
|
||||
const { data: verifyResult } = await api.post(`/cloud-configs/${driveForm.cloud_type}/test`, { cookie: driveForm.cookie })
|
||||
if (!verifyResult.success) {
|
||||
ElMessage.error(`Cookie验证失败:${verifyResult.message}`)
|
||||
adding.value = false
|
||||
return
|
||||
}
|
||||
// 自动获取网盘昵称和空间信息
|
||||
if (verifyResult.nickname) verifyNickname = verifyResult.nickname
|
||||
if (verifyResult.storage_used) verifyStorageUsed = verifyResult.storage_used
|
||||
if (verifyResult.storage_total) verifyStorageTotal = verifyResult.storage_total
|
||||
} catch (e: any) {
|
||||
ElMessage.error(`Cookie验证失败:${e.response?.data?.error || '网络错误'}`)
|
||||
adding.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const promotionAccount = account.value // 自动使用当前用户的「推广平台-手机号」
|
||||
if (editingDrive.value) {
|
||||
await userApi().put(`/cloud-configs/${editingDrive.value.id}`, {
|
||||
cloud_type: driveForm.cloud_type,
|
||||
nickname: verifyNickname || undefined,
|
||||
promotion_account: promotionAccount,
|
||||
cookie: driveForm.cookie || undefined,
|
||||
storage_used: verifyStorageUsed || undefined,
|
||||
storage_total: verifyStorageTotal || undefined,
|
||||
})
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
if (!driveForm.cookie) { ElMessage.warning('请填写Cookie'); adding.value = false; return }
|
||||
await userApi().post('/cloud-configs', {
|
||||
cloud_type: driveForm.cloud_type,
|
||||
cookie: driveForm.cookie,
|
||||
nickname: verifyNickname || undefined,
|
||||
promotion_account: promotionAccount,
|
||||
storage_used: verifyStorageUsed || undefined,
|
||||
storage_total: verifyStorageTotal || undefined,
|
||||
})
|
||||
ElMessage.success('添加成功')
|
||||
}
|
||||
showAddDrive.value = false
|
||||
editingDrive.value = null
|
||||
loadDrives()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.error || '保存失败')
|
||||
} finally { adding.value = false }
|
||||
}
|
||||
|
||||
async function handleDeleteDrive(id: number) {
|
||||
try {
|
||||
await userApi().delete(`/cloud-configs/${id}`)
|
||||
ElMessage.success('已删除')
|
||||
loadDrives()
|
||||
} catch (e: any) { ElMessage.error(e.response?.data?.error || '删除失败') }
|
||||
}
|
||||
|
||||
// ── 推送配置 ──
|
||||
const notifyChannel = ref('')
|
||||
const notifyWebhook = ref('')
|
||||
const notifyEvents = ref<string[]>([])
|
||||
const savingNotify = ref(false)
|
||||
|
||||
async function loadNotify() {
|
||||
try {
|
||||
const { data } = await userApi().get('/notify-config')
|
||||
const cfg = JSON.parse(data.notifyConfig || '{}')
|
||||
if (cfg.webhook) {
|
||||
notifyChannel.value = 'feishu'
|
||||
notifyWebhook.value = cfg.webhook
|
||||
notifyEvents.value = cfg.events || []
|
||||
}
|
||||
} catch { /* */ }
|
||||
}
|
||||
|
||||
async function saveNotify() {
|
||||
savingNotify.value = true
|
||||
try {
|
||||
const config = notifyChannel.value === 'feishu'
|
||||
? JSON.stringify({ webhook: notifyWebhook.value, events: notifyEvents.value })
|
||||
: '{}'
|
||||
await userApi().put('/notify-config', { notifyConfig: config })
|
||||
ElMessage.success('推送配置已保存')
|
||||
} catch (e: any) { ElMessage.error(e.response?.data?.error || '保存失败') }
|
||||
finally { savingNotify.value = false }
|
||||
}
|
||||
|
||||
watch(activeMenu, (tab) => {
|
||||
if (tab === 'records') loadRecords()
|
||||
else if (tab === 'drives') { loadEnabledCloudTypes(); loadDrives() }
|
||||
else if (tab === 'notify') loadNotify()
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-layout { min-height: 100vh; background: #f0f2f5; }
|
||||
.user-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 0 24px; height: 56px; background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
}
|
||||
.user-title { font-size: 16px; font-weight: 600; color: #303133; }
|
||||
.header-right { display: flex; align-items: center; gap: 12px; }
|
||||
.user-account { font-size: 13px; color: #606266; }
|
||||
.user-body { display: flex; min-height: calc(100vh - 56px); }
|
||||
.user-sidebar {
|
||||
width: 180px; background: #fff; border-right: 1px solid #e4e7ed;
|
||||
padding: 12px 0; flex-shrink: 0;
|
||||
}
|
||||
.sidebar-item {
|
||||
padding: 10px 20px; cursor: pointer; font-size: 14px; color: #606266;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.sidebar-item:hover { background: #f5f7fa; color: #303133; }
|
||||
.sidebar-item.active { background: #ecf5ff; color: #409eff; font-weight: 500; border-right: 2px solid #409eff; }
|
||||
.user-content { flex: 1; padding: 20px; overflow: auto; }
|
||||
|
||||
/* ── Cookie tutorial card (same as admin CloudConfig) ── */
|
||||
.cookie-tips-item :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
.cookie-tips {
|
||||
background: #f8faff;
|
||||
border: 1px solid #e8f0fe;
|
||||
border-radius: 6px;
|
||||
padding: 14px 16px;
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
color: #606266;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.cookie-tips-header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.cookie-tips-title {
|
||||
font-weight: 700;
|
||||
color: #409eff;
|
||||
font-size: 13px;
|
||||
}
|
||||
.cookie-tips-steps {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.cookie-tips-steps li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.cookie-tips-steps code {
|
||||
background: #ecf5ff;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||
}
|
||||
.cookie-tips-note {
|
||||
margin-top: 8px;
|
||||
padding: 6px 10px;
|
||||
background: #fffbe6;
|
||||
border: 1px solid #fff3c4;
|
||||
border-radius: 4px;
|
||||
color: #8a6d3b;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.cookie-tips-note code {
|
||||
background: #f5f0e0;
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
215
source_clean/frontend-src/src/pages/user/UserLogin.vue
Normal file
215
source_clean/frontend-src/src/pages/user/UserLogin.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div class="user-login-page">
|
||||
<div class="login-card">
|
||||
<h1 class="login-title">CloudSearch 用户中心</h1>
|
||||
<p class="login-subtitle">{{ isLogin ? '登录您的账号' : '注册新账号' }}</p>
|
||||
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="0" @keyup.enter="handleSubmit">
|
||||
<el-form-item prop="platform">
|
||||
<el-select v-model="form.platform" placeholder="选择推广平台" size="large" style="width: 100%">
|
||||
<el-option
|
||||
v-for="p in platforms"
|
||||
:key="p.name"
|
||||
:label="p.name"
|
||||
:value="p.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="手机号码" size="large" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="form.password" type="password" placeholder="密码" size="large" show-password />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="!isLogin" prop="confirmPassword">
|
||||
<el-input v-model="form.confirmPassword" type="password" placeholder="确认密码" size="large" show-password />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" size="large" :loading="loading" style="width: 100%" @click="handleSubmit">
|
||||
{{ isLogin ? '登 录' : '注 册' }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="login-switch">
|
||||
{{ isLogin ? '还没有账号?' : '已有账号?' }}
|
||||
<el-button link type="primary" @click="isLogin = !isLogin">
|
||||
{{ isLogin ? '立即注册' : '去登录' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 二维码区域 -->
|
||||
<div v-if="platforms.length > 0" class="qr-section">
|
||||
<div class="qr-title">{{ qrTitle }}</div>
|
||||
<div class="qr-grid">
|
||||
<div v-for="p in platforms" :key="p.name" class="qr-item">
|
||||
<div class="qr-label">加入【{{ p.name }}】团队</div>
|
||||
<div class="qr-img-wrap">
|
||||
<img
|
||||
:src="'https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=' + encodeURIComponent(p.join_url)"
|
||||
:alt="'加入' + p.name"
|
||||
class="qr-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
|
||||
const router = useRouter()
|
||||
const isLogin = ref(true)
|
||||
const loading = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
interface Platform {
|
||||
id: number
|
||||
name: string
|
||||
join_url: string
|
||||
sort_order: number
|
||||
}
|
||||
const platforms = ref<Platform[]>([])
|
||||
const qrTitle = ref('扫码加入推广团队')
|
||||
|
||||
const form = reactive({
|
||||
platform: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
})
|
||||
|
||||
const validatePhone = (_rule: any, value: string, callback: any) => {
|
||||
if (!value) callback(new Error('请输入手机号码'))
|
||||
else if (!/^1[3-9]\d{9}$/.test(value)) callback(new Error('请输入有效的手机号码'))
|
||||
else callback()
|
||||
}
|
||||
|
||||
const rules = {
|
||||
platform: [{ required: true, message: '请选择推广平台', trigger: 'change' }],
|
||||
phone: [{ validator: validatePhone, trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }, { min: 6, message: '密码至少6位', trigger: 'blur' }],
|
||||
confirmPassword: [{
|
||||
validator: (_rule: any, value: string, callback: any) => {
|
||||
if (!value) callback(new Error('请确认密码'))
|
||||
else if (value !== form.password) callback(new Error('两次密码不一致'))
|
||||
else callback()
|
||||
}, trigger: 'blur'
|
||||
}],
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { data } = await axios.get('/api/promotion-platforms')
|
||||
platforms.value = data.platforms
|
||||
if (data.title) qrTitle.value = data.title
|
||||
} catch { /* */ }
|
||||
})
|
||||
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
if (isLogin.value) {
|
||||
const account = form.platform + '-' + form.phone.trim()
|
||||
const { data } = await axios.post('/api/user/login', {
|
||||
account,
|
||||
password: form.password,
|
||||
})
|
||||
localStorage.setItem('user_token', data.token)
|
||||
localStorage.setItem('user_account', account)
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/user/dashboard')
|
||||
} else {
|
||||
const { data } = await axios.post('/api/user/register', {
|
||||
platform: form.platform,
|
||||
phone: form.phone.trim(),
|
||||
password: form.password,
|
||||
})
|
||||
ElMessage.success(data.message || '注册成功,请登录')
|
||||
isLogin.value = true
|
||||
form.password = ''
|
||||
form.confirmPassword = ''
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.error || '操作失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f0f2f5;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
.login-card {
|
||||
width: 420px;
|
||||
padding: 40px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
.login-title { text-align: center; font-size: 24px; margin: 0 0 8px; color: #303133; }
|
||||
.login-subtitle { text-align: center; font-size: 14px; color: #909399; margin: 0 0 32px; }
|
||||
.login-switch { text-align: center; font-size: 14px; color: #909399; margin-top: 16px; }
|
||||
|
||||
.qr-section {
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
}
|
||||
.qr-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.qr-grid {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.qr-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.qr-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
.qr-img-wrap {
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 6px rgba(0,0,0,0.08);
|
||||
}
|
||||
.qr-image {
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
</style>
|
||||
@@ -57,6 +57,20 @@ const routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/user/login',
|
||||
name: 'user-login',
|
||||
component: () => import('./pages/user/UserLogin.vue'),
|
||||
},
|
||||
{
|
||||
path: '/user/dashboard',
|
||||
name: 'user-dashboard',
|
||||
component: () => import('./pages/user/UserDashboard.vue'),
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
redirect: '/user/login',
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
Reference in New Issue
Block a user