From b8c05d23aed72c00423bff8da052acf23c32942a Mon Sep 17 00:00:00 2001 From: Ls <2391972606@qq.com> Date: Sat, 28 Mar 2026 08:55:48 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 88 + src/Service/Service.ts | 15 + src/pages.json | 136 +- src/pages/dataAnalyze/Curve.vue | 710 +++++++ src/pages/dataAnalyze/baoganAnalyze.vue | 360 ++++ src/pages/dataAnalyze/grades.vue | 535 +++++ src/pages/dataAnalyze/paragraphAnalyze.vue | 18 + src/pages/dataAnalyze/timingAnalze.vue | 470 +++++ src/pages/index/index.vue | 62 +- src/pages/index/user.vue | 292 ++- src/pages/userFunc/analyze.vue | 457 ++--- src/pages/userFunc/project.vue | 591 +++++- src/pages/userFunc/projectList.vue | 176 +- src/pages/userFunc/segmentation.vue | 2 +- src/pages/userFunc/student.vue | 226 +- src/pages/userFunc/swiming.vue | 88 +- src/static/tab/01.png | Bin 5508 -> 10160 bytes src/static/tab/02.png | Bin 5737 -> 10760 bytes src/static/tab/03.png | Bin 8650 -> 10109 bytes src/static/tab/04.png | Bin 8925 -> 10964 bytes src/static/tab/05.png | Bin 6941 -> 0 bytes src/static/tab/06.png | Bin 7169 -> 0 bytes src/static/tab/07.png | Bin 6769 -> 0 bytes src/static/tab/08.png | Bin 6994 -> 0 bytes src/static/tab/09.png | Bin 6045 -> 0 bytes src/static/tab/10.png | Bin 6260 -> 0 bytes .../sl-table/DYNAMIC_SLOT_COMPATIBILITY.md | 101 + src/uni_modules/sl-table/changelog.md | 55 + .../components/sl-table/header/index.vue | 853 ++++++++ .../sl-table/components/sl-table/sl-table.vue | 1822 +++++++++++++++++ .../sl-table/examples/miniprogram-example.vue | 182 ++ .../sl-table/examples/vue2-example.vue | 303 +++ .../sl-table/examples/vue3-example.vue | 297 +++ src/uni_modules/sl-table/package.json | 86 + src/uni_modules/sl-table/readme.md | 441 ++++ src/uni_modules/sl-table/test/slot-test.vue | 161 ++ src/uni_modules/sl-table/utils/vue-compat.js | 139 ++ src/uni_modules/uni-calendar/changelog.md | 30 + .../components/uni-calendar/calendar.js | 544 +++++ .../components/uni-calendar/i18n/en.json | 12 + .../components/uni-calendar/i18n/index.js | 8 + .../components/uni-calendar/i18n/zh-Hans.json | 12 + .../components/uni-calendar/i18n/zh-Hant.json | 12 + .../uni-calendar/uni-calendar-item.vue | 187 ++ .../components/uni-calendar/uni-calendar.vue | 567 +++++ .../components/uni-calendar/util.js | 360 ++++ src/uni_modules/uni-calendar/package.json | 86 + src/uni_modules/uni-calendar/readme.md | 103 + 48 files changed, 9948 insertions(+), 639 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/pages/dataAnalyze/Curve.vue create mode 100644 src/pages/dataAnalyze/baoganAnalyze.vue create mode 100644 src/pages/dataAnalyze/grades.vue create mode 100644 src/pages/dataAnalyze/paragraphAnalyze.vue create mode 100644 src/pages/dataAnalyze/timingAnalze.vue delete mode 100644 src/static/tab/05.png delete mode 100644 src/static/tab/06.png delete mode 100644 src/static/tab/07.png delete mode 100644 src/static/tab/08.png delete mode 100644 src/static/tab/09.png delete mode 100644 src/static/tab/10.png create mode 100644 src/uni_modules/sl-table/DYNAMIC_SLOT_COMPATIBILITY.md create mode 100644 src/uni_modules/sl-table/changelog.md create mode 100644 src/uni_modules/sl-table/components/sl-table/header/index.vue create mode 100644 src/uni_modules/sl-table/components/sl-table/sl-table.vue create mode 100644 src/uni_modules/sl-table/examples/miniprogram-example.vue create mode 100644 src/uni_modules/sl-table/examples/vue2-example.vue create mode 100644 src/uni_modules/sl-table/examples/vue3-example.vue create mode 100644 src/uni_modules/sl-table/package.json create mode 100644 src/uni_modules/sl-table/readme.md create mode 100644 src/uni_modules/sl-table/test/slot-test.vue create mode 100644 src/uni_modules/sl-table/utils/vue-compat.js create mode 100644 src/uni_modules/uni-calendar/changelog.md create mode 100644 src/uni_modules/uni-calendar/components/uni-calendar/calendar.js create mode 100644 src/uni_modules/uni-calendar/components/uni-calendar/i18n/en.json create mode 100644 src/uni_modules/uni-calendar/components/uni-calendar/i18n/index.js create mode 100644 src/uni_modules/uni-calendar/components/uni-calendar/i18n/zh-Hans.json create mode 100644 src/uni_modules/uni-calendar/components/uni-calendar/i18n/zh-Hant.json create mode 100644 src/uni_modules/uni-calendar/components/uni-calendar/uni-calendar-item.vue create mode 100644 src/uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue create mode 100644 src/uni_modules/uni-calendar/components/uni-calendar/util.js create mode 100644 src/uni_modules/uni-calendar/package.json create mode 100644 src/uni_modules/uni-calendar/readme.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fcd1167 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,88 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a uni-app Vue 3 + TypeScript swimming timer/training application called "智能秒表" (Smart Stopwatch). It supports multiple platforms including H5, WeChat mini-program, and native apps. + +## Development Commands + +```bash +# Development +npm run dev:h5 # Run H5 development server +npm run dev:mp-weixin # Run WeChat mini-program development +npm run dev:app # Run app development + +# Build +npm run build:h5 # Build for H5 +npm run build:mp-weixin # Build for WeChat mini-program +npm run build:app # Build for app + +# Type checking +npm run type-check # Run TypeScript type checking +``` + +## Architecture + +### Directory Structure +``` +src/ +├── pages/ # Main pages (index/, user/) +├── pages/userFunc/ # Sub-package for user features (timing, segmentation, project) +├── components/ # Reusable Vue components +├── Service/ # API service layer +│ ├── Service.ts # Main service class (extends BaseConfig) +│ └── BaseConfig.ts # Base configuration +├── common/ +│ ├── Domain/ # TypeScript interfaces (Project, StudentRecord, TimingState) +│ └── Unit/ # Utility classes (HttpRequest, StoreAssist, UploadAssist) +├── static/ # Static assets +├── types/ # Additional TypeScript types +└── colorui/ # ColorUI CSS framework +``` + +### Service Layer Pattern +- `Service` class extends `BaseConfig` and provides: + - API requests: `Service.Request(url, method, data)` - handles 401 token expiration automatically + - Navigation: `Service.GoPage()`, `Service.GoPageTab()`, `Service.GoPageBack()`, `Service.GoPageDelse()` + - Storage: `Service.SetStorageCache()`, `Service.GetStorageCache()`, `Service.DelStorageCache()` + - Messages: `Service.Msg()`, `Service.Alert()` + - Loading: `Service.LoadIng()`, `Service.LoadClose()` + - File uploads: `Service.UpLoadMedia()`, `Service.uploadH5()` + +### Pages Configuration +- Pages registered in `src/pages.json` +- User features in `subPackages` under `pages/userFunc/` +- Tab bar with 2 tabs: "项目" (index) and "我的" (user) +- Navigation style is custom for some pages (e.g., swimim page) + +### Key Domain Types +- `Project`: Swimming project data (id, name, createdAt) +- `StudentRecord`: Student timing records +- `TimingState`: Timer state management +- `ResultData`: Standard API response wrapper + +## Code Style + +- Use tabs for indentation +- Single quotes for strings +- PascalCase for components and classes +- camelCase for variables and functions +- kebab-case for CSS classes +- Use `@/` path alias for imports from src/ directory +- Import Vue composition API from 'vue', uni-app hooks from `@dcloudio/uni-app` + +## UI Framework + +- **uview-plus** as primary UI component library (configured with easycom) +- **ColorUI** for CSS framework and icons +- Component prefix: `u-` (e.g., ``, ``) +- Use `rpx` units for responsive design + +## Important Notes + +- Automatic 401 token handling: API requests through `Service.Request()` redirect to login on token expiration +- Clean up intervals in `onUnmounted()` lifecycle hook +- No test framework currently configured +- Platform-specific APIs prefixed with `uni.` (uni-app framework) diff --git a/src/Service/Service.ts b/src/Service/Service.ts index 9f4d504..1a687d9 100644 --- a/src/Service/Service.ts +++ b/src/Service/Service.ts @@ -185,6 +185,21 @@ export class Service extends BaseConfig { }) } + static Confirm(msg : string, cb ?: any) { + uni.showModal({ + title: '确认', + content: msg, + showCancel: true, + cancelText: '取消', + confirmText: '确定', + success: res => { + if (res.confirm) { + cb && cb(); + } + } + }) + } + static LoadIng(text : any) : void { uni.showLoading({ title: text, diff --git a/src/pages.json b/src/pages.json index 803c398..a2cf92b 100644 --- a/src/pages.json +++ b/src/pages.json @@ -26,69 +26,95 @@ "subPackages": [{ "root": "pages/userFunc", "pages": [{ - "path" : "setCourse", - "style" : + "path": "setCourse", + "style": { + "navigationBarTitleText": "新增项目" + } + }, { - "navigationBarTitleText" : "新增项目" - } - }, - { - "path" : "swiming", - "style" : + "path": "swiming", + "style": { + "navigationBarTitleText": "游泳项目", + "navigationStyle": "custom" + } + }, { - "navigationBarTitleText" : "游泳项目", - "navigationStyle": "custom" - } - }, - { - "path" : "segmentation", - "style" : + "path": "segmentation", + "style": { + "navigationBarTitleText": "分段" + } + }, { - "navigationBarTitleText" : "分段" - } - }, - { - "path" : "student", - "style" : + "path": "student", + "style": { + "navigationBarTitleText": "学员管理" + } + }, { - "navigationBarTitleText" : "学员管理" - } - }, - { - "path" : "analyze", - "style" : + "path": "analyze", + "style": { + "navigationBarTitleText": "数据分析" + } + }, { - "navigationBarTitleText" : "数据分析" - } - }, - { - "path" : "dataAnalyze", - "style" : + "path": "dataAnalyze", + "style": { + "navigationBarTitleText": "数据分析图" + } + }, { - "navigationBarTitleText" : "数据分析图" - } - }, - { - "path" : "set", - "style" : + "path": "set", + "style": { + "navigationBarTitleText": "设置" + } + }, { - "navigationBarTitleText" : "设置" - } - }, - { - "path" : "projectList", - "style" : + "path": "projectList", + "style": { + "navigationBarTitleText": "项目列表" + } + }, { - "navigationBarTitleText" : "项目列表" + "path": "project", + "style": { + "navigationBarTitleText": "包干" + } } - }, - { - "path" : "project", - "style" : + ] + }, { + "root": "pages/dataAnalyze", + "pages": [ { - "navigationBarTitleText" : "包干" + "path": "baoganAnalyze", + "style": { + "navigationBarTitleText": "包干数据" + } + }, + { + "path": "timingAnalze", + "style": { + "navigationBarTitleText": "计时数据" + } + }, + { + "path": "Curve", + "style": { + "navigationBarTitleText": "曲线走势图" + } + }, + { + "path": "paragraphAnalyze", + "style": { + "navigationBarTitleText": "分段数据" + } + }, + { + "path": "grades", + "style": { + "navigationBarTitleText": "成绩排名" + } } - }] + ] }], "globalStyle": { "navigationBarTextStyle": "black", @@ -104,12 +130,12 @@ "pagePath": "pages/index/index", "iconPath": "static/tab/01.png", "selectedIconPath": "static/tab/02.png", - "text": "智能秒表" + "text": "项目" }, { "pagePath": "pages/index/user", - "iconPath": "static/tab/01.png", - "selectedIconPath": "static/tab/02.png", + "iconPath": "static/tab/03.png", + "selectedIconPath": "static/tab/04.png", "text": "我的" } ] diff --git a/src/pages/dataAnalyze/Curve.vue b/src/pages/dataAnalyze/Curve.vue new file mode 100644 index 0000000..7569e32 --- /dev/null +++ b/src/pages/dataAnalyze/Curve.vue @@ -0,0 +1,710 @@ + + + + + diff --git a/src/pages/dataAnalyze/baoganAnalyze.vue b/src/pages/dataAnalyze/baoganAnalyze.vue new file mode 100644 index 0000000..2cba9ff --- /dev/null +++ b/src/pages/dataAnalyze/baoganAnalyze.vue @@ -0,0 +1,360 @@ + + + + + diff --git a/src/pages/dataAnalyze/grades.vue b/src/pages/dataAnalyze/grades.vue new file mode 100644 index 0000000..953762d --- /dev/null +++ b/src/pages/dataAnalyze/grades.vue @@ -0,0 +1,535 @@ + + + + + \ No newline at end of file diff --git a/src/pages/dataAnalyze/paragraphAnalyze.vue b/src/pages/dataAnalyze/paragraphAnalyze.vue new file mode 100644 index 0000000..2ed65ea --- /dev/null +++ b/src/pages/dataAnalyze/paragraphAnalyze.vue @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/src/pages/dataAnalyze/timingAnalze.vue b/src/pages/dataAnalyze/timingAnalze.vue new file mode 100644 index 0000000..a131d46 --- /dev/null +++ b/src/pages/dataAnalyze/timingAnalze.vue @@ -0,0 +1,470 @@ + + + + + diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index d1cf3be..337737c 100644 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -27,11 +27,67 @@ 00:00''00 - - - + + + + + + 分段模式 + + 1个任务 + + + + + + + + + + + + + + + + 自由泳500米 + 00:00''00 + + + + + + + + + + 包干模式 + + 1个任务 + + + + + + + + + + + + + + + + 自由泳500米 + 00:00''00 + + + + + diff --git a/src/pages/index/user.vue b/src/pages/index/user.vue index 0caab92..0718fe2 100644 --- a/src/pages/index/user.vue +++ b/src/pages/index/user.vue @@ -6,12 +6,11 @@ - + @@ -23,19 +22,27 @@ - - 12 - 我的项目 + + + + 12 + 我的项目 + - - 8 - 记录数 + + + + 8 + 记录数 + - - 8 - 学员数 + + + 8 + 学员数 + @@ -44,45 +51,60 @@ - - + + - 项目管理 + + 项目管理 + + 常用 + + 管理训练项目和设置 - + - - + + - 学员管理 + + 学员管理 + + 核心 + + 管理学员信息和课程 - + - - - + + + - 数据分析 + + 数据分析 + + 智能 + + 查看训练数据和统计 - + @@ -102,7 +124,7 @@ }) - + @@ -138,7 +160,7 @@ align-items: center; gap: 20rpx; box-shadow: 0 12rpx 32rpx rgba(24, 144, 255, 0.35), - 0 4rpx 12rpx rgba(24, 144, 255, 0.2); + 0 4rpx 12rpx rgba(24, 144, 255, 0.2); position: relative; overflow: hidden; @@ -250,39 +272,78 @@ .stats-card { background-color: #fff; - border-radius: 24rpx; - padding: 30rpx 20rpx; + border-radius: 28rpx; + padding: 36rpx 24rpx; display: flex; align-items: center; - box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06); + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08); + position: relative; + overflow: hidden; } .stat-item { flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 16rpx; + cursor: pointer; + transition: all 0.3s ease; + + &:active { + transform: scale(0.98); + } + } + + .stat-icon-bg { + width: 68rpx; + height: 68rpx; + border-radius: 18rpx; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.12); + + &.project-stat { + background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); + } + + &.record-stat { + background: linear-gradient(135deg, #faad14 0%, #d48806 100%); + } + + &.student-stat { + background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%); + } + } + + .stat-info { display: flex; flex-direction: column; align-items: center; - gap: 8rpx; + justify-content: center; + gap: 4rpx; + } - .stat-value { - font-size: 40rpx; - font-weight: 700; - background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - } + .stat-value { + font-size: 42rpx; + font-weight: 700; + background: linear-gradient(135deg, #333 0%, #666 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + line-height: 1; + } - .stat-label { - font-size: 24rpx; - color: #999; - font-weight: 500; - } + .stat-label { + font-size: 24rpx; + color: #999; + font-weight: 500; } .stat-divider { width: 1rpx; - height: 60rpx; + height: 80rpx; background: linear-gradient(180deg, transparent 0%, #e8e8e8 50%, transparent 100%); } } @@ -294,108 +355,159 @@ .menu-card { background-color: #fff; - border-radius: 24rpx; - padding: 10rpx 0; - box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06); + border-radius: 28rpx; + padding: 8rpx 0; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08); overflow: hidden; } .menu-item { display: flex; align-items: center; - padding: 32rpx 30rpx; - transition: all 0.25s ease; + padding: 36rpx 32rpx; + transition: all 0.3s ease; position: relative; - &::after { + &::before { content: ''; position: absolute; - left: 30rpx; - right: 30rpx; - bottom: 0; + left: 0; + right: 0; + top: 0; height: 100%; - background: linear-gradient(90deg, rgba(24, 144, 255, 0.05) 0%, transparent 100%); + background: linear-gradient(90deg, rgba(24, 144, 255, 0.06) 0%, transparent 50%); opacity: 0; - transition: opacity 0.25s ease; + transition: opacity 0.3s ease; pointer-events: none; } &:active { - background-color: #fafafa; - - &::after { + &::before { opacity: 1; } } } - .menu-icon { - width: 88rpx; - height: 88rpx; - border-radius: 20rpx; + .menu-icon-bg { + width: 84rpx; + height: 84rpx; + border-radius: 22rpx; display: flex; align-items: center; justify-content: center; margin-right: 24rpx; - box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.1); - transition: transform 0.3s ease; + box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.12); + transition: all 0.3s ease; + position: relative; - &.project-icon { - background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); + &::after { + content: ''; + position: absolute; + top: 6rpx; + right: 6rpx; + width: 18rpx; + height: 18rpx; + background: rgba(255, 255, 255, 0.3); + border-radius: 50%; } - &.academy-icon { - background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%); + &.project-icon-bg { + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%); } - &.analysis-icon { - background: linear-gradient(135deg, #faad14 0%, #d48806 100%); + &.academy-icon-bg { + background: linear-gradient(135deg, #52c41a 0%, #73d13d 50%, #389e0d 100%); } - &.settings-icon { - background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); + &.analysis-icon-bg { + background: linear-gradient(135deg, #faad14 0%, #ffc53d 50%, #d48806 100%); } - &.about-icon { - background: linear-gradient(135deg, #722ed1 0%, #531dab 100%); + &.settings-icon-bg { + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%); + } + + &.about-icon-bg { + background: linear-gradient(135deg, #722ed1 0%, #9254de 50%, #531dab 100%); } } .menu-content { flex: 1; + } - .menu-title { - font-size: 32rpx; - font-weight: 600; - color: #333; - margin-bottom: 6rpx; - display: block; + .menu-header { + display: flex; + align-items: center; + gap: 12rpx; + margin-bottom: 8rpx; + } + + .menu-title { + font-size: 34rpx; + font-weight: 600; + color: #333; + } + + .menu-tag { + padding: 4rpx 12rpx; + border-radius: 16rpx; + font-size: 20rpx; + font-weight: 500; + + &.project-tag { + background: linear-gradient(135deg, rgba(24, 144, 255, 0.12) 0%, rgba(24, 144, 255, 0.08) 100%); + color: #1890ff; } - .menu-desc { - font-size: 24rpx; - color: #999; + &.academy-tag { + background: linear-gradient(135deg, rgba(82, 196, 26, 0.12) 0%, rgba(82, 196, 26, 0.08) 100%); + color: #52c41a; } + + &.analysis-tag { + background: linear-gradient(135deg, rgba(250, 173, 20, 0.12) 0%, rgba(250, 173, 20, 0.08) 100%); + color: #faad14; + } + + .tag-text { + font-size: 20rpx; + font-weight: 500; + } + } + + .menu-desc { + font-size: 24rpx; + color: #999; + line-height: 1.4; } .menu-arrow { - transition: transform 0.3s ease; + width: 48rpx; + height: 48rpx; + border-radius: 50%; + background: rgba(0, 0, 0, 0.04); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; } .menu-item:active { - .menu-icon { - transform: scale(0.95); + .menu-icon-bg { + transform: scale(0.92) translateY(2rpx); } .menu-arrow { - transform: translateX(6rpx); + background: rgba(24, 144, 255, 0.1); + transform: scale(1.1); } } .menu-divider { height: 1rpx; - background: linear-gradient(90deg, transparent 0%, #f0f0f0 20%, #f0f0f0 80%, transparent 100%); - margin: 0 30rpx; + background: linear-gradient(90deg, transparent 0%, #f0f0f0 15%, #f0f0f0 85%, transparent 100%); + margin: 0 32rpx; } } - + \ No newline at end of file diff --git a/src/pages/userFunc/analyze.vue b/src/pages/userFunc/analyze.vue index 2cd328b..d0b231c 100644 --- a/src/pages/userFunc/analyze.vue +++ b/src/pages/userFunc/analyze.vue @@ -8,71 +8,78 @@ - - - - - {{ projects.length }} - 项目总数 + + + + + + - - - {{ totalStudents }} - 学员总数 + + 包干数据 + 查看包干项目训练记录 - - - {{ totalRecords }} - 记录次数 + + + - - - - - 项目列表 - {{ projects.length }}个项目 - - - - - + + + + + + + 计时数据 + 查看计时项目训练记录 + + + - 暂无数据 - 创建项目后这里会显示数据分析 - - - - - {{ project.mode }} - - - - {{ project.createTime }} - - + + + + + + + 曲线走势图 + 查看学员成绩趋势变化 + - - {{ project.name }} - - - - {{ project.studentCount }}位学员 - - - - {{ project.recordCount }}次记录 - - - + + + + - - - + + + + + + + 分段数据 + 查看分段训练记录 + + + + + + + + + + + + + + 成绩排名 + 查看学员成绩排名情况 + + + @@ -82,70 +89,14 @@ @@ -197,106 +163,13 @@ } } - /* 统计数据区域 */ - .stats-section { - margin-bottom: 24rpx; - - .stats-card { - background-color: #fff; - border-radius: 24rpx; - padding: 30rpx 20rpx; - display: flex; - align-items: center; - box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06); - } - - .stat-item { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - gap: 8rpx; - - .stat-value { - font-size: 44rpx; - font-weight: 700; - background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - } - - .stat-label { - font-size: 24rpx; - color: #999; - font-weight: 500; - } - } - - .stat-divider { - width: 1rpx; - height: 60rpx; - background: linear-gradient(180deg, transparent 0%, #e8e8e8 50%, transparent 100%); - } - } - - /* 项目列表区域 */ - .projects-section { - .section-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20rpx; - - .section-title { - font-size: 30rpx; - font-weight: 600; - color: #333; - } - - .section-count { - font-size: 24rpx; - color: #999; - background-color: #f5f5f5; - padding: 6rpx 16rpx; - border-radius: 16rpx; - } - } - - .empty-state { - background-color: #fff; - border-radius: 24rpx; - padding: 100rpx 40rpx; - display: flex; - flex-direction: column; - align-items: center; - box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06); - - .empty-icon { - margin-bottom: 24rpx; - opacity: 0.6; - } - - .empty-text { - font-size: 30rpx; - font-weight: 600; - color: #666; - margin-bottom: 12rpx; - } - - .empty-desc { - font-size: 24rpx; - color: #999; - } - } - } - - .projects-list { - .project-card { + /* 功能入口卡片列表 */ + .function-list { + .function-card { background-color: #fff; border-radius: 20rpx; - padding: 28rpx 24rpx; - margin-bottom: 16rpx; + padding: 32rpx 28rpx; + margin-bottom: 20rpx; display: flex; align-items: center; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); @@ -311,7 +184,6 @@ top: 0; bottom: 0; width: 6rpx; - background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%); opacity: 0; transition: opacity 0.3s ease; } @@ -325,101 +197,90 @@ } } - .project-header { - position: absolute; - top: 20rpx; - right: 24rpx; + &.package-card::before { + background: linear-gradient(180deg, #faad14 0%, #ffc53d 100%); + } + + &.timing-card::before { + background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%); + } + + &.chart-card::before { + background: linear-gradient(180deg, #52c41a 0%, #73d13d 100%); + } + + &.segment-card::before { + background: linear-gradient(180deg, #722ed1 0%, #9254de 100%); + } + + &.ranking-card::before { + background: linear-gradient(180deg, #eb2f96 0%, #f759ab 100%); + } + + .card-icon { + width: 80rpx; + height: 80rpx; + border-radius: 16rpx; display: flex; align-items: center; - gap: 12rpx; + justify-content: center; + flex-shrink: 0; + margin-right: 24rpx; - .project-mode { - padding: 6rpx 14rpx; - border-radius: 12rpx; - font-size: 22rpx; - font-weight: 600; - - &.mode-timing { - background-color: #e6f7ff; - color: #1890ff; - } - - &.mode-package { - background-color: #fff7e6; - color: #faad14; - } - - &.mode-segment { - background-color: #f6ffed; - color: #52c41a; - } - - .mode-text { - font-size: 22rpx; - font-weight: 600; - } + &.package-icon { + background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%); + box-shadow: 0 4rpx 12rpx rgba(250, 173, 20, 0.3); } - .project-time { - display: flex; - align-items: center; - gap: 6rpx; + &.timing-icon { + background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); + box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3); + } - .time-text { - font-size: 22rpx; - color: #999; - } + &.chart-icon { + background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); + box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.3); + } + + &.segment-icon { + background: linear-gradient(135deg, #722ed1 0%, #9254de 100%); + box-shadow: 0 4rpx 12rpx rgba(114, 46, 209, 0.3); + } + + &.ranking-icon { + background: linear-gradient(135deg, #eb2f96 0%, #f759ab 100%); + box-shadow: 0 4rpx 12rpx rgba(235, 47, 150, 0.3); } } - .project-info { + .card-info { flex: 1; - padding-right: 60rpx; + margin-right: 20rpx; - .project-name { + .card-title { font-size: 32rpx; - font-weight: 600; + font-weight: 700; color: #333; display: block; - margin-bottom: 16rpx; + margin-bottom: 8rpx; } - .project-stats { - display: flex; - gap: 12rpx; - flex-wrap: wrap; - - .stat-badge { - display: flex; - align-items: center; - gap: 6rpx; - padding: 8rpx 14rpx; - background-color: #f5f5f5; - border-radius: 12rpx; - - &.record-badge { - background-color: #f6ffed; - } - - .badge-text { - font-size: 22rpx; - color: #666; - font-weight: 500; - } - } + .card-desc { + font-size: 24rpx; + color: #999; + display: block; } } - .project-arrow { - position: absolute; - right: 24rpx; - top: 50%; - transform: translateY(-50%); + + + .card-arrow { + flex-shrink: 0; transition: transform 0.3s ease; } - &:active .project-arrow { - transform: translateY(-50%) translateX(6rpx); + &:active .card-arrow { + transform: translateX(6rpx); } } } diff --git a/src/pages/userFunc/project.vue b/src/pages/userFunc/project.vue index 1358f76..c1d1639 100644 --- a/src/pages/userFunc/project.vue +++ b/src/pages/userFunc/project.vue @@ -20,14 +20,14 @@ - + - + - + {{ timer.studentName || '计时器 ' + (index + 1) }} @@ -35,18 +35,26 @@ {{ formatTime(timer.currentTime) }} {{ formatDuration(timer.currentTime) }} - + 休息 {{ formatRestTime(timer.displayRestTime) }} - - - - - + + + + + + + + + + + @@ -74,6 +82,57 @@ + + + + + + {{ selectedTimer?.studentName || '计时器' }} + + + + {{ formatStopwatchTime(selectedTimer?.currentTime || 0) }} + {{ formatMillis(selectedTimer?.currentTime || 0) }} + + + + + 休息时间 + {{ formatRestTime(selectedTimer?.displayRestTime || 0) }} + + + + + + + 记录 {{ selectedTimer.records.length - idx }} + {{ formatFullTime(record.time) }} + + + 休息 + {{ formatRestTime(record.restTime) }} + + + + + + + + + 重置 + + + + {{ selectedTimer?.status === 'running' ? '暂停' : '开始' }} + + + + 记录 + + + + @@ -81,14 +140,21 @@ import { ref, onUnmounted, computed } from 'vue' import { Service } from '@/Service/Service' + interface RecordItem { + time : number + restTime : number + } + interface TimerItem { - id: string - currentTime: number - status: 'idle' | 'running' | 'paused' | 'completed' - studentName: string - totalRestTime: number - pauseStartTime: number - displayRestTime: number + id : string + currentTime : number + status : 'idle' | 'running' | 'paused' | 'completed' + studentName : string + totalRestTime : number + pauseStartTime : number + displayRestTime : number + lastRecordRestTime : number + records ?: RecordItem[] } // 计划时长(秒) @@ -98,13 +164,18 @@ const studentNames = ['张三', '李四', '王五', '赵六', '钱七', '孙八'] // 计时器列表 + const timers = ref([ - { id: '1', currentTime: 0, status: 'idle', studentName: studentNames[0], totalRestTime: 0, pauseStartTime: 0, displayRestTime: 0 }, - { id: '2', currentTime: 0, status: 'idle', studentName: studentNames[1], totalRestTime: 0, pauseStartTime: 0, displayRestTime: 0 } + { id: '1', currentTime: 0, status: 'idle', studentName: studentNames[0], totalRestTime: 0, pauseStartTime: 0, displayRestTime: 0, lastRecordRestTime: 0, records: [] }, + { id: '2', currentTime: 0, status: 'idle', studentName: studentNames[1], totalRestTime: 0, pauseStartTime: 0, displayRestTime: 0, lastRecordRestTime: 0, records: [] } ]) + // 底部弹出框相关 + const showTimerDetail = ref(false) + const selectedTimer = ref(null) + // 计时器间隔 - 主计时器 - const timerIntervals = ref>(new Map()) + const timerIntervals = ref>(new Map()) // 休息时间计时器 const restTimerIntervals = ref>(new Map()) @@ -114,7 +185,7 @@ }) // 格式化计划时间显示 - const formatPlanTime = (seconds: number): string => { + const formatPlanTime = (seconds : number) : string => { const hours = Math.floor(seconds / 3600) const mins = Math.floor((seconds % 3600) / 60) if (hours > 0) { @@ -124,7 +195,7 @@ } // 格式化时间显示 - const formatTime = (seconds: number): string => { + const formatTime = (seconds : number) : string => { const hours = Math.floor(seconds / 3600) const mins = Math.floor((seconds % 3600) / 60) const secs = Math.floor(seconds % 60) @@ -132,7 +203,7 @@ } // 格式化持续时间 - const formatDuration = (seconds: number): string => { + const formatDuration = (seconds : number) : string => { const hours = Math.floor(seconds / 3600) const mins = Math.floor((seconds % 3600) / 60) let result = '' @@ -149,12 +220,33 @@ } // 格式化休息时间 - const formatRestTime = (seconds: number): string => { + const formatRestTime = (seconds : number) : string => { const mins = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` } + // 格式化秒表显示 (MM:SS) + const formatStopwatchTime = (seconds : number) : string => { + const mins = Math.floor(seconds / 60) + const secs = Math.floor(seconds % 60) + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` + } + + // 格式化毫秒显示 + const formatMillis = (seconds : number) : string => { + const millis = Math.floor((seconds % 1) * 100) + return `.${millis.toString().padStart(2, '0')}` + } + + // 格式化完整时间 (MM:SS.xx) + const formatFullTime = (seconds : number) : string => { + const mins = Math.floor(seconds / 60) + const secs = Math.floor(seconds % 60) + const millis = Math.floor((seconds % 1) * 100) + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${millis.toString().padStart(2, '0')}` + } + // 添加计时器 const addTimer = () => { const nextIndex = timers.value.length % studentNames.length @@ -165,13 +257,15 @@ studentName: studentNames[nextIndex], totalRestTime: 0, pauseStartTime: 0, - displayRestTime: 0 + displayRestTime: 0, + lastRecordRestTime: 0, + records: [] }) Service.Msg('添加成功', 'success') } // 删除计时器 - const deleteTimer = (timer: TimerItem, index: number) => { + const deleteTimer = (timer : TimerItem, index : number) => { uni.showModal({ title: '确认删除', content: `确定要删除 ${timer.studentName || '计时器 ' + (index + 1)} 吗?`, @@ -193,12 +287,15 @@ } // 开始主计时器 - const startTimer = (timer: TimerItem) => { + const startTimer = (timer : TimerItem) => { if (timer.status === 'running') return // 停止休息计时器,保存当前休息时间 if (timer.status === 'paused') { stopRestTimer(timer) + // 计算这次暂停的休息时间间隔,累加到lastRecordRestTime + const currentPauseRestTime = timer.displayRestTime - timer.totalRestTime + timer.lastRecordRestTime += currentPauseRestTime } timer.status = 'running' @@ -213,23 +310,23 @@ } // 停止单个主计时器 - const stopSingleTimer = (timer: TimerItem) => { + const stopSingleTimer = (timer : TimerItem) => { if (timer.status !== 'running') return timer.status = 'paused' - + const timerData = timerIntervals.value.get(timer.id) if (timerData) { clearInterval(timerData.interval) timerIntervals.value.delete(timer.id) } - // 开始休息计时器,从总休息时间继续 + // 开始休息计时器,从总休息时间继续,自动计算休息时间间隔 startRestTimer(timer) } // 开始休息计时器 - const startRestTimer = (timer: TimerItem) => { + const startRestTimer = (timer : TimerItem) => { timer.pauseStartTime = Date.now() const interval = setInterval(() => { @@ -241,7 +338,7 @@ } // 停止休息计时器 - const stopRestTimer = (timer: TimerItem) => { + const stopRestTimer = (timer : TimerItem) => { const interval = restTimerIntervals.value.get(timer.id) if (interval) { clearInterval(interval) @@ -281,7 +378,7 @@ } // 切换计时器状态 - const toggleTimer = (timer: TimerItem) => { + const toggleTimer = (timer : TimerItem) => { if (timer.status === 'completed') return if (timer.status === 'running') { stopSingleTimer(timer) @@ -291,24 +388,38 @@ } // 完成计时器 - const completeTimer = (timer: TimerItem) => { + const completeTimer = (timer : TimerItem) => { // 如果正在暂停中,保存休息时间 if (timer.status === 'paused') { stopRestTimer(timer) } - + // 先停止当前计时器 stopSingleTimer(timer) timer.status = 'completed' - + // 停止所有其他计时器 stopAllTimers() - + Service.Msg('已完成,所有计时器已暂停', 'success') } + // 记录计时 + const recordTimer = (timer : TimerItem) => { + if (timer.status !== 'running') { + Service.Msg('请在运行中记录') + return + } + if (!timer.records) { + timer.records = [] + } + timer.records.push({ time: timer.currentTime, restTime: timer.lastRecordRestTime }) + timer.lastRecordRestTime = 0 + Service.Msg('已记录') + } + // 重置计时器 - const resetTimer = (timer: TimerItem) => { + const resetTimer = (timer : TimerItem) => { stopSingleTimer(timer) stopRestTimer(timer) timer.currentTime = 0 @@ -316,14 +427,73 @@ timer.totalRestTime = 0 timer.displayRestTime = 0 timer.pauseStartTime = 0 + timer.lastRecordRestTime = 0 + timer.records = [] Service.Msg('已重置') } + // 打开计时器详情 + const openTimerDetail = (timer : TimerItem) => { + selectedTimer.value = timer + showTimerDetail.value = true + } + + // 记录圈数 + const recordLap = () => { + if (!selectedTimer.value) { + Service.Msg('未选择计时器') + return + } + + // 如果是暂停状态,需要先保存当前的休息时间 + if (selectedTimer.value.status === 'paused') { + stopRestTimer(selectedTimer.value) + const currentRestTime = selectedTimer.value.displayRestTime - selectedTimer.value.totalRestTime + selectedTimer.value.lastRecordRestTime += currentRestTime + selectedTimer.value.totalRestTime = selectedTimer.value.displayRestTime + } + + // 如果不是运行状态,提示需要先开始 + if (selectedTimer.value.status !== 'running') { + Service.Msg('请先开始计时器') + return + } + + // 创建记录数组 + if (!selectedTimer.value.records) { + selectedTimer.value.records = [] + } + + // 保存当前休息时间到记录 + selectedTimer.value.records.push({ + time: selectedTimer.value.currentTime, + restTime: selectedTimer.value.lastRecordRestTime + }) + + // 重置上次记录的休息时间 + selectedTimer.value.lastRecordRestTime = 0 + Service.Msg('已记录') + } + + // 切换选中的计时器 + const toggleSelectedTimer = () => { + if (selectedTimer.value) { + toggleTimer(selectedTimer.value) + } + } + + // 重置选中的计时器 + const resetSelectedTimer = () => { + if (selectedTimer.value) { + resetTimer(selectedTimer.value) + } + } + // 提交数据 const submitData = () => { // 先暂停所有计时器 pauseAllTimers() - + // 检查是否有数据 const hasData = timers.value.some(timer => timer.currentTime > 0 || timer.totalRestTime > 0) if (!hasData) { @@ -362,24 +532,38 @@ /* 卡片区域 */ .card-section { background-color: #fff; - border-radius: 20rpx; - padding: 30rpx; - margin-bottom: 20rpx; - box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); + border-radius: 24rpx; + padding: 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06); } .section-title { - font-size: 28rpx; + font-size: 30rpx; font-weight: 600; - color: #333; - margin-bottom: 24rpx; + color: #262626; + margin-bottom: 28rpx; + position: relative; + padding-left: 20rpx; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 6rpx; + height: 28rpx; + background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%); + border-radius: 3rpx; + } } .section-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 30rpx; + margin-bottom: 32rpx; .section-title { margin-bottom: 0; @@ -388,13 +572,20 @@ .add-btn { display: flex; align-items: center; - gap: 8rpx; + gap: 10rpx; background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); - padding: 12rpx 24rpx; - border-radius: 30rpx; + padding: 14rpx 28rpx; + border-radius: 32rpx; + box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3); + transition: all 0.3s ease; + + &:active { + transform: scale(0.95); + box-shadow: 0 2rpx 8rpx rgba(24, 144, 255, 0.2); + } .add-text { - font-size: 24rpx; + font-size: 26rpx; color: #fff; font-weight: 500; } @@ -404,14 +595,45 @@ /* 计划时长显示 */ .plan-duration-display { text-align: center; - padding: 30rpx; - background-color: #f5f5f5; - border-radius: 16rpx; + padding: 40rpx 30rpx; + background: linear-gradient(135deg, #f0f5ff 0%, #e6f7ff 100%); + border-radius: 20rpx; + border: 2rpx solid #bae7ff; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: -50%; + right: -50%; + width: 100%; + height: 200%; + background: linear-gradient( + 45deg, + transparent 40%, + rgba(255, 255, 255, 0.3) 50%, + transparent 60% + ); + animation: shimmer 3s infinite; + } + + @keyframes shimmer { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } + } .duration-value { - font-size: 36rpx; - font-weight: 600; - color: #333; + font-size: 42rpx; + font-weight: 700; + color: #1890ff; + position: relative; + z-index: 1; + font-family: 'DIN Alternate', 'Helvetica Neue', monospace; } } @@ -607,4 +829,257 @@ background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); } } - + + /* 计时器卡片可点击 */ + .timer-circle { + cursor: pointer; + transition: all 0.2s ease; + + &:active { + transform: scale(0.98); + } + } + + /* 底部弹出框样式 - 简洁版 */ + .timer-detail-popup { + padding: 50rpx 40rpx; + padding-bottom: calc(50rpx + env(safe-area-inset-bottom)); + min-height: 450rpx; + max-height: 75vh; + display: flex; + flex-direction: column; + } + + /* 详情姓名 */ + .detail-name { + text-align: center; + font-size: 32rpx; + font-weight: 500; + color: #8c8c8c; + margin-bottom: 40rpx; + letter-spacing: 2rpx; + } + + /* 秒表显示 */ + .stopwatch-display { + display: flex; + align-items: baseline; + justify-content: center; + margin-bottom: 50rpx; + } + + .stopwatch-time { + font-size: 100rpx; + font-weight: 300; + color: #1a1a1a; + font-family: 'DIN Alternate', 'Helvetica Neue', monospace; + letter-spacing: 2rpx; + } + + .stopwatch-millis { + font-size: 48rpx; + font-weight: 300; + color: #bfbfbf; + font-family: 'DIN Alternate', 'Helvetica Neue', monospace; + margin-left: 4rpx; + } + + /* 休息时间显示 */ + .stopwatch-rest { + display: flex; + align-items: center; + justify-content: center; + gap: 16rpx; + margin-bottom: 40rpx; + padding: 16rpx 32rpx; + background-color: #fff7e6; + border-radius: 16rpx; + + .rest-label { + font-size: 26rpx; + color: #fa8c16; + font-weight: 500; + } + + .rest-time-value { + font-size: 32rpx; + color: #fa8c16; + font-weight: 600; + font-family: 'DIN Alternate', 'Helvetica Neue', monospace; + } + } + + /* 记录列表 */ + .records-list { + flex: 1; + max-height: 280rpx; + margin-bottom: 40rpx; + background-color: #fafafa; + border-radius: 16rpx; + padding: 0 20rpx; + } + + .record-item { + padding: 20rpx 24rpx; + margin-bottom: 12rpx; + background-color: #fff; + border-radius: 12rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); + transition: all 0.2s ease; + + &:active { + transform: scale(0.98); + box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.08); + } + } + + .record-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8rpx; + } + + .record-index { + font-size: 24rpx; + color: #8c8c8c; + font-weight: 400; + background: linear-gradient(135deg, #f0f0f0 0%, #fafafa 100%); + padding: 4rpx 14rpx; + border-radius: 8rpx; + } + + .record-time { + font-size: 32rpx; + font-weight: 600; + color: #262626; + font-family: 'DIN Alternate', 'Helvetica Neue', monospace; + } + + .record-rest { + display: flex; + align-items: center; + gap: 12rpx; + margin-top: 8rpx; + } + + .rest-tag { + font-size: 20rpx; + color: #fff; + font-weight: 500; + background: linear-gradient(135deg, #faad14 0%, #d48806 100%); + padding: 4rpx 14rpx; + border-radius: 8rpx; + } + + .rest-value { + font-size: 24rpx; + color: #fa8c16; + font-weight: 600; + font-family: 'DIN Alternate', 'Helvetica Neue', monospace; + } + + + + /* 详情按钮 - 符合项目整体风格的圆形按钮 */ + .detail-buttons { + display: flex; + justify-content: center; + align-items: center; + gap: 50rpx; + } + + .detail-btn { + width: 120rpx; + height: 120rpx; + border-radius: 60rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6rpx; + transition: all 0.3s ease; + cursor: pointer; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, transparent 50%); + pointer-events: none; + } + + &:active { + transform: scale(0.95); + } + + .btn-label { + font-size: 22rpx; + font-weight: 500; + } + } + + /* 重置按钮 - 项目蓝色渐变风格 */ + .detail-btn-reset { + background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); + box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.4); + + &:active { + box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3); + } + + .btn-label { + color: #fff; + } + } + + /* 主按钮(开始/暂停)- 项目橙色渐变风格(与计时器卡片一致) */ + .detail-btn-main { + width: 160rpx; + height: 160rpx; + border-radius: 80rpx; + background: linear-gradient(135deg, #faad14 0%, #d48806 100%); + box-shadow: 0 8rpx 24rpx rgba(250, 140, 22, 0.4); + gap: 8rpx; + + &:active { + box-shadow: 0 4rpx 12rpx rgba(250, 140, 22, 0.3); + } + + .btn-label { + color: #fff; + font-size: 26rpx; + font-weight: 600; + } + } + + /* 记录按钮 - 项目绿色渐变风格 */ + .detail-btn-record { + background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%); + box-shadow: 0 8rpx 24rpx rgba(82, 196, 26, 0.4); + + &:active { + box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.3); + } + + .btn-label { + color: #fff; + } + } + + /* 记录按钮禁用状态 */ + .detail-btn-record.disabled { + background: #d9d9d9; + box-shadow: none; + pointer-events: none; + + .btn-label { + color: #8c8c8c; + } + } + \ No newline at end of file diff --git a/src/pages/userFunc/projectList.vue b/src/pages/userFunc/projectList.vue index 7a1a5bb..f3a389a 100644 --- a/src/pages/userFunc/projectList.vue +++ b/src/pages/userFunc/projectList.vue @@ -2,7 +2,7 @@ - + 项目列表 {{ projects.length }}个项目 @@ -19,60 +19,55 @@ - - - - {{ project.mode }} - - - - {{ project.createTime }} - + + + - {{ project.name }} + + {{ project.name }} + + {{ project.mode }} + + {{ project.studentCount }}位学员 - - - {{ project.recordCount }}条记录 - - - + + + + + + + {{ project.createTime }} + + - - - - - + + diff --git a/src/uni_modules/sl-table/components/sl-table/sl-table.vue b/src/uni_modules/sl-table/components/sl-table/sl-table.vue new file mode 100644 index 0000000..5721ef0 --- /dev/null +++ b/src/uni_modules/sl-table/components/sl-table/sl-table.vue @@ -0,0 +1,1822 @@ + + + + + diff --git a/src/uni_modules/sl-table/examples/miniprogram-example.vue b/src/uni_modules/sl-table/examples/miniprogram-example.vue new file mode 100644 index 0000000..7fd165d --- /dev/null +++ b/src/uni_modules/sl-table/examples/miniprogram-example.vue @@ -0,0 +1,182 @@ + + + + + + \ No newline at end of file diff --git a/src/uni_modules/sl-table/examples/vue2-example.vue b/src/uni_modules/sl-table/examples/vue2-example.vue new file mode 100644 index 0000000..628ad9d --- /dev/null +++ b/src/uni_modules/sl-table/examples/vue2-example.vue @@ -0,0 +1,303 @@ + + + + + + \ No newline at end of file diff --git a/src/uni_modules/sl-table/examples/vue3-example.vue b/src/uni_modules/sl-table/examples/vue3-example.vue new file mode 100644 index 0000000..bca0ccf --- /dev/null +++ b/src/uni_modules/sl-table/examples/vue3-example.vue @@ -0,0 +1,297 @@ + + + + + + \ No newline at end of file diff --git a/src/uni_modules/sl-table/package.json b/src/uni_modules/sl-table/package.json new file mode 100644 index 0000000..f9e69fd --- /dev/null +++ b/src/uni_modules/sl-table/package.json @@ -0,0 +1,86 @@ +{ + "id": "sl-table", + "displayName": "可合并单元格表格", + "version": "1.5.7", + "description": "自定义合并单元格和多级表头,支持多个自定义slot插槽,表头支持配置宽度百分比,支持vue3", + "keywords": [ + "合并单元格", + "表格", + "多级表头" +], + "repository": "", + "engines": { + "HBuilderX": "^4.36" + }, + "dcloudext": { + "type": "component-vue", + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "610947208" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y", + "alipay": "y" + }, + "client": { + "Vue": { + "vue2": "y", + "vue3": "y" + }, + "App": { + "app-vue": "y", + "app-nvue": "u", + "app-uvue": "u", + "app-harmony": "u" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "u", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "u", + "百度": "u", + "字节跳动": "u", + "QQ": "u", + "钉钉": "u", + "快手": "u", + "飞书": "u", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + } + } + } + } +} \ No newline at end of file diff --git a/src/uni_modules/sl-table/readme.md b/src/uni_modules/sl-table/readme.md new file mode 100644 index 0000000..a774fbb --- /dev/null +++ b/src/uni_modules/sl-table/readme.md @@ -0,0 +1,441 @@ +# sl-table + +一个支持Vue2和Vue3的高性能表格组件 + +## 特性 + +- ✅ **Vue2/Vue3兼容**: 同时支持Vue2和Vue3环境 +- ✅ **表头合并**: 支持多级表头和单元格合并 +- ✅ **单元格合并**: 支持行合并和列合并 +- ✅ **自定义插槽**: 支持单元格自定义内容 +- ✅ **响应式**: 自适应不同屏幕尺寸 +- ✅ **虚拟滚动**: 支持大量数据的分页加载 +- ✅ **样式自定义**: 支持自定义单元格样式 +- ✅ **横向滚动**: 支持表格横向滚动,自动处理宽度计算 +- ✅ **固定列**: 支持左侧和右侧固定列,滚动时显示阴影提示 +- ✅ **动态合并单元格**: 横竖向列合并单元格 +- ✅ **单选/多选**: 支持单选和多选表格行 + +## 兼容性 + +| Vue版本 | 支持状态 | 说明 | +|---------|----------|------| +| Vue 2.6+ | ✅ 完全支持 | 使用Options API | +| Vue 3.0+ | ✅ 完全支持 | 使用Options API,兼容Composition API | + +| 平台 | 支持状态 | 说明 | +|------|----------|------| +| H5 | ✅ 完全支持 | 所有功能正常 | +| 微信小程序 | ✅ 完全支持 | 插槽需要提前注册 | +| App | ✅ 完全支持 | 所有功能正常 | + +## 安装 + +```bash +# 将sl-table文件夹复制到项目的uni_modules目录下,插件中有examples文件夹,可查看示例 +``` + +## 基础用法 + +### Vue3风格(推荐) +```vue + +``` + +### Vue2风格 +```vue + + + +``` + +### 固定列 + +支持左侧和右侧固定列,在横向滚动时固定列会保持可见,并显示阴影提示: + +```javascript +columns: [ + { + label: '姓名', + prop: 'name', + width: '100px', + fixed: 'left' // 固定在左侧 + }, + { + label: '年龄', + prop: 'age', + width: '80px' + // 不固定,可横向滚动 + }, + { + label: '操作', + prop: 'action', + width: '100px', + fixed: 'right' // 固定在右侧 + } +] +``` + +**固定列特性**: +- 支持 `fixed: 'left'` 左侧固定 +- 支持 `fixed: 'right'` 右侧固定 +- 横向滚动时,固定列会显示阴影效果,提示用户有固定列存在 +- 左侧固定列在滚动后显示右侧阴影 +- 右侧固定列在未滚动到最右时显示左侧阴影 + +### 横向滚动 + +表格支持横向滚动,当列宽度总和超过容器宽度时自动启用: + +```javascript +columns: [ + { + label: '列1', + prop: 'col1', + width: '200px' // 固定宽度 + }, + { + label: '列2', + prop: 'col2', + width: '30%' // 百分比宽度,会自动转换为px + }, + { + label: '列3', + prop: 'col3', + width: '1fr' // 自适应宽度 + } +] +``` + +**宽度说明**: +- 支持 `px`、`rpx`、`%` 等单位 +- 百分比宽度会根据容器实际宽度自动转换为 `px` +- 未设置宽度时默认使用 `1fr` 自适应 + +### 微信小程序插槽注册 +在微信小程序中使用自定义插槽时,需要在组件中提前注册插槽: + +```vue + +``` + + +``` + +## 高级用法 + +### 单元格合并 + +```javascript +tableData: [ + { + name: '张三', + info: { + value: '合并单元格', + rowspan: 2, // 跨2行 + colspan: 1, // 跨1列 + cellStyle:{ + backgroundColor: '#f0f0f0', + color: '#333', + fontWeight: 'bold', + } + } + }, + { + name: '李四', + info: { + display: false // 被合并的单元格设为不显示 + } + } +] +``` + +### 自定义样式 + +```javascript +tableData: [ + { + name: { + value: '重要数据', + cellStyle:{ + backgroundColor: '#f0f0f0', + color: '#333', + fontWeight: 'bold', + } + } + } +] +``` + +## API + +### Props + +| 参数 | 说明 | 类型 | 默认值 | +|------|------|------|--------| +| columns | 表头配置 | Array | [] | +| tableData | 表格数据 | Array | [] | +| enableLoadMore | 是否开启上拉加载 | Boolean | false | +| useDynamicMergeCellsCol | 是否使用动态横向合并单元格 | Boolean | false | +| useDynamicMergeCellsRow | 是否使用动态竖向合并单元格 | Boolean | false | +| selection | 选择模式:'single' 单选,'multiple' 多选,false 不启用选择 | String\|Boolean | false | +| selectedRows | 已选中的行数据(受控模式),支持传入行数据对象数组、索引数组或键值数组 | Array | [] | +| rowKey | 行的唯一标识字段,如果不提供则使用行索引 | String | null | +| showRowIndex | 是否在表格左侧显示行号列(从1开始) | Boolean | false | +| rowIndexConfig | 行号列配置,如 { width: '60px', label: '序号', fixed: 'left' } | Object | { width: '60px', label: '序号', fixed: 'left' } | + +### Events + +| 事件名 | 说明 | 回调参数 | +|--------|------|----------| +| cell-click | 单元格点击事件 | { rowIndex, colIndex, value, cell } | +| load-more | 上拉加载事件 | {pageNum,done} | +| selection-change | 选择变化事件 | { selectedRowKeys: Array, selectedRows: Array } | +| sort-change | 选择变化事件 | { prop: String, order: String } | + +### Columns配置 + +| 参数 | 说明 | 类型 | 默认值 | +|------|------|------|--------| +| label | 表头显示文本 | String | - | +| prop | 对应数据字段 | String | - | +| width | 列宽度(支持px、rpx、%、1fr) | String | '1fr' | +| fixed | 固定列('left' 或 'right') | String | - | +| slot | 自定义插槽名 | String | - | +| children | 子列配置 | Array | - | +| headerStyle | 表头样式 | Object | {} | +| sort | 是否排序 | Boolean | false | + +### 单元格数据配置 + +> **注意**:以下带删除线的配置项已废弃,请使用 `cellStyle` 对象来设置单元格样式。 + +| 参数 | 说明 | 类型 | 默认值 | +|------|------|------|--------| +| value | 显示值 | Any | - | +| rowspan | 行合并数 | Number | 1 | +| colspan | 列合并数 | Number | 1 | +| ~~display~~ | ~~是否显示~~ | ~~Boolean~~ | ~~true~~ | +| ~~backgroundColor~~ | ~~背景色~~ | ~~String~~ | ~~'#fff'~~ | +| ~~textColor~~ | ~~文字颜色~~ | ~~String~~ | ~~'#1A1A1A'~~ | +| ~~bold~~ | ~~是否加粗~~ | ~~Boolean~~ | ~~false~~ | +| cellStyle | 单元格样式 | Object | {} | + +## 兼容性处理 + +组件内部会自动检测Vue版本并进行相应的兼容性处理: + +- **生命周期钩子**: 同时支持Vue2的`destroyed`和Vue3的`unmounted` +- **事件声明**: Vue3环境下自动添加`emits`声明 +- **响应式数据**: 兼容两个版本的响应式系统 + +## 注意事项 + +1. 在uni-app项目中使用时,确保已正确配置easycom +2. **插槽语法**: + - 请使用 `