refactor(frontend): 清理废弃资源与代码,优化项目结构

1.  删除大量旧版静态图标、图片资源,替换为新的svg图标
2.  移除废弃的API模块、页面组件与配置文件
3.  新增全局消息store与通用列表分页hook
4.  优化代码逻辑:简化错误处理、调整样式类顺序、更新路由鉴权
5.  替换旧的按钮样式属性,统一代码规范
This commit is contained in:
zhangtao
2026-08-03 00:15:44 +08:00
parent 6b14ee9edf
commit 27ab0d802c
82 changed files with 1073 additions and 5216 deletions
+280 -150
View File
@@ -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;
+42 -42
View File
@@ -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;
+41 -13
View File
@@ -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;
}
+20 -17
View File
@@ -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 {