mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-17 15:05:40 +00:00
chore: 初始化uniapp前端项目基础框架
新增项目配置、静态资源、工具类、状态管理、API接口、组件库集成等全套基础代码,完成项目初始化搭建
This commit is contained in:
@@ -0,0 +1,506 @@
|
||||
<script setup lang="ts">
|
||||
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 { DashboardAPI } from '@/api/module_monitor/dashboard'
|
||||
import { NoticeAPI } from '@/api/module_system/notice'
|
||||
import { TicketAPI } from '@/api/module_system/ticket'
|
||||
|
||||
definePage({
|
||||
name: 'home',
|
||||
layout: 'tabbar',
|
||||
style: { navigationBarTitleText: '工作台概览' },
|
||||
})
|
||||
|
||||
let _router: ReturnType<typeof useRouter> | null = null
|
||||
function getRouter() {
|
||||
if (!_router) {
|
||||
try {
|
||||
_router = useRouter()
|
||||
}
|
||||
catch {}
|
||||
return _router
|
||||
}
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const dashboardStats = ref<DashboardStats | null>(null)
|
||||
const pendingTickets = ref(0)
|
||||
const recentNotices = ref<NoticeItem[]>([])
|
||||
|
||||
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' },
|
||||
]
|
||||
|
||||
function navigateTo(name: string) {
|
||||
const r = getRouter()
|
||||
if (r)
|
||||
r.push({ name })
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [statsRes, ticketRes] = await Promise.allSettled([
|
||||
DashboardAPI.getStats(),
|
||||
TicketAPI.getPage({ page_no: 1, page_size: 1, status: '0' }),
|
||||
])
|
||||
if (statsRes.status === 'fulfilled')
|
||||
dashboardStats.value = statsRes.value
|
||||
if (ticketRes.status === 'fulfilled')
|
||||
pendingTickets.value = ticketRes.value.total || 0
|
||||
}
|
||||
catch { /* silent */ }
|
||||
finally {
|
||||
loading.value = false
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNotices() {
|
||||
try {
|
||||
const res = await NoticeAPI.getPage({ page_no: 1, page_size: 3, status: 1 })
|
||||
recentNotices.value = res.list || []
|
||||
}
|
||||
catch { /* silent */ }
|
||||
}
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
Promise.all([loadData(), loadNotices()])
|
||||
})
|
||||
|
||||
onReady(() => {
|
||||
Promise.all([loadData(), loadNotices()])
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0 && pages[pages.length - 1].route === 'pages/index/index')
|
||||
uni.$emit('updateTabbar', 'index')
|
||||
})
|
||||
|
||||
function getDateString() {
|
||||
const now = new Date()
|
||||
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
||||
return `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日 星期${weekDays[now.getDay()]}`
|
||||
}
|
||||
|
||||
function getGreeting() {
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 6)
|
||||
return '夜深了'
|
||||
if (hour < 9)
|
||||
return '早上好'
|
||||
if (hour < 12)
|
||||
return '上午好'
|
||||
if (hour < 14)
|
||||
return '中午好'
|
||||
if (hour < 18)
|
||||
return '下午好'
|
||||
return '晚上好'
|
||||
}
|
||||
</script>
|
||||
|
||||
<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 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>
|
||||
</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" />
|
||||
<text class="home-grid__label">
|
||||
{{ item.title }}
|
||||
</text>
|
||||
</wd-grid-item>
|
||||
</wd-grid>
|
||||
</view>
|
||||
|
||||
<!-- Stats -->
|
||||
<view class="section-header">
|
||||
<text class="section-header__title">
|
||||
运营概览
|
||||
</text>
|
||||
</view>
|
||||
<SkeletonPage v-if="loading && !dashboardStats" :rows="3" />
|
||||
<template v-else>
|
||||
<view class="stats-row">
|
||||
<view class="stat-item card-pressable fade-in-up-1">
|
||||
<text class="stat-item__value stat-item__value--primary count-up">
|
||||
{{ dashboardStats?.total_users ?? '-' }}
|
||||
</text>
|
||||
<text class="stat-item__label">
|
||||
注册用户
|
||||
</text>
|
||||
<text class="stat-item__sub">
|
||||
本周新增 {{ dashboardStats?.week_user_created ?? 0 }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="stat-item card-pressable fade-in-up-2">
|
||||
<text class="stat-item__value stat-item__value--success count-up">
|
||||
{{ dashboardStats?.online_users ?? '-' }}
|
||||
</text>
|
||||
<text class="stat-item__label">
|
||||
在线用户
|
||||
</text>
|
||||
<text class="stat-item__sub">
|
||||
当前在线
|
||||
</text>
|
||||
</view>
|
||||
<view class="stat-item card-pressable fade-in-up-3">
|
||||
<text class="stat-item__value stat-item__value--warning count-up">
|
||||
{{ dashboardStats?.today_login_count ?? '-' }}
|
||||
</text>
|
||||
<text class="stat-item__label">
|
||||
今日登录
|
||||
</text>
|
||||
<text class="stat-item__sub">
|
||||
{{ dashboardStats?.today_unique_users ?? 0 }} 独立用户
|
||||
</text>
|
||||
</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 class="section-header">
|
||||
<text class="section-header__title">
|
||||
最新公告
|
||||
</text>
|
||||
<text class="section-header__more" @click="navigateTo('work-notices')">
|
||||
全部 →
|
||||
</text>
|
||||
</view>
|
||||
<view class="home-card">
|
||||
<wd-cell-group border>
|
||||
<wd-cell v-for="item in recentNotices" :key="item.id" :title="item.notice_title" is-link @click="navigateTo('work-notices')">
|
||||
<template #label>
|
||||
<text class="home-cell__time">
|
||||
{{ item.created_time || '' }}
|
||||
</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Recent logins -->
|
||||
<view v-if="dashboardStats?.recent_logins?.length" class="home-section">
|
||||
<view class="section-header">
|
||||
<text class="section-header__title">
|
||||
最近登录
|
||||
</text>
|
||||
</view>
|
||||
<view class="home-card">
|
||||
<wd-cell-group border>
|
||||
<wd-cell v-for="(item, i) in dashboardStats.recent_logins" :key="i" :title="item.username">
|
||||
<template #label>
|
||||
<text class="home-cell__time">
|
||||
{{ item.login_time || '' }}{{ item.login_ip ? ` · ${item.login_ip}` : '' }}
|
||||
</text>
|
||||
</template>
|
||||
<StatusBadge :status="item.status === 1 ? 'success' : 'failed'" />
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- Bottom safe area -->
|
||||
<view style="height: 100rpx;" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home-page {
|
||||
padding: 0 32rpx;
|
||||
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
background: var(--page-bg-color, #F9F9F9);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ===== Navigation ===== */
|
||||
.home-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0 24rpx;
|
||||
|
||||
&__title {
|
||||
font-size: var(--font-2xl, 40rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__bell {
|
||||
position: relative;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-icon { font-size: var(--font-lg, 32rpx); }
|
||||
}
|
||||
|
||||
&__badge {
|
||||
position: absolute;
|
||||
top: 14rpx;
|
||||
right: 14rpx;
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--danger-color, #EF4444);
|
||||
border: 2rpx solid var(--bg-color, #FFFFFF);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Greeting ===== */
|
||||
.greeting-card {
|
||||
background: var(--gradient-primary, linear-gradient(135deg, #4F8CFF, #2563EB));
|
||||
border-radius: 32rpx;
|
||||
padding: 40rpx 32rpx;
|
||||
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 {
|
||||
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;
|
||||
|
||||
&__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); }
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
&__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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 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));
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
}
|
||||
|
||||
.home-nav-icon { width: 64rpx; height: 64rpx; border-radius: 12rpx; }
|
||||
|
||||
/* ===== Section header ===== */
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
&__title {
|
||||
font-size: var(--font-lg, 32rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__more {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: var(--primary-color, #4F8CFF);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Stats row ===== */
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: var(--shadow-xs, 0 3rpx 8rpx rgba(79, 140, 255, 0.03));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
|
||||
|
||||
&__value {
|
||||
font-size: var(--font-3xl, 48rpx);
|
||||
font-weight: 600;
|
||||
font-family: 'Inter', -apple-system, sans-serif;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
&--primary { color: var(--primary-color, #4F8CFF); }
|
||||
&--success { color: var(--success-color, #10B981); }
|
||||
&--warning { color: var(--warning-color, #F59E0B); }
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: var(--font-xs, 20rpx);
|
||||
color: var(--text-color-4, #B0B0B0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 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;
|
||||
}
|
||||
|
||||
.home-card {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
box-shadow: var(--shadow-xs, 0 3rpx 8rpx rgba(1, 77, 178, 0.03));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===== Home cell time ===== */
|
||||
.home-cell__time {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: var(--text-color-4, #B0B0B0);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,611 @@
|
||||
<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 { 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 loading = ref(false)
|
||||
const userStore = useUserStore()
|
||||
const redirect = ref('/pages/index/index')
|
||||
|
||||
/** 表单验证 schema — 用户名/密码字段级错误提示(wot-ui 函数式校验) */
|
||||
const loginSchema: FormSchema = {
|
||||
validate: (model) => {
|
||||
const errors: Array<{ path: Array<string | number>, message: string }> = []
|
||||
const username = String(model.username ?? '')
|
||||
const password = String(model.password ?? '')
|
||||
if (!username)
|
||||
errors.push({ path: ['username'], message: '请输入用户名' })
|
||||
else if (username.length < 3 || username.length > 20)
|
||||
errors.push({ path: ['username'], message: '用户名长度为 3-20 个字符' })
|
||||
if (!password)
|
||||
errors.push({ path: ['password'], message: '请输入密码' })
|
||||
else if (password.length < 6 || password.length > 20)
|
||||
errors.push({ path: ['password'], message: '密码长度为 6-20 个字符' })
|
||||
return errors
|
||||
},
|
||||
}
|
||||
|
||||
const loginFormData = reactive<LoginFormData>({
|
||||
username: '',
|
||||
password: '',
|
||||
captcha: '',
|
||||
captcha_key: '',
|
||||
remember: true,
|
||||
login_type: '移动端',
|
||||
})
|
||||
|
||||
const captchaState = reactive<CaptchaInfo>({ enable: false, key: '', img_base: '' })
|
||||
|
||||
/**
|
||||
* 滑块验证状态:
|
||||
* - idle 待验证(初始/重置后)
|
||||
* - pending 验证中(已拖动到终点,正在请求后端)
|
||||
* - success 验证成功(后端返回 verified=true)
|
||||
* - fail 验证失败(后端返回错误或网络异常)
|
||||
*/
|
||||
const sliderStatus = ref<'idle' | 'pending' | 'success' | 'fail'>('idle')
|
||||
|
||||
/** 从本地存储恢复记住的用户名(仅用户名,不存储密码) */
|
||||
function restoreRememberedUser() {
|
||||
const remembered = Storage.get<{ username: string, remember: boolean }>(REMEMBER_ME_KEY)
|
||||
if (remembered) {
|
||||
loginFormData.username = remembered.username || ''
|
||||
loginFormData.remember = remembered.remember ?? true
|
||||
}
|
||||
}
|
||||
|
||||
async function getLoginCaptcha() {
|
||||
try {
|
||||
const result = await AuthAPI.getCaptcha()
|
||||
if (result && typeof result === 'object') {
|
||||
captchaState.enable = Boolean(result.enable)
|
||||
captchaState.key = result.key || ''
|
||||
captchaState.img_base = result.img_base || ''
|
||||
if (captchaState.enable) {
|
||||
loginFormData.captcha_key = captchaState.key
|
||||
// 重置滑块到初始状态
|
||||
sliderStatus.value = 'idle'
|
||||
sliderCaptchaRef.value?.reset?.()
|
||||
}
|
||||
}
|
||||
else {
|
||||
captchaState.enable = false
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
logger.error('验证码获取失败', e)
|
||||
captchaState.enable = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 滑块拖动到终点时触发 — 调用后端 slider_complete 接口标记验证完成
|
||||
* 后端仅标记 captcha_key 状态为 verified,不校验 x 坐标
|
||||
*/
|
||||
async function handleSliderVerify() {
|
||||
if (!captchaState.key) {
|
||||
sliderStatus.value = 'fail'
|
||||
uni.showToast({ title: '验证码已过期,请刷新', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
sliderStatus.value = 'pending'
|
||||
try {
|
||||
const result = await AuthAPI.completeSliderCaptcha({
|
||||
captcha_key: captchaState.key,
|
||||
x: 100, // 占位值,后端未使用
|
||||
})
|
||||
if (result?.verified) {
|
||||
sliderStatus.value = 'success'
|
||||
loginFormData.captcha = 'verified' // 占位值,后端只校验 captcha_key 状态
|
||||
}
|
||||
else {
|
||||
sliderStatus.value = 'fail'
|
||||
uni.showToast({ title: '验证失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
logger.error('滑块验证失败', e)
|
||||
sliderStatus.value = 'fail'
|
||||
uni.showToast({ title: getErrorMessage(e, '验证失败,请重试'), icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
/** 滑块重置时清空占位验证值 */
|
||||
function handleSliderReset() {
|
||||
loginFormData.captcha = ''
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
const from = options?.redirect ? decodeURIComponent(options.redirect) : ''
|
||||
// 安全验证:仅允许本地页面路径,防止开放重定向攻击
|
||||
if (from && from !== '/pages/login/index' && from.startsWith('/pages/')) {
|
||||
redirect.value = from
|
||||
}
|
||||
restoreRememberedUser()
|
||||
getLoginCaptcha()
|
||||
})
|
||||
|
||||
/** 登录提交 — 防抖 + 字段验证 + 滑块校验 + 错误提示 + 记住密码 */
|
||||
async function handleSubmit() {
|
||||
if (loading.value)
|
||||
return
|
||||
|
||||
// 滑块验证前置校验
|
||||
if (captchaState.enable && sliderStatus.value !== 'success') {
|
||||
uni.showToast({ title: '请先完成滑块验证', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const { valid } = await loginFormRef.value.validate()
|
||||
if (!valid) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
await userStore.login(loginFormData)
|
||||
// 登录成功后处理记住密码:仅存储用户名,不存储密码
|
||||
if (loginFormData.remember) {
|
||||
Storage.set(REMEMBER_ME_KEY, { username: loginFormData.username, remember: true })
|
||||
}
|
||||
else {
|
||||
Storage.remove(REMEMBER_ME_KEY)
|
||||
}
|
||||
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 })
|
||||
// 登录失败后自动刷新验证码并重置滑块
|
||||
if (captchaState.enable)
|
||||
getLoginCaptcha()
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 第三方 OAuth 登录 */
|
||||
async function handleOAuth(provider: OAuthProvider) {
|
||||
try {
|
||||
const result = await AuthAPI.getOAuthLoginUrl(provider)
|
||||
if (result?.url) {
|
||||
// #ifdef H5
|
||||
window.location.href = result.url
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.setClipboardData({
|
||||
data: result.url,
|
||||
success: () => {
|
||||
uni.showToast({ title: '链接已复制,请在浏览器中打开', icon: 'none' })
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
uni.showToast({ title: getErrorMessage(e, 'OAuth登录失败'), icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<!-- Brand area -->
|
||||
<view class="login-brand fade-in-up">
|
||||
<image class="brand-logo" src="/static/logo.png" mode="aspectFit" />
|
||||
<text class="brand-title">
|
||||
FastapiAdmin
|
||||
</text>
|
||||
<text class="brand-subtitle">
|
||||
高效的企业管理移动平台
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- Form card -->
|
||||
<view class="login-card bounce-in">
|
||||
<text class="login-card__title">
|
||||
账号登录
|
||||
</text>
|
||||
|
||||
<wd-form ref="loginFormRef" :model="loginFormData" :schema="loginSchema">
|
||||
<!-- Username -->
|
||||
<wd-form-item prop="username">
|
||||
<view class="login-field">
|
||||
<wd-input
|
||||
v-model="loginFormData.username"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
confirm-type="next"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<template #prefix>
|
||||
<wd-icon name="user" size="20rpx" color="#9CA3AF" />
|
||||
</template>
|
||||
</wd-input>
|
||||
</view>
|
||||
</wd-form-item>
|
||||
|
||||
<!-- Password — show-password 自动启用密码可见性切换 -->
|
||||
<wd-form-item prop="password">
|
||||
<view class="login-field">
|
||||
<wd-input
|
||||
v-model="loginFormData.password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
clearable
|
||||
confirm-type="go"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<template #prefix>
|
||||
<wd-icon name="lock" size="20rpx" color="#9CA3AF" />
|
||||
</template>
|
||||
</wd-input>
|
||||
</view>
|
||||
</wd-form-item>
|
||||
|
||||
<!-- Slider Captcha — 滑块拖动验证(条件显示) -->
|
||||
<wd-form-item v-if="captchaState.enable">
|
||||
<SliderCaptcha
|
||||
ref="sliderCaptchaRef"
|
||||
:status="sliderStatus"
|
||||
@verify="handleSliderVerify"
|
||||
@reset="handleSliderReset"
|
||||
/>
|
||||
</wd-form-item>
|
||||
|
||||
<!-- 记住密码 + 忘记密码 -->
|
||||
<view class="login-options">
|
||||
<view class="remember-check" @click="loginFormData.remember = !loginFormData.remember">
|
||||
<view class="remember-check__box" :class="{ 'is-checked': loginFormData.remember }">
|
||||
<text v-if="loginFormData.remember" class="remember-check__tick">
|
||||
✓
|
||||
</text>
|
||||
</view>
|
||||
<text class="remember-check__label">
|
||||
记住密码
|
||||
</text>
|
||||
</view>
|
||||
<text class="forgot-link">
|
||||
忘记密码?
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- Submit -->
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="loading"
|
||||
round
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ loading ? '登录中...' : '登 录' }}
|
||||
</wd-button>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- OAuth Login -->
|
||||
<view class="oauth-section fade-in-up-3">
|
||||
<view class="oauth-divider">
|
||||
<view class="oauth-divider__line" />
|
||||
<text class="oauth-divider__text">
|
||||
第三方登录
|
||||
</text>
|
||||
<view class="oauth-divider__line" />
|
||||
</view>
|
||||
<view class="oauth-buttons">
|
||||
<!-- 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" />
|
||||
</view>
|
||||
<text class="oauth-btn__label">
|
||||
微信
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 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" />
|
||||
</view>
|
||||
<text class="oauth-btn__label">
|
||||
GitHub
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 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" />
|
||||
</view>
|
||||
<text class="oauth-btn__label">
|
||||
Gitee
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 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" />
|
||||
</view>
|
||||
<text class="oauth-btn__label">
|
||||
QQ
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Footer -->
|
||||
<view class="login-footer">
|
||||
<text class="login-footer__text">
|
||||
立即注册 | 游客访问
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
/* H5 下 100vh 包含导航栏,使用uni-app的可用视口高度变量避免溢出 */
|
||||
/* #ifdef H5 */
|
||||
height: calc(100vh - 44px);
|
||||
/* #endif */
|
||||
/* #ifndef H5 */
|
||||
height: 100vh;
|
||||
/* #endif */
|
||||
padding: 0 64rpx;
|
||||
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
|
||||
background: var(--page-bg-color, #F9F9F9);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 暗黑模式下整页背景 + 所有子元素继承深色,消除白色断层 */
|
||||
:deep(.theme-dark) .login-page,
|
||||
.login-page:has(.theme-dark),
|
||||
.theme-dark .login-page {
|
||||
background: var(--bg-color-1, #0F0F11);
|
||||
}
|
||||
|
||||
/* ===== Brand ===== */
|
||||
.login-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 72rpx;
|
||||
padding-bottom: 40rpx;
|
||||
gap: 12rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
.brand-logo {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: var(--font-3xl, 48rpx);
|
||||
font-weight: 700;
|
||||
color: var(--text-color, #0A1628);
|
||||
|
||||
/* 暗黑模式下使用纯白,提升品牌标题醒目度 */
|
||||
:deep(.theme-dark) &,
|
||||
.theme-dark & {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
font-size: var(--font-md, 28rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
|
||||
/* 暗黑下提亮到 --text-color-2,避免 #9CA3AF 在深底上偏暗 */
|
||||
:deep(.theme-dark) &,
|
||||
.theme-dark & {
|
||||
color: var(--text-color-2, #D1D5DB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Card(不再是毛玻璃,使用纯色背景 + 与页面背景形成层级差) ===== */
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 654rpx;
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: var(--radius-xl, 32rpx);
|
||||
padding: 40rpx 36rpx;
|
||||
border: 2rpx solid var(--border-color, #EAECF0);
|
||||
box-shadow: var(--shadow-md, 0 8rpx 32rpx rgba(15, 23, 42, 0.04));
|
||||
margin-bottom: 20rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
/* 暗黑模式:卡片用深色 2 级,页面背景用深色 1 级,形成细微层级差 */
|
||||
:deep(.theme-dark) &,
|
||||
.theme-dark & {
|
||||
background: var(--bg-color-3, #1A1A1A);
|
||||
border-color: var(--border-color, #2C2C2E);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
font-size: var(--font-xl, 36rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
margin-bottom: 28rpx;
|
||||
|
||||
/* 暗黑模式下使用纯白,提升卡片标题醒目度 */
|
||||
:deep(.theme-dark) &,
|
||||
.theme-dark & {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 表单输入框容器 — 圆角底色包裹,替代 uview 的 border="surround" ===== */
|
||||
.login-field {
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
border: 2rpx solid var(--border-color, #EAECF0);
|
||||
border-radius: 16rpx;
|
||||
padding: 0 20rpx;
|
||||
transition: border-color var(--transition-fast, 0.15s ease);
|
||||
}
|
||||
|
||||
/* 暗黑模式适配(wot-ui 输入框主题变量 + 容器背景) */
|
||||
:deep(.wd-form-item) {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.wd-input) {
|
||||
--wot-input-bg: var(--bg-color-3, #2C2C2E);
|
||||
--wot-input-inner-color: var(--text-color, #FFFFFF);
|
||||
--wot-input-inner-placeholder-color: var(--text-color-4, #6B7280);
|
||||
}
|
||||
|
||||
.theme-dark .login-field {
|
||||
background: var(--bg-color-3, #2C2C2E);
|
||||
border-color: var(--border-color, #2C2C2E);
|
||||
}
|
||||
|
||||
/* ===== 记住密码 + 忘记密码 ===== */
|
||||
.login-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 28rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.remember-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 8rpx 0;
|
||||
|
||||
&__box {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 8rpx;
|
||||
border: 2rpx solid var(--text-color-4, #B0B0B0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background var(--transition-fast, 0.15s ease), border-color var(--transition-fast, 0.15s ease);
|
||||
|
||||
&.is-checked {
|
||||
background: var(--primary-color, #4F8CFF);
|
||||
border-color: var(--primary-color, #4F8CFF);
|
||||
}
|
||||
}
|
||||
|
||||
&__tick {
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-md, 28rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
font-size: var(--font-md, 28rpx);
|
||||
color: var(--primary-color, #4F8CFF);
|
||||
}
|
||||
|
||||
/* ===== OAuth Section ===== */
|
||||
.oauth-section {
|
||||
width: 100%;
|
||||
max-width: 654rpx;
|
||||
margin-top: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.oauth-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
&__line {
|
||||
flex: 1;
|
||||
height: 1rpx;
|
||||
background: var(--border-color, #EAEAEA);
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: var(--text-color-4, #B0B0B0);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.oauth-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 48rpx;
|
||||
}
|
||||
|
||||
.oauth-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
|
||||
&__icon {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform var(--transition-fast, 0.15s ease);
|
||||
}
|
||||
|
||||
&:active &__icon {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
&__iconify {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-xs, 20rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Footer ===== */
|
||||
.login-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24rpx 0 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
&__text {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: var(--text-color-4, #B0B0B0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,339 @@
|
||||
<script lang="ts" setup>
|
||||
definePage({
|
||||
name: 'mine',
|
||||
layout: 'tabbar',
|
||||
style: { navigationBarTitleText: '我的' },
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
|
||||
function navigateTo(name: string) {
|
||||
router.push({ name })
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认退出登录吗?',
|
||||
success(res) {
|
||||
if (res.confirm) {
|
||||
userStore.logout()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0 && pages[pages.length - 1].route === 'pages/mine/index') {
|
||||
uni.$emit('updateTabbar', 'mine')
|
||||
}
|
||||
})
|
||||
|
||||
const settingsList = [
|
||||
{ title: '首页示例', name: 'home-demo' },
|
||||
{ title: '个人资料', name: 'profile' },
|
||||
{ title: '修改密码', name: 'account' },
|
||||
{ title: '通知设置', name: 'settings' },
|
||||
{ title: '关于我们', name: 'about' },
|
||||
]
|
||||
|
||||
const quickLinks = [
|
||||
{ title: '用户管理', name: 'work-users', icon: 'user', color: 'var(--primary-color)' },
|
||||
{ title: '角色管理', name: 'work-roles', icon: 'lock', color: '#F59E0B' },
|
||||
{ title: '部门管理', name: 'work-depts', icon: 'apps', color: '#10B981' },
|
||||
{ title: '菜单管理', name: 'work-menus', icon: 'menu', color: '#8B5CF6' },
|
||||
{ title: '岗位管理', name: 'work-positions', icon: 'list', color: 'var(--primary-color)' },
|
||||
{ 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>
|
||||
|
||||
<template>
|
||||
<view class="mine-page">
|
||||
<!-- Header -->
|
||||
<view class="mine-header fade-in-up">
|
||||
<view class="mine-header__avatar">
|
||||
<text class="mine-header__avatar-text">
|
||||
{{ (userInfo?.name || '管').charAt(0) }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="mine-header__name">
|
||||
{{ userInfo?.name || '管理员' }}
|
||||
</text>
|
||||
<text class="mine-header__role">
|
||||
{{ userInfo?.roles?.map(r => r.name).join(', ') || '超级管理员 · 技术部' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- Stats -->
|
||||
<view class="mine-stats fade-in-up-1">
|
||||
<view class="mine-stat-item card-pressable">
|
||||
<text class="mine-stat-item__value">
|
||||
1,024
|
||||
</text>
|
||||
<text class="mine-stat-item__label">
|
||||
操作次数
|
||||
</text>
|
||||
</view>
|
||||
<view class="mine-stat-item card-pressable">
|
||||
<text class="mine-stat-item__value">
|
||||
365
|
||||
</text>
|
||||
<text class="mine-stat-item__label">
|
||||
登录天数
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Quick links -->
|
||||
<view class="mine-section fade-in-up-2">
|
||||
<text class="mine-section__title">
|
||||
快捷入口
|
||||
</text>
|
||||
<view class="quick-grid">
|
||||
<view
|
||||
v-for="item in quickLinks" :key="item.name"
|
||||
class="quick-item"
|
||||
hover-class="quick-item--hover"
|
||||
@click="navigateTo(item.name)"
|
||||
>
|
||||
<view class="quick-item__icon" :style="{ background: `${item.color}15` }">
|
||||
<wd-icon :name="item.icon" size="22px" :color="item.color" />
|
||||
</view>
|
||||
<text class="quick-item__text">
|
||||
{{ item.title }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Settings list -->
|
||||
<view class="mine-section fade-in-up-3">
|
||||
<text class="mine-section__title">
|
||||
设置
|
||||
</text>
|
||||
<view class="settings-card">
|
||||
<view
|
||||
v-for="(item, i) in settingsList" :key="i"
|
||||
class="settings-item"
|
||||
hover-class="settings-item--hover"
|
||||
@click="navigateTo(item.name)"
|
||||
>
|
||||
<text class="settings-item__label">
|
||||
{{ item.title }}
|
||||
</text>
|
||||
<text class="settings-item__arrow">
|
||||
›
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Logout -->
|
||||
<view class="mine-section fade-in-up-4">
|
||||
<view class="logout-btn" hover-class="logout-btn--hover" @click="handleLogout">
|
||||
<text class="logout-btn__text">
|
||||
退出登录
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Bottom safe area -->
|
||||
<view style="height: 100rpx;" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mine-page {
|
||||
padding: 0 32rpx;
|
||||
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
background: var(--page-bg-color, #F9F9F9);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ===== Header ===== */
|
||||
.mine-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40rpx 40rpx 48rpx;
|
||||
margin: 16rpx 0 32rpx;
|
||||
background: var(--gradient-primary, linear-gradient(135deg, #4F8CFF, #2563EB));
|
||||
border-radius: 32rpx;
|
||||
|
||||
&__avatar {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.20);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
&__avatar-text {
|
||||
font-size: var(--font-3xl, 48rpx);
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: var(--font-2xl, 40rpx);
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&__role {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: rgba(255, 255, 255, 0.80);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Stats ===== */
|
||||
.mine-stats {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.mine-stat-item {
|
||||
flex: 1;
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&__value {
|
||||
font-size: var(--font-2xl, 40rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
font-family: 'Inter', -apple-system, sans-serif;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Section ===== */
|
||||
.mine-section {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
font-size: var(--font-lg, 32rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Quick grid ===== */
|
||||
.quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.quick-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx 16rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&--hover {
|
||||
opacity: 0.85;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: var(--font-xs, 20rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Settings ===== */
|
||||
.settings-card {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1rpx solid var(--border-color-light, #F3F4F6);
|
||||
}
|
||||
|
||||
&--hover {
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-lg, 32rpx);
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
font-size: var(--font-xl, 36rpx);
|
||||
color: var(--text-color-4, #B0B0B0);
|
||||
font-family: 'Inter', -apple-system, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Logout ===== */
|
||||
.logout-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 96rpx;
|
||||
background: var(--danger-color-light, #FEF2F2);
|
||||
border-radius: 24rpx;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&--hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: var(--font-lg, 32rpx);
|
||||
font-weight: 500;
|
||||
color: var(--danger-color, #EF4444);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,273 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
|
||||
definePage({
|
||||
name: 'work',
|
||||
layout: 'tabbar',
|
||||
style: { navigationBarTitleText: '工作台' },
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
function navigateTo(name: string) {
|
||||
router.push({ name })
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0 && pages[pages.length - 1].route === 'pages/work/index') {
|
||||
uni.$emit('updateTabbar', 'work')
|
||||
}
|
||||
})
|
||||
|
||||
const groups = [
|
||||
{
|
||||
title: '系统管理',
|
||||
color: 'var(--primary-color)',
|
||||
bg: 'var(--primary-color-light)',
|
||||
items: [
|
||||
{ icon: 'user', text: '用户管理', name: 'work-users' },
|
||||
{ icon: 'lock', text: '角色管理', name: 'work-roles' },
|
||||
{ icon: 'apps', text: '部门管理', name: 'work-depts' },
|
||||
{ icon: 'menu', text: '菜单管理', name: 'work-menus' },
|
||||
{ icon: 'list', text: '岗位管理', name: 'work-positions' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '系统配置',
|
||||
color: '#F59E0B',
|
||||
bg: 'var(--warning-color-light)',
|
||||
items: [
|
||||
{ icon: 'tags', text: '字典管理', name: 'work-dicts' },
|
||||
{ icon: 'notification', text: '通知公告', name: 'work-notices' },
|
||||
{ icon: 'settings', text: '参数管理', name: 'work-params' },
|
||||
{ icon: 'refresh', text: '版本管理', name: 'work-versions' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '运维监控',
|
||||
color: '#10B981',
|
||||
bg: 'var(--success-color-light)',
|
||||
items: [
|
||||
{ icon: 'message', text: '工单管理', name: 'work-tickets' },
|
||||
{ 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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '开发者工具',
|
||||
color: '#8B5CF6',
|
||||
bg: 'var(--bg-color-2)',
|
||||
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' },
|
||||
],
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="work-page">
|
||||
<!-- Navigation -->
|
||||
<view class="work-nav">
|
||||
<text class="work-nav__title">
|
||||
工作台
|
||||
</text>
|
||||
<view class="work-nav__avatar" @click="navigateTo('mine')">
|
||||
<text class="work-nav__avatar-text">
|
||||
{{ (userInfo?.name || '管').charAt(0) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- User info card -->
|
||||
<view class="user-card fade-in-up">
|
||||
<view class="user-card__avatar">
|
||||
<text class="user-card__avatar-text">
|
||||
{{ (userInfo?.name || '管').charAt(0) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="user-card__info">
|
||||
<text class="user-card__name">
|
||||
{{ userInfo?.name || 'FastapiAdmin' }}
|
||||
</text>
|
||||
<text class="user-card__role">
|
||||
{{ userInfo?.roles?.map(r => r.name).join(', ') || '超级管理员' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 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-grid">
|
||||
<view
|
||||
v-for="item in group.items"
|
||||
:key="item.name"
|
||||
class="module-item"
|
||||
hover-class="module-item--hover"
|
||||
@click="navigateTo(item.name)"
|
||||
>
|
||||
<view class="module-item__icon" :style="{ background: group.bg }">
|
||||
<wd-icon :name="item.icon" size="22px" :color="group.color" />
|
||||
</view>
|
||||
<text class="module-item__text">
|
||||
{{ item.text }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Bottom safe area -->
|
||||
<view style="height: 100rpx;" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.work-page {
|
||||
padding: 0 32rpx;
|
||||
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
background: var(--page-bg-color, #F9F9F9);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ===== Navigation ===== */
|
||||
.work-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0 24rpx;
|
||||
|
||||
&__title {
|
||||
font-size: var(--font-2xl, 40rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--primary-color, #4F8CFF), var(--primary-color-dark, #2970FF));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__avatar-text {
|
||||
font-size: var(--font-md, 28rpx);
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== User card ===== */
|
||||
.user-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 32rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 40rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
|
||||
&__avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--primary-color, #4F8CFF), var(--primary-color-dark, #2970FF));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__avatar-text {
|
||||
font-size: var(--font-xl, 36rpx);
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: var(--font-xl, 36rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__role {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Module group ===== */
|
||||
.module-group {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
font-size: var(--font-lg, 32rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.module-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.module-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&--hover {
|
||||
opacity: 0.85;
|
||||
transform: scale(0.98);
|
||||
box-shadow: var(--shadow-md, 0 4rpx 12rpx rgba(1, 77, 178,0.10));
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: var(--font-md, 28rpx);
|
||||
font-weight: 500;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user