mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-16 22:51:14 +00:00
refactor(frontend): 清理废弃资源与代码,优化项目结构
1. 删除大量旧版静态图标、图片资源,替换为新的svg图标 2. 移除废弃的API模块、页面组件与配置文件 3. 新增全局消息store与通用列表分页hook 4. 优化代码逻辑:简化错误处理、调整样式类顺序、更新路由鉴权 5. 替换旧的按钮样式属性,统一代码规范
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { SwiperItem } from '@wot-ui/ui/components/wd-swiper/types'
|
||||
import type { DashboardStats } from '@/api/module_monitor/dashboard'
|
||||
import type { NoticeItem } from '@/api/module_system/notice'
|
||||
import { onPullDownRefresh, onReady, onShow } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { DashboardAPI } from '@/api/module_monitor/dashboard'
|
||||
import { NoticeAPI } from '@/api/module_system/notice'
|
||||
import { TicketAPI } from '@/api/module_system/ticket'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
|
||||
definePage({
|
||||
name: 'home',
|
||||
@@ -20,22 +22,72 @@ function getRouter() {
|
||||
_router = useRouter()
|
||||
}
|
||||
catch {}
|
||||
return _router
|
||||
}
|
||||
return _router
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const dashboardStats = ref<DashboardStats | null>(null)
|
||||
const pendingTickets = ref(0)
|
||||
const recentNotices = ref<NoticeItem[]>([])
|
||||
const userStore = useUserStore()
|
||||
|
||||
const NAV_LIST = [
|
||||
{ icon: '/static/icons/user.png', title: '用户管理', name: 'work-users' },
|
||||
{ icon: '/static/icons/role.png', title: '角色管理', name: 'work-roles' },
|
||||
{ icon: '/static/icons/notice.png', title: '通知公告', name: 'work-notices' },
|
||||
{ icon: '/static/icons/setting.png', title: '系统配置', name: 'work-params' },
|
||||
{ icon: '/static/icons/user.svg', title: '用户管理', name: 'work-users', color: '#4F8CFF', bg: 'rgba(79, 140, 255, 0.12)' },
|
||||
{ icon: '/static/icons/role.svg', title: '角色管理', name: 'work-roles', color: '#F59E0B', bg: 'rgba(245, 158, 11, 0.14)' },
|
||||
{ icon: '/static/icons/notice.svg', title: '通知公告', name: 'work-notices', color: '#10B981', bg: 'rgba(16, 185, 129, 0.12)' },
|
||||
{ icon: '/static/icons/setting.svg', title: '系统配置', name: 'work-params', color: '#8B5CF6', bg: 'rgba(139, 92, 246, 0.12)' },
|
||||
]
|
||||
|
||||
/** 轮播 Banner 条目 */
|
||||
interface BannerItem extends SwiperItem {
|
||||
key: string
|
||||
tag: string
|
||||
cls: string
|
||||
title: string
|
||||
subtitle: string
|
||||
desc: string
|
||||
cta: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 顶部轮播 Banner(轮播图风格首页核心):
|
||||
* 1. 今日问候 2. 待办工单 3. 数据概览
|
||||
*/
|
||||
const banners = computed<BannerItem[]>(() => [
|
||||
{
|
||||
key: 'greet',
|
||||
tag: '今日',
|
||||
cls: 'banner-slide--greet',
|
||||
title: `${getGreeting()},${userStore.userInfo?.name || '管理员'}`,
|
||||
subtitle: getDateString(),
|
||||
desc: '欢迎回来,开启高效的一天',
|
||||
cta: '数据概览',
|
||||
onClick: () => navigateTo('dashboard'),
|
||||
},
|
||||
{
|
||||
key: 'ticket',
|
||||
tag: '待办',
|
||||
cls: pendingTickets.value > 0 ? 'banner-slide--ticket' : 'banner-slide--ticket-empty',
|
||||
title: pendingTickets.value > 0 ? `${pendingTickets.value} 个待处理工单` : '工单已全部处理',
|
||||
subtitle: pendingTickets.value > 0 ? '点击立即处理,避免影响业务' : '处理及时,继续保持',
|
||||
desc: '点击进入工单列表',
|
||||
cta: pendingTickets.value > 0 ? '去处理' : '查看工单',
|
||||
onClick: () => navigateTo('work-tickets'),
|
||||
},
|
||||
{
|
||||
key: 'stats',
|
||||
tag: '数据',
|
||||
cls: 'banner-slide--stats',
|
||||
title: '数据概览',
|
||||
subtitle: '系统运行状态与趋势分析',
|
||||
desc: `总用户 ${dashboardStats.value?.total_users ?? '-'} · 在线 ${dashboardStats.value?.online_users ?? '-'} · 今日登录 ${dashboardStats.value?.today_login_count ?? '-'}`,
|
||||
cta: '查看详情',
|
||||
onClick: () => navigateTo('dashboard'),
|
||||
},
|
||||
])
|
||||
|
||||
function navigateTo(name: string) {
|
||||
const r = getRouter()
|
||||
if (r)
|
||||
@@ -107,54 +159,80 @@ function getGreeting() {
|
||||
|
||||
<template>
|
||||
<view class="home-page">
|
||||
<!-- Navigation bar -->
|
||||
<!-- 顶部导航 -->
|
||||
<view class="home-nav">
|
||||
<text class="home-nav__title">
|
||||
工作台概览
|
||||
</text>
|
||||
<view class="home-nav__bell">
|
||||
<text class="home-nav__bell-icon" @click="navigateTo('work-notices')">
|
||||
🔔
|
||||
</text>
|
||||
<view class="home-nav__bell" @click="navigateTo('work-notices')">
|
||||
<wd-icon name="notification" size="18px" color="var(--text-color-2)" />
|
||||
<view v-if="pendingTickets > 0" class="home-nav__badge" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Greeting card -->
|
||||
<view class="greeting-card fade-in-up">
|
||||
<text class="greeting-card__text">
|
||||
{{ getGreeting() }},管理员
|
||||
</text>
|
||||
<text class="greeting-card__date">
|
||||
{{ getDateString() }}
|
||||
</text>
|
||||
<!-- 轮播 Banner -->
|
||||
<view class="home-banner fade-in-up">
|
||||
<wd-swiper
|
||||
:list="banners"
|
||||
height="340"
|
||||
radius="28"
|
||||
:interval="4500"
|
||||
:autoplay="true"
|
||||
:loop="true"
|
||||
>
|
||||
<template #default="{ index }">
|
||||
<view class="banner-slide" :class="banners[index].cls" @click="banners[index].onClick">
|
||||
<view class="banner-slide__body">
|
||||
<text class="banner-slide__tag">
|
||||
{{ banners[index].tag }}
|
||||
</text>
|
||||
<text class="banner-slide__title">
|
||||
{{ banners[index].title }}
|
||||
</text>
|
||||
<text class="banner-slide__subtitle">
|
||||
{{ banners[index].subtitle }}
|
||||
</text>
|
||||
<text class="banner-slide__desc">
|
||||
{{ banners[index].desc }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="banner-slide__cta">
|
||||
<text class="banner-slide__cta-text">
|
||||
{{ banners[index].cta }}
|
||||
</text>
|
||||
<text class="banner-slide__cta-arrow">
|
||||
›
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template #indicator="{ current, total }">
|
||||
<view class="banner-dots">
|
||||
<view
|
||||
v-for="i in total"
|
||||
:key="i"
|
||||
class="banner-dots__dot"
|
||||
:class="{ 'is-active': i === current + 1 }"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
</wd-swiper>
|
||||
</view>
|
||||
|
||||
<!-- Dashboard entry -->
|
||||
<view class="dashboard-entry card-pressable fade-in-up-1" @click="navigateTo('dashboard')">
|
||||
<view class="dashboard-entry__icon">
|
||||
<text class="dashboard-entry__icon-text">
|
||||
📊
|
||||
</text>
|
||||
</view>
|
||||
<view class="dashboard-entry__info">
|
||||
<text class="dashboard-entry__title">
|
||||
数据概览
|
||||
</text>
|
||||
<text class="dashboard-entry__sub">
|
||||
查看系统运行状态与趋势分析
|
||||
</text>
|
||||
</view>
|
||||
<text class="dashboard-entry__arrow">
|
||||
›
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- Quick nav -->
|
||||
<view class="home-grid home-grid--nav">
|
||||
<wd-grid :column="4" clickable>
|
||||
<wd-grid-item v-for="(item, i) in NAV_LIST" :key="i" @click="navigateTo(item.name)">
|
||||
<image class="home-nav-icon" :src="item.icon" mode="aspectFit" />
|
||||
<!-- 快捷入口 -->
|
||||
<view class="home-grid fade-in-up-1">
|
||||
<wd-grid :column="4">
|
||||
<wd-grid-item v-for="item in NAV_LIST" :key="item.name" @click="navigateTo(item.name)">
|
||||
<view class="nav-item__icon" :style="{ background: item.bg }">
|
||||
<view
|
||||
class="nav-item__svg"
|
||||
:style="{
|
||||
backgroundColor: item.color,
|
||||
maskImage: `url(${item.icon})`,
|
||||
WebkitMaskImage: `url(${item.icon})`,
|
||||
}"
|
||||
/>
|
||||
</view>
|
||||
<text class="home-grid__label">
|
||||
{{ item.title }}
|
||||
</text>
|
||||
@@ -162,8 +240,8 @@ function getGreeting() {
|
||||
</wd-grid>
|
||||
</view>
|
||||
|
||||
<!-- Stats -->
|
||||
<view class="section-header">
|
||||
<!-- 运营概览 -->
|
||||
<view class="section-header fade-in-up-2">
|
||||
<text class="section-header__title">
|
||||
运营概览
|
||||
</text>
|
||||
@@ -206,16 +284,8 @@ function getGreeting() {
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Pending ticket alert -->
|
||||
<view v-if="pendingTickets > 0" class="alert-bar btn-press fade-in-up-4" @click="navigateTo('work-tickets')">
|
||||
<view class="alert-bar__dot" />
|
||||
<text class="alert-bar__text">
|
||||
您有 {{ pendingTickets }} 个待处理工单,点击查看 →
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- Recent notices -->
|
||||
<view v-if="recentNotices.length > 0" class="home-section">
|
||||
<!-- 最新公告 -->
|
||||
<view v-if="recentNotices.length > 0" class="home-section fade-in-up-3">
|
||||
<view class="section-header">
|
||||
<text class="section-header__title">
|
||||
最新公告
|
||||
@@ -237,8 +307,8 @@ function getGreeting() {
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Recent logins -->
|
||||
<view v-if="dashboardStats?.recent_logins?.length" class="home-section">
|
||||
<!-- 最近登录 -->
|
||||
<view v-if="dashboardStats?.recent_logins?.length" class="home-section fade-in-up-4">
|
||||
<view class="section-header">
|
||||
<text class="section-header__title">
|
||||
最近登录
|
||||
@@ -272,7 +342,7 @@ function getGreeting() {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ===== Navigation ===== */
|
||||
/* ===== 顶部导航 ===== */
|
||||
.home-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -295,7 +365,7 @@ function getGreeting() {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-icon { font-size: var(--font-lg, 32rpx); }
|
||||
&:active { opacity: 0.7; }
|
||||
}
|
||||
|
||||
&__badge {
|
||||
@@ -310,88 +380,152 @@ function getGreeting() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Greeting ===== */
|
||||
.greeting-card {
|
||||
background: var(--gradient-primary, linear-gradient(135deg, #4F8CFF, #2563EB));
|
||||
border-radius: 32rpx;
|
||||
padding: 40rpx 32rpx;
|
||||
/* ===== 轮播 Banner ===== */
|
||||
.home-banner {
|
||||
margin-bottom: 32rpx;
|
||||
box-shadow: var(--shadow-lg, 0 12rpx 40rpx rgba(1, 77, 178, 0.15));
|
||||
|
||||
&__text {
|
||||
display: block;
|
||||
font-size: var(--font-2xl, 40rpx);
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&__date {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: rgba(255, 255, 255, 0.80);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Dashboard entry ===== */
|
||||
.dashboard-entry {
|
||||
.banner-slide {
|
||||
position: relative;
|
||||
height: 340rpx;
|
||||
padding: 36rpx 40rpx;
|
||||
border-radius: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
background: linear-gradient(135deg, #F0F5FF, #E8F0FE);
|
||||
border: 2rpx solid rgba(1, 77, 178, 0.08);
|
||||
border-radius: 24rpx;
|
||||
padding: 28rpx;
|
||||
margin-bottom: 32rpx;
|
||||
transition: all 0.15s ease;
|
||||
justify-content: space-between;
|
||||
color: #FFFFFF;
|
||||
overflow: hidden;
|
||||
|
||||
&__icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(135deg, var(--primary-color, #4F8CFF), var(--primary-color-dark, #2970FF));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&-text { font-size: var(--font-xl, 36rpx); }
|
||||
/* 装饰圆环 */
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -80rpx;
|
||||
top: -80rpx;
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.10);
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 40rpx;
|
||||
bottom: -120rpx;
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
&--greet { background: linear-gradient(135deg, #4F8CFF, #2563EB); }
|
||||
&--ticket { background: linear-gradient(135deg, #F59E0B, #D97706); }
|
||||
&--ticket-empty { background: linear-gradient(135deg, #10B981, #059669); }
|
||||
&--stats { background: linear-gradient(135deg, #8B5CF6, #6D28D9); }
|
||||
|
||||
&__body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
align-self: flex-start;
|
||||
font-size: var(--font-xs, 20rpx);
|
||||
line-height: 1;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: var(--radius-full, 9999rpx);
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: var(--font-lg, 32rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
font-size: var(--font-2xl, 40rpx);
|
||||
font-weight: 300;
|
||||
color: var(--primary-color, #4F8CFF);
|
||||
font-family: 'Inter', -apple-system, sans-serif;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 8rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: var(--font-xs, 20rpx);
|
||||
color: rgba(255, 255, 255, 0.68);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__cta {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
margin-left: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: var(--radius-full, 9999rpx);
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.35);
|
||||
|
||||
&-text {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
font-weight: 500;
|
||||
color: #FFFFFF;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
font-size: var(--font-md, 28rpx);
|
||||
font-weight: 400;
|
||||
color: #FFFFFF;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Quick nav grid ===== */
|
||||
.home-grid {
|
||||
&--nav {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 16rpx 0;
|
||||
margin-bottom: 32rpx;
|
||||
box-shadow: var(--shadow-xs, 0 3rpx 8rpx rgba(1, 77, 178, 0.03));
|
||||
.banner-dots {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
bottom: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
|
||||
&__dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.is-active {
|
||||
width: 28rpx;
|
||||
border-radius: var(--radius-full, 9999rpx);
|
||||
background: #FFFFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 快捷入口 ===== */
|
||||
.home-grid {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 20rpx 0;
|
||||
margin-bottom: 32rpx;
|
||||
box-shadow: var(--shadow-xs, 0 3rpx 8rpx rgba(1, 77, 178, 0.03));
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
@@ -399,7 +533,27 @@ function getGreeting() {
|
||||
}
|
||||
}
|
||||
|
||||
.home-nav-icon { width: 64rpx; height: 64rpx; border-radius: 12rpx; }
|
||||
.nav-item__icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
/* SVG 图标:以 mask 蒙版取形,background-color 着色(currentColor 方案) */
|
||||
.nav-item__svg {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
}
|
||||
|
||||
/* ===== Section header ===== */
|
||||
.section-header {
|
||||
@@ -424,7 +578,7 @@ function getGreeting() {
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
@@ -462,30 +616,6 @@ function getGreeting() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Alert bar ===== */
|
||||
.alert-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
background: var(--warning-color-light, #FFF8E6);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
&__dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--warning-color, #F59E0B);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: var(--font-md, 28rpx);
|
||||
color: var(--warning-color, #F59E0B);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Home card ===== */
|
||||
.home-section {
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CaptchaInfo, LoginFormData, OAuthProvider } from '@/api/module_system/auth'
|
||||
import type { FormSchema } from '@wot-ui/ui/components/wd-form/types'
|
||||
import type { SlideVerifyInstance } from '@wot-ui/ui/components/wd-slide-verify/types'
|
||||
import type { CaptchaInfo, LoginFormData, OAuthProvider } from '@/api/module_system/auth'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
import AuthAPI from '@/api/module_system/auth'
|
||||
import { REMEMBER_ME_KEY } from '@/constants'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { logger } from '@/utils/logger'
|
||||
import { Storage } from '@/utils/storage'
|
||||
|
||||
definePage({ name: 'login', style: { navigationBarTitleText: '登录' } })
|
||||
|
||||
const loginFormRef = ref()
|
||||
const sliderCaptchaRef = ref()
|
||||
const sliderCaptchaRef = ref<SlideVerifyInstance>()
|
||||
const loading = ref(false)
|
||||
const userStore = useUserStore()
|
||||
const redirect = ref('/pages/index/index')
|
||||
@@ -46,14 +46,8 @@ const loginFormData = reactive<LoginFormData>({
|
||||
|
||||
const captchaState = reactive<CaptchaInfo>({ enable: false, key: '', img_base: '' })
|
||||
|
||||
/**
|
||||
* 滑块验证状态:
|
||||
* - idle 待验证(初始/重置后)
|
||||
* - pending 验证中(已拖动到终点,正在请求后端)
|
||||
* - success 验证成功(后端返回 verified=true)
|
||||
* - fail 验证失败(后端返回错误或网络异常)
|
||||
*/
|
||||
const sliderStatus = ref<'idle' | 'pending' | 'success' | 'fail'>('idle')
|
||||
/** 滑块验证是否已通过(后端返回 verified=true 后置位) */
|
||||
const sliderPassed = ref(false)
|
||||
|
||||
/** 从本地存储恢复记住的用户名(仅用户名,不存储密码) */
|
||||
function restoreRememberedUser() {
|
||||
@@ -74,8 +68,7 @@ async function getLoginCaptcha() {
|
||||
if (captchaState.enable) {
|
||||
loginFormData.captcha_key = captchaState.key
|
||||
// 重置滑块到初始状态
|
||||
sliderStatus.value = 'idle'
|
||||
sliderCaptchaRef.value?.reset?.()
|
||||
resetSliderCaptcha()
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -83,47 +76,48 @@ async function getLoginCaptcha() {
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
logger.error('验证码获取失败', e)
|
||||
console.error('验证码获取失败', e)
|
||||
captchaState.enable = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 滑块拖动到终点时触发 — 调用后端 slider_complete 接口标记验证完成
|
||||
* wd-slide-verify 拖动到终点触发 success — 调用后端 slider_complete 接口标记验证完成
|
||||
* 后端仅标记 captcha_key 状态为 verified,不校验 x 坐标
|
||||
*/
|
||||
async function handleSliderVerify() {
|
||||
async function handleSliderSuccess() {
|
||||
if (!captchaState.key) {
|
||||
sliderStatus.value = 'fail'
|
||||
uni.showToast({ title: '验证码已过期,请刷新', icon: 'none' })
|
||||
sliderCaptchaRef.value?.reset()
|
||||
return
|
||||
}
|
||||
|
||||
sliderStatus.value = 'pending'
|
||||
try {
|
||||
const result = await AuthAPI.completeSliderCaptcha({
|
||||
captcha_key: captchaState.key,
|
||||
x: 100, // 占位值,后端未使用
|
||||
})
|
||||
if (result?.verified) {
|
||||
sliderStatus.value = 'success'
|
||||
sliderPassed.value = true
|
||||
loginFormData.captcha = 'verified' // 占位值,后端只校验 captcha_key 状态
|
||||
}
|
||||
else {
|
||||
sliderStatus.value = 'fail'
|
||||
uni.showToast({ title: '验证失败,请重试', icon: 'none' })
|
||||
sliderCaptchaRef.value?.reset()
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
logger.error('滑块验证失败', e)
|
||||
sliderStatus.value = 'fail'
|
||||
uni.showToast({ title: getErrorMessage(e, '验证失败,请重试'), icon: 'none' })
|
||||
console.error('滑块验证失败', e)
|
||||
uni.showToast({ title: '验证失败,请重试', icon: 'none' })
|
||||
sliderCaptchaRef.value?.reset()
|
||||
}
|
||||
}
|
||||
|
||||
/** 滑块重置时清空占位验证值 */
|
||||
function handleSliderReset() {
|
||||
/** 重置滑块验证状态并清空占位验证值(验证码刷新 / 拖动未到终点触发 fail 时) */
|
||||
function resetSliderCaptcha() {
|
||||
sliderPassed.value = false
|
||||
loginFormData.captcha = ''
|
||||
sliderCaptchaRef.value?.reset()
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
@@ -142,7 +136,7 @@ async function handleSubmit() {
|
||||
return
|
||||
|
||||
// 滑块验证前置校验
|
||||
if (captchaState.enable && sliderStatus.value !== 'success') {
|
||||
if (captchaState.enable && !sliderPassed.value) {
|
||||
uni.showToast({ title: '请先完成滑块验证', icon: 'none' })
|
||||
return
|
||||
}
|
||||
@@ -164,10 +158,8 @@ async function handleSubmit() {
|
||||
}
|
||||
uni.reLaunch({ url: redirect.value })
|
||||
}
|
||||
catch (e) {
|
||||
// 全局错误提示 — 优先使用后端返回的具体错误信息(兼容 Error / 对象 data.message)
|
||||
const errMsg = getErrorMessage(e) || (e as { data?: { message?: string } })?.data?.message || '登录失败,请检查账号密码'
|
||||
uni.showToast({ title: errMsg, icon: 'none', duration: 2500 })
|
||||
catch {
|
||||
uni.showToast({ title: '登录失败,请检查账号密码', icon: 'none', duration: 2500 })
|
||||
// 登录失败后自动刷新验证码并重置滑块
|
||||
if (captchaState.enable)
|
||||
getLoginCaptcha()
|
||||
@@ -195,8 +187,8 @@ async function handleOAuth(provider: OAuthProvider) {
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
uni.showToast({ title: getErrorMessage(e, 'OAuth登录失败'), icon: 'none' })
|
||||
catch {
|
||||
uni.showToast({ title: 'OAuth登录失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -256,13 +248,14 @@ async function handleOAuth(provider: OAuthProvider) {
|
||||
</view>
|
||||
</wd-form-item>
|
||||
|
||||
<!-- Slider Captcha — 滑块拖动验证(条件显示) -->
|
||||
<!-- Slider Captcha — wd-slide-verify 滑块拖动验证(条件显示) -->
|
||||
<wd-form-item v-if="captchaState.enable">
|
||||
<SliderCaptcha
|
||||
<wd-slide-verify
|
||||
ref="sliderCaptchaRef"
|
||||
:status="sliderStatus"
|
||||
@verify="handleSliderVerify"
|
||||
@reset="handleSliderReset"
|
||||
text="向右滑动滑块验证"
|
||||
success-text="验证成功"
|
||||
@success="handleSliderSuccess"
|
||||
@fail="resetSliderCaptcha"
|
||||
/>
|
||||
</wd-form-item>
|
||||
|
||||
@@ -286,9 +279,9 @@ async function handleOAuth(provider: OAuthProvider) {
|
||||
<!-- Submit -->
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="loading"
|
||||
round
|
||||
block
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ loading ? '登录中...' : '登 录' }}
|
||||
@@ -309,7 +302,7 @@ async function handleOAuth(provider: OAuthProvider) {
|
||||
<!-- WeChat -->
|
||||
<view class="oauth-btn" @click="handleOAuth('wechat')">
|
||||
<view class="oauth-btn__icon" style="background: #07C160">
|
||||
<image class="oauth-btn__iconify" src="/static/icons/oauth/wechat.svg" />
|
||||
<image class="oauth-btn__iconify" src="/static/icons/wechat.svg" />
|
||||
</view>
|
||||
<text class="oauth-btn__label">
|
||||
微信
|
||||
@@ -319,7 +312,7 @@ async function handleOAuth(provider: OAuthProvider) {
|
||||
<!-- GitHub -->
|
||||
<view class="oauth-btn" @click="handleOAuth('github')">
|
||||
<view class="oauth-btn__icon" style="background: #24292F">
|
||||
<image class="oauth-btn__iconify" src="/static/icons/oauth/github.svg" />
|
||||
<image class="oauth-btn__iconify" src="/static/icons/github.svg" />
|
||||
</view>
|
||||
<text class="oauth-btn__label">
|
||||
GitHub
|
||||
@@ -329,7 +322,7 @@ async function handleOAuth(provider: OAuthProvider) {
|
||||
<!-- Gitee -->
|
||||
<view class="oauth-btn" @click="handleOAuth('gitee')">
|
||||
<view class="oauth-btn__icon" style="background: #C71D23">
|
||||
<image class="oauth-btn__iconify" src="/static/icons/oauth/gitee.svg" />
|
||||
<image class="oauth-btn__iconify" src="/static/icons/gitee.svg" />
|
||||
</view>
|
||||
<text class="oauth-btn__label">
|
||||
Gitee
|
||||
@@ -339,7 +332,7 @@ async function handleOAuth(provider: OAuthProvider) {
|
||||
<!-- QQ -->
|
||||
<view class="oauth-btn" @click="handleOAuth('qq')">
|
||||
<view class="oauth-btn__icon" style="background: #12B7F5">
|
||||
<image class="oauth-btn__iconify" src="/static/icons/oauth/qq.svg" />
|
||||
<image class="oauth-btn__iconify" src="/static/icons/qq.svg" />
|
||||
</view>
|
||||
<text class="oauth-btn__label">
|
||||
QQ
|
||||
@@ -483,6 +476,13 @@ async function handleOAuth(provider: OAuthProvider) {
|
||||
border-color: var(--border-color, #2C2C2E);
|
||||
}
|
||||
|
||||
/* 暗黑模式适配(wot-ui 滑块主题变量 + 深色层级体系) */
|
||||
.theme-dark :deep(.wd-slide-verify) {
|
||||
--wot-slide-verify-bg: var(--bg-color-2, #1C1C1E);
|
||||
--wot-slide-verify-button-bg: var(--bg-color-3, #2C2C2E);
|
||||
--wot-slide-verify-text-color: var(--text-color-3, #9CA3AF);
|
||||
}
|
||||
|
||||
/* ===== 记住密码 + 忘记密码 ===== */
|
||||
.login-options {
|
||||
display: flex;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { TicketAPI } from '@/api/module_system/ticket'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
|
||||
definePage({
|
||||
name: 'mine',
|
||||
layout: 'tabbar',
|
||||
@@ -25,10 +28,30 @@ function handleLogout() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 工单统计(真实数据:待处理 / 处理中 / 已完成) */
|
||||
const pendingTickets = ref<number | null>(null)
|
||||
const processingTickets = ref<number | null>(null)
|
||||
const doneTickets = ref<number | null>(null)
|
||||
|
||||
async function loadTicketStats() {
|
||||
const [pending, processing, done] = await Promise.allSettled([
|
||||
TicketAPI.getPage({ page_no: 1, page_size: 1, status: '0' }),
|
||||
TicketAPI.getPage({ page_no: 1, page_size: 1, status: '1' }),
|
||||
TicketAPI.getPage({ page_no: 1, page_size: 1, status: '2' }),
|
||||
])
|
||||
if (pending.status === 'fulfilled')
|
||||
pendingTickets.value = pending.value.total || 0
|
||||
if (processing.status === 'fulfilled')
|
||||
processingTickets.value = processing.value.total || 0
|
||||
if (done.status === 'fulfilled')
|
||||
doneTickets.value = done.value.total || 0
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0 && pages[pages.length - 1].route === 'pages/mine/index') {
|
||||
uni.$emit('updateTabbar', 'mine')
|
||||
loadTicketStats()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -49,9 +72,6 @@ const quickLinks = [
|
||||
{ title: '字典管理', name: 'work-dicts', icon: 'tags', color: '#F59E0B' },
|
||||
{ title: '通知公告', name: 'work-notices', icon: 'notification', color: '#10B981' },
|
||||
{ title: '参数管理', name: 'work-params', icon: 'settings', color: '#8B5CF6' },
|
||||
{ title: '定时任务', name: 'work-cronjob', icon: 'clock-circle', color: '#EC4899' },
|
||||
{ title: '工作流', name: 'work-workflow', icon: 'play-arrow', color: '#8B5CF6' },
|
||||
{ title: '设备管理', name: 'work-devices', icon: 'phone', color: '#10B981' },
|
||||
{ title: 'AI 模型', name: 'work-ai-models', icon: 'robot', color: '#8B5CF6' },
|
||||
]
|
||||
</script>
|
||||
@@ -73,22 +93,30 @@ const quickLinks = [
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- Stats -->
|
||||
<!-- Stats(真实工单数据) -->
|
||||
<view class="mine-stats fade-in-up-1">
|
||||
<view class="mine-stat-item card-pressable">
|
||||
<text class="mine-stat-item__value">
|
||||
1,024
|
||||
<view class="mine-stat-item card-pressable" @click="navigateTo('work-tickets')">
|
||||
<text class="mine-stat-item__value" style="color: var(--warning-color, #F59E0B);">
|
||||
{{ pendingTickets ?? '-' }}
|
||||
</text>
|
||||
<text class="mine-stat-item__label">
|
||||
操作次数
|
||||
待处理工单
|
||||
</text>
|
||||
</view>
|
||||
<view class="mine-stat-item card-pressable">
|
||||
<text class="mine-stat-item__value">
|
||||
365
|
||||
<view class="mine-stat-item card-pressable" @click="navigateTo('work-tickets')">
|
||||
<text class="mine-stat-item__value" style="color: var(--primary-color, #4F8CFF);">
|
||||
{{ processingTickets ?? '-' }}
|
||||
</text>
|
||||
<text class="mine-stat-item__label">
|
||||
登录天数
|
||||
处理中工单
|
||||
</text>
|
||||
</view>
|
||||
<view class="mine-stat-item card-pressable" @click="navigateTo('work-tickets')">
|
||||
<text class="mine-stat-item__value" style="color: var(--success-color, #10B981);">
|
||||
{{ doneTickets ?? '-' }}
|
||||
</text>
|
||||
<text class="mine-stat-item__label">
|
||||
已完成工单
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -246,7 +274,7 @@ const quickLinks = [
|
||||
/* ===== Quick grid ===== */
|
||||
.quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,11 +55,6 @@ const groups = [
|
||||
{ icon: 'file', text: '操作日志', name: 'work-oplogs' },
|
||||
{ icon: 'eye-fill', text: '登录日志', name: 'work-loginlogs' },
|
||||
{ icon: 'user', text: '在线用户', name: 'work-online' },
|
||||
{ icon: 'dashboard', text: '服务监控', name: 'work-server' },
|
||||
{ icon: 'refresh', text: '缓存管理', name: 'work-cache' },
|
||||
{ icon: 'folder', text: '文件管理', name: 'work-file-manager' },
|
||||
{ icon: 'clock-circle-fill', text: '定时任务', name: 'work-cronjob' },
|
||||
{ icon: 'phone', text: '设备管理', name: 'work-devices' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -69,8 +64,6 @@ const groups = [
|
||||
items: [
|
||||
{ icon: 'message', text: 'AI 助手', name: 'work-chat' },
|
||||
{ icon: 'robot', text: 'AI 模型', name: 'work-ai-models' },
|
||||
{ icon: 'file', text: '代码生成', name: 'work-gencode' },
|
||||
{ icon: 'play-arrow', text: '工作流', name: 'work-workflow' },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -109,9 +102,10 @@ const groups = [
|
||||
|
||||
<!-- Module groups -->
|
||||
<view v-for="(group, gi) in groups" :key="gi" class="module-group" :class="[`fade-in-up-${gi + 1}`]">
|
||||
<text class="module-group__title">
|
||||
{{ group.title }}
|
||||
</text>
|
||||
<view class="module-group__title">
|
||||
<view class="module-group__dot" :style="{ background: group.color }" />
|
||||
<text>{{ group.title }}</text>
|
||||
</view>
|
||||
<view class="module-grid">
|
||||
<view
|
||||
v-for="item in group.items"
|
||||
@@ -173,22 +167,22 @@ const groups = [
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== User card ===== */
|
||||
/* ===== User card(渐变欢迎卡,与 mine/login 设计语言一致) ===== */
|
||||
.user-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
background: var(--gradient-primary, linear-gradient(135deg, #4F8CFF, #2563EB));
|
||||
border-radius: 32rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 40rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
box-shadow: 0 8rpx 24rpx rgba(37, 99, 235, 0.18);
|
||||
|
||||
&__avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--primary-color, #4F8CFF), var(--primary-color-dark, #2970FF));
|
||||
background: rgba(255, 255, 255, 0.20);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -210,12 +204,12 @@ const groups = [
|
||||
&__name {
|
||||
font-size: var(--font-xl, 36rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
&__role {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,12 +218,21 @@ const groups = [
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
font-size: var(--font-lg, 32rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 4rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.module-grid {
|
||||
|
||||
Reference in New Issue
Block a user