feat: 初始化前端项目基础结构

- 添加项目配置文件(tsconfig, eslint, prettier等)
- 实现基础路由和页面布局
- 添加全局状态管理和API请求封装
- 集成UI组件库和主题系统
- 添加文档网站和示例页面
- 配置CI/CD和工作流
This commit is contained in:
zhangtao
2026-05-01 00:47:48 +08:00
parent ff738007cd
commit 5c29d22aa5
256 changed files with 45257 additions and 2 deletions
+331
View File
@@ -0,0 +1,331 @@
<script setup lang="ts">
import { onReady, onShow } from '@dcloudio/uni-app'
import { reactive, ref } from 'vue'
import LineChart from '@/subPages/echarts/components/LineChart.vue'
defineOptions({
componentPlaceholder: {
LineChart: 'view',
},
})
definePage({
name: 'home',
layout: 'tabbar',
style: {
navigationBarTitleText: '首页',
},
})
const current = ref<number>(0)
/**
* 访问统计数据
*/
interface VisitStatsVO {
todayUvCount: number
uvGrowthRate: number
totalUvCount: number
todayPvCount: number
pvGrowthRate: number
totalPvCount: number
}
/**
* 导航项
*/
interface NavItem {
icon: string
title: string
url: string
prem: string
}
/**
* 快捷导航列表
*/
const NAV_LIST: NavItem[] = [
{
icon: '/static/icons/user.png',
title: '用户管理',
url: '/pages/work/user/index',
prem: 'sys:user:query',
},
{
icon: '/static/icons/role.png',
title: '角色管理',
url: '/pages/work/role/index',
prem: 'sys:role:query',
},
{
icon: '/static/icons/notice.png',
title: '通知公告',
url: '/pages/work/notice/index',
prem: 'sys:notice:query',
},
{
icon: '/static/icons/setting.png',
title: '系统配置',
url: '/pages/work/config/index',
prem: 'sys:config:query',
},
]
/**
* 默认访问统计数据
*/
const DEFAULT_VISIT_STATS: VisitStatsVO = {
todayUvCount: 1234,
uvGrowthRate: 15.6,
totalUvCount: 45678,
todayPvCount: 5678,
pvGrowthRate: 23.4,
totalPvCount: 123456,
}
// 通知公告文本
const NOTICE_TEXT = '通知公告: fastapp 是一个基于 Vue3 + UniApp 的前端模板项目,提供了一套完整的前端解决方案'
// 默认日期范围(天数)
const DEFAULT_DAYS_RANGE = 7
// 访问统计数据
const visitStatsData = ref<VisitStatsVO>(DEFAULT_VISIT_STATS)
// 图表数据
const chartData = ref({})
// 日期范围
const recentDaysRange = ref(DEFAULT_DAYS_RANGE)
// 轮播图列表
const swiperList = ref([
'/static/images/banner01.jpg',
'/static/images/banner02.jpg',
])
// 快捷导航列表
const navList = reactive(NAV_LIST)
// 导航到指定页面
function navigateTo(url: string) {
// 避免空 URL 导航
if (!url)
return
// 根据 URL 类型选择不同的导航方式
if (
url.startsWith('/pages/')
&& (url.includes('/index/')
|| url.includes('/mine/')
|| url.includes('/work/'))
) {
uni.switchTab({
url,
})
}
else {
uni.navigateTo({
url,
})
}
}
// 生成静态的访问趋势数据
function generateStaticTrendData(days: number) {
const ipList = []
const pvList = []
const today = new Date()
for (let i = days - 1; i >= 0; i--) {
const date = new Date(today)
date.setDate(today.getDate() - i)
// 生成模拟数据
ipList.push(Math.floor(Math.random() * 500) + 200)
pvList.push(Math.floor(Math.random() * 1000) + 500)
}
return {
ipList,
pvList,
}
}
// 加载访问统计数据(使用静态数据)
async function loadVisitStatsData() {
// 模拟异步加载
setTimeout(() => {
visitStatsData.value = { ...DEFAULT_VISIT_STATS }
}, 100)
}
// 加载访问趋势数据(使用静态数据)
function loadVisitTrendData() {
// 模拟异步加载
setTimeout(() => {
const data = generateStaticTrendData(recentDaysRange.value)
const res = {
series: [
{
name: 'UV',
data: data.ipList,
},
{
name: 'PV',
data: data.pvList,
},
],
}
chartData.value = JSON.parse(JSON.stringify(res))
}, 100)
}
onReady(() => {
loadVisitStatsData()
loadVisitTrendData()
})
onShow(() => {
// 确保 tabbar 状态正确
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
if (currentPage.route === 'pages/index/index') {
// 通过事件通知 tabbar 布局更新状态
uni.$emit('updateTabbar', 'index')
}
}
})
</script>
<template>
<view class="app-container">
<!-- 轮播图 -->
<view class="swiper-container">
<wd-swiper
v-model:current="current"
:list="swiperList"
autoplay
indicator
image-mode="scaleToFill"
/>
</view>
<!-- 快捷导航 -->
<view class="nav-grid">
<wd-grid clickable :column="4">
<wd-grid-item
v-for="(item, index) in navList"
:key="index"
@click="navigateTo(item.url)"
>
<template #icon>
<image class="icon-image" :src="item.icon" lazy-load />
</template>
<template #text>
<view class="nav-title">
<wd-text :text="item.title" />
</view>
</template>
</wd-grid-item>
</wd-grid>
</view>
<!-- 通知公告 -->
<view class="notice-text">
<wd-notice-bar
type="warning"
prefix="warn-bold"
:text="NOTICE_TEXT"
/>
</view>
<!-- 数据统计 -->
<view class="stats-grid">
<view class="stats-item">
<image class="stats-icon" src="/static/icons/visitor.png" lazy-load />
<view class="stats-info">
<wd-text text="访客数" />
<view class="stats-value">
<wd-text type="warning" :text="visitStatsData.todayUvCount" bold />
</view>
</view>
</view>
<view class="stats-item">
<image class="stats-icon" src="/static/icons/browser.png" lazy-load />
<view class="stats-info">
<wd-text text="浏览量" />
<view class="stats-value">
<wd-text type="success" :text="visitStatsData.todayPvCount" bold />
</view>
</view>
</view>
</view>
<!-- 访问趋势 -->
<view class="mb-1 rounded-2 bg-gray-50 p-3 shadow-sm">
<LineChart />
</view>
</view>
</template>
<style lang="scss" scoped>
.app-container {
padding: 20rpx;
.swiper-container {
margin-bottom: 20rpx;
border-radius: 16rpx;
overflow: hidden;
}
.nav-grid {
margin-bottom: 20rpx;
border-radius: 16rpx;
.icon-image {
width: 64rpx;
height: 64rpx;
border-radius: 8rpx;
}
.nav-title {
margin-bottom: 20rpx;
border-radius: 16rpx;
font-size: 24rpx;
text-align: center;
margin-top: 12rpx;
}
}
.notice-text {
margin-bottom: 20rpx;
border-radius: 16rpx;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10rpx;
margin-bottom: 20rpx;
}
.stats-item {
display: flex;
align-items: center;
padding: 20rpx;
background-color: var(--bg-color-2);
border-radius: 16rpx;
}
.stats-icon {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
margin-right: 20rpx;
}
}
</style>
+386
View File
@@ -0,0 +1,386 @@
<script lang="ts" setup>
import type { CaptchaInfo, LoginFormData } from '@/api/auth'
import { onLoad } from '@dcloudio/uni-app'
import { onMounted, reactive, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import AuthAPI from '@/api/auth'
import { useUserStore } from '@/store/userStore'
definePage({
name: 'login',
style: {
navigationBarTitleText: '登录',
},
})
// 主题相关
const loginFormRef = ref()
const toast = useToast()
const loading = ref(false)
const userStore = useUserStore()
// 登录表单数据
const loginFormData = reactive<LoginFormData>({
username: 'admin',
password: '123456',
captcha: '',
captcha_key: '',
remember: true,
login_type: '移动端',
})
// 验证码状态
const captchaState = reactive<CaptchaInfo>({
enable: false,
key: '',
img_base: '',
})
// 防重复请求标志
const isCaptchaLoading = ref(false)
// 获取验证码
async function getLoginCaptcha() {
if (isCaptchaLoading.value) {
return
}
isCaptchaLoading.value = true
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 || result.img || ''
if (captchaState.enable) {
loginFormData.captcha = ''
loginFormData.captcha_key = captchaState.key
}
}
else {
captchaState.enable = false
}
}
catch (error: any) {
console.error('获取验证码失败:', error)
toast.error(error?.message || '验证码获取失败')
captchaState.img_base = ''
captchaState.enable = false
}
finally {
isCaptchaLoading.value = false
}
}
// 仅使用 query 作为重定向来源
const redirect = ref('/pages/index/index')
// 页面加载时初始化
onLoad((options) => {
uni.setNavigationBarTitle({ title: '登录' })
// 处理重定向逻辑
const fromQuery
= options && options.redirect ? decodeURIComponent(options.redirect) : ''
if (fromQuery && fromQuery !== '/pages/login/index') {
redirect.value = fromQuery
}
// 获取验证码
getLoginCaptcha()
})
// 页面挂载时确保验证码正确显示
onMounted(() => {
// 如果验证码未启用或未获取到,重新获取
setTimeout(() => {
if (!captchaState.enable || !captchaState.img_base) {
getLoginCaptcha()
}
// 强制清除输入框背景色,优化视觉效果
const inputs = document.querySelectorAll('input')
inputs.forEach((input) => {
input.style.backgroundColor = 'transparent'
input.style.boxShadow = 'none'
})
}, 100)
})
// 账号密码登录
async function handleSubmit() {
try {
const { valid, errors } = await loginFormRef.value.validate()
if (!valid)
return toast.error(errors[0].message)
loading.value = true
await userStore.login(loginFormData)
toast.success('登录成功')
// 登录成功后跳转到指定页面
setTimeout(() => {
uni.reLaunch({
url: redirect.value,
success: () => toast.success('跳转成功'),
fail: err => toast.error(`跳转失败: ${err.errMsg}`),
})
}, 500)
}
catch (error: any) {
toast.error(error?.message || '登录失败')
getLoginCaptcha() // 刷新验证码
}
finally {
loading.value = false
}
}
// 忘记密码
async function handleForgotPassword() {
toast.error('忘记密码功能开发中...')
}
// 注册账号
async function handleRegister() {
toast.error('注册账号功能开发中...')
}
</script>
<template>
<view class="app-container">
<!-- 背景图 -->
<wd-img
src="/static/images/login-bg.svg"
mode="aspectFill"
class="login-bg"
/>
<view class="login-card">
<!-- Logo和标题区域 -->
<view class="header">
<wd-img src="/static/logo.png" class="logo" mode="aspectFit" />
<text class="title theme-text-inverse">
FastApp管理系统
</text>
<text class="subtitle theme-text-inverse">
欢迎使用移动端管理平台
</text>
</view>
<!-- 登录表单区域 -->
<wd-form
ref="loginFormRef"
:model="loginFormData"
class="login-form"
>
<wd-cell-group border class="form-group">
<wd-input
v-model="loginFormData.username"
label="账号"
label-width="100px"
prefix-icon="user"
type="text"
prop="username"
placeholder="请输入账号"
clearable
center
clear-trigger="focus"
:rules="[{ required: true, message: '请输入账号' }]"
/>
<wd-input
v-model="loginFormData.password"
label="密码"
label-width="100px"
prefix-icon="lock-on"
type="text"
prop="password"
show-password
placeholder="请输入密码"
clear-trigger="focus"
clearable
center
:rules="[{ required: true, message: '请输入密码' }]"
/>
<!-- 验证码输入框 -->
<view v-if="captchaState.enable" class="captcha">
<wd-input
v-model="loginFormData.captcha"
label="验证码"
label-width="100px"
prefix-icon="lock-on"
class="captcha-input"
type="text"
prop="captcha"
placeholder="请输入验证码"
clear-trigger="focus"
clearable
center
:rules="[{ required: true, message: '请输入验证码' }]"
/>
<wd-img
:src="captchaState.img_base"
class="captcha-image"
mode="aspectFit"
@click="getLoginCaptcha"
/>
<view
v-if="!captchaState.img_base && captchaState.enable"
@click="getLoginCaptcha"
>
<text>点击加载</text>
</view>
</view>
<wd-divider />
<!-- 忘记密码和注册账号 -->
<view class="auth-links">
<!-- 记住我 -->
<view class="remember-me">
<wd-checkbox v-model="loginFormData.remember">
<wd-text size="24rpx" text="记住我" />
</wd-checkbox>
</view>
<view class="forgot-password">
<wd-text
type="primary"
text="忘记密码?"
size="24rpx"
@click="handleForgotPassword"
/>
</view>
</view>
<!-- 登录按钮 -->
<view class="footer">
<wd-button
type="primary"
size="large"
block
class="login-button"
:loading="loading"
loading-size="24"
@click="handleSubmit"
>
{{ loading ? "登录中..." : "账号登录" }}
</wd-button>
</view>
<view class="register-link">
<wd-text size="24rpx" text="您没有账号?" />
<wd-text
type="primary"
text="注册账号"
size="24rpx"
@click="handleRegister"
/>
</view>
<!-- 版权信息 -->
<view class="copyright">
<wd-text
text="Copyright © 2025-2026 service.fastapiadmin.com 版权所有 陕ICP备2025069493号-1"
size="20rpx"
/>
</view>
</wd-cell-group>
</wd-form>
</view>
</view>
</template>
<style lang="scss" scoped>
.app-container {
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
.login-bg {
position: absolute;
width: 100%;
height: 100%;
}
.login-card {
z-index: 2;
margin-top: 40rpx;
width: 92%;
.header {
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 30rpx;
.logo {
width: 250rpx;
height: 250rpx;
}
.title {
margin-bottom: 20rpx;
font-size: 48rpx;
font-weight: bold;
color: #ffffff;
}
.subtitle {
font-size: 28rpx;
color: #ffffff;
text-align: center;
}
}
.login-form {
margin-top: 40rpx;
padding: 24rpx;
.form-group {
padding: 24rpx;
}
.captcha {
display: flex;
align-items: center;
margin-bottom: 40rpx;
}
.captcha-input {
flex: 1;
min-width: 0;
}
.captcha-image {
width: 160rpx;
height: 60rpx;
}
.footer {
margin-top: 32rpx;
}
.auth-links {
display: flex;
justify-content: space-between;
margin-top: 32rpx;
}
.register-link {
display: flex;
justify-content: center;
align-items: center;
margin-top: 32rpx;
}
.copyright {
margin-top: 32rpx;
text-align: center;
}
}
}
}
</style>
+168
View File
@@ -0,0 +1,168 @@
<!-- layout: tabbar -->
<script lang="ts" setup>
definePage({
name: 'mine',
layout: 'tabbar',
style: {
navigationBarTitleText: '我的',
},
})
const router = useRouter()
const toast = useToast()
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const isLogin = computed(() => !!userInfo.value)
const defaultAvatar = '/static/images/default-avatar.png'
const isLoading = ref(false)
// 退出登录
function handleLogout() {
uni.showModal({
title: '提示',
content: '确认退出登录吗?',
success(res) {
if (res.confirm) {
userStore.logout()
toast.show('已退出登录')
}
},
})
}
// 页面跳转方法
function navigateTo(name: string) {
router.push({
name,
})
}
function openUrl(url: string) {
window.open(url, '_blank')
}
// 消息按钮点击处理
function handleMessagesClick(e?: any) {
if (e) {
e.stopPropagation()
e.preventDefault()
}
navigateToSection('messages')
}
// 导航到各个板块
function navigateToSection(section: string, subSection?: string, e?: any) {
if (e) {
e.stopPropagation()
e.preventDefault()
}
// 这里可以根据需要实现具体的导航逻辑
uni.showToast({
title: '功能开发中',
icon: 'none',
})
}
onShow(() => {
// 确保 tabbar 状态正确
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
if (currentPage.route === 'pages/mine/index') {
// 通过事件通知 tabbar 布局更新状态
uni.$emit('updateTabbar', 'mine')
}
}
// 每次显示页面时都检查并刷新用户信息
loadUserInfo()
})
// 加载用户信息
async function loadUserInfo() {
if (isLogin.value) {
isLoading.value = true
try {
await userStore.getInfo()
}
catch (error) {
console.error('获取用户信息失败', error)
}
finally {
isLoading.value = false
}
}
}
// 监听用户信息变化,确保数据及时更新
watch(
() => userInfo.value,
() => {},
{
deep: true,
immediate: true,
},
)
</script>
<template>
<view class="p-3">
<!-- 用户信息卡片 -->
<wd-card type="rectangle">
<wd-row class="flex items-center">
<wd-col :span="6">
<wd-badge v-if="true" model-value="99+" @click="handleMessagesClick">
<wd-avatar
size="medium"
shape="round"
:src="isLogin ? userInfo!.avatar : defaultAvatar"
/>
</wd-badge>
</wd-col>
<wd-col :span="16">
<view class="mt-2 flex flex-col">
<wd-text type="success" :text="userInfo?.name || '匿名用户'" size="16" bold />
<wd-text :text="`ID: ${userInfo?.username}`" size="12" />
</view>
</wd-col>
<wd-col :span="2">
<wd-icon name="setting" size="22" color="#0083ff" @click="navigateTo('settings')" />
</wd-col>
</wd-row>
</wd-card>
<!-- 常用工具 -->
<wd-card type="rectangle" title="常用工具">
<wd-grid clickable border :gutter="10" :column="4">
<wd-grid-item is-dot icon="user" text="用户管理" clickable @click="navigateTo('user')" />
<wd-grid-item is-dot icon="help-circle" text="岗位管理" clickable @click="navigateTo('position')" />
<wd-grid-item is-dot icon="check-circle" text="角色管理" clickable @click="navigateTo('role')" />
<wd-grid-item is-dot icon="info-circle" text="参数管理" clickable @click="navigateTo('parameter')" />
<wd-grid-item is-dot icon="user" text="字典管理" clickable @click="navigateTo('dictionary')" />
<wd-grid-item is-dot icon="help-circle" text="部门管理" clickable @click="navigateTo('department')" />
<wd-grid-item is-dot icon="check-circle" text="菜单管理" clickable @click="navigateTo('role')" />
<wd-grid-item is-dot icon="info-circle" text="日志管理" clickable @click="navigateTo('log')" />
</wd-grid>
</wd-card>
<!-- 推荐服务 -->
<wd-card type="rectangle" title="推荐服务">
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
<wd-cell title="项目官网" is-link @click="openUrl('https://service.fastapiadmin.com/')" />
<wd-cell title="问题反馈" is-link @click="navigateTo('feedback')" />
<wd-cell title="个人资料" is-link @click="navigateTo('profile')" />
<wd-cell title="账号设置" is-link @click="navigateTo('account')" />
</wd-cell-group>
</wd-card>
<!-- 退出登录按钮 -->
<wd-button v-if="isLogin" class="p-3" size="large" block @click="handleLogout">
退出登录
</wd-button>
</view>
</template>
<style lang="scss" scoped>
</style>
+54
View File
@@ -0,0 +1,54 @@
<script setup lang="ts">
definePage({
name: 'work',
layout: 'tabbar',
style: {
navigationBarTitleText: '工作台',
},
})
const router = useRouter()
// 页面跳转方法
function navigateTo(name: string) {
router.push({
name,
})
}
function openUrl(url: string) {
window.open(url, '_blank')
}
</script>
<template>
<view class="box-border py-3">
<view class="mx-3 box-border rounded-3 bg-white px-4 py-6 text-center dark:bg-[var(--wot-dark-background2)]">
<text class="mb-3 block text-left text-5 text-gray-800 font-bold dark:text-gray-200">
Wot Starter
</text>
<text class="mb-3 block text-left text-30rpx text-gray-600 leading-relaxed dark:text-gray-300">
⚡️ 基于 vitesse-uni-app 由 vite & uni-app 驱动的、深度整合 Wot UI 组件库的快速启动模板
</text>
<text class="block text-left text-3 text-gray-400 leading-relaxed dark:text-gray-400">
背靠 Uni Helper、Wot UI 团队,告别 HBuilderX ,拥抱现代前端开发工具链
</text>
</view>
<demo-block title="工具链介绍" transparent>
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
<wd-cell title="🧩 WotUI组件库" is-link @click="openUrl('https://wot-ui.cn/')" />
<wd-cell title="🧠 Agent Skills" is-link @click="navigateTo('skills')" />
<wd-cell title="🚦 Router 路由管理" is-link @click="navigateTo('router')" />
<wd-cell title="🌐 Alova 网络请求" is-link @click="navigateTo('request')" />
<wd-cell title="🎨 Icon 图标" is-link @click="navigateTo('icon')" />
<wd-cell title="✨ Unocss 原子化" is-link @click="navigateTo('styles')" />
<wd-cell title="🍍 Pinia 持久化" is-link @click="navigateTo('pinia')" />
<wd-cell title="🌱 CreateUni 脚手架" is-link @click="navigateTo('create-uni') " />
<wd-cell title="🔄 CI/CD 持续集成" is-link @click="navigateTo('ci')" />
<wd-cell title="🦾 uni-ku/root" is-link @click="navigateTo('root')" />
<wd-cell title="📊 uni-echarts" is-link @click="navigateTo('echarts')" />
</wd-cell-group>
</demo-block>
</view>
</template>