first commit
@@ -0,0 +1,19 @@
|
||||
# ===========================================
|
||||
# API配置 - 根据你的后端接口修改
|
||||
# ===========================================
|
||||
# 开发环境
|
||||
VUE_APP_API_BASE_URL=https://apigo.yunzer.cn
|
||||
VUE_APP_API_TIMEOUT=10000
|
||||
|
||||
# ===========================================
|
||||
# 应用配置
|
||||
# ===========================================
|
||||
VUE_APP_APP_NAME=企业办公移动应用
|
||||
VUE_APP_APP_VERSION=1.0.0
|
||||
VUE_APP_DEBUG=true
|
||||
|
||||
# ===========================================
|
||||
# 其他配置 (可选)
|
||||
# ===========================================
|
||||
# VUE_APP_UPLOAD_MAX_SIZE=10485760
|
||||
# VUE_APP_PAGE_SIZE=20
|
||||
@@ -0,0 +1,35 @@
|
||||
# 环境配置示例文件
|
||||
# 复制此文件为 .env 并修改相应的配置值
|
||||
|
||||
# ===========================================
|
||||
# 基础配置
|
||||
# ===========================================
|
||||
NODE_ENV=development
|
||||
|
||||
# ===========================================
|
||||
# API配置 - 根据你的后端接口修改
|
||||
# ===========================================
|
||||
# 开发环境
|
||||
VUE_APP_API_BASE_URL=https://dev-api.company.com
|
||||
VUE_APP_API_TIMEOUT=10000
|
||||
|
||||
# 测试环境 (注释掉上面的,取消注释下面的)
|
||||
# VUE_APP_API_BASE_URL=https://test-api.company.com
|
||||
# VUE_APP_API_TIMEOUT=10000
|
||||
|
||||
# 生产环境 (注释掉上面的,取消注释下面的)
|
||||
# VUE_APP_API_BASE_URL=https://api.company.com
|
||||
# VUE_APP_API_TIMEOUT=10000
|
||||
|
||||
# ===========================================
|
||||
# 应用配置
|
||||
# ===========================================
|
||||
VUE_APP_APP_NAME=企业办公移动应用
|
||||
VUE_APP_APP_VERSION=1.0.0
|
||||
VUE_APP_DEBUG=true
|
||||
|
||||
# ===========================================
|
||||
# 其他配置 (可选)
|
||||
# ===========================================
|
||||
# VUE_APP_UPLOAD_MAX_SIZE=10485760
|
||||
# VUE_APP_PAGE_SIZE=20
|
||||
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<view id="app">
|
||||
<!-- 全局组件可以在这里添加 -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAuthStore } from "./src/store/authStore.js";
|
||||
import { initRouteGuard } from "./src/utils/routeGuard.js";
|
||||
|
||||
export default {
|
||||
globalData: {
|
||||
isWarmStart: false,
|
||||
userInfo: null,
|
||||
appConfig: {},
|
||||
isMobile: false, // 是否为移动设备
|
||||
systemInfo: null, // 系统信息
|
||||
},
|
||||
onLaunch: function () {
|
||||
console.log("App启动");
|
||||
this.initApp();
|
||||
},
|
||||
onShow: function () {
|
||||
console.log("App显示");
|
||||
},
|
||||
onHide: function () {
|
||||
console.log("App隐藏");
|
||||
},
|
||||
methods: {
|
||||
initApp() {
|
||||
// 初始化应用
|
||||
this.detectDevice();
|
||||
this.initAuth();
|
||||
this.initRouteGuard();
|
||||
this.initTheme();
|
||||
},
|
||||
|
||||
initAuth() {
|
||||
// 初始化认证状态
|
||||
const authStore = useAuthStore();
|
||||
authStore.initAuth();
|
||||
},
|
||||
|
||||
initRouteGuard() {
|
||||
// 初始化路由守卫
|
||||
initRouteGuard();
|
||||
},
|
||||
|
||||
initTheme() {
|
||||
// 初始化主题
|
||||
// 这里可以设置全局主题
|
||||
},
|
||||
|
||||
detectDevice() {
|
||||
// 全局设备检测
|
||||
try {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
this.globalData.systemInfo = systemInfo;
|
||||
|
||||
// 判断是否为移动设备
|
||||
this.globalData.isMobile = systemInfo.platform !== 'devtools' &&
|
||||
(systemInfo.platform === 'ios' ||
|
||||
systemInfo.platform === 'android');
|
||||
|
||||
console.log('设备信息:', systemInfo);
|
||||
console.log('是否为移动设备:', this.globalData.isMobile);
|
||||
} catch (e) {
|
||||
// 备用检测方案
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
this.globalData.isMobile = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent);
|
||||
console.log('使用备用检测方案,是否为移动设备:', this.globalData.isMobile);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* 引入 FontAwesome */
|
||||
@import "./static/css/all.css";
|
||||
@import "./static/css/style.scss";
|
||||
@import "./static/css/iconfont.css";
|
||||
@import "./static/css/index.css";
|
||||
|
||||
/* 全局样式 */
|
||||
page {
|
||||
background-color: #f5f6fa;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
|
||||
"Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
/* 通用样式类 */
|
||||
.container {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx 40rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #2b7ce9 0%, #1e5f99 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f5f6fa;
|
||||
color: #666;
|
||||
border: 1rpx solid #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #2b7ce9;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #4ecdc4;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #feca57;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.mt-20 {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.mt-30 {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
.mt-40 {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.mb-10 {
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
.mb-20 {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.mb-30 {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
.mb-40 {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.ml-10 {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
.ml-20 {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
.ml-30 {
|
||||
margin-left: 30rpx;
|
||||
}
|
||||
|
||||
.mr-10 {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
.mr-20 {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.mr-30 {
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.p-10 {
|
||||
padding: 10rpx;
|
||||
}
|
||||
.p-20 {
|
||||
padding: 20rpx;
|
||||
}
|
||||
.p-30 {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.pt-10 {
|
||||
padding-top: 10rpx;
|
||||
}
|
||||
.pt-20 {
|
||||
padding-top: 20rpx;
|
||||
}
|
||||
.pt-30 {
|
||||
padding-top: 30rpx;
|
||||
}
|
||||
|
||||
.pb-10 {
|
||||
padding-bottom: 10rpx;
|
||||
}
|
||||
.pb-20 {
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
.pb-30 {
|
||||
padding-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.pl-10 {
|
||||
padding-left: 10rpx;
|
||||
}
|
||||
.pl-20 {
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
.pl-30 {
|
||||
padding-left: 30rpx;
|
||||
}
|
||||
|
||||
.pr-10 {
|
||||
padding-right: 10rpx;
|
||||
}
|
||||
.pr-20 {
|
||||
padding-right: 20rpx;
|
||||
}
|
||||
.pr-30 {
|
||||
padding-right: 30rpx;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 安全区域适配 */
|
||||
.safe-area-top {
|
||||
padding-top: constant(safe-area-inset-top);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
|
||||
.safe-area-bottom {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-in-right {
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media screen and (max-width: 750rpx) {
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
# 企业级办公App
|
||||
|
||||
一个基于UniApp开发的企业级办公移动应用,提供完整的办公功能解决方案。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 🏠 工作台
|
||||
- **数据概览**: 待审批数量、考勤状态、报销进度等关键数据展示
|
||||
- **快捷操作**: 自定义快捷功能入口,支持拖拽排序
|
||||
- **常用工具**: 计算器、通讯录、文件传输等辅助工具
|
||||
|
||||
### 💬 消息
|
||||
- **会话管理**: 支持单聊和群聊,消息预览和未读提醒
|
||||
- **通知中心**: 按类型分组的系统通知,支持分类查看
|
||||
- **消息搜索**: 全局搜索会话和通知内容
|
||||
|
||||
### ⚙️ 功能
|
||||
- **人事管理**: 考勤打卡、请假申请、加班申请、调休记录
|
||||
- **财务报销**: 报销提交、进度查询、历史记录
|
||||
- **工作协同**: 任务管理、会议预订、项目协作
|
||||
- **资源管理**: 客户目录、企业文件库、权限管理
|
||||
|
||||
### 👤 我的
|
||||
- **个人信息**: 头像、姓名、部门、工号等基础信息
|
||||
- **数据概览**: 考勤率、任务完成数、待报销金额
|
||||
- **账号安全**: 密码修改、手机绑定、账号注销
|
||||
- **APP设置**: 消息提醒、主题切换、缓存清理、版本更新
|
||||
- **帮助反馈**: 常见问题、意见反馈、客服联系
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 前端技术栈
|
||||
- **框架**: UniApp (Vue 3 + Vite)
|
||||
- **状态管理**: Pinia + 持久化插件
|
||||
- **UI组件**: uView UI 2.0
|
||||
- **样式**: SCSS + 响应式设计
|
||||
- **构建工具**: Vite
|
||||
|
||||
### 项目结构
|
||||
```
|
||||
mobile/
|
||||
├── pages/ # 页面文件
|
||||
│ ├── index/ # 工作台
|
||||
│ ├── message/ # 消息
|
||||
│ ├── function/ # 功能
|
||||
│ └── profile/ # 我的
|
||||
├── src/
|
||||
│ ├── store/ # 状态管理
|
||||
│ ├── components/ # 通用组件
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── api/ # API接口
|
||||
├── static/ # 静态资源
|
||||
│ └── tabbar/ # TabBar图标
|
||||
├── App.vue # 应用入口
|
||||
├── main.js # 主文件
|
||||
├── pages.json # 页面配置
|
||||
├── manifest.json # 应用配置
|
||||
└── vue.config.js # Vite配置
|
||||
```
|
||||
|
||||
## 核心设计理念
|
||||
|
||||
### 1. 用户体验优先
|
||||
- **3步内触达**: 确保用户3步内可触达任意功能
|
||||
- **个性化定制**: 支持快捷操作自定义和主题切换
|
||||
- **智能提醒**: 未读消息红点提示,避免信息遗漏
|
||||
|
||||
### 2. 功能模块化
|
||||
- **分类聚合**: 按业务场景分类,避免功能杂乱
|
||||
- **权限控制**: 基于角色的功能权限管理
|
||||
- **流程跟踪**: 申请流程实时进度跟踪
|
||||
|
||||
### 3. 数据驱动
|
||||
- **实时数据**: 工作数据实时更新和同步
|
||||
- **智能分析**: 使用频率统计,自动优化功能排序
|
||||
- **离线支持**: 关键数据本地缓存,支持离线使用
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 环境要求
|
||||
- Node.js >= 16.0.0
|
||||
- HBuilderX 或 VS Code + uni-app插件
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 开发运行
|
||||
```bash
|
||||
# H5端
|
||||
npm run dev:h5
|
||||
|
||||
# 微信小程序
|
||||
npm run dev:mp-weixin
|
||||
|
||||
# App端
|
||||
npm run dev:app
|
||||
```
|
||||
|
||||
### 构建发布
|
||||
```bash
|
||||
# 构建H5
|
||||
npm run build:h5
|
||||
|
||||
# 构建微信小程序
|
||||
npm run build:mp-weixin
|
||||
|
||||
# 构建App
|
||||
npm run build:app
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 1. API配置
|
||||
在 `src/api/index.js` 中修改 `BASE_URL` 为实际的后端API地址。
|
||||
|
||||
### 2. 权限配置
|
||||
在 `src/store/userStore.js` 中配置用户权限列表。
|
||||
|
||||
### 3. 主题配置
|
||||
在 `App.vue` 中修改全局样式变量。
|
||||
|
||||
### 4. TabBar图标
|
||||
将图标文件放置在 `static/tabbar/` 目录下,参考 `static/tabbar/README.md`。
|
||||
|
||||
## 功能扩展
|
||||
|
||||
### 添加新页面
|
||||
1. 在 `pages/` 目录下创建页面文件夹
|
||||
2. 在 `pages.json` 中注册页面路由
|
||||
3. 如需要TabBar,添加到 `tabBar.list` 配置中
|
||||
|
||||
### 添加新组件
|
||||
1. 在 `src/components/` 目录下创建组件文件
|
||||
2. 在需要使用的页面中导入组件
|
||||
3. 遵循Vue 3 Composition API规范
|
||||
|
||||
### 添加新API
|
||||
1. 在 `src/api/index.js` 中添加API方法
|
||||
2. 在页面中导入并使用API
|
||||
3. 处理请求和响应拦截器
|
||||
|
||||
## 部署说明
|
||||
|
||||
### 微信小程序
|
||||
1. 在微信公众平台注册小程序
|
||||
2. 配置 `manifest.json` 中的 `mp-weixin.appid`
|
||||
3. 使用微信开发者工具上传代码
|
||||
|
||||
### App端
|
||||
1. 配置 `manifest.json` 中的App信息
|
||||
2. 使用HBuilderX云打包或本地打包
|
||||
3. 生成安装包并分发
|
||||
|
||||
### H5端
|
||||
1. 构建完成后将 `dist/build/h5` 目录部署到Web服务器
|
||||
2. 配置域名和HTTPS证书
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题或建议,请联系开发团队。
|
||||
@@ -0,0 +1,224 @@
|
||||
# 启动画面功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
启动画面功能为应用提供了优雅的启动体验,包括品牌展示、加载动画和初始化过程。
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── config/
|
||||
│ └── splash.js # 启动画面配置文件
|
||||
├── utils/
|
||||
│ └── splashManager.js # 启动画面管理器
|
||||
pages/
|
||||
├── splash/
|
||||
│ └── splash.vue # 启动画面页面
|
||||
└── splash-test/
|
||||
└── splash-test.vue # 启动画面测试页面
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 智能启动控制
|
||||
- **冷启动**: 首次启动或应用被完全关闭后重新启动
|
||||
- **热启动**: 应用在后台被重新激活
|
||||
- **自动判断**: 系统自动判断启动类型,决定是否显示启动画面
|
||||
|
||||
### 2. 优雅的视觉效果
|
||||
- **渐变背景**: 使用紫色渐变背景,营造现代感
|
||||
- **浮动动画**: 背景圆形元素的浮动动画效果
|
||||
- **毛玻璃效果**: 使用 `backdrop-filter` 实现毛玻璃质感
|
||||
- **脉冲动画**: Logo图标的脉冲呼吸效果
|
||||
|
||||
### 3. 加载步骤管理
|
||||
- **分步加载**: 将启动过程分为多个步骤
|
||||
- **实时反馈**: 显示当前加载步骤和进度
|
||||
- **错误处理**: 单个步骤失败不影响整体启动流程
|
||||
|
||||
### 4. 可配置性
|
||||
- **显示时间**: 可配置最小和最大显示时间
|
||||
- **加载步骤**: 可自定义加载步骤和文本
|
||||
- **主题样式**: 可配置颜色、字体等视觉元素
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 基本配置
|
||||
|
||||
```javascript
|
||||
// src/config/splash.js
|
||||
export const splashConfig = {
|
||||
app: {
|
||||
name: '企业办公',
|
||||
nameEn: 'Enterprise Office',
|
||||
version: '1.0.0',
|
||||
logo: '🏢'
|
||||
},
|
||||
display: {
|
||||
minDuration: 2000, // 最小显示时间
|
||||
maxDuration: 5000, // 最大显示时间
|
||||
showOnWarmStart: false, // 热启动时是否显示
|
||||
showVersion: true // 是否显示版本信息
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 加载步骤配置
|
||||
|
||||
```javascript
|
||||
loadingSteps: [
|
||||
{
|
||||
text: '正在初始化...',
|
||||
duration: 800,
|
||||
action: 'init',
|
||||
icon: '⚙️'
|
||||
},
|
||||
{
|
||||
text: '加载用户数据...',
|
||||
duration: 1000,
|
||||
action: 'loadUserData',
|
||||
icon: '👤'
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 基本使用
|
||||
|
||||
启动画面会自动在应用启动时显示,无需额外配置。
|
||||
|
||||
### 2. 自定义配置
|
||||
|
||||
```javascript
|
||||
import { updateSplashConfig } from '@/src/config/splash.js'
|
||||
|
||||
// 更新配置
|
||||
updateSplashConfig({
|
||||
display: {
|
||||
minDuration: 3000,
|
||||
showOnWarmStart: true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 添加自定义加载步骤
|
||||
|
||||
```javascript
|
||||
import { updateSplashConfig } from '@/src/config/splash.js'
|
||||
|
||||
updateSplashConfig({
|
||||
loadingSteps: [
|
||||
// 现有步骤...
|
||||
{
|
||||
text: '同步云端数据...',
|
||||
duration: 1200,
|
||||
action: 'syncCloudData',
|
||||
icon: '☁️'
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### 4. 实现自定义加载逻辑
|
||||
|
||||
```javascript
|
||||
// src/utils/splashManager.js
|
||||
async function syncCloudData() {
|
||||
// 实现云端数据同步逻辑
|
||||
await syncUserData()
|
||||
await syncWorkData()
|
||||
console.log('云端数据同步完成')
|
||||
}
|
||||
```
|
||||
|
||||
## 测试功能
|
||||
|
||||
访问 `/pages/splash-test/splash-test` 页面可以:
|
||||
|
||||
- 测试启动画面效果
|
||||
- 重置启动状态
|
||||
- 模拟冷启动/热启动
|
||||
- 查看当前配置信息
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 1. 启动检测
|
||||
|
||||
```javascript
|
||||
// 检查是否应该显示启动画面
|
||||
export function shouldShowSplash() {
|
||||
const isColdStart = !getApp().globalData?.isWarmStart
|
||||
return isColdStart
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 步骤执行
|
||||
|
||||
```javascript
|
||||
// 执行加载步骤
|
||||
export async function executeLoadingStep(step) {
|
||||
try {
|
||||
switch (step.action) {
|
||||
case 'init':
|
||||
await initializeApp()
|
||||
break
|
||||
case 'loadUserData':
|
||||
await loadUserData()
|
||||
break
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`步骤 ${step.action} 执行失败:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 动画效果
|
||||
|
||||
```scss
|
||||
// 浮动动画
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) rotate(180deg);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
// 脉冲动画
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **性能优化**: 启动画面不应包含过重的计算或网络请求
|
||||
2. **错误处理**: 确保单个步骤失败不会影响整体启动流程
|
||||
3. **用户体验**: 避免启动画面显示时间过长
|
||||
4. **兼容性**: 确保在不同设备和平台上都能正常显示
|
||||
|
||||
## 扩展功能
|
||||
|
||||
### 1. 添加品牌动画
|
||||
可以在启动画面中添加更复杂的品牌动画效果。
|
||||
|
||||
### 2. 多语言支持
|
||||
根据用户语言设置显示不同的启动文本。
|
||||
|
||||
### 3. 主题切换
|
||||
根据用户设置显示不同主题的启动画面。
|
||||
|
||||
### 4. 网络检测
|
||||
根据网络状态调整加载步骤和显示内容。
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||
CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||
</script>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createSSRApp } from 'vue'
|
||||
// 引入 createPinia 方法(命名导出)
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
// FontAwesome CSS 将通过 App.vue 中的全局样式引入
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
// 创建 Pinia 实例
|
||||
const pinia = createPinia()
|
||||
// 注册 Pinia 持久化
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
// 注册 Pinia
|
||||
app.use(pinia)
|
||||
|
||||
// 暂时注释掉 uView,因为与 Vue 3 有兼容性问题
|
||||
// 后续可以使用 uView Plus 或其他 Vue 3 兼容的 UI 库
|
||||
// app.use(uView)
|
||||
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"name" : "mobile",
|
||||
"appid" : "__UNI__AD84D36",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules" : {},
|
||||
/* 应用发布信息 */
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
},
|
||||
/* H5特有相关 */
|
||||
"h5" : {
|
||||
"devServer" : {
|
||||
"disableHostCheck" : true
|
||||
}
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
"vueVersion" : "3"
|
||||
}
|
||||
@@ -0,0 +1,682 @@
|
||||
{
|
||||
"name": "enterprise-office-app",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "enterprise-office-app",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"autoprefixer": "^10.4.21",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"postcss": "^8.5.6",
|
||||
"uview-ui": "^2.0.38"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^4.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.4.tgz",
|
||||
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.4"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.4.tgz",
|
||||
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
|
||||
"integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@vue/shared": "3.5.22",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
|
||||
"integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
|
||||
"integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@vue/compiler-core": "3.5.22",
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/compiler-ssr": "3.5.22",
|
||||
"@vue/shared": "3.5.22",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.19",
|
||||
"postcss": "^8.5.6",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz",
|
||||
"integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
"version": "7.7.7",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
|
||||
"integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-kit": "^7.7.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-kit": {
|
||||
"version": "7.7.7",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
|
||||
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-shared": "^7.7.7",
|
||||
"birpc": "^2.3.0",
|
||||
"hookable": "^5.5.3",
|
||||
"mitt": "^3.0.1",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"speakingurl": "^14.0.1",
|
||||
"superjson": "^2.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-shared": {
|
||||
"version": "7.7.7",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
|
||||
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rfdc": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.22.tgz",
|
||||
"integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
|
||||
"integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
|
||||
"integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.22",
|
||||
"@vue/runtime-core": "3.5.22",
|
||||
"@vue/shared": "3.5.22",
|
||||
"csstype": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
|
||||
"integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.22.tgz",
|
||||
"integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.21",
|
||||
"resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
||||
"integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.24.4",
|
||||
"caniuse-lite": "^1.0.30001702",
|
||||
"fraction.js": "^4.3.7",
|
||||
"normalize-range": "^0.1.2",
|
||||
"picocolors": "^1.1.1",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"autoprefixer": "bin/autoprefixer"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.17",
|
||||
"resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.17.tgz",
|
||||
"integrity": "sha512-j5zJcx6golJYTG6c05LUZ3Z8Gi+M62zRT/ycz4Xq4iCOdpcxwg7ngEYD4KA0eWZC7U17qh/Smq8bYbACJ0ipBA==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/birpc": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.6.1.tgz",
|
||||
"integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.26.3",
|
||||
"resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.26.3.tgz",
|
||||
"integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.9",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
"electron-to-chromium": "^1.5.227",
|
||||
"node-releases": "^2.0.21",
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001751",
|
||||
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
|
||||
"integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/copy-anything": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz",
|
||||
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-what": "^4.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/deep-pick-omit": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/deep-pick-omit/-/deep-pick-omit-1.2.1.tgz",
|
||||
"integrity": "sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/defu": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz",
|
||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/destr": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz",
|
||||
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.237",
|
||||
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz",
|
||||
"integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"type": "patreon",
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/hookable": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
||||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-what": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-4.1.16.tgz",
|
||||
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.19",
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.19.tgz",
|
||||
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.25",
|
||||
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.25.tgz",
|
||||
"integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-range": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz",
|
||||
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.3.tgz",
|
||||
"integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^7.7.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.4.4",
|
||||
"vue": "^2.7.0 || ^3.5.11"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pinia-plugin-persistedstate": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.5.0.tgz",
|
||||
"integrity": "sha512-QTkP1xJVyCdr2I2p3AKUZM84/e+IS+HktRxKGAIuDzkyaKKV48mQcYkJFVVDuvTxlI5j6X3oZObpqoVB8JnWpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deep-pick-omit": "^1.2.1",
|
||||
"defu": "^6.1.4",
|
||||
"destr": "^2.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nuxt/kit": ">=3.0.0",
|
||||
"@pinia/nuxt": ">=0.10.0",
|
||||
"pinia": ">=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nuxt/kit": {
|
||||
"optional": true
|
||||
},
|
||||
"@pinia/nuxt": {
|
||||
"optional": true
|
||||
},
|
||||
"pinia": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-value-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
|
||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/speakingurl": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
|
||||
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/superjson": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.2.tgz",
|
||||
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"copy-anything": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.1.14.tgz",
|
||||
"integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escalade": "^3.2.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"update-browserslist-db": "cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uview-ui": {
|
||||
"version": "2.0.38",
|
||||
"resolved": "https://registry.npmmirror.com/uview-ui/-/uview-ui-2.0.38.tgz",
|
||||
"integrity": "sha512-6egHDf9lXHKpG3hEjRE0vMx4+VWwKk/ReTf5x18KrIKqdvdPRqO3+B8Unh7vYYwrIxzAWIlmhZ9RJpKI/4UqPQ==",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.22.tgz",
|
||||
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/compiler-sfc": "3.5.22",
|
||||
"@vue/runtime-dom": "3.5.22",
|
||||
"@vue/server-renderer": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "enterprise-office-app",
|
||||
"version": "1.0.0",
|
||||
"description": "企业级办公移动应用",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev:h5": "uni build --mode development --platform h5",
|
||||
"dev:mp-weixin": "uni build --mode development --platform mp-weixin",
|
||||
"dev:app": "uni build --mode development --platform app",
|
||||
"dev:test:h5": "NODE_ENV=test uni build --mode development --platform h5",
|
||||
"dev:test:mp-weixin": "NODE_ENV=test uni build --mode development --platform mp-weixin",
|
||||
"dev:test:app": "NODE_ENV=test uni build --mode development --platform app",
|
||||
"build:h5": "uni build --mode production --platform h5",
|
||||
"build:mp-weixin": "uni build --mode production --platform mp-weixin",
|
||||
"build:app": "uni build --mode production --platform app",
|
||||
"build:test:h5": "NODE_ENV=test uni build --mode production --platform h5",
|
||||
"build:test:mp-weixin": "NODE_ENV=test uni build --mode production --platform mp-weixin",
|
||||
"build:test:app": "NODE_ENV=test uni build --mode production --platform app"
|
||||
},
|
||||
"dependencies": {
|
||||
"autoprefixer": "^10.4.21",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"postcss": "^8.5.6",
|
||||
"uview-ui": "^2.0.38"
|
||||
},
|
||||
"keywords": [
|
||||
"uniapp",
|
||||
"vue3",
|
||||
"pinia",
|
||||
"enterprise",
|
||||
"office"
|
||||
],
|
||||
"author": "Your Name",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^4.1.14"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/splash/splash",
|
||||
"style": {
|
||||
"navigationBarTitleText": "启动页",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工作台",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/message/message",
|
||||
"style": {
|
||||
"navigationBarTitleText": "消息",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/message/chat",
|
||||
"style": {
|
||||
"navigationBarTitleText": "聊天",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/message/chatdetail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "聊天详情",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/message/userdetail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用户详情",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/hr/attendance/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "考勤",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/hr/attendance/statistics",
|
||||
"style": {
|
||||
"navigationBarTitleText": "考勤统计",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/function/function",
|
||||
"style": {
|
||||
"navigationBarTitleText": "功能",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/profile",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/editprofile",
|
||||
"style": {
|
||||
"navigationBarTitleText": "编辑个人资料",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/tasks/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "任务管理",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "企业办公",
|
||||
"navigationBarBackgroundColor": "#2B7CE9",
|
||||
"backgroundColor": "#F5F6FA"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#7A7E83",
|
||||
"selectedColor": "#2B7CE9",
|
||||
"borderStyle": "black",
|
||||
"backgroundColor": "#ffffff",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"iconPath": "static/tabbar/workbench.png",
|
||||
"selectedIconPath": "static/tabbar/workbench-active.png",
|
||||
"text": "工作台"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/message/message",
|
||||
"iconPath": "static/tabbar/message.png",
|
||||
"selectedIconPath": "static/tabbar/message-active.png",
|
||||
"text": "消息"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/function/function",
|
||||
"iconPath": "static/tabbar/function.png",
|
||||
"selectedIconPath": "static/tabbar/function-active.png",
|
||||
"text": "功能"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
"iconPath": "static/tabbar/profile.png",
|
||||
"selectedIconPath": "static/tabbar/profile-active.png",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
<template>
|
||||
<view class="function-page">
|
||||
<!-- 统一顶部导航 -->
|
||||
<view class="unified-header">
|
||||
<view class="header-content">
|
||||
<view class="header-left">
|
||||
<!-- <i class="fas fa-search header-icon" @click="handleSearch"></i> -->
|
||||
</view>
|
||||
<view class="header-title">功能</view>
|
||||
<view class="header-right">
|
||||
<!-- <i class="fas fa-bell header-icon" @click="handleNotification">
|
||||
<view class="badge" v-if="unreadCount > 0">{{ unreadCount }}</view>
|
||||
</i> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近使用 -->
|
||||
<view class="recent-section" v-if="recentFunctions.length > 0">
|
||||
<view class="section-title">最近使用</view>
|
||||
<view class="recent-functions">
|
||||
<view
|
||||
class="recent-item"
|
||||
v-for="(func, index) in recentFunctions"
|
||||
:key="index"
|
||||
@click="handleFunction(func)"
|
||||
>
|
||||
<view class="recent-icon">
|
||||
<i
|
||||
class="icon-text"
|
||||
:class="func.icon"
|
||||
:style="{ color: func.color }"
|
||||
></i>
|
||||
</view>
|
||||
<text class="recent-label">{{func.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类导航 -->
|
||||
<view class="category-tabs">
|
||||
<scroll-view scroll-x class="tabs-scroll">
|
||||
<view class="tabs">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeCategory === category.key }"
|
||||
v-for="category in categories"
|
||||
:key="category.key"
|
||||
@click="switchCategory(category.key)"
|
||||
>
|
||||
<text>{{category.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 功能列表 -->
|
||||
<scroll-view scroll-y class="unified-content">
|
||||
<view class="function-list">
|
||||
<view
|
||||
class="function-group"
|
||||
v-for="(group, groupIndex) in currentFunctions"
|
||||
:key="groupIndex"
|
||||
>
|
||||
<view class="group-title">{{group.title}}</view>
|
||||
<view class="group-functions">
|
||||
<view
|
||||
class="function-item"
|
||||
v-for="(func, funcIndex) in group.functions"
|
||||
:key="funcIndex"
|
||||
@click="handleFunction(func)"
|
||||
>
|
||||
<view class="function-icon">
|
||||
<i
|
||||
class="icon-text"
|
||||
:class="func.icon"
|
||||
:style="{ color: func.color }"
|
||||
></i>
|
||||
</view>
|
||||
<view class="function-content">
|
||||
<text class="function-name">{{func.name}}</text>
|
||||
<text class="function-desc">{{func.description}}</text>
|
||||
</view>
|
||||
<view class="function-arrow">
|
||||
<text class="arrow-icon">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
|
||||
// 已全部用FontAwesome图标,无需emoji变量
|
||||
// FontAwesome icon mapping:
|
||||
const FA = {
|
||||
clock: 'fas fa-clock',
|
||||
calendar: 'fas fa-calendar-alt',
|
||||
tasks: 'fas fa-tasks',
|
||||
sync: 'fas fa-sync-alt',
|
||||
money: 'fas fa-money-bill-wave',
|
||||
file: 'fas fa-file-alt',
|
||||
userFriends: 'fas fa-user-friends',
|
||||
folder: 'fas fa-folder-open'
|
||||
}
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const activeCategory = ref('hr')
|
||||
const unreadCount = ref(2)
|
||||
|
||||
// 分类数据
|
||||
const categories = reactive([
|
||||
{ key: 'hr', label: '人事管理' },
|
||||
{ key: 'finance', label: '财务报销' },
|
||||
{ key: 'work', label: '工作协同' },
|
||||
{ key: 'resource', label: '资源管理' }
|
||||
])
|
||||
|
||||
// 最近使用功能 (全部FontAwesome图标)
|
||||
const recentFunctions = reactive([
|
||||
{ icon: FA.calendar, color: '#FF6B6B', label: '请假', action: 'leave' },
|
||||
{ icon: FA.money, color: '#4ECDC4', label: '报销', action: 'reimbursement' },
|
||||
{ icon: FA.clock, color: '#45B7D1', label: '打卡', action: 'checkin' }
|
||||
])
|
||||
|
||||
// 功能数据 (全部FontAwesome图标)
|
||||
const functionData = reactive({
|
||||
hr: [
|
||||
{
|
||||
title: '考勤管理',
|
||||
functions: [
|
||||
{ name: '考勤打卡', description: '支持定位打卡、WiFi打卡', icon: FA.clock, color: '#45B7D1', action: 'checkin' },
|
||||
{ name: '请假申请', description: '选择类型、日期、理由,附附件', icon: FA.calendar, color: '#FF6B6B', action: 'leave' },
|
||||
{ name: '加班申请', description: '申请加班,记录加班时长', icon: FA.tasks, color: '#96CEB4', action: 'overtime' },
|
||||
{ name: '调休记录', description: '查看调休申请和记录', icon: FA.sync, color: '#FECA57', action: 'compensatory' }
|
||||
]
|
||||
}
|
||||
],
|
||||
finance: [
|
||||
{
|
||||
title: '报销管理',
|
||||
functions: [
|
||||
{ name: '报销提交', description: '选择报销类型、上传发票、填写金额', icon: FA.money, color: '#4ECDC4', action: 'reimbursement' },
|
||||
{ name: '报销进度', description: '查看报销申请进度', icon: FA.file, color: '#45B7D1', action: 'reimbursement-progress' },
|
||||
{ name: '报销历史', description: '查看历史报销记录', icon: FA.tasks, color: '#96CEB4', action: 'reimbursement-history' }
|
||||
]
|
||||
}
|
||||
],
|
||||
work: [
|
||||
{
|
||||
title: '任务管理',
|
||||
functions: [
|
||||
{ name: '待办任务', description: '关联项目、设置截止时间', icon: FA.tasks, color: '#FF6B6B', action: 'todo' },
|
||||
{ name: '已办任务', description: '查看已完成的任务', icon: FA.tasks, color: '#4ECDC4', action: 'completed' },
|
||||
{ name: '会议预订', description: '选择会议室、邀请参会人', icon: FA.calendar, color: '#45B7D1', action: 'meeting' }
|
||||
]
|
||||
}
|
||||
],
|
||||
resource: [
|
||||
{
|
||||
title: '资源管理',
|
||||
functions: [
|
||||
{ name: '客户目录', description: '按行业、区域分类,显示联系方式', icon: FA.userFriends, color: '#FECA57', action: 'customer' },
|
||||
{ name: '企业文件库', description: '按部门权限查看,支持在线预览', icon: FA.folder, color: '#96CEB4', action: 'files' }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 计算当前分类的功能
|
||||
const currentFunctions = computed(() => {
|
||||
return functionData[activeCategory.value] || []
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleSearch = () => {
|
||||
uni.showToast({
|
||||
title: '搜索功能'
|
||||
})
|
||||
}
|
||||
|
||||
const handleNotification = () => {
|
||||
uni.switchTab({
|
||||
url: '/pages/message/message'
|
||||
})
|
||||
}
|
||||
|
||||
const switchCategory = (category) => {
|
||||
activeCategory.value = category
|
||||
}
|
||||
|
||||
const handleFunction = (func) => {
|
||||
if (func.action === 'todo') {
|
||||
uni.navigateTo({
|
||||
url: '/pages/tasks/index'
|
||||
});
|
||||
return;
|
||||
}
|
||||
uni.showToast({
|
||||
title: `功能开发中...`
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
activeCategory,
|
||||
unreadCount,
|
||||
categories,
|
||||
recentFunctions,
|
||||
currentFunctions,
|
||||
handleSearch,
|
||||
handleNotification,
|
||||
switchCategory,
|
||||
handleFunction
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.function-page {
|
||||
height: 100vh;
|
||||
background-color: var(--background);
|
||||
position: relative;
|
||||
padding-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.function-page {
|
||||
padding-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 25rpx;
|
||||
padding: 15rpx 20rpx;
|
||||
margin-right: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 28rpx;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.notification {
|
||||
position: relative;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.recent-section {
|
||||
background: var(--white);
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.recent-functions {
|
||||
display: flex;
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.recent-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.recent-icon {
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.recent-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.category-tabs {
|
||||
background: var(--white);
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
.tabs-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 30rpx 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60rpx;
|
||||
height: 4rpx;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
height: calc(100vh - 300rpx);
|
||||
}
|
||||
|
||||
.function-list {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.function-group {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.group-functions {
|
||||
background: var(--white);
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.function-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
.function-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.function-icon {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.function-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.function-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.function-desc {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.function-arrow {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.search-icon, .notification-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 10rpx;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 0rpx;
|
||||
right: 15rpx;
|
||||
background-color: var(--error);
|
||||
color: var(--white);
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
border-radius: 50%;
|
||||
// min-width: 30rpx;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 32rpx;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,993 @@
|
||||
<template>
|
||||
<view class="statistics-container">
|
||||
<!-- 移动设备顶部状态栏 -->
|
||||
<view class="top_bar flex w-full" v-if="isMobile">
|
||||
<view class="chat-header">
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">考勤统计</view>
|
||||
<view class="header-right" @click="showMoreOptions">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 浏览器环境下的导航栏 -->
|
||||
<view class="chat-header" v-else>
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">考勤统计</view>
|
||||
<view class="header-right" @click="showMoreOptions">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<scroll-view scroll-y class="statistics-content">
|
||||
|
||||
<!-- 导出报表 -->
|
||||
<view class="export-card card">
|
||||
<view class="export-title">导出报表</view>
|
||||
<view class="exports">
|
||||
<view class="btn-export" @click="exportReport"> 导出 </view>
|
||||
<i class="fas fa-angle-right" style="font-size: 20rpx; color: var(--tab-inactive);position: relative;top: 4rpx;"></i>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计数据 -->
|
||||
<view class="stats-card card">
|
||||
<!-- Tabbar -->
|
||||
<view class="stat-tabs">
|
||||
<view
|
||||
v-for="t in tabs"
|
||||
:key="t.value"
|
||||
class="stat-tab"
|
||||
:class="{ active: statTab === t.value }"
|
||||
@click="statTab = t.value"
|
||||
>{{ t.label }}</view
|
||||
>
|
||||
</view>
|
||||
|
||||
<!-- 日统计(显示日历) -->
|
||||
<view v-if="statTab === 'day'" class="tab-content">
|
||||
<view class="calendar">
|
||||
<view class="cal-title">
|
||||
{{ today.getFullYear() }}年{{ today.getMonth() + 1 }}月
|
||||
</view>
|
||||
<view class="cal-week-head">
|
||||
<text
|
||||
v-for="w in ['日', '一', '二', '三', '四', '五', '六']"
|
||||
:key="w"
|
||||
>{{ w }}</text
|
||||
>
|
||||
</view>
|
||||
<view class="cal-body">
|
||||
<view
|
||||
v-for="(week, weekIndex) in calendarWeeks"
|
||||
:key="weekIndex"
|
||||
class="cal-row"
|
||||
>
|
||||
<view
|
||||
v-for="day in week"
|
||||
:key="day.key"
|
||||
class="cal-cell"
|
||||
:class="{
|
||||
empty: day.empty,
|
||||
selected:
|
||||
!day.empty && calendarSelected.includes(day.fullDate),
|
||||
}"
|
||||
@click="
|
||||
!day.empty &&
|
||||
selectDay({
|
||||
fullDate: day.fullDate,
|
||||
day: day.day,
|
||||
month: day.month,
|
||||
year: day.year,
|
||||
})
|
||||
"
|
||||
>{{ day.empty ? "" : day.day }}</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 选中日期的数据显示卡片 -->
|
||||
<view v-if="selectedDayInfo" class="day-data-card">
|
||||
<view class="day-data-header">
|
||||
<view class="day-data-title">
|
||||
<i class="fas fa-calendar-day"></i>
|
||||
<text>{{ selectedDayInfo.year }}年{{ selectedDayInfo.month }}月{{ selectedDayInfo.day }}日</text>
|
||||
</view>
|
||||
<view class="day-data-subtitle">考勤详情</view>
|
||||
</view>
|
||||
|
||||
<view class="day-data-content">
|
||||
<view
|
||||
class="day-data-item"
|
||||
v-for="(item, index) in dailyStats"
|
||||
:key="index"
|
||||
>
|
||||
<view class="day-data-icon" :class="item.status || 'default'">
|
||||
<i :class="item.icon"></i>
|
||||
</view>
|
||||
<view class="day-data-info">
|
||||
<view class="day-data-label">{{ item.title }}</view>
|
||||
<view class="day-data-value" :class="item.status || 'default'">
|
||||
{{ item.value }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 周统计 -->
|
||||
<view v-if="statTab === 'week'" class="tab-content">
|
||||
<view class="bar-tabs">
|
||||
<view
|
||||
class="bar-tab"
|
||||
v-for="(item, index) in weekTabs"
|
||||
:key="item.value"
|
||||
:class="{ active: weekTab === item.value }"
|
||||
@click="weekTab = item.value"
|
||||
>{{ item.label }}</view
|
||||
>
|
||||
</view>
|
||||
<view class="count-list">
|
||||
<view
|
||||
class="count-item"
|
||||
v-for="(item, idx) in (weekStats[weekTab] || [])"
|
||||
:key="idx"
|
||||
>
|
||||
<view class="c-value">{{ item.value }}</view>
|
||||
<view class="c-title">{{ item.title }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 月统计 -->
|
||||
<view v-if="statTab === 'month'" class="tab-content">
|
||||
<view class="month-grid">
|
||||
<view
|
||||
class="month-item"
|
||||
v-for="(item, index) in monthTabs"
|
||||
:key="item.value"
|
||||
:class="{ active: monthTab === item.value }"
|
||||
@click="monthTab = item.value"
|
||||
>
|
||||
<view class="month-label">{{ item.label }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="count-list">
|
||||
<view
|
||||
class="count-item"
|
||||
v-for="(item, idx) in (monthStats[monthTab] || [])"
|
||||
:key="idx"
|
||||
>
|
||||
<view class="c-value">{{ item.value }}</view>
|
||||
<view class="c-title">{{ item.title }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
const today = new Date();
|
||||
return {
|
||||
today: today,
|
||||
tabs: [
|
||||
{ label: "日统计", value: "day" },
|
||||
{ label: "周统计", value: "week" },
|
||||
{ label: "月统计", value: "month" },
|
||||
],
|
||||
statTab: "day",
|
||||
calendarSelected: [
|
||||
`${today.getFullYear()}-${String(today.getMonth() + 1).padStart(
|
||||
2,
|
||||
"0"
|
||||
)}-${String(today.getDate()).padStart(2, "0")}`,
|
||||
],
|
||||
weekTabs: [
|
||||
{ label: "第1周", value: "1" },
|
||||
{ label: "第2周", value: "2" },
|
||||
{ label: "第3周", value: "3" },
|
||||
{ label: "第4周", value: "4" },
|
||||
],
|
||||
weekTab: "1",
|
||||
monthTabs: Array.from({ length: 12 }, (_, i) => ({
|
||||
label: `${i + 1}月`,
|
||||
value: String(i + 1),
|
||||
})),
|
||||
monthTab: String(today.getMonth() + 1),
|
||||
baseStat: [
|
||||
{ title: "平均工时", value: "7.8h" },
|
||||
{ title: "迟到次数", value: "1" },
|
||||
{ title: "早退次数", value: "0" },
|
||||
{ title: "缺卡次数", value: "0" },
|
||||
{ title: "旷工次数", value: "0" },
|
||||
{ title: "外勤次数", value: "2" },
|
||||
{ title: "加班时长", value: "4h" },
|
||||
{ title: "调休时长", value: "2h" },
|
||||
],
|
||||
selectedDayData: null, // 选中日期的数据
|
||||
selectedDayInfo: null, // 选中日期的基本信息
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
weekStats() {
|
||||
try {
|
||||
return {
|
||||
"1": this.baseStat || [],
|
||||
"2": (this.baseStat || []).map((item, i) =>
|
||||
i === 1 ? { ...item, value: "0" } : item
|
||||
),
|
||||
"3": (this.baseStat || []).map((item, i) =>
|
||||
i === 3 ? { ...item, value: "1" } : item
|
||||
),
|
||||
"4": (this.baseStat || []).map((item, i) =>
|
||||
i === 5 ? { ...item, value: "3" } : item
|
||||
),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('周统计数据生成错误:', error);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
monthStats() {
|
||||
try {
|
||||
const stats = {};
|
||||
(this.monthTabs || []).forEach((_, i) => {
|
||||
stats[String(i + 1)] = (this.baseStat || []).map((item) => ({
|
||||
...item,
|
||||
value: String(
|
||||
Math.floor(Math.random() * 3) +
|
||||
(item.value.includes("h") ? "h" : "")
|
||||
),
|
||||
}));
|
||||
});
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('月统计数据生成错误:', error);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// 日历相关计算属性
|
||||
days() {
|
||||
const d = new Date(
|
||||
this.today.getFullYear(),
|
||||
this.today.getMonth() + 1,
|
||||
0
|
||||
).getDate();
|
||||
return Array.from({ length: d }, (_, i) => i + 1);
|
||||
},
|
||||
firstDay() {
|
||||
return new Date(
|
||||
this.today.getFullYear(),
|
||||
this.today.getMonth(),
|
||||
1
|
||||
).getDay();
|
||||
},
|
||||
calendarWeeks() {
|
||||
try {
|
||||
const year = this.today.getFullYear();
|
||||
const month = this.today.getMonth();
|
||||
const firstDay = new Date(year, month, 1).getDay();
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
|
||||
const weeks = [];
|
||||
let currentWeek = [];
|
||||
|
||||
// 添加空白日期(上个月的末尾几天)
|
||||
for (let i = 0; i < firstDay; i++) {
|
||||
currentWeek.push({
|
||||
key: `empty-${i}`,
|
||||
empty: true,
|
||||
day: "",
|
||||
fullDate: "",
|
||||
month: month,
|
||||
year: year,
|
||||
});
|
||||
}
|
||||
|
||||
// 添加当前月的日期
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const fullDate = `${year}-${String(month + 1).padStart(
|
||||
2,
|
||||
"0"
|
||||
)}-${String(day).padStart(2, "0")}`;
|
||||
currentWeek.push({
|
||||
key: `day-${day}`,
|
||||
empty: false,
|
||||
day: day,
|
||||
fullDate: fullDate,
|
||||
month: month + 1,
|
||||
year: year,
|
||||
});
|
||||
|
||||
// 如果一周满了(7天),开始新的一周
|
||||
if (currentWeek.length === 7) {
|
||||
weeks.push([...currentWeek]);
|
||||
currentWeek = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 如果最后一周不满7天,用空白日期填充
|
||||
while (currentWeek.length > 0 && currentWeek.length < 7) {
|
||||
currentWeek.push({
|
||||
key: `empty-end-${currentWeek.length}`,
|
||||
empty: true,
|
||||
day: "",
|
||||
fullDate: "",
|
||||
month: month,
|
||||
year: year,
|
||||
});
|
||||
}
|
||||
|
||||
// 如果还有未完成的周,添加到结果中
|
||||
if (currentWeek.length > 0) {
|
||||
weeks.push(currentWeek);
|
||||
}
|
||||
|
||||
return weeks;
|
||||
} catch (error) {
|
||||
console.error('日历生成错误:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
isMobile() {
|
||||
return getApp().globalData.isMobile;
|
||||
},
|
||||
// 生成每日数据
|
||||
dailyStats() {
|
||||
if (!this.selectedDayInfo) return [];
|
||||
|
||||
const { year, month, day } = this.selectedDayInfo;
|
||||
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
|
||||
// 模拟不同日期的不同数据
|
||||
const randomSeed = year * 10000 + month * 100 + day;
|
||||
const random = (seed) => {
|
||||
const x = Math.sin(seed) * 10000;
|
||||
return x - Math.floor(x);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
title: "上班时间",
|
||||
value: `${String(Math.floor(random(randomSeed) * 2) + 8).padStart(2, '0')}:${String(Math.floor(random(randomSeed + 1) * 60)).padStart(2, '0')}`,
|
||||
icon: "fas fa-clock"
|
||||
},
|
||||
{
|
||||
title: "下班时间",
|
||||
value: `${String(Math.floor(random(randomSeed + 2) * 2) + 17).padStart(2, '0')}:${String(Math.floor(random(randomSeed + 3) * 60)).padStart(2, '0')}`,
|
||||
icon: "fas fa-clock"
|
||||
},
|
||||
{
|
||||
title: "迟到状态",
|
||||
value: random(randomSeed + 5) > 0.8 ? "迟到" : "正常",
|
||||
icon: random(randomSeed + 5) > 0.8 ? "fas fa-exclamation-triangle" : "fas fa-check-circle",
|
||||
status: random(randomSeed + 5) > 0.8 ? "warning" : "success"
|
||||
},
|
||||
{
|
||||
title: "早退状态",
|
||||
value: random(randomSeed + 6) > 0.9 ? "早退" : "正常",
|
||||
icon: random(randomSeed + 6) > 0.9 ? "fas fa-exclamation-triangle" : "fas fa-check-circle",
|
||||
status: random(randomSeed + 6) > 0.9 ? "warning" : "success"
|
||||
},
|
||||
{
|
||||
title: "工作时长",
|
||||
value: `${(random(randomSeed + 4) * 4 + 6).toFixed(1)}h`,
|
||||
icon: "fas fa-hourglass-half"
|
||||
},
|
||||
{
|
||||
title: "外勤次数",
|
||||
value: Math.floor(random(randomSeed + 7) * 3).toString(),
|
||||
icon: "fas fa-map-marker-alt"
|
||||
},
|
||||
{
|
||||
title: "加班时长",
|
||||
value: random(randomSeed + 8) > 0.6 ? `${(random(randomSeed + 8) * 3).toFixed(1)}h` : "0h",
|
||||
icon: "fas fa-moon"
|
||||
},
|
||||
{
|
||||
title: "调休时长",
|
||||
value: random(randomSeed + 9) > 0.7 ? `${(random(randomSeed + 9) * 2).toFixed(1)}h` : "0h",
|
||||
icon: "fas fa-calendar-alt"
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
console.log('统计页面初始化');
|
||||
console.log('当前月份:', this.today.getMonth() + 1);
|
||||
console.log('基础数据:', this.baseStat);
|
||||
console.log('周标签:', this.weekTabs);
|
||||
console.log('月标签:', this.monthTabs);
|
||||
console.log('当前周标签值:', this.weekTab, typeof this.weekTab);
|
||||
console.log('当前月标签值:', this.monthTab, typeof this.monthTab);
|
||||
console.log('周统计数据:', this.weekStats);
|
||||
console.log('月统计数据:', this.monthStats);
|
||||
console.log('周统计键:', Object.keys(this.weekStats));
|
||||
console.log('月统计键:', Object.keys(this.monthStats));
|
||||
console.log('周统计访问测试:', this.weekStats[this.weekTab]);
|
||||
console.log('月统计访问测试:', this.monthStats[this.monthTab]);
|
||||
|
||||
// 初始化默认选中今天
|
||||
this.selectedDayInfo = {
|
||||
year: this.today.getFullYear(),
|
||||
month: this.today.getMonth() + 1,
|
||||
day: this.today.getDate(),
|
||||
fullDate: `${this.today.getFullYear()}-${String(this.today.getMonth() + 1).padStart(2, '0')}-${String(this.today.getDate()).padStart(2, '0')}`
|
||||
};
|
||||
console.log('默认选中今天:', this.selectedDayInfo);
|
||||
},
|
||||
methods: {
|
||||
selectDay(day) {
|
||||
this.calendarSelected = [day.fullDate];
|
||||
this.selectedDayInfo = {
|
||||
year: day.year,
|
||||
month: day.month,
|
||||
day: day.day,
|
||||
fullDate: day.fullDate
|
||||
};
|
||||
console.log('选择日期:', day);
|
||||
console.log('选中日期信息:', this.selectedDayInfo);
|
||||
},
|
||||
exportReport() {
|
||||
uni.showToast({ title: "导出功能暂未开放" });
|
||||
},
|
||||
goBack() {
|
||||
uni.navigateBack();
|
||||
},
|
||||
showMoreOptions() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['刷新数据', '导出报表', '设置'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
uni.showToast({ title: '刷新成功' });
|
||||
} else if (res.tapIndex === 1) {
|
||||
this.exportReport();
|
||||
} else if (res.tapIndex === 2) {
|
||||
uni.showToast({ title: '设置功能暂未开放' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 移动设备顶部状态栏 */
|
||||
.top_bar {
|
||||
background: var(--gradient-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding-top: var(--status-bar-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.top_bar {
|
||||
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.top_bar + .statistics-content {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
background-color: var(--surface);
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* 移动设备下的导航栏样式 */
|
||||
.top_bar .chat-header {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.top_bar .header-title {
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.top_bar .header-left i,
|
||||
.top_bar .header-right i {
|
||||
color: var(--white);
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
width: 80rpx;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* 移动设备下的按钮悬停效果 */
|
||||
.top_bar .header-left:active,
|
||||
.top_bar .header-right:active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 36rpx;
|
||||
color: var(--text-color);
|
||||
padding: 10rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.header-icon:active {
|
||||
background: var(--hover-bg);
|
||||
transform: scale(0.95);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
/* 移动设备下为内容添加顶部间距 */
|
||||
.top_bar + .statistics-content {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
/* 浏览器环境下为内容添加顶部间距 */
|
||||
.chat-header + .statistics-content {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
.statistics-container {
|
||||
background: #f7f8fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.statistics-content {
|
||||
padding: 32rpx 0;
|
||||
min-height: calc(100vh - 88rpx);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 4rpx 16rpx 0 rgba(0, 0, 0, 0.04);
|
||||
margin: 0 32rpx 32rpx;
|
||||
padding: 32rpx;
|
||||
}
|
||||
.export-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.export-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-export {
|
||||
// background: #3c9cff;
|
||||
color: var(--tab-inactive);
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
// padding: 4rpx 40rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.exports {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.btn-export {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
.stats-card {
|
||||
.stat-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 32rpx;
|
||||
.stat-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-muted);
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 4rpx solid transparent;
|
||||
transition: all 0.2s;
|
||||
&.active {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
.tab-content {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
// 月统计网格布局
|
||||
.month-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.month-item {
|
||||
background: var(--gray-lighter);
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 12rpx;
|
||||
text-align: center;
|
||||
border: 1rpx solid var(--border-light);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
.month-label {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2rpx);
|
||||
|
||||
.month-label {
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active):active {
|
||||
background: var(--primary-light);
|
||||
transform: scale(0.98);
|
||||
|
||||
.month-label {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar-tabs {
|
||||
display: flex;
|
||||
background: var(--gray-lighter);
|
||||
border-radius: 12rpx;
|
||||
padding: 6rpx;
|
||||
margin-bottom: 24rpx;
|
||||
position: relative;
|
||||
|
||||
.bar-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
padding: 16rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
margin: 0 2rpx;
|
||||
background: transparent;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
font-weight: 500;
|
||||
|
||||
&.active {
|
||||
color: var(--primary-color);
|
||||
background: var(--surface);
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1rpx);
|
||||
}
|
||||
|
||||
&:not(.active):active {
|
||||
background: rgba(60, 156, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar-tabs-scroll {
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
padding: 6rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.bar-scroll {
|
||||
white-space: nowrap;
|
||||
|
||||
.bar-tab {
|
||||
display: inline-block;
|
||||
min-width: 120rpx;
|
||||
margin: 0 4rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
border-radius: 8rpx;
|
||||
background: transparent;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
|
||||
&.active {
|
||||
color: #3c9cff;
|
||||
background: #fff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1rpx);
|
||||
}
|
||||
|
||||
&:not(.active):active {
|
||||
background: rgba(60, 156, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 日统计数据显示卡片
|
||||
.day-data-card {
|
||||
margin-top: 24rpx;
|
||||
background: var(--surface);
|
||||
border-radius: 16rpx;
|
||||
box-shadow: var(--shadow-md);
|
||||
overflow: hidden;
|
||||
border: 1rpx solid var(--border);
|
||||
|
||||
.day-data-header {
|
||||
background: var(--gradient-primary);
|
||||
padding: 32rpx 24rpx;
|
||||
color: var(--white);
|
||||
|
||||
.day-data-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8rpx;
|
||||
color: var(--white);
|
||||
|
||||
i {
|
||||
margin-right: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.day-data-subtitle {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.9;
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.day-data-content {
|
||||
padding: 24rpx;
|
||||
background: var(--surface);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
.day-data-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 16rpx;
|
||||
width: 48%;
|
||||
margin-bottom: 16rpx;
|
||||
background: var(--gray-lighter);
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid var(--border-light);
|
||||
box-sizing: border-box;
|
||||
|
||||
&:nth-child(odd) {
|
||||
margin-right: 2%;
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
margin-left: 2%;
|
||||
}
|
||||
|
||||
.day-data-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
i {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
&.default {
|
||||
background: var(--gray-light);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
&.success {
|
||||
background: var(--success-light);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: var(--warning-light);
|
||||
color: var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
.day-data-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.day-data-label {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 6rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.day-data-value {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
|
||||
&.default {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: var(--warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.count-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16rpx;
|
||||
justify-content: space-between;
|
||||
.count-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 48%;
|
||||
margin-bottom: 16rpx;
|
||||
padding: 24rpx;
|
||||
background: var(--gray-lighter);
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
border: 1rpx solid var(--border-light);
|
||||
.c-title {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.c-value {
|
||||
color: var(--text-color);
|
||||
font-size: 40rpx;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 日历样式 */
|
||||
.calendar {
|
||||
background: var(--surface);
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-top: 16rpx;
|
||||
border: 1rpx solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
.cal-title {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 32rpx;
|
||||
color: var(--title-color);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.cal-week-head {
|
||||
display: flex;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
text {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cal-body {
|
||||
.cal-row {
|
||||
display: flex;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.cal-cell {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
text-align: center;
|
||||
border-radius: 8rpx;
|
||||
margin: 0 4rpx;
|
||||
background: var(--gray-lighter);
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color);
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--primary-color);
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
&.empty {
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:not(.empty):not(.selected):hover {
|
||||
background: var(--primary-light);
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep() {
|
||||
.uni-scroll-view {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,927 @@
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<!-- 背景装饰 -->
|
||||
<view class="bg-decoration">
|
||||
<view class="gradient-orb orb-1"></view>
|
||||
<view class="gradient-orb orb-2"></view>
|
||||
<view class="gradient-orb orb-3"></view>
|
||||
<view class="floating-shapes">
|
||||
<view class="shape shape-1"></view>
|
||||
<view class="shape shape-2"></view>
|
||||
<view class="shape shape-3"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<view class="login-container">
|
||||
<!-- 头部区域 -->
|
||||
<view class="header-section">
|
||||
<view class="logo-container">
|
||||
<view class="logo-wrapper">
|
||||
<image src="/static/logo.png" class="logo" mode="aspectFit"></image>
|
||||
<view class="logo-ring"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="welcome-content">
|
||||
<text class="app-title">企业办公系统</text>
|
||||
<text class="welcome-subtitle">欢迎回来,开始您的工作之旅</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录卡片 -->
|
||||
<view class="login-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">登录账户</text>
|
||||
<view class="card-subtitle">请输入您的登录信息</view>
|
||||
</view>
|
||||
|
||||
<view class="form-container">
|
||||
<!-- 用户名输入 -->
|
||||
<view class="input-field-group">
|
||||
<view class="input-container" :class="{ 'focused': usernameFocused, 'error': usernameError }">
|
||||
<view class="input-icon-wrapper">
|
||||
<i class="fas fa-user input-icon"></i>
|
||||
</view>
|
||||
<input
|
||||
v-model="form.username"
|
||||
placeholder="用户名"
|
||||
class="input"
|
||||
type="text"
|
||||
@focus="handleUsernameFocus"
|
||||
@blur="handleUsernameBlur"
|
||||
@input="clearUsernameError">
|
||||
<view class="input-border"></view>
|
||||
</view>
|
||||
<text class="error-text" v-if="usernameError">{{ usernameError }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 密码输入 -->
|
||||
<view class="input-field-group">
|
||||
<view class="input-container" :class="{ 'focused': passwordFocused, 'error': passwordError }">
|
||||
<view class="input-icon-wrapper">
|
||||
<i class="fas fa-lock input-icon"></i>
|
||||
</view>
|
||||
<input
|
||||
v-model="form.password"
|
||||
placeholder="密码"
|
||||
class="input"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
@focus="handlePasswordFocus"
|
||||
@blur="handlePasswordBlur"
|
||||
@input="clearPasswordError">
|
||||
<view class="password-toggle" @click="togglePassword">
|
||||
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</view>
|
||||
<view class="input-border"></view>
|
||||
</view>
|
||||
<text class="error-text" v-if="passwordError">{{ passwordError }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 选项区域 -->
|
||||
<view class="options-row">
|
||||
<view class="remember-section" @click="toggleRemember">
|
||||
<view class="custom-checkbox" :class="{ 'checked': rememberMe }">
|
||||
<i class="fas fa-check" v-if="rememberMe"></i>
|
||||
</view>
|
||||
<text class="remember-text">记住我</text>
|
||||
</view>
|
||||
<text class="forgot-link" @click="handleForgotPassword">忘记密码?</text>
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button
|
||||
:disabled="loading || !isFormValid"
|
||||
class="login-button"
|
||||
:class="{ 'loading': loading, 'disabled': !isFormValid }"
|
||||
@click="handleLogin">
|
||||
<view class="button-content">
|
||||
<view class="button-icon" v-if="!loading">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</view>
|
||||
<view class="loading-spinner" v-if="loading">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</view>
|
||||
<text class="button-text">{{ loading ? '登录中...' : '立即登录' }}</text>
|
||||
</view>
|
||||
<view class="button-shine" v-if="!loading"></view>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 测试提示 -->
|
||||
<view class="test-tips">
|
||||
<view class="tips-icon">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</view>
|
||||
<text class="tips-text">测试账号:test / 123456</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { userApi } from '../../src/api/index.js'
|
||||
import { useAuthStore } from '../../src/store/authStore.js'
|
||||
import { redirectAfterLogin } from '../../src/utils/routeGuard.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
loading: false,
|
||||
usernameFocused: false,
|
||||
passwordFocused: false,
|
||||
showPassword: false,
|
||||
rememberMe: false,
|
||||
usernameError: '',
|
||||
passwordError: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isFormValid() {
|
||||
return this.form.username.trim() && this.form.password.trim()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleLogin() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await userApi.login(this.form);
|
||||
console.log('登录响应:', res);
|
||||
|
||||
// 根据你的后端返回结构调整
|
||||
const token = res.accessToken || res.token;
|
||||
|
||||
if (!token) {
|
||||
throw new Error('登录失败:未获取到访问令牌');
|
||||
}
|
||||
|
||||
const authStore = useAuthStore();
|
||||
// 从响应中获取用户信息
|
||||
const userInfo = res.user || {
|
||||
username: this.form.username,
|
||||
id: res.id
|
||||
};
|
||||
|
||||
authStore.login(userInfo, token);
|
||||
|
||||
uni.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 延迟跳转,让用户看到成功提示
|
||||
setTimeout(() => {
|
||||
redirectAfterLogin();
|
||||
}, 500);
|
||||
} catch (e) {
|
||||
this.loading = false;
|
||||
console.error('登录错误:', e);
|
||||
|
||||
// 显示错误信息
|
||||
const errorMessage = e.message || e.msg || '登录失败,请检查网络连接';
|
||||
uni.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// async handleLogin2() {
|
||||
// if(!this.form.username || !this.form.password) {
|
||||
// uni.showToast({
|
||||
// title: '请输入用户名和密码',
|
||||
// icon: 'none'
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this.loading = true;
|
||||
|
||||
// try {
|
||||
// // 模拟登录API请求
|
||||
// // 实际项目中应该调用真实的后端API
|
||||
// const mockLogin = () => {
|
||||
// return new Promise((resolve) => {
|
||||
// setTimeout(() => {
|
||||
// // 模拟登录成功
|
||||
// if (this.form.username === 'admin' && this.form.password === '123456') {
|
||||
// resolve({
|
||||
// code: 0,
|
||||
// message: '登录成功',
|
||||
// data: {
|
||||
// token: 'mock_token_' + Date.now(),
|
||||
// userInfo: {
|
||||
// id: 1,
|
||||
// username: this.form.username,
|
||||
// name: '管理员',
|
||||
// avatar: '/static/logo.png',
|
||||
// department: '技术部',
|
||||
// role: 'admin'
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// } else {
|
||||
// resolve({
|
||||
// code: 1,
|
||||
// message: '用户名或密码错误'
|
||||
// });
|
||||
// }
|
||||
// }, 1000);
|
||||
// });
|
||||
// };
|
||||
|
||||
// const result = await mockLogin();
|
||||
|
||||
// if (result.code === 0) {
|
||||
// // 使用 Pinia store 管理登录状态
|
||||
// const authStore = useAuthStore();
|
||||
// authStore.login(result.data.userInfo, result.data.token);
|
||||
|
||||
// uni.showToast({
|
||||
// title: '登录成功',
|
||||
// icon: 'success'
|
||||
// });
|
||||
|
||||
// // 延迟跳转,让用户看到成功提示
|
||||
// setTimeout(() => {
|
||||
// redirectAfterLogin();
|
||||
// }, 500);
|
||||
// } else {
|
||||
// uni.showToast({
|
||||
// title: result.message || '登录失败',
|
||||
// icon: 'none'
|
||||
// });
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('登录错误:', error);
|
||||
// uni.showToast({
|
||||
// title: '网络异常,请稍后重试',
|
||||
// icon: 'none'
|
||||
// });
|
||||
// }
|
||||
|
||||
// this.loading = false;
|
||||
// },
|
||||
|
||||
// 切换密码显示
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword
|
||||
},
|
||||
|
||||
// 切换记住我
|
||||
toggleRemember() {
|
||||
this.rememberMe = !this.rememberMe
|
||||
},
|
||||
|
||||
// 忘记密码
|
||||
handleForgotPassword() {
|
||||
uni.showToast({
|
||||
title: '请联系管理员重置密码',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
|
||||
// 处理用户名输入框焦点
|
||||
handleUsernameFocus() {
|
||||
this.usernameFocused = true;
|
||||
this.clearUsernameError();
|
||||
},
|
||||
|
||||
handleUsernameBlur() {
|
||||
this.usernameFocused = false;
|
||||
this.validateUsername();
|
||||
},
|
||||
|
||||
// 处理密码输入框焦点
|
||||
handlePasswordFocus() {
|
||||
this.passwordFocused = true;
|
||||
this.clearPasswordError();
|
||||
},
|
||||
|
||||
handlePasswordBlur() {
|
||||
this.passwordFocused = false;
|
||||
this.validatePassword();
|
||||
},
|
||||
|
||||
// 清除用户名错误
|
||||
clearUsernameError() {
|
||||
this.usernameError = '';
|
||||
},
|
||||
|
||||
// 清除密码错误
|
||||
clearPasswordError() {
|
||||
this.passwordError = '';
|
||||
},
|
||||
|
||||
// 验证用户名
|
||||
validateUsername() {
|
||||
if (!this.form.username.trim()) {
|
||||
this.usernameError = '请输入用户名';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// 验证密码
|
||||
validatePassword() {
|
||||
if (!this.form.password.trim()) {
|
||||
this.passwordError = '请输入密码';
|
||||
return false;
|
||||
}
|
||||
if (this.form.password.length < 6) {
|
||||
this.passwordError = '密码至少6位';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 主容器 */
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: var(--gradient-primary);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx 20rpx;
|
||||
}
|
||||
|
||||
/* 背景装饰 */
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.gradient-orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(45deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
|
||||
animation: float 8s ease-in-out infinite;
|
||||
filter: blur(1rpx);
|
||||
}
|
||||
|
||||
.orb-1 {
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
top: -150rpx;
|
||||
left: -150rpx;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.orb-2 {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
top: 20%;
|
||||
right: -100rpx;
|
||||
animation-delay: 3s;
|
||||
}
|
||||
|
||||
.orb-3 {
|
||||
width: 150rpx;
|
||||
height: 150rpx;
|
||||
bottom: 10%;
|
||||
left: 10%;
|
||||
animation-delay: 6s;
|
||||
}
|
||||
|
||||
.floating-shapes {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shape-1 {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
top: 30%;
|
||||
left: 20%;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.shape-2 {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 8rpx;
|
||||
top: 60%;
|
||||
right: 30%;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
.shape-3 {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
bottom: 30%;
|
||||
right: 20%;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px) rotate(0deg) scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-30px) rotate(180deg) scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录容器 */
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 600rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 头部区域 */
|
||||
.header-section {
|
||||
text-align: center;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.logo-ring {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
left: -8rpx;
|
||||
right: -8rpx;
|
||||
bottom: -8rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 28rpx;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
display: block;
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16rpx;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
opacity: 0.9;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 登录卡片 */
|
||||
.login-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 24rpx;
|
||||
padding: 50rpx 40rpx;
|
||||
box-shadow: var(--shadow-lg);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4rpx;
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 表单容器 */
|
||||
.form-container {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
/* 输入框组 */
|
||||
.input-field-group {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--gray-lighter);
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid var(--border);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
min-height: 88rpx;
|
||||
}
|
||||
|
||||
.input-container.focused {
|
||||
border-color: var(--primary-color);
|
||||
background: var(--white);
|
||||
box-shadow: 0 0 0 4rpx var(--info-light);
|
||||
transform: translateY(-2rpx);
|
||||
}
|
||||
|
||||
.input-container.error {
|
||||
border-color: var(--error);
|
||||
background: var(--error-light);
|
||||
}
|
||||
|
||||
.input-icon-wrapper {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
color: var(--text-muted);
|
||||
font-size: 28rpx;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.input-container.focused .input-icon {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.input-container.error .input-icon {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
padding: 24rpx 20rpx;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 30rpx;
|
||||
color: var(--text-color);
|
||||
outline: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 28rpx;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.input-border {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2rpx;
|
||||
background: var(--gradient-primary);
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.input-container.focused .input-border {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 24rpx;
|
||||
color: var(--error);
|
||||
margin-top: 8rpx;
|
||||
margin-left: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 选项区域 */
|
||||
.options-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.remember-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-checkbox {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border: 2rpx solid var(--border);
|
||||
border-radius: 8rpx;
|
||||
margin-right: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.custom-checkbox.checked {
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.custom-checkbox i {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.remember-text {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
font-size: 28rpx;
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.forgot-link:hover {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* 登录按钮 */
|
||||
.login-button {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: var(--gradient-primary);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
border-radius: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-lg);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-button:hover:not(:disabled) {
|
||||
transform: translateY(-2rpx);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.login-button:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.login-button.disabled {
|
||||
opacity: 0.5;
|
||||
transform: none;
|
||||
cursor: not-allowed;
|
||||
background: var(--gray);
|
||||
color: var(--text-muted);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.login-button.loading {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.button-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.button-shine {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
animation: shine 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
/* 测试提示 */
|
||||
.test-tips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
margin-top: 30rpx;
|
||||
padding: 20rpx;
|
||||
background: var(--info-light);
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid var(--primary-light);
|
||||
}
|
||||
|
||||
.tips-icon {
|
||||
color: var(--primary-color);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.tips-text {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media screen and (max-width: 750rpx) {
|
||||
.login-container {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding: 40rpx 30rpx;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 42rpx;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色模式适配 - 保持亮色主题 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.login-page {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.input-container {
|
||||
background: var(--gray-lighter);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.input-container.focused {
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.input {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.remember-text {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.test-tips {
|
||||
background: var(--info-light);
|
||||
border-color: var(--primary-light);
|
||||
}
|
||||
|
||||
.tips-text {
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,850 @@
|
||||
<template>
|
||||
<view class="chat-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="top_bar flex w-full" v-if="isMobile">
|
||||
<view class="chat-header">
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">聊天</view>
|
||||
<view class="header-right" @click="goToChatDetail">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 浏览器环境下的导航栏 -->
|
||||
<view class="chat-header" v-else>
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">聊天</view>
|
||||
<view class="header-right" @click="goToChatDetail">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天消息列表 -->
|
||||
<scroll-view
|
||||
class="message-list"
|
||||
scroll-y
|
||||
:scroll-top="scrollTop"
|
||||
:scroll-into-view="scrollIntoView"
|
||||
>
|
||||
<block v-for="(message, index) in messages" :key="index">
|
||||
<view
|
||||
:id="'message-' + index"
|
||||
:class="[
|
||||
'message-item',
|
||||
message.type === 'sent' ? 'sent' : 'received',
|
||||
]"
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<view class="avatar" @click="goToUserDetail">
|
||||
<image
|
||||
:src="
|
||||
message.type === 'sent'
|
||||
? '/static/imgs/default_avatar.png'
|
||||
: '/static/imgs/default_avatar.png'
|
||||
"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<view class="message-content">
|
||||
<view class="message-bubble">
|
||||
<!-- 文本消息 -->
|
||||
<text class="message-text">{{
|
||||
parseEmoji(message.content)
|
||||
}}</text>
|
||||
</view>
|
||||
<!-- 消息时间 -->
|
||||
<view class="message-time" v-if="message.time">
|
||||
{{ formatTime(message.time) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<view class="input-area">
|
||||
<!-- 表情选择器 -->
|
||||
<view class="emoji-picker-container" v-if="showEmojiPicker">
|
||||
<EmojiPicker
|
||||
:visible="showEmojiPicker"
|
||||
@select="onEmojiSelect"
|
||||
@close="showEmojiPicker = false"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 输入工具栏 -->
|
||||
<view class="input-toolbar">
|
||||
<!-- 左侧切换按钮 -->
|
||||
<view class="left-switch">
|
||||
<view
|
||||
class="switch-btn"
|
||||
@click="switchInputMode"
|
||||
:class="{ active: inputMode === 'voice' }"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
inputMode === 'voice' ? 'fas fa-keyboard' : 'fas fa-microphone'
|
||||
"
|
||||
></i>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 中间输入区域 -->
|
||||
<view class="input-wrapper">
|
||||
<!-- 语音模式:按住说话按钮 -->
|
||||
<view
|
||||
v-if="inputMode === 'voice'"
|
||||
class="voice-input"
|
||||
:class="{ recording: isRecording }"
|
||||
@touchstart="startVoiceRecord"
|
||||
@touchend="endVoiceRecord"
|
||||
@touchcancel="cancelVoiceRecord"
|
||||
>
|
||||
<text class="voice-text">{{
|
||||
isRecording ? "松开结束" : "按住说话"
|
||||
}}</text>
|
||||
<view v-if="isRecording" class="recording-indicator">
|
||||
<view class="recording-dot"></view>
|
||||
<view class="recording-dot"></view>
|
||||
<view class="recording-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 文本模式:输入框 -->
|
||||
<view v-else class="text-input-container">
|
||||
<textarea
|
||||
v-model="inputMessage"
|
||||
placeholder="输入消息..."
|
||||
class="message-input"
|
||||
:style="{ height: inputHeight + 'rpx' }"
|
||||
:maxlength="500"
|
||||
@confirm="sendMessage"
|
||||
@focus="onInputFocus"
|
||||
@blur="onInputBlur"
|
||||
@input="onInputChange"
|
||||
@linechange="onLineChange"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧操作区 -->
|
||||
<view class="right-actions">
|
||||
<!-- 文本模式:表情按钮 -->
|
||||
<view
|
||||
v-if="inputMode === 'text'"
|
||||
class="emoji-btn"
|
||||
@click="toggleEmojiPicker"
|
||||
:class="{ active: showEmojiPicker }"
|
||||
>
|
||||
<i class="fas fa-smile"></i>
|
||||
</view>
|
||||
|
||||
<!-- 文本模式:发送按钮(有内容时显示) -->
|
||||
<view
|
||||
v-if="inputMode === 'text' && inputMessage.trim()"
|
||||
class="send-btn"
|
||||
@click="sendMessage"
|
||||
>
|
||||
发送
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EmojiPicker from "../../src/components/EmojiPicker.vue";
|
||||
import { parseEmoji } from "../../src/utils/emojiParser.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmojiPicker,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
type: "received",
|
||||
content: "欢迎使用聊天功能!有什么可以帮您?",
|
||||
time: new Date(Date.now() - 3000 * 60 * 1000), // 30分钟前
|
||||
},
|
||||
{
|
||||
type: "received",
|
||||
content: "您可以咨询产品信息、下单流程等相关问题。",
|
||||
time: new Date(Date.now() - 29 * 60 * 1000), // 29分钟前
|
||||
},
|
||||
{
|
||||
type: "sent",
|
||||
content: "请问你们有哪些热门产品?",
|
||||
time: new Date(Date.now() - 28 * 60 * 1000), // 28分钟前
|
||||
},
|
||||
{
|
||||
type: "received",
|
||||
content: "我们的热门产品有A、B、C三款,您想了解哪一款?",
|
||||
time: new Date(Date.now() - 27 * 60 * 1000), // 27分钟前
|
||||
},
|
||||
{
|
||||
type: "received",
|
||||
content: "点击发送按钮或按回车键发送消息哦~",
|
||||
time: new Date(Date.now() - 26 * 60 * 1000), // 26分钟前
|
||||
},
|
||||
],
|
||||
inputMessage: "",
|
||||
showEmojiPicker: false,
|
||||
inputMode: "text", // 'text' 或 'voice'
|
||||
isRecording: false,
|
||||
recordingTimer: null,
|
||||
inputHeight: 80, // 输入框高度,默认1行
|
||||
scrollTop: 0, // 消息列表滚动位置
|
||||
scrollIntoView: "", // 滚动到指定元素
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// 确保输入框初始高度为1行
|
||||
this.inputHeight = 80;
|
||||
},
|
||||
computed: {
|
||||
// 从全局数据获取设备信息
|
||||
isMobile() {
|
||||
return getApp().globalData.isMobile;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听消息数组变化,自动滚动到最新消息
|
||||
messages: {
|
||||
handler(newMessages, oldMessages) {
|
||||
if (newMessages.length > (oldMessages ? oldMessages.length : 0)) {
|
||||
// 有新消息时,延迟滚动确保DOM更新完成
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom();
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
immediate: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
parseEmoji(text) {
|
||||
// 使用工具函数解析emoji
|
||||
return parseEmoji(text);
|
||||
},
|
||||
sendMessage() {
|
||||
if (this.inputMessage.trim() !== "") {
|
||||
// 添加用户发送的消息
|
||||
const newMessage = {
|
||||
type: "sent",
|
||||
content: this.inputMessage,
|
||||
time: new Date(),
|
||||
};
|
||||
this.messages.push(newMessage);
|
||||
|
||||
// 强制更新视图
|
||||
this.$forceUpdate();
|
||||
|
||||
// 清空输入框并重置高度
|
||||
const message = this.inputMessage;
|
||||
this.inputMessage = "";
|
||||
this.inputHeight = 80; // 重置为1行高度
|
||||
this.showEmojiPicker = false;
|
||||
|
||||
// 立即滚动到最新消息
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
|
||||
// 模拟回复
|
||||
setTimeout(() => {
|
||||
this.simulateReply(message);
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
simulateReply(message) {
|
||||
// 简单的回复逻辑
|
||||
let reply = "感谢你的消息!";
|
||||
if (message.includes("你好")) {
|
||||
reply = "你好!很高兴见到你!";
|
||||
} else if (message.includes("产品")) {
|
||||
reply = "我们的产品非常棒,你可以查看我们的官网了解更多信息。";
|
||||
}
|
||||
|
||||
const replyMessage = {
|
||||
type: "received",
|
||||
content: reply,
|
||||
time: new Date(),
|
||||
};
|
||||
|
||||
this.messages.push(replyMessage);
|
||||
|
||||
// 强制更新视图
|
||||
this.$forceUpdate();
|
||||
|
||||
// 立即滚动到最新消息
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
},
|
||||
toggleEmojiPicker() {
|
||||
this.showEmojiPicker = !this.showEmojiPicker;
|
||||
},
|
||||
onEmojiSelect(emoji) {
|
||||
this.inputMessage += emoji.unicode;
|
||||
},
|
||||
onInputFocus() {
|
||||
this.showEmojiPicker = false;
|
||||
},
|
||||
onInputBlur() {
|
||||
// 输入框失焦时的处理
|
||||
},
|
||||
onInputChange(e) {
|
||||
// 输入内容变化时的处理
|
||||
this.inputMessage = e.detail.value;
|
||||
|
||||
// 根据内容长度估算行数并调整高度
|
||||
this.adjustInputHeight();
|
||||
},
|
||||
onLineChange(e) {
|
||||
// 行数变化时调整高度,限制最大10行
|
||||
const lineCount = e.detail.lineCount;
|
||||
const minHeight = 80; // 1行高度
|
||||
const lineHeight = 40; // 每行高度
|
||||
const maxLines = 10; // 最大10行
|
||||
|
||||
let newHeight = minHeight + (lineCount - 1) * lineHeight;
|
||||
newHeight = Math.min(
|
||||
Math.max(newHeight, minHeight),
|
||||
minHeight + (maxLines - 1) * lineHeight
|
||||
);
|
||||
|
||||
this.inputHeight = newHeight;
|
||||
},
|
||||
adjustInputHeight() {
|
||||
// 根据输入内容调整高度
|
||||
const content = this.inputMessage;
|
||||
if (!content.trim()) {
|
||||
// 如果内容为空,设置为1行高度
|
||||
this.inputHeight = 80;
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = content.split("\n").length;
|
||||
const minHeight = 80;
|
||||
const lineHeight = 40;
|
||||
const maxLines = 10;
|
||||
|
||||
let newHeight = minHeight + (lines - 1) * lineHeight;
|
||||
newHeight = Math.min(
|
||||
Math.max(newHeight, minHeight),
|
||||
minHeight + (maxLines - 1) * lineHeight
|
||||
);
|
||||
|
||||
this.inputHeight = newHeight;
|
||||
},
|
||||
// 切换输入模式
|
||||
switchInputMode() {
|
||||
if (this.inputMode === "text") {
|
||||
this.inputMode = "voice";
|
||||
this.showEmojiPicker = false;
|
||||
} else {
|
||||
this.inputMode = "text";
|
||||
}
|
||||
},
|
||||
// 切换更多选项
|
||||
toggleMoreOptions() {
|
||||
// 这里可以添加更多选项的弹窗
|
||||
},
|
||||
// 语音录制相关
|
||||
startVoiceRecord() {
|
||||
this.isRecording = true;
|
||||
// 这里可以添加实际的录音逻辑
|
||||
// 例如调用 uni.getRecorderManager()
|
||||
},
|
||||
endVoiceRecord() {
|
||||
this.isRecording = false;
|
||||
// 这里可以添加录音结束的处理逻辑
|
||||
// 例如发送语音消息
|
||||
},
|
||||
cancelVoiceRecord() {
|
||||
this.isRecording = false;
|
||||
// 这里可以添加取消录音的处理逻辑
|
||||
},
|
||||
scrollToBottom() {
|
||||
// 滚动到消息列表底部
|
||||
// 方法1:使用scroll-top
|
||||
this.scrollTop = 99999;
|
||||
|
||||
// 方法2:使用scroll-into-view滚动到最后一个消息
|
||||
if (this.messages.length > 0) {
|
||||
const lastIndex = this.messages.length - 1;
|
||||
this.scrollIntoView = "message-" + lastIndex;
|
||||
|
||||
// 重置scrollIntoView以允许重复滚动
|
||||
setTimeout(() => {
|
||||
this.scrollIntoView = "";
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 返回
|
||||
*/
|
||||
goBack() {
|
||||
uni.navigateBack();
|
||||
},
|
||||
/**
|
||||
* 跳转到聊天详情
|
||||
*/
|
||||
goToChatDetail() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/message/chatdetail",
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 跳转到用户详情
|
||||
*/
|
||||
goToUserDetail() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/message/userdetail",
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 格式化时间
|
||||
*/
|
||||
formatTime(time) {
|
||||
const date = new Date(time);
|
||||
const now = new Date();
|
||||
|
||||
// 如果是今天,只显示时间
|
||||
if (date.toDateString() === now.toDateString()) {
|
||||
return date.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
// 如果是昨天,显示"昨天"
|
||||
const yesterday = new Date(now);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
if (date.toDateString() === yesterday.toDateString()) {
|
||||
return (
|
||||
"昨天 " +
|
||||
date.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 其他情况显示完整日期
|
||||
return (
|
||||
date.toLocaleDateString("zh-CN") +
|
||||
" " +
|
||||
date.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
/* 移动设备顶部状态栏 */
|
||||
.top_bar {
|
||||
background: var(--gradient-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding-top: var(--status-bar-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.top_bar {
|
||||
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.top_bar + .message-list {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
background-color: var(--surface);
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* 移动设备下的导航栏样式 */
|
||||
.top_bar .chat-header {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.top_bar .header-title {
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.top_bar .header-left i,
|
||||
.top_bar .header-right i {
|
||||
color: var(--white);
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
width: 80rpx;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.header-right:active {
|
||||
background-color: var(--surface-hover);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
/* 移动设备下的按钮悬停效果 */
|
||||
.top_bar .header-left:active,
|
||||
.top_bar .header-right:active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 36rpx;
|
||||
font-style: normal;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 消息列表 */
|
||||
.message-list {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
overflow-y: auto;
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
/* 移动设备下为消息列表添加顶部间距 */
|
||||
.top_bar + .message-list {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.message-item.sent {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
/* 头像 */
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
|
||||
.avatar image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 消息内容 */
|
||||
.message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.message-item.sent .message-content {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.message-item.received .message-content {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* 消息气泡 */
|
||||
.message-bubble {
|
||||
position: relative;
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.4;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.message-item.sent .message-bubble {
|
||||
background: var(--gradient-primary);
|
||||
color: var(--white);
|
||||
border-radius: 12rpx 2rpx 12rpx 12rpx;
|
||||
}
|
||||
|
||||
.message-item.received .message-bubble {
|
||||
background-color: var(--surface);
|
||||
color: var(--text-color);
|
||||
border-radius: 2rpx 12rpx 12rpx 12rpx;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* 消息时间 */
|
||||
.message-time {
|
||||
font-size: 20rpx;
|
||||
color: var(--text-muted);
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
/* 输入区域 */
|
||||
.input-area {
|
||||
background-color: var(--surface);
|
||||
border-top: 1rpx solid var(--border);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* 表情选择器容器 */
|
||||
.emoji-picker-container {
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
/* 输入工具栏 */
|
||||
.input-toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 20rpx 16rpx;
|
||||
gap: 16rpx;
|
||||
min-height: 100rpx;
|
||||
}
|
||||
|
||||
/* 左侧切换按钮 */
|
||||
.left-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.switch-btn.active {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
.switch-btn i {
|
||||
font-size: 40rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.switch-btn.active i {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
/* 中间输入区域 */
|
||||
.input-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 文本输入容器 */
|
||||
.text-input-container {
|
||||
background-color: var(--surface);
|
||||
border-radius: 8rpx;
|
||||
border: 1rpx solid var(--border);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.message-input {
|
||||
width: 100%;
|
||||
height: 80rpx; /* 固定默认高度为1行 */
|
||||
min-height: 80rpx;
|
||||
max-height: 440rpx; /* 10行高度:80rpx + 9 * 40rpx = 440rpx */
|
||||
padding: 20rpx 24rpx;
|
||||
border: none;
|
||||
font-size: 32rpx;
|
||||
line-height: 1.4;
|
||||
background-color: transparent;
|
||||
box-sizing: border-box;
|
||||
resize: none;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
overflow-y: auto; /* 超出10行时显示滚动条 */
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 语音输入 */
|
||||
.voice-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--gray-lighter);
|
||||
border-radius: 8rpx;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
border: 1rpx solid var(--border);
|
||||
}
|
||||
|
||||
.voice-input:active,
|
||||
.voice-input.recording {
|
||||
background-color: var(--gray);
|
||||
}
|
||||
|
||||
.voice-text {
|
||||
font-size: 32rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.recording-indicator {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.recording-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
background-color: #ff4444;
|
||||
border-radius: 50%;
|
||||
animation: recordingPulse 1s infinite;
|
||||
}
|
||||
|
||||
.recording-dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.recording-dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes recordingPulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧操作区 */
|
||||
.right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.emoji-btn {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.emoji-btn.active {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
.emoji-btn:active {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.emoji-btn i {
|
||||
font-size: 40rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.emoji-btn.active i {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 16rpx 32rpx;
|
||||
background: var(--gradient-primary);
|
||||
color: var(--white);
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 120rpx;
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.send-btn:active {
|
||||
background: var(--primary-dark);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,475 @@
|
||||
<template>
|
||||
<view class="chat-detail-container">
|
||||
<!-- 移动设备顶部状态栏 -->
|
||||
<view class="top_bar flex w-full" v-if="isMobile">
|
||||
<view class="chat-header">
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">聊天详情</view>
|
||||
<view class="header-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 浏览器环境顶部导航栏 -->
|
||||
<view class="chat-header" v-else>
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">聊天详情</view>
|
||||
<view class="header-right"></view>
|
||||
</view>
|
||||
<!-- 顶部用户信息卡片 -->
|
||||
<view class="user-card">
|
||||
<view class="user-info">
|
||||
<view class="avatar-container">
|
||||
<image
|
||||
class="avatar"
|
||||
src="/static/imgs/default_avatar.png"
|
||||
mode="aspectFill"
|
||||
@error="onAvatarError"
|
||||
/>
|
||||
</view>
|
||||
<view class="user-base-info">
|
||||
<view class="nickname">{{ user.nickname }}</view>
|
||||
<view class="meteid">账号:{{ user.meteid }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能操作卡片 -->
|
||||
<view class="action-card">
|
||||
<view class="card-header">
|
||||
<i class="fas fa-search"></i>
|
||||
<text>聊天功能</text>
|
||||
</view>
|
||||
<view class="action-list">
|
||||
<view class="action-item" @click="goToSearchRecord">
|
||||
<view class="action-icon">
|
||||
<i class="fas fa-search"></i>
|
||||
</view>
|
||||
<text>查找聊天记录</text>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天设置卡片 -->
|
||||
<view class="setting-card">
|
||||
<view class="card-header">
|
||||
<i class="fas fa-cog"></i>
|
||||
<text>聊天设置</text>
|
||||
</view>
|
||||
<view class="setting-list">
|
||||
<view class="setting-item">
|
||||
<view class="setting-icon">
|
||||
<i class="fas fa-bell-slash"></i>
|
||||
</view>
|
||||
<text>消息免打扰</text>
|
||||
<switch :checked="disturb" @change="switchDisturb" />
|
||||
</view>
|
||||
<view class="setting-item">
|
||||
<view class="setting-icon">
|
||||
<i class="fas fa-thumbtack"></i>
|
||||
</view>
|
||||
<text>置顶聊天</text>
|
||||
<switch :checked="pinned" @change="switchPinned" />
|
||||
</view>
|
||||
<view class="setting-item" @click="chooseChatBg">
|
||||
<view class="setting-icon">
|
||||
<i class="fas fa-image"></i>
|
||||
</view>
|
||||
<text>设置当前聊天背景</text>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 危险操作卡片 -->
|
||||
<view class="danger-card">
|
||||
<view class="card-header">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<text>危险操作</text>
|
||||
</view>
|
||||
<view class="danger-list">
|
||||
<view class="danger-item" @click="clearRecord">
|
||||
<view class="danger-icon">
|
||||
<i class="fas fa-trash"></i>
|
||||
</view>
|
||||
<text>清空聊天记录</text>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
<view class="danger-item" @click="complain">
|
||||
<view class="danger-icon">
|
||||
<i class="fas fa-flag"></i>
|
||||
</view>
|
||||
<text>投诉</text>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
user: {
|
||||
avatar: "",
|
||||
nickname: "美天科技用户",
|
||||
meteid: "mete_123456",
|
||||
},
|
||||
disturb: false,
|
||||
pinned: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 从全局数据获取设备信息
|
||||
isMobile() {
|
||||
return getApp().globalData.isMobile;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack();
|
||||
},
|
||||
onAvatarError() {
|
||||
// 头像加载失败时的处理
|
||||
console.log('头像加载失败');
|
||||
},
|
||||
goToSearchRecord() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/message/chatsearch?userid=" + this.user.meteid,
|
||||
});
|
||||
},
|
||||
switchDisturb(e) {
|
||||
this.disturb = e.detail.value;
|
||||
uni.showToast({
|
||||
title: this.disturb ? "已开启免打扰" : "已关闭免打扰",
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
switchPinned(e) {
|
||||
this.pinned = e.detail.value;
|
||||
uni.showToast({
|
||||
title: this.pinned ? "已置顶聊天" : "已取消置顶",
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
remindOnce() {
|
||||
uni.showToast({
|
||||
title: "已为你开启消息提醒一次",
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
chooseChatBg() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
const bgPath = res.tempFilePaths[0];
|
||||
uni.setStorageSync("chat_bg_" + this.user.meteid, bgPath);
|
||||
uni.showToast({ title: "聊天背景已设置", icon: "none" });
|
||||
},
|
||||
});
|
||||
},
|
||||
clearRecord() {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要清空与该用户的聊天记录吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 这里可以调用API或清空本地数据
|
||||
uni.showToast({ title: "聊天记录已清空", icon: "none" });
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
complain() {
|
||||
uni.showModal({
|
||||
title: "投诉",
|
||||
content: "如发现对方有违规行为,请进行投诉,我们将协助核实处理。",
|
||||
confirmText: "投诉",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({
|
||||
title: "已投诉,平台将进行核查",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-detail-container {
|
||||
background: var(--background);
|
||||
min-height: 100vh;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
/* 移动设备顶部状态栏 */
|
||||
.top_bar {
|
||||
background: var(--gradient-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding-top: var(--status-bar-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.top_bar {
|
||||
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.top_bar + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.chat-detail-container .chat-header:not(.top_bar .chat-header) {
|
||||
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.chat-detail-container .chat-header:not(.top_bar .chat-header) + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
background-color: var(--surface);
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* 浏览器环境下的固定定位 */
|
||||
.chat-detail-container .chat-header:not(.top_bar .chat-header) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
padding-top: var(--status-bar-height);
|
||||
}
|
||||
|
||||
/* 移动设备下的导航栏样式 */
|
||||
.top_bar .chat-header {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.top_bar .header-title {
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.top_bar .header-left i,
|
||||
.top_bar .header-right i {
|
||||
color: var(--white);
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
width: 80rpx;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.header-left:active,
|
||||
.header-right:active {
|
||||
background-color: var(--gray-lighter);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.top_bar .header-left:active,
|
||||
.top_bar .header-right:active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
/* 移动设备下为内容添加顶部间距 */
|
||||
.top_bar + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
/* 浏览器环境下为内容添加顶部间距 */
|
||||
.chat-detail-container .chat-header:not(.top_bar .chat-header) + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
/* 用户信息卡片 */
|
||||
.user-card {
|
||||
background: var(--surface);
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1rpx solid var(--border-light);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40rpx 30rpx;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 16rpx;
|
||||
background: var(--gray);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.user-base-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.meteid {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 卡片通用样式 */
|
||||
.action-card, .setting-card, .danger-card {
|
||||
background: var(--surface);
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1rpx solid var(--border-light);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx 30rpx 20rpx 30rpx;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
background: var(--gray-lighter);
|
||||
}
|
||||
|
||||
.card-header i {
|
||||
font-size: 28rpx;
|
||||
color: var(--icon-color);
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.card-header text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
/* 操作列表 */
|
||||
.action-list, .setting-list, .danger-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.action-item, .setting-item, .danger-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.action-item:last-child, .setting-item:last-child, .danger-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.action-item:active, .setting-item:active, .danger-item:active {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
/* 图标样式 */
|
||||
.action-icon, .setting-icon, .danger-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
.setting-icon {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
.danger-icon {
|
||||
background: var(--gradient-error);
|
||||
}
|
||||
|
||||
.action-icon i, .setting-icon i, .danger-icon i {
|
||||
color: var(--white);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* 文字样式 */
|
||||
.action-item text, .setting-item text, .danger-item text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.danger-item text {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.action-item i.fas.fa-chevron-right, .setting-item i.fas.fa-chevron-right, .danger-item i.fas.fa-chevron-right {
|
||||
color: var(--text-muted);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,683 @@
|
||||
<template>
|
||||
<view class="message-page">
|
||||
<!-- 统一顶部导航 -->
|
||||
<view class="unified-header">
|
||||
<view class="header-content">
|
||||
<view class="header-left">
|
||||
<!-- <i class="fas fa-search header-icon" @click="handleSearch"></i> -->
|
||||
</view>
|
||||
<view class="header-title">消息</view>
|
||||
<view class="header-right">
|
||||
<!-- <i class="fas fa-bell header-icon" @click="handleNotification">
|
||||
<view class="badge" v-if="unreadCount > 0">{{ unreadCount }}</view>
|
||||
</i> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类标签 -->
|
||||
<view class="tabs-container">
|
||||
<view class="tabs">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'chat' }"
|
||||
@click="switchTab('chat')"
|
||||
>
|
||||
<text>会话</text>
|
||||
<view class="tab-badge" v-if="chatUnreadCount > 0">{{ chatUnreadCount }}</view>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'notification' }"
|
||||
@click="switchTab('notification')"
|
||||
>
|
||||
<text>通知</text>
|
||||
<view class="tab-badge" v-if="notificationUnreadCount > 0">{{ notificationUnreadCount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<scroll-view scroll-y class="unified-content" @click="closeAllSwipes">
|
||||
<!-- 会话列表 -->
|
||||
<view v-if="activeTab === 'chat'" class="chat-list">
|
||||
<view
|
||||
class="chat-item"
|
||||
v-for="(chat, index) in chatList"
|
||||
:key="index"
|
||||
>
|
||||
<!-- 左侧点击区域 -->
|
||||
<view class="chat-left" @click="openChat(chat)">
|
||||
<view class="chat-avatar">
|
||||
<view class="avatar">
|
||||
<i class="fas fa-user avatar-icon"></i>
|
||||
</view>
|
||||
<view class="chat-badge" v-if="chat.unread > 0">{{ chat.unread }}</view>
|
||||
</view>
|
||||
<view class="chat-content">
|
||||
<view class="chat-header">
|
||||
<text class="chat-name">{{chat.name}}</text>
|
||||
<text class="chat-time">{{chat.time}}</text>
|
||||
</view>
|
||||
<view class="chat-preview">
|
||||
<text class="chat-message">{{chat.lastMessage}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧操作区域 -->
|
||||
<view class="chat-right">
|
||||
<view class="more-btn" @click="showActionMenu(chat, index)">
|
||||
<i class="fas fa-ellipsis-v more-icon"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 通知列表 -->
|
||||
<view v-if="activeTab === 'notification'" class="notification-list">
|
||||
<view
|
||||
class="notification-item"
|
||||
v-for="(notification, index) in notificationList"
|
||||
:key="index"
|
||||
@click="openNotification(notification)"
|
||||
>
|
||||
<view class="notification-icon" :style="{ backgroundColor: notification.color + '20' }">
|
||||
<i :class="notification.iconClass" class="notification-icon-fa" :style="{ color: notification.color }"></i>
|
||||
</view>
|
||||
<view class="notification-content">
|
||||
<view class="notification-header">
|
||||
<text class="notification-title">{{notification.title}}</text>
|
||||
<text class="notification-time">{{notification.time}}</text>
|
||||
</view>
|
||||
<view class="notification-preview">
|
||||
<text class="notification-message">{{notification.content}}</text>
|
||||
</view>
|
||||
<view class="notification-source">
|
||||
<text>{{notification.source}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="notification-status" v-if="!notification.read">
|
||||
<view class="unread-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 操作菜单弹窗 -->
|
||||
<view class="action-menu" v-if="showMenu" @click="hideActionMenu">
|
||||
<view class="menu-mask"></view>
|
||||
<view class="menu-content" @click.stop>
|
||||
<view class="menu-item" @click="pinChat(currentChat, currentIndex)">
|
||||
<i class="fas fa-thumbtack menu-icon"></i>
|
||||
<text>{{ currentChat && currentChat.pinned ? '取消置顶' : '置顶' }}</text>
|
||||
</view>
|
||||
<view class="menu-item delete-item" @click="deleteChat(currentChat, currentIndex)">
|
||||
<i class="fas fa-trash menu-icon"></i>
|
||||
<text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const activeTab = ref('chat')
|
||||
const unreadCount = ref(5)
|
||||
const chatUnreadCount = ref(3)
|
||||
const notificationUnreadCount = ref(2)
|
||||
|
||||
// 会话列表数据
|
||||
const chatList = reactive([
|
||||
{
|
||||
id: 1,
|
||||
name: '技术部群聊',
|
||||
avatar: '/static/avatar/group1.png',
|
||||
lastMessage: '张工:项目进度如何?',
|
||||
time: '10:30',
|
||||
unread: 2,
|
||||
pinned: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '李经理',
|
||||
avatar: '/static/avatar/manager.png',
|
||||
lastMessage: '好的,我马上处理',
|
||||
time: '09:45',
|
||||
unread: 1,
|
||||
pinned: true
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '财务部群聊',
|
||||
avatar: '/static/avatar/group2.png',
|
||||
lastMessage: '报销单据已审核',
|
||||
time: '昨天',
|
||||
unread: 0,
|
||||
pinned: false
|
||||
}
|
||||
])
|
||||
|
||||
// 菜单相关数据
|
||||
const showMenu = ref(false)
|
||||
const currentChat = ref(null)
|
||||
const currentIndex = ref(-1)
|
||||
|
||||
// 通知列表数据
|
||||
const notificationList = reactive([
|
||||
{
|
||||
id: 1,
|
||||
title: '审批通知',
|
||||
content: '您的请假申请已通过部门经理审批',
|
||||
source: '人事部',
|
||||
time: '2小时前',
|
||||
iconClass: 'fas fa-file-alt',
|
||||
color: '#10b981',
|
||||
read: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '考勤通知',
|
||||
content: '今日考勤打卡成功,上班时间:09:00',
|
||||
source: '考勤系统',
|
||||
time: '3小时前',
|
||||
iconClass: 'fas fa-clock',
|
||||
color: '#3b82f6',
|
||||
read: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '系统公告',
|
||||
content: '系统将于今晚22:00-24:00进行维护升级',
|
||||
source: 'IT部门',
|
||||
time: '1天前',
|
||||
iconClass: 'fas fa-info-circle',
|
||||
color: '#8b5cf6',
|
||||
read: true
|
||||
}
|
||||
])
|
||||
|
||||
// 方法
|
||||
const handleSearch = () => {
|
||||
uni.showToast({
|
||||
title: '搜索功能',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleNotification = () => {
|
||||
uni.showToast({
|
||||
title: '通知设置',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const switchTab = (tab) => {
|
||||
activeTab.value = tab
|
||||
}
|
||||
|
||||
const openChat = (chat) => {
|
||||
// 跳转到 chat.vue 页面,并传递会话 ID
|
||||
uni.navigateTo({
|
||||
// url: `/pages/message/chat/chat?id=${chat.id}`
|
||||
url: `/pages/message/chat`
|
||||
})
|
||||
}
|
||||
|
||||
const openNotification = (notification) => {
|
||||
uni.showToast({
|
||||
title: `查看通知:${notification.title}`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 菜单相关方法
|
||||
const showActionMenu = (chat, index) => {
|
||||
currentChat.value = chat
|
||||
currentIndex.value = index
|
||||
showMenu.value = true
|
||||
}
|
||||
|
||||
const hideActionMenu = () => {
|
||||
showMenu.value = false
|
||||
currentChat.value = null
|
||||
currentIndex.value = -1
|
||||
}
|
||||
|
||||
// 操作按钮方法
|
||||
const pinChat = (chat, index) => {
|
||||
chat.pinned = !chat.pinned
|
||||
hideActionMenu()
|
||||
|
||||
// 重新排序:置顶的放在前面
|
||||
const pinnedChats = chatList.filter(item => item.pinned)
|
||||
const unpinnedChats = chatList.filter(item => !item.pinned)
|
||||
|
||||
// 清空原数组并重新添加
|
||||
chatList.splice(0, chatList.length, ...pinnedChats, ...unpinnedChats)
|
||||
|
||||
uni.showToast({
|
||||
title: chat.pinned ? '已置顶' : '已取消置顶',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const deleteChat = (chat, index) => {
|
||||
hideActionMenu()
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定要删除与"${chat.name}"的会话吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
chatList.splice(index, 1)
|
||||
uni.showToast({
|
||||
title: '已删除',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
unreadCount,
|
||||
chatUnreadCount,
|
||||
notificationUnreadCount,
|
||||
chatList,
|
||||
notificationList,
|
||||
showMenu,
|
||||
currentChat,
|
||||
currentIndex,
|
||||
handleSearch,
|
||||
handleNotification,
|
||||
switchTab,
|
||||
openChat,
|
||||
openNotification,
|
||||
showActionMenu,
|
||||
hideActionMenu,
|
||||
pinChat,
|
||||
deleteChat
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.message-page {
|
||||
height: 100vh;
|
||||
background-color: var(--background);
|
||||
position: relative;
|
||||
padding-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.message-page {
|
||||
padding-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 25rpx;
|
||||
padding: 15rpx 20rpx;
|
||||
margin-right: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 10rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.notification {
|
||||
position: relative;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
font-size: 40rpx;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 0rpx;
|
||||
right: 15rpx;
|
||||
background-color: var(--error);
|
||||
color: var(--white);
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
border-radius: 50%;
|
||||
// min-width: 30rpx;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
background: var(--white);
|
||||
padding: 0 30rpx;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30rpx 0;
|
||||
position: relative;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60rpx;
|
||||
height: 4rpx;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.tab-badge {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 80rpx;
|
||||
background-color: var(--error);
|
||||
color: var(--white);
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
border-radius: 20rpx;
|
||||
min-width: 30rpx;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
height: calc(100vh - 200rpx);
|
||||
}
|
||||
|
||||
.chat-list, .notification-list {
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.chat-item, .notification-item {
|
||||
background: var(--white);
|
||||
border-radius: 16rpx;
|
||||
// padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.notification-item{
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.chat-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-right: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
.chat-right {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.more-btn:active {
|
||||
background-color: var(--gray-lighter);
|
||||
}
|
||||
|
||||
.chat-avatar {
|
||||
position: relative;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
color: var(--white);
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.chat-badge {
|
||||
position: absolute;
|
||||
top: -5rpx;
|
||||
right: -5rpx;
|
||||
background-color: var(--error);
|
||||
color: var(--white);
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
border-radius: 20rpx;
|
||||
min-width: 30rpx;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.more-icon {
|
||||
font-size: 32rpx;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.chat-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.chat-time {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.chat-preview {
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.chat-actions {
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.notification-icon-fa {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.unread-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
background-color: var(--error);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.notification-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.notification-preview {
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.notification-source {
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.notification-source text {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-muted);
|
||||
background: var(--gray-lighter);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.notification-status {
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
/* 操作菜单弹窗 */
|
||||
.action-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.menu-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
position: relative;
|
||||
background: var(--white);
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
padding: 40rpx 0 20rpx;
|
||||
width: 100%;
|
||||
max-width: 750rpx;
|
||||
animation: slideUp 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx 40rpx;
|
||||
font-size: 32rpx;
|
||||
color: var(--text-color);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
background-color: var(--gray-lighter);
|
||||
}
|
||||
|
||||
.menu-item.delete-item {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 36rpx;
|
||||
margin-right: 20rpx;
|
||||
width: 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,589 @@
|
||||
<template>
|
||||
<view class="user-detail-container">
|
||||
<!-- 移动设备顶部状态栏 -->
|
||||
<view class="top_bar flex w-full" v-if="isMobile">
|
||||
<view class="chat-header">
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">用户详情</view>
|
||||
<view class="header-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 浏览器环境顶部导航栏 -->
|
||||
<view class="chat-header" v-else>
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">用户详情</view>
|
||||
<view class="header-right"></view>
|
||||
</view>
|
||||
<!-- 顶部用户信息卡片 -->
|
||||
<view class="user-card">
|
||||
<view class="user-info">
|
||||
<view class="avatar-container">
|
||||
<image
|
||||
class="avatar"
|
||||
:src="user.avatar || '/static/imgs/default_avatar.png'"
|
||||
mode="aspectFill"
|
||||
@error="onAvatarError"
|
||||
/>
|
||||
<view class="online-status" v-if="user.isOnline"></view>
|
||||
</view>
|
||||
<view class="user-base-info">
|
||||
<view class="nickname">{{ user.nickname }}</view>
|
||||
<view class="meteid">账号:{{ user.meteid }}</view>
|
||||
<view class="meta-info">
|
||||
<view class="meta-item" v-if="user.department">
|
||||
部门:<text>{{ user.department }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="meta-info">
|
||||
<view class="meta-item" v-if="user.position">
|
||||
职位:<text>{{ user.position }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能按钮区域 -->
|
||||
<view class="action-section">
|
||||
<view class="action-btn primary" @click="sendMessage">
|
||||
<i class="fas fa-comment-dots"></i>
|
||||
<text>发消息</text>
|
||||
</view>
|
||||
<view class="action-btn" @click="startAudioCall">
|
||||
<i class="fas fa-phone-alt"></i>
|
||||
<text>语音通话</text>
|
||||
</view>
|
||||
<view class="action-btn" @click="startVideoCall">
|
||||
<i class="fas fa-video"></i>
|
||||
<text>视频通话</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 资料信息卡片 -->
|
||||
<view class="info-card">
|
||||
<view class="card-header">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
<text>个人资料</text>
|
||||
</view>
|
||||
<view class="info-list">
|
||||
<view class="info-item" v-if="user.phone" @click="copyToClipboard(user.phone, '手机号')">
|
||||
<view class="info-icon phone">
|
||||
<i class="fas fa-phone"></i>
|
||||
</view>
|
||||
<view class="info-content">
|
||||
<text class="info-label">手机号码</text>
|
||||
<text class="info-value">{{ user.phone }}</text>
|
||||
</view>
|
||||
<i class="fas fa-copy"></i>
|
||||
</view>
|
||||
<view class="info-item" v-if="user.wechat" @click="copyToClipboard(user.wechat, '微信号')">
|
||||
<view class="info-icon wechat">
|
||||
<i class="fab fa-weixin"></i>
|
||||
</view>
|
||||
<view class="info-content">
|
||||
<text class="info-label">微信</text>
|
||||
<text class="info-value">{{ user.wechat }}</text>
|
||||
</view>
|
||||
<i class="fas fa-copy"></i>
|
||||
</view>
|
||||
<view class="info-item" v-if="user.email" @click="copyToClipboard(user.email, '邮箱')">
|
||||
<view class="info-icon email">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</view>
|
||||
<view class="info-content">
|
||||
<text class="info-label">邮箱</text>
|
||||
<text class="info-value">{{ user.email }}</text>
|
||||
</view>
|
||||
<i class="fas fa-copy"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 更多功能卡片 -->
|
||||
<view class="more-card">
|
||||
<view class="card-header">
|
||||
<i class="fas fa-cog"></i>
|
||||
<text>更多功能</text>
|
||||
</view>
|
||||
<view class="more-list">
|
||||
<view class="more-item" @click="addToContacts">
|
||||
<view class="more-icon">
|
||||
<i class="fas fa-user-plus"></i>
|
||||
</view>
|
||||
<text>添加到通讯录</text>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
<view class="more-item" @click="shareContact">
|
||||
<view class="more-icon">
|
||||
<i class="fas fa-share-alt"></i>
|
||||
</view>
|
||||
<text>发送名片</text>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
<view class="more-item" @click="viewMoments">
|
||||
<view class="more-icon">
|
||||
<i class="fas fa-images"></i>
|
||||
</view>
|
||||
<text>查看朋友圈</text>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
user: {
|
||||
avatar: '',
|
||||
nickname: '张三',
|
||||
meteid: 'mete_10001',
|
||||
position: '产品经理',
|
||||
department: '产品部',
|
||||
phone: '13455668888',
|
||||
wechat: 'zhangsan_weixin',
|
||||
email: 'zhangsan@email.com',
|
||||
isOnline: true
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 从全局数据获取设备信息
|
||||
isMobile() {
|
||||
return getApp().globalData.isMobile;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack();
|
||||
},
|
||||
onAvatarError(e) {
|
||||
this.user.avatar = '/static/imgs/default_avatar.png'
|
||||
},
|
||||
sendMessage() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/message/chat?meteid=' + this.user.meteid
|
||||
});
|
||||
},
|
||||
startAudioCall() {
|
||||
uni.showToast({
|
||||
title: '语音通话功能暂未开放',
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
startVideoCall() {
|
||||
uni.showToast({
|
||||
title: '视频通话功能暂未开放',
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
copyToClipboard(text, type) {
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: `${type}已复制`,
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
addToContacts() {
|
||||
uni.showToast({
|
||||
title: '已添加到通讯录',
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
shareContact() {
|
||||
uni.showToast({
|
||||
title: '已发送名片',
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
viewMoments() {
|
||||
uni.showToast({
|
||||
title: '朋友圈功能暂未开放',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-detail-container {
|
||||
background: var(--background);
|
||||
min-height: 100vh;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
/* 移动设备顶部状态栏 */
|
||||
.top_bar {
|
||||
background: var(--gradient-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding-top: var(--status-bar-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.top_bar {
|
||||
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.top_bar + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.user-detail-container .chat-header:not(.top_bar .chat-header) {
|
||||
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.user-detail-container .chat-header:not(.top_bar .chat-header) + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
background-color: var(--surface);
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* 浏览器环境下的固定定位 */
|
||||
.user-detail-container .chat-header:not(.top_bar .chat-header) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
padding-top: var(--status-bar-height);
|
||||
}
|
||||
|
||||
/* 移动设备下的导航栏样式 */
|
||||
.top_bar .chat-header {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.top_bar .header-title {
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.top_bar .header-left i,
|
||||
.top_bar .header-right i {
|
||||
color: var(--white);
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
width: 80rpx;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.header-left:active,
|
||||
.header-right:active {
|
||||
background-color: var(--gray-lighter);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.top_bar .header-left:active,
|
||||
.top_bar .header-right:active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
/* 移动设备下为内容添加顶部间距 */
|
||||
.top_bar + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
/* 浏览器环境下为内容添加顶部间距 */
|
||||
.user-detail-container .chat-header:not(.top_bar .chat-header) + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
/* 用户信息卡片 */
|
||||
.user-card {
|
||||
background: var(--surface);
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: var(--shadow-md);
|
||||
overflow: hidden;
|
||||
border: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40rpx 30rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
background: var(--gray-light);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.online-status {
|
||||
position: absolute;
|
||||
bottom: 8rpx;
|
||||
right: 8rpx;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background: var(--success);
|
||||
border: 4rpx solid var(--white);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-base-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.meteid {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.meta-item i {
|
||||
margin-right: 12rpx;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
/* 功能按钮区域 */
|
||||
.action-section {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
background: var(--surface);
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1rpx solid var(--border-light);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: translateY(2rpx);
|
||||
box-shadow: var(--shadow-md);
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: var(--gradient-primary);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn i {
|
||||
font-size: 32rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.action-btn text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 信息卡片 */
|
||||
.info-card, .more-card {
|
||||
background: var(--surface);
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1rpx solid var(--border-light);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx 30rpx 20rpx 30rpx;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
background: var(--gray-lighter);
|
||||
}
|
||||
|
||||
.card-header i {
|
||||
font-size: 28rpx;
|
||||
color: var(--icon-color);
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.card-header text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
.info-list, .more-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.info-item, .more-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.info-item:last-child, .more-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-item:active, .more-item:active {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.info-icon.phone {
|
||||
background: var(--gradient-success);
|
||||
}
|
||||
|
||||
.info-icon.wechat {
|
||||
background: var(--gradient-success);
|
||||
}
|
||||
|
||||
.info-icon.email {
|
||||
background: var(--gradient-warning);
|
||||
}
|
||||
|
||||
.info-icon i {
|
||||
color: var(--white);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-item i.fas.fa-copy, .more-item i.fas.fa-chevron-right {
|
||||
color: var(--text-muted);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.more-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 12rpx;
|
||||
background: var(--gradient-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.more-icon i {
|
||||
color: var(--white);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.more-item text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<view class="splash-container">
|
||||
<!-- 背景动画 -->
|
||||
<view class="splash-background">
|
||||
<view class="gradient-circle circle-1"></view>
|
||||
<view class="gradient-circle circle-2"></view>
|
||||
<view class="gradient-circle circle-3"></view>
|
||||
</view>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<view class="splash-content">
|
||||
<!-- Logo区域 -->
|
||||
<view class="logo-section">
|
||||
<view class="logo-container">
|
||||
<text class="logo-text">{{appInfo.name}}</text>
|
||||
<view class="logo-subtitle">{{appInfo.nameEn}}</view>
|
||||
</view>
|
||||
<view class="logo-icon">
|
||||
<text class="icon-text">{{appInfo.logo}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载动画 -->
|
||||
<view class="loading-section">
|
||||
<view class="loading-dots">
|
||||
<view class="dot" :class="{ active: loadingStep >= 1 }"></view>
|
||||
<view class="dot" :class="{ active: loadingStep >= 2 }"></view>
|
||||
<view class="dot" :class="{ active: loadingStep >= 3 }"></view>
|
||||
</view>
|
||||
<text class="loading-text">{{loadingText}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 版本信息 -->
|
||||
<!-- <view class="version-info">
|
||||
<text class="version-text">v{{appVersion}}</text>
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useAuthStore } from '../../src/store/authStore.js'
|
||||
import {
|
||||
initSplash,
|
||||
shouldShowSplash,
|
||||
markSplashShown,
|
||||
getSplashDuration,
|
||||
loadingSteps,
|
||||
executeLoadingStep,
|
||||
onSplashComplete
|
||||
} from '../../src/utils/splashManager.js'
|
||||
import { getAppInfo, getLoadingSteps, getThemeConfig } from '../../src/config/splash.js'
|
||||
|
||||
export default {
|
||||
name: 'SplashScreen',
|
||||
setup() {
|
||||
const authStore = useAuthStore()
|
||||
const loadingStep = ref(0)
|
||||
const loadingText = ref('正在初始化...')
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 获取配置信息
|
||||
const appInfo = getAppInfo()
|
||||
const themeConfig = getThemeConfig()
|
||||
const configLoadingSteps = getLoadingSteps()
|
||||
|
||||
// 初始化启动画面
|
||||
initSplash()
|
||||
|
||||
// 启动加载动画
|
||||
const startLoading = async () => {
|
||||
if (isLoading.value) return
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// 执行每个加载步骤
|
||||
for (let i = 0; i < configLoadingSteps.length; i++) {
|
||||
const step = configLoadingSteps[i]
|
||||
|
||||
// 更新UI
|
||||
loadingStep.value = i + 1
|
||||
loadingText.value = step.text
|
||||
|
||||
// 执行步骤
|
||||
const success = await executeLoadingStep(step)
|
||||
|
||||
if (!success) {
|
||||
console.warn(`步骤 ${step.action} 执行失败,继续下一步`)
|
||||
}
|
||||
|
||||
// 等待步骤完成时间
|
||||
await new Promise(resolve => setTimeout(resolve, step.duration))
|
||||
}
|
||||
|
||||
// 确保最小显示时间
|
||||
const minDuration = getSplashDuration()
|
||||
if (minDuration > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, minDuration))
|
||||
}
|
||||
|
||||
// 启动完成
|
||||
onSplashComplete()
|
||||
|
||||
// 根据登录状态跳转
|
||||
if (authStore.isAuthenticated) {
|
||||
// 已登录,跳转到主页面
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
} else {
|
||||
// 未登录,跳转到登录页面
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index'
|
||||
})
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('启动过程出错:', error)
|
||||
// 即使出错也要跳转,根据登录状态决定
|
||||
if (authStore.isAuthenticated) {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
} else {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 检查是否应该显示启动画面
|
||||
if (shouldShowSplash()) {
|
||||
// 延迟启动动画,让用户看到启动画面
|
||||
setTimeout(() => {
|
||||
startLoading()
|
||||
}, 500)
|
||||
} else {
|
||||
// 直接跳转,根据登录状态决定
|
||||
setTimeout(() => {
|
||||
if (authStore.isAuthenticated) {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
} else {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index'
|
||||
})
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
loadingStep,
|
||||
loadingText,
|
||||
appVersion: appInfo.version,
|
||||
appInfo,
|
||||
isLoading
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.splash-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.splash-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gradient-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.circle-1 {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.circle-2 {
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
top: 60%;
|
||||
right: 10%;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.circle-3 {
|
||||
width: 150rpx;
|
||||
height: 150rpx;
|
||||
top: 30%;
|
||||
right: 30%;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) rotate(180deg);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.splash-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 120rpx;
|
||||
animation: fadeInUp 1s ease-out;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 64rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.logo-subtitle {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(20rpx);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 60rpx;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 80rpx;
|
||||
animation: fadeInUp 1s ease-out 0.5s both;
|
||||
}
|
||||
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
background: #fff;
|
||||
transform: scale(1.2);
|
||||
box-shadow: 0 0 20rpx rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
position: absolute;
|
||||
bottom: 60rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
animation: fadeInUp 1s ease-out 1s both;
|
||||
}
|
||||
|
||||
.version-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media screen and (max-width: 750rpx) {
|
||||
.logo-text {
|
||||
font-size: 56rpx;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 50rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,607 @@
|
||||
/**
|
||||
* API接口配置
|
||||
*/
|
||||
|
||||
import { apiBaseUrl, apiTimeout } from '../config/index.js'
|
||||
|
||||
// 基础配置 - 从配置文件获取
|
||||
const BASE_URL = apiBaseUrl
|
||||
const TIMEOUT = apiTimeout
|
||||
|
||||
|
||||
/**
|
||||
* 请求拦截器
|
||||
*/
|
||||
const requestInterceptor = (config) => {
|
||||
// 添加token
|
||||
const token = uni.getStorageSync('token')
|
||||
if (token) {
|
||||
config.header = {
|
||||
...config.header,
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
// 添加通用请求头
|
||||
config.header = {
|
||||
'Content-Type': 'application/json',
|
||||
...config.header
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应拦截器
|
||||
*/
|
||||
const responseInterceptor = (response) => {
|
||||
const { statusCode, data } = response
|
||||
|
||||
if (statusCode === 200) {
|
||||
if (data.code === 0) {
|
||||
return data.data
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: data.message || '请求失败',
|
||||
icon: 'none'
|
||||
})
|
||||
return Promise.reject(new Error(data.message || '请求失败'))
|
||||
}
|
||||
} else if (statusCode === 401) {
|
||||
// token过期,跳转登录
|
||||
uni.removeStorageSync('token')
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
return Promise.reject(new Error('登录已过期'))
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '网络错误',
|
||||
icon: 'none'
|
||||
})
|
||||
return Promise.reject(new Error('网络错误'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用请求方法
|
||||
*/
|
||||
const request = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 请求拦截
|
||||
const config = requestInterceptor({
|
||||
url: options.url.startsWith('/') ? BASE_URL + options.url : BASE_URL + '/' + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header: options.header || {},
|
||||
timeout: options.timeout || TIMEOUT
|
||||
})
|
||||
|
||||
uni.request({
|
||||
...config,
|
||||
success: (response) => {
|
||||
try {
|
||||
const result = responseInterceptor(response)
|
||||
resolve(result)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
uni.showToast({
|
||||
title: '网络连接失败',
|
||||
icon: 'none'
|
||||
})
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户相关API
|
||||
*/
|
||||
export const userApi = {
|
||||
// 登录
|
||||
login(data) {
|
||||
return request({
|
||||
url: '/api/login',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 登出
|
||||
logout() {
|
||||
return request({
|
||||
url: '/api/logout',
|
||||
method: 'POST'
|
||||
})
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo() {
|
||||
return request({
|
||||
url: '/api/user/info',
|
||||
method: 'GET'
|
||||
})
|
||||
},
|
||||
|
||||
// 更新用户信息
|
||||
updateUserInfo(data) {
|
||||
return request({
|
||||
url: '/api/user/info',
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 修改密码
|
||||
changePassword(data) {
|
||||
return request({
|
||||
url: '/api/user/password',
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 上传头像
|
||||
uploadAvatar(file) {
|
||||
return request({
|
||||
url: '/api/user/avatar',
|
||||
method: 'POST',
|
||||
data: file
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 考勤相关API
|
||||
*/
|
||||
export const attendanceApi = {
|
||||
// 打卡
|
||||
checkIn(data) {
|
||||
return request({
|
||||
url: '/api/attendance/checkin',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 下班打卡
|
||||
checkOut(data) {
|
||||
return request({
|
||||
url: '/api/attendance/checkout',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 获取考勤记录
|
||||
getAttendanceList(params) {
|
||||
return request({
|
||||
url: '/api/attendance/list',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
// 获取考勤统计
|
||||
getAttendanceStats(params) {
|
||||
return request({
|
||||
url: '/api/attendance/stats',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
// 获取考勤详情
|
||||
getAttendanceDetail(id) {
|
||||
return request({
|
||||
url: '/api/attendance/detail',
|
||||
method: 'GET',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请假相关API
|
||||
*/
|
||||
export const leaveApi = {
|
||||
// 申请请假
|
||||
applyLeave(data) {
|
||||
return request({
|
||||
url: '/api/leave/apply',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 获取请假列表
|
||||
getLeaveList(params) {
|
||||
return request({
|
||||
url: '/api/leave/list',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
// 获取请假详情
|
||||
getLeaveDetail(id) {
|
||||
return request({
|
||||
url: '/api/leave/detail',
|
||||
method: 'GET',
|
||||
data: { id }
|
||||
})
|
||||
},
|
||||
|
||||
// 取消请假
|
||||
cancelLeave(id) {
|
||||
return request({
|
||||
url: '/api/leave/cancel',
|
||||
method: 'PUT',
|
||||
data: { id }
|
||||
})
|
||||
},
|
||||
|
||||
// 审批请假
|
||||
approveLeave(id, data) {
|
||||
return request({
|
||||
url: '/api/leave/approve',
|
||||
method: 'PUT',
|
||||
data: { id, ...data }
|
||||
})
|
||||
},
|
||||
|
||||
// 拒绝请假
|
||||
rejectLeave(id, data) {
|
||||
return request({
|
||||
url: '/api/leave/reject',
|
||||
method: 'PUT',
|
||||
data: { id, ...data }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 报销相关API
|
||||
*/
|
||||
export const reimbursementApi = {
|
||||
// 提交报销
|
||||
submitReimbursement(data) {
|
||||
return request({
|
||||
url: '/api/reimbursement/submit',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 获取报销列表
|
||||
getReimbursementList(params) {
|
||||
return request({
|
||||
url: '/api/reimbursement/list',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
// 获取报销详情
|
||||
getReimbursementDetail(id) {
|
||||
return request({
|
||||
url: '/api/reimbursement/detail',
|
||||
method: 'GET',
|
||||
data: { id }
|
||||
})
|
||||
},
|
||||
|
||||
// 上传发票
|
||||
uploadInvoice(file) {
|
||||
return request({
|
||||
url: '/api/reimbursement/upload',
|
||||
method: 'POST',
|
||||
data: file
|
||||
})
|
||||
},
|
||||
|
||||
// 审批报销
|
||||
approveReimbursement(id, data) {
|
||||
return request({
|
||||
url: '/api/reimbursement/approve',
|
||||
method: 'PUT',
|
||||
data: { id, ...data }
|
||||
})
|
||||
},
|
||||
|
||||
// 拒绝报销
|
||||
rejectReimbursement(id, data) {
|
||||
return request({
|
||||
url: '/api/reimbursement/reject',
|
||||
method: 'PUT',
|
||||
data: { id, ...data }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务相关API
|
||||
*/
|
||||
export const taskApi = {
|
||||
// 获取任务列表
|
||||
getTaskList(params) {
|
||||
return request({
|
||||
url: '/api/task/list',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
// 创建任务
|
||||
createTask(data) {
|
||||
return request({
|
||||
url: '/api/task/create',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 获取任务详情
|
||||
getTaskDetail(id) {
|
||||
return request({
|
||||
url: '/api/task/detail',
|
||||
method: 'GET',
|
||||
data: { id }
|
||||
})
|
||||
},
|
||||
|
||||
// 更新任务
|
||||
updateTask(id, data) {
|
||||
return request({
|
||||
url: '/api/task/update',
|
||||
method: 'PUT',
|
||||
data: { id, ...data }
|
||||
})
|
||||
},
|
||||
|
||||
// 更新任务状态
|
||||
updateTaskStatus(id, status) {
|
||||
return request({
|
||||
url: '/api/task/status',
|
||||
method: 'PUT',
|
||||
data: { id, status }
|
||||
})
|
||||
},
|
||||
|
||||
// 分配任务
|
||||
assignTask(id, data) {
|
||||
return request({
|
||||
url: '/api/task/assign',
|
||||
method: 'PUT',
|
||||
data: { id, ...data }
|
||||
})
|
||||
},
|
||||
|
||||
// 完成任务
|
||||
completeTask(id, data) {
|
||||
return request({
|
||||
url: '/api/task/complete',
|
||||
method: 'PUT',
|
||||
data: { id, ...data }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息相关API
|
||||
*/
|
||||
export const messageApi = {
|
||||
// 获取消息列表
|
||||
getMessageList(params) {
|
||||
return request({
|
||||
url: '/api/message/list',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
// 获取消息详情
|
||||
getMessageDetail(id) {
|
||||
return request({
|
||||
url: '/api/message/detail',
|
||||
method: 'GET',
|
||||
data: { id }
|
||||
})
|
||||
},
|
||||
|
||||
// 标记消息为已读
|
||||
markAsRead(id) {
|
||||
return request({
|
||||
url: '/api/message/read',
|
||||
method: 'PUT',
|
||||
data: { id }
|
||||
})
|
||||
},
|
||||
|
||||
// 获取未读消息数量
|
||||
getUnreadCount() {
|
||||
return request({
|
||||
url: '/api/message/unread-count',
|
||||
method: 'GET'
|
||||
})
|
||||
},
|
||||
|
||||
// 发送消息
|
||||
sendMessage(data) {
|
||||
return request({
|
||||
url: '/api/message/send',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件相关API
|
||||
*/
|
||||
export const fileApi = {
|
||||
// 上传文件
|
||||
uploadFile(file) {
|
||||
return request({
|
||||
url: '/api/file/upload',
|
||||
method: 'POST',
|
||||
data: file
|
||||
})
|
||||
},
|
||||
|
||||
// 获取文件列表
|
||||
getFileList(params) {
|
||||
return request({
|
||||
url: '/api/file/list',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
// 下载文件
|
||||
downloadFile(id) {
|
||||
return request({
|
||||
url: '/api/file/download',
|
||||
method: 'GET',
|
||||
data: { id }
|
||||
})
|
||||
},
|
||||
|
||||
// 删除文件
|
||||
deleteFile(id) {
|
||||
return request({
|
||||
url: '/api/file/delete',
|
||||
method: 'DELETE',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户相关API
|
||||
*/
|
||||
export const customerApi = {
|
||||
// 获取客户列表
|
||||
getCustomerList(params) {
|
||||
return request({
|
||||
url: '/api/customer/list',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
// 获取客户详情
|
||||
getCustomerDetail(id) {
|
||||
return request({
|
||||
url: '/api/customer/detail',
|
||||
method: 'GET',
|
||||
data: { id }
|
||||
})
|
||||
},
|
||||
|
||||
// 添加客户
|
||||
addCustomer(data) {
|
||||
return request({
|
||||
url: '/api/customer/add',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// 更新客户信息
|
||||
updateCustomer(id, data) {
|
||||
return request({
|
||||
url: '/api/customer/update',
|
||||
method: 'PUT',
|
||||
data: { id, ...data }
|
||||
})
|
||||
},
|
||||
|
||||
// 删除客户
|
||||
deleteCustomer(id) {
|
||||
return request({
|
||||
url: '/api/customer/delete',
|
||||
method: 'DELETE',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门相关API
|
||||
*/
|
||||
export const departmentApi = {
|
||||
// 获取部门列表
|
||||
getDepartmentList(params) {
|
||||
return request({
|
||||
url: '/api/department/list',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
// 获取部门树
|
||||
getDepartmentTree() {
|
||||
return request({
|
||||
url: '/api/department/tree',
|
||||
method: 'GET'
|
||||
})
|
||||
},
|
||||
|
||||
// 获取部门详情
|
||||
getDepartmentDetail(id) {
|
||||
return request({
|
||||
url: '/api/department/detail',
|
||||
method: 'GET',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知相关API
|
||||
*/
|
||||
export const notificationApi = {
|
||||
// 获取通知列表
|
||||
getNotificationList(params) {
|
||||
return request({
|
||||
url: '/api/notification/list',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
// 标记通知为已读
|
||||
markAsRead(id) {
|
||||
return request({
|
||||
url: '/api/notification/read',
|
||||
method: 'PUT',
|
||||
data: { id }
|
||||
})
|
||||
},
|
||||
|
||||
// 获取未读通知数量
|
||||
getUnreadCount() {
|
||||
return request({
|
||||
url: '/api/notification/unread-count',
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
userApi,
|
||||
attendanceApi,
|
||||
leaveApi,
|
||||
reimbursementApi,
|
||||
taskApi,
|
||||
messageApi,
|
||||
fileApi,
|
||||
customerApi,
|
||||
departmentApi,
|
||||
notificationApi
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<view class="custom-navbar" :style="navbarStyle">
|
||||
<view class="navbar-content">
|
||||
<view class="navbar-left" v-if="showBack" @click="handleBack">
|
||||
<u-icon name="arrow-left" size="20" color="#fff"></u-icon>
|
||||
</view>
|
||||
<view class="navbar-center">
|
||||
<text class="navbar-title">{{ title }}</text>
|
||||
</view>
|
||||
<view class="navbar-right">
|
||||
<slot name="right">
|
||||
<view class="search-box" @click="handleSearch" v-if="showSearch">
|
||||
<u-icon name="search" size="16" color="#999"></u-icon>
|
||||
<text class="search-placeholder">搜索</text>
|
||||
</view>
|
||||
<view class="notification" @click="handleNotification" v-if="showNotification">
|
||||
<u-icon name="bell" size="20" color="#fff"></u-icon>
|
||||
<u-badge :count="unreadCount" :offset="[-2, 2]" v-if="unreadCount > 0"></u-badge>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CustomNavbar',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '企业办公'
|
||||
},
|
||||
showBack: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showNotification: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
unreadCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: 'linear-gradient(135deg, #2B7CE9 0%, #1E5F99 100%)'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
navbarStyle() {
|
||||
return {
|
||||
background: this.backgroundColor
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleBack() {
|
||||
this.$emit('back')
|
||||
uni.navigateBack()
|
||||
},
|
||||
handleSearch() {
|
||||
this.$emit('search')
|
||||
},
|
||||
handleNotification() {
|
||||
this.$emit('notification')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
background: linear-gradient(135deg, #2B7CE9 0%, #1E5F99 100%);
|
||||
padding: 20rpx 30rpx;
|
||||
padding-top: calc(var(--status-bar-height) + 20rpx);
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
width: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.navbar-center {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
width: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 25rpx;
|
||||
padding: 15rpx 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 200rpx;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 28rpx;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.notification {
|
||||
position: relative;
|
||||
padding: 10rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<view class="data-card" :class="{ clickable: clickable }" @click="handleClick">
|
||||
<view class="card-icon" :style="{ backgroundColor: iconBgColor }">
|
||||
<u-icon :name="icon" size="24" :color="iconColor"></u-icon>
|
||||
</view>
|
||||
<view class="card-content">
|
||||
<text class="card-number" :style="{ color: numberColor }">{{ number }}</text>
|
||||
<text class="card-label">{{ label }}</text>
|
||||
<text class="card-desc" v-if="description">{{ description }}</text>
|
||||
</view>
|
||||
<view class="card-arrow" v-if="showArrow">
|
||||
<u-icon name="arrow-right" size="16" color="#999"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DataCard',
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: '#2B7CE9'
|
||||
},
|
||||
iconBgColor: {
|
||||
type: String,
|
||||
default: 'rgba(43, 124, 233, 0.1)'
|
||||
},
|
||||
number: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
numberColor: {
|
||||
type: String,
|
||||
default: '#333'
|
||||
},
|
||||
clickable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showArrow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
if (this.clickable) {
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.data-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.data-card.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.data-card.clickable:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-number {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.card-arrow {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<view class="emoji-picker" v-if="visible">
|
||||
<!-- 表情内容区域 -->
|
||||
<scroll-view class="emoji-content" scroll-y>
|
||||
<view class="emoji-category">
|
||||
<view class="category-title">{{ emojiCategories[activeCategoryIndex].name }}</view>
|
||||
<view class="emoji-grid">
|
||||
<view
|
||||
class="emoji-item"
|
||||
v-for="(emoji, index) in emojiCategories[activeCategoryIndex].emojis"
|
||||
:key="index"
|
||||
@click="selectEmoji(emoji)"
|
||||
>
|
||||
<text class="emoji-text">{{ emoji.unicode }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 分类标签 -->
|
||||
<view class="category-tabs">
|
||||
<scroll-view class="tabs-container" scroll-x>
|
||||
<view class="tabs-content">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeCategoryIndex === index }"
|
||||
v-for="(category, index) in emojiCategories"
|
||||
:key="category.id"
|
||||
@click="scrollToCategory(index)"
|
||||
>
|
||||
<text class="tab-emoji">{{ category.emojis[0].unicode }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { emojiCategories } from '../utils/emojis.js';
|
||||
|
||||
export default {
|
||||
name: 'EmojiPicker',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
emojiCategories: emojiCategories,
|
||||
activeCategoryIndex: 0
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
selectEmoji(emoji) {
|
||||
this.$emit('select', emoji);
|
||||
},
|
||||
closePicker() {
|
||||
this.$emit('close');
|
||||
},
|
||||
scrollToCategory(index) {
|
||||
this.activeCategoryIndex = index;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.emoji-picker {
|
||||
background-color: #fff;
|
||||
border-top: 1rpx solid #eee;
|
||||
max-height: 400rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.emoji-content {
|
||||
flex: 1;
|
||||
max-height: 300rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.emoji-category {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 20rpx;
|
||||
padding-left: 10rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.emoji-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.emoji-item {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12rpx;
|
||||
background-color: #f8f8f8;
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.emoji-item:active {
|
||||
background-color: #e8f5e8;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.emoji-text {
|
||||
font-size: 40rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.category-tabs {
|
||||
border-top: 1rpx solid #eee;
|
||||
background-color: #f8f8f8;
|
||||
padding: 10rpx 0;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
display: inline-block;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: inline-block;
|
||||
padding: 16rpx 24rpx;
|
||||
margin-right: 10rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #fff;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-item:active {
|
||||
background-color: #e8f5e8;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
background-color: #07c160;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-emoji {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tab-emoji {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<view class="function-list">
|
||||
<view class="list-header" v-if="title">
|
||||
<text class="list-title">{{ title }}</text>
|
||||
<text class="list-more" v-if="showMore" @click="handleMore">更多</text>
|
||||
</view>
|
||||
<view class="list-content">
|
||||
<view
|
||||
class="function-item"
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
@click="handleItemClick(item, index)"
|
||||
>
|
||||
<view class="item-icon">
|
||||
<u-icon :name="item.icon" size="24" :color="item.color"></u-icon>
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<text class="item-name">{{ item.name }}</text>
|
||||
<text class="item-desc" v-if="item.description">{{ item.description }}</text>
|
||||
</view>
|
||||
<view class="item-arrow" v-if="showArrow">
|
||||
<u-icon name="arrow-right" size="16" color="#999"></u-icon>
|
||||
</view>
|
||||
<view class="item-badge" v-if="item.badge">
|
||||
<u-badge :count="item.badge" :offset="[5, -5]"></u-badge>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FunctionList',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
list: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
showMore: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showArrow: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleItemClick(item, index) {
|
||||
this.$emit('item-click', item, index)
|
||||
},
|
||||
handleMore() {
|
||||
this.$emit('more-click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.function-list {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx 30rpx 20rpx;
|
||||
background-color: #F8F9FA;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.list-more {
|
||||
font-size: 26rpx;
|
||||
color: #2B7CE9;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.function-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx 0;
|
||||
border-bottom: 1rpx solid #F0F0F0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.function-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.item-arrow {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.item-badge {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<view class="task-card" :class="{ completed: task.completed, overdue: isOverdue }">
|
||||
<view class="task-checkbox" @click="toggleTask">
|
||||
<view class="checkbox" :class="{ checked: task.completed }">
|
||||
<i class="fas fa-check" v-if="task.completed"></i>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="task-content" @click="toggleTask">
|
||||
<view class="task-header">
|
||||
<text class="task-title">{{ task.title }}</text>
|
||||
<view class="task-priority" :class="task.priority">
|
||||
<i class="fas fa-circle"></i>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="task-description" v-if="task.description">{{ task.description }}</text>
|
||||
|
||||
<view class="task-meta">
|
||||
<view class="task-tags" v-if="task.tags.length > 0">
|
||||
<text
|
||||
v-for="tag in task.tags.slice(0, 3)"
|
||||
:key="tag"
|
||||
class="task-tag">
|
||||
{{ tag }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="task-due-date" v-if="task.dueDate">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
<text class="due-date-text">{{ formatDate(task.dueDate) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="task-actions">
|
||||
<view class="action-btn" @click="editTask">
|
||||
<i class="fas fa-edit"></i>
|
||||
</view>
|
||||
<view class="action-btn delete" @click="deleteTask">
|
||||
<i class="fas fa-trash"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TaskCard',
|
||||
props: {
|
||||
task: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['toggle', 'edit', 'delete'],
|
||||
computed: {
|
||||
isOverdue() {
|
||||
if (this.task.completed || !this.task.dueDate) return false
|
||||
return new Date(this.task.dueDate) < new Date()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleTask() {
|
||||
this.$emit('toggle', this.task.id)
|
||||
},
|
||||
|
||||
editTask() {
|
||||
this.$emit('edit', this.task)
|
||||
},
|
||||
|
||||
deleteTask() {
|
||||
this.$emit('delete', this.task.id)
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
const date = new Date(dateString)
|
||||
const now = new Date()
|
||||
const diffTime = date - now
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays === 0) return '今天'
|
||||
if (diffDays === 1) return '明天'
|
||||
if (diffDays === -1) return '昨天'
|
||||
if (diffDays < 0) return `${Math.abs(diffDays)}天前`
|
||||
if (diffDays <= 7) return `${diffDays}天后`
|
||||
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.task-card.completed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.task-card.overdue {
|
||||
border-left: 6rpx solid #e74c3c;
|
||||
}
|
||||
|
||||
.task-checkbox {
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border: 2rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.checkbox.checked {
|
||||
background: #27ae60;
|
||||
border-color: #27ae60;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.task-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-priority {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.task-priority.high {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.task-priority.medium {
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
.task-priority.low {
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
.task-description {
|
||||
font-size: 26rpx;
|
||||
color: #7f8c8d;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-tags {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.task-tag {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.task-due-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
color: #7f8c8d;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #f8f9fa;
|
||||
color: #7f8c8d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn.delete {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<view class="task-stats">
|
||||
<view class="stats-card">
|
||||
<view class="stat-item" @click="onStatClick('total')">
|
||||
<text class="stat-number">{{ stats.total }}</text>
|
||||
<text class="stat-label">总任务</text>
|
||||
</view>
|
||||
<view class="stat-item" @click="onStatClick('pending')">
|
||||
<text class="stat-number">{{ stats.pending }}</text>
|
||||
<text class="stat-label">待完成</text>
|
||||
</view>
|
||||
<view class="stat-item" @click="onStatClick('completed')">
|
||||
<text class="stat-number">{{ stats.completed }}</text>
|
||||
<text class="stat-label">已完成</text>
|
||||
</view>
|
||||
<view class="stat-item" @click="onStatClick('overdue')">
|
||||
<text class="stat-number">{{ stats.overdue }}</text>
|
||||
<text class="stat-label">已逾期</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="progress-section" v-if="stats.total > 0">
|
||||
<view class="progress-header">
|
||||
<text class="progress-title">完成进度</text>
|
||||
<text class="progress-percentage">{{ stats.completionRate }}%</text>
|
||||
</view>
|
||||
<view class="progress-bar">
|
||||
<view
|
||||
class="progress-fill"
|
||||
:style="{ width: stats.completionRate + '%' }">
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TaskStats',
|
||||
props: {
|
||||
stats: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['statClick'],
|
||||
methods: {
|
||||
onStatClick(type) {
|
||||
this.$emit('statClick', type)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-stats {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.stat-item:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
display: block;
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #3498db;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.progress-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.progress-percentage {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 12rpx;
|
||||
background: #f0f0f0;
|
||||
border-radius: 6rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #3498db 0%, #27ae60 100%);
|
||||
border-radius: 6rpx;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 配置模块统一导出
|
||||
* 直接定义配置,简单明了
|
||||
*/
|
||||
|
||||
// 常用配置 - 直接定义
|
||||
export const apiBaseUrl = 'https://apigo.yunzer.cn'
|
||||
export const apiTimeout = 10000
|
||||
export const appName = '企业办公移动应用'
|
||||
export const appVersion = '1.0.0'
|
||||
export const debug = true
|
||||
|
||||
// 环境判断
|
||||
export const isDev = true
|
||||
export const isTest = false
|
||||
export const isProd = false
|
||||
|
||||
// 默认导出
|
||||
export default {
|
||||
apiBaseUrl,
|
||||
apiTimeout,
|
||||
appName,
|
||||
appVersion,
|
||||
debug,
|
||||
isDev,
|
||||
isTest,
|
||||
isProd
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 启动画面配置文件
|
||||
*/
|
||||
|
||||
export const splashConfig = {
|
||||
// 应用信息
|
||||
app: {
|
||||
name: '企业办公',
|
||||
nameEn: 'Enterprise Office',
|
||||
version: '1.0.0',
|
||||
logo: '🏢'
|
||||
},
|
||||
|
||||
// 启动画面设置
|
||||
display: {
|
||||
// 最小显示时间(毫秒)
|
||||
minDuration: 2000,
|
||||
// 最大显示时间(毫秒)
|
||||
maxDuration: 5000,
|
||||
// 是否在热启动时显示
|
||||
showOnWarmStart: false,
|
||||
// 是否显示版本信息
|
||||
showVersion: true
|
||||
},
|
||||
|
||||
// 加载步骤配置
|
||||
loadingSteps: [
|
||||
{
|
||||
text: '正在初始化...',
|
||||
duration: 800,
|
||||
action: 'init',
|
||||
icon: '⚙️'
|
||||
},
|
||||
{
|
||||
text: '加载用户数据...',
|
||||
duration: 1000,
|
||||
action: 'loadUserData',
|
||||
icon: '👤'
|
||||
},
|
||||
{
|
||||
text: '同步工作数据...',
|
||||
duration: 800,
|
||||
action: 'syncWorkData',
|
||||
icon: '📊'
|
||||
},
|
||||
{
|
||||
text: '准备就绪...',
|
||||
duration: 600,
|
||||
action: 'ready',
|
||||
icon: '✅'
|
||||
}
|
||||
],
|
||||
|
||||
// 动画配置
|
||||
animation: {
|
||||
// 背景动画持续时间
|
||||
backgroundDuration: 6000,
|
||||
// Logo动画延迟
|
||||
logoDelay: 0,
|
||||
// 加载动画延迟
|
||||
loadingDelay: 500,
|
||||
// 版本信息延迟
|
||||
versionDelay: 1000
|
||||
},
|
||||
|
||||
// 主题配置
|
||||
theme: {
|
||||
// 背景渐变
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
// 主色调
|
||||
primaryColor: '#667eea',
|
||||
// 文字颜色
|
||||
textColor: '#ffffff',
|
||||
// 次要文字颜色
|
||||
secondaryTextColor: 'rgba(255, 255, 255, 0.8)'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启动画面配置
|
||||
*/
|
||||
export function getSplashConfig() {
|
||||
return splashConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新启动画面配置
|
||||
*/
|
||||
export function updateSplashConfig(newConfig) {
|
||||
Object.assign(splashConfig, newConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用信息
|
||||
*/
|
||||
export function getAppInfo() {
|
||||
return splashConfig.app
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取显示设置
|
||||
*/
|
||||
export function getDisplaySettings() {
|
||||
return splashConfig.display
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加载步骤
|
||||
*/
|
||||
export function getLoadingSteps() {
|
||||
return splashConfig.loadingSteps
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动画配置
|
||||
*/
|
||||
export function getAnimationConfig() {
|
||||
return splashConfig.animation
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主题配置
|
||||
*/
|
||||
export function getThemeConfig() {
|
||||
return splashConfig.theme
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// 状态
|
||||
const userInfo = ref(null)
|
||||
const token = ref(null)
|
||||
const isLoggedIn = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const isAuthenticated = computed(() => {
|
||||
return isLoggedIn.value && token.value && userInfo.value
|
||||
})
|
||||
|
||||
// 登录
|
||||
const login = (userData, authToken) => {
|
||||
userInfo.value = userData
|
||||
token.value = authToken
|
||||
isLoggedIn.value = true
|
||||
|
||||
// 保存到本地存储
|
||||
uni.setStorageSync('userInfo', userData)
|
||||
uni.setStorageSync('token', authToken)
|
||||
uni.setStorageSync('isLoggedIn', true)
|
||||
}
|
||||
|
||||
// 登出
|
||||
const logout = () => {
|
||||
userInfo.value = null
|
||||
token.value = null
|
||||
isLoggedIn.value = false
|
||||
|
||||
// 清除本地存储
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('isLoggedIn')
|
||||
|
||||
console.log('用户已登出')
|
||||
}
|
||||
|
||||
// 初始化认证状态(从本地存储恢复)
|
||||
const initAuth = () => {
|
||||
try {
|
||||
const savedUserInfo = uni.getStorageSync('userInfo')
|
||||
const savedToken = uni.getStorageSync('token')
|
||||
const savedIsLoggedIn = uni.getStorageSync('isLoggedIn')
|
||||
|
||||
if (savedUserInfo && savedToken && savedIsLoggedIn) {
|
||||
userInfo.value = savedUserInfo
|
||||
token.value = savedToken
|
||||
isLoggedIn.value = savedIsLoggedIn
|
||||
console.log('从本地存储恢复用户状态')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化认证状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
const checkAuth = () => {
|
||||
return isAuthenticated.value
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
const updateUserInfo = (newUserInfo) => {
|
||||
userInfo.value = { ...userInfo.value, ...newUserInfo }
|
||||
uni.setStorageSync('userInfo', userInfo.value)
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
userInfo,
|
||||
token,
|
||||
isLoggedIn,
|
||||
|
||||
// 计算属性
|
||||
isAuthenticated,
|
||||
|
||||
// 方法
|
||||
login,
|
||||
logout,
|
||||
initAuth,
|
||||
checkAuth,
|
||||
updateUserInfo
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,276 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useTaskStore = defineStore('task', () => {
|
||||
// 状态
|
||||
const tasks = ref([])
|
||||
const currentFilter = ref('all') // all, pending, completed, overdue
|
||||
const currentSort = ref('dueDate') // dueDate, priority, created
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 计算属性
|
||||
const filteredTasks = computed(() => {
|
||||
let filtered = tasks.value
|
||||
|
||||
// 按状态过滤
|
||||
if (currentFilter.value === 'pending') {
|
||||
filtered = filtered.filter(task => !task.completed)
|
||||
} else if (currentFilter.value === 'completed') {
|
||||
filtered = filtered.filter(task => task.completed)
|
||||
} else if (currentFilter.value === 'overdue') {
|
||||
const now = new Date()
|
||||
filtered = filtered.filter(task =>
|
||||
!task.completed &&
|
||||
task.dueDate &&
|
||||
new Date(task.dueDate) < now
|
||||
)
|
||||
}
|
||||
|
||||
// 按关键词搜索
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
filtered = filtered.filter(task =>
|
||||
task.title.toLowerCase().includes(keyword) ||
|
||||
task.description.toLowerCase().includes(keyword) ||
|
||||
task.tags.some(tag => tag.toLowerCase().includes(keyword))
|
||||
)
|
||||
}
|
||||
|
||||
// 排序
|
||||
filtered.sort((a, b) => {
|
||||
switch (currentSort.value) {
|
||||
case 'priority':
|
||||
const priorityOrder = { high: 3, medium: 2, low: 1 }
|
||||
return (priorityOrder[b.priority] || 0) - (priorityOrder[a.priority] || 0)
|
||||
case 'created':
|
||||
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||
case 'dueDate':
|
||||
default:
|
||||
if (!a.dueDate && !b.dueDate) return 0
|
||||
if (!a.dueDate) return 1
|
||||
if (!b.dueDate) return -1
|
||||
return new Date(a.dueDate) - new Date(b.dueDate)
|
||||
}
|
||||
})
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
const taskStats = computed(() => {
|
||||
const total = tasks.value.length
|
||||
const completed = tasks.value.filter(task => task.completed).length
|
||||
const pending = total - completed
|
||||
const overdue = tasks.value.filter(task =>
|
||||
!task.completed &&
|
||||
task.dueDate &&
|
||||
new Date(task.dueDate) < new Date()
|
||||
).length
|
||||
|
||||
return {
|
||||
total,
|
||||
completed,
|
||||
pending,
|
||||
overdue,
|
||||
completionRate: total > 0 ? Math.round((completed / total) * 100) : 0
|
||||
}
|
||||
})
|
||||
|
||||
// 方法
|
||||
const addTask = (taskData) => {
|
||||
const newTask = {
|
||||
id: Date.now().toString(),
|
||||
title: taskData.title,
|
||||
description: taskData.description || '',
|
||||
priority: taskData.priority || 'medium',
|
||||
dueDate: taskData.dueDate || null,
|
||||
tags: taskData.tags || [],
|
||||
completed: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
tasks.value.unshift(newTask)
|
||||
saveToStorage()
|
||||
return newTask
|
||||
}
|
||||
|
||||
const updateTask = (id, updates) => {
|
||||
const index = tasks.value.findIndex(task => task.id === id)
|
||||
if (index !== -1) {
|
||||
tasks.value[index] = {
|
||||
...tasks.value[index],
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
saveToStorage()
|
||||
return tasks.value[index]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const deleteTask = (id) => {
|
||||
const index = tasks.value.findIndex(task => task.id === id)
|
||||
if (index !== -1) {
|
||||
tasks.value.splice(index, 1)
|
||||
saveToStorage()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const toggleTask = (id) => {
|
||||
const task = tasks.value.find(task => task.id === id)
|
||||
if (task) {
|
||||
task.completed = !task.completed
|
||||
task.updatedAt = new Date().toISOString()
|
||||
saveToStorage()
|
||||
return task
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const setFilter = (filter) => {
|
||||
currentFilter.value = filter
|
||||
}
|
||||
|
||||
const setSort = (sort) => {
|
||||
currentSort.value = sort
|
||||
}
|
||||
|
||||
const setSearchKeyword = (keyword) => {
|
||||
searchKeyword.value = keyword
|
||||
}
|
||||
|
||||
const clearCompleted = () => {
|
||||
tasks.value = tasks.value.filter(task => !task.completed)
|
||||
saveToStorage()
|
||||
}
|
||||
|
||||
const getTaskById = (id) => {
|
||||
return tasks.value.find(task => task.id === id)
|
||||
}
|
||||
|
||||
const getTasksByTag = (tag) => {
|
||||
return tasks.value.filter(task => task.tags.includes(tag))
|
||||
}
|
||||
|
||||
const getOverdueTasks = () => {
|
||||
const now = new Date()
|
||||
return tasks.value.filter(task =>
|
||||
!task.completed &&
|
||||
task.dueDate &&
|
||||
new Date(task.dueDate) < now
|
||||
)
|
||||
}
|
||||
|
||||
const getTodayTasks = () => {
|
||||
const today = new Date().toDateString()
|
||||
return tasks.value.filter(task =>
|
||||
!task.completed &&
|
||||
task.dueDate &&
|
||||
new Date(task.dueDate).toDateString() === today
|
||||
)
|
||||
}
|
||||
|
||||
// 本地存储
|
||||
const saveToStorage = () => {
|
||||
try {
|
||||
uni.setStorageSync('tasks', JSON.stringify(tasks.value))
|
||||
} catch (error) {
|
||||
console.error('保存任务数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadFromStorage = () => {
|
||||
try {
|
||||
const stored = uni.getStorageSync('tasks')
|
||||
if (stored) {
|
||||
tasks.value = JSON.parse(stored)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载任务数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化示例数据
|
||||
const initSampleData = () => {
|
||||
if (tasks.value.length === 0) {
|
||||
const sampleTasks = [
|
||||
{
|
||||
id: '1',
|
||||
title: '完成项目需求分析',
|
||||
description: '分析用户需求,制定详细的功能规格说明',
|
||||
priority: 'high',
|
||||
dueDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tags: ['工作', '项目'],
|
||||
completed: false,
|
||||
createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updatedAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: '准备会议材料',
|
||||
description: '准备下周团队会议的PPT和资料',
|
||||
priority: 'medium',
|
||||
dueDate: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tags: ['工作', '会议'],
|
||||
completed: false,
|
||||
createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updatedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: '购买生活用品',
|
||||
description: '去超市购买日用品和食材',
|
||||
priority: 'low',
|
||||
dueDate: null,
|
||||
tags: ['生活', '购物'],
|
||||
completed: true,
|
||||
createdAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updatedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: '学习新技术',
|
||||
description: '学习Vue 3和Pinia状态管理',
|
||||
priority: 'medium',
|
||||
dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tags: ['学习', '技术'],
|
||||
completed: false,
|
||||
createdAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updatedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
]
|
||||
tasks.value = sampleTasks
|
||||
saveToStorage()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
tasks,
|
||||
currentFilter,
|
||||
currentSort,
|
||||
searchKeyword,
|
||||
|
||||
// 计算属性
|
||||
filteredTasks,
|
||||
taskStats,
|
||||
|
||||
// 方法
|
||||
addTask,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
toggleTask,
|
||||
setFilter,
|
||||
setSort,
|
||||
setSearchKeyword,
|
||||
clearCompleted,
|
||||
getTaskById,
|
||||
getTasksByTag,
|
||||
getOverdueTasks,
|
||||
getTodayTasks,
|
||||
loadFromStorage,
|
||||
initSampleData
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,205 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
// 用户基本信息
|
||||
userInfo: {
|
||||
name: '张三',
|
||||
avatar: '/static/avatar/user.png',
|
||||
department: '技术部',
|
||||
employeeId: 'EMP001',
|
||||
position: '高级工程师',
|
||||
phone: '138****8888',
|
||||
email: 'zhangsan@company.com'
|
||||
},
|
||||
|
||||
// 登录状态
|
||||
token: '',
|
||||
isLoggedIn: true,
|
||||
|
||||
// 用户权限
|
||||
permissions: [
|
||||
'attendance:view',
|
||||
'attendance:checkin',
|
||||
'leave:apply',
|
||||
'leave:view',
|
||||
'reimbursement:apply',
|
||||
'reimbursement:view',
|
||||
'task:view',
|
||||
'task:create',
|
||||
'meeting:book',
|
||||
'customer:view',
|
||||
'file:view'
|
||||
],
|
||||
|
||||
// 用户设置
|
||||
settings: {
|
||||
theme: 'light', // light, dark, auto
|
||||
messageEnabled: true,
|
||||
nightMode: false,
|
||||
language: 'zh-CN'
|
||||
},
|
||||
|
||||
// 工作数据
|
||||
workData: {
|
||||
attendanceRate: 98,
|
||||
completedTasks: 15,
|
||||
pendingTasks: 3,
|
||||
pendingApproval: 2,
|
||||
pendingAmount: 2580,
|
||||
thisMonthLeave: 2,
|
||||
thisMonthOvertime: 8
|
||||
},
|
||||
|
||||
// 最近使用功能
|
||||
recentFunctions: [
|
||||
{ icon: 'calendar', color: '#FF6B6B', label: '请假', action: 'leave' },
|
||||
{ icon: 'rmb-circle', color: '#4ECDC4', label: '报销', action: 'reimbursement' },
|
||||
{ icon: 'clock', color: '#45B7D1', label: '打卡', action: 'checkin' }
|
||||
],
|
||||
|
||||
// 快捷操作配置
|
||||
quickActions: [
|
||||
{ icon: 'calendar', color: '#FF6B6B', label: '请假', action: 'leave' },
|
||||
{ icon: 'rmb-circle', color: '#4ECDC4', label: '报销', action: 'reimbursement' },
|
||||
{ icon: 'clock', color: '#45B7D1', label: '打卡', action: 'checkin' },
|
||||
{ icon: 'calendar', color: '#96CEB4', label: '会议', action: 'meeting' },
|
||||
{ icon: 'account', color: '#FECA57', label: '客户', action: 'customer' }
|
||||
]
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 获取用户显示名称
|
||||
getUserName: (state) => state.userInfo?.name || '未登录用户',
|
||||
|
||||
// 获取用户头像
|
||||
getUserAvatar: (state) => state.userInfo?.avatar || '/static/avatar/default.png',
|
||||
|
||||
// 获取用户部门
|
||||
getUserDepartment: (state) => state.userInfo?.department || '',
|
||||
|
||||
// 检查是否有特定权限
|
||||
hasPermission: (state) => (permission) => {
|
||||
return state.permissions.includes(permission)
|
||||
},
|
||||
|
||||
// 获取工作数据概览
|
||||
getWorkOverview: (state) => ({
|
||||
attendanceRate: state.workData.attendanceRate,
|
||||
completedTasks: state.workData.completedTasks,
|
||||
pendingTasks: state.workData.pendingTasks,
|
||||
pendingApproval: state.workData.pendingApproval,
|
||||
pendingAmount: state.workData.pendingAmount
|
||||
}),
|
||||
|
||||
// 获取当前主题
|
||||
getCurrentTheme: (state) => state.settings.theme,
|
||||
|
||||
// 获取消息设置
|
||||
getMessageSettings: (state) => ({
|
||||
enabled: state.settings.messageEnabled,
|
||||
nightMode: state.settings.nightMode
|
||||
})
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 设置用户信息
|
||||
setUserInfo(info) {
|
||||
this.userInfo = { ...this.userInfo, ...info }
|
||||
this.isLoggedIn = true
|
||||
},
|
||||
|
||||
// 设置token
|
||||
setToken(token) {
|
||||
this.token = token
|
||||
},
|
||||
|
||||
// 更新用户设置
|
||||
updateSettings(settings) {
|
||||
this.settings = { ...this.settings, ...settings }
|
||||
},
|
||||
|
||||
// 更新工作数据
|
||||
updateWorkData(data) {
|
||||
this.workData = { ...this.workData, ...data }
|
||||
},
|
||||
|
||||
// 添加最近使用功能
|
||||
addRecentFunction(func) {
|
||||
const existingIndex = this.recentFunctions.findIndex(item => item.action === func.action)
|
||||
if (existingIndex > -1) {
|
||||
this.recentFunctions.splice(existingIndex, 1)
|
||||
}
|
||||
this.recentFunctions.unshift(func)
|
||||
if (this.recentFunctions.length > 5) {
|
||||
this.recentFunctions.pop()
|
||||
}
|
||||
},
|
||||
|
||||
// 更新快捷操作
|
||||
updateQuickActions(actions) {
|
||||
this.quickActions = actions
|
||||
},
|
||||
|
||||
// 添加权限
|
||||
addPermission(permission) {
|
||||
if (!this.permissions.includes(permission)) {
|
||||
this.permissions.push(permission)
|
||||
}
|
||||
},
|
||||
|
||||
// 移除权限
|
||||
removePermission(permission) {
|
||||
const index = this.permissions.indexOf(permission)
|
||||
if (index > -1) {
|
||||
this.permissions.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
// 登出
|
||||
logout() {
|
||||
this.userInfo = {
|
||||
name: '',
|
||||
avatar: '',
|
||||
department: '',
|
||||
employeeId: '',
|
||||
position: '',
|
||||
phone: '',
|
||||
email: ''
|
||||
}
|
||||
this.token = ''
|
||||
this.isLoggedIn = false
|
||||
this.permissions = []
|
||||
this.workData = {
|
||||
attendanceRate: 0,
|
||||
completedTasks: 0,
|
||||
pendingTasks: 0,
|
||||
pendingApproval: 0,
|
||||
pendingAmount: 0,
|
||||
thisMonthLeave: 0,
|
||||
thisMonthOvertime: 0
|
||||
}
|
||||
this.recentFunctions = []
|
||||
this.quickActions = []
|
||||
},
|
||||
|
||||
// 重置设置
|
||||
resetSettings() {
|
||||
this.settings = {
|
||||
theme: 'light',
|
||||
messageEnabled: true,
|
||||
nightMode: false,
|
||||
language: 'zh-CN'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
persist: {
|
||||
key: 'user-store',
|
||||
storage: {
|
||||
getItem: (key) => uni.getStorageSync(key),
|
||||
setItem: (key, value) => uni.setStorageSync(key, value),
|
||||
removeItem: (key) => uni.removeStorageSync(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Emoji处理工具函数
|
||||
*/
|
||||
|
||||
// 将emoji代码转换为Unicode字符
|
||||
export function parseEmoji(text) {
|
||||
// 这里可以添加具体的emoji解析逻辑
|
||||
// 例如将 :smile: 转换为 😊
|
||||
return text;
|
||||
}
|
||||
|
||||
// 将Unicode字符转换为emoji代码
|
||||
export function encodeEmoji(text) {
|
||||
// 这里可以添加具体的emoji编码逻辑
|
||||
return text;
|
||||
}
|
||||
|
||||
// 检查文本中是否包含emoji
|
||||
export function containsEmoji(text) {
|
||||
// 基本的emoji Unicode范围检查
|
||||
const emojiRegex = /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu;
|
||||
return emojiRegex.test(text);
|
||||
}
|
||||
|
||||
export default {
|
||||
parseEmoji,
|
||||
encodeEmoji,
|
||||
containsEmoji
|
||||
};
|
||||
@@ -0,0 +1,165 @@
|
||||
// 常用emoji表情数据
|
||||
export const emojis = [
|
||||
// 笑脸和情感
|
||||
{ id: 'smile', unicode: '😊', name: '微笑' },
|
||||
{ id: 'laugh', unicode: '😄', name: '大笑' },
|
||||
{ id: 'grin', unicode: '😁', name: ' grin' },
|
||||
{ id: 'tears_of_joy', unicode: '😂', name: '笑哭' },
|
||||
{ id: 'wink', unicode: '😉', name: '眨眼' },
|
||||
{ id: 'blush', unicode: '😊', name: '脸红' },
|
||||
{ id: 'innocent', unicode: '😇', name: '天使' },
|
||||
{ id: 'heart_eyes', unicode: '😍', name: '花痴' },
|
||||
{ id: 'kissing_heart', unicode: '😘', name: '飞吻' },
|
||||
{ id: 'kissing_closed_eyes', unicode: '😚', name: '闭眼亲亲' },
|
||||
{ id: 'yum', unicode: '😋', name: '好吃' },
|
||||
{ id: 'stuck_out_tongue_winking_eye', unicode: '😜', name: '吐舌头眨眼' },
|
||||
{ id: 'sunglasses', unicode: '😎', name: '酷' },
|
||||
{ id: 'smirk', unicode: '😏', name: '得意' },
|
||||
{ id: 'expressionless', unicode: '😑', name: '面无表情' },
|
||||
{ id: 'neutral_face', unicode: '😐', name: '中性' },
|
||||
|
||||
// 手势和身体部位
|
||||
{ id: 'thumbsup', unicode: '👍', name: '赞' },
|
||||
{ id: 'thumbsdown', unicode: '👎', name: '踩' },
|
||||
{ id: 'ok_hand', unicode: '👌', name: 'OK' },
|
||||
{ id: 'fist', unicode: '✊', name: '拳头' },
|
||||
{ id: 'v', unicode: '✌️', name: '胜利' },
|
||||
{ id: 'wave', unicode: '👋', name: '挥手' },
|
||||
{ id: 'clap', unicode: '👏', name: '鼓掌' },
|
||||
{ id: 'muscle', unicode: '💪', name: '肌肉' },
|
||||
{ id: 'pray', unicode: '🙏', name: '祈祷' },
|
||||
|
||||
// 动物和自然
|
||||
{ id: 'dog', unicode: '🐶', name: '狗' },
|
||||
{ id: 'cat', unicode: '🐱', name: '猫' },
|
||||
{ id: 'pig', unicode: '🐷', name: '猪' },
|
||||
{ id: 'rabbit', unicode: '🐰', name: '兔子' },
|
||||
{ id: 'koala', unicode: '🐨', name: '考拉' },
|
||||
{ id: 'tiger', unicode: '🐯', name: '老虎' },
|
||||
{ id: 'horse', unicode: '🐴', name: '马' },
|
||||
{ id: 'cow', unicode: '🐮', name: '牛' },
|
||||
{ id: 'panda_face', unicode: '🐼', name: '熊猫' },
|
||||
{ id: 'pig_nose', unicode: '🐽', name: '猪鼻子' },
|
||||
{ id: 'feet', unicode: '🐾', name: '爪子' },
|
||||
{ id: 'turtle', unicode: '🐢', name: '乌龟' },
|
||||
{ id: 'hatching_chick', unicode: '🐣', name: '孵化' },
|
||||
{ id: 'baby_chick', unicode: '🐤', name: '小鸡' },
|
||||
{ id: 'hatched_chick', unicode: '🐥', name: '雏鸡' },
|
||||
{ id: 'bird', unicode: '🐦', name: '鸟' },
|
||||
|
||||
// 食物和饮料
|
||||
{ id: 'grapes', unicode: '🍇', name: '葡萄' },
|
||||
{ id: 'melon', unicode: '🍈', name: '甜瓜' },
|
||||
{ id: 'watermelon', unicode: '🍉', name: '西瓜' },
|
||||
{ id: 'tangerine', unicode: '🍊', name: '橘子' },
|
||||
{ id: 'lemon', unicode: '🍋', name: '柠檬' },
|
||||
{ id: 'banana', unicode: '🍌', name: '香蕉' },
|
||||
{ id: 'pineapple', unicode: '🍍', name: '菠萝' },
|
||||
{ id: 'apple', unicode: '🍎', name: '苹果' },
|
||||
{ id: 'green_apple', unicode: '🍏', name: '青苹果' },
|
||||
{ id: 'cherries', unicode: '🍒', name: '樱桃' },
|
||||
{ id: 'strawberry', unicode: '🍓', name: '草莓' },
|
||||
{ id: 'hamburger', unicode: '🍔', name: '汉堡' },
|
||||
{ id: 'pizza', unicode: '🍕', name: '披萨' },
|
||||
{ id: 'meat_on_bone', unicode: '🍖', name: '排骨' },
|
||||
{ id: 'poultry_leg', unicode: '🍗', name: '鸡腿' },
|
||||
{ id: 'rice_cracker', unicode: '🍘', name: '米饼' },
|
||||
|
||||
// 活动和运动
|
||||
{ id: 'soccer', unicode: '⚽', name: '足球' },
|
||||
{ id: 'basketball', unicode: '🏀', name: '篮球' },
|
||||
{ id: 'football', unicode: '🏈', name: '橄榄球' },
|
||||
{ id: 'baseball', unicode: '⚾', name: '棒球' },
|
||||
{ id: 'tennis', unicode: '🎾', name: '网球' },
|
||||
{ id: 'golf', unicode: '⛳', name: '高尔夫' },
|
||||
{ id: 'ski', unicode: '🎿', name: '滑雪' },
|
||||
{ id: 'snowboarder', unicode: '🏂', name: '滑雪板' },
|
||||
{ id: 'swimmer', unicode: '🏊', name: '游泳' },
|
||||
{ id: 'surfer', unicode: '🏄', name: '冲浪' },
|
||||
{ id: 'cyclist', unicode: '🚴', name: '骑车' },
|
||||
{ id: 'runner', unicode: '🏃', name: '跑步' },
|
||||
{ id: 'dancer', unicode: '💃', name: '跳舞' },
|
||||
{ id: 'guitar', unicode: '🎸', name: '吉他' },
|
||||
{ id: 'musical_keyboard', unicode: '🎹', name: '键盘' },
|
||||
{ id: 'violin', unicode: '🎻', name: '小提琴' },
|
||||
|
||||
// 旅行和地点
|
||||
{ id: 'rocket', unicode: '🚀', name: '火箭' },
|
||||
{ id: 'helicopter', unicode: '🚁', name: '直升机' },
|
||||
{ id: 'steam_locomotive', unicode: '🚂', name: '火车头' },
|
||||
{ id: 'railway_car', unicode: '🚃', name: '车厢' },
|
||||
{ id: 'bullettrain_side', unicode: '🚄', name: '高铁' },
|
||||
{ id: 'bullettrain_front', unicode: '🚅', name: '子弹头' },
|
||||
{ id: 'train2', unicode: '🚆', name: '火车' },
|
||||
{ id: 'metro', unicode: '🚇', name: '地铁' },
|
||||
{ id: 'light_rail', unicode: '🚈', name: '轻轨' },
|
||||
{ id: 'station', unicode: '🚉', name: '车站' },
|
||||
{ id: 'tram', unicode: '🚊', name: '电车' },
|
||||
{ id: 'bus', unicode: '🚌', name: '公交车' },
|
||||
{ id: 'blue_car', unicode: '🚙', name: '汽车' },
|
||||
{ id: 'car', unicode: '🚗', name: '轿车' },
|
||||
{ id: 'taxi', unicode: '🚕', name: '出租车' },
|
||||
{ id: 'truck', unicode: '🚚', name: '卡车' },
|
||||
|
||||
// 符号和标志
|
||||
{ id: 'heart', unicode: '❤️', name: '爱心' },
|
||||
{ id: 'broken_heart', unicode: '💔', name: '心碎' },
|
||||
{ id: 'heartpulse', unicode: '💗', name: '心动' },
|
||||
{ id: 'sparkling_heart', unicode: '💖', name: '闪心' },
|
||||
{ id: 'cupid', unicode: '💘', name: '丘比特' },
|
||||
{ id: 'gift_heart', unicode: '💝', name: '礼盒心' },
|
||||
{ id: 'heart_decoration', unicode: '💟', name: '心装饰' },
|
||||
{ id: 'purple_heart', unicode: '💜', name: '紫心' },
|
||||
{ id: 'yellow_heart', unicode: '💛', name: '黄心' },
|
||||
{ id: 'green_heart', unicode: '💚', name: '绿心' },
|
||||
{ id: 'blue_heart', unicode: '💙', name: '蓝心' },
|
||||
{ id: 'star', unicode: '⭐', name: '星星' },
|
||||
{ id: 'sparkles', unicode: '✨', name: '闪亮' },
|
||||
{ id: 'zap', unicode: '⚡', name: '闪电' },
|
||||
{ id: 'fire', unicode: '🔥', name: '火' },
|
||||
{ id: 'boom', unicode: '💥', name: '爆炸' }
|
||||
];
|
||||
|
||||
// 将emoji按类别分组
|
||||
export const emojiCategories = [
|
||||
{
|
||||
id: 'people',
|
||||
name: '笑脸和情感',
|
||||
emojis: emojis.slice(0, 16)
|
||||
},
|
||||
{
|
||||
id: 'hands',
|
||||
name: '手势和身体',
|
||||
emojis: emojis.slice(16, 25)
|
||||
},
|
||||
{
|
||||
id: 'animals',
|
||||
name: '动物和自然',
|
||||
emojis: emojis.slice(25, 41)
|
||||
},
|
||||
{
|
||||
id: 'food',
|
||||
name: '食物和饮料',
|
||||
emojis: emojis.slice(41, 57)
|
||||
},
|
||||
{
|
||||
id: 'activity',
|
||||
name: '活动和运动',
|
||||
emojis: emojis.slice(57, 73)
|
||||
},
|
||||
{
|
||||
id: 'travel',
|
||||
name: '旅行和地点',
|
||||
emojis: emojis.slice(73, 89)
|
||||
},
|
||||
{
|
||||
id: 'symbols',
|
||||
name: '符号和标志',
|
||||
emojis: emojis.slice(89, 105)
|
||||
}
|
||||
];
|
||||
|
||||
export default {
|
||||
emojis,
|
||||
emojiCategories
|
||||
};
|
||||
@@ -0,0 +1,355 @@
|
||||
/**
|
||||
* 通用工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param {Date|string|number} date 时间
|
||||
* @param {string} format 格式 'YYYY-MM-DD HH:mm:ss'
|
||||
*/
|
||||
export function formatTime(date, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
if (!date) return ''
|
||||
|
||||
const d = new Date(date)
|
||||
if (isNaN(d.getTime())) return ''
|
||||
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hours = String(d.getHours()).padStart(2, '0')
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0')
|
||||
|
||||
return format
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化相对时间
|
||||
* @param {Date|string|number} date 时间
|
||||
*/
|
||||
export function formatRelativeTime(date) {
|
||||
if (!date) return ''
|
||||
|
||||
const now = new Date()
|
||||
const target = new Date(date)
|
||||
const diff = now.getTime() - target.getTime()
|
||||
|
||||
const minute = 60 * 1000
|
||||
const hour = 60 * minute
|
||||
const day = 24 * hour
|
||||
const week = 7 * day
|
||||
const month = 30 * day
|
||||
|
||||
if (diff < minute) {
|
||||
return '刚刚'
|
||||
} else if (diff < hour) {
|
||||
return `${Math.floor(diff / minute)}分钟前`
|
||||
} else if (diff < day) {
|
||||
return `${Math.floor(diff / hour)}小时前`
|
||||
} else if (diff < week) {
|
||||
return `${Math.floor(diff / day)}天前`
|
||||
} else if (diff < month) {
|
||||
return `${Math.floor(diff / week)}周前`
|
||||
} else {
|
||||
return formatTime(target, 'YYYY-MM-DD')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化金额
|
||||
* @param {number} amount 金额
|
||||
* @param {string} currency 货币符号
|
||||
*/
|
||||
export function formatMoney(amount, currency = '¥') {
|
||||
if (amount === null || amount === undefined || isNaN(amount)) return '0'
|
||||
return `${currency}${Number(amount).toLocaleString()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param {number} bytes 字节数
|
||||
*/
|
||||
export function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B'
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param {Function} func 要防抖的函数
|
||||
* @param {number} delay 延迟时间
|
||||
*/
|
||||
export function debounce(func, delay = 300) {
|
||||
let timeoutId
|
||||
return function (...args) {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(() => func.apply(this, args), delay)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param {Function} func 要节流的函数
|
||||
* @param {number} delay 延迟时间
|
||||
*/
|
||||
export function throttle(func, delay = 300) {
|
||||
let lastCall = 0
|
||||
return function (...args) {
|
||||
const now = Date.now()
|
||||
if (now - lastCall >= delay) {
|
||||
lastCall = now
|
||||
return func.apply(this, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝
|
||||
* @param {any} obj 要拷贝的对象
|
||||
*/
|
||||
export function deepClone(obj) {
|
||||
if (obj === null || typeof obj !== 'object') return obj
|
||||
if (obj instanceof Date) return new Date(obj.getTime())
|
||||
if (obj instanceof Array) return obj.map(item => deepClone(item))
|
||||
if (typeof obj === 'object') {
|
||||
const clonedObj = {}
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
clonedObj[key] = deepClone(obj[key])
|
||||
}
|
||||
}
|
||||
return clonedObj
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
*/
|
||||
export function generateId() {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证手机号
|
||||
* @param {string} phone 手机号
|
||||
*/
|
||||
export function validatePhone(phone) {
|
||||
const reg = /^1[3-9]\d{9}$/
|
||||
return reg.test(phone)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱
|
||||
* @param {string} email 邮箱
|
||||
*/
|
||||
export function validateEmail(email) {
|
||||
const reg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return reg.test(email)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证身份证号
|
||||
* @param {string} idCard 身份证号
|
||||
*/
|
||||
export function validateIdCard(idCard) {
|
||||
const reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
|
||||
return reg.test(idCard)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取URL参数
|
||||
* @param {string} name 参数名
|
||||
* @param {string} url URL地址
|
||||
*/
|
||||
export function getUrlParam(name, url = window.location.href) {
|
||||
const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)')
|
||||
const r = url.match(reg)
|
||||
if (r != null) return decodeURIComponent(r[2])
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储到本地
|
||||
* @param {string} key 键
|
||||
* @param {any} value 值
|
||||
*/
|
||||
export function setStorage(key, value) {
|
||||
try {
|
||||
uni.setStorageSync(key, JSON.stringify(value))
|
||||
} catch (error) {
|
||||
console.error('存储失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地获取
|
||||
* @param {string} key 键
|
||||
* @param {any} defaultValue 默认值
|
||||
*/
|
||||
export function getStorage(key, defaultValue = null) {
|
||||
try {
|
||||
const value = uni.getStorageSync(key)
|
||||
return value ? JSON.parse(value) : defaultValue
|
||||
} catch (error) {
|
||||
console.error('获取存储失败:', error)
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除本地存储
|
||||
* @param {string} key 键
|
||||
*/
|
||||
export function removeStorage(key) {
|
||||
try {
|
||||
uni.removeStorageSync(key)
|
||||
} catch (error) {
|
||||
console.error('删除存储失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示加载提示
|
||||
* @param {string} title 提示文字
|
||||
*/
|
||||
export function showLoading(title = '加载中...') {
|
||||
uni.showLoading({
|
||||
title,
|
||||
mask: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏加载提示
|
||||
*/
|
||||
export function hideLoading() {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示成功提示
|
||||
* @param {string} title 提示文字
|
||||
*/
|
||||
export function showSuccess(title) {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误提示
|
||||
* @param {string} title 提示文字
|
||||
*/
|
||||
export function showError(title) {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示普通提示
|
||||
* @param {string} title 提示文字
|
||||
*/
|
||||
export function showToast(title) {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示确认对话框
|
||||
* @param {string} content 内容
|
||||
* @param {string} title 标题
|
||||
*/
|
||||
export function showConfirm(content, title = '提示') {
|
||||
return new Promise((resolve) => {
|
||||
uni.showModal({
|
||||
title,
|
||||
content,
|
||||
success: (res) => {
|
||||
resolve(res.confirm)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面跳转
|
||||
* @param {string} url 页面路径
|
||||
* @param {object} params 参数
|
||||
*/
|
||||
export function navigateTo(url, params = {}) {
|
||||
const query = Object.keys(params).map(key => `${key}=${encodeURIComponent(params[key])}`).join('&')
|
||||
const fullUrl = query ? `${url}?${query}` : url
|
||||
|
||||
uni.navigateTo({
|
||||
url: fullUrl,
|
||||
fail: (error) => {
|
||||
console.error('页面跳转失败:', error)
|
||||
showError('页面跳转失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面重定向
|
||||
* @param {string} url 页面路径
|
||||
* @param {object} params 参数
|
||||
*/
|
||||
export function redirectTo(url, params = {}) {
|
||||
const query = Object.keys(params).map(key => `${key}=${encodeURIComponent(params[key])}`).join('&')
|
||||
const fullUrl = query ? `${url}?${query}` : url
|
||||
|
||||
uni.redirectTo({
|
||||
url: fullUrl,
|
||||
fail: (error) => {
|
||||
console.error('页面重定向失败:', error)
|
||||
showError('页面重定向失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换Tab页面
|
||||
* @param {string} url 页面路径
|
||||
*/
|
||||
export function switchTab(url) {
|
||||
uni.switchTab({
|
||||
url,
|
||||
fail: (error) => {
|
||||
console.error('Tab切换失败:', error)
|
||||
showError('页面切换失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
* @param {number} delta 返回层数
|
||||
*/
|
||||
export function navigateBack(delta = 1) {
|
||||
uni.navigateBack({
|
||||
delta,
|
||||
fail: (error) => {
|
||||
console.error('返回失败:', error)
|
||||
showError('返回失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { useAuthStore } from '../store/authStore.js'
|
||||
|
||||
// 需要登录的页面路径
|
||||
const authRequiredPages = [
|
||||
'/pages/index/index',
|
||||
'/pages/profile/profile',
|
||||
'/pages/function/function',
|
||||
'/pages/message/message',
|
||||
'/pages/tasks/index'
|
||||
]
|
||||
|
||||
// 登录页面路径
|
||||
const loginPage = '/pages/login/index'
|
||||
|
||||
// 首页路径
|
||||
const homePage = '/pages/index/index'
|
||||
|
||||
/**
|
||||
* 检查当前页面是否需要登录
|
||||
* @param {string} currentPath 当前页面路径
|
||||
* @returns {boolean} 是否需要登录
|
||||
*/
|
||||
export function isAuthRequired(currentPath) {
|
||||
return authRequiredPages.includes(currentPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由守卫 - 检查登录状态
|
||||
* @param {string} toPath 目标页面路径
|
||||
* @returns {boolean} 是否允许访问
|
||||
*/
|
||||
export function checkAuthGuard(toPath) {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 如果是登录页面,直接允许访问
|
||||
if (toPath === loginPage) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否需要登录
|
||||
if (isAuthRequired(toPath)) {
|
||||
// 检查是否已登录
|
||||
if (!authStore.isAuthenticated) {
|
||||
// 跳转到登录页面
|
||||
uni.reLaunch({
|
||||
url: loginPage
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化路由守卫
|
||||
*/
|
||||
export function initRouteGuard() {
|
||||
// 监听页面跳转
|
||||
uni.addInterceptor('navigateTo', {
|
||||
invoke(args) {
|
||||
const toPath = args.url.split('?')[0] // 移除查询参数
|
||||
if (!checkAuthGuard(toPath)) {
|
||||
return false // 阻止跳转
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
uni.addInterceptor('redirectTo', {
|
||||
invoke(args) {
|
||||
const toPath = args.url.split('?')[0]
|
||||
if (!checkAuthGuard(toPath)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
uni.addInterceptor('switchTab', {
|
||||
invoke(args) {
|
||||
const toPath = args.url.split('?')[0]
|
||||
if (!checkAuthGuard(toPath)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
uni.addInterceptor('reLaunch', {
|
||||
invoke(args) {
|
||||
const toPath = args.url.split('?')[0]
|
||||
if (!checkAuthGuard(toPath)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功后跳转
|
||||
* @param {string} redirectPath 重定向路径,默认为首页
|
||||
*/
|
||||
export function redirectAfterLogin(redirectPath = homePage) {
|
||||
uni.reLaunch({
|
||||
url: redirectPath
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出后跳转
|
||||
*/
|
||||
export function redirectAfterLogout() {
|
||||
uni.reLaunch({
|
||||
url: loginPage
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 启动画面管理器
|
||||
*/
|
||||
|
||||
// 启动画面配置
|
||||
export const splashConfig = {
|
||||
// 最小显示时间(毫秒)
|
||||
minDisplayTime: 2000,
|
||||
// 最大显示时间(毫秒)
|
||||
maxDisplayTime: 5000,
|
||||
// 是否已显示过启动画面
|
||||
hasShown: false,
|
||||
// 启动时间戳
|
||||
startTime: null
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化启动画面
|
||||
*/
|
||||
export function initSplash() {
|
||||
splashConfig.startTime = Date.now()
|
||||
splashConfig.hasShown = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该显示启动画面
|
||||
*/
|
||||
export function shouldShowSplash() {
|
||||
// 如果已经显示过,不再显示
|
||||
if (splashConfig.hasShown) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否在冷启动状态
|
||||
const isColdStart = !getApp().globalData?.isWarmStart
|
||||
|
||||
return isColdStart
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记启动画面已显示
|
||||
*/
|
||||
export function markSplashShown() {
|
||||
splashConfig.hasShown = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启动画面显示时长
|
||||
*/
|
||||
export function getSplashDuration() {
|
||||
if (!splashConfig.startTime) {
|
||||
return splashConfig.minDisplayTime
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - splashConfig.startTime
|
||||
return Math.max(splashConfig.minDisplayTime - elapsed, 500)
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动画面加载步骤配置
|
||||
*/
|
||||
export const loadingSteps = [
|
||||
{
|
||||
text: '正在初始化...',
|
||||
duration: 800,
|
||||
action: 'init'
|
||||
},
|
||||
{
|
||||
text: '加载用户数据...',
|
||||
duration: 1000,
|
||||
action: 'loadUserData'
|
||||
},
|
||||
{
|
||||
text: '同步工作数据...',
|
||||
duration: 800,
|
||||
action: 'syncWorkData'
|
||||
},
|
||||
{
|
||||
text: '准备就绪...',
|
||||
duration: 600,
|
||||
action: 'ready'
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 执行启动步骤
|
||||
*/
|
||||
export async function executeLoadingStep(step) {
|
||||
try {
|
||||
switch (step.action) {
|
||||
case 'init':
|
||||
await initializeApp()
|
||||
break
|
||||
case 'loadUserData':
|
||||
await loadUserData()
|
||||
break
|
||||
case 'syncWorkData':
|
||||
await syncWorkData()
|
||||
break
|
||||
case 'ready':
|
||||
await prepareReady()
|
||||
break
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`启动步骤 ${step.action} 执行失败:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化应用
|
||||
*/
|
||||
async function initializeApp() {
|
||||
// 模拟初始化过程
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
|
||||
// 这里可以添加实际的初始化逻辑
|
||||
// 例如:检查网络状态、初始化全局配置等
|
||||
console.log('应用初始化完成')
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载用户数据
|
||||
*/
|
||||
async function loadUserData() {
|
||||
// 模拟加载用户数据
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
|
||||
// 这里可以添加实际的用户数据加载逻辑
|
||||
// 例如:从本地存储加载用户信息、验证登录状态等
|
||||
console.log('用户数据加载完成')
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步工作数据
|
||||
*/
|
||||
async function syncWorkData() {
|
||||
// 模拟同步工作数据
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
|
||||
// 这里可以添加实际的工作数据同步逻辑
|
||||
// 例如:同步待办事项、考勤数据、消息等
|
||||
console.log('工作数据同步完成')
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备就绪
|
||||
*/
|
||||
async function prepareReady() {
|
||||
// 模拟准备就绪过程
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
|
||||
// 这里可以添加最后的准备逻辑
|
||||
// 例如:预加载关键数据、设置全局状态等
|
||||
console.log('应用准备就绪')
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动画面完成回调
|
||||
*/
|
||||
export function onSplashComplete() {
|
||||
// 标记应用为热启动
|
||||
if (getApp().globalData) {
|
||||
getApp().globalData.isWarmStart = true
|
||||
}
|
||||
|
||||
// 标记启动画面已显示
|
||||
markSplashShown()
|
||||
|
||||
console.log('启动画面完成')
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置启动状态(用于测试)
|
||||
*/
|
||||
export function resetSplashState() {
|
||||
splashConfig.hasShown = false
|
||||
splashConfig.startTime = null
|
||||
if (getApp().globalData) {
|
||||
getApp().globalData.isWarmStart = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@font-face {
|
||||
font-family: "iconfont";
|
||||
src: url('../fonts/iconfont.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 36rpx;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-back::before {
|
||||
content: "\e601";
|
||||
}
|
||||
|
||||
.icon-more::before {
|
||||
content: "\e602";
|
||||
}
|
||||
|
||||
.icon-emoji::before {
|
||||
content: "\e603";
|
||||
}
|
||||
|
||||
.icon-add::before {
|
||||
content: "\e604";
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,258 @@
|
||||
// 蓝白亮色主题 (Light Theme)
|
||||
:root,
|
||||
.theme-light {
|
||||
--primary-color: #2B7CE9;
|
||||
--primary-dark: #1E5F99;
|
||||
--primary-light: #5A9EF0;
|
||||
--primary-gradient: linear-gradient(135deg, #2B7CE9 0%, #5A9EF0 100%);
|
||||
--background: #F8FAFC;
|
||||
--background-light: #FFFFFF;
|
||||
--surface: #fff;
|
||||
--surface-hover: #F8FAFC;
|
||||
--border: #E2E8F0;
|
||||
--border-light: #F1F5F9;
|
||||
--accent: #3498db;
|
||||
--text-color: #1A202C;
|
||||
--text-secondary: #718096;
|
||||
--text-muted: #A0AEC0;
|
||||
--title-color: #1A202C;
|
||||
--subtitle-color: #718096;
|
||||
--icon-color: #2B7CE9;
|
||||
--gray: #E2E8F0;
|
||||
--gray-dark: #A0AEC0;
|
||||
--gray-light: #F7FAFC;
|
||||
--gray-lighter: #F8FAFC;
|
||||
--shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.08);
|
||||
--shadow-md: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* 功能色 */
|
||||
--blue: #3182CE;
|
||||
--blue-light: #63B3ED;
|
||||
--red: #E53E3E;
|
||||
--red-light: #FC8181;
|
||||
--green: #38A169;
|
||||
--green-light: #68D391;
|
||||
--yellow: #D69E2E;
|
||||
--yellow-light: #F6E05E;
|
||||
--purple: #805AD5;
|
||||
--purple-light: #B794F6;
|
||||
--orange: #DD6B20;
|
||||
--orange-light: #FBD38D;
|
||||
--pink: #D53F8C;
|
||||
--pink-light: #F687B3;
|
||||
--cyan: #319795;
|
||||
--cyan-light: #4FD1C7;
|
||||
|
||||
/* 状态色 */
|
||||
--success: #38A169;
|
||||
--success-light: #C6F6D5;
|
||||
--warning: #D69E2E;
|
||||
--warning-light: #FEFCBF;
|
||||
--error: #E53E3E;
|
||||
--error-light: #FED7D7;
|
||||
--info: #3182CE;
|
||||
--info-light: #BEE3F8;
|
||||
|
||||
/* 组件色 */
|
||||
--white: #fff;
|
||||
--black: #000;
|
||||
--badge-bg: #E53E3E;
|
||||
--badge-text: #fff;
|
||||
--tab-active: #2B7CE9;
|
||||
--tab-inactive: #A0AEC0;
|
||||
--button-primary: #2B7CE9;
|
||||
--button-primary-hover: #1E5F99;
|
||||
--button-secondary: #E2E8F0;
|
||||
--button-secondary-hover: #CBD5E0;
|
||||
|
||||
/* 渐变 */
|
||||
--gradient-primary: linear-gradient(135deg, #2B7CE9 0%, #5A9EF0 100%);
|
||||
--gradient-success: linear-gradient(135deg, #38A169 0%, #68D391 100%);
|
||||
--gradient-warning: linear-gradient(135deg, #D69E2E 0%, #F6E05E 100%);
|
||||
--gradient-error: linear-gradient(135deg, #E53E3E 0%, #FC8181 100%);
|
||||
--gradient-surface: linear-gradient(180deg, #FFFFFF 0%, #F8FAFC 100%);
|
||||
}
|
||||
|
||||
// 黑暗主题 (Dark Theme)
|
||||
.theme-dark {
|
||||
--primary-color: #2B7CE9;
|
||||
--primary-dark: #113358;
|
||||
--background: #171B26;
|
||||
--surface: #23273A;
|
||||
--border: #283044;
|
||||
--accent: #317CD9;
|
||||
--text-color: #ececec;
|
||||
--text-secondary: #8D93A4;
|
||||
--title-color: #fff;
|
||||
--subtitle-color: #8D93A4;
|
||||
--icon-color: #5DA6FF;
|
||||
--gray: #232b3a;
|
||||
--shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.35);
|
||||
|
||||
/* 功能色 */
|
||||
--blue: #4FC3F7;
|
||||
--red: #FF648A;
|
||||
--green: #44D7B6;
|
||||
--yellow: #FFD76F;
|
||||
--gray-light: #202537;
|
||||
--white: #23273A;
|
||||
--badge-bg: #FF648A;
|
||||
--badge-text: #fff;
|
||||
--tab-active: #5DA6FF;
|
||||
--tab-inactive: #5f6a86;
|
||||
}
|
||||
|
||||
:root{
|
||||
--fa-width:1.25em;
|
||||
}
|
||||
|
||||
.fas{
|
||||
width: 1.25em !important;
|
||||
}
|
||||
|
||||
.top_bar{
|
||||
height: 176rpx !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.underline{
|
||||
border-bottom: 1rpx solid #efefef;
|
||||
}
|
||||
/* 统一的顶部导航样式 */
|
||||
.unified-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
background: var(--gradient-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
.unified-header .header-content {
|
||||
padding: 20rpx 30rpx;
|
||||
padding-top: calc(var(--status-bar-height) + 20rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.unified-header .header-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
color: var(--white);
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
|
||||
.unified-header .header-left,
|
||||
.unified-header .header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 80rpx;
|
||||
}
|
||||
|
||||
.unified-header .header-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.unified-header .header-icon {
|
||||
color: var(--white);
|
||||
font-size: 36rpx;
|
||||
padding: 10rpx;
|
||||
// border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.unified-header .header-icon:active {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.unified-header .badge {
|
||||
position: absolute;
|
||||
top: 5rpx !important;
|
||||
right: 30rpx !important;
|
||||
background-color: var(--error);
|
||||
color: var(--white);
|
||||
font-size: .3rem !important;
|
||||
padding: 6rpx 9rpx !important;
|
||||
// padding: 10rpx !important;
|
||||
border-radius: 50%;
|
||||
// min-width: 24rpx;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.unified-header {
|
||||
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.unified-header .header-content {
|
||||
padding-top: calc(var(--status-bar-height) + 20rpx + env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
/* 页面容器适配 */
|
||||
.page-container {
|
||||
padding-top: calc(var(--status-bar-height) + 88rpx);
|
||||
min-height: 100vh;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
/* 安全区域适配 */
|
||||
@supports (padding: max(0px)) {
|
||||
.page-container {
|
||||
padding-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
/* 统一的内容区域样式 */
|
||||
.unified-content {
|
||||
padding: 30rpx;
|
||||
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
min-height: calc(100vh - 120rpx);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.items-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: calc(var(--status-bar-height, 0rpx) + 88rpx);
|
||||
background-color: var(--surface);
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* 浏览器环境下的固定定位 */
|
||||
.attendance-container .chat-header:not(.top_bar .chat-header) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
padding-top: var(--status-bar-height);
|
||||
}
|
||||
|
||||
/* 移动设备下的导航栏样式 */
|
||||
.top_bar .chat-header {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# 字体文件目录
|
||||
|
||||
此目录用于存放项目中使用的字体文件,包括图标字体。
|
||||
|
||||
## 文件说明
|
||||
|
||||
- `iconfont.ttf` - 图标字体文件
|
||||
@@ -0,0 +1,14 @@
|
||||
#ifndef _EMOJI_FONT_H_
|
||||
#define _EMOJI_FONT_H_
|
||||
|
||||
// 这是一个示例文件,实际项目中需要替换为真实的字体文件
|
||||
// 在真实项目中,这个文件应该包含有效的TTF字体数据
|
||||
|
||||
const unsigned char iconfont_ttf[] = {
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
unsigned int iconfont_ttf_len = 16;
|
||||
|
||||
#endif
|
||||
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 482 B |
|
After Width: | Height: | Size: 440 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 958 B |
|
After Width: | Height: | Size: 920 B |
|
After Width: | Height: | Size: 836 B |
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
purge: ['./src/‌**/*.vue', './src/**‌/*.jsx', './src/**/*.tsx'],
|
||||
darkMode: false, // 或者 'media' 或 'class'
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||