first commit

This commit is contained in:
2025-10-27 23:13:08 +08:00
commit 476c5e7658
2968 changed files with 49547 additions and 0 deletions
+19
View File
@@ -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
+35
View File
@@ -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
+370
View File
@@ -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>
+166
View File
@@ -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
## 联系方式
如有问题或建议,请联系开发团队。
+224
View File
@@ -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. 网络检测
根据网络状态调整加载步骤和显示内容。
+20
View File
@@ -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>
+24
View File
@@ -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
}
}
+78
View File
@@ -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"
}
+682
View File
@@ -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
}
}
}
}
}
+39
View File
@@ -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"
}
}
+134
View File
@@ -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": {}
}
+429
View File
@@ -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>
File diff suppressed because it is too large Load Diff
+993
View File
@@ -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>
File diff suppressed because it is too large Load Diff
+927
View File
@@ -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>
+850
View File
@@ -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>
+475
View File
@@ -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>
+683
View File
@@ -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>
+589
View File
@@ -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>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+370
View File
@@ -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>
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+607
View File
@@ -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
}
+143
View File
@@ -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>
+124
View File
@@ -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>
+167
View File
@@ -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>
+137
View File
@@ -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>
+234
View File
@@ -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>
+132
View File
@@ -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>
+28
View File
@@ -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
}
+126
View File
@@ -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
}
+86
View File
@@ -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
}
})
+276
View File
@@ -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
}
})
+205
View File
@@ -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)
}
}
})
+29
View File
@@ -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
};
+165
View File
@@ -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
};
+355
View File
@@ -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('返回失败')
}
})
}
+114
View File
@@ -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
})
}
+183
View File
@@ -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
}
}
File diff suppressed because it is too large Load Diff
+9
View File
File diff suppressed because one or more lines are too long
+28
View File
@@ -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";
}
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+258
View File
@@ -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;
}
+7
View File
@@ -0,0 +1,7 @@
# 字体文件目录
此目录用于存放项目中使用的字体文件,包括图标字体。
## 文件说明
- `iconfont.ttf` - 图标字体文件
+14
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
module.exports = {
purge: ['./src/&zwnj;**/*.vue', './src/**&zwnj;/*.jsx', './src/**/*.tsx'],
darkMode: false, // 或者 'media' 或 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}